lib/reticulum/src/interface/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty = @import("pretty");
3 const reticulum = @import("../root.zig");
4
5 const ifac = reticulum.interface.ifac;
6 const corpus = reticulum.conformance.ifac;
7
8 fn failBytes(
9 vector_name: []const u8,
10 field_name: []const u8,
11 expected: []const u8,
12 actual: []const u8,
13 ) !void {
14 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
15 defer arena.deinit();
16 var report = try pretty.diagnostic.Report.init(
17 arena.allocator(),
18 "Reticulum IFAC conformance mismatch",
19 );
20 defer report.deinit();
21 try report.field("vector", "{s}", .{vector_name});
22 try report.field("field", "{s}", .{field_name});
23 try report.field("expected", "{any}", .{expected});
24 try report.field("actual", "{any}", .{actual});
25 pretty.diagnostic.writeStderr(&report, .{ .width = 100 });
26 return error.ConformanceMismatch;
27 }
28
29 fn failVerdict(
30 vector_name: []const u8,
31 expected: []const u8,
32 actual: []const u8,
33 ) !void {
34 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
35 defer arena.deinit();
36 var report = try pretty.diagnostic.Report.init(
37 arena.allocator(),
38 "Reticulum IFAC verdict mismatch",
39 );
40 defer report.deinit();
41 try report.field("vector", "{s}", .{vector_name});
42 try report.field("expected", "{s}", .{expected});
43 try report.field("actual", "{s}", .{actual});
44 pretty.diagnostic.writeStderr(&report, .{ .width = 100 });
45 return error.ConformanceMismatch;
46 }
47
48 fn expectBytes(
49 vector_name: []const u8,
50 field_name: []const u8,
51 expected: []const u8,
52 actual: []const u8,
53 ) !void {
54 if (!std.mem.eql(u8, expected, actual)) {
55 return failBytes(vector_name, field_name, expected, actual);
56 }
57 }
58
59 fn expectedError(verdict: []const u8) ?[]const u8 {
60 if (std.mem.eql(u8, verdict, "invalid-code")) return "InvalidCode";
61 if (std.mem.eql(u8, verdict, "missing-flag")) return "MissingFlag";
62 if (std.mem.eql(u8, verdict, "truncated")) return "Truncated";
63 return null;
64 }
65
66 fn optional(bytes: []const u8) ?[]const u8 {
67 if (bytes.len == 0) return null;
68 return bytes;
69 }
70
71 fn expectStrip(
72 vector: corpus.Vector,
73 key: *const ifac.Key,
74 size: ifac.Size,
75 ) !void {
76 var raw_storage: [reticulum.wire.mtu]u8 = undefined;
77 const raw = ifac.strip(key, size, vector.masked, &raw_storage) catch |err| {
78 const expected = expectedError(vector.verdict) orelse
79 return failVerdict(vector.name, vector.verdict, @errorName(err));
80 if (!std.mem.eql(u8, expected, @errorName(err))) {
81 return failVerdict(vector.name, expected, @errorName(err));
82 }
83 return;
84 };
85 if (expectedError(vector.verdict)) |expected| {
86 return failVerdict(vector.name, expected, "accept");
87 }
88 try expectBytes(vector.name, "raw", vector.raw, raw);
89 }
90
91 fn expectCase(vector: corpus.Vector) !void {
92 var key = try ifac.derive(optional(vector.netname), optional(vector.netkey));
93 defer std.crypto.secureZero(u8, &key);
94 try expectBytes(vector.name, "ifac_key", vector.ifac_key, &key);
95 const size = ifac.Size.fromByte(vector.size) orelse
96 return failVerdict(vector.name, "size 1 through 64", "invalid size");
97 if (std.mem.eql(u8, vector.verdict, "accept")) {
98 var masked_storage: [ifac.max_frame_bytes]u8 = undefined;
99 const masked = try ifac.apply(&key, size, vector.raw, &masked_storage);
100 try expectBytes(vector.name, "masked", vector.masked, masked);
101 }
102 try expectStrip(vector, &key, size);
103 }
104
105 fn expectNamed(index: usize, name: []const u8) !void {
106 const vector = corpus.vectors[index];
107 if (!std.mem.eql(u8, name, vector.name)) {
108 return failVerdict(name, name, vector.name);
109 }
110 try expectCase(vector);
111 }
112
113 test "Reticulum@1.5.0 RNS/Transport.py:1244-1687 IFAC differential corpus" {
114 try std.testing.expectEqual(@as(usize, 10), corpus.vectors.len);
115 for (corpus.vectors) |vector| try expectCase(vector);
116 }
117
118 test "Reticulum@1.5.0 RNS/Transport.py:1244 IFAC vector name-only-size-16" {
119 try expectNamed(0, "name-only-size-16");
120 }
121
122 test "Reticulum@1.5.0 RNS/Transport.py:1244 IFAC vector key-only-size-16" {
123 try expectNamed(1, "key-only-size-16");
124 }
125
126 test "Reticulum@1.5.0 RNS/Transport.py:1244 IFAC vector both-size-16" {
127 try expectNamed(2, "both-size-16");
128 }
129
130 test "Reticulum@1.5.0 RNS/Transport.py:1244 IFAC vector both-size-1" {
131 try expectNamed(3, "both-size-1");
132 }
133
134 test "Reticulum@1.5.0 RNS/Transport.py:1244 IFAC vector both-size-64" {
135 try expectNamed(4, "both-size-64");
136 }
137
138 test "Reticulum@1.5.0 RNS/Transport.py:1244 IFAC vector raw-20-size-16" {
139 try expectNamed(5, "raw-20-size-16");
140 }
141
142 test "Reticulum@1.5.0 RNS/Transport.py:1244 IFAC vector raw-500-size-16" {
143 try expectNamed(6, "raw-500-size-16");
144 }
145
146 test "Reticulum@1.5.0 RNS/Transport.py:1636 IFAC vector tamper-payload" {
147 try expectNamed(7, "tamper-payload");
148 }
149
150 test "Reticulum@1.5.0 RNS/Transport.py:1636 IFAC vector missing-flag" {
151 try expectNamed(8, "missing-flag");
152 }
153
154 test "Reticulum@1.5.0 RNS/Transport.py:1636 IFAC vector truncated" {
155 try expectNamed(9, "truncated");
156 }
157
158 test "Reticulum@1.5.0 RNS/Reticulum.py:152 IFAC size domain rejects bounds" {
159 try std.testing.expect(ifac.Size.fromByte(0) == null);
160 try std.testing.expect(ifac.Size.fromByte(1) != null);
161 try std.testing.expect(ifac.Size.fromByte(64) != null);
162 try std.testing.expect(ifac.Size.fromByte(65) == null);
163 try std.testing.expectEqual(@as(u7, 16), ifac.default_size.byte());
164 }
165
166 test "Reticulum@1.5.0 RNS/Reticulum.py:989 IFAC needs network material" {
167 try std.testing.expectError(error.NoNetwork, ifac.derive(null, null));
168 }
169
170 test "Reticulum@1.5.0 RNS/Transport.py:1250 IFAC admits MTU and rejects max plus one" {
171 var key = try ifac.derive("maximum-network", null);
172 defer std.crypto.secureZero(u8, &key);
173 var raw: [reticulum.wire.mtu + 1]u8 = @splat(0x35);
174 raw[0] &= 0x7f;
175 var masked: [ifac.max_frame_bytes]u8 = undefined;
176 const maximum = try ifac.apply(&key, .bytes_64, raw[0..reticulum.wire.mtu], &masked);
177 try std.testing.expectEqual(@as(usize, ifac.max_frame_bytes), maximum.len);
178 try std.testing.expectError(
179 error.PacketTooLarge,
180 ifac.apply(&key, .bytes_64, &raw, &masked),
181 );
182 }
183
184 test "Reticulum@1.5.0 RNS/Transport.py:1693 IFAC flag detection" {
185 try std.testing.expect(ifac.hasFlag(0x80));
186 try std.testing.expect(ifac.hasFlag(0xff));
187 try std.testing.expect(!ifac.hasFlag(0x7f));
188 }