From 23f7ad8f94d6e0a260a9fd6e78b55e5c9e18bec8 Mon Sep 17 00:00:00 2001 From: Robby Zambito Date: Sun, 13 Apr 2025 16:50:52 -0400 Subject: Break out the impl to a lib This will make it easier to make a C library. --- build.zig | 18 ++++- src/Saprus.zig | 124 +++++++++++++++++++++++++++++ src/main.zig | 13 +-- src/message.zig | 211 +++++++++++++++++++++++++++++++++++++++++++++++++ src/root.zig | 2 + src/saprus.zig | 124 ----------------------------- src/saprus_message.zig | 211 ------------------------------------------------- 7 files changed, 361 insertions(+), 342 deletions(-) create mode 100644 src/Saprus.zig create mode 100644 src/message.zig create mode 100644 src/root.zig delete mode 100644 src/saprus.zig delete mode 100644 src/saprus_message.zig diff --git a/build.zig b/build.zig index b6d15a4..a45ac7c 100644 --- a/build.zig +++ b/build.zig @@ -15,6 +15,12 @@ pub fn build(b: *std.Build) void { // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); + const lib_mod = b.createModule(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + // We will also create a module for our other entry point, 'main.zig'. const exe_mod = b.createModule(.{ // `root_source_file` is the Zig "entry point" of the module. If a module @@ -26,9 +32,19 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - exe_mod.addImport("network", b.dependency("network", .{}).module("network")); + lib_mod.addImport("network", b.dependency("network", .{}).module("network")); + + exe_mod.addImport("zaprus", lib_mod); exe_mod.addImport("clap", b.dependency("clap", .{}).module("clap")); + const lib = b.addLibrary(.{ + .linkage = .static, + .name = "zaprus", + .root_module = lib_mod, + }); + + b.installArtifact(lib); + // This creates another `std.Build.Step.Compile`, but this one builds an executable // rather than a static library. const exe = b.addExecutable(.{ diff --git a/src/Saprus.zig b/src/Saprus.zig new file mode 100644 index 0000000..8f60326 --- /dev/null +++ b/src/Saprus.zig @@ -0,0 +1,124 @@ +var rand: ?Random = null; + +pub fn init() !void { + var prng = Random.DefaultPrng.init(blk: { + var seed: u64 = undefined; + try posix.getrandom(mem.asBytes(&seed)); + break :blk seed; + }); + rand = prng.random(); + try network.init(); +} + +pub fn deinit() void { + network.deinit(); +} + +fn broadcastSaprusMessage(msg: SaprusMessage, udp_port: u16, allocator: Allocator) !void { + const msg_bytes = try msg.toBytes(allocator); + defer allocator.free(msg_bytes); + + var sock = try network.Socket.create(.ipv4, .udp); + defer sock.close(); + + try sock.setBroadcast(true); + + // Bind to 0.0.0.0:0 + const bind_addr = network.EndPoint{ + .address = network.Address{ .ipv4 = network.Address.IPv4.any }, + .port = 0, + }; + + const dest_addr = network.EndPoint{ + .address = network.Address{ .ipv4 = network.Address.IPv4.broadcast }, + .port = udp_port, + }; + + try sock.bind(bind_addr); + + _ = try sock.sendTo(dest_addr, msg_bytes); +} + +pub fn sendRelay(payload: []const u8, allocator: Allocator) !void { + const msg = SaprusMessage{ + .relay = .{ + .header = .{ .dest = .{ 70, 70, 70, 70 } }, + .payload = payload, + }, + }; + + try broadcastSaprusMessage(msg, 8888, allocator); +} + +fn randomPort() u16 { + var p: u16 = 0; + if (rand) |r| { + p = r.intRangeAtMost(u16, 1024, 65000); + } else unreachable; + + return p; +} + +pub fn sendInitialConnection(payload: []const u8, initial_port: u16, allocator: Allocator) !SaprusMessage { + const dest_port = randomPort(); + const msg = SaprusMessage{ + .connection = .{ + .header = .{ + .src_port = initial_port, + .dest_port = dest_port, + }, + .payload = payload, + }, + }; + + try broadcastSaprusMessage(msg, 8888, allocator); + + return msg; +} + +pub fn connect(payload: []const u8, allocator: Allocator) !?SaprusMessage { + var initial_port: u16 = 0; + if (rand) |r| { + initial_port = r.intRangeAtMost(u16, 1024, 65000); + } else unreachable; + + var initial_conn_res: ?SaprusMessage = null; + errdefer if (initial_conn_res) |c| c.deinit(allocator); + + var sock = try network.Socket.create(.ipv4, .udp); + defer sock.close(); + + // Bind to 255.255.255.255:8888 + const bind_addr = network.EndPoint{ + .address = network.Address{ .ipv4 = network.Address.IPv4.broadcast }, + .port = 8888, + }; + + // timeout 1s + try sock.setReadTimeout(1 * std.time.us_per_s); + try sock.bind(bind_addr); + + const msg = try sendInitialConnection(payload, initial_port, allocator); + + var response_buf: [4096]u8 = undefined; + _ = try sock.receive(&response_buf); // Ignore message that I sent. + const len = try sock.receive(&response_buf); + + initial_conn_res = try SaprusMessage.fromBytes(response_buf[0..len], allocator); + + // Complete handshake after awaiting response + try broadcastSaprusMessage(msg, randomPort(), allocator); + + return initial_conn_res; +} + +const SaprusMessage = @import("message.zig").Message; + +const std = @import("std"); +const Random = std.Random; +const posix = std.posix; +const mem = std.mem; + +const network = @import("network"); + +const Allocator = mem.Allocator; diff --git a/src/main.zig b/src/main.zig index a0b5288..2b7dca2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -41,19 +41,19 @@ pub fn main() !void { }; defer res.deinit(); - try Saprus.init(); - defer Saprus.deinit(); + try SaprusClient.init(); + defer SaprusClient.deinit(); if (res.args.help != 0) { return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); } if (res.args.relay) |r| { - try Saprus.sendRelay(if (r.len > 0) r else "Hello darkness my old friend", gpa); + try SaprusClient.sendRelay(if (r.len > 0) r else "Hello darkness my old friend", gpa); // std.debug.print("Sent: {s}\n", .{r}); return; } else if (res.args.connect) |c| { - const conn_res: ?SaprusMessage = Saprus.connect(if (c.len > 0) c else "Hello darkness my old friend", gpa) catch |err| switch (err) { + const conn_res: ?SaprusMessage = SaprusClient.connect(if (c.len > 0) c else "Hello darkness my old friend", gpa) catch |err| switch (err) { error.WouldBlock => null, else => return err, }; @@ -74,7 +74,8 @@ const std = @import("std"); const DebugAllocator = std.heap.DebugAllocator(.{}); const ArrayList = std.ArrayList; -const Saprus = @import("./saprus.zig"); -const SaprusMessage = Saprus.SaprusMessage; +const zaprus = @import("zaprus"); +const SaprusClient = zaprus.Client; +const SaprusMessage = zaprus.Message; const clap = @import("clap"); diff --git a/src/message.zig b/src/message.zig new file mode 100644 index 0000000..403bfcd --- /dev/null +++ b/src/message.zig @@ -0,0 +1,211 @@ +const base64Enc = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, '='); +const base64Dec = std.base64.Base64Decoder.init(std.base64.standard_alphabet_chars, '='); + +/// Type tag for Message union. +/// This is the first value in the actual packet sent over the network. +pub const PacketType = enum(u16) { + relay = 0x003C, + file_transfer = 0x8888, + connection = 0x00E9, + _, +}; + +/// Reserved option values. +/// Currently unused. +pub const ConnectionOptions = packed struct(u8) { + opt1: bool = false, + opt2: bool = false, + opt3: bool = false, + opt4: bool = false, + opt5: bool = false, + opt6: bool = false, + opt7: bool = false, + opt8: bool = false, +}; + +pub const Error = error{ + NotImplementedSaprusType, + UnknownSaprusType, +}; + +/// All Saprus messages +pub const Message = union(PacketType) { + pub const Relay = struct { + pub const Header = packed struct { + dest: @Vector(4, u8), + }; + header: Header, + payload: []const u8, + }; + pub const Connection = struct { + pub const Header = packed struct { + src_port: u16, // random number > 1024 + dest_port: u16, // random number > 1024 + seq_num: u32 = 0, + msg_id: u32 = 0, + reserved: u8 = 0, + options: ConnectionOptions = .{}, + }; + header: Header, + payload: []const u8, + }; + relay: Relay, + file_transfer: void, // unimplemented + connection: Connection, + + /// Should be called for any Message that was declared using a function that you pass an allocator to. + pub fn deinit(self: Message, allocator: Allocator) void { + switch (self) { + .relay => |r| allocator.free(r.payload), + .connection => |c| allocator.free(c.payload), + else => unreachable, + } + } + + fn toBytesAux( + header: anytype, + payload: []const u8, + buf: *std.ArrayList(u8), + allocator: Allocator, + ) !void { + const Header = @TypeOf(header); + // Create a growable string to store the base64 bytes in. + // Doing this first so I can use the length of the encoded bytes for the length field. + var payload_list = std.ArrayList(u8).init(allocator); + defer payload_list.deinit(); + const buf_w = payload_list.writer(); + + // Write the payload bytes as base64 to the growable string. + try base64Enc.encodeWriter(buf_w, payload); + + // At this point, payload_list contains the base64 encoded payload. + + // Add the payload length to the output buf. + try buf.*.appendSlice( + asBytes(&nativeToBig(u16, @intCast(payload_list.items.len + @bitSizeOf(Header) / 8))), + ); + + // Add the header bytes to the output buf. + var header_buf: [@sizeOf(Header)]u8 = undefined; + var header_buf_stream = std.io.fixedBufferStream(&header_buf); + try header_buf_stream.writer().writeStructEndian(header, .big); + // Add the exact number of bits in the header without padding. + try buf.*.appendSlice(header_buf[0 .. @bitSizeOf(Header) / 8]); + + try buf.*.appendSlice(payload_list.items); + } + + /// Caller is responsible for freeing the returned bytes. + pub fn toBytes(self: Message, allocator: Allocator) ![]u8 { + // Create a growable list of bytes to store the output in. + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + + // Start with writing the message type, which is the first 16 bits of every Saprus message. + try buf.appendSlice(asBytes(&nativeToBig(u16, @intFromEnum(self)))); + + // Write the proper header and payload for the given packet type. + switch (self) { + .relay => |r| try toBytesAux(r.header, r.payload, &buf, allocator), + .connection => |c| try toBytesAux(c.header, c.payload, &buf, allocator), + .file_transfer => return Error.NotImplementedSaprusType, + } + + // Collect the growable list as a slice and return it. + return buf.toOwnedSlice(); + } + + fn fromBytesAux( + comptime packet: PacketType, + len: u16, + r: std.io.FixedBufferStream([]const u8).Reader, + allocator: Allocator, + ) !Message { + const Header = @field(@FieldType(Message, @tagName(packet)), "Header"); + + // Read the header for the current message type. + var header_bytes: [@sizeOf(Header)]u8 = undefined; + _ = try r.read(header_bytes[0 .. @bitSizeOf(Header) / 8]); + var header_stream = std.io.fixedBufferStream(&header_bytes); + const header = try header_stream.reader().readStructEndian(Header, .big); + + // Read the base64 bytes into a list to be able to call the decoder on it. + const payload_buf = try allocator.alloc(u8, len - @bitSizeOf(Header) / 8); + defer allocator.free(payload_buf); + _ = try r.readAll(payload_buf); + + // Create a buffer to store the payload in, and decode the base64 bytes into the payload field. + const payload = try allocator.alloc(u8, try base64Dec.calcSizeForSlice(payload_buf)); + try base64Dec.decode(payload, payload_buf); + + // Return the type of Message specified by the `packet` argument. + return @unionInit(Message, @tagName(packet), .{ + .header = header, + .payload = payload, + }); + } + + /// Caller is responsible for calling .deinit on the returned value. + pub fn fromBytes(bytes: []const u8, allocator: Allocator) !Message { + var s = std.io.fixedBufferStream(bytes); + const r = s.reader(); + + // Read packet type + const packet_type = @as(PacketType, @enumFromInt(try r.readInt(u16, .big))); + + // Read the length of the header + base64 encoded payload. + const len = try r.readInt(u16, .big); + + switch (packet_type) { + .relay => return fromBytesAux(.relay, len, r, allocator), + .connection => return fromBytesAux(.connection, len, r, allocator), + .file_transfer => return Error.NotImplementedSaprusType, + else => return Error.UnknownSaprusType, + } + } +}; + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const asBytes = std.mem.asBytes; +const nativeToBig = std.mem.nativeToBig; + +test "Round trip Relay toBytes and fromBytes" { + const gpa = std.testing.allocator; + const msg = Message{ + .relay = .{ + .header = .{ .dest = .{ 255, 255, 255, 255 } }, + .payload = "Hello darkness my old friend", + }, + }; + + const to_bytes = try msg.toBytes(gpa); + defer gpa.free(to_bytes); + + const from_bytes = try Message.fromBytes(to_bytes, gpa); + defer from_bytes.deinit(gpa); + + try std.testing.expectEqualDeep(msg, from_bytes); +} + +test "Round trip Connection toBytes and fromBytes" { + const gpa = std.testing.allocator; + const msg = Message{ + .connection = .{ + .header = .{ + .src_port = 0, + .dest_port = 0, + }, + .payload = "Hello darkness my old friend", + }, + }; + + const to_bytes = try msg.toBytes(gpa); + defer gpa.free(to_bytes); + + const from_bytes = try Message.fromBytes(to_bytes, gpa); + defer from_bytes.deinit(gpa); + + try std.testing.expectEqualDeep(msg, from_bytes); +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..739dd47 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,2 @@ +pub const Client = @import("Saprus.zig"); +pub usingnamespace @import("message.zig"); diff --git a/src/saprus.zig b/src/saprus.zig deleted file mode 100644 index cde6520..0000000 --- a/src/saprus.zig +++ /dev/null @@ -1,124 +0,0 @@ -var rand: ?Random = null; - -pub fn init() !void { - var prng = Random.DefaultPrng.init(blk: { - var seed: u64 = undefined; - try posix.getrandom(mem.asBytes(&seed)); - break :blk seed; - }); - rand = prng.random(); - try network.init(); -} - -pub fn deinit() void { - network.deinit(); -} - -fn broadcastSaprusMessage(msg: SaprusMessage, udp_port: u16, allocator: Allocator) !void { - const msg_bytes = try msg.toBytes(allocator); - defer allocator.free(msg_bytes); - - var sock = try network.Socket.create(.ipv4, .udp); - defer sock.close(); - - try sock.setBroadcast(true); - - // Bind to 0.0.0.0:0 - const bind_addr = network.EndPoint{ - .address = network.Address{ .ipv4 = network.Address.IPv4.any }, - .port = 0, - }; - - const dest_addr = network.EndPoint{ - .address = network.Address{ .ipv4 = network.Address.IPv4.broadcast }, - .port = udp_port, - }; - - try sock.bind(bind_addr); - - _ = try sock.sendTo(dest_addr, msg_bytes); -} - -pub fn sendRelay(payload: []const u8, allocator: Allocator) !void { - const msg = SaprusMessage{ - .relay = .{ - .header = .{ .dest = .{ 70, 70, 70, 70 } }, - .payload = payload, - }, - }; - - try broadcastSaprusMessage(msg, 8888, allocator); -} - -fn randomPort() u16 { - var p: u16 = 0; - if (rand) |r| { - p = r.intRangeAtMost(u16, 1024, 65000); - } else unreachable; - - return p; -} - -pub fn sendInitialConnection(payload: []const u8, initial_port: u16, allocator: Allocator) !SaprusMessage { - const dest_port = randomPort(); - const msg = SaprusMessage{ - .connection = .{ - .header = .{ - .src_port = initial_port, - .dest_port = dest_port, - }, - .payload = payload, - }, - }; - - try broadcastSaprusMessage(msg, 8888, allocator); - - return msg; -} - -pub fn connect(payload: []const u8, allocator: Allocator) !?SaprusMessage { - var initial_port: u16 = 0; - if (rand) |r| { - initial_port = r.intRangeAtMost(u16, 1024, 65000); - } else unreachable; - - var initial_conn_res: ?SaprusMessage = null; - errdefer if (initial_conn_res) |c| c.deinit(allocator); - - var sock = try network.Socket.create(.ipv4, .udp); - defer sock.close(); - - // Bind to 255.255.255.255:8888 - const bind_addr = network.EndPoint{ - .address = network.Address{ .ipv4 = network.Address.IPv4.broadcast }, - .port = 8888, - }; - - // timeout 1s - try sock.setReadTimeout(1 * std.time.us_per_s); - try sock.bind(bind_addr); - - const msg = try sendInitialConnection(payload, initial_port, allocator); - - var response_buf: [4096]u8 = undefined; - _ = try sock.receive(&response_buf); // Ignore message that I sent. - const len = try sock.receive(&response_buf); - - initial_conn_res = try SaprusMessage.fromBytes(response_buf[0..len], allocator); - - // Complete handshake after awaiting response - try broadcastSaprusMessage(msg, randomPort(), allocator); - - return initial_conn_res; -} - -pub const SaprusMessage = @import("./saprus_message.zig").SaprusMessage; - -const std = @import("std"); -const Random = std.Random; -const posix = std.posix; -const mem = std.mem; - -const network = @import("network"); - -const Allocator = mem.Allocator; diff --git a/src/saprus_message.zig b/src/saprus_message.zig deleted file mode 100644 index 3d47f66..0000000 --- a/src/saprus_message.zig +++ /dev/null @@ -1,211 +0,0 @@ -const base64Enc = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, '='); -const base64Dec = std.base64.Base64Decoder.init(std.base64.standard_alphabet_chars, '='); - -/// Type tag for SaprusMessage union. -/// This is the first value in the actual packet sent over the network. -pub const SaprusPacketType = enum(u16) { - relay = 0x003C, - file_transfer = 0x8888, - connection = 0x00E9, - _, -}; - -/// Reserved option values. -/// Currently unused. -pub const SaprusConnectionOptions = packed struct(u8) { - opt1: bool = false, - opt2: bool = false, - opt3: bool = false, - opt4: bool = false, - opt5: bool = false, - opt6: bool = false, - opt7: bool = false, - opt8: bool = false, -}; - -pub const SaprusError = error{ - NotImplementedSaprusType, - UnknownSaprusType, -}; - -/// All Saprus messages -pub const SaprusMessage = union(SaprusPacketType) { - pub const Relay = struct { - pub const Header = packed struct { - dest: @Vector(4, u8), - }; - header: Header, - payload: []const u8, - }; - pub const Connection = struct { - pub const Header = packed struct { - src_port: u16, // random number > 1024 - dest_port: u16, // random number > 1024 - seq_num: u32 = 0, - msg_id: u32 = 0, - reserved: u8 = 0, - options: SaprusConnectionOptions = .{}, - }; - header: Header, - payload: []const u8, - }; - relay: Relay, - file_transfer: void, // unimplemented - connection: Connection, - - /// Should be called for any SaprusMessage that was declared using a function that you pass an allocator to. - pub fn deinit(self: SaprusMessage, allocator: Allocator) void { - switch (self) { - .relay => |r| allocator.free(r.payload), - .connection => |c| allocator.free(c.payload), - else => unreachable, - } - } - - fn toBytesAux( - header: anytype, - payload: []const u8, - buf: *std.ArrayList(u8), - allocator: Allocator, - ) !void { - const Header = @TypeOf(header); - // Create a growable string to store the base64 bytes in. - // Doing this first so I can use the length of the encoded bytes for the length field. - var payload_list = std.ArrayList(u8).init(allocator); - defer payload_list.deinit(); - const buf_w = payload_list.writer(); - - // Write the payload bytes as base64 to the growable string. - try base64Enc.encodeWriter(buf_w, payload); - - // At this point, payload_list contains the base64 encoded payload. - - // Add the payload length to the output buf. - try buf.*.appendSlice( - asBytes(&nativeToBig(u16, @intCast(payload_list.items.len + @bitSizeOf(Header) / 8))), - ); - - // Add the header bytes to the output buf. - var header_buf: [@sizeOf(Header)]u8 = undefined; - var header_buf_stream = std.io.fixedBufferStream(&header_buf); - try header_buf_stream.writer().writeStructEndian(header, .big); - // Add the exact number of bits in the header without padding. - try buf.*.appendSlice(header_buf[0 .. @bitSizeOf(Header) / 8]); - - try buf.*.appendSlice(payload_list.items); - } - - /// Caller is responsible for freeing the returned bytes. - pub fn toBytes(self: SaprusMessage, allocator: Allocator) ![]u8 { - // Create a growable list of bytes to store the output in. - var buf = std.ArrayList(u8).init(allocator); - errdefer buf.deinit(); - - // Start with writing the message type, which is the first 16 bits of every Saprus message. - try buf.appendSlice(asBytes(&nativeToBig(u16, @intFromEnum(self)))); - - // Write the proper header and payload for the given packet type. - switch (self) { - .relay => |r| try toBytesAux(r.header, r.payload, &buf, allocator), - .connection => |c| try toBytesAux(c.header, c.payload, &buf, allocator), - .file_transfer => return SaprusError.NotImplementedSaprusType, - } - - // Collect the growable list as a slice and return it. - return buf.toOwnedSlice(); - } - - fn fromBytesAux( - comptime packet: SaprusPacketType, - len: u16, - r: std.io.FixedBufferStream([]const u8).Reader, - allocator: Allocator, - ) !SaprusMessage { - const Header = @field(@FieldType(SaprusMessage, @tagName(packet)), "Header"); - - // Read the header for the current message type. - var header_bytes: [@sizeOf(Header)]u8 = undefined; - _ = try r.read(header_bytes[0 .. @bitSizeOf(Header) / 8]); - var header_stream = std.io.fixedBufferStream(&header_bytes); - const header = try header_stream.reader().readStructEndian(Header, .big); - - // Read the base64 bytes into a list to be able to call the decoder on it. - const payload_buf = try allocator.alloc(u8, len - @bitSizeOf(Header) / 8); - defer allocator.free(payload_buf); - _ = try r.readAll(payload_buf); - - // Create a buffer to store the payload in, and decode the base64 bytes into the payload field. - const payload = try allocator.alloc(u8, try base64Dec.calcSizeForSlice(payload_buf)); - try base64Dec.decode(payload, payload_buf); - - // Return the type of SaprusMessage specified by the `packet` argument. - return @unionInit(SaprusMessage, @tagName(packet), .{ - .header = header, - .payload = payload, - }); - } - - /// Caller is responsible for calling .deinit on the returned value. - pub fn fromBytes(bytes: []const u8, allocator: Allocator) !SaprusMessage { - var s = std.io.fixedBufferStream(bytes); - const r = s.reader(); - - // Read packet type - const packet_type = @as(SaprusPacketType, @enumFromInt(try r.readInt(u16, .big))); - - // Read the length of the header + base64 encoded payload. - const len = try r.readInt(u16, .big); - - switch (packet_type) { - .relay => return fromBytesAux(.relay, len, r, allocator), - .connection => return fromBytesAux(.connection, len, r, allocator), - .file_transfer => return SaprusError.NotImplementedSaprusType, - else => return SaprusError.UnknownSaprusType, - } - } -}; - -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const asBytes = std.mem.asBytes; -const nativeToBig = std.mem.nativeToBig; - -test "Round trip Relay toBytes and fromBytes" { - const gpa = std.testing.allocator; - const msg = SaprusMessage{ - .relay = .{ - .header = .{ .dest = .{ 255, 255, 255, 255 } }, - .payload = "Hello darkness my old friend", - }, - }; - - const to_bytes = try msg.toBytes(gpa); - defer gpa.free(to_bytes); - - const from_bytes = try SaprusMessage.fromBytes(to_bytes, gpa); - defer from_bytes.deinit(gpa); - - try std.testing.expectEqualDeep(msg, from_bytes); -} - -test "Round trip Connection toBytes and fromBytes" { - const gpa = std.testing.allocator; - const msg = SaprusMessage{ - .connection = .{ - .header = .{ - .src_port = 0, - .dest_port = 0, - }, - .payload = "Hello darkness my old friend", - }, - }; - - const to_bytes = try msg.toBytes(gpa); - defer gpa.free(to_bytes); - - const from_bytes = try SaprusMessage.fromBytes(to_bytes, gpa); - defer from_bytes.deinit(gpa); - - try std.testing.expectEqualDeep(msg, from_bytes); -} -- cgit