summaryrefslogtreecommitdiff
path: root/web/client.zig
diff options
context:
space:
mode:
Diffstat (limited to 'web/client.zig')
-rw-r--r--web/client.zig176
1 files changed, 176 insertions, 0 deletions
diff --git a/web/client.zig b/web/client.zig
new file mode 100644
index 0000000..2ec9163
--- /dev/null
+++ b/web/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;
+}