//! Pardes filesystem adapter. The 9P protocol and transports live in cloud9. const std = @import("std"); const assert = std.debug.assert; const testing = std.testing; const cloud9 = @import("cloud9"); pub const Error = cloud9.Error; pub const Type = cloud9.Type; pub const isT = cloud9.isT; pub const header_len = cloud9.header_len; pub const qid_len = cloud9.qid_len; pub const stat_fixed = cloud9.stat_fixed; pub const notag = cloud9.notag; pub const nofid = cloud9.nofid; pub const max_welem = cloud9.max_welem; pub const iohdrsz = cloud9.iohdrsz; pub const Qid = cloud9.Qid; pub const Stat = cloud9.Stat; pub const Msg = cloud9.Msg; pub const Decoded = cloud9.Decoded; pub const frameLen = cloud9.frameLen; pub const encode = cloud9.encode; pub const decode = cloud9.decode; pub const qtdir = cloud9.qtdir; pub const qtappend = cloud9.qtappend; pub const qtexcl = cloud9.qtexcl; pub const qtmount = cloud9.qtmount; pub const qtauth = cloud9.qtauth; pub const qttmp = cloud9.qttmp; pub const qtfile = cloud9.qtfile; pub const dmdir = cloud9.dmdir; pub const dmappend = cloud9.dmappend; pub const dmexcl = cloud9.dmexcl; pub const dmmount = cloud9.dmmount; pub const dmauth = cloud9.dmauth; pub const dmtmp = cloud9.dmtmp; 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 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", }; } 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; 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 = 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); } 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 "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 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(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); }