diff options
| -rw-r--r-- | build.zig.zon | 4 | ||||
| -rw-r--r-- | docs/cloud9.md | 16 | ||||
| -rw-r--r-- | docs/fs.md | 14 | ||||
| -rw-r--r-- | src/9p.zig | 2311 | ||||
| -rw-r--r-- | src/9p_io.zig | 2 | ||||
| -rw-r--r-- | src/esp32p4_9p.zig | 2 | ||||
| -rw-r--r-- | src/esp32p4_gpio.zig | 54 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 100 | ||||
| -rw-r--r-- | src/pardes.zig | 2 |
9 files changed, 90 insertions, 2415 deletions
diff --git a/build.zig.zon b/build.zig.zon index 79c2a261..5bf0d5dc 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,8 +9,8 @@ // read-only HTTPS URL is what a manifest can carry. Re-pin with // `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`. .cloud9 = .{ - .url = "git+https://git.sr.ht/~gbrls/cloud9#ae310a207534b33b7321dd2b9f423a73b1969159", - .hash = "cloud9-0.1.0-yt86qsv9AwAy7XqxowpkCFum_p_xfl4S74L8KIeVz4j9", + .url = "git+https://git.sr.ht/~gbrls/cloud9#65209217b5b68f56bc0bd5bc6c4dce33911ded59", + .hash = "cloud9-0.1.0-yt86qs3eEQDqC8duRUGBEWOLj82-iCpAb_afpu0KPjPB", }, // ZLS as a LIBRARY, not a language server: src/lsp_zls.zig imports the // `zls` module its build.zig publishes and calls the analyser in diff --git a/docs/cloud9.md b/docs/cloud9.md index e18130ef..d31d2cb3 100644 --- a/docs/cloud9.md +++ b/docs/cloud9.md @@ -7,14 +7,18 @@ other dependency; no sibling checkout is required. Re-pin with `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`, and swap in `.cloud9 = .{ .path = "../cloud9" }` while editing both packages at once. -`src/9p.zig` remains the adapter between cloud9 requests and Pardes filesystem -operations. Mounting, Unix namespace discovery and permissions, the editor event -loop, connection limits, and exported tree policy remain here. `src/9p_quic.zig` +The file-server engine (fids, jobs, parking, flush, hangup) is cloud9's +`fs.Server`; the control tree in `src/ninep/` is its backend, using cloud9's +`fs.Req`, `fs.Op`, `fs.Status`, `fs.E` and `fs.ReplyWith` (extended with the +editor's reply payload locator). `src/9p.zig` names the editor's and the +board's `fs.Options` and re-exports the wire names the transports use. +Mounting, Unix namespace discovery and permissions, the editor event loop, +connection limits, and exported tree policy remain here. `src/9p_quic.zig` selects the existing `pardes-9p` ALPN for cloud9's optional OpenSSL transport. -The snapshot client and standalone protocol/GPIO tests import the same module. -The separate `05-zig-p4` build also supplies cloud9 for the GPIO firmware entry. +The standalone protocol/GPIO tests import the same module. The separate +`05-zig-p4` build also supplies cloud9 for the GPIO firmware entry. -Run `zig build 9p-test` for the filesystem adapter and +Run `zig build 9p-test` for the engine configurations and `zig build 9p-io-test -Dquic=true` for native transport/client integration. Run cloud9's `zig build test`, `transport-test`, `quic-test -Dquic=true`, `fuzz`, and `differential` steps for the shared implementation. See cloud9's `docs/validation.md` for recorded runs and @@ -137,15 +137,17 @@ Zero-length truncation accepts the accompanying `mtime` hint sent by Linux v9fs; the hint is not stored. Standalone timestamp changes remain refused. The tree lives in `src/ninep/`: `tree.zig` (nodes, lookup, readdir, dispatch, -the backend contract), `pane.zig` (pane files), `ctl.zig`, `addr.zig`, -`pty.zig`, `events.zig` (event and log streams), `screen.zig` and -`sources.zig`. The protocol engine is `src/9p.zig`, the transports -`src/9p_io.zig`; `src/fs.zig` keeps host access, mounts, resolution, find and -grep. +and the editor's reply payload over cloud9's backend contract), `pane.zig` +(pane files), `ctl.zig`, `addr.zig`, `pty.zig`, `events.zig` (event and log +streams), `screen.zig` and `sources.zig`. The protocol engine is cloud9's +`fs.Server`, configured in `src/9p.zig` (the editor's and the board's +capacities); the transports are `src/9p_io.zig`; `src/fs.zig` keeps host +access, mounts, resolution, find and grep. `zig build fs-test` drives real sessions using the independent Python client in `test/ninep.py`; `zig build fs-discovery-test` checks that browsing creates nothing and that `new`, `ctl`, `name`, `sel` and `log` behave. `zig build -9p-test` checks the freestanding wire protocol; `zig build fs-bench` measures +9p-test` checks the two engine configurations' budgets (the engine's own +tests are cloud9's `zig build test`); `zig build fs-bench` measures filesystem transactions in the core. `zig build fs-test quic-test -Dquic=true` also exercises QUIC mounts and I/O. @@ -1,8 +1,10 @@ -//! Pardes filesystem adapter. The 9P protocol and transports live in cloud9. -const std = @import("std"); -const assert = std.debug.assert; -const testing = std.testing; +//! The editor's 9P surface. The wire protocol, the client, the transports +//! and the file-server engine (`cloud9.fs.Server`) live in cloud9; this +//! module names the two engine configurations Pardes builds, the editor's +//! and the board's, and re-exports what the transports and firmware use. const cloud9 = @import("cloud9"); +pub const fs = cloud9.fs; +pub const Server = cloud9.fs.Server; pub const Error = cloud9.Error; pub const Type = cloud9.Type; pub const isT = cloud9.isT; @@ -37,2299 +39,50 @@ pub const dmperm = cloud9.dmperm; pub const Client = cloud9.Client; pub const ClientError = cloud9.ClientError; pub const max_tags = cloud9.max_tags; -const totalLen = cloud9.encodedLen; -pub const errmax: usize = 128; -pub const min_msize: u32 = 4096; -const sample_qid: Qid = .{ .type = qtdir, .version = 3, .path = 0x0102_0304_0506_0708 }; - -const sample_stat: Stat = .{ - .type = 0, - .dev = 0, - .qid = sample_qid, - .mode = dmdir | 0o755, - .atime = 1, - .mtime = 2, - .length = 0, - .name = "body", - .uid = "goblin", - .gid = "goblin", - .muid = "goblin", -}; +pub const oread = cloud9.oread; +pub const owrite = cloud9.owrite; +pub const ordwr = cloud9.ordwr; +pub const oexec = cloud9.oexec; +pub const otrunc = cloud9.otrunc; +pub const ocexec = cloud9.ocexec; +pub const orclose = cloud9.orclose; +pub const errmax = cloud9.fs.errmax; +pub const errString = cloud9.fs.errString; +pub const msize_min = cloud9.fs.msize_min; -pub const e_unknown_fid = "fid unknown or out of range"; -pub const e_fid_in_use = "fid already in use"; -pub const e_bad_use = "bad use of fid"; -pub const e_bad_offset = "bad offset in directory read"; -pub const e_perm = "permission denied"; -pub const e_not_dir = "not a directory"; -pub const e_already_open = "file already open for I/O"; -pub const e_illegal_name = "illegal name"; -pub const e_too_many_fids = "Too many open files in system"; -pub const e_botch = "protocol botch"; -pub const e_interrupted = "Interrupted system call"; -pub const e_trunc_only = "only support truncation to zero length"; -pub const e_wstat = "wstat prohibited"; -pub const e_again = "Resource temporarily unavailable"; -pub const e_count_small = "Invalid argument"; -pub const e_no_tree = "No such file or directory"; -pub const e_no_auth = "authentication not required"; -pub const e_small_msize = "Invalid argument"; - -pub fn errString(errno: u16) []const u8 { - return switch (errno) { - 1 => "Operation not permitted", // E.PERM, EPERM - 2 => "No such file or directory", // E.NOENT, ENOENT - 5 => "Input/output error", // E.IO, EIO - 12 => "Cannot allocate memory", // E.NOMEM, ENOMEM - 20 => "Not a directory", // E.NOTDIR, ENOTDIR - 22 => "Invalid argument", // E.INVAL, EINVAL - 23 => "Too many open files in system", // E.NFILE, ENFILE - 28 => "No space left on device", // E.NOSPC, ENOSPC - 38 => "Function not implemented", // E.NOSYS, ENOSYS - else => "Input/output error", - }; -} +/// The smallest msize a native listener offers (src/9p_io.zig). +pub const min_msize: u32 = 4096; -pub const oread: u8 = 0; -pub const owrite: u8 = 1; -pub const ordwr: u8 = 2; -pub const oexec: u8 = 3; -pub const otrunc: u8 = 16; -pub const ocexec: u8 = 32; -pub const orclose: u8 = 64; +/// The editor's engine: native fid count, native file names. +pub const editor: cloud9.fs.Options = .{ .fid_capacity = max_fids, .name_capacity = 255 }; +/// The board's engine: the GPIO firmware's budget (src/esp32p4_9p.zig). +pub const board: cloud9.fs.Options = .{ .fid_capacity = board_fids, .name_capacity = board_name_capacity }; pub const max_fids: usize = 256; - pub const board_fids: usize = 32; - -pub const max_slots: usize = 32; - -pub const park_data_max: usize = 128; - pub const board_name_capacity: usize = 28; -const username_capacity: usize = 28; - -pub const msize_min: u32 = header_len + 2 + max_welem * qid_len; - -comptime { - assert(msize_min == 217); - assert(header_len + 2 + stat_fixed + board_name_capacity + 3 * username_capacity <= msize_min); - assert(header_len + 4 + stat_fixed + board_name_capacity + 3 * username_capacity <= msize_min); - assert(username_capacity >= 20); - assert(oread | owrite | ordwr | oexec == 3); - assert(otrunc | ocexec | orclose == 112); -} - -fn qidOf(node: u64, dir: bool) Qid { - return .{ .type = if (dir) qtdir else qtfile, .version = 0, .path = node }; -} - -pub const dirent_dir_perm: u16 = 0o500; -pub const dirent_file_perm: u16 = 0o600; - -pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { - if (fid_capacity == 0) @compileError("9P server needs at least one fid"); - const name_capacity = if (@hasDecl(fs, "name_capacity")) fs.name_capacity else board_name_capacity; - if (name_capacity == 0 or name_capacity > 255) @compileError("9P backend name capacity must fit a directory entry"); - return struct { - const Self = @This(); - - protocol: cloud9.Server, - root: u64, - - uname: [username_capacity]u8 = @splat(0), - uname_len: u8 = 0, - - fids: [fid_capacity]Fid = @splat(.{}), - slots: [max_slots]Slot = @splat(.{}), - job: Job = .{}, - seq: u64 = 0, - - const Kind = enum { none, attach, walk, open, read, readdir, write, clunk, remove, stat, wstat }; - - const Fid = struct { - used: bool = false, - fid: u32 = 0, - node: u64 = 0, - dir: bool = false, - perm: u16 = 0, - open: bool = false, - omode: u8 = 0, - handle: u32 = 0, - diroff: u64 = 0, - dirindex: u32 = 0, - orphan: bool = false, - name: [name_capacity]u8 = @splat(0), - name_len: u8 = 0, - }; - - const Slot = struct { - used: bool = false, - parked: bool = false, - retried: bool = false, - copied: bool = false, - seq: u64 = 0, - tag: u16 = 0, - kind: Kind = .none, - fid: u32 = 0, - count: u32 = 0, - req: fs.Req = undefined, - data: [park_data_max]u8 = undefined, - }; - - const Job = struct { - kind: Kind = .none, - tag: u16 = 0, - req_tag: u64 = 0, - req: fs.Req = undefined, - step: u8 = 0, - fid: u32 = 0, - newfid: u32 = 0, - count: u32 = 0, - offset: u64 = 0, - omode: u8 = 0, - node: u64 = 0, - dir: bool = false, - perm: u16 = 0, - name: [name_capacity]u8 = @splat(0), - name_len: u8 = 0, - nwname: u8 = 0, - nwqid: u8 = 0, - wqid: [max_welem]Qid = @splat(.{ .type = 0, .version = 0, .path = 0 }), - msg: Msg = .rflush, - }; - - pub const Options = struct { - in: []u8, - out: []u8, - root: u64, - }; - - pub fn init(opts: Options) Self { - assert(opts.in.len >= msize_min); - assert(opts.out.len >= 2 * msize_min); - assert(opts.root != 0); - return .{ .protocol = .init(.{ .in = opts.in, .out = opts.out }), .root = opts.root }; - } - - pub fn references(s: *const Self, node: u64) bool { - for (s.fids) |fid| if (fid.used and fid.node == node) return true; - if (s.job.kind != .none and s.job.node == node) return true; - for (s.slots) |slot| if (slot.used and slot.req.node == node) return true; - return false; - } - - pub fn hangup(s: *Self) void { - s.reset(); - s.protocol.hangup(); - } - - fn reset(s: *Self) void { - for (&s.fids) |*f| { - if (!f.used) continue; - if (f.open) f.orphan = true else f.* = .{}; - } - for (&s.slots) |*sl| sl.* = .{}; - s.job = .{}; - } - - pub fn push(s: *Self, bytes: []const u8) usize { - return s.protocol.push(bytes); - } - - pub fn output(s: *const Self) []const u8 { - return s.protocol.output(); - } - - pub fn wrote(s: *Self, n: usize) void { - s.protocol.wrote(n); - } - - fn hasRoom(s: *Self) bool { - return s.protocol.hasRoom(); - } - - fn emit(s: *Self, tag: u16, msg: Msg) void { - s.protocol.reply(tag, msg) catch { - s.protocol.dead = true; - }; - } - - fn fail(s: *Self, tag: u16, ename: []const u8) void { - assert(ename.len <= errmax); - s.emit(tag, .{ .rerror = .{ .ename = ename } }); - } - - fn tick(s: *Self) u64 { - s.seq += 1; - return s.seq; - } - - fn findFid(s: *Self, fid: u32) ?usize { - for (&s.fids, 0..) |*f, i| if (f.used and !f.orphan and f.fid == fid) return i; - return null; - } - - fn freeFid(s: *Self) ?usize { - for (&s.fids, 0..) |*f, i| if (!f.used) return i; - return null; - } - - fn dropFid(s: *Self, fid: u32) void { - if (s.findFid(fid)) |i| s.fids[i] = .{}; - } - - fn findSlot(s: *Self, req_tag: u64) ?usize { - for (&s.slots, 0..) |*sl, i| if (sl.used and sl.req.tag == req_tag) return i; - return null; - } - - fn freeSlot(s: *Self) ?usize { - for (&s.slots, 0..) |*sl, i| if (!sl.used) return i; - return null; - } - - fn findTag(s: *Self, tag: u16) ?usize { - for (&s.slots, 0..) |*sl, i| if (sl.used and sl.tag == tag) return i; - return null; - } - - fn setUname(s: *Self, uname: []const u8) void { - const n = @min(uname.len, username_capacity); - @memcpy(s.uname[0..n], uname[0..n]); - s.uname_len = @intCast(n); - } - - pub fn retry(s: *Self) ?fs.Req { - var best: ?usize = null; - for (&s.slots, 0..) |*sl, i| { - if (!sl.used or !sl.parked or sl.retried) continue; - if (best == null or sl.seq < s.slots[best.?].seq) best = i; - } - const i = best orelse { - for (&s.slots) |*sl| sl.retried = false; - return null; - }; - if (!s.hasRoom()) { - for (&s.slots) |*sl| sl.retried = false; - return null; - } - s.slots[i].retried = true; - s.slots[i].parked = false; - return s.slots[i].req; - } - - pub fn next(s: *Self) ?fs.Req { - while (true) { - if (s.job.kind != .none) { - if (s.job.req_tag != 0) return null; - if (s.stepJob()) |req| return req; - assert(s.job.kind == .none); - continue; - } - if (s.orphan()) |req| return req; - if (!s.hasRoom()) return null; - if (!s.startFrame()) return null; - } - } - - pub fn reply(s: *Self, r: *const fs.Reply, bytes: []const u8) void { - if (s.job.kind != .none and s.job.req_tag == r.tag) return s.jobReply(r, bytes); - if (s.findSlot(r.tag)) |i| return s.slotReply(i, r, bytes); - } - - fn orphan(s: *Self) ?fs.Req { - for (&s.fids) |*f| { - if (!f.used or !f.orphan) continue; - assert(f.open); - const req: fs.Req = .{ - .tag = s.tick(), - .op = .release, - .node = f.node, - .handle = f.handle, - }; - f.* = .{}; - return req; - } - return null; - } - - fn startFrame(s: *Self) bool { - assert(s.job.kind == .none); - const got = (s.protocol.receive() catch return false) orelse return false; - defer if (s.job.kind == .none) s.dropFrame(); - s.dispatch(got); - return true; - } - - fn dropFrame(s: *Self) void { - s.protocol.release(); - } - - fn dispatch(s: *Self, got: Decoded) void { - switch (got.msg) { - .tversion => |m| s.version(got.tag, m.msize, m.version), - .tauth => s.fail(got.tag, e_no_auth), - .tcreate => s.fail(got.tag, e_perm), - .tattach => |m| s.attach(got.tag, m.fid, m.uname, m.aname), - .tflush => |m| s.flush(got.tag, m.oldtag), - .twalk => |m| s.walk(got, m.fid, m.newfid, @intCast(m.nwname)), - .topen => |m| s.open(got.tag, m.fid, m.mode), - .tread => |m| s.read(got.tag, m.fid, m.offset, m.count), - .twrite => |m| s.write(got, m.fid, m.offset, m.data.len), - .tclunk => |m| s.clunk(got.tag, m.fid, .clunk), - .tremove => |m| s.clunk(got.tag, m.fid, .remove), - .tstat => |m| s.stat(got.tag, m.fid), - .twstat => |m| s.wstat(got.tag, m.fid, m.stat), - else => s.fail(got.tag, e_botch), - } - } - - fn version(s: *Self, tag: u16, want: u32, ver: []const u8) void { - assert(tag == notag); - s.reset(); - // The adapter's fixed directory staging requires this capacity. - if (want < msize_min) { - s.protocol.dead = true; - return; - } - s.protocol.negotiate(want, ver) catch { - s.protocol.dead = true; - }; - } - - fn attach(s: *Self, tag: u16, fid: u32, uname: []const u8, aname: []const u8) void { - if (aname.len != 0) return s.fail(tag, e_no_tree); - if (fid == nofid) return s.fail(tag, e_unknown_fid); - if (s.findFid(fid) != null) return s.fail(tag, e_fid_in_use); - if (s.freeFid() == null) return s.fail(tag, e_too_many_fids); - s.setUname(uname); - s.job = .{ .kind = .attach, .tag = tag, .fid = fid, .node = s.root }; - } - - fn walk(s: *Self, got: Decoded, fid: u32, newfid: u32, nwname: u8) void { - const tag = got.tag; - const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); - if (s.fids[i].open) return s.fail(tag, e_bad_use); - if (newfid == nofid) return s.fail(tag, e_unknown_fid); - if (newfid != fid) { - if (s.findFid(newfid) != null) return s.fail(tag, e_fid_in_use); - if (s.freeFid() == null) return s.fail(tag, e_too_many_fids); - } - if (nwname == 0) { - if (newfid != fid) { - const j = s.freeFid().?; - s.fids[j] = s.fids[i]; - s.fids[j].fid = newfid; - s.fids[j].diroff = 0; - s.fids[j].dirindex = 0; - } - s.emit(tag, .{ .rwalk = .{ .nwqid = 0 } }); - return; - } - if (!s.fids[i].dir) return s.fail(tag, e_not_dir); - s.job = .{ - .kind = .walk, - .tag = tag, - .fid = fid, - .newfid = newfid, - .nwname = nwname, - .node = s.fids[i].node, - .dir = s.fids[i].dir, - .perm = s.fids[i].perm, - .name = s.fids[i].name, - .name_len = s.fids[i].name_len, - .msg = got.msg, - }; - } - - fn open(s: *Self, tag: u16, fid: u32, mode: u8) void { - const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); - const f = &s.fids[i]; - if (f.open) return s.fail(tag, e_already_open); - if (mode & orclose != 0) return s.fail(tag, e_perm); - const rw = mode & 3; - if (rw == oexec) return s.fail(tag, e_perm); - if (f.dir and (rw != oread or mode & otrunc != 0)) return s.fail(tag, e_perm); - var need: u16 = 0; - if (rw == oread or rw == ordwr) need |= 0o400; - if (rw == owrite or rw == ordwr or mode & otrunc != 0) need |= 0o200; - if (f.perm & need != need) return s.fail(tag, e_perm); - s.job = .{ .kind = .open, .tag = tag, .fid = fid, .omode = mode }; - } - - fn read(s: *Self, tag: u16, fid: u32, offset: u64, count: u32) void { - const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); - const f = &s.fids[i]; - if (!f.open or (f.omode & 3) == owrite) return s.fail(tag, e_bad_use); - const want = @min(count, s.protocol.msize - header_len - 4); - if (!f.dir) { - s.job = .{ .kind = .read, .tag = tag, .fid = fid, .offset = offset, .count = want }; - return; - } - if (offset != f.diroff) { - if (offset != 0) return s.fail(tag, e_bad_offset); - f.diroff = 0; - f.dirindex = 0; - } - s.job = .{ .kind = .readdir, .tag = tag, .fid = fid, .offset = offset, .count = want }; - } - - fn write(s: *Self, got: Decoded, fid: u32, offset: u64, len: usize) void { - const tag = got.tag; - const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); - const f = &s.fids[i]; - if (!f.open or (f.omode & 3) == oread) return s.fail(tag, e_bad_use); - s.job = .{ - .kind = .write, - .tag = tag, - .fid = fid, - .offset = offset, - .count = @intCast(len), - .msg = got.msg, - }; - } - - fn clunk(s: *Self, tag: u16, fid: u32, kind: Kind) void { - assert(kind == .clunk or kind == .remove); - const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); - if (s.fids[i].open) { - s.job = .{ .kind = kind, .tag = tag, .fid = fid }; - return; - } - s.fids[i] = .{}; - if (kind == .remove) s.fail(tag, e_perm) else s.emit(tag, .rclunk); - } - - fn stat(s: *Self, tag: u16, fid: u32) void { - if (s.findFid(fid) == null) return s.fail(tag, e_unknown_fid); - s.job = .{ .kind = .stat, .tag = tag, .fid = fid }; - } - - fn wstat(s: *Self, tag: u16, fid: u32, st: Stat) void { - if (s.findFid(fid) == null) return s.fail(tag, e_unknown_fid); - if (st.type != std.math.maxInt(u16) or st.dev != std.math.maxInt(u32) or - st.qid.type != std.math.maxInt(u8) or st.qid.version != std.math.maxInt(u32) or - st.qid.path != std.math.maxInt(u64) or st.mode != std.math.maxInt(u32) or - st.atime != std.math.maxInt(u32) or - // Linux v9fs follows O_TRUNC with Twstat(length=0, mtime=now). - // Accept that timestamp hint with truncation; this control - // filesystem does not persist caller-selected timestamps. - (st.mtime != std.math.maxInt(u32) and st.length != 0) or - st.name.len != 0 or st.uid.len != 0 or st.gid.len != 0 or st.muid.len != 0) - return s.fail(tag, e_wstat); - if (st.length == std.math.maxInt(u64)) { - s.emit(tag, .rwstat); - return; - } - if (st.length != 0) return s.fail(tag, e_trunc_only); - s.job = .{ .kind = .wstat, .tag = tag, .fid = fid }; - } - - fn flush(s: *Self, tag: u16, oldtag: u16) void { - if (s.findTag(oldtag)) |i| { - s.fail(s.slots[i].tag, e_interrupted); - s.slots[i] = .{}; - } - s.emit(tag, .rflush); - } - - fn ask(s: *Self, req: fs.Req) fs.Req { - assert(req.tag != 0); - s.job.req = req; - s.job.req_tag = req.tag; - return req; - } - - fn jobFid(s: *Self) ?*Fid { - const i = s.findFid(s.job.fid) orelse { - s.fail(s.job.tag, e_unknown_fid); - s.finishJob(); - return null; - }; - return &s.fids[i]; - } - - fn stepJob(s: *Self) ?fs.Req { - const j = &s.job; - assert(j.kind != .none); - assert(j.req_tag == 0); - switch (j.kind) { - .none => unreachable, - .attach => return s.ask(.{ .tag = s.tick(), .op = .getattr, .node = s.root }), - .walk => return s.stepWalk(), - .open => { - const f = s.jobFid() orelse return null; - if (j.step == 0 and j.omode & otrunc != 0) return s.ask(.{ - .tag = s.tick(), - .op = .setattr, - .node = f.node, - .truncate = true, - }); - return s.ask(.{ .tag = s.tick(), .op = .open, .node = f.node }); - }, - .read => { - const f = s.jobFid() orelse return null; - return s.ask(.{ - .tag = s.tick(), - .op = .read, - .node = f.node, - .handle = f.handle, - .off = j.offset, - .size = j.count, - }); - }, - .readdir => { - const f = s.jobFid() orelse return null; - return s.ask(.{ - .tag = s.tick(), - .op = .readdir, - .node = f.node, - .handle = f.handle, - .off = f.dirindex, - .size = j.count, - }); - }, - .write => { - const f = s.jobFid() orelse return null; - return s.ask(.{ - .tag = s.tick(), - .op = .write, - .node = f.node, - .handle = f.handle, - .off = j.offset, - .size = j.count, - .data = j.msg.twrite.data, - }); - }, - .clunk, .remove => { - const f = s.jobFid() orelse return null; - return s.ask(.{ - .tag = s.tick(), - .op = .release, - .node = f.node, - .handle = f.handle, - }); - }, - .stat => { - const f = s.jobFid() orelse return null; - return s.ask(.{ .tag = s.tick(), .op = .getattr, .node = f.node }); - }, - .wstat => { - const f = s.jobFid() orelse return null; - return s.ask(.{ .tag = s.tick(), .op = .setattr, .node = f.node, .truncate = true }); - }, - } - } - - fn stepWalk(s: *Self) ?fs.Req { - const j = &s.job; - while (j.step < j.nwname) { - const name = j.msg.twalk.wname[j.step]; - if (name.len > name_capacity) { - s.stopWalk(e_illegal_name); - return null; - } - if (std.mem.eql(u8, name, ".")) { - j.wqid[j.nwqid] = qidOf(j.node, j.dir); - j.nwqid += 1; - j.step += 1; - continue; - } - @memcpy(j.name[0..name.len], name); - j.name_len = @intCast(name.len); - return s.ask(.{ .tag = s.tick(), .op = .lookup, .node = j.node, .data = name }); - } - const dst = pick: { - if (j.newfid == j.fid) break :pick s.findFid(j.fid) orelse { - s.fail(j.tag, e_unknown_fid); - s.finishJob(); - return null; - }; - break :pick s.freeFid() orelse { - s.fail(j.tag, e_too_many_fids); - s.finishJob(); - return null; - }; - }; - s.fids[dst] = .{ - .used = true, - .fid = j.newfid, - .node = j.node, - .dir = j.dir, - .perm = j.perm, - .name = j.name, - .name_len = j.name_len, - }; - s.emit(j.tag, .{ .rwalk = .{ .nwqid = j.nwqid, .wqid = j.wqid } }); - s.finishJob(); - return null; - } - - fn stopWalk(s: *Self, ename: []const u8) void { - const j = &s.job; - if (j.nwqid == 0) - s.fail(j.tag, ename) - else - s.emit(j.tag, .{ .rwalk = .{ .nwqid = j.nwqid, .wqid = j.wqid } }); - s.finishJob(); - } - - fn finishJob(s: *Self) void { - s.job = .{}; - if (s.protocol.frame != 0) s.dropFrame(); - } - - fn jobReply(s: *Self, r: *const fs.Reply, bytes: []const u8) void { - const j = &s.job; - assert(j.kind != .none); - assert(j.req_tag == r.tag); - j.req_tag = 0; - - if (j.kind == .clunk or j.kind == .remove) { - s.dropFid(j.fid); - if (j.kind == .remove) s.fail(j.tag, e_perm) else s.emit(j.tag, .rclunk); - s.finishJob(); - return; - } - if (r.status == .again) return s.parkJob(); - if (r.status == .err) { - const ename = errString(r.errno); - if (j.kind == .walk) return s.stopWalk(ename); - s.fail(j.tag, ename); - s.finishJob(); - return; - } - - switch (j.kind) { - .none, .clunk, .remove => unreachable, - .attach => { - const i = s.freeFid() orelse { - s.fail(j.tag, e_too_many_fids); - s.finishJob(); - return; - }; - const node = if (r.attr.node != 0) r.attr.node else s.root; - s.fids[i] = .{ - .used = true, - .fid = j.fid, - .node = node, - .dir = r.attr.dir, - .perm = r.attr.mode, - }; - s.fids[i].name[0] = '/'; - s.fids[i].name_len = 1; - s.emit(j.tag, .{ .rattach = .{ .qid = qidOf(node, r.attr.dir) } }); - s.finishJob(); - }, - .walk => { - if (r.attr.node != 0) j.node = r.attr.node; - if (comptime @hasField(@TypeOf(r.attr), "name")) { - if (r.attr.name.len != 0) { - j.name_len = @intCast(@min(r.attr.name.len, j.name.len)); - @memcpy(j.name[0..j.name_len], r.attr.name[0..j.name_len]); - } - } - j.dir = r.attr.dir; - j.perm = r.attr.mode; - j.wqid[j.nwqid] = qidOf(j.node, j.dir); - j.nwqid += 1; - j.step += 1; - }, - .open => { - if (j.step == 0 and j.omode & otrunc != 0) { - j.step = 1; - return; - } - const f = s.jobFid() orelse return; - if (r.attr.node != 0) f.node = r.attr.node; - f.open = true; - f.omode = j.omode; - f.handle = r.handle; - f.diroff = 0; - f.dirindex = 0; - s.emit(j.tag, .{ .ropen = .{ - .qid = qidOf(f.node, f.dir), - .iounit = s.protocol.msize - iohdrsz, - } }); - s.finishJob(); - }, - .read => { - s.emit(j.tag, .{ .rread = .{ .data = bytes[0..@min(bytes.len, j.count)] } }); - s.finishJob(); - }, - .readdir => { - s.emitDirRead(j.tag, j.fid, bytes, j.count); - s.finishJob(); - }, - .write => { - s.emit(j.tag, .{ .rwrite = .{ .count = @min(r.written, j.count) } }); - s.finishJob(); - }, - .stat => { - const f = s.jobFid() orelse return; - f.perm = r.attr.mode; - f.dir = r.attr.dir; - const response: Msg = .{ .rstat = .{ .stat = s.statOf(f, r.attr) } }; - if ((totalLen(response) catch unreachable) > s.protocol.msize) - s.fail(j.tag, e_small_msize) - else - s.emit(j.tag, response); - s.finishJob(); - }, - .wstat => { - s.emit(j.tag, .rwstat); - s.finishJob(); - }, - } - } - fn parkJob(s: *Self) void { - const j = &s.job; - switch (j.kind) { - .read, .readdir, .write => {}, - else => { - s.fail(j.tag, e_again); - s.finishJob(); - return; - }, - } - const i = s.freeSlot() orelse { - s.fail(j.tag, e_again); - s.finishJob(); - return; - }; - const sl = &s.slots[i]; - sl.* = .{ - .used = true, - .parked = true, - .seq = s.tick(), - .tag = j.tag, - .kind = j.kind, - .fid = j.fid, - .count = j.count, - .req = j.req, - }; - if (j.req.data.len != 0) { - if (j.req.data.len > park_data_max) { - sl.* = .{}; - s.fail(j.tag, e_again); - s.finishJob(); - return; - } - @memcpy(sl.data[0..j.req.data.len], j.req.data); - sl.copied = true; - sl.req.data = sl.data[0..j.req.data.len]; - } - s.finishJob(); - } - - fn slotReply(s: *Self, i: usize, r: *const fs.Reply, bytes: []const u8) void { - const sl = &s.slots[i]; - if (r.status == .again) { - sl.parked = true; - return; - } - if (r.status == .err) { - s.fail(sl.tag, errString(r.errno)); - sl.* = .{}; - return; - } - switch (sl.kind) { - .read => s.emit(sl.tag, .{ .rread = .{ .data = bytes[0..@min(bytes.len, sl.count)] } }), - .readdir => s.emitDirRead(sl.tag, sl.fid, bytes, sl.count), - .write => s.emit(sl.tag, .{ .rwrite = .{ .count = @min(r.written, sl.count) } }), - else => s.fail(sl.tag, e_botch), - } - sl.* = .{}; - } - - fn emitDirRead(s: *Self, tag: u16, fid: u32, staging: []const u8, count: u32) void { - const buf = s.protocol.out[s.protocol.out_len..]; - assert(buf.len > header_len + 4); - const cap = @min(@as(usize, count), buf.len - header_len - 4); - const who = s.uname[0..s.uname_len]; - var n: usize = header_len + 4; - var entries: u32 = 0; - var i: usize = 0; - while (i + 10 <= staging.len) { - const nlen: usize = staging[i + 9]; - if (i + 10 + nlen > staging.len) break; - const dir = staging[i + 8] != 0; - const rec: Stat = .{ - .type = 0, - .dev = 0, - .qid = qidOf(std.mem.readInt(u64, staging[i..][0..8], .little), dir), - .mode = (if (dir) dmdir else 0) | @as(u32, if (dir) dirent_dir_perm else dirent_file_perm), - .atime = 0, - .mtime = 0, - .length = 0, - .name = staging[i + 10 ..][0..nlen], - .uid = who, - .gid = who, - .muid = who, - }; - const size = @as(usize, rec.size() catch break) + 2; - if (n - header_len - 4 + size > cap) break; - _ = rec.encode(buf[n..]) catch break; - n += size; - entries += 1; - i += 10 + nlen; - } - if (entries == 0 and staging.len != 0) return s.fail(tag, e_count_small); - const payload: u32 = @intCast(n - header_len - 4); - comptime assert(header_len == 7); - s.emit(tag, .{ .rread = .{ .data = buf[header_len + 4 .. n] } }); - if (s.findFid(fid)) |k| { - s.fids[k].diroff += payload; - s.fids[k].dirindex += entries; - } - } - - fn statOf(s: *const Self, f: *const Fid, a: fs.Reply.Attr) Stat { - const who = s.uname[0..s.uname_len]; - return .{ - .type = 0, - .dev = 0, - .qid = qidOf(if (a.node != 0) a.node else f.node, a.dir), - .mode = (if (a.dir) dmdir else 0) | @as(u32, a.mode), - .atime = 0, - .mtime = if (@hasField(@TypeOf(a), "mtime")) a.mtime else 0, - .length = a.size, - .name = f.name[0..f.name_len], - .uid = who, - .gid = who, - .muid = who, - }; - } - }; -} - -const StubFs = struct { - pub const Op = enum(u8) { lookup, getattr, setattr, open, read, write, release, readdir }; - pub const Status = enum(u8) { ok, again, err }; - - pub const Req = struct { - tag: u64, - op: Op, - node: u64, - handle: u32 = 0, - off: u64 = 0, - size: u32 = 0, - data: []const u8 = &.{}, - truncate: bool = false, - }; - - pub const Reply = struct { - tag: u64, - status: Status = .ok, - errno: u16 = 0, - attr: Attr = .{}, - handle: u32 = 0, - written: u32 = 0, - - pub const Attr = struct { - name: []const u8 = "", - node: u64 = 0, - dir: bool = false, - size: u64 = 0, - mode: u16 = 0o600, - }; - }; - - const Entry = struct { node: u64, parent: u64, name: []const u8, dir: bool, mode: u16 }; - - const tree = [_]Entry{ - .{ .node = 1, .parent = 1, .name = "/", .dir = true, .mode = 0o500 }, - .{ .node = 2, .parent = 1, .name = "index", .dir = false, .mode = 0o400 }, - .{ .node = 3, .parent = 1, .name = "cons", .dir = false, .mode = 0o200 }, - .{ .node = 4, .parent = 1, .name = "new", .dir = true, .mode = 0o500 }, - .{ .node = 16, .parent = 1, .name = "1", .dir = true, .mode = 0o500 }, - .{ .node = 32, .parent = 1, .name = "2", .dir = true, .mode = 0o500 }, - .{ .node = 17, .parent = 16, .name = "addr", .dir = false, .mode = 0o600 }, - .{ .node = 18, .parent = 16, .name = "body", .dir = false, .mode = 0o600 }, - .{ .node = 19, .parent = 16, .name = "ctl", .dir = false, .mode = 0o600 }, - .{ .node = 21, .parent = 16, .name = "errors", .dir = false, .mode = 0o200 }, - .{ .node = 22, .parent = 16, .name = "event", .dir = false, .mode = 0o600 }, - .{ .node = 23, .parent = 16, .name = "tag", .dir = false, .mode = 0o600 }, - }; - - const body_node = 18; - const index_node = 2; - const event_node = 22; - - body: []const u8 = "hello, body\n", - filler: [1024]u8 = @splat('x'), - event: ?[]const u8 = null, - park_writes: bool = false, - releases: u32 = 0, - calls: u32 = 0, - writes: [128]u8 = undefined, - writes_len: usize = 0, - stage: [1024]u8 = undefined, - - const Answer = struct { reply: Reply, bytes: []const u8 = "" }; - - fn find(node: u64) ?usize { - for (tree, 0..) |e, i| if (e.node == node) return i; - return null; - } - - fn sizeOf(st: *const StubFs, node: u64) u64 { - return switch (node) { - body_node => st.body.len, - index_node => st.filler.len, - else => 0, - }; - } - - fn contentOf(st: *const StubFs, node: u64) []const u8 { - return switch (node) { - body_node => st.body, - index_node => &st.filler, - else => "", - }; - } - - fn attrOf(st: *const StubFs, e: Entry) Reply.Attr { - return .{ .name = e.name, .node = e.node, .dir = e.dir, .mode = e.mode, .size = st.sizeOf(e.node) }; - } - - fn stageDir(st: *StubFs, node: u64, skip: u64) []const u8 { - var n: usize = 0; - var seen: u64 = 0; - for (tree) |e| { - if (e.parent != node or e.node == node) continue; - if (seen < skip) { - seen += 1; - continue; - } - std.mem.writeInt(u64, st.stage[n..][0..8], e.node, .little); - st.stage[n + 8] = @intFromBool(e.dir); - st.stage[n + 9] = @intCast(e.name.len); - @memcpy(st.stage[n + 10 ..][0..e.name.len], e.name); - n += 10 + e.name.len; - } - return st.stage[0..n]; - } - - fn handle(st: *StubFs, req: Req) Answer { - st.calls += 1; - const fail: Answer = .{ .reply = .{ .tag = req.tag, .status = .err, .errno = 2 } }; - const i = find(req.node) orelse return fail; - switch (req.op) { - .lookup => { - if (std.mem.eql(u8, req.data, "..")) - return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[find(tree[i].parent).?]) } }; - for (tree) |e| { - if (e.parent != req.node or e.node == req.node) continue; - if (!std.mem.eql(u8, e.name, req.data)) continue; - return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(e) } }; - } - return fail; - }, - .getattr => return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[i]) } }, - .setattr => { - if (req.truncate and req.node == body_node) st.body = ""; - return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[i]) } }; - }, - .open => return .{ .reply = .{ .tag = req.tag, .handle = 7 } }, - .release => { - st.releases += 1; - return .{ .reply = .{ .tag = req.tag } }; - }, - .readdir => { - if (!tree[i].dir) return .{ .reply = .{ .tag = req.tag, .status = .err, .errno = 20 } }; - return .{ .reply = .{ .tag = req.tag }, .bytes = st.stageDir(req.node, req.off) }; - }, - .read => { - if (req.node == event_node) { - const rec = st.event orelse return .{ .reply = .{ .tag = req.tag, .status = .again } }; - st.event = null; - return .{ .reply = .{ .tag = req.tag }, .bytes = rec }; - } - const all = st.contentOf(req.node); - if (req.off >= all.len) return .{ .reply = .{ .tag = req.tag } }; - const from = all[@intCast(req.off)..]; - return .{ .reply = .{ .tag = req.tag }, .bytes = from[0..@min(from.len, req.size)] }; - }, - .write => { - if (st.park_writes) return .{ .reply = .{ .tag = req.tag, .status = .again } }; - const n = @min(req.data.len, st.writes.len - st.writes_len); - @memcpy(st.writes[st.writes_len..][0..n], req.data[0..n]); - st.writes_len += n; - return .{ .reply = .{ .tag = req.tag, .written = @intCast(n) } }; - }, - } - } -}; - -const Srv = Server(StubFs, max_fids); - -const Harness = struct { - in: [4096]u8 = undefined, - out: [8192]u8 = undefined, - fsys: StubFs = .{}, - srv: Srv = undefined, - - fn start(h: *Harness) void { - h.srv = Srv.init(.{ .in = &h.in, .out = &h.out, .root = 1 }); - } - - fn answer(h: *Harness, req: StubFs.Req) void { - const a = h.fsys.handle(req); - h.srv.reply(&a.reply, a.bytes); - } - - fn pump(h: *Harness) void { - while (h.srv.retry()) |req| h.answer(req); - while (h.srv.next()) |req| h.answer(req); - } - - fn send(h: *Harness, tag: u16, msg: Msg) !void { - var buf: [1024]u8 = undefined; - const bytes = try encode(msg, tag, &buf); - try testing.expectEqual(bytes.len, h.srv.push(bytes)); - h.pump(); - } - - fn reap(h: *Harness) !Decoded { - const out = h.srv.output(); - const len = frameLen(out) orelse return error.NoReply; - if (len > out.len) return error.ShortReply; - const got = try decode(out[0..len]); - h.srv.wrote(len); - return got; - } - - fn quiet(h: *Harness) !void { - try testing.expectEqual(@as(usize, 0), h.srv.output().len); - } - - fn handshake(h: *Harness, msize: u32) !void { - h.start(); - try h.send(notag, .{ .tversion = .{ .msize = msize, .version = "9P2000" } }); - const v = try h.reap(); - try testing.expectEqualStrings("9P2000", v.msg.rversion.version); - try h.send(0, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); - const a = try h.reap(); - try testing.expectEqual(@as(u64, 1), a.msg.rattach.qid.path); - } +const std = @import("std"); +const testing = std.testing; - fn walkTo(h: *Harness, tag: u16, newfid: u32, list: []const []const u8) !Decoded { - try h.send(tag, .{ .twalk = .{ - .fid = 0, - .newfid = newfid, - .nwname = @intCast(list.len), - .wname = wnames(list), - } }); - return h.reap(); - } +const Stub = struct { + pub const Req = cloud9.fs.Req; + pub const Reply = cloud9.fs.Reply; }; -fn wnames(list: []const []const u8) [max_welem][]const u8 { - var out: [max_welem][]const u8 = @splat(""); - for (list, 0..) |n, i| out[i] = n; - return out; -} - -fn dirNames(data: []const u8, out: [][]const u8) !usize { - var n: usize = 0; - var i: usize = 0; - while (i < data.len) { - const size = std.mem.readInt(u16, data[i..][0..2], .little); - const st = try Stat.decode(data[i..][0 .. @as(usize, size) + 2]); - out[n] = st.name; - n += 1; - i += @as(usize, size) + 2; - } - return n; -} - -test "9p server: the version handshake clamps, falls back, and refuses" { - var h: Harness = .{}; - h.start(); - - try h.send(notag, .{ .tversion = .{ .msize = 1 << 20, .version = "9P2000" } }); - var got = try h.reap(); - try testing.expectEqual(notag, got.tag); - try testing.expectEqual(@as(u32, 4096), got.msg.rversion.msize); - try testing.expectEqualStrings("9P2000", got.msg.rversion.version); - - try h.send(notag, .{ .tversion = .{ .msize = 512, .version = "9P2000" } }); - got = try h.reap(); - try testing.expectEqual(@as(u32, 512), got.msg.rversion.msize); - - try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000.u" } }); - got = try h.reap(); - try testing.expectEqualStrings("9P2000", got.msg.rversion.version); - - try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "TCP/IP" } }); - got = try h.reap(); - try testing.expectEqualStrings("unknown", got.msg.rversion.version); - try h.send(1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); - try testing.expect(h.srv.protocol.dead); - h.start(); - try h.send(notag, .{ .tversion = .{ .msize = 64, .version = "9P2000" } }); - try testing.expect(h.srv.protocol.dead); - try testing.expectEqual(@as(u32, 216), msize_min - 1); -} - -test "9p server: attach names the root, and the only tree there is" { - var h: Harness = .{}; - h.start(); - try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000" } }); - _ = try h.reap(); - - try h.send(1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "work" } }); - var got = try h.reap(); - try testing.expectEqualStrings(e_no_tree, got.msg.rerror.ename); - - try h.send(2, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); - got = try h.reap(); - try testing.expectEqual(@as(u64, 1), got.msg.rattach.qid.path); - try testing.expectEqual(qtdir, got.msg.rattach.qid.type); - try testing.expectEqual(@as(u32, 0), got.msg.rattach.qid.version); - - try h.send(3, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); - got = try h.reap(); - try testing.expectEqualStrings(e_fid_in_use, got.msg.rerror.ename); - - try h.send(4, .{ .tauth = .{ .afid = 1, .uname = "goblin", .aname = "" } }); - got = try h.reap(); - try testing.expectEqualStrings(e_no_auth, got.msg.rerror.ename); - - try h.send(5, .{ .tstat = .{ .fid = 0 } }); - got = try h.reap(); - try testing.expectEqualStrings("/", got.msg.rstat.stat.name); - try testing.expectEqualStrings("goblin", got.msg.rstat.stat.uid); - try testing.expectEqualStrings("goblin", got.msg.rstat.stat.muid); - try testing.expect(got.msg.rstat.stat.mode & dmdir != 0); -} - -test "9p server: a three-element walk, and `..` with no kernel to resolve it" { - var h: Harness = .{}; - try h.handshake(4096); - - var got = try h.walkTo(5, 1, &.{ "..", "1", "body" }); - try testing.expectEqual(@as(u16, 3), got.msg.rwalk.nwqid); - try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[0].path); - try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[1].path); - try testing.expectEqual(@as(u64, 18), got.msg.rwalk.wqid[2].path); - try testing.expectEqual(qtdir, got.msg.rwalk.wqid[1].type); - try testing.expectEqual(qtfile, got.msg.rwalk.wqid[2].type); - for (got.msg.rwalk.wqid[0..3]) |q| try testing.expectEqual(@as(u32, 0), q.version); - - got = try h.walkTo(6, 2, &.{ "..", "1", "..", "1", "body" }); - try testing.expectEqual(@as(u16, 5), got.msg.rwalk.nwqid); - try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[2].path); - try testing.expectEqual(@as(u64, 18), got.msg.rwalk.wqid[4].path); - try h.send(7, .{ .tstat = .{ .fid = 2 } }); - got = try h.reap(); - try testing.expectEqualStrings("body", got.msg.rstat.stat.name); - try testing.expectEqual(@as(u64, 12), got.msg.rstat.stat.length); - - got = try h.walkTo(8, 3, &.{ "1", "body", ".." }); - try testing.expectEqual(@as(u16, 3), got.msg.rwalk.nwqid); - try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[2].path); - try testing.expectEqual(qtdir, got.msg.rwalk.wqid[2].type); - try h.send(9, .{ .tstat = .{ .fid = 3 } }); - got = try h.reap(); - try testing.expectEqualStrings("1", got.msg.rstat.stat.name); - - const before = h.fsys.calls; - got = try h.walkTo(10, 4, &.{ ".", "." }); - try testing.expectEqual(@as(u16, 2), got.msg.rwalk.nwqid); - try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[1].path); - try testing.expectEqual(before, h.fsys.calls); -} - -test "9p server: a walk failing on the first element is Rerror, on the second a short Rwalk" { - var h: Harness = .{}; - try h.handshake(4096); - - var got = try h.walkTo(5, 1, &.{ "nope", "body" }); - try testing.expectEqualStrings("No such file or directory", got.msg.rerror.ename); - - got = try h.walkTo(6, 1, &.{ "1", "nope" }); - try testing.expectEqual(@as(u16, 1), got.msg.rwalk.nwqid); - try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[0].path); - - try h.send(7, .{ .tstat = .{ .fid = 1 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); - - try h.send(8, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 0 } }); - got = try h.reap(); - try testing.expectEqual(@as(u16, 0), got.msg.rwalk.nwqid); - try h.send(9, .{ .tstat = .{ .fid = 1 } }); - got = try h.reap(); - try testing.expectEqualStrings("/", got.msg.rstat.stat.name); - - got = try h.walkTo(10, 2, &.{"index"}); - try testing.expectEqual(@as(u16, 1), got.msg.rwalk.nwqid); - try h.send(11, .{ .twalk = .{ .fid = 2, .newfid = 3, .nwname = 1, .wname = wnames(&.{"body"}) } }); - got = try h.reap(); - try testing.expectEqualStrings(e_not_dir, got.msg.rerror.ename); - - got = try h.walkTo(12, 4, &.{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}); - try testing.expectEqualStrings(e_illegal_name, got.msg.rerror.ename); - - try h.send(13, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 1, .wname = wnames(&.{"1"}) } }); - got = try h.reap(); - try testing.expectEqualStrings(e_fid_in_use, got.msg.rerror.ename); -} - -test "9p server: backend-sized filenames survive walk and stat within negotiated msize" { - const NativeFs = struct { - pub const Req = StubFs.Req; - pub const Reply = StubFs.Reply; - pub const name_capacity: usize = 255; - }; - const Native = Server(NativeFs, 2); - try testing.expectEqual(@as(usize, 28), @sizeOf(@FieldType(Srv.Fid, "name"))); - try testing.expectEqual(@as(usize, 255), @sizeOf(@FieldType(Native.Fid, "name"))); - - const filename: [255]u8 = @splat('f'); - for ([_]struct { length: usize, msize: u32 }{ - .{ .length = 29, .msize = 512 }, - .{ .length = 128, .msize = 512 }, - .{ .length = 255, .msize = 512 }, - .{ .length = 200, .msize = 256 }, - }) |case| { - var in: [1024]u8 = undefined; - var out: [2048]u8 = undefined; - var encoded: [1024]u8 = undefined; - var server = Native.init(.{ .in = &in, .out = &out, .root = 1 }); - server.protocol.msize = case.msize; - server.setUname("u" ** username_capacity); - server.fids[0] = .{ .used = true, .fid = 0, .node = 1, .dir = true, .perm = 0o500 }; - const name = filename[0..case.length]; - const walk = try encode(.{ .twalk = .{ - .fid = 0, - .newfid = 1, - .nwname = 1, - .wname = wnames(&.{name}), - } }, 1, &encoded); - try testing.expectEqual(walk.len, server.push(walk)); - const lookup = server.next() orelse return error.MissingLookup; - try testing.expectEqual(.lookup, lookup.op); - try testing.expectEqualStrings(name, lookup.data); - server.reply(&.{ .tag = lookup.tag, .attr = .{ .node = 2, .name = name, .size = 12 } }, ""); - try testing.expectEqual(null, server.next()); - var response = try decode(server.output()); - try testing.expectEqual(@as(u16, 1), response.msg.rwalk.nwqid); - server.wrote(server.output().len); - - const stat = try encode(.{ .tstat = .{ .fid = 1 } }, 2, &encoded); - try testing.expectEqual(stat.len, server.push(stat)); - const getattr = server.next() orelse return error.MissingGetattr; - try testing.expectEqual(.getattr, getattr.op); - server.reply(&.{ .tag = getattr.tag, .attr = .{ .node = 2, .name = name, .size = 12 } }, ""); - try testing.expect(server.output().len <= case.msize); - response = try decode(server.output()); - if (case.msize == 256) { - try testing.expectEqualStrings(e_small_msize, response.msg.rerror.ename); - } else { - try testing.expectEqualStrings(name, response.msg.rstat.stat.name); - try testing.expectEqual(@as(u64, 12), response.msg.rstat.stat.length); - } - server.wrote(server.output().len); - const clunk = try encode(.{ .tclunk = .{ .fid = 1 } }, 3, &encoded); - try testing.expectEqual(clunk.len, server.push(clunk)); - try testing.expectEqual(null, server.next()); - response = try decode(server.output()); - try testing.expect(response.msg == .rclunk); - try testing.expect(!server.protocol.dead); - } -} - -test "9p server: open then read then clunk, and the release a clunk owes the core" { - var h: Harness = .{}; - try h.handshake(4096); - _ = try h.walkTo(5, 1, &.{ "1", "body" }); - - try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); - var got = try h.reap(); - try testing.expectEqual(@as(u64, 18), got.msg.ropen.qid.path); - try testing.expectEqual(@as(u32, 4096 - iohdrsz), got.msg.ropen.iounit); - - try h.send(7, .{ .topen = .{ .fid = 1, .mode = oread } }); - got = try h.reap(); - try testing.expectEqualStrings(e_already_open, got.msg.rerror.ename); - - try h.send(8, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); - got = try h.reap(); - try testing.expectEqualStrings("hello, body\n", got.msg.rread.data); - - try h.send(9, .{ .tread = .{ .fid = 1, .offset = 7, .count = 4096 } }); - got = try h.reap(); - try testing.expectEqualStrings("body\n", got.msg.rread.data); - - try h.send(10, .{ .tread = .{ .fid = 1, .offset = 99, .count = 16 } }); - got = try h.reap(); - try testing.expectEqual(@as(usize, 0), got.msg.rread.data.len); - - try testing.expectEqual(@as(u32, 0), h.fsys.releases); - try h.send(11, .{ .tclunk = .{ .fid = 1 } }); - got = try h.reap(); - try testing.expect(got.msg == .rclunk); - try testing.expectEqual(@as(u32, 1), h.fsys.releases); - - try h.send(12, .{ .tread = .{ .fid = 1, .offset = 0, .count = 16 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); - try h.send(13, .{ .tclunk = .{ .fid = 1 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); - - _ = try h.walkTo(14, 2, &.{"index"}); - try h.send(15, .{ .tclunk = .{ .fid = 2 } }); - got = try h.reap(); - try testing.expect(got.msg == .rclunk); - try testing.expectEqual(@as(u32, 1), h.fsys.releases); -} - -test "9p server: every Rread is clamped to the client's count and to the msize" { - var h: Harness = .{}; - try h.handshake(512); - _ = try h.walkTo(5, 1, &.{"index"}); - try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); - _ = try h.reap(); - - try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 5 } }); - var got = try h.reap(); - try testing.expectEqual(@as(usize, 5), got.msg.rread.data.len); - - try h.send(8, .{ .tread = .{ .fid = 1, .offset = 0, .count = 1 << 20 } }); - const out = h.srv.output(); - try testing.expectEqual(@as(?u32, 512), frameLen(out)); - got = try h.reap(); - try testing.expectEqual(@as(usize, 512 - header_len - 4), got.msg.rread.data.len); - - try h.send(9, .{ .tread = .{ .fid = 1, .offset = 0, .count = 1 << 20 } }); - got = try h.reap(); - try testing.expectEqual(@as(usize, 501), got.msg.rread.data.len); -} - -test "9p server: a directory read is whole stat records at a cursor the client cannot invent" { - var h: Harness = .{}; - try h.handshake(4096); - _ = try h.walkTo(4, 1, &.{"index"}); - try h.send(5, .{ .topen = .{ .fid = 0, .mode = oread } }); - _ = try h.reap(); - - var found: [8][]const u8 = undefined; - - try h.send(6, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } }); - var got = try h.reap(); - const first = got.msg.rread.data.len; - try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); - try testing.expectEqualStrings("index", found[0]); - try testing.expectEqualStrings("cons", found[1]); - try testing.expect(first <= 150); - - try h.send(7, .{ .tread = .{ .fid = 0, .offset = first, .count = 150 } }); - got = try h.reap(); - const second = got.msg.rread.data.len; - try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); - try testing.expectEqualStrings("new", found[0]); - try testing.expectEqualStrings("1", found[1]); - - try h.send(8, .{ .tread = .{ .fid = 0, .offset = first + second + 1, .count = 150 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_bad_offset, got.msg.rerror.ename); - try h.send(9, .{ .tread = .{ .fid = 0, .offset = 3, .count = 150 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_bad_offset, got.msg.rerror.ename); - - try h.send(10, .{ .tread = .{ .fid = 0, .offset = first + second, .count = 150 } }); - got = try h.reap(); - try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found)); - try testing.expectEqualStrings("2", found[0]); - - try h.send(11, .{ .tread = .{ .fid = 0, .offset = first + second + got.msg.rread.data.len, .count = 150 } }); - got = try h.reap(); - try testing.expectEqual(@as(usize, 0), got.msg.rread.data.len); - - try h.send(12, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } }); - got = try h.reap(); - try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); - try testing.expectEqualStrings("index", found[0]); - - try h.send(13, .{ .tread = .{ .fid = 0, .offset = 0, .count = 40 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_count_small, got.msg.rerror.ename); - - try h.send(14, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } }); - got = try h.reap(); - const one = try Stat.decode(got.msg.rread.data[0 .. std.mem.readInt(u16, got.msg.rread.data[0..2], .little) + 2]); - try testing.expectEqualStrings("index", one.name); - try testing.expectEqual(@as(u32, dirent_file_perm), one.mode); - try testing.expectEqual(@as(u64, 0), one.length); - try testing.expectEqual(@as(u32, 0), one.qid.version); - - try h.send(16, .{ .tstat = .{ .fid = 1 } }); - got = try h.reap(); - try testing.expectEqual(@as(u32, 0o400), got.msg.rstat.stat.mode); - try testing.expectEqual(@as(u64, 1024), got.msg.rstat.stat.length); -} - -test "9p server: long directory names remain whole across pages" { - var h: Harness = .{}; - try h.handshake(4096); - var entries: [10 + 255 + 10 + 4]u8 = @splat(0); - std.mem.writeInt(u64, entries[0..8], 41, .little); - entries[9] = 255; - @memset(entries[10..265], 'f'); - std.mem.writeInt(u64, entries[265..273], 42, .little); - entries[274] = 4; - @memcpy(entries[275..], "next"); - var found: [2][]const u8 = undefined; - - { - var request: [64]u8 = undefined; - const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 330 } }, 5, &request); - _ = h.srv.protocol.push(bytes); - _ = (try h.srv.protocol.receive()).?; - h.srv.protocol.release(); - } - h.srv.emitDirRead(5, 0, &entries, 330); - var got = try h.reap(); - const first = got.msg.rread.data.len; - try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found)); - try testing.expectEqualStrings(entries[10..265], found[0]); - try testing.expectEqual(@as(u64, 1), h.srv.fids[0].dirindex); - - { - var request: [64]u8 = undefined; - const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 330 } }, 6, &request); - _ = h.srv.protocol.push(bytes); - _ = (try h.srv.protocol.receive()).?; - h.srv.protocol.release(); - } - h.srv.emitDirRead(6, 0, entries[265..], 330); - got = try h.reap(); - try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found)); - try testing.expectEqualStrings("next", found[0]); - try testing.expectEqual(@as(u64, 2), h.srv.fids[0].dirindex); - try testing.expectEqual(first + got.msg.rread.data.len, h.srv.fids[0].diroff); - - { - var request: [64]u8 = undefined; - const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 4096 } }, 7, &request); - _ = h.srv.protocol.push(bytes); - _ = (try h.srv.protocol.receive()).?; - h.srv.protocol.release(); - } - h.srv.emitDirRead(7, 0, &entries, 4096); - got = try h.reap(); - try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); - try testing.expectEqualStrings(entries[10..265], found[0]); - try testing.expectEqualStrings("next", found[1]); -} - -test "9p server: a blocked read parks, and the connection keeps working" { - var h: Harness = .{}; - try h.handshake(4096); - _ = try h.walkTo(5, 1, &.{ "1", "event" }); - try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); - _ = try h.reap(); - - try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); - try h.quiet(); - - h.pump(); - h.pump(); - try h.quiet(); - - _ = try h.walkTo(8, 2, &.{ "1", "body" }); - try h.send(9, .{ .tstat = .{ .fid = 2 } }); - var got = try h.reap(); - try testing.expectEqualStrings("body", got.msg.rstat.stat.name); - - h.fsys.event = "Kli7 7 0 0 hello\n"; - h.pump(); - got = try h.reap(); - try testing.expectEqual(@as(u16, 7), got.tag); - try testing.expectEqualStrings("Kli7 7 0 0 hello\n", got.msg.rread.data); - try h.quiet(); - - _ = try h.walkTo(10, 3, &.{ "1", "ctl" }); - try h.send(11, .{ .topen = .{ .fid = 3, .mode = owrite } }); - _ = try h.reap(); - h.fsys.park_writes = true; - try h.send(12, .{ .twrite = .{ .fid = 3, .offset = 0, .data = "clean\n" } }); - try h.quiet(); - try h.send(13, .{ .tstat = .{ .fid = 2 } }); - _ = try h.reap(); - h.fsys.park_writes = false; - h.pump(); - got = try h.reap(); - try testing.expectEqual(@as(u16, 12), got.tag); - try testing.expectEqual(@as(u32, 6), got.msg.rwrite.count); - try testing.expectEqualStrings("clean\n", h.fsys.writes[0..h.fsys.writes_len]); - for (0..max_slots) |k| { - try h.send(@intCast(100 + k), .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); - try h.quiet(); - } - try h.send(200, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_again, got.msg.rerror.ename); -} - -test "9p server: the reply queue is a FIFO that survives a partial write" { - var h: Harness = .{}; - try h.handshake(4096); - - try h.send(5, .{ .tstat = .{ .fid = 0 } }); - var saved: [256]u8 = undefined; - const one = h.srv.output(); - const n = one.len; - @memcpy(saved[0..n], one); - h.srv.wrote(3); - try testing.expectEqual(n - 3, h.srv.output().len); - - try h.send(6, .{ .tstat = .{ .fid = 0 } }); - const rest = h.srv.output(); - try testing.expectEqualSlices(u8, saved[3..n], rest[0 .. n - 3]); - const tail = rest[n - 3 ..]; - const second = try decode(tail[0..frameLen(tail).?]); - try testing.expectEqual(@as(u16, 6), second.tag); - - var flood: [8192]u8 = @splat(0); - try testing.expectEqual(@as(usize, 4096), h.srv.push(&flood)); - h.pump(); - try testing.expect(h.srv.protocol.dead); - try testing.expectEqual(@as(usize, 0), h.srv.push(&flood)); -} - -test "9p server: Tflush answers the original first and the Rflush second" { - var h: Harness = .{}; - try h.handshake(4096); - _ = try h.walkTo(5, 1, &.{ "1", "event" }); - try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); - _ = try h.reap(); - try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); - try h.quiet(); - - try h.send(8, .{ .tflush = .{ .oldtag = 7 } }); - var got = try h.reap(); - try testing.expectEqual(@as(u16, 7), got.tag); - try testing.expectEqualStrings(e_interrupted, got.msg.rerror.ename); - got = try h.reap(); - try testing.expectEqual(@as(u16, 8), got.tag); - try testing.expect(got.msg == .rflush); - try h.quiet(); - - h.fsys.event = "Kli7 7 0 0 hello\n"; - h.pump(); - try h.quiet(); - - try h.send(9, .{ .tflush = .{ .oldtag = 99 } }); - got = try h.reap(); - try testing.expectEqual(@as(u16, 9), got.tag); - try testing.expect(got.msg == .rflush); - try h.quiet(); -} - -test "9p server: the fid and permission refusals, each in a string Linux knows" { - var h: Harness = .{}; - try h.handshake(4096); - - for ([_]Msg{ - .{ .tread = .{ .fid = 99, .offset = 0, .count = 16 } }, - .{ .tstat = .{ .fid = 99 } }, - .{ .tclunk = .{ .fid = 99 } }, - .{ .topen = .{ .fid = 99, .mode = oread } }, - .{ .twalk = .{ .fid = 99, .newfid = 98, .nwname = 0 } }, - }, 20..) |msg, tag| { - try h.send(@intCast(tag), msg); - const got = try h.reap(); - try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); - } - - _ = try h.walkTo(5, 1, &.{ "1", "body" }); - try h.send(6, .{ .tread = .{ .fid = 1, .offset = 0, .count = 16 } }); - var got = try h.reap(); - try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename); - try h.send(7, .{ .topen = .{ .fid = 1, .mode = oread } }); - _ = try h.reap(); - try h.send(8, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "x" } }); - got = try h.reap(); - try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename); - - try h.send(9, .{ .twalk = .{ .fid = 1, .newfid = 2, .nwname = 0 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename); - - _ = try h.walkTo(10, 3, &.{ "1", "errors" }); - try h.send(11, .{ .topen = .{ .fid = 3, .mode = oread } }); - got = try h.reap(); - try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); - try h.send(12, .{ .topen = .{ .fid = 3, .mode = owrite } }); - got = try h.reap(); - try testing.expect(got.msg == .ropen); - - try h.send(13, .{ .topen = .{ .fid = 0, .mode = ordwr } }); - got = try h.reap(); - try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); - _ = try h.walkTo(14, 4, &.{ "1", "tag" }); - for ([_]u8{ orclose, oexec, oread | orclose }, 30..) |mode, tag| { - try h.send(@intCast(tag), .{ .topen = .{ .fid = 4, .mode = mode } }); - got = try h.reap(); - try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); - } -} - -test "9p server: Twstat with a zero length is the truncate, and so is OTRUNC" { - var h: Harness = .{}; - try h.handshake(4096); - _ = try h.walkTo(5, 1, &.{ "1", "body" }); - - const sentinel: Stat = .{ - .type = std.math.maxInt(u16), - .dev = std.math.maxInt(u32), - .qid = .{ .type = 0xFF, .version = std.math.maxInt(u32), .path = std.math.maxInt(u64) }, - .mode = std.math.maxInt(u32), - .atime = std.math.maxInt(u32), - .mtime = std.math.maxInt(u32), - .length = std.math.maxInt(u64), - .name = "", - .uid = "", - .gid = "", - .muid = "", - }; - - try h.send(6, .{ .twstat = .{ .fid = 1, .stat = sentinel } }); - var got = try h.reap(); - try testing.expect(got.msg == .rwstat); - try testing.expectEqualStrings("hello, body\n", h.fsys.body); - - var five = sentinel; - five.length = 5; - try h.send(7, .{ .twstat = .{ .fid = 1, .stat = five } }); - got = try h.reap(); - try testing.expectEqualStrings(e_trunc_only, got.msg.rerror.ename); - - var renamed = sentinel; - renamed.name = "other"; - try h.send(8, .{ .twstat = .{ .fid = 1, .stat = renamed } }); - got = try h.reap(); - try testing.expectEqualStrings(e_wstat, got.msg.rerror.ename); - try testing.expectEqualStrings("hello, body\n", h.fsys.body); - - var changes: [12]Stat = @splat(sentinel); - changes[0].type = 0; - changes[1].dev = 0; - changes[2].qid.type = 0; - changes[3].qid.version = 0; - changes[4].qid.path = 0; - changes[5].mode = 0o644; - changes[6].atime = 0; - changes[7].mtime = 0; - changes[8].name = "renamed"; - changes[9].uid = "owner"; - changes[10].gid = "group"; - changes[11].muid = "writer"; - for (changes) |change| { - for ([_]u64{ std.math.maxInt(u64), 0 }) |length| { - if (change.mtime != std.math.maxInt(u32) and length == 0) continue; - var attributes = change; - attributes.length = length; - const calls = h.fsys.calls; - try h.send(20, .{ .twstat = .{ .fid = 1, .stat = attributes } }); - got = try h.reap(); - try testing.expect(got.msg == .rerror); - try testing.expectEqualStrings(e_wstat, got.msg.rerror.ename); - try testing.expectEqual(calls, h.fsys.calls); - try testing.expectEqualStrings("hello, body\n", h.fsys.body); - } - } - - // Captured Linux v9fs O_TRUNC follow-up: length=0 plus current mtime. - var linux_truncate = sentinel; - linux_truncate.length = 0; - linux_truncate.mtime = 1789432552; - try h.send(21, .{ .twstat = .{ .fid = 1, .stat = linux_truncate } }); - got = try h.reap(); - try testing.expect(got.msg == .rwstat); - try testing.expectEqualStrings("", h.fsys.body); - h.fsys.body = "hello, body\n"; - - var zero = sentinel; - zero.length = 0; - try h.send(9, .{ .twstat = .{ .fid = 1, .stat = zero } }); - got = try h.reap(); - try testing.expect(got.msg == .rwstat); - try testing.expectEqualStrings("", h.fsys.body); - - h.fsys.body = "hello, body\n"; - _ = try h.walkTo(10, 2, &.{"index"}); - try h.send(11, .{ .topen = .{ .fid = 2, .mode = oread | otrunc } }); - got = try h.reap(); - try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); - try h.send(12, .{ .topen = .{ .fid = 1, .mode = owrite | otrunc } }); - got = try h.reap(); - try testing.expectEqual(@as(u64, 18), got.msg.ropen.qid.path); - try testing.expectEqualStrings("", h.fsys.body); -} - -test "9p server: create and remove are refused, and a remove clunks the fid anyway" { - var h: Harness = .{}; - try h.handshake(4096); - - try h.send(5, .{ .tcreate = .{ .fid = 0, .name = "thing", .perm = 0o600, .mode = owrite } }); - var got = try h.reap(); - try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); - - _ = try h.walkTo(6, 1, &.{ "1", "body" }); - try h.send(7, .{ .topen = .{ .fid = 1, .mode = ordwr } }); - _ = try h.reap(); - try h.send(8, .{ .tremove = .{ .fid = 1 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); - try testing.expectEqual(@as(u32, 1), h.fsys.releases); - try h.send(9, .{ .tstat = .{ .fid = 1 } }); - got = try h.reap(); - try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); -} - -test "9p server: a message arriving a byte at a time is served when its last byte lands" { - var h: Harness = .{}; - try h.handshake(4096); - - var buf: [64]u8 = undefined; - const bytes = try encode(.{ .tstat = .{ .fid = 0 } }, 5, &buf); - for (bytes[0 .. bytes.len - 1]) |b| { - try testing.expectEqual(@as(usize, 1), h.srv.push(&.{b})); - h.pump(); - try h.quiet(); - } - try testing.expectEqual(@as(usize, 1), h.srv.push(bytes[bytes.len - 1 ..])); - h.pump(); - const got = try h.reap(); - try testing.expectEqualStrings("/", got.msg.rstat.stat.name); - - var pair: [128]u8 = undefined; - const a = try encode(.{ .tstat = .{ .fid = 0 } }, 6, &pair); - const b = try encode(.{ .tstat = .{ .fid = 0 } }, 7, pair[a.len..]); - try testing.expectEqual(a.len + b.len, h.srv.push(pair[0 .. a.len + b.len])); - h.pump(); - try testing.expectEqual(@as(u16, 6), (try h.reap()).tag); - try testing.expectEqual(@as(u16, 7), (try h.reap()).tag); - try h.quiet(); -} - -test "9p server: invalid framing and reply types terminate the connection" { - for ([_]u8{ @intFromEnum(Type.rstat), 8, 0 }) |kind| { - var h: Harness = .{}; - try h.handshake(4096); - var buf: [64]u8 = undefined; - const raw = try encode(.{ .tstat = .{ .fid = 0 } }, 5, &buf); - if (kind == 0) std.mem.writeInt(u32, raw[0..4], 3, .little) else raw[4] = kind; - _ = h.srv.push(raw); - h.pump(); - try h.quiet(); - try testing.expect(h.srv.protocol.dead); - } -} - -test "9p server: a connection that drops still pays the core its releases" { - var h: Harness = .{}; - try h.handshake(4096); - _ = try h.walkTo(5, 1, &.{ "1", "event" }); - _ = try h.walkTo(6, 2, &.{ "1", "body" }); - for ([_]u32{ 1, 2 }, 7..) |fid, tag| { - try h.send(@intCast(tag), .{ .topen = .{ .fid = fid, .mode = oread } }); - _ = try h.reap(); - } - try h.send(9, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); - try h.quiet(); - - h.srv.hangup(); - h.pump(); - try testing.expectEqual(@as(u32, 2), h.fsys.releases); - try h.quiet(); - - var g: Harness = .{}; - try g.handshake(4096); - _ = try g.walkTo(5, 1, &.{ "1", "event" }); - try g.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); - _ = try g.reap(); - try g.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); - try g.quiet(); - try g.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000" } }); - const v = try g.reap(); - try testing.expectEqualStrings("9P2000", v.msg.rversion.version); - try testing.expectEqual(@as(u32, 1), g.fsys.releases); - g.fsys.event = "Kli7 7 0 0 hello\n"; - g.pump(); - try g.quiet(); - try g.send(8, .{ .tstat = .{ .fid = 1 } }); - const got = try g.reap(); - try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); -} - -test "9p server: every errno the core can answer is a string Linux knows" { - try testing.expectEqualStrings("Operation not permitted", errString(1)); - try testing.expectEqualStrings("No such file or directory", errString(2)); - try testing.expectEqualStrings("Input/output error", errString(5)); - try testing.expectEqualStrings("Cannot allocate memory", errString(12)); - try testing.expectEqualStrings("Not a directory", errString(20)); - try testing.expectEqualStrings("Invalid argument", errString(22)); - try testing.expectEqualStrings("Too many open files in system", errString(23)); - try testing.expectEqualStrings("No space left on device", errString(28)); - try testing.expectEqualStrings("Function not implemented", errString(38)); - try testing.expectEqualStrings("Input/output error", errString(0)); - try testing.expectEqualStrings("Input/output error", errString(999)); - - try testing.expectEqualStrings("fid unknown or out of range", e_unknown_fid); - try testing.expectEqualStrings("fid already in use", e_fid_in_use); - try testing.expectEqualStrings("bad use of fid", e_bad_use); - try testing.expectEqualStrings("bad offset in directory read", e_bad_offset); - try testing.expectEqualStrings("permission denied", e_perm); - try testing.expectEqualStrings("not a directory", e_not_dir); - try testing.expectEqualStrings("file already open for I/O", e_already_open); - try testing.expectEqualStrings("illegal name", e_illegal_name); - try testing.expectEqualStrings("Too many open files in system", e_too_many_fids); - try testing.expectEqualStrings("protocol botch", e_botch); - try testing.expectEqualStrings("Interrupted system call", e_interrupted); - try testing.expectEqualStrings("only support truncation to zero length", e_trunc_only); - try testing.expectEqualStrings("wstat prohibited", e_wstat); - try testing.expectEqualStrings("Resource temporarily unavailable", e_again); - for ([_][]const u8{ - e_unknown_fid, e_fid_in_use, e_bad_use, e_bad_offset, e_perm, - e_not_dir, e_already_open, e_botch, e_interrupted, e_trunc_only, - e_wstat, e_again, e_no_tree, e_no_auth, e_count_small, - e_illegal_name, e_too_many_fids, e_small_msize, - }) |s| try testing.expect(s.len <= errmax); -} - test "9p server: board and native capacities size the actual fid storage" { - const Board = Server(StubFs, board_fids); - const Native = Server(StubFs, max_fids); + const Board = Server(Stub, board); + const Native = Server(Stub, editor); const BoardFids = @FieldType(Board, "fids"); const NativeFids = @FieldType(Native, "fids"); try testing.expectEqual(32, @typeInfo(BoardFids).array.len); try testing.expectEqual(256, @typeInfo(NativeFids).array.len); + try testing.expectEqual(28, @sizeOf(@FieldType(Board.Fid, "name"))); + try testing.expectEqual(255, @sizeOf(@FieldType(Native.Fid, "name"))); try testing.expectEqual(32 * @sizeOf(Board.Fid), @sizeOf(BoardFids)); try testing.expectEqual(256 * @sizeOf(Native.Fid), @sizeOf(NativeFids)); - try testing.expectEqual( - @sizeOf(NativeFids) - @sizeOf(BoardFids), - @sizeOf(Native) - @sizeOf(Board), - ); try testing.expect(@sizeOf(BoardFids) <= 3 * 1024); - try testing.expect(@sizeOf(NativeFids) <= 24 * 1024); try testing.expect(@sizeOf(@FieldType(Board, "slots")) <= 8 * 1024); try testing.expect(3 * 4096 + @sizeOf(Board) <= 24 * 1024); -} - -const Pair = struct { - srv_in: [4096]u8 = undefined, - srv_out: [8192]u8 = undefined, - cli_in: [4096]u8 = undefined, - cli_out: [4096]u8 = undefined, - fsys: StubFs = .{}, - srv: Srv = undefined, - cli: Client = undefined, - - fn start(p: *Pair) void { - p.srv = Srv.init(.{ .in = &p.srv_in, .out = &p.srv_out, .root = 1 }); - p.cli = Client.init(.{ .in = &p.cli_in, .out = &p.cli_out }); - } - - fn answer(p: *Pair, req: StubFs.Req) void { - const a = p.fsys.handle(req); - p.srv.reply(&a.reply, a.bytes); - } - - fn wire(p: *Pair) void { - var moved = true; - while (moved) { - moved = false; - while (p.cli.output().len != 0) { - const n = p.srv.push(p.cli.output()); - if (n == 0) break; - p.cli.wrote(n); - moved = true; - } - while (p.srv.retry()) |req| { - p.answer(req); - moved = true; - } - while (p.srv.next()) |req| { - p.answer(req); - moved = true; - } - while (p.srv.output().len != 0) { - const n = p.cli.push(p.srv.output()); - if (n == 0) break; - p.srv.wrote(n); - moved = true; - } - } - } - - fn one(p: *Pair, req: Client.Request) !Client.Done { - const tag = try p.cli.submit(req); - p.wire(); - const done = p.cli.take() orelse return error.NoReply; - try testing.expectEqual(tag, done.tag); - try testing.expectEqual(std.meta.activeTag(req), done.op); - try testing.expectEqual(@as(usize, 0), p.cli.pending()); - return done; - } - - fn handshake(p: *Pair) !void { - p.start(); - const v = try p.one(.{ .version = .{} }); - try testing.expectEqualStrings("9P2000", v.result.version.version); - try testing.expectEqual(@as(u16, notag), v.tag); - const a = try p.one(.{ .attach = .{ .fid = 0, .uname = "goblin" } }); - try testing.expectEqual(@as(u64, 1), a.result.attach.path); - try testing.expectEqual(qtdir, a.result.attach.type); - } -}; - -test "9p client: a whole session against the server in this file" { - var p: Pair = .{}; - try p.handshake(); - try testing.expectEqual(@as(u32, 4096), p.cli.msize); - try testing.expectEqual(@as(u32, 4096 - 11), p.cli.maxRead()); - try testing.expectEqual(@as(u32, 4096 - 23), p.cli.maxWrite()); - - const w = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } }); - try testing.expectEqual(@as(u16, 2), w.result.walk.nwqid); - try testing.expectEqual(@as(u64, 16), w.result.walk.wqid[0].path); - try testing.expectEqual(@as(u64, 18), w.result.walk.wqid[1].path); - try testing.expectEqual(qtfile, w.result.walk.wqid[1].type); - - const o = try p.one(.{ .open = .{ .fid = 1, .mode = ordwr } }); - try testing.expectEqual(@as(u64, 18), o.result.open.qid.path); - try testing.expectEqual(@as(u32, 4096 - iohdrsz), o.result.open.iounit); - - const r = try p.one(.{ .read = .{ .fid = 1, .offset = 0, .count = 64 } }); - try testing.expectEqualStrings("hello, body\n", r.result.read); - const eof = try p.one(.{ .read = .{ .fid = 1, .offset = 12, .count = 64 } }); - try testing.expectEqual(@as(usize, 0), eof.result.read.len); - - const wr = try p.one(.{ .write = .{ .fid = 1, .offset = 0, .data = "abc" } }); - try testing.expectEqual(@as(u32, 3), wr.result.write); - try testing.expectEqualStrings("abc", p.fsys.writes[0..p.fsys.writes_len]); - - const st = try p.one(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqualStrings("body", st.result.stat.name); - try testing.expectEqual(@as(u64, 12), st.result.stat.length); - try testing.expectEqualStrings("goblin", st.result.stat.uid); - - _ = try p.one(.{ .clunk = .{ .fid = 1 } }); - try testing.expectEqual(@as(u32, 1), p.fsys.releases); - try testing.expectEqual(@as(usize, 0), p.cli.pending()); - try testing.expectEqual(@as(usize, 0), p.cli.output().len); - try testing.expect(p.cli.take() == null); - try testing.expect(!p.cli.dead); -} - -fn deliverReversed(p: *Pair) !void { - var scratch: [4096]u8 = undefined; - const out = p.srv.output(); - try testing.expect(out.len <= scratch.len); - @memcpy(scratch[0..out.len], out); - const total = out.len; - p.srv.wrote(total); - - var at: [max_tags]usize = undefined; - var lens: [max_tags]u32 = undefined; - var count: usize = 0; - var i: usize = 0; - while (i < total) { - const len = frameLen(scratch[i..total]) orelse return error.ShortReply; - at[count] = i; - lens[count] = len; - count += 1; - i += len; - } - try testing.expect(count >= 2); - var k = count; - while (k > 0) { - k -= 1; - const f = scratch[at[k]..][0..lens[k]]; - try testing.expectEqual(f.len, p.cli.push(f)); - } -} - -test "9p client: replies out of order are matched by tag and not by arrival" { - var p: Pair = .{}; - try p.handshake(); - const w = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"index"} } }); - try testing.expectEqual(@as(u16, 1), w.result.walk.nwqid); - - const root_tag = try p.cli.submit(.{ .stat = .{ .fid = 0 } }); - const index_tag = try p.cli.submit(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqual(@as(u16, 0), root_tag); - try testing.expectEqual(@as(u16, 1), index_tag); - try testing.expectEqual(@as(usize, 2), p.cli.pending()); - - while (p.cli.output().len != 0) { - const n = p.srv.push(p.cli.output()); - p.cli.wrote(n); - } - while (p.srv.next()) |req| p.answer(req); - try deliverReversed(&p); - - const first = p.cli.take() orelse return error.NoReply; - try testing.expectEqual(index_tag, first.tag); - try testing.expectEqualStrings("index", first.result.stat.name); - const second = p.cli.take() orelse return error.NoReply; - try testing.expectEqual(root_tag, second.tag); - try testing.expectEqualStrings("/", second.result.stat.name); - try testing.expectEqual(@as(usize, 0), p.cli.pending()); - try testing.expect(!p.cli.dead); -} - -test "9p client: an Rerror answers one operation and the session carries on" { - var p: Pair = .{}; - try p.handshake(); - - const bad = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"nope"} } }); - try testing.expectEqual(Client.Op.walk, bad.op); - try testing.expectEqualStrings(errString(2), bad.result.fail); - - try testing.expectEqual(@as(usize, 0), p.cli.pending()); - try testing.expect(!p.cli.dead); - const st = try p.one(.{ .stat = .{ .fid = 0 } }); - try testing.expectEqualStrings("/", st.result.stat.name); - - const stale = try p.one(.{ .stat = .{ .fid = 9 } }); - try testing.expectEqualStrings(e_unknown_fid, stale.result.fail); - try testing.expect(!p.cli.dead); -} - -test "9p client: a reply arriving a byte at a time is taken when its last byte lands" { - var p: Pair = .{}; - try p.handshake(); - const tag = try p.cli.submit(.{ .stat = .{ .fid = 0 } }); - - while (p.cli.output().len != 0) { - const n = p.srv.push(p.cli.output()); - p.cli.wrote(n); - } - while (p.srv.next()) |req| p.answer(req); - var scratch: [512]u8 = undefined; - const out = p.srv.output(); - try testing.expect(out.len > 4 and out.len <= scratch.len); - @memcpy(scratch[0..out.len], out); - const reply = scratch[0..out.len]; - p.srv.wrote(reply.len); - - for (reply[0 .. reply.len - 1]) |b| { - try testing.expectEqual(@as(usize, 1), p.cli.push(&.{b})); - try testing.expect(p.cli.take() == null); - try testing.expect(!p.cli.dead); - } - try testing.expectEqual(@as(usize, 1), p.cli.push(reply[reply.len - 1 ..])); - const done = p.cli.take() orelse return error.NoReply; - try testing.expectEqual(tag, done.tag); - try testing.expectEqualStrings("/", done.result.stat.name); -} - -test "9p client: sixteen tags outstanding, and the seventeenth is refused" { - var p: Pair = .{}; - try p.handshake(); - - var tags: [max_tags]u16 = undefined; - for (&tags, 0..) |*t, i| { - t.* = try p.cli.submit(.{ .stat = .{ .fid = 0 } }); - try testing.expectEqual(@as(u16, @intCast(i)), t.*); - } - try testing.expectEqual(max_tags, p.cli.pending()); - try testing.expectError(error.NoTags, p.cli.submit(.{ .stat = .{ .fid = 0 } })); - const owed = p.cli.output().len; - try testing.expectError(error.NoTags, p.cli.submit(.{ .clunk = .{ .fid = 0 } })); - try testing.expectEqual(owed, p.cli.output().len); - - p.wire(); - var seen: [max_tags]bool = @splat(false); - for (0..max_tags) |_| { - const done = p.cli.take() orelse return error.NoReply; - try testing.expectEqual(Client.Op.stat, done.op); - try testing.expect(!seen[done.tag]); - seen[done.tag] = true; - } - for (seen) |s| try testing.expect(s); - try testing.expectEqual(@as(usize, 0), p.cli.pending()); - _ = try p.one(.{ .stat = .{ .fid = 0 } }); -} - -test "9p client: what a caller may not ask for is refused before a tag is spent" { - var p: Pair = .{}; - p.start(); - - try testing.expectError(error.Handshake, p.cli.submit(.{ .stat = .{ .fid = 0 } })); - try p.handshake(); - - try testing.expectError(error.BadRequest, p.cli.submit(.{ .stat = .{ .fid = nofid } })); - try testing.expectError(error.BadRequest, p.cli.submit(.{ .clunk = .{ .fid = nofid } })); - try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = nofid, .names = &.{} } })); - - try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"1/body"} } })); - try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{""} } })); - const seventeen: [max_welem + 1][]const u8 = @splat("x"); - try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &seventeen } })); - - try testing.expectError(error.TooLarge, p.cli.submit(.{ .read = .{ - .fid = 0, - .offset = 0, - .count = p.cli.maxRead() + 1, - } })); - var big: [4096]u8 = @splat('x'); - try testing.expectError(error.TooLarge, p.cli.submit(.{ .write = .{ - .fid = 0, - .offset = 0, - .data = big[0 .. p.cli.maxWrite() + 1], - } })); - p.cli.wrote(p.cli.output().len); - _ = try p.cli.submit(.{ .read = .{ .fid = 0, .offset = 0, .count = p.cli.maxRead() } }); - p.cli.wrote(p.cli.output().len); - _ = try p.cli.submit(.{ .write = .{ .fid = 0, .offset = 0, .data = big[0..p.cli.maxWrite()] } }); - - try testing.expectError(error.Handshake, p.cli.submit(.{ .version = .{} })); - try testing.expectError(error.NoSpace, p.cli.submit(.{ .stat = .{ .fid = 0 } })); -} - -test "9p client: an msize below the floor, and one the server tried to raise" { - var in: [512]u8 = undefined; - var out: [512]u8 = undefined; - var buf: [64]u8 = undefined; - - var c = Client.init(.{ .in = &in, .out = &out }); - try testing.expectError(error.BadRequest, c.submit(.{ .version = .{ .msize = cloud9.Server.msize_min - 1 } })); - _ = try c.submit(.{ .version = .{} }); - try testing.expectEqual(@as(u32, 512), c.asked); - - _ = c.push(try encode(.{ .rversion = .{ .msize = 1024, .version = "9P2000" } }, notag, &buf)); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - - var c2 = Client.init(.{ .in = &in, .out = &out }); - _ = try c2.submit(.{ .version = .{} }); - _ = c2.push(try encode(.{ .rversion = .{ .msize = 512, .version = "unknown" } }, notag, &buf)); - const done = c2.take() orelse return error.NoReply; - try testing.expectEqualStrings("unknown", done.result.version.version); - try testing.expect(!c2.dead); - try testing.expectEqual(@as(u32, 0), c2.msize); - try testing.expectError(error.Handshake, c2.submit(.{ .stat = .{ .fid = 0 } })); - - var c3 = Client.init(.{ .in = &in, .out = &out }); - _ = try c3.submit(.{ .version = .{} }); - _ = c3.push(try encode(.{ .rversion = .{ .msize = 512, .version = "9P2000.u" } }, notag, &buf)); - try testing.expect(c3.take() == null); - try testing.expect(c3.dead); -} - -test "9p client: what is not an answer to one of our requests ends the connection" { - var buf: [64]u8 = undefined; - const Case = struct { - fn armed(in: []u8, out: []u8, scratch: []u8) !Client { - var c = Client.init(.{ .in = in, .out = out }); - _ = try c.submit(.{ .version = .{} }); - c.wrote(c.output().len); - _ = c.push(try encode(.{ .rversion = .{ .msize = 512, .version = "9P2000" } }, notag, scratch)); - _ = c.take() orelse return error.NoReply; - _ = try c.submit(.{ .stat = .{ .fid = 0 } }); - c.wrote(c.output().len); - return c; - } - }; - var in: [512]u8 = undefined; - var out: [512]u8 = undefined; - - { - var c = try Case.armed(&in, &out, &buf); - _ = c.push(try encode(.{ .tstat = .{ .fid = 0 } }, 0, &buf)); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - } - { - var c = try Case.armed(&in, &out, &buf); - _ = c.push(try encode(.rclunk, 3, &buf)); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - } - { - var c = try Case.armed(&in, &out, &buf); - _ = c.push(try encode(.rclunk, 900, &buf)); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - } - { - var c = try Case.armed(&in, &out, &buf); - _ = c.push(try encode(.rclunk, 0, &buf)); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - } - { - var c = try Case.armed(&in, &out, &buf); - _ = c.push(try encode(.rwstat, 0, &buf)); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - } - { - var c = try Case.armed(&in, &out, &buf); - _ = c.push(&.{ 3, 0, 0, 0 }); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - } - { - var c = try Case.armed(&in, &out, &buf); - _ = c.push(&.{ 0, 4, 0, 0 }); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - } - { - var c = try Case.armed(&in, &out, &buf); - _ = c.push(&.{ 8, 0, 0, 0, @intFromEnum(Type.rstat), 0, 0, 0 }); - try testing.expect(c.take() == null); - try testing.expect(c.dead); - } -} - -test "9p client: an Rread longer than the Tread asked for is refused" { - var p: Pair = .{}; - try p.handshake(); - _ = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } }); - _ = try p.one(.{ .open = .{ .fid = 1, .mode = oread } }); - - const tag = try p.cli.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 4 } }); - p.cli.wrote(p.cli.output().len); - var buf: [64]u8 = undefined; - _ = p.cli.push(try encode(.{ .rread = .{ .data = "hello, body\n" } }, tag, &buf)); - try testing.expect(p.cli.take() == null); - try testing.expect(p.cli.dead); - - var q: Pair = .{}; - try q.handshake(); - _ = try q.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } }); - _ = try q.one(.{ .open = .{ .fid = 1, .mode = oread } }); - const short = try q.one(.{ .read = .{ .fid = 1, .offset = 0, .count = 4 } }); - try testing.expectEqualStrings("hell", short.result.read); -} - -test "9p client: hangup and a dead connection refuse everything after" { - var p: Pair = .{}; - try p.handshake(); - p.cli.hangup(); - try testing.expectEqual(@as(usize, 0), p.cli.pending()); - try testing.expectEqual(@as(usize, 0), p.cli.output().len); - try testing.expectEqual(@as(usize, 0), p.cli.push("anything")); - try testing.expect(p.cli.take() == null); - try testing.expectError(error.Dead, p.cli.submit(.{ .stat = .{ .fid = 0 } })); - try testing.expectError(error.Dead, p.cli.submit(.{ .version = .{} })); -} - -test "9p client: session memory is bounded independently of input" { - try testing.expect(@sizeOf(Client) <= 512); - try testing.expect(2 * 8192 + @sizeOf(Client) <= 17 * 1024); - try testing.expect(2 * cloud9.Server.msize_min + @sizeOf(Client) <= 700); + try testing.expect(min_msize >= msize_min); } diff --git a/src/9p_io.zig b/src/9p_io.zig index d0db1c86..57ce67b5 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -138,7 +138,7 @@ fn localIp(address: std.Io.net.IpAddress) bool { return false; } -const Srv = ninep.Server(pardes.ctlfs, ninep.max_fids); +const Srv = ninep.Server(pardes.ctlfs, ninep.editor); const Conn = struct { fd: c_int = -1, diff --git a/src/esp32p4_9p.zig b/src/esp32p4_9p.zig index f6f66c15..e23235a0 100644 --- a/src/esp32p4_9p.zig +++ b/src/esp32p4_9p.zig @@ -26,7 +26,7 @@ comptime { } const Fs = gpio.Fs(Pads); -const Server = ninep.Server(Fs, ninep.board_fids); +const Server = ninep.Server(Fs, ninep.board); comptime { std.debug.assert(@typeInfo(@FieldType(Server, "fids")).array.len == 32); diff --git a/src/esp32p4_gpio.zig b/src/esp32p4_gpio.zig index 92468581..d6c29a36 100644 --- a/src/esp32p4_gpio.zig +++ b/src/esp32p4_gpio.zig @@ -1,5 +1,6 @@ //! ESP32-P4 JP1 GPIO data and its freestanding filesystem. const std = @import("std"); +const ninep = @import("9p.zig"); // JP1: JC-ESP32P4-M3-DEV schematic, sheet 2 "Expand IO": // 01-esp32p4-m3/docs/schematics/2_EXPAND_IO&BAT.png @@ -107,11 +108,7 @@ pub const Header = struct { } }; -pub const E = struct { - pub const NOENT: u16 = 2; - pub const NOTDIR: u16 = 20; - pub const INVAL: u16 = 22; -}; +pub const E = ninep.fs.E; pub const root: u64 = 1; const gpio_node: u64 = 16; @@ -167,43 +164,10 @@ const out_capacity = size: { pub fn Fs(comptime Pads: type) type { return struct { const Self = @This(); - pub const name_capacity = 28; - - pub const Op = enum(u8) { lookup, getattr, setattr, open, read, write, release, readdir }; - - pub const Status = enum(u8) { ok, again, err }; - - pub const Req = struct { - tag: u64, - op: Op, - node: u64, - handle: u32 = 0, - off: u64 = 0, - size: u32 = 0, - data: []const u8 = &.{}, - truncate: bool = false, - }; - - pub const Reply = struct { - tag: u64, - status: Status = .ok, - errno: u16 = 0, - attr: Attr = .{}, - handle: u32 = 0, - written: u32 = 0, - - pub const Attr = struct { - name: []const u8 = "", - node: u64 = 0, - dir: bool = false, - size: u64 = 0, - mode: u16 = 0o600, - }; - - pub fn fail(tag: u64, e: u16) Reply { - return .{ .tag = tag, .status = .err, .errno = e }; - } - }; + pub const Op = ninep.fs.Op; + pub const Status = ninep.fs.Status; + pub const Req = ninep.fs.Req; + pub const Reply = ninep.fs.Reply; // Reply bytes are borrowed until the next handle call. pub const Answer = struct { reply: Reply, bytes: []const u8 = "" }; @@ -484,7 +448,7 @@ fn names(t: *Board, node: u64, skip: u64) ![][]const u8 { test "GPIO: the whole tree costs one buffer, and the table says how big" { try testing.expectEqual(@as(usize, 143), out_capacity); try testing.expectEqual(@as(usize, 143), @sizeOf(Board)); - try testing.expectEqual(@as(usize, 28), Board.name_capacity); + try testing.expectEqual(@as(usize, 28), ninep.board.name_capacity); try testing.expect(Header.text.len > out_capacity); } @@ -511,11 +475,9 @@ test "GPIO: stat and truncation preserve the register value and reads respect of try testing.expectEqualStrings(Header.text[10..22], text.bytes); } -const ninep = @import("9p.zig"); - test "GPIO: a real 9P client reads a pin's value off this tree" { StubPads.reset(); - const Server = ninep.Server(Board, ninep.board_fids); + const Server = ninep.Server(Board, ninep.board); var in: [1024]u8 = undefined; var out: [2048]u8 = undefined; var fsys: Board = .{}; diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index f716bf5f..56c3112d 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -1,11 +1,14 @@ //! The control tree served over 9P: its nodes, lookup, readdir and the -//! dispatch that answers the engine in src/9p.zig. The backend contract -//! (Req, Reply, Op, Status, E) lives here; fs.zig keeps host access and mounts. +//! dispatch that answers the engine (cloud9.fs.Server, configured in +//! src/9p.zig). The backend contract (Req, Reply, Op, Status, E) is cloud9's, +//! extended here with the editor's reply payload; fs.zig keeps host access +//! and mounts. //! //! /README /index /ctl /new /log /screen /listeners //! /pane/<serial>/{name,body,tag,ctl,addr,data,xdata,sel,errors,event,pty/} //! /os/... the host filesystem /src/... embedded sources (opt-in) const std = @import("std"); +const cloud9 = @import("cloud9"); const pardes = @import("../pardes.zig"); const fs = @import("../fs.zig"); pub const events = @import("events.zig"); @@ -22,84 +25,35 @@ const MAX_PANES = pardes.MAX_PANES; pub const name_capacity: usize = 255; -// ---- the backend contract ---- - -pub const Op = enum(u8) { - lookup, - getattr, - setattr, - open, - read, - write, - release, - readdir, -}; +comptime { + // The editor's engine (src/9p.zig) remembers names this long. + std.debug.assert(@import("../9p.zig").editor.name_capacity == name_capacity); +} -pub const Status = enum(u8) { - ok, - again, - err, -}; +// ---- the backend contract: cloud9.fs, with the editor's payload locator ---- -pub const Req = struct { - tag: u64, - op: Op, - node: u64, - handle: u32 = 0, - off: u64 = 0, - size: u32 = 0, - data: []const u8 = &.{}, - truncate: bool = false, +pub const Op = cloud9.fs.Op; +pub const Status = cloud9.fs.Status; +pub const Req = cloud9.fs.Req; +pub const E = cloud9.fs.E; - pub fn changesPane(req: Req) bool { - return switch (req.op) { - .write, .setattr => true, - .open => req.node == @intFromEnum(TopFile.new), - .lookup, .getattr, .read, .release, .readdir => false, - }; - } +/// Where a reply's bytes live. The engine ignores it; `Pardes.fsPayload` +/// resolves it into the slice that travels beside the reply. +pub const Payload = union(enum) { + none, + staged: u32, + region: struct { pane: u8, serial: u32, off: u32, len: u32 }, }; -pub const Reply = struct { - tag: u64, - status: Status = .ok, - errno: u16 = 0, - attr: Attr = .{}, - handle: u32 = 0, - payload: Payload = .none, - written: u32 = 0, - - pub const Attr = struct { - name: []const u8 = "", - node: u64 = 0, - dir: bool = false, - size: u64 = 0, - mode: u16 = 0o644, - mtime: u32 = 0, - }; +pub const Reply = cloud9.fs.ReplyWith(Payload); - pub const Payload = union(enum) { - none, - staged: u32, - region: struct { pane: u8, serial: u32, off: u32, len: u32 }, +pub fn changesPane(req: Req) bool { + return switch (req.op) { + .write, .setattr => true, + .open => req.node == @intFromEnum(TopFile.new), + .lookup, .getattr, .read, .release, .readdir => false, }; - - pub fn fail(tag: u64, e: u16) Reply { - return .{ .tag = tag, .status = .err, .errno = e }; - } -}; - -pub const E = struct { - pub const PERM: u16 = 1; - pub const NOENT: u16 = 2; - pub const IO: u16 = 5; - pub const NOMEM: u16 = 12; - pub const NOTDIR: u16 = 20; - pub const INVAL: u16 = 22; - pub const NFILE: u16 = 23; - pub const NOSPC: u16 = 28; - pub const NOSYS: u16 = 38; -}; +} pub const out_reserve = 4 * 1024; diff --git a/src/pardes.zig b/src/pardes.zig index 88442542..ed71102e 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -7049,7 +7049,7 @@ pub const Pardes = struct { p.shell_rows.sweep(p.gpa); switch (ev) { .tick => {}, - .fs_req => |req| if (req.changesPane()) { + .fs_req => |req| if (ctlfs.changesPane(req)) { p.raw_hover_intent = false; p.cancelLookHover(); }, |
