25 Commits

Author SHA1 Message Date
56b6b8a386 Use Client as var type instead of singleton 2025-05-11 13:52:42 -04:00
14ed0bc3f3 Fix issue returning stack pointer 2025-05-11 13:40:55 -04:00
c72503fce6 Fix extra bytes in connection message. 2025-05-11 13:40:23 -04:00
373dbebc8c Add broadcast initial interest using raw sockets
Use this from the relay message
2025-05-11 11:40:15 -04:00
cde289d648 Update gatorcat dep and use bytes for broadcast message
The latter is helpful for the lifetime of the message.
2025-05-11 10:12:26 -04:00
716fb466fa Remove allocation for messages 2025-05-10 21:46:53 -04:00
583f9d8b8f Add comments and fix tests
Also added networkBytesAsValue and restored bytesAsValue.
These are useful for treating the bytes from the network directly as a Message.
Otherwise, the init function would overwrite the packet type and length to be correct.
I would like the message handling to fail if the message body is incorrect.
2025-05-10 21:46:53 -04:00
56e72928c6 fix use after free 2025-05-10 21:46:53 -04:00
a80c9abfe7 Attempt to base64 encode the connection payload
For some reason I am still getting this:

2025/05/10 16:37:06 Error decoding message: SGVsbG8gZGFya25lc3MgbXkgb2xkIGZyaWVuZA==::53475673624738675a4746796132356c63334d6762586b676232786b49475a79615756755a413d3daaaa
2025-05-10 21:46:53 -04:00
245dab4909 Use slice for init, and add better error sets.
The slice sets us avoid allocating within the init function.
This means init can't fail, and it also makes it easier to stack allocate messages (slice an array buffer, instead of creating a stack allocator).
2025-05-10 21:46:53 -04:00
cde5c3626c 2025-05-10 21:46:53 -04:00
e84d1a2300 2025-05-10 21:46:53 -04:00
1b7d9bbb1a Remove bytesAsValueUnchecked
Callers can instead use std.mem.bytesAsValue directly.
2025-05-10 21:46:53 -04:00
1512ec1a86 Cleanup asBytes and test it 2025-05-10 21:46:53 -04:00
f1dce257be Simplify init interface 2025-05-10 21:46:53 -04:00
bcab1e4d00 2025-05-10 21:46:53 -04:00
0e8f016978 Align the bytes instead of the struct 2025-05-10 21:46:53 -04:00
fc53e87389 2025-05-10 21:46:53 -04:00
cbf554e853 2025-05-10 21:46:53 -04:00
775212013f 2025-05-10 21:46:53 -04:00
339ac5cfe5 2025-05-10 21:46:53 -04:00
eacfaffb6b 2025-05-10 21:46:53 -04:00
1731b2e643 2025-05-10 21:46:53 -04:00
dae66a0039 Starting real connections 2025-05-10 21:46:53 -04:00
683a2015b0 Use FAIL as the default dest if unable to parse 2025-04-27 18:03:06 -04:00
9 changed files with 399 additions and 405 deletions

View File

