From ae310a207534b33b7321dd2b9f423a73b1969159 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 14 Sep 2026 14:28:15 -0300 Subject: Add reusable HTTP transport, serial gateway, and WASM file browser --- app/client.zig | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 app/client.zig (limited to 'app/client.zig') diff --git a/app/client.zig b/app/client.zig new file mode 100644 index 0000000..2ec9163 --- /dev/null +++ b/app/client.zig @@ -0,0 +1,176 @@ +//! Browser ABI. Protocol state, encoding, reply validation, and directory decoding +//! all use the same cloud9 sources as native clients. No allocator or host imports. +const std = @import("std"); +const c9 = @import("cloud9"); +const capacity = 65536; +var input: [capacity]u8 = undefined; +var output: [capacity]u8 = undefined; +var staging: [capacity]u8 = undefined; +var json: [capacity * 2]u8 = undefined; +var client: c9.Client = undefined; +var data: []const u8 = ""; +var number: u32 = 0; +var directory: bool = false; + +export fn init() void { + client = .init(.{ .in = &input, .out = &output }); + data = ""; + number = 0; + directory = false; +} +export fn input_ptr() [*]u8 { + return &staging; +} +export fn input_capacity() u32 { + return capacity; +} +export fn output_ptr() [*]const u8 { + return client.output().ptr; +} +export fn output_len() u32 { + return @intCast(client.output().len); +} +export fn sent() void { + client.wrote(client.output().len); +} +export fn data_ptr() [*]const u8 { + return data.ptr; +} +export fn data_len() u32 { + return @intCast(data.len); +} +export fn result_number() u32 { + return number; +} +export fn result_directory() bool { + return directory; +} +export fn is_dead() bool { + return client.dead; +} +export fn max_read() u32 { + return client.maxRead(); +} +export fn max_write() u32 { + return client.maxWrite(); +} + +fn fail(message: []const u8) i32 { + data = message; + return -1; +} +fn submit(request: c9.Client.Request) i32 { + _ = client.submit(request) catch |err| return fail(@errorName(err)); + return 0; +} +export fn version() i32 { + return submit(.{ .version = .{} }); +} +export fn attach(user_len: u32, tree_len: u32) i32 { + if (user_len > capacity or tree_len > capacity - user_len) return fail("InputTooLarge"); + return submit(.{ .attach = .{ .fid = 0, .uname = staging[0..user_len], .aname = staging[user_len..][0..tree_len] } }); +} +export fn walk(fid: u32, newfid: u32, len: u32) i32 { + if (len > capacity) return fail("InputTooLarge"); + var names: [c9.max_welem][]const u8 = undefined; + var count: usize = 0; + var it = std.mem.splitScalar(u8, staging[0..len], '/'); + while (it.next()) |name| { + if (name.len == 0) continue; + if (count == names.len) return fail("TooManyNames"); + names[count] = name; + count += 1; + } + return submit(.{ .walk = .{ .fid = fid, .newfid = newfid, .names = names[0..count] } }); +} +export fn open(fid: u32, mode: u8) i32 { + return submit(.{ .open = .{ .fid = fid, .mode = mode } }); +} +export fn read(fid: u32, offset: u64, count: u32) i32 { + return submit(.{ .read = .{ .fid = fid, .offset = offset, .count = count } }); +} +export fn write(fid: u32, offset: u64, len: u32) i32 { + if (len > capacity) return fail("InputTooLarge"); + return submit(.{ .write = .{ .fid = fid, .offset = offset, .data = staging[0..len] } }); +} +export fn clunk(fid: u32) i32 { + return submit(.{ .clunk = .{ .fid = fid } }); +} +export fn stat(fid: u32) i32 { + return submit(.{ .stat = .{ .fid = fid } }); +} + +fn writeStat(s: c9.Stat, writer: *std.Io.Writer) !void { + try std.json.Stringify.value(.{ .name = s.name, .directory = s.qid.type & c9.qtdir != 0, .length = s.length, .mode = s.mode }, .{}, writer); +} + +/// Result codes: 0 incomplete, -1 error, 1 version, 2 attach, 3 walk, +/// 4 open, 5 read, 6 write, 7 clunk, 8 stat. Data is borrowed until next call. +export fn receive(len: u32) i32 { + if (len > capacity) return fail("InputTooLarge"); + if (client.push(staging[0..len]) != len) return fail("InputFull"); + const done = client.take() orelse return if (client.dead) fail("ProtocolError") else 0; + data = ""; + number = 0; + directory = false; + return switch (done.result) { + .fail => |message| fail(message), + .version => |v| blk: { + data = v.version; + number = v.msize; + break :blk 1; + }, + .attach => |qid| blk: { + directory = qid.type & c9.qtdir != 0; + break :blk 2; + }, + .walk => |w| blk: { + number = w.nwqid; + if (w.nwqid != 0) directory = w.wqid[w.nwqid - 1].type & c9.qtdir != 0; + break :blk 3; + }, + .open => |o| blk: { + number = o.iounit; + directory = o.qid.type & c9.qtdir != 0; + break :blk 4; + }, + .read => |bytes| blk: { + data = bytes; + break :blk 5; + }, + .write => |count| blk: { + number = count; + break :blk 6; + }, + .clunk => 7, + .stat => |s| blk: { + var writer: std.Io.Writer = .fixed(&json); + writeStat(s, &writer) catch return fail("OutputTooLarge"); + data = writer.buffered(); + break :blk 8; + }, + else => fail("UnexpectedReply"), + }; +} + +/// Parse complete directory stat records from one Rread, using cloud9.Stat. +export fn decode_directory(len: u32) i32 { + if (len > capacity) return fail("InputTooLarge"); + var remaining: []const u8 = staging[0..len]; + var writer: std.Io.Writer = .fixed(&json); + writer.writeByte('[') catch unreachable; + var first = true; + while (remaining.len != 0) { + if (remaining.len < 2) return fail("TruncatedDirectory"); + const size: usize = @as(usize, std.mem.readInt(u16, remaining[0..2], .little)) + 2; + if (size > remaining.len) return fail("TruncatedDirectory"); + const s = c9.Stat.decode(remaining[0..size]) catch return fail("InvalidDirectory"); + if (!first) writer.writeByte(',') catch return fail("OutputTooLarge"); + writeStat(s, &writer) catch return fail("OutputTooLarge"); + first = false; + remaining = remaining[size..]; + } + writer.writeByte(']') catch return fail("OutputTooLarge"); + data = writer.buffered(); + return 0; +} -- cgit v1.3