/// 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, 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.* - @sizeOf(Relay)]; } }; 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.* - @sizeOf(Connection)]; } 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 = {}, pub fn init(allocator: Allocator, comptime @"type": PacketType, payload_len: u16) !*Self { const header_size = @sizeOf(switch (@"type") { .relay => Relay, .connection => Connection, .file_transfer => return Error.NotImplementedSaprusType, else => return Error.UnknownSaprusType, }); const size = payload_len + @sizeOf(Self) + header_size; const bytes = try allocator.alignedAlloc(u8, @alignOf(Self), size); const res: *Self = @ptrCast(bytes.ptr); res.type = @"type"; res.length = payload_len + header_size; return res; } pub fn deinit(self: *Self, allocator: Allocator) void { allocator.free(self.asBytes()); } 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); } pub fn getSaprusTypePayload(self: *Self) Error!(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 => Error.NotImplementedSaprusType, else => Error.UnknownSaprusType, }; } pub fn nativeFromNetworkEndian(self: *Self) Error!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, } } pub fn networkFromNativeEndian(self: *Self) Error!void { try switch (try self.getSaprusTypePayload()) { .relay => {}, .connection => |*con| con.*.networkFromNativeEndian(), .file_transfer => Error.NotImplementedSaprusType, else => Error.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 bytesAsValue(bytes: SelfBytes) !*Self { const res = std.mem.bytesAsValue(Self, bytes); return switch (res.type) { .relay, .connection => if (bytes.len == res.length + @sizeOf(Self)) res else Error.InvalidMessage, .file_transfer => Error.NotImplementedSaprusType, else => Error.UnknownSaprusType, }; } 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" { const gpa = std.testing.allocator; const payload = "Hello darkness my old friend"; // Create a view of the byte slice as a Message const msg: *Message = try .init(gpa, .relay, payload.len); defer msg.deinit(gpa); { // 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); } { const bytes = msg.asBytes(); // 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("network bytes: {x}\n", .{bytes}); std.debug.print("bytes len: {d}\n", .{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())); } 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" { 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); } test { std.testing.refAllDeclsRecursive(@This()); }