/// 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 MessageTypeError = error{ NotImplementedSaprusType, UnknownSaprusType, }; 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 { 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]; } }; 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(@alignOf(Self)) 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: []align(@alignOf(Self)) u8) *Self { std.debug.assert(bytes.len >= @sizeOf(Self)); const res: *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: *Self) *align(1) Relay { return std.mem.bytesAsValue(Relay, &self.bytes); } fn getConnection(self: *Self) *align(1) Connection { return std.mem.bytesAsValue(Connection, &self.bytes); } /// Access the message Saprus payload. pub fn getSaprusTypePayload(self: *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: *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: *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!*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!*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: *Self) SelfBytes { const size = @sizeOf(Self) + self.length; return @as([*]align(@alignOf(Self)) 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: *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: *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; } test { std.testing.refAllDeclsRecursive(@This()); }