diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a57d616 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.zig-cache/ +zig-out/ +/release/ +/debug/ +/build/ +/build-*/ +/docgen_tmp/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..a35ecd3 --- /dev/null +++ b/build.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Create library + const reticulumzero = b.addLibrary(.{ + .name = "reticulum-zero", + .linkage = .static, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }), + }); + + // Output library object + b.installArtifact(reticulumzero); + + // Expose library as a module + try b.modules.put(b.dupe("reticulum-zero"), reticulumzero.root_module); + + // Set up unit testing + const unit_tests = b.addTest(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunStep(unit_tests); + + // `zig build test` command + const test_step = b.step("test", "Run library unit tests"); + test_step.dependency(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..d123225 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,13 @@ +.{ + .name = "reticulum-zero", + .version = "0.0.0", + .minimum_zig_version = "0.16.0", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "LICENSE", + "README.md", + }, +} diff --git a/src/packet.zig b/src/packet.zig new file mode 100644 index 0000000..191d835 --- /dev/null +++ b/src/packet.zig @@ -0,0 +1,324 @@ +///////////////// Imports +// + +const std = @import("std"); + +///////////////// Constants +// + +const expect = std.testing.expect; +const memeql = std.mem.eql; +const asBytes = std.mem.asBytes; + +const MAX_DATA_SIZE = 465; +const ADDRESS_SIZE = 16; + +///////////////// Enums +// + +const IfacFlag = enum(u1) { + open = 0, // Packet for publically accessible interface + authenticated = 1, // Interface authentication is included in packet +}; +const HeaderType = enum(u1) { + type1 = 0, // One address field + type2 = 1, // Two address fields +}; +const ContextFlag = enum(u1) { // Meaning depends on packet context + unset = 0, + set = 1, +}; +const PropagationType = enum(u1) { broadcast = 0, transport = 1 }; + +const DestinationType = enum(u2) { // + single = 0b0, + group = 0b01, + plain = 0b10, + link = 0b11, +}; + +const PacketType = enum(u2) { // + data = 0b0, + announce = 0b01, + link_request = 0b10, + proof = 0b11, +}; + +const PacketContext = enum(u8) { + none = 0x00, // Generic data packet + resource = 0x01, // Packet is part of a resource + resource_adv = 0x02, // Packet is a resource advertisement + resource_req = 0x03, // Packet is a resource part request + resource_hmu = 0x04, // Packet is a resource hashmap update + resource_prf = 0x05, // Packet is a resource proof + resource_icl = 0x06, // Packet is a resource initiator cancel message + resource_rcl = 0x07, // Packet is a resource receiver cancel message + cache_request = 0x08, // Packet is a cache request + request = 0x09, // Packet is a request + response = 0x0A, // Packet is a response to a request + path_response = 0x0B, // Packet is a response to a path request + command = 0x0C, // Packet is a command + command_status = 0x0D, // Packet is a status of an executed command + channel = 0x0E, // Packet contains link channel data + keepalive = 0xFA, // Packet is a keepalive packet + linkidentify = 0xFB, // Packet is a link peer identification proof + linkclose = 0xFC, // Packet is a link close message + linkproof = 0xFD, // Packet is a link packet proof + lrrtt = 0xFE, // Packet is a link request round-trip time measurement + lrproof = 0xFF, // Packet is a link request proof +}; + +///////////////// Structs +// + +const PacketHeader = packed struct { + ifac: IfacFlag, + header: HeaderType, + context: ContextFlag, + propagation: PropagationType, + destination: DestinationType, + packet: PacketType, + hops: u8, // +}; + +const Packet = struct { + header: PacketHeader, + address1: [ADDRESS_SIZE]u8, + address2: [ADDRESS_SIZE]u8, + context: PacketContext, + data: []const u8, // +}; + +///////////////// Functions +// + +pub fn serializePacket(packet: Packet, output_buffer: []u8) !usize { + const has_two_addresses = packet.header.header == HeaderType.type2; + + // Compute size of final packet (in bytes) + var target_size: u16 = @sizeOf(Packet); + if (!has_two_addresses) { + target_size -= packet.address2.len; + } + // TODO check data len + + if (output_buffer.len < target_size) { + return error.BufferTooShort; + } + + // Write buffer + var offset: usize = 0; + + @memcpy(output_buffer[offset .. offset + @sizeOf(PacketHeader)], asBytes(&packet.header)); + offset += @sizeOf(PacketHeader); + + @memcpy(output_buffer[offset .. offset + ADDRESS_SIZE], asBytes(&packet.address1)); + offset += ADDRESS_SIZE; + if (has_two_addresses) { + @memcpy(output_buffer[offset .. offset + ADDRESS_SIZE], asBytes(&packet.address2)); + offset += ADDRESS_SIZE; + } + + @memcpy(output_buffer[offset .. offset + @sizeOf(PacketContext)], asBytes(&packet.context)); + offset += @sizeOf(PacketContext); + + @memcpy(output_buffer[offset .. offset + packet.data.len], packet.data); + offset += packet.data.len; + + if (target_size != offset) { + return error.InternalError; + } + + return offset; +} + +fn copyToPacket(dst: anytype, src: []const u8, offset: usize, count: usize) !usize { + if (src.len > offset + count) { + return error.BufferTooShort; + } + @memcpy(@as(*u8, &dst), src[offset .. offset + count]); + + return offset + count; +} + +// Reads the message buffer and builds the corresponding packet struct inside dest_packet +pub fn deserializePacket(message_buffer: []u8, dest_packet: *Packet) !void { + var offset: usize = 0; + + offset = try copyToPacket(&dest_packet.header, message_buffer, offset, @sizeOf(PacketHeader)); + offset = try copyToPacket(&dest_packet.address1, message_buffer, offset, ADDRESS_SIZE); + const has_two_addresses = dest_packet.header.header == HeaderType.type2; + if (has_two_addresses) { + offset = try copyToPacket(&dest_packet.address2, message_buffer, offset, ADDRESS_SIZE); + } + offset = try copyToPacket(&dest_packet.context, message_buffer, offset, @sizeOf(PacketContext)); + // TODO compute data length + const data_len = 0; + offset = try copyToPacket(&dest_packet.data, message_buffer, offset, data_len); +} + +///////////////// Tests +// + +// Serializes the given packet and makes sure the output is correct +fn testPacketSerialization(packet: Packet) !void { + const buf_size = comptime @sizeOf(Packet) + MAX_DATA_SIZE; + var buf: [buf_size]u8 = undefined; + const res: usize = try serializePacket(packet, &buf); + var expected_res: usize = @sizeOf(PacketHeader) + packet.address1.len + @sizeOf(PacketContext) + packet.data.len; + const has_second_address = packet.header.header == HeaderType.type2; + if (has_second_address) { + expected_res += packet.address2.len; + } + + try expect(res == expected_res); + + var offset: usize = 0; + + try expect(memeql(u8, buf[offset .. offset + @sizeOf(PacketHeader)], asBytes(&packet.header))); + offset += @sizeOf(PacketHeader); + + try expect(memeql(u8, buf[offset .. offset + packet.address1.len], asBytes(&packet.address1))); + offset += packet.address1.len; + + try expect(memeql(u8, buf[offset .. offset + @sizeOf(PacketContext)], asBytes(&packet.context))); + offset += @sizeOf(PacketContext); + + try expect(memeql(u8, buf[offset .. offset + packet.data.len], packet.data)); + offset += packet.data.len; +} + +test "Structs size" { + try expect(@sizeOf(PacketHeader) == 2); +} + +test "Basic serialization: header type 1, max data size" { + const header: PacketHeader = .{ + .ifac = IfacFlag.open, + .header = HeaderType.type1, + .context = ContextFlag.set, + .propagation = PropagationType.transport, + .destination = DestinationType.single, + .packet = PacketType.announce, + .hops = 0, + }; + + const data_size = MAX_DATA_SIZE; + const data: [data_size]u8 = undefined; + + const packet: Packet = .{ // + .header = header, + .address1 = undefined, + .address2 = undefined, + .context = PacketContext.none, + .data = &data, + }; + + try testPacketSerialization(packet); +} + +test "Basic serialization: header type 2, max data size" { + const header: PacketHeader = .{ + .ifac = IfacFlag.open, + .header = HeaderType.type2, + .context = ContextFlag.set, + .propagation = PropagationType.transport, + .destination = DestinationType.single, + .packet = PacketType.announce, + .hops = 0, + }; + + const data_size = MAX_DATA_SIZE; + const data: [data_size]u8 = undefined; + + const packet: Packet = .{ // + .header = header, + .address1 = undefined, + .address2 = undefined, + .context = PacketContext.none, + .data = &data, + }; + + try testPacketSerialization(packet); +} + +test "Basic serialization: header type 1, medium data size" { + const header: PacketHeader = .{ + .ifac = IfacFlag.open, + .header = HeaderType.type1, + .context = ContextFlag.set, + .propagation = PropagationType.transport, + .destination = DestinationType.single, + .packet = PacketType.announce, + .hops = 0, + }; + + const data_size = MAX_DATA_SIZE / 2 + 3; + const data: [data_size]u8 = undefined; + + const packet: Packet = .{ // + .header = header, + .address1 = undefined, + .address2 = undefined, + .context = PacketContext.none, + .data = &data, + }; + + try testPacketSerialization(packet); +} + +test "Basic serialization: header type 2, medium data size" { + const header: PacketHeader = .{ + .ifac = IfacFlag.open, + .header = HeaderType.type2, + .context = ContextFlag.set, + .propagation = PropagationType.transport, + .destination = DestinationType.single, + .packet = PacketType.announce, + .hops = 0, + }; + + const data_size = MAX_DATA_SIZE / 2 + 3; + const data: [data_size]u8 = undefined; + + const packet: Packet = .{ // + .header = header, + .address1 = undefined, + .address2 = undefined, + .context = PacketContext.none, + .data = &data, + }; + + try testPacketSerialization(packet); +} + +test "Serialize / Deserialize Packet: Header type2, Medium data size" { + const header: PacketHeader = .{ + .ifac = IfacFlag.open, + .header = HeaderType.type2, + .context = ContextFlag.set, + .propagation = PropagationType.transport, + .destination = DestinationType.single, + .packet = PacketType.announce, + .hops = 0, + }; + + const data_size = MAX_DATA_SIZE / 2 + 3; + const data: [data_size]u8 = undefined; + + const packet: Packet = .{ // + .header = header, + .address1 = undefined, + .address2 = undefined, + .context = PacketContext.none, + .data = &data, + }; + + const buf_size = comptime @sizeOf(Packet) + MAX_DATA_SIZE; + var buf: [buf_size]u8 = undefined; + var res_packet: Packet = undefined; + _ = try serializePacket(packet, &buf); + try deserializePacket(&buf, &res_packet); + + try expect(res_packet == packet); +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..ad85c39 --- /dev/null +++ b/src/root.zig @@ -0,0 +1 @@ +const packet = @import("packet.zig");