From 0d9c0c33fa7c7e73e2e8448ca2e8071584bfaf39 Mon Sep 17 00:00:00 2001 From: Robby Zambito Date: Wed, 14 Jan 2026 18:58:13 -0500 Subject: --- src/message.zig | 610 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 343 insertions(+), 267 deletions(-) (limited to 'src/message.zig') diff --git a/src/message.zig b/src/message.zig index eb15557..c705a0f 100644 --- a/src/message.zig +++ b/src/message.zig @@ -28,289 +28,365 @@ pub const MessageParseError = MessageTypeError || error{ InvalidMessage, }; -// ZERO COPY STUFF -// &payload could be a void value that is treated as a pointer to a [*]u8 -/// All Saprus messages -pub const Message = packed struct { - pub const Relay = packed struct { - dest: @Vector(4, u8), - payload: void, - - pub fn getPayload(self: *align(1) Relay) []u8 { - const len: *u16 = @ptrFromInt(@intFromPtr(self) - @sizeOf(u16)); - return @as([*]u8, @ptrCast(&self.payload))[0 .. len.* - @bitSizeOf(Relay) / 8]; +pub const RelayMessage = struct { + dest: Dest, + payload: []const u8, + + pub const @"type": PacketType = .relay; + pub const Dest = struct { + bytes: [4]u8, + + /// Asserts bytes is less than or equal to 4 bytes + pub fn fromBytes(bytes: []const u8) Dest { + var buf: [4]u8 = @splat(0); + std.debug.assert(bytes.len <= buf.len); + @memcpy(buf[0..bytes.len], bytes); + return .{ .bytes = buf }; } }; - pub const Connection = 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 = .{}, - payload: void, - - pub fn getPayload(self: *align(1) Connection) []u8 { - const len: *u16 = @ptrFromInt(@intFromPtr(self) - @sizeOf(u16)); - return @as([*]u8, @ptrCast(&self.payload))[0 .. len.* - @bitSizeOf(Connection) / 8]; - } - - fn nativeFromNetworkEndian(self: *align(1) Connection) void { - self.src_port = bigToNative(@TypeOf(self.src_port), self.src_port); - self.dest_port = bigToNative(@TypeOf(self.dest_port), self.dest_port); - self.seq_num = bigToNative(@TypeOf(self.seq_num), self.seq_num); - self.msg_id = bigToNative(@TypeOf(self.msg_id), self.msg_id); - } - - fn networkFromNativeEndian(self: *align(1) Connection) void { - self.src_port = nativeToBig(@TypeOf(self.src_port), self.src_port); - self.dest_port = nativeToBig(@TypeOf(self.dest_port), self.dest_port); - self.seq_num = nativeToBig(@TypeOf(self.seq_num), self.seq_num); - self.msg_id = nativeToBig(@TypeOf(self.msg_id), self.msg_id); - } - }; - - const Self = @This(); - const SelfBytes = []align(1) u8; - - type: PacketType, - length: u16, - bytes: void = {}, - - /// Takes a byte slice, and returns a Message struct backed by the slice. - /// This properly initializes the top level headers within the slice. - /// This is used for creating new messages. For reading messages from the network, - /// see: networkBytesAsValue. - pub fn init(@"type": PacketType, bytes: []u8) *align(1) Self { - std.debug.assert(bytes.len >= @sizeOf(Self)); - const res: *align(1) Self = @ptrCast(bytes.ptr); - res.type = @"type"; - res.length = @intCast(bytes.len - @sizeOf(Self)); - return res; - } - - /// Compute the number of bytes required to store a given payload size for a given message type. - pub fn calcSize(comptime @"type": PacketType, payload_len: usize) MessageTypeError!u16 { - const header_size = @bitSizeOf(switch (@"type") { - .relay => Relay, - .connection => Connection, - .file_transfer => return MessageTypeError.NotImplementedSaprusType, - else => return MessageTypeError.UnknownSaprusType, - }) / 8; - return @intCast(payload_len + @sizeOf(Self) + header_size); - } - - fn getRelay(self: *align(1) Self) *align(1) Relay { - return std.mem.bytesAsValue(Relay, &self.bytes); - } - fn getConnection(self: *align(1) Self) *align(1) Connection { - return std.mem.bytesAsValue(Connection, &self.bytes); - } - /// Access the message Saprus payload. - pub fn getSaprusTypePayload(self: *align(1) Self) MessageTypeError!(union(PacketType) { - relay: *align(1) Relay, - file_transfer: void, - connection: *align(1) Connection, - }) { - return switch (self.type) { - .relay => .{ .relay = self.getRelay() }, - .connection => .{ .connection = self.getConnection() }, - .file_transfer => MessageTypeError.NotImplementedSaprusType, - else => MessageTypeError.UnknownSaprusType, - }; + pub fn init(dest: Dest, payload: []const u8) RelayMessage { + return .{ .dest = dest, .payload = payload }; } - /// Convert the message to native endianness from network endianness in-place. - pub fn nativeFromNetworkEndian(self: *align(1) Self) MessageTypeError!void { - self.type = @enumFromInt(bigToNative( - @typeInfo(@TypeOf(self.type)).@"enum".tag_type, - @intFromEnum(self.type), - )); - self.length = bigToNative(@TypeOf(self.length), self.length); - errdefer { - // If the payload specific headers fail, revert the top level header values - self.type = @enumFromInt(nativeToBig( - @typeInfo(@TypeOf(self.type)).@"enum".tag_type, - @intFromEnum(self.type), - )); - self.length = nativeToBig(@TypeOf(self.length), self.length); - } - switch (try self.getSaprusTypePayload()) { - .relay => {}, - .connection => |*con| con.*.nativeFromNetworkEndian(), - // We know other values are unreachable, - // because they would have returned an error from the switch condition. - else => unreachable, - } + /// Asserts that buf is large enough to fit the relay message. + pub fn toBytes(self: RelayMessage, buf: []u8) []u8 { + var out: Writer = .fixed(buf); + out.writeInt(u16, @intFromEnum(RelayMessage.type), .big) catch unreachable; + out.writeInt(u16, @intCast(self.payload.len + self.dest.bytes.len), .big) catch unreachable; + out.writeAll(&self.dest.bytes) catch unreachable; + out.writeAll(self.payload) catch unreachable; + return out.buffered(); } - /// Convert the message to network endianness from native endianness in-place. - pub fn networkFromNativeEndian(self: *align(1) Self) MessageTypeError!void { - try switch (try self.getSaprusTypePayload()) { - .relay => {}, - .connection => |*con| con.*.networkFromNativeEndian(), - .file_transfer => MessageTypeError.NotImplementedSaprusType, - else => MessageTypeError.UnknownSaprusType, - }; - self.type = @enumFromInt(nativeToBig( - @typeInfo(@TypeOf(self.type)).@"enum".tag_type, - @intFromEnum(self.type), - )); - self.length = nativeToBig(@TypeOf(self.length), self.length); + pub fn fromBytes(buf: []const u8) RelayMessage { + var in: Reader = .fixed(buf); } - /// Convert network endian bytes to a native endian value in-place. - pub fn networkBytesAsValue(bytes: SelfBytes) MessageParseError!*align(1) Self { - const res = std.mem.bytesAsValue(Self, bytes); - try res.nativeFromNetworkEndian(); - return .bytesAsValue(bytes); - } - - /// Create a structured view of the bytes without initializing the length or type, - /// and without converting the endianness. - pub fn bytesAsValue(bytes: SelfBytes) MessageParseError!*align(1) Self { - const res = std.mem.bytesAsValue(Self, bytes); - return switch (res.type) { - .relay, .connection => if (bytes.len == res.length + @sizeOf(Self)) - res - else - MessageParseError.InvalidMessage, - .file_transfer => MessageParseError.NotImplementedSaprusType, - else => MessageParseError.UnknownSaprusType, + test toBytes { + var buf: [1024]u8 = undefined; + const relay: RelayMessage = .init( + .fromBytes(&.{ 172, 18, 1, 30 }), + // zig fmt: off + &[_]u8{ + 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 + }, + // zig fmt: on + ); + // zig fmt: off + var expected = [_]u8{ + 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 }; - } - - /// Deprecated. - /// If I need the bytes, I should just pass around the slice that is backing this to begin with. - pub fn asBytes(self: *align(1) Self) SelfBytes { - const size = @sizeOf(Self) + self.length; - return @as([*]align(1) u8, @ptrCast(self))[0..size]; + // zig fmt: on + try std.testing.expectEqualSlices(u8, &expected, relay.toBytes(&buf)); } }; -test "testing variable length zero copy struct" { - { - // Relay test - const payload = "Hello darkness my old friend"; - var msg_bytes: [try Message.calcSize(.relay, payload.len)]u8 align(@alignOf(Message)) = undefined; - - // Create a view of the byte slice as a Message - const msg: *align(1) Message = .init(.relay, &msg_bytes); - - { - // Set the message values - { - // These are both set by the init call. - // msg.type = .relay; - // msg.length = payload_len; - } - const relay = (try msg.getSaprusTypePayload()).relay; - relay.dest = .{ 1, 2, 3, 4 }; - @memcpy(relay.getPayload(), payload); - } - - { - // Print the message as hex using the network byte order - try msg.networkFromNativeEndian(); - // We know the error from nativeFromNetworkEndian is unreachable because - // it would have returned an error from networkFromNativeEndian. - defer msg.nativeFromNetworkEndian() catch unreachable; - std.debug.print("relay network bytes: {x}\n", .{msg_bytes}); - std.debug.print("bytes len: {d}\n", .{msg_bytes.len}); - } - - if (false) { - // Illegal behavior - std.debug.print("{any}\n", .{(try msg.getSaprusTypePayload()).connection}); - } - - try std.testing.expectEqualDeep(msg, try Message.bytesAsValue(msg.asBytes())); - } - - { - // Connection test - const payload = "Hello darkness my old friend"; - var msg_bytes: [try Message.calcSize(.connection, payload.len)]u8 align(@alignOf(Message)) = undefined; - - // Create a view of the byte slice as a Message - const msg: *align(1) Message = .init(.connection, &msg_bytes); - - { - // Initializing connection header values - const connection = (try msg.getSaprusTypePayload()).connection; - connection.src_port = 1; - connection.dest_port = 2; - connection.seq_num = 3; - connection.msg_id = 4; - connection.reserved = 5; - connection.options = @bitCast(@as(u8, 6)); - @memcpy(connection.getPayload(), payload); - } - - { - // Print the message as hex using the network byte order - try msg.networkFromNativeEndian(); - // We know the error from nativeFromNetworkEndian is unreachable because - // it would have returned an error from networkFromNativeEndian. - defer msg.nativeFromNetworkEndian() catch unreachable; - std.debug.print("connection network bytes: {x}\n", .{msg_bytes}); - std.debug.print("bytes len: {d}\n", .{msg_bytes.len}); - } - } -} +// pub const ConnectionMessage = struct { +// length: u16, +// src_port: u16, +// dest_port: u16, +// seq_num: u32 = 0, +// msg_id: u32 = 0, +// // _reserved: u8 = 0, +// options: ConnectionOptions = .{}, +// payload: []u8, + +// pub const type: PacketType = .connection; + +// /// Asserts that buf is large enough to fit the connection message. +// fn toBytes(self: ConnectionMessage, buf: []u8) []u8 { +// var out: Writer = .fixed(buf); +// out.writeInt(u16, ConnectionMessage.type, .big) catch unreachable; +// return w.buffered(); +// } +// }; + +// // ZERO COPY STUFF +// // &payload could be a void value that is treated as a pointer to a [*]u8 +// /// All Saprus messages +// pub const Message = packed struct { +// pub const Relay = packed struct { +// dest: @Vector(4, u8), +// payload: void, + +// pub fn getPayload(self: *align(1) Relay) []u8 { +// const len: *u16 = @ptrFromInt(@intFromPtr(self) - @sizeOf(u16)); +// return @as([*]u8, @ptrCast(&self.payload))[0 .. len.* - @bitSizeOf(Relay) / 8]; +// } +// }; +// pub const Connection = 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 = .{}, +// payload: void, + +// pub fn getPayload(self: *align(1) Connection) []u8 { +// const len: *u16 = @ptrFromInt(@intFromPtr(self) - @sizeOf(u16)); +// return @as([*]u8, @ptrCast(&self.payload))[0 .. len.* - @bitSizeOf(Connection) / 8]; +// } + +// fn nativeFromNetworkEndian(self: *align(1) Connection) void { +// self.src_port = bigToNative(@TypeOf(self.src_port), self.src_port); +// self.dest_port = bigToNative(@TypeOf(self.dest_port), self.dest_port); +// self.seq_num = bigToNative(@TypeOf(self.seq_num), self.seq_num); +// self.msg_id = bigToNative(@TypeOf(self.msg_id), self.msg_id); +// } + +// fn networkFromNativeEndian(self: *align(1) Connection) void { +// self.src_port = nativeToBig(@TypeOf(self.src_port), self.src_port); +// self.dest_port = nativeToBig(@TypeOf(self.dest_port), self.dest_port); +// self.seq_num = nativeToBig(@TypeOf(self.seq_num), self.seq_num); +// self.msg_id = nativeToBig(@TypeOf(self.msg_id), self.msg_id); +// } +// }; + +// const Self = @This(); +// const SelfBytes = []align(1) u8; + +// type: PacketType, +// length: u16, +// bytes: void = {}, + +// /// Takes a byte slice, and returns a Message struct backed by the slice. +// /// This properly initializes the top level headers within the slice. +// /// This is used for creating new messages. For reading messages from the network, +// /// see: networkBytesAsValue. +// pub fn init(@"type": PacketType, bytes: []u8) *align(1) Self { +// std.debug.assert(bytes.len >= @sizeOf(Self)); +// const res: *align(1) Self = @ptrCast(bytes.ptr); +// res.type = @"type"; +// res.length = @intCast(bytes.len - @sizeOf(Self)); +// return res; +// } + +// /// Compute the number of bytes required to store a given payload size for a given message type. +// pub fn calcSize(comptime @"type": PacketType, payload_len: usize) MessageTypeError!u16 { +// const header_size = @bitSizeOf(switch (@"type") { +// .relay => Relay, +// .connection => Connection, +// .file_transfer => return MessageTypeError.NotImplementedSaprusType, +// else => return MessageTypeError.UnknownSaprusType, +// }) / 8; +// return @intCast(payload_len + @sizeOf(Self) + header_size); +// } + +// fn getRelay(self: *align(1) Self) *align(1) Relay { +// return std.mem.bytesAsValue(Relay, &self.bytes); +// } +// fn getConnection(self: *align(1) Self) *align(1) Connection { +// return std.mem.bytesAsValue(Connection, &self.bytes); +// } + +// /// Access the message Saprus payload. +// pub fn getSaprusTypePayload(self: *align(1) Self) MessageTypeError!(union(PacketType) { +// relay: *align(1) Relay, +// file_transfer: void, +// connection: *align(1) Connection, +// }) { +// return switch (self.type) { +// .relay => .{ .relay = self.getRelay() }, +// .connection => .{ .connection = self.getConnection() }, +// .file_transfer => MessageTypeError.NotImplementedSaprusType, +// else => MessageTypeError.UnknownSaprusType, +// }; +// } + +// /// Convert the message to native endianness from network endianness in-place. +// pub fn nativeFromNetworkEndian(self: *align(1) Self) MessageTypeError!void { +// self.type = @enumFromInt(bigToNative( +// @typeInfo(@TypeOf(self.type)).@"enum".tag_type, +// @intFromEnum(self.type), +// )); +// self.length = bigToNative(@TypeOf(self.length), self.length); +// errdefer { +// // If the payload specific headers fail, revert the top level header values +// self.type = @enumFromInt(nativeToBig( +// @typeInfo(@TypeOf(self.type)).@"enum".tag_type, +// @intFromEnum(self.type), +// )); +// self.length = nativeToBig(@TypeOf(self.length), self.length); +// } +// switch (try self.getSaprusTypePayload()) { +// .relay => {}, +// .connection => |*con| con.*.nativeFromNetworkEndian(), +// // We know other values are unreachable, +// // because they would have returned an error from the switch condition. +// else => unreachable, +// } +// } + +// /// Convert the message to network endianness from native endianness in-place. +// pub fn networkFromNativeEndian(self: *align(1) Self) MessageTypeError!void { +// try switch (try self.getSaprusTypePayload()) { +// .relay => {}, +// .connection => |*con| con.*.networkFromNativeEndian(), +// .file_transfer => MessageTypeError.NotImplementedSaprusType, +// else => MessageTypeError.UnknownSaprusType, +// }; +// self.type = @enumFromInt(nativeToBig( +// @typeInfo(@TypeOf(self.type)).@"enum".tag_type, +// @intFromEnum(self.type), +// )); +// self.length = nativeToBig(@TypeOf(self.length), self.length); +// } + +// /// Convert network endian bytes to a native endian value in-place. +// pub fn networkBytesAsValue(bytes: SelfBytes) MessageParseError!*align(1) Self { +// const res = std.mem.bytesAsValue(Self, bytes); +// try res.nativeFromNetworkEndian(); +// return .bytesAsValue(bytes); +// } + +// /// Create a structured view of the bytes without initializing the length or type, +// /// and without converting the endianness. +// pub fn bytesAsValue(bytes: SelfBytes) MessageParseError!*align(1) Self { +// const res = std.mem.bytesAsValue(Self, bytes); +// return switch (res.type) { +// .relay, .connection => if (bytes.len == res.length + @sizeOf(Self)) +// res +// else +// MessageParseError.InvalidMessage, +// .file_transfer => MessageParseError.NotImplementedSaprusType, +// else => MessageParseError.UnknownSaprusType, +// }; +// } + +// /// Deprecated. +// /// If I need the bytes, I should just pass around the slice that is backing this to begin with. +// pub fn asBytes(self: *align(1) Self) SelfBytes { +// const size = @sizeOf(Self) + self.length; +// return @as([*]align(1) u8, @ptrCast(self))[0..size]; +// } +// }; + +// test "testing variable length zero copy struct" { +// { +// // Relay test +// const payload = "Hello darkness my old friend"; +// var msg_bytes: [try Message.calcSize(.relay, payload.len)]u8 align(@alignOf(Message)) = undefined; + +// // Create a view of the byte slice as a Message +// const msg: *align(1) Message = .init(.relay, &msg_bytes); + +// { +// // Set the message values +// { +// // These are both set by the init call. +// // msg.type = .relay; +// // msg.length = payload_len; +// } +// const relay = (try msg.getSaprusTypePayload()).relay; +// relay.dest = .{ 1, 2, 3, 4 }; +// @memcpy(relay.getPayload(), payload); +// } + +// { +// // Print the message as hex using the network byte order +// try msg.networkFromNativeEndian(); +// // We know the error from nativeFromNetworkEndian is unreachable because +// // it would have returned an error from networkFromNativeEndian. +// defer msg.nativeFromNetworkEndian() catch unreachable; +// std.debug.print("relay network bytes: {x}\n", .{msg_bytes}); +// std.debug.print("bytes len: {d}\n", .{msg_bytes.len}); +// } + +// if (false) { +// // Illegal behavior +// std.debug.print("{any}\n", .{(try msg.getSaprusTypePayload()).connection}); +// } + +// try std.testing.expectEqualDeep(msg, try Message.bytesAsValue(msg.asBytes())); +// } + +// { +// // Connection test +// const payload = "Hello darkness my old friend"; +// var msg_bytes: [try Message.calcSize(.connection, payload.len)]u8 align(@alignOf(Message)) = undefined; + +// // Create a view of the byte slice as a Message +// const msg: *align(1) Message = .init(.connection, &msg_bytes); + +// { +// // Initializing connection header values +// const connection = (try msg.getSaprusTypePayload()).connection; +// connection.src_port = 1; +// connection.dest_port = 2; +// connection.seq_num = 3; +// connection.msg_id = 4; +// connection.reserved = 5; +// connection.options = @bitCast(@as(u8, 6)); +// @memcpy(connection.getPayload(), payload); +// } + +// { +// // Print the message as hex using the network byte order +// try msg.networkFromNativeEndian(); +// // We know the error from nativeFromNetworkEndian is unreachable because +// // it would have returned an error from networkFromNativeEndian. +// defer msg.nativeFromNetworkEndian() catch unreachable; +// std.debug.print("connection network bytes: {x}\n", .{msg_bytes}); +// std.debug.print("bytes len: {d}\n", .{msg_bytes.len}); +// } +// } +// } const std = @import("std"); const Allocator = std.mem.Allocator; - -const asBytes = std.mem.asBytes; -const nativeToBig = std.mem.nativeToBig; -const bigToNative = std.mem.bigToNative; - -test "Round trip Relay toBytes and fromBytes" { - if (false) { - 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); - } - return error.SkipZigTest; -} - -test "Round trip Connection toBytes and fromBytes" { - if (false) { - 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); - } - return error.SkipZigTest; -} +const Writer = std.Io.Writer; + +// const asBytes = std.mem.asBytes; +// const nativeToBig = std.mem.nativeToBig; +// const bigToNative = std.mem.bigToNative; + +// test "Round trip Relay toBytes and fromBytes" { +// if (true) return error.SkipZigTest; +// 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" { +// if (false) { +// 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); +// } +// return error.SkipZigTest; +// } test { std.testing.refAllDeclsRecursive(@This()); -- cgit