lib/reticulum/src/wire/header.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const context = @import("context.zig");
3 const flags = @import("flags.zig");
4
5 const assert = std.debug.assert;
6
7 pub const HeaderType = flags.HeaderType;
8 pub const TransportType = flags.TransportType;
9 pub const DestinationType = flags.DestinationType;
10 pub const PacketType = flags.PacketType;
11 pub const Context = context.Context;
12
13 /// Five hundred bytes, the largest Reticulum datagram (*packet*) every
14 /// Reticulum implementation accepts, for a caller sizing the buffer it reads a
15 /// frame into and checking its own payload against this before building a
16 /// packet, following Reticulum@1.5.0 RNS/Reticulum.py:93. Writing refuses a
17 /// packet longer than this and reading refuses bytes longer than this.
18 pub const mtu: u16 = 500;
19 /// Nineteen bytes, the header shape that carries no transport id (the 16 bytes
20 /// naming a node the packet passes through): the flags byte, the hop count, a
21 /// 16-byte destination hash, and the context byte, for a caller working out
22 /// where a payload starts behind the shorter of the two headers, following
23 /// Reticulum@1.5.0 RNS/Reticulum.py:150.
24 pub const header_one_bytes: u16 = 19;
25 /// Thirty-five bytes, the header shape that carries a 16-byte transport id
26 /// ahead of the destination hash, for a caller working out where a payload
27 /// starts behind the longer of the two headers, following Reticulum@1.5.0
28 /// RNS/Reticulum.py:151.
29 pub const header_two_bytes: u16 = 35;
30 /// Sixteen bytes, the width of a destination hash and of a transport id inside
31 /// a header, for a caller sizing those fields in a header, following
32 /// Reticulum@1.5.0 RNS/Reticulum.py:148.
33 pub const truncated_hash_bytes: u8 = 16;
34 /// One hundred twenty-eight, the hop count at and above which a packet is
35 /// refused, for a caller checking how far a packet has traveled before
36 /// forwarding the packet again, following Reticulum@1.5.0 RNS/Transport.py:115.
37 /// Writing and reading both return `error.InvalidHops` at this count.
38 pub const pathfinder_hops: u8 = 128;
39
40 pub const Packet = struct {
41 ifac: u1,
42 header: HeaderType,
43 context_flag: u1,
44 transport: TransportType,
45 destination_type: DestinationType,
46 packet_type: PacketType,
47 hops: u8,
48 transport_id: ?[truncated_hash_bytes]u8,
49 destination: [truncated_hash_bytes]u8,
50 context: Context,
51 payload: []const u8,
52 };
53
54 pub const DecodeError = error{
55 InvalidHops,
56 MissingTransportId,
57 PacketTooLarge,
58 PacketTooShort,
59 };
60
61 pub const EncodeError = error{
62 InvalidHops,
63 MissingTransportId,
64 OutputTooSmall,
65 PacketTooLarge,
66 UnexpectedTransportId,
67 };
68
69 pub fn headerLength(header: HeaderType) u16 {
70 return switch (header) {
71 .one => header_one_bytes,
72 .two => header_two_bytes,
73 };
74 }
75
76 pub fn encodedLength(packet: Packet) EncodeError!u16 {
77 if (packet.hops >= pathfinder_hops) return error.InvalidHops;
78 switch (packet.header) {
79 .one => if (packet.transport_id != null) return error.UnexpectedTransportId,
80 .two => if (packet.transport_id == null) return error.MissingTransportId,
81 }
82 const header_bytes: usize = headerLength(packet.header);
83 const total = std.math.add(usize, header_bytes, packet.payload.len) catch
84 return error.PacketTooLarge;
85 assert(total >= header_bytes);
86 if (total > mtu) return error.PacketTooLarge;
87 return @intCast(total);
88 }
89
90 fn wireDestinationType(packet: Packet) DestinationType {
91 if (packet.context == .lrproof) return .link;
92 return packet.destination_type;
93 }
94
95 /// Writes the header and the payload into `out` and returns the bytes written,
96 /// for a caller writing a built packet into the buffer handed to a network
97 /// interface, following Reticulum@1.5.0 RNS/Packet.py:178-239. The call returns
98 /// `error.InvalidHops` at 128 hops or above, `error.UnexpectedTransportId` for
99 /// a HEADER_1 packet that carries a transport id, `error.MissingTransportId`
100 /// for a HEADER_2 packet that lacks one, `error.PacketTooLarge` past 500 bytes,
101 /// and `error.OutputTooSmall` when `out` is shorter than the packet. A packet
102 /// whose context byte is `lrproof` goes out addressed as a link whatever
103 /// destination type it carries, because a link request proof answers a link.
104 pub fn encode(packet: Packet, out: []u8) EncodeError![]u8 {
105 const encoded_bytes = try encodedLength(packet);
106 const total: usize = encoded_bytes;
107 assert(total <= mtu);
108 if (out.len < total) return error.OutputTooSmall;
109 assert(out.len >= total);
110 out[0] = (flags.Flags{
111 .ifac = packet.ifac,
112 .header = packet.header,
113 .context = packet.context_flag,
114 .transport = packet.transport,
115 .destination = wireDestinationType(packet),
116 .packet = packet.packet_type,
117 }).encode();
118 out[1] = packet.hops;
119 const payload_start: usize = headerLength(packet.header);
120 assert(payload_start <= total);
121 switch (packet.header) {
122 .one => std.mem.copyForwards(u8, out[2..18], &packet.destination),
123 .two => {
124 std.mem.copyForwards(u8, out[2..18], &packet.transport_id.?);
125 std.mem.copyForwards(u8, out[18..34], &packet.destination);
126 },
127 }
128 out[payload_start - 1] = packet.context.encode();
129 std.mem.copyForwards(u8, out[payload_start..total], packet.payload);
130 return out[0..total];
131 }
132
133 /// Reads a packet out of `bytes`, for a caller splitting the bytes a network
134 /// interface delivered into fields it can read, following Reticulum@1.5.0
135 /// RNS/Packet.py:243-271. The call returns `error.PacketTooLarge` past 500
136 /// bytes, `error.PacketTooShort` under two bytes or under the header its flags
137 /// byte claims, `error.MissingTransportId` for a HEADER_2 packet of fewer than
138 /// 18 bytes, and `error.InvalidHops` at 128 hops or above. The payload field
139 /// points into the caller's own buffer, so it stays good as long as that buffer
140 /// does.
141 pub fn decode(bytes: []const u8) DecodeError!Packet {
142 if (bytes.len > mtu) return error.PacketTooLarge;
143 if (bytes.len < 2) return error.PacketTooShort;
144 const unpacked = flags.Flags.decode(bytes[0]);
145 if (bytes[1] >= pathfinder_hops) return error.InvalidHops;
146 const payload_start: usize = headerLength(unpacked.header);
147 if (unpacked.header == .two and bytes.len < 18) return error.MissingTransportId;
148 if (bytes.len < payload_start) return error.PacketTooShort;
149 assert(payload_start <= bytes.len);
150 const transport_id: ?[truncated_hash_bytes]u8 = switch (unpacked.header) {
151 .one => null,
152 .two => bytes[2..18].*,
153 };
154 const destination: [truncated_hash_bytes]u8 = switch (unpacked.header) {
155 .one => bytes[2..18].*,
156 .two => bytes[18..34].*,
157 };
158 return .{
159 .ifac = unpacked.ifac,
160 .header = unpacked.header,
161 .context_flag = unpacked.context,
162 .transport = unpacked.transport,
163 .destination_type = unpacked.destination,
164 .packet_type = unpacked.packet,
165 .hops = bytes[1],
166 .transport_id = transport_id,
167 .destination = destination,
168 .context = Context.decode(bytes[payload_start - 1]),
169 .payload = bytes[payload_start..],
170 };
171 }
172
173 test "Reticulum@1.5.0 RNS/Reticulum.py:150-151 fixes both header sizes" {
174 try std.testing.expectEqual(@as(u16, 19), headerLength(.one));
175 try std.testing.expectEqual(@as(u16, 35), headerLength(.two));
176 }
177
178 test "Reticulum@1.5.0 RNS/Packet.py:172,184-186 frames link request proofs" {
179 const link_id: [16]u8 = @splat(0xa5);
180 const packet = Packet{
181 .ifac = 0,
182 .header = .one,
183 .context_flag = 0,
184 .transport = .broadcast,
185 .destination_type = .single,
186 .packet_type = .proof,
187 .hops = 0,
188 .transport_id = null,
189 .destination = link_id,
190 .context = .lrproof,
191 .payload = &.{},
192 };
193 var raw: [header_one_bytes]u8 = undefined;
194 const encoded = try encode(packet, &raw);
195 try std.testing.expectEqual(DestinationType.link, flags.Flags.decode(encoded[0]).destination);
196 try std.testing.expectEqualSlices(u8, &link_id, encoded[2..18]);
197 const decoded = try decode(encoded);
198 try std.testing.expectEqual(Context.lrproof, decoded.context);
199 try std.testing.expectEqual(DestinationType.link, decoded.destination_type);
200 }
201
202 test "Reticulum@1.5.0 RNS/Packet.py:222-230 requires header two transport IDs" {
203 const packet = Packet{
204 .ifac = 0,
205 .header = .two,
206 .context_flag = 0,
207 .transport = .transport,
208 .destination_type = .plain,
209 .packet_type = .data,
210 .hops = 0,
211 .transport_id = null,
212 .destination = @splat(0),
213 .context = .none,
214 .payload = &.{},
215 };
216 var raw: [header_two_bytes]u8 = undefined;
217 try std.testing.expectError(error.MissingTransportId, encode(packet, &raw));
218 try std.testing.expectError(error.MissingTransportId, decode(&.{ 0x40, 0x00 }));
219 }