summaryrefslogtreecommitdiff
path: root/src/fs.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/fs.zig')
-rw-r--r--src/fs.zig2334
1 files changed, 2334 insertions, 0 deletions
diff --git a/src/fs.zig b/src/fs.zig
new file mode 100644
index 0000000..69cdf9f
--- /dev/null
+++ b/src/fs.zig
@@ -0,0 +1,2334 @@
+//! An asynchronous 9P2000 file-server engine over `Server` (the session).
+//!
+//! The engine turns each request into a job of one or more backend
+//! operations (`Req`), issued through `next()`/`retry()` and answered by tag
+//! through `reply()`; a backend may answer at once or later, and may park a
+//! read or write with `Status.again`. The engine owns fids, permissions, the
+//! directory-read cursor, Tflush and the releases a dropped connection owes.
+//! Like the session it is built on, it is allocation-free, makes no OS
+//! calls, and is driven by `push()`/`output()`/`wrote()`. Its tables are
+//! sized by comptime `Options`. See docs/design.md, "File server engine".
+const std = @import("std");
+const assert = std.debug.assert;
+const testing = std.testing;
+const c9 = @import("root.zig");
+const wire = @import("wire.zig");
+const Protocol = @import("Server.zig");
+
+const Qid = wire.Qid;
+const Stat = wire.Stat;
+const Msg = wire.Msg;
+const Decoded = wire.Decoded;
+const header_len = wire.header_len;
+const qid_len = wire.qid_len;
+const stat_fixed = wire.stat_fixed;
+const max_welem = wire.max_welem;
+const iohdrsz = wire.iohdrsz;
+const notag = wire.notag;
+const nofid = wire.nofid;
+const qtdir = wire.qtdir;
+const qtfile = wire.qtfile;
+const dmdir = wire.dmdir;
+
+// ---- the backend contract ----
+
+/// What the engine asks of a backend. Every request names a node; open,
+/// read, write, readdir and release also carry the handle open returned.
+pub const Op = enum(u8) {
+ lookup,
+ getattr,
+ setattr,
+ open,
+ read,
+ write,
+ release,
+ readdir,
+};
+
+/// `again` parks a read or write until the backend answers the same tag
+/// later (or the engine retries it); any other op answering `again` fails.
+pub const Status = enum(u8) {
+ ok,
+ again,
+ err,
+};
+
+/// Node attributes a lookup, getattr or setattr answers with. `node` zero
+/// keeps the node the engine asked about; `mode` is the permission bits.
+pub const Attr = struct {
+ name: []const u8 = "",
+ node: u64 = 0,
+ dir: bool = false,
+ size: u64 = 0,
+ mode: u16 = 0o644,
+ mtime: u32 = 0,
+};
+const attr_type = Attr;
+
+/// One backend operation. `tag` is unique per connection and never zero.
+/// `data` is a lookup's name or a write's bytes and is borrowed until the
+/// reply, unless the write parks, in which case the engine keeps a copy.
+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,
+};
+
+/// A reply carrying an application-defined `payload`: a locator for the
+/// reply's bytes that the engine ignores and the application resolves
+/// before calling `reply()`. `Payload` is `void` or a type with a `none`
+/// tag, which is the default value.
+pub fn ReplyWith(comptime Payload: type) type {
+ return struct {
+ tag: u64,
+ status: Status = .ok,
+ errno: u16 = 0,
+ attr: attr_type = .{},
+ handle: u32 = 0,
+ payload: Payload = noPayload(Payload),
+ written: u32 = 0,
+
+ pub const Attr = attr_type;
+
+ pub fn fail(tag: u64, e: u16) @This() {
+ return .{ .tag = tag, .status = .err, .errno = e };
+ }
+ };
+}
+
+fn noPayload(comptime Payload: type) Payload {
+ return if (Payload == void) {} else .none;
+}
+
+/// The backend's answer to one `Req`, matched by tag. The bytes of a read
+/// or readdir travel beside it as the `bytes` argument of `reply()`.
+pub const Reply = ReplyWith(void);
+
+/// The errno values a backend may answer with; `errString` names each.
+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;
+};
+
+/// Longest Rerror string the engine emits.
+pub const errmax: usize = 128;
+
+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";
+
+/// The Rerror string for a backend errno, in the spelling Linux v9fs maps back.
+pub fn errString(errno: u16) []const u8 {
+ return switch (errno) {
+ E.PERM => "Operation not permitted",
+ E.NOENT => "No such file or directory",
+ E.IO => "Input/output error",
+ E.NOMEM => "Cannot allocate memory",
+ E.NOTDIR => "Not a directory",
+ E.INVAL => "Invalid argument",
+ E.NFILE => "Too many open files in system",
+ E.NOSPC => "No space left on device",
+ E.NOSYS => "Function not implemented",
+ else => "Input/output error",
+ };
+}
+
+/// The modes directory entries report; a stat of the entry gives the real ones.
+pub const dirent_dir_perm: u16 = 0o500;
+pub const dirent_file_perm: u16 = 0o600;
+
+/// The smallest msize the engine negotiates: one full Rwalk must fit.
+pub const msize_min: u32 = header_len + 2 + max_welem * qid_len;
+
+/// Comptime capacities of one engine instance. The defaults are the
+/// editor's, except the name capacity, which is the board's.
+pub const Options = struct {
+ /// Fids one connection may hold at once.
+ fid_capacity: usize = 256,
+ /// Parked reads and writes one connection may hold at once.
+ slot_capacity: usize = 32,
+ /// Longest write payload a parked write keeps a copy of.
+ park_data_max: usize = 128,
+ /// Longest file name a fid remembers; longer walk elements are illegal.
+ name_capacity: usize = 28,
+ /// Longest attach uname kept for the uid, gid and muid of stats.
+ username_capacity: usize = 28,
+};
+
+comptime {
+ const defaults: Options = .{};
+ assert(msize_min == 217);
+ assert(header_len + 2 + stat_fixed + defaults.name_capacity + 3 * defaults.username_capacity <= msize_min);
+ assert(header_len + 4 + stat_fixed + defaults.name_capacity + 3 * defaults.username_capacity <= msize_min);
+ assert(defaults.username_capacity >= 20);
+ assert(c9.oread | c9.owrite | c9.ordwr | c9.oexec == 3);
+ assert(c9.otrunc | c9.ocexec | c9.orclose == 112);
+}
+
+fn qidOf(node: u64, dir: bool) Qid {
+ return .{ .type = if (dir) qtdir else qtfile, .version = 0, .path = node };
+}
+
+/// The engine for a `Backend` that declares `Req` (this module's `Req`) and
+/// `Reply` (this module's `Reply`, or a `ReplyWith`). Drive it like the
+/// session: `push()` bytes in, answer every `retry()` then every `next()`
+/// request through `reply()`, drain `output()` and report `wrote()`. On
+/// `protocol.dead` call `hangup()`, answer the releases `next()` still
+/// yields, and `init()` again.
+pub fn Server(comptime Backend: type, comptime opts: Options) type {
+ if (opts.fid_capacity == 0) @compileError("9P server needs at least one fid");
+ if (opts.slot_capacity == 0) @compileError("9P server needs at least one parking slot");
+ if (opts.name_capacity == 0 or opts.name_capacity > 255) @compileError("9P backend name capacity must fit a directory entry");
+ if (opts.username_capacity == 0) @compileError("9P server needs room for a user name");
+ if (Backend.Req != Req) @compileError("9P backend requests must be cloud9.fs.Req");
+ inline for (.{ "tag", "status", "errno", "attr", "handle", "written" }) |field| {
+ if (!@hasField(Backend.Reply, field)) @compileError("9P backend replies must be cloud9.fs.Reply or a cloud9.fs.ReplyWith");
+ }
+ if (@FieldType(Backend.Reply, "attr") != Attr) @compileError("9P backend replies must carry cloud9.fs.Attr");
+ const name_capacity = opts.name_capacity;
+ const username_capacity = opts.username_capacity;
+ const park_data_max = opts.park_data_max;
+ return struct {
+ const Self = @This();
+
+ protocol: Protocol,
+ root: u64,
+
+ uname: [username_capacity]u8 = @splat(0),
+ uname_len: u8 = 0,
+
+ fids: [opts.fid_capacity]Fid = @splat(.{}),
+ slots: [opts.slot_capacity]Slot = @splat(.{}),
+ job: Job = .{},
+ seq: u64 = 0,
+
+ pub const options = opts;
+
+ const Kind = enum { none, attach, walk, open, read, readdir, write, clunk, remove, stat, wstat };
+
+ pub 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: Backend.Req = undefined,
+ data: [park_data_max]u8 = undefined,
+ };
+
+ const Job = struct {
+ kind: Kind = .none,
+ tag: u16 = 0,
+ req_tag: u64 = 0,
+ req: Backend.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 InitOptions = struct {
+ in: []u8,
+ out: []u8,
+ root: u64,
+ };
+
+ pub fn init(o: InitOptions) Self {
+ assert(o.in.len >= msize_min);
+ assert(o.out.len >= 2 * msize_min);
+ assert(o.root != 0);
+ return .{ .protocol = .init(.{ .in = o.in, .out = o.out }), .root = o.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);
+ }
+
+ /// The oldest parked request not yet retried this round, or null when
+ /// every parked request has been retried (which starts a new round).
+ pub fn retry(s: *Self) ?Backend.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;
+ }
+
+ /// The next backend operation, or null while one is outstanding, the
+ /// output is full, or no whole request has arrived.
+ pub fn next(s: *Self) ?Backend.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;
+ }
+ }
+
+ /// Answer a request by tag; `bytes` are a read's or readdir's data.
+ /// A tag the engine no longer waits for (flushed, hung up) is ignored.
+ pub fn reply(s: *Self, r: *const Backend.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) ?Backend.Req {
+ for (&s.fids) |*f| {
+ if (!f.used or !f.orphan) continue;
+ assert(f.open);
+ const req: Backend.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 engine'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 & c9.orclose != 0) return s.fail(tag, e_perm);
+ const rw = mode & 3;
+ if (rw == c9.oexec) return s.fail(tag, e_perm);
+ if (f.dir and (rw != c9.oread or mode & c9.otrunc != 0)) return s.fail(tag, e_perm);
+ var need: u16 = 0;
+ if (rw == c9.oread or rw == c9.ordwr) need |= 0o400;
+ if (rw == c9.owrite or rw == c9.ordwr or mode & c9.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) == c9.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) == c9.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; a 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: Backend.Req) Backend.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) ?Backend.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 & c9.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) ?Backend.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 Backend.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 (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 & c9.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 ((wire.encodedLen(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 Backend.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.* = .{};
+ }
+
+ /// A readdir answers records of `node:u64le dir:u8 len:u8 name`; the
+ /// engine encodes whole Stat entries into the output frame directly.
+ 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: 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 = a.mtime,
+ .length = a.size,
+ .name = f.name[0..f.name_len],
+ .uid = who,
+ .gid = who,
+ .muid = who,
+ };
+ }
+ };
+}
+
+// ---- tests: a stub backend, the engine, and the client against it ----
+
+const oread = c9.oread;
+const owrite = c9.owrite;
+const ordwr = c9.ordwr;
+const oexec = c9.oexec;
+const otrunc = c9.otrunc;
+const orclose = c9.orclose;
+const encode = wire.encode;
+const decode = wire.decode;
+const frameLen = wire.frameLen;
+const Client = c9.Client;
+const max_tags = c9.max_tags;
+
+const req_type = Req;
+const reply_type = Reply;
+
+const StubFs = struct {
+ pub const Req = req_type;
+ pub const Reply = reply_type;
+
+ 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_type, 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) 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_type) Answer {
+ st.calls += 1;
+ const fail: Answer = .{ .reply = .{ .tag = req.tag, .status = .err, .errno = E.NOENT } };
+ 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 = E.NOTDIR } };
+ 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, .{});
+
+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: 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);
+ }
+
+ 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();
+ }
+};
+
+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 "fs 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 "fs 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 "fs 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 "fs 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 "fs server: backend-sized filenames survive walk and stat within negotiated msize" {
+ const Native = Server(StubFs, .{ .fid_capacity = 2, .name_capacity = 255 });
+ 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" ** Native.options.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 "fs server: open then read then clunk, and the release a clunk owes the backend" {
+ 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 "fs 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 "fs 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 "fs 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 "fs 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..Srv.options.slot_capacity) |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 "fs 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 "fs 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 "fs 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 "fs 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 "fs 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 "fs 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 "fs server: invalid framing and reply types terminate the connection" {
+ for ([_]u8{ @intFromEnum(wire.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 "fs server: a connection that drops still pays the backend 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 "fs server: every errno the backend can answer is a string Linux knows" {
+ try testing.expectEqualStrings("Operation not permitted", errString(E.PERM));
+ try testing.expectEqualStrings("No such file or directory", errString(E.NOENT));
+ try testing.expectEqualStrings("Input/output error", errString(E.IO));
+ try testing.expectEqualStrings("Cannot allocate memory", errString(E.NOMEM));
+ try testing.expectEqualStrings("Not a directory", errString(E.NOTDIR));
+ try testing.expectEqualStrings("Invalid argument", errString(E.INVAL));
+ try testing.expectEqualStrings("Too many open files in system", errString(E.NFILE));
+ try testing.expectEqualStrings("No space left on device", errString(E.NOSPC));
+ try testing.expectEqualStrings("Function not implemented", errString(E.NOSYS));
+ 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 "fs server: the fid capacity sizes the actual fid storage, and nothing else" {
+ const Small = Server(StubFs, .{ .fid_capacity = 32 });
+ const Large = Server(StubFs, .{ .fid_capacity = 256 });
+ const SmallFids = @FieldType(Small, "fids");
+ const LargeFids = @FieldType(Large, "fids");
+ try testing.expectEqual(32, @typeInfo(SmallFids).array.len);
+ try testing.expectEqual(256, @typeInfo(LargeFids).array.len);
+ try testing.expectEqual(32 * @sizeOf(Small.Fid), @sizeOf(SmallFids));
+ try testing.expectEqual(256 * @sizeOf(Large.Fid), @sizeOf(LargeFids));
+ try testing.expectEqual(
+ @sizeOf(LargeFids) - @sizeOf(SmallFids),
+ @sizeOf(Large) - @sizeOf(Small),
+ );
+ try testing.expect(@sizeOf(SmallFids) <= 3 * 1024);
+ try testing.expect(@sizeOf(LargeFids) <= 24 * 1024);
+ try testing.expect(@sizeOf(@FieldType(Small, "slots")) <= 8 * 1024);
+ try testing.expect(3 * 4096 + @sizeOf(Small) <= 24 * 1024);
+}
+
+test "fs server: a reply type with a payload locator is accepted and ignored" {
+ const Payload = union(enum) { none, staged: u32 };
+ const Located = struct {
+ pub const Req = req_type;
+ pub const Reply = ReplyWith(Payload);
+ };
+ const Engine = Server(Located, .{ .fid_capacity = 2 });
+ const r: Located.Reply = .{ .tag = 1 };
+ try testing.expect(r.payload == .none);
+ try testing.expect(Located.Reply.Attr == Attr);
+ try testing.expectEqual(E.IO, Located.Reply.fail(1, E.IO).errno);
+ var in: [1024]u8 = undefined;
+ var out: [2048]u8 = undefined;
+ var encoded: [64]u8 = undefined;
+ var server = Engine.init(.{ .in = &in, .out = &out, .root = 1 });
+ _ = server.push(try encode(.{ .tversion = .{ .msize = 1024, .version = "9P2000" } }, notag, &encoded));
+ try testing.expectEqual(null, server.next());
+ server.wrote(server.output().len);
+ _ = server.push(try encode(.{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "u", .aname = "" } }, 1, &encoded));
+ const getattr = server.next() orelse return error.MissingGetattr;
+ server.reply(&.{ .tag = getattr.tag, .attr = .{ .dir = true, .mode = 0o500 }, .payload = .{ .staged = 0 } }, "");
+ try testing.expectEqual(null, server.next());
+ const got = try decode(server.output());
+ try testing.expectEqual(@as(u64, 1), got.msg.rattach.qid.path);
+}
+
+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: 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 "fs client: a whole session against the engine" {
+ 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 "fs 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 "fs 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(E.NOENT), 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 "fs 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 "fs client: sixteen tags outstanding against the engine, 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 "fs 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 "fs 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 "fs 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 = .{} }));
+}