lib/reticulum/src/properties/wire.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const wire = @import("reticulum").wire;
4
5 fn settings(seed: u64) hypothesis.Settings {
6 return hypothesis.Settings.quick()
7 .withSeed(seed)
8 .withDatabase("zig-out/hypothesis-failures/reticulum");
9 }
10
11 fn drawArray(data: *hypothesis.ConjectureData, comptime length: usize) ![length]u8 {
12 return (try data.drawBytes(length, length))[0..length].*;
13 }
14
15 fn drawPacket(data: *hypothesis.ConjectureData) !wire.Packet {
16 const header: wire.HeaderType = @fromBackingInt(@intCast(
17 @as(u1, @intCast(try data.drawInteger(0, 1, 0))),
18 ));
19 const context_byte: u8 = @intCast(try data.drawInteger(0, 255, 0));
20 const destination_type: wire.DestinationType = if (context_byte == 0xff)
21 .link
22 else
23 @fromBackingInt(@intCast(@as(u2, @intCast(try data.drawInteger(0, 3, 0)))));
24 const payload_max: u16 = switch (header) {
25 .one => wire.mtu - wire.header_one_bytes,
26 .two => wire.mtu - wire.header_two_bytes,
27 };
28 const payload_len: usize = @intCast(try data.drawInteger(0, payload_max, 0));
29 return .{
30 .ifac = @intCast(try data.drawInteger(0, 1, 0)),
31 .header = header,
32 .context_flag = @intCast(try data.drawInteger(0, 1, 0)),
33 .transport = @fromBackingInt(@intCast(@as(u1, @intCast(try data.drawInteger(0, 1, 0))))),
34 .destination_type = destination_type,
35 .packet_type = @fromBackingInt(@intCast(@as(u2, @intCast(try data.drawInteger(0, 3, 0))))),
36 .hops = @intCast(try data.drawInteger(0, 127, 0)),
37 .transport_id = if (header == .two) try drawArray(data, 16) else null,
38 .destination = try drawArray(data, 16),
39 .context = wire.Context.decode(context_byte),
40 .payload = try data.drawBytes(payload_len, payload_len),
41 };
42 }
43
44 fn expectPacket(expected: wire.Packet, actual: wire.Packet) !void {
45 try std.testing.expectEqual(expected.ifac, actual.ifac);
46 try std.testing.expectEqual(expected.header, actual.header);
47 try std.testing.expectEqual(expected.context_flag, actual.context_flag);
48 try std.testing.expectEqual(expected.transport, actual.transport);
49 try std.testing.expectEqual(expected.destination_type, actual.destination_type);
50 try std.testing.expectEqual(expected.packet_type, actual.packet_type);
51 try std.testing.expectEqual(expected.hops, actual.hops);
52 try std.testing.expectEqual(expected.transport_id, actual.transport_id);
53 try std.testing.expectEqual(expected.destination, actual.destination);
54 try std.testing.expectEqual(expected.context, actual.context);
55 try std.testing.expectEqualSlices(u8, expected.payload, actual.payload);
56 }
57
58 const PacketRoundTrip = struct {
59 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
60 const packet = try drawPacket(data);
61 var out: [wire.mtu]u8 = undefined;
62 const encoded = try wire.encode(packet, &out);
63 try std.testing.expectEqual(@as(usize, try wire.encodedLength(packet)), encoded.len);
64 try expectPacket(packet, try wire.decode(encoded));
65 }
66 };
67
68 test "property: Reticulum@1.5.0 RNS/Packet.py:178-271 packet round trip" {
69 try hypothesis.checkNamed(
70 PacketRoundTrip,
71 "reticulum-wire-packet-round-trip",
72 settings(0x5245_5449_4355_4c51),
73 );
74 }
75
76 const DecoderTotal = struct {
77 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
78 const bytes = try data.drawBytes(0, 600);
79 const packet: ?wire.Packet = wire.decode(bytes) catch null;
80 const full: ?[32]u8 = wire.hash.full(bytes) catch null;
81 const proof: ?wire.proof.Proof = wire.proof.decode(bytes) catch null;
82 _ = packet;
83 _ = full;
84 _ = proof;
85 }
86 };
87
88 test "property: arbitrary byte slices never panic Reticulum wire decoders" {
89 try hypothesis.checkNamed(
90 DecoderTotal,
91 "reticulum-wire-decoders-total",
92 settings(0x5245_5449_4355_4c52),
93 );
94 }
95
96 fn boundaryPacket(header: wire.HeaderType, payload: []const u8) wire.Packet {
97 return .{
98 .ifac = 0,
99 .header = header,
100 .context_flag = 0,
101 .transport = if (header == .two) .transport else .broadcast,
102 .destination_type = .plain,
103 .packet_type = .data,
104 .hops = 127,
105 .transport_id = if (header == .two) @as([16]u8, @splat(0xa5)) else null,
106 .destination = @splat(0x5a),
107 .context = .none,
108 .payload = payload,
109 };
110 }
111
112 const PacketBoundaries = struct {
113 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
114 const payload = try data.drawBytes(482, 482);
115 var out: [wire.mtu]u8 = undefined;
116 var overflow: [wire.mtu + 1]u8 = undefined;
117
118 var header_one = boundaryPacket(.one, payload[0..481]);
119 const one_max = try wire.encode(header_one, &out);
120 try std.testing.expectEqual(@as(usize, 500), one_max.len);
121 _ = try wire.decode(one_max);
122 header_one.payload = payload[0..482];
123 try std.testing.expectError(error.PacketTooLarge, wire.encode(header_one, &overflow));
124
125 var header_two = boundaryPacket(.two, payload[0..465]);
126 const two_max = try wire.encode(header_two, &out);
127 try std.testing.expectEqual(@as(usize, 500), two_max.len);
128 _ = try wire.decode(two_max);
129 std.mem.copyForwards(u8, overflow[0..two_max.len], two_max);
130 overflow[wire.mtu] = 0;
131 try std.testing.expectError(error.PacketTooLarge, wire.decode(&overflow));
132 header_two.payload = payload[0..466];
133 try std.testing.expectError(error.PacketTooLarge, wire.encode(header_two, &overflow));
134
135 var hops = boundaryPacket(.one, &.{});
136 try std.testing.expectEqual(@as(usize, 19), (try wire.encode(hops, &out)).len);
137 hops.hops = 128;
138 try std.testing.expectError(error.InvalidHops, wire.encode(hops, &out));
139 }
140 };
141
142 test "property: Reticulum@1.5.0 packet hop and MTU boundaries" {
143 try hypothesis.checkNamed(
144 PacketBoundaries,
145 "reticulum-wire-packet-boundaries",
146 settings(0x5245_5449_4355_4c53),
147 );
148 }