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 }