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 }