@@ -1,5 +1,4 @@
const std = @import("std"); const std = @import("std");
const Step = std.Build.Step;
// Although this function looks imperative, note that its job is to // Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external // declaratively construct a build graph that will be executed by an external
@@ -34,33 +33,18 @@ pub fn build(b: *std.Build) void {
}); });
lib_mod.addImport("network", b.dependency("network", .{}).module("network")); lib_mod.addImport("network", b.dependency("network", .{}).module("network"));
lib_mod.addImport("gatorcat", b.dependency("gatorcat", .{}).module("gatorcat"));
exe_mod.addImport("zaprus", lib_mod); exe_mod.addImport("zaprus", lib_mod);
exe_mod.addImport("clap", b.dependency("clap", .{}).module("clap")); exe_mod.addImport("clap", b.dependency("clap", .{}).module("clap"));
const static_lib = b.addLibrary(.{ const lib = b.addLibrary(.{
.linkage = .static, .linkage = .static,
.name = "zaprus", .name = "zaprus",
.root_module = lib_mod, .root_module = lib_mod,
}); });
static_lib.addIncludePath(.{ .cwd_relative = "include" });
static_lib.linkLibC();
b.installArtifact(static_lib); b.installArtifact(lib);
const dynamic_lib = b.addLibrary(.{
.linkage = .dynamic,
.name = "zaprus",
.root_module = lib_mod,
});
dynamic_lib.addIncludePath(.{ .cwd_relative = "include" });
dynamic_lib.linkLibC();
b.installArtifact(dynamic_lib);
// C Headers
const c_header = b.addInstallHeaderFile(b.path("include/zaprus.h"), "zaprus.h");
b.getInstallStep().dependOn(&c_header.step);
// This creates another `std.Build.Step.Compile`, but this one builds an executable // This creates another `std.Build.Step.Compile`, but this one builds an executable
// rather than a static library. // rather than a static library.

View File

@@ -44,6 +44,10 @@
.url = "git+https://github.com/Hejsil/zig-clap?ref=0.10.0#e47028deaefc2fb396d3d9e9f7bd776ae0b2a43a", .url = "git+https://github.com/Hejsil/zig-clap?ref=0.10.0#e47028deaefc2fb396d3d9e9f7bd776ae0b2a43a",
.hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0",
}, },
.gatorcat = .{
.url = "git+https://github.com/kj4tmp/gatorcat.git#0a97b666677501db4939e3e8245f88a19e015893",
.hash = "gatorcat-0.3.4-WcrpTcleBwCta_9TjomuIGb3bdg2Pke_FXI_WkMTEivH",
},
}, },
.paths = .{ .paths = .{
"build.zig", "build.zig",

View File

@@ -1,51 +0,0 @@
// client
#include<stdint.h>
#include<stdlib.h>
int zaprus_init(void);
int zaprus_deinit(void);
int zaprus_send_relay(const char* payload, size_t len, char dest[4]);
int zaprus_send_initial_connection(const char* payload, size_t len, uint16_t initial_port);
struct SaprusMessage* zaprus_connect(const char* payload, size_t len);
// message
#define SAPRUS_RELAY_MESSAGE_TYPE 0x003C
#define SAPRUS_FILE_TRANSFER_MESSAGE_TYPE 0x8888
#define SAPRUS_CONNECTION_MESSAGE_TYPE 0x00E9
struct SaprusMessage {
uint16_t packet_type;
uint16_t payload_len;
union {
struct {
struct {
char dest[4];
};
} relay;
struct {
struct {
uint16_t src_port;
uint16_t dest_port;
uint32_t seq_num;
uint32_t msg_id;
char _reserved;
char options;
};
} connection;
} headers;
char *payload;
};
// ptr should be freed by the caller.
int zaprus_message_to_bytes(struct SaprusMessage msg, char** ptr, size_t* len);
// Return value should be destroyed with zaprus_message_deinit.
struct SaprusMessage* zaprus_message_from_bytes(const char* bytes, size_t len);
void zaprus_message_deinit(struct SaprusMessage* msg);

View File

@@ -1,22 +1,88 @@
var rand: ?Random = null; const base64Enc = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, '=');
const base64Dec = std.base64.Base64Decoder.init(std.base64.standard_alphabet_chars, '=');
pub fn init() !void { rand: Random,
socket: gcat.nic.RawSocket,
const Self = @This();
const max_message_size = 2048;
pub fn init(interface_name: [:0]const u8) !Self {
var prng = Random.DefaultPrng.init(blk: { var prng = Random.DefaultPrng.init(blk: {
var seed: u64 = undefined; var seed: u64 = undefined;
try posix.getrandom(mem.asBytes(&seed)); try posix.getrandom(mem.asBytes(&seed));
break :blk seed; break :blk seed;
}); });
rand = prng.random(); const rand = prng.random();
try network.init();
const socket: gcat.nic.RawSocket = try .init(interface_name);
return .{
.rand = rand,
.socket = socket,
};
} }
pub fn deinit() void { pub fn deinit(self: *Self) void {
network.deinit(); self.socket.deinit();
} }
fn broadcastSaprusMessage(msg: SaprusMessage, udp_port: u16, allocator: Allocator) !void { /// Used for relay messages and connection handshake.
const msg_bytes = try msg.toBytes(allocator); /// Assumes Client .init has been called.
defer allocator.free(msg_bytes); fn broadcastInitialInterestMessage(self: *Self, msg_bytes: []align(@alignOf(SaprusMessage)) u8) !void {
var packet_bytes: [max_message_size]u8 = comptime blk: {
var b: [max_message_size]u8 = @splat(0);
// Destination MAC addr to FF:FF:FF:FF:FF:FF
for (0..6) |i| {
b[i] = 0xff;
}
// Set Ethernet type to IPv4
b[0x0c] = 0x08;
b[0x0d] = 0x00;
// Set IPv4 version to 4
b[0x0e] = 0x45;
// Destination broadcast
for (0x1e..0x22) |i| {
b[i] = 0xff;
}
// Set TTL
b[0x16] = 0x40;
// Set IPv4 protocol to UDP
b[0x17] = 0x11;
// Set interest filter value to 8888.
b[0x24] = 0x22;
b[0x25] = 0xb8;
break :blk b;
};
var msg: *SaprusMessage = try .bytesAsValue(msg_bytes);
try msg.networkFromNativeEndian();
defer msg.nativeFromNetworkEndian() catch unreachable;
// The byte position within the packet that the saprus message starts at.
const saprus_start_byte = 42;
@memcpy(packet_bytes[saprus_start_byte .. saprus_start_byte + msg_bytes.len], msg_bytes);
try self.socket.linkLayer().send(packet_bytes[0 .. saprus_start_byte + msg_bytes.len]);
}
// fn broadcastSaprusMessage(msg_bytes: []align(@alignOf(SaprusMessage)) u8) !void {}
fn broadcastSaprusMessage(msg_bytes: []align(@alignOf(SaprusMessage)) u8, udp_port: u16) !void {
if (false) {
var foo: gcat.nic.RawSocket = try .init("enp7s0"); // /proc/net/dev
defer foo.deinit();
}
const msg: *SaprusMessage = try .bytesAsValue(msg_bytes);
try msg.networkFromNativeEndian();
defer msg.nativeFromNetworkEndian() catch unreachable;
var sock = try network.Socket.create(.ipv4, .udp); var sock = try network.Socket.create(.ipv4, .udp);
defer sock.close(); defer sock.close();
@@ -36,54 +102,57 @@ fn broadcastSaprusMessage(msg: SaprusMessage, udp_port: u16, allocator: Allocato
try sock.bind(bind_addr); try sock.bind(bind_addr);
std.debug.print("{x}\n", .{msg_bytes});
_ = try sock.sendTo(dest_addr, msg_bytes); _ = try sock.sendTo(dest_addr, msg_bytes);
} }
pub fn sendRelay(payload: []const u8, dest: [4]u8, allocator: Allocator) !void { pub fn sendRelay(self: *Self, payload: []const u8, dest: [4]u8) !void {
const msg = SaprusMessage{ var buf: [max_message_size]u8 align(@alignOf(SaprusMessage)) = undefined;
.relay = .{ const msg_bytes = buf[0..try SaprusMessage.calcSize(
.header = .{ .dest = dest }, .relay,
.payload = payload, base64Enc.calcSize(payload.len),
}, )];
}; const msg: *SaprusMessage = .init(.relay, msg_bytes);
try broadcastSaprusMessage(msg, 8888, allocator); const relay = (try msg.getSaprusTypePayload()).relay;
relay.dest = dest;
_ = base64Enc.encode(relay.getPayload(), payload);
try self.broadcastInitialInterestMessage(msg_bytes);
} }
fn randomPort() u16 { fn randomPort(self: Self) u16 {
var p: u16 = 0; return self.rand.intRangeAtMost(u16, 1024, 65000);
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 { pub fn sendInitialConnection(
const dest_port = randomPort(); self: Self,
const msg = SaprusMessage{ payload: []const u8,
.connection = .{ output_bytes: []align(@alignOf(SaprusMessage)) u8,
.header = .{ initial_port: u16,
.src_port = initial_port, ) !*SaprusMessage {
.dest_port = dest_port, const dest_port = self.randomPort();
}, const msg_bytes = output_bytes[0..try SaprusMessage.calcSize(
.payload = payload, .connection,
}, base64Enc.calcSize(payload.len),
}; )];
const msg: *SaprusMessage = .init(.connection, msg_bytes);
try broadcastSaprusMessage(msg, 8888, allocator); const connection = (try msg.getSaprusTypePayload()).connection;
connection.src_port = initial_port;
connection.dest_port = dest_port;
_ = base64Enc.encode(connection.getPayload(), payload);
try broadcastSaprusMessage(msg_bytes, 8888);
return msg; return msg;
} }
pub fn connect(payload: []const u8, allocator: Allocator) !?SaprusMessage { pub fn connect(self: Self, payload: []const u8) !?SaprusConnection {
var initial_port: u16 = 0; const initial_port = self.randomPort();
if (rand) |r| {
initial_port = r.intRangeAtMost(u16, 1024, 65000);
} else unreachable;
var initial_conn_res: ?SaprusMessage = null; var initial_conn_res: ?*SaprusMessage = null;
errdefer if (initial_conn_res) |c| c.deinit(allocator);
var sock = try network.Socket.create(.ipv4, .udp); var sock = try network.Socket.create(.ipv4, .udp);
defer sock.close(); defer sock.close();
@@ -98,21 +167,26 @@ pub fn connect(payload: []const u8, allocator: Allocator) !?SaprusMessage {
try sock.setReadTimeout(1 * std.time.us_per_s); try sock.setReadTimeout(1 * std.time.us_per_s);
try sock.bind(bind_addr); try sock.bind(bind_addr);
const msg = try sendInitialConnection(payload, initial_port, allocator); var sent_msg_bytes: [max_message_size]u8 align(@alignOf(SaprusMessage)) = undefined;
const msg = try self.sendInitialConnection(payload, &sent_msg_bytes, initial_port);
var response_buf: [4096]u8 = undefined; var response_buf: [max_message_size]u8 align(@alignOf(SaprusMessage)) = undefined;
_ = try sock.receive(&response_buf); // Ignore message that I sent. _ = try sock.receive(&response_buf); // Ignore message that I sent.
const len = try sock.receive(&response_buf); const len = try sock.receive(&response_buf);
initial_conn_res = try SaprusMessage.fromBytes(response_buf[0..len], allocator); initial_conn_res = try .networkBytesAsValue(response_buf[0..len]);
// Complete handshake after awaiting response // Complete handshake after awaiting response
try broadcastSaprusMessage(msg, randomPort(), allocator); try broadcastSaprusMessage(msg.asBytes(), self.randomPort());
return initial_conn_res; if (false) {
return initial_conn_res.?;
}
return null;
} }
const SaprusMessage = @import("message.zig").Message; const SaprusMessage = @import("message.zig").Message;
const SaprusConnection = @import("Connection.zig");
const std = @import("std"); const std = @import("std");
const Random = std.Random; const Random = std.Random;
@@ -120,5 +194,4 @@ const posix = std.posix;
const mem = std.mem; const mem = std.mem;
const network = @import("network"); const network = @import("network");
const gcat = @import("gatorcat");
const Allocator = mem.Allocator;

0
src/Connection.zig Normal file
View File

View File

@@ -1,115 +0,0 @@
const c = @cImport({
@cInclude("zaprus.h");
});
fn zigToCMessage(msg: ?*zaprus.Message) !?*c.SaprusMessage {
if (msg) |m| {
var res = c.SaprusMessage{
.packet_type = @intFromEnum(m.*),
};
switch (m.*) {
.relay => |r| {
res.headers.relay = .{
.unnamed_0 = .{
.dest = r.header.dest,
},
};
res.payload_len = @intCast(r.payload.len);
res.payload = (try allocator.alloc(u8, r.payload.len)).ptr;
},
.connection => |con| {
res.headers.connection = .{
.unnamed_0 = .{
.src_port = con.header.src_port,
.dest_port = con.header.dest_port,
.seq_num = con.header.seq_num,
.msg_id = con.header.msg_id,
._reserved = con.header.reserved,
.options = @bitCast(con.header.options),
},
};
res.payload_len = @intCast(con.payload.len);
res.payload = (try allocator.alloc(u8, con.payload.len)).ptr;
},
.file_transfer => return zaprus.Error.NotImplementedSaprusType,
else => return zaprus.Error.UnknownSaprusType,
}
return &res;
} else return null;
}
fn cToZigMessage(msg: ?*c.SaprusMessage) ?*zaprus.Message {
_ = msg;
return @constCast(&zaprus.Message{
.relay = .{
.header = .{ .dest = @splat(0) },
.payload = &.{0},
},
});
}
// client
export fn zaprus_init() c_int {
SaprusClient.init() catch return 1;
return 0;
}
export fn zaprus_deinit() c_int {
SaprusClient.deinit();
return 0;
}
export fn zaprus_send_relay(payload: [*]const u8, len: usize, dest: [*]u8) c_int {
SaprusClient.sendRelay(payload[0..len], dest[0..4].*, allocator) catch return 1;
return 0;
}
export fn zaprus_send_initial_connection(payload: [*]const u8, len: usize, initial_port: u16) c_int {
_ = SaprusClient.sendInitialConnection(payload[0..len], initial_port, allocator) catch return 1;
return 0;
}
export fn zaprus_connect(payload: [*]const u8, len: usize) ?*c.SaprusMessage {
if (SaprusClient.connect(payload[0..len], allocator)) |msg| {
return zigToCMessage(@constCast(&(msg.?))) catch null;
} else |_| {
return null;
}
}
// message
/// ptr should be freed by the caller.
export fn zaprus_message_to_bytes(msg: c.SaprusMessage, ptr: *[*]u8, len: *usize) c_int {
if (cToZigMessage(@constCast(&msg))) |m| {
const bytes = m.toBytes(allocator) catch return 1;
ptr.* = bytes.ptr;
len.* = bytes.len;
return 0;
} else return 1;
}
/// Return value should be destroyed with zaprus_message_deinit.
export fn zaprus_message_from_bytes(bytes: [*]const u8, len: usize) ?*c.SaprusMessage {
if (zaprus.Message.fromBytes(bytes[0..len], allocator)) |msg| {
return zigToCMessage(@constCast(&msg)) catch null;
} else |_| return null;
}
export fn zaprus_message_deinit(msg: *c.SaprusMessage) void {
// noop
_ = msg;
// msg.*.deinit(allocator);
}
const std = @import("std");
const zaprus = @import("./root.zig");
const SaprusClient = zaprus.Client;
const allocator = std.heap.c_allocator;
test {
std.testing.refAllDeclsRecursively(@This());
}

View File

@@ -42,40 +42,33 @@ pub fn main() !void {
}; };
defer res.deinit(); defer res.deinit();
try SaprusClient.init();
defer SaprusClient.deinit();
if (res.args.help != 0) { if (res.args.help != 0) {
return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{}); return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{});
} }
var client = try SaprusClient.init("enp7s0");
defer client.deinit();
if (res.args.relay) |r| { if (res.args.relay) |r| {
const dest = parseDest(res.args.dest) catch .{ 70, 70, 70, 70 }; const dest = parseDest(res.args.dest);
try SaprusClient.sendRelay( try client.sendRelay(
if (r.len > 0) r else "Hello darkness my old friend", if (r.len > 0) r else "Hello darkness my old friend",
dest, dest,
gpa,
); );
// std.debug.print("Sent: {s}\n", .{r}); // std.debug.print("Sent: {s}\n", .{r});
return; return;
} else if (res.args.connect) |c| { } else if (res.args.connect) |c| {
const conn_res: ?SaprusMessage = SaprusClient.connect(if (c.len > 0) c else "Hello darkness my old friend", gpa) catch |err| switch (err) { _ = client.connect(if (c.len > 0) c else "Hello darkness my old friend") catch |err| switch (err) {
error.WouldBlock => null, error.WouldBlock => null,
else => return err, else => return err,
}; };
defer if (conn_res) |r| r.deinit(gpa);
if (conn_res) |r| {
std.debug.print("{s}\n", .{r.connection.payload});
} else {
std.debug.print("No response from connection request\n", .{});
}
return; return;
} }
return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{}); return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{});
} }
fn parseDest(in: ?[]const u8) ![4]u8 { fn parseDest(in: ?[]const u8) [4]u8 {
if (in) |dest| { if (in) |dest| {
if (dest.len <= 4) { if (dest.len <= 4) {
var res: [4]u8 = @splat(0); var res: [4]u8 = @splat(0);
@@ -83,10 +76,10 @@ fn parseDest(in: ?[]const u8) ![4]u8 {
return res; return res;
} }
const addr = try std.net.Ip4Address.parse(dest, 0); const addr = std.net.Ip4Address.parse(dest, 0) catch return "FAIL".*;
return @bitCast(addr.sa.addr); return @bitCast(addr.sa.addr);
} }
return .{ 70, 70, 70, 70 }; return "zap\x00".*;
} }
const builtin = @import("builtin"); const builtin = @import("builtin");

View File

@@ -1,6 +1,3 @@
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. /// Type tag for Message union.
/// This is the first value in the actual packet sent over the network. /// This is the first value in the actual packet sent over the network.
pub const PacketType = enum(u16) { pub const PacketType = enum(u16) {
@@ -23,155 +20,255 @@ pub const ConnectionOptions = packed struct(u8) {
opt8: bool = false, opt8: bool = false,
}; };
pub const Error = error{ pub const MessageTypeError = error{
NotImplementedSaprusType, NotImplementedSaprusType,
UnknownSaprusType, 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 /// All Saprus messages
pub const Message = union(PacketType) { pub const Message = packed struct {
pub const Relay = struct { const Relay = packed struct {
pub const Header = packed struct {
dest: @Vector(4, u8), 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];
}
}; };
header: Header, const Connection = packed struct {
payload: []const u8,
};
pub const Connection = struct {
pub const Header = packed struct {
src_port: u16, // random number > 1024 src_port: u16, // random number > 1024
dest_port: u16, // random number > 1024 dest_port: u16, // random number > 1024
seq_num: u32 = 0, seq_num: u32 = 0,
msg_id: u32 = 0, msg_id: u32 = 0,
reserved: u8 = 0, reserved: u8 = 0,
options: ConnectionOptions = .{}, options: ConnectionOptions = .{},
}; payload: void,
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 getPayload(self: *align(1) Connection) []u8 {
pub fn deinit(self: Message, allocator: Allocator) void { const len: *u16 = @ptrFromInt(@intFromPtr(self) - @sizeOf(u16));
switch (self) { return @as([*]u8, @ptrCast(&self.payload))[0 .. len.* - @bitSizeOf(Connection) / 8];
.relay => |r| allocator.free(r.payload), }
.connection => |c| allocator.free(c.payload),
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, else => unreachable,
} }
} }
fn toBytesAux( /// Convert the message to network endianness from native endianness in-place.
header: anytype, pub fn networkFromNativeEndian(self: *Self) MessageTypeError!void {
payload: []const u8, try switch (try self.getSaprusTypePayload()) {
buf: *std.ArrayList(u8), .relay => {},
allocator: Allocator, .connection => |*con| con.*.networkFromNativeEndian(),
) !void { .file_transfer => MessageTypeError.NotImplementedSaprusType,
const Header = @TypeOf(header); else => MessageTypeError.UnknownSaprusType,
// 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. self.type = @enumFromInt(nativeToBig(
var payload_list = std.ArrayList(u8).init(allocator); @typeInfo(@TypeOf(self.type)).@"enum".tag_type,
defer payload_list.deinit(); @intFromEnum(self.type),
const buf_w = payload_list.writer(); ));
self.length = nativeToBig(@TypeOf(self.length), self.length);
// 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. /// Convert network endian bytes to a native endian value in-place.
pub fn toBytes(self: Message, allocator: Allocator) ![]u8 { pub fn networkBytesAsValue(bytes: SelfBytes) MessageParseError!*Self {
// Create a growable list of bytes to store the output in. const res = std.mem.bytesAsValue(Self, bytes);
var buf = std.ArrayList(u8).init(allocator); try res.nativeFromNetworkEndian();
errdefer buf.deinit(); return .bytesAsValue(bytes);
// 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. /// Create a structured view of the bytes without initializing the length or type,
return buf.toOwnedSlice(); /// 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,
};
} }
fn fromBytesAux( /// Deprecated.
comptime packet: PacketType, /// If I need the bytes, I should just pass around the slice that is backing this to begin with.
len: u16, pub fn asBytes(self: *Self) SelfBytes {
r: std.io.FixedBufferStream([]const u8).Reader, const size = @sizeOf(Self) + self.length;
allocator: Allocator, return @as([*]align(@alignOf(Self)) u8, @ptrCast(self))[0..size];
) !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,
}
} }
}; };
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 std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const asBytes = std.mem.asBytes; const asBytes = std.mem.asBytes;
const nativeToBig = std.mem.nativeToBig; const nativeToBig = std.mem.nativeToBig;
const bigToNative = std.mem.bigToNative;
test "Round trip Relay toBytes and fromBytes" { test "Round trip Relay toBytes and fromBytes" {
if (false) {
const gpa = std.testing.allocator; const gpa = std.testing.allocator;
const msg = Message{ const msg = Message{
.relay = .{ .relay = .{
@@ -187,9 +284,12 @@ test "Round trip Relay toBytes and fromBytes" {
defer from_bytes.deinit(gpa); defer from_bytes.deinit(gpa);
try std.testing.expectEqualDeep(msg, from_bytes); try std.testing.expectEqualDeep(msg, from_bytes);
}
return error.SkipZigTest;
} }
test "Round trip Connection toBytes and fromBytes" { test "Round trip Connection toBytes and fromBytes" {
if (false) {
const gpa = std.testing.allocator; const gpa = std.testing.allocator;
const msg = Message{ const msg = Message{
.connection = .{ .connection = .{
@@ -208,4 +308,10 @@ test "Round trip Connection toBytes and fromBytes" {
defer from_bytes.deinit(gpa); defer from_bytes.deinit(gpa);
try std.testing.expectEqualDeep(msg, from_bytes); try std.testing.expectEqualDeep(msg, from_bytes);
}
return error.SkipZigTest;
}
test {
std.testing.refAllDeclsRecursive(@This());
} }

View File

@@ -1,4 +1,4 @@
pub const Client = @import("Client.zig"); pub const Client = @import("Client.zig");
pub usingnamespace @import("message.zig"); pub const Connection = @import("Connection.zig");
pub usingnamespace @import("c_api.zig"); pub usingnamespace @import("message.zig");