From 66f2e492c348677ab3050f5e378b9eb4c04c98ce Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 20 Sep 2026 01:47:28 -0300 Subject: 9proc core becomes a backend of cloud9.fs; engine gains optional features 9proc's own fid table, walk loop and dir-read engine are replaced by cloud9.fs.Server; the tree (static, vars, providers) is served through the engine's Req/Reply contract with node ids that keep the old qid scheme. Providers may answer later by returning error.Again (parked in the engine, retried each step, Tflush -> EINTR); no new files are exposed. Engine (backward compatible, all opt-in via Backend.features / Options): create, remove, wstat, reference accounting for backends that count handles, a salted fid index, name_capacity 0 (names from getattr), Reply.ename for backend-chosen error text, Attr.path/version/atime. Engine-level error strings and the 217-byte msize floor now apply to 9proc; tests updated accordingly. Co-Authored-By: Claude Fable 5.1 --- 9proc/src/core.zig | 1181 ++++++++++++++++++++------------------------- 9proc/src/linux/probe.zig | 2 +- 9proc/src/scratch.zig | 30 +- 3 files changed, 552 insertions(+), 661 deletions(-) (limited to '9proc/src') diff --git a/9proc/src/core.zig b/9proc/src/core.zig index cead8f3..ce71809 100644 --- a/9proc/src/core.zig +++ b/9proc/src/core.zig @@ -1,12 +1,14 @@ -//! The freestanding 9P2000 introspection engine: a static tree generated at +//! The freestanding 9P2000 introspection tree: a static tree generated at //! comptime from a `Config` (README, /build, /comptime, /runtime/fn, /ctl, -//! /vars) plus runtime `Provider`s mounted at the top level, served over a -//! `cloud9.Server` connection. No allocator, no OS, no threads: every buffer is -//! caller-owned (`Storage`, `Shared`, `Conn`), every table is sized at comptime. -//! See docs/LIBRARY.md. +//! /vars) plus runtime `Provider`s mounted at the top level, served as a +//! backend of `cloud9.fs.Server` (the file-server engine, which owns fids, +//! walks, directory cursors, permissions and Tflush). No allocator, no OS, +//! no threads: every buffer is caller-owned (`Storage`, `Shared`, `Conn`), +//! every table is sized at comptime. See docs/LIBRARY.md. const std = @import("std"); const builtin = @import("builtin"); const cloud9 = @import("cloud9"); +const fs = cloud9.fs; const vars = @import("vars.zig"); const Writer = std.Io.Writer; @@ -50,6 +52,8 @@ pub const Config = struct { /// slots so that reads at arbitrary offsets are consistent. snapshot_slots: u8 = 8, snapshot_bytes: u32 = 16 * 1024, + /// Reads a provider has parked with `error.Again`, per connection. + max_parked: u8 = 8, }; /// Attributes of a provider node, filled by `VTable.stat` and `VTable.list`. @@ -85,6 +89,16 @@ pub const NodeStat = struct { /// `clunk(0)` as a no-op. `walk` must accept "." on any node, file or directory /// (a fresh reference to the same node; the core clones fids with it), and ".." /// on directories (except at the root, which the core resolves itself). +/// +/// Answering later: `read` may return `error.Again` when nothing is there +/// yet. The request then parks in the engine (up to `Config.max_parked` per +/// connection; a further one fails with EAGAIN) and every later `Conn.step` +/// asks the provider again, with the same handle and offset, until it +/// answers; the fid stays open. A Tflush of a parked read answers it with +/// "Interrupted system call"; a hangup or Tversion drops it and closes the +/// file as usual. Nothing wakes a connection by itself: the platform layer +/// steps it when its transport moves, so a provider that becomes ready has +/// to make that happen (out of the core's hands). pub const Provider = struct { name: []const u8, ctx: *anyopaque, @@ -94,7 +108,7 @@ pub const Provider = struct { pub const Handle = u64; pub const root: Handle = 0; - pub const Error = error{ NotFound, Exists, Perm, NotDir, IsDir, NotEmpty, BadOffset, NoSpace, Io, Unsupported, Excl }; + pub const Error = error{ NotFound, Exists, Perm, NotDir, IsDir, NotEmpty, BadOffset, NoSpace, Io, Unsupported, Excl, Again }; pub const VTable = struct { walk: *const fn (ctx: *anyopaque, parent: Handle, name: []const u8) Error!Handle, @@ -102,6 +116,7 @@ pub const Provider = struct { /// The `index`-th entry of `dir`; false when done. list: *const fn (ctx: *anyopaque, dir: Handle, index: usize, out: *NodeStat) Error!bool, open: *const fn (ctx: *anyopaque, h: Handle, mode: u8) Error!void, + /// `error.Again` parks the read; see `Provider`. read: *const fn (ctx: *anyopaque, h: Handle, offset: u64, buf: []u8) Error!usize, write: *const fn (ctx: *anyopaque, h: Handle, offset: u64, data: []const u8) Error!usize, /// Returns the new node, already open with `mode`. @@ -117,7 +132,8 @@ pub const Provider = struct { }; }; -/// The Plan 9 error string for any error the engine or a provider can raise. +/// The Plan 9 error string for any error the tree or a provider can raise +/// (the engine's own refusals carry cloud9.fs's strings). pub fn ename(err: anyerror) []const u8 { return switch (err) { error.NotFound, error.NoFile => "file does not exist", @@ -144,10 +160,28 @@ pub fn ename(err: anyerror) []const u8 { error.WriteFailed => "no space in buffer", error.ReplyTooLarge => "reply too large for msize", error.OutOfMemory => "out of memory", + error.Again => "would block", else => "i/o error", }; } +/// The engine errno closest to an error, beside its `ename`. +fn errno(err: anyerror) u16 { + return switch (err) { + error.NotFound, error.NoFile => fs.E.NOENT, + error.Perm, error.Excl => fs.E.PERM, + error.Exists => fs.E.EXIST, + error.NotEmpty => fs.E.NOTEMPTY, + error.NotDir => fs.E.NOTDIR, + error.IsDir => fs.E.ISDIR, + error.BadOffset, error.Invalid, error.BadValue, error.BadCommand, error.BadName => fs.E.INVAL, + error.NoSpace, error.WriteFailed, error.NoSnapshot => fs.E.NOSPC, + error.Unsupported => fs.E.NOSYS, + error.OutOfMemory => fs.E.NOMEM, + else => fs.E.IO, + }; +} + /// A "don't care" Twstat: every field left as it is. pub const stat_dontcare: cloud9.Stat = .{ .type = 0xFFFF, @@ -380,24 +414,26 @@ pub fn Server(comptime cfg: Config) type { comptime { for (flat) |f| if (f.node.kind == .dynamic and f.node.gen == null) @compileError("dynamic node without generator"); - std.debug.assert(cfg.msize >= cloud9.Server.msize_min); - std.debug.assert(cfg.snapshot_slots > 0 and cfg.max_fids > 0); + std.debug.assert(cfg.msize >= fs.msize_min); + std.debug.assert(cfg.snapshot_slots > 0 and cfg.max_fids > 0 and cfg.max_parked > 0); // Provider index 0xFE/0xFF would collide with the var/static qid tags. std.debug.assert(cfg.max_providers < 0xFE); } - // -- qid paths ------------------------------------------------------ + // -- node ids and qid paths ------------------------------------------ const static_tag: u64 = 0xFF << 56; const var_tag: u64 = 0xFE << 56; const handle_mask: u64 = (1 << 56) - 1; + /// The engine's root: static node 0. + const root_id: u64 = static_tag; // -- storage -------------------------------------------------------- /// Per-connection buffers; the caller places one in static memory. pub const Storage = struct { in: [cfg.msize]u8, - out: [cfg.msize]u8, + out: [@max(cfg.msize, 2 * fs.msize_min)]u8, /// Staging area for read replies (directory records, provider and raw reads). data: [cfg.msize]u8, snapshots: [cfg.snapshot_slots][cfg.snapshot_bytes]u8, @@ -426,9 +462,9 @@ pub fn Server(comptime cfg: Config) type { /// Length of the current ctl result (in `ctl_bufs[ctl_cur]`). ctl_len: u32 = 0, ctl_version: u32 = 0, - /// Entropy for the per-connection fid hash. The core mixes in a + /// Entropy for the per-connection fid index. The core mixes in a /// connection counter and buffer addresses; a platform layer with a - /// random source may set this once after `init` to make the seed + /// random source may set this once after `init` to make the salt /// unpredictable even where addresses are static. hash_seed: u32 = 0, conn_seq: u32 = 0, @@ -486,47 +522,44 @@ pub fn Server(comptime cfg: Config) type { return null; } - // -- connection ----------------------------------------------------- + // -- the engine and its backend --------------------------------------- + /// What the engine knows of this tree: the contract types and the + /// capabilities it may ask for. The requests themselves are served + /// by `Conn.serve`. + pub const Backend = struct { + pub const Req = fs.Req; + pub const Reply = fs.Reply; + pub const features: fs.Features = .{ .create = true, .remove = true, .wstat = true, .references = true }; + }; + + /// The file-server engine this tree is a backend of: it owns the fid + /// table, permissions, directory cursors, Tflush and the releases a + /// dropped connection owes. Names are not kept per fid (`getattr` + /// answers them), so a fid costs the same whatever the name length. + pub const Engine = fs.Server(Backend, .{ + .fid_capacity = cfg.max_fids, + .slot_capacity = cfg.max_parked, + .park_data_max = 0, + .name_capacity = 0, + .fid_index = true, + }); + + /// A reference to a node of the tree: the engine's node id decoded. const NodeRef = union(enum) { static: u32, prov: struct { idx: u8, h: Provider.Handle }, @"var": struct { idx: u8, node: u32 }, }; - const Fid = struct { - id: u32 = 0, - used: bool = false, - node: NodeRef = .{ .static = 0 }, - is_dir: bool = true, - open: bool = false, - mode: u8 = 0, - rclose: bool = false, - dir_offset: u64 = 0, - dir_index: usize = 0, - /// Snapshot slot of an open dynamic file. - snap: ?u8 = null, - /// Free-list link, meaningful while `!used`. - next_free: u16 = no_slot, - }; - - const no_slot: u16 = std.math.maxInt(u16); - /// The fid index is an open-addressing (linear probing) table from fid - /// number to a slot of `Conn.fids`, sized to stay at most half full so - /// that lookups are O(1) with any number of fids. - const index_len: usize = std.math.ceilPowerOfTwoAssert(usize, @as(usize, cfg.max_fids) * 2); - const index_mask: usize = index_len - 1; - const index_shift: u5 = @intCast(32 - @as(usize, std.math.log2_int(usize, index_len))); - - /// MurmurHash3's 32-bit finalizer: every input bit affects every output bit. - fn fmix32(x: u32) u32 { - var h = x; - h ^= h >> 16; - h *%= 0x85EB_CA6B; - h ^= h >> 13; - h *%= 0xC2B2_AE35; - h ^= h >> 16; - return h; + fn nodeId(ref: NodeRef) u64 { + return switch (ref) { + .static => |idx| static_tag | idx, + .@"var" => |v| var_tag | (@as(u64, v.idx) << 32) | v.node, + // Provider ids are offset by one: the engine reads node 0 as + // "the node asked about", and provider 0's root would be 0. + .prov => |p| (@as(u64, p.idx + 1) << 56) | (p.h & handle_mask), + }; } /// Everything the engine needs to know about a node for qid/stat. @@ -537,240 +570,166 @@ pub fn Server(comptime cfg: Config) type { atime: u32, mtime: u32, version: u32, + /// The qid path. path: u64, name: []const u8, - fn qid(i: Info) cloud9.Qid { - var t: u8 = if (i.is_dir) cloud9.qtdir else cloud9.qtfile; - if (i.mode & cloud9.dmappend != 0) t |= cloud9.qtappend; - if (i.mode & cloud9.dmexcl != 0) t |= cloud9.qtexcl; - return .{ .type = t, .version = i.version, .path = i.path }; - } - - fn stat(i: Info) cloud9.Stat { + fn attr(i: Info, ref: NodeRef) fs.Attr { + const id = nodeId(ref); return .{ - .type = 0, - .dev = 0, - .qid = i.qid(), - .mode = i.mode, - .atime = i.atime, - .mtime = i.mtime, - .length = i.length, .name = i.name, - .uid = cfg.name, - .gid = cfg.name, - .muid = cfg.name, + .node = id, + .dir = i.is_dir, + .size = i.length, + .mode = @truncate(i.mode), + .mtime = i.mtime, + .atime = i.atime, + .version = i.version, + .path = if (i.path != id) i.path else null, + .append = i.mode & cloud9.dmappend != 0, + .excl = i.mode & cloud9.dmexcl != 0, }; } }; - /// One 9P connection: a cloud9.Server plus a fid table and snapshot slots. + /// A directory entry as the engine's readdir record wants it. + const Entry = struct { path: u64, dir: bool, name: []const u8 }; + + /// The engine's handle of an open file that holds no snapshot slot. + const no_snapshot: u32 = std.math.maxInt(u32); + + /// One 9P connection: the engine plus this tree's per-connection + /// state (snapshot slots). pub const Conn = struct { shared: *Shared, storage: *Storage, - server: cloud9.Server, + engine: Engine, /// Largest msize this connection negotiates. msize_cap: u32, - fids: [cfg.max_fids]Fid = @splat(.{}), - /// XORed into every fid number before hashing so that a client - /// cannot precompute fid numbers that collide (which would turn the - /// index back into a linear scan). - hash_seed: u32, - /// fid number -> slot of `fids` (`no_slot` = empty bucket). - index: [index_len]u16 = @splat(no_slot), - /// Head of the free list threaded through `Fid.next_free`. - free_head: u16 = no_slot, - /// Slots `high_water..` have never been used (bump allocation). - high_water: u16 = 0, - nfids: u16 = 0, slot_used: [cfg.snapshot_slots]bool = @splat(false), slot_len: [cfg.snapshot_slots]u32 = @splat(0), - name_buf: [max_name]u8 = undefined, pub fn init(shared: *Shared, storage: *Storage, msize: u32) Conn { shared.conn_seq +%= 1; const addr = @intFromPtr(storage) ^ (@intFromPtr(shared) << 7); - const seed = fmix32(shared.hash_seed ^ (shared.conn_seq *% 0x9E37_79B1) ^ @as(u32, @truncate(addr)) ^ @as(u32, @truncate(addr >> 16))); + const seed = shared.hash_seed ^ (shared.conn_seq *% 0x9E37_79B1) ^ @as(u32, @truncate(addr)) ^ @as(u32, @truncate(addr >> 16)); + const cap = @max(@min(msize, cfg.msize), fs.msize_min); return .{ .shared = shared, .storage = storage, - .server = .init(.{ .in = &storage.in, .out = &storage.out }), - .msize_cap = @max(@min(msize, cfg.msize), cloud9.Server.msize_min), - .hash_seed = seed, + .engine = .init(.{ .in = storage.in[0..cap], .out = &storage.out, .root = root_id, .seed = seed }), + .msize_cap = cap, }; } - /// Fibonacci hashing of the (seeded) fid number into `index_len` buckets. - fn fidHome(c: *const Conn, id: u32) usize { - return @intCast(((id ^ c.hash_seed) *% 0x9E37_79B1) >> index_shift); - } - /// Feeds transport bytes; returns how many were taken. pub fn push(c: *Conn, bytes: []const u8) usize { - return c.server.push(bytes); + return c.engine.push(bytes); } /// Bytes to send to the client. pub fn output(c: *const Conn) []const u8 { - return c.server.output(); + return c.engine.output(); } pub fn wrote(c: *Conn, n: usize) void { - c.server.wrote(n); + c.engine.wrote(n); + } + + /// Free space in the input buffer (one msize-sized frame at most). + pub fn inputRoom(c: *const Conn) usize { + return c.engine.protocol.in.len - c.engine.protocol.in_len; } /// Drops every fid (telling providers) and kills the session. pub fn hangup(c: *Conn) void { - c.resetFids(); - c.server.hangup(); + c.engine.hangup(); + while (c.engine.next()) |req| c.serve(req); } - /// Handles at most one request. Returns false when more input (or - /// output drainage) is needed. `error.Protocol` is terminal. + /// Serves at most one request of the engine (a 9P request is one + /// or more of them), after re-asking providers about every parked + /// read. Returns false when more input (or output drainage) is + /// needed. `error.Protocol` is terminal. pub fn step(c: *Conn) error{Protocol}!bool { - const req = (c.server.receive() catch return error.Protocol) orelse return false; - defer c.server.release(); - switch (req.msg) { - .tversion => |m| { - c.resetFids(); - c.server.negotiate(@min(m.msize, c.msize_cap), m.version) catch return error.Protocol; - }, - else => { - const reply = c.dispatch(req.msg) catch |e| cloud9.Msg{ .rerror = .{ .ename = ename(e) } }; - c.server.reply(req.tag, reply) catch |e| switch (e) { - // The reply does not fit the negotiated msize (Rstat or a long - // Rwalk at a tiny msize). receive() guarantees room for one - // msize-sized message, so this is never backpressure: answer with - // an Rerror (truncated to fit by cloud9). Rwalk, the only - // variable-size reply that follows a state change, is size-checked - // in walk() before anything is mutated. - error.TooLarge => c.server.reply(req.tag, .{ .rerror = .{ .ename = ename(error.ReplyTooLarge) } }) catch return error.Protocol, - else => return error.Protocol, - }; - }, - } + if (c.engine.protocol.dead) return error.Protocol; + while (c.engine.retry()) |req| c.serve(req); + const req = c.engine.next() orelse { + if (c.engine.protocol.dead) return error.Protocol; + return false; + }; + c.serve(req); + if (c.engine.protocol.dead) return error.Protocol; return true; } /// Number of fids currently held. pub fn fidCount(c: *const Conn) usize { - return c.nfids; + return c.engine.fidCount(); } - fn dispatch(c: *Conn, msg: cloud9.Msg) anyerror!cloud9.Msg { - return switch (msg) { - .tauth => error.AuthNotRequired, - .tattach => |m| c.attach(m), - .tflush => .rflush, - .twalk => |m| c.walk(m), - .topen => |m| c.open(m), - .tcreate => |m| c.create(m), - .tread => |m| c.read(m), - .twrite => |m| c.write(m), - .tclunk => |m| c.clunk(m), - .tremove => |m| c.remove(m), - .tstat => |m| c.stat(m), - .twstat => |m| c.wstat(m), - else => error.Protocol, - }; - } - - // -- fid table -- - - /// The index bucket holding `id`, if any. - fn findBucket(c: *const Conn, id: u32) ?usize { - var pos = c.fidHome(id); - while (true) : (pos = (pos + 1) & index_mask) { - const slot = c.index[pos]; - if (slot == no_slot) return null; - if (c.fids[slot].id == id) return pos; + /// The provider handle a fid holds, if it holds a provider node. + pub fn providerHandle(c: *const Conn, fid: u32) ?Provider.Handle { + for (c.engine.fids) |f| { + if (!f.used or f.orphan or f.fid != fid) continue; + return switch (c.refOf(f.node) orelse return null) { + .prov => |p| p.h, + else => null, + }; } + return null; } - fn findFid(c: *Conn, id: u32) ?*Fid { - const pos = c.findBucket(id) orelse return null; - return &c.fids[c.index[pos]]; - } - - fn allocFid(c: *Conn, id: u32) !*Fid { - if (c.findBucket(id) != null) return error.FidInUse; - if (c.nfids >= cfg.max_fids) return error.TooManyFids; - const slot: u16 = if (c.free_head != no_slot) blk: { - const slot = c.free_head; - c.free_head = c.fids[slot].next_free; - break :blk slot; - } else blk: { - const slot = c.high_water; - c.high_water += 1; - break :blk slot; - }; - c.fids[slot] = .{ .id = id, .used = true }; - var pos = c.fidHome(id); - while (c.index[pos] != no_slot) pos = (pos + 1) & index_mask; - c.index[pos] = slot; - c.nfids += 1; - return &c.fids[slot]; - } - - /// Removes `id` from the index (backward-shift deletion: no tombstones). - fn unlinkFid(c: *Conn, id: u32) void { - var i = c.findBucket(id).?; - var j = i; - while (true) { - j = (j + 1) & index_mask; - const slot = c.index[j]; - if (slot == no_slot) break; - const k = c.fidHome(c.fids[slot].id); - // The entry at j may move into the hole at i unless its home - // lies in the cyclic interval (i, j]. - const stays = if (i <= j) (k > i and k <= j) else (k > i or k <= j); - if (!stays) { - c.index[i] = slot; - i = j; - } - } - c.index[i] = no_slot; - } + // -- serving the engine -- - /// Releases everything a fid holds; the slot stays allocated. - fn dropContents(c: *Conn, f: *Fid) void { - if (f.snap) |s| c.slot_used[s] = false; - f.snap = null; - if (f.node == .prov) { - const p = c.shared.providers[f.node.prov.idx]; - if (f.open) if (p.vtable.close) |close| close(p.ctx, f.node.prov.h); - if (f.rclose and f.open) if (p.vtable.remove) |rm| rm(p.ctx, f.node.prov.h) catch {}; - p.vtable.clunk(p.ctx, f.node.prov.h); - } - f.open = false; - f.rclose = false; + fn serve(c: *Conn, req: fs.Req) void { + var bytes: []const u8 = ""; + const reply = c.dispatch(req, &bytes) catch |e| failing(req.tag, e); + c.engine.reply(&reply, bytes); } - fn freeFid(c: *Conn, f: *Fid) void { - c.dropContents(f); - c.unlinkFid(f.id); - const slot: u16 = @intCast((@intFromPtr(f) - @intFromPtr(&c.fids)) / @sizeOf(Fid)); - f.* = .{ .next_free = c.free_head }; - c.free_head = slot; - c.nfids -= 1; + fn failing(tag: u64, e: anyerror) fs.Reply { + if (e == error.Again) return .{ .tag = tag, .status = .again }; + return .{ .tag = tag, .status = .err, .errno = errno(e), .ename = ename(e) }; } - fn resetFids(c: *Conn) void { - for (c.fids[0..c.high_water]) |*f| { - if (f.used) c.dropContents(f); - f.* = .{}; + fn dispatch(c: *Conn, req: fs.Req, bytes: *[]const u8) anyerror!fs.Reply { + const ref = c.refOf(req.node) orelse return error.NotFound; + switch (req.op) { + .lookup => { + const l = try c.lookup(ref, req.data); + return .{ .tag = req.tag, .attr = l.info.attr(l.ref) }; + }, + .getattr => return .{ .tag = req.tag, .attr = (try c.info(ref)).attr(ref) }, + .setattr => return c.setattr(req, ref), + .open => return if (req.create) c.create(req, ref) else c.open(req, ref), + .read => return c.read(req, ref, bytes), + .readdir => return c.readDir(req, ref, bytes), + .write => return c.write(req, ref), + .release => return c.release(req, ref), } - @memset(&c.index, no_slot); - c.free_head = no_slot; - c.high_water = 0; - c.nfids = 0; } - /// Releases a provider handle that is not held by any fid. - fn releaseRef(c: *Conn, ref: NodeRef) void { - if (ref == .prov) { - const p = c.shared.providers[ref.prov.idx]; - p.vtable.clunk(p.ctx, ref.prov.h); + /// Decodes an engine node id; null for one this tree never issued. + fn refOf(c: *const Conn, id: u64) ?NodeRef { + const top: u8 = @intCast(id >> 56); + switch (top) { + 0xFF => { + const idx = id & handle_mask; + if (idx >= flat_len) return null; + return .{ .static = @intCast(idx) }; + }, + 0xFE => { + const vidx: u8 = @truncate(id >> 32); + const node: u32 = @truncate(id); + if ((id >> 40) & 0xFFFF != 0 or vidx >= c.shared.nvars) return null; + if (node >= c.shared.vars[vidx].vt.nodes.len) return null; + return .{ .@"var" = .{ .idx = vidx, .node = node } }; + }, + else => { + if (top == 0 or top - 1 >= c.shared.nprov) return null; + return .{ .prov = .{ .idx = top - 1, .h = id & handle_mask } }; + }, } } @@ -835,6 +794,10 @@ pub fn Server(comptime cfg: Config) type { } } + fn provPath(idx: u8, h: Provider.Handle, st: NodeStat) u64 { + return (@as(u64, idx) << 56) | ((if (st.path != 0) st.path else h) & handle_mask); + } + fn provInfo(pr: Provider, idx: u8, h: Provider.Handle, st: NodeStat) Info { return .{ .is_dir = st.isDir(), @@ -843,24 +806,23 @@ pub fn Server(comptime cfg: Config) type { .atime = st.atime, .mtime = st.mtime, .version = st.version, - .path = (@as(u64, idx) << 56) | ((if (st.path != 0) st.path else h) & handle_mask), + .path = provPath(idx, h, st), .name = if (h == Provider.root) pr.name else st.name, }; } - /// Copies `st.name` into the connection so the reply cannot dangle. - fn pinName(c: *Conn, st: cloud9.Stat) cloud9.Stat { - var out = st; - const n = @min(st.name.len, c.name_buf.len); - @memcpy(c.name_buf[0..n], st.name[0..n]); - out.name = c.name_buf[0..n]; - return out; + /// Releases a provider handle that no fid holds. + fn releaseRef(c: *Conn, ref: NodeRef) void { + if (ref == .prov) { + const p = c.shared.providers[ref.prov.idx]; + p.vtable.clunk(p.ctx, ref.prov.h); + } } const Looked = struct { ref: NodeRef, info: Info }; /// Resolves `name` in the directory `ref`. A returned provider ref - /// is a fresh handle the caller must release or retain. + /// is a fresh handle the engine will release exactly once. fn lookup(c: *Conn, ref: NodeRef, name: []const u8) !Looked { switch (ref) { .static => |idx| { @@ -909,34 +871,39 @@ pub fn Server(comptime cfg: Config) type { } } - /// The i-th entry of directory `ref` as a Stat, or null past the end. - /// The name borrows either static memory or the provider's NodeStat. - fn entryStat(c: *Conn, ref: NodeRef, i: usize) !?cloud9.Stat { + /// The i-th entry of directory `ref`, or null past the end. The + /// name borrows static memory or the provider's NodeStat. + fn entry(c: *Conn, ref: NodeRef, i: usize) !?Entry { switch (ref) { .static => |idx| { const f = flat[idx]; if (f.node.kind == .vars) { if (i >= c.shared.nvars) return null; - return (try c.info(.{ .@"var" = .{ .idx = @intCast(i), .node = 0 } })).stat(); + return .{ .path = var_tag | (@as(u64, i) << 32), .dir = true, .name = c.shared.vars[i].name }; + } + if (i < f.count) { + const ci = f.first + @as(u32, @intCast(i)); + return .{ .path = static_tag | ci, .dir = flat[ci].node.isDir(), .name = flat[ci].node.name }; } - if (i < f.count) return (try c.info(.{ .static = f.first + @as(u32, @intCast(i)) })).stat(); if (idx == 0) { const pi = i - f.count; if (pi >= c.shared.nprov) return null; - return (try c.info(.{ .prov = .{ .idx = @intCast(pi), .h = Provider.root } })).stat(); + return .{ .path = @as(u64, pi) << 56, .dir = true, .name = c.shared.providers[pi].name }; } return null; }, .@"var" => |v| { - const n = c.shared.vars[v.idx].vt.nodes[v.node]; + const vt = c.shared.vars[v.idx].vt; + const n = vt.nodes[v.node]; if (i >= n.count) return null; - return (try c.info(.{ .@"var" = .{ .idx = v.idx, .node = n.first + @as(u32, @intCast(i)) } })).stat(); + const ci = n.first + @as(u32, @intCast(i)); + return .{ .path = var_tag | (@as(u64, v.idx) << 32) | ci, .dir = vt.nodes[ci].isDir(), .name = vt.nodes[ci].name }; }, .prov => |p| { const pr = c.provider(p.idx); var st: NodeStat = .{ .mode = 0 }; if (!try pr.vtable.list(pr.ctx, p.h, i, &st)) return null; - return provInfo(pr, p.idx, st.handle, st).stat(); + return .{ .path = provPath(p.idx, st.handle, st), .dir = st.isDir(), .name = st.name }; }, } } @@ -951,12 +918,11 @@ pub fn Server(comptime cfg: Config) type { return error.NoSnapshot; } - /// (Re)generates the content of a dynamic file into its slot. - fn generate(c: *Conn, f: *Fid) !void { - const s = f.snap.?; + /// (Re)generates the content of a dynamic file into slot `s`. + fn generate(c: *Conn, ref: NodeRef, s: u8) !void { var w: Writer = .fixed(&c.storage.snapshots[s]); c.slot_len[s] = 0; - switch (f.node) { + switch (ref) { .static => |idx| try flat[idx].node.gen.?(c.shared.ctx, &w), .@"var" => |v| { const n = c.shared.vars[v.idx].vt.nodes[v.node]; @@ -983,162 +949,85 @@ pub fn Server(comptime cfg: Config) type { }; } - // -- request handlers -- - - fn attach(c: *Conn, m: anytype) !cloud9.Msg { - const f = try c.allocFid(m.fid); - f.node = .{ .static = 0 }; - f.is_dir = true; - return .{ .rattach = .{ .qid = (try c.info(f.node)).qid() } }; + fn snapshot(c: *Conn, handle: u32) []const u8 { + return c.storage.snapshots[handle][0..c.slot_len[handle]]; } - fn walk(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - if (m.newfid != m.fid and c.findFid(m.newfid) != null) return error.FidInUse; - if (m.newfid != m.fid and c.nfids >= cfg.max_fids) return error.TooManyFids; - if (m.nwname > 0 and f.open) return error.AlreadyOpen; - // Cloning a fid onto itself changes nothing; in particular it must not - // close an open fid or discard generated content. - if (m.nwname == 0 and m.newfid == m.fid) return .{ .rwalk = .{ .nwqid = 0 } }; - // A full Rwalk must fit the negotiated msize; check before binding anything. - if (cloud9.header_len + 2 + cloud9.qid_len * @as(usize, m.nwname) > c.server.msize) return error.ReplyTooLarge; - var cur = f.node; - var cur_is_dir = f.is_dir; - var held = false; // cur is a provider handle obtained here, not the fid's - var reply: cloud9.Msg = .{ .rwalk = .{ .nwqid = 0 } }; - const names = m.wname[0..m.nwname]; - for (names, 0..) |name, i| { - if (!cur_is_dir) { - if (i == 0) return error.NotDir; - break; - } - const next = c.lookup(cur, name) catch |e| { - if (i == 0) return e; - break; - }; - if (held) c.releaseRef(cur); - cur = next.ref; - cur_is_dir = next.info.is_dir; - held = cur == .prov; - reply.rwalk.wqid[i] = next.info.qid(); - reply.rwalk.nwqid += 1; - } - if (reply.rwalk.nwqid != names.len) { - if (held) c.releaseRef(cur); - return reply; - } - if (names.len == 0 and cur == .prov) { - // A clone of a provider handle needs its own reference. - const dup = try c.lookup(cur, "."); - cur = dup.ref; - cur_is_dir = dup.info.is_dir; - held = true; - } - const target = if (m.newfid == m.fid) f else c.allocFid(m.newfid) catch |e| { - if (held) c.releaseRef(cur); - return e; - }; - if (target == f) c.dropContents(f); - target.node = cur; - target.is_dir = cur_is_dir; - return reply; - } + // -- request handlers -- - fn open(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - if (f.open) return error.AlreadyOpen; - const acc = m.mode & 3; + fn open(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { + const mode = req.omode; + const acc = mode & 3; const want_write = acc == cloud9.owrite or acc == cloud9.ordwr; - const trunc = m.mode & cloud9.otrunc != 0; - if (f.is_dir and (want_write or trunc)) return error.IsDir; - switch (f.node) { + const trunc = mode & cloud9.otrunc != 0; + switch (ref) { .static => |idx| switch (flat[idx].node.kind) { .dir, .vars, .ctl => {}, .static, .dynamic => if (want_write or trunc) return error.Perm, }, .@"var" => |v| { const n = c.shared.vars[v.idx].vt.nodes[v.node]; - if ((want_write or trunc) and !n.writable()) return error.Perm; + if (n.isDir()) { + if (want_write or trunc) return error.IsDir; + } else if ((want_write or trunc) and !n.writable()) return error.Perm; }, .prov => |p| { const pr = c.provider(p.idx); - try pr.vtable.open(pr.ctx, p.h, m.mode); + try pr.vtable.open(pr.ctx, p.h, mode); }, } - const qid = (c.info(f.node) catch |e| { + const i = c.info(ref) catch |e| { // The provider's open succeeded but its stat did not: undo the open. - if (f.node == .prov) { - const pr = c.provider(f.node.prov.idx); - if (pr.vtable.close) |close| close(pr.ctx, f.node.prov.h); + if (ref == .prov) { + const pr = c.provider(ref.prov.idx); + if (pr.vtable.close) |close| close(pr.ctx, ref.prov.h); } return e; - }).qid(); - if (c.isDynamic(f.node)) { - f.snap = try c.takeSlot(); - c.generate(f) catch |e| { - c.slot_used[f.snap.?] = false; - f.snap = null; - if (f.node == .prov) unreachable; + }; + var handle: u32 = no_snapshot; + if (c.isDynamic(ref)) { + const s = try c.takeSlot(); + c.generate(ref, s) catch |e| { + c.slot_used[s] = false; return e; }; + handle = s; } - f.open = true; - f.mode = m.mode; - f.rclose = m.mode & cloud9.orclose != 0; - f.dir_offset = 0; - f.dir_index = 0; - return .{ .ropen = .{ .qid = qid, .iounit = 0 } }; + return .{ .tag = req.tag, .attr = i.attr(ref), .handle = handle }; } - fn create(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - if (f.open) return error.AlreadyOpen; - const p = switch (f.node) { + fn create(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { + const p = switch (ref) { .prov => |p| p, else => return error.Perm, }; - if (!f.is_dir) return error.NotDir; const pr = c.provider(p.idx); const create_fn = pr.vtable.create orelse return error.Perm; - try validName(m.name); - const is_dir = m.perm & cloud9.dmdir != 0; - const acc = m.mode & 3; - if (is_dir and (acc != cloud9.oread or m.mode & cloud9.otrunc != 0)) return error.IsDir; - const h = try create_fn(pr.ctx, p.h, m.name, m.perm, m.mode); + try validName(req.data); + const h = try create_fn(pr.ctx, p.h, req.data, req.perm, req.omode); const node: NodeRef = .{ .prov = .{ .idx = p.idx, .h = h } }; - const qid = (c.info(node) catch |e| { + const i = c.info(node) catch |e| { if (pr.vtable.close) |close| close(pr.ctx, h); pr.vtable.clunk(pr.ctx, h); return e; - }).qid(); - c.dropContents(f); - f.node = node; - f.is_dir = is_dir; - f.open = true; - f.mode = m.mode; - f.rclose = m.mode & cloud9.orclose != 0; - f.dir_offset = 0; - f.dir_index = 0; - return .{ .rcreate = .{ .qid = qid, .iounit = 0 } }; + }; + return .{ .tag = req.tag, .attr = i.attr(node), .handle = no_snapshot }; } - fn read(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - if (!f.open or (f.mode & 3) == cloud9.owrite) return error.NotOpen; - const count: usize = @min(m.count, c.server.msize -| cloud9.iohdrsz, c.storage.data.len); - if (f.is_dir) return c.readDir(f, m.offset, count); + fn read(c: *Conn, req: fs.Req, ref: NodeRef, bytes: *[]const u8) !fs.Reply { const data = &c.storage.data; - const src: []const u8 = switch (f.node) { + const count: usize = @min(req.size, data.len); + const src: []const u8 = switch (ref) { .static => |idx| blk: { const n = flat[idx].node; switch (n.kind) { .static => break :blk n.content, .ctl => break :blk c.shared.ctlResult(), .dynamic => { - if (m.offset == 0) try c.generate(f); - break :blk c.storage.snapshots[f.snap.?][0..c.slot_len[f.snap.?]]; + if (req.off == 0) try c.generate(ref, @intCast(req.handle)); + break :blk c.snapshot(req.handle); }, - .dir, .vars => unreachable, + .dir, .vars => return error.IsDir, } }, .@"var" => |v| blk: { @@ -1146,72 +1035,75 @@ pub fn Server(comptime cfg: Config) type { switch (n.kind) { .type_name, .size => break :blk n.content, .value, .addr => { - if (m.offset == 0) try c.generate(f); - break :blk c.storage.snapshots[f.snap.?][0..c.slot_len[f.snap.?]]; + if (req.off == 0) try c.generate(ref, @intCast(req.handle)); + break :blk c.snapshot(req.handle); }, .raw => break :blk c.varBase(v.idx, v.node)[0..n.size], - .dir, .fields => unreachable, + .dir, .fields => return error.IsDir, } }, .prov => |p| { const pr = c.provider(p.idx); - const n = try pr.vtable.read(pr.ctx, p.h, m.offset, data[0..count]); - return .{ .rread = .{ .data = data[0..@min(n, count)] } }; + const n = try pr.vtable.read(pr.ctx, p.h, req.off, data[0..count]); + bytes.* = data[0..@min(n, count)]; + return .{ .tag = req.tag }; }, }; - if (m.offset >= src.len) return .{ .rread = .{ .data = "" } }; - const off: usize = @intCast(m.offset); + if (req.off >= src.len) return .{ .tag = req.tag }; + const off: usize = @intCast(req.off); const n = @min(count, src.len - off); - if (f.node == .@"var" and c.shared.vars[f.node.@"var".idx].vt.nodes[f.node.@"var".node].kind == .raw) { + if (ref == .@"var" and c.shared.vars[ref.@"var".idx].vt.nodes[ref.@"var".node].kind == .raw) { // Copy out of the variable so the reply does not read live memory twice. @memcpy(data[0..n], src[off..][0..n]); - return .{ .rread = .{ .data = data[0..n] } }; + bytes.* = data[0..n]; + } else { + bytes.* = src[off..][0..n]; } - return .{ .rread = .{ .data = src[off..][0..n] } }; + return .{ .tag = req.tag }; } - fn readDir(c: *Conn, f: *Fid, offset: u64, count: usize) !cloud9.Msg { - if (offset == 0) { - f.dir_offset = 0; - f.dir_index = 0; - } else if (offset != f.dir_offset) return error.BadOffset; + /// Stages `node:u64le dir:u8 len:u8 name` records from entry + /// `req.off` on, about as many as the engine can fit in `req.size`. + fn readDir(c: *Conn, req: fs.Req, ref: NodeRef, bytes: *[]const u8) !fs.Reply { const data = &c.storage.data; - var used: usize = 0; - var i = f.dir_index; - while (try c.entryStat(f.node, i)) |st| : (i += 1) { - const rec = st.encode(data[used..count]) catch |e| switch (e) { - error.NoSpace => break, - else => return error.Io, - }; - used += rec.len; + const who = c.engine.uname_len; + var n: usize = 0; + var est: usize = 0; + var i: usize = @intCast(req.off); + while (est < req.size) : (i += 1) { + const e = (try c.entry(ref, i)) orelse break; + if (e.name.len > 255 or n + 10 + e.name.len > data.len) break; + std.mem.writeInt(u64, data[n..][0..8], e.path, .little); + data[n + 8] = @intFromBool(e.dir); + data[n + 9] = @intCast(e.name.len); + @memcpy(data[n + 10 ..][0..e.name.len], e.name); + n += 10 + e.name.len; + est += cloud9.stat_fixed + 2 + e.name.len + 3 * who; } - f.dir_offset += used; - f.dir_index = i; - return .{ .rread = .{ .data = data[0..used] } }; + bytes.* = data[0..n]; + return .{ .tag = req.tag }; } - fn write(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - const acc = f.mode & 3; - if (!f.open or (acc != cloud9.owrite and acc != cloud9.ordwr)) return error.NotOpen; - if (f.is_dir) return error.IsDir; - switch (f.node) { + fn write(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { + switch (ref) { .static => |idx| switch (flat[idx].node.kind) { - .ctl => try c.ctlCommand(m.data), + .ctl => try c.ctlCommand(req.data), + .dir, .vars => return error.IsDir, else => return error.Perm, }, .@"var" => |v| { const n = c.shared.vars[v.idx].vt.nodes[v.node]; + if (n.isDir()) return error.IsDir; const set = n.set orelse return error.Perm; - try set(c.varBase(v.idx, v.node), m.data); + try set(c.varBase(v.idx, v.node), req.data); }, .prov => |p| { const pr = c.provider(p.idx); - const n = try pr.vtable.write(pr.ctx, p.h, m.offset, m.data); - return .{ .rwrite = .{ .count = @intCast(@min(n, m.data.len)) } }; + const n = try pr.vtable.write(pr.ctx, p.h, req.off, req.data); + return .{ .tag = req.tag, .written = @intCast(@min(n, req.data.len)) }; }, } - return .{ .rwrite = .{ .count = @intCast(m.data.len) } }; + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } /// Runs `cfg.ctl`; on success its output becomes the ctl result. @@ -1227,60 +1119,45 @@ pub fn Server(comptime cfg: Config) type { s.ctl_version +%= 1; } - fn clunk(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - c.freeFid(f); - return .rclunk; - } - - fn remove(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - defer c.freeFid(f); // Tremove always clunks - f.rclose = false; - switch (f.node) { - .prov => |p| { - const pr = c.provider(p.idx); - const rm = pr.vtable.remove orelse return error.Perm; - try rm(pr.ctx, p.h); - }, - else => return error.Perm, - } - return .rremove; - } - - fn stat(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - return .{ .rstat = .{ .stat = c.pinName((try c.info(f.node)).stat()) } }; - } - - fn wstat(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - const p = switch (f.node) { + /// A wstat, or the truncation hint of an OTRUNC open (which the + /// provider's `open` performs itself; nothing to do here). + fn setattr(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { + if (!req.set.any()) return .{ .tag = req.tag }; + const p = switch (ref) { .prov => |p| p, else => return error.Perm, }; const pr = c.provider(p.idx); const ws = pr.vtable.wstat orelse return error.Perm; - const cur = try c.info(f.node); - const st = m.stat; - const q = cur.qid(); - // Fields we cannot change must be "don't care" or unchanged. - if (st.type != 0xFFFF and st.type != 0) return error.Perm; - if (st.dev != 0xFFFF_FFFF and st.dev != 0) return error.Perm; - if (st.qid.type != 0xFF and st.qid.type != q.type) return error.Perm; - if (st.qid.version != 0xFFFF_FFFF and st.qid.version != q.version) return error.Perm; - if (st.qid.path != 0xFFFF_FFFF_FFFF_FFFF and st.qid.path != q.path) return error.Perm; - if (st.uid.len != 0 and !std.mem.eql(u8, st.uid, cfg.name)) return error.Perm; - if (st.gid.len != 0 and !std.mem.eql(u8, st.gid, cfg.name)) return error.Perm; - if (st.muid.len != 0 and !std.mem.eql(u8, st.muid, cfg.name)) return error.Perm; - if (st.name.len != 0 and !std.mem.eql(u8, st.name, cur.name)) { - if (p.h == Provider.root) return error.Perm; - try validName(st.name); - } - if (st.length != 0xFFFF_FFFF_FFFF_FFFF and st.length != cur.length and cur.is_dir) return error.IsDir; - if (st.mode != 0xFFFF_FFFF and (st.mode & cloud9.dmdir) != (cur.mode & cloud9.dmdir)) return error.Perm; + if (req.set.name and p.h == Provider.root) return error.Perm; + var st = stat_dontcare; + if (req.set.name) st.name = req.data; + if (req.set.mode) st.mode = req.perm; + if (req.set.mtime) st.mtime = req.mtime; + if (req.set.length) st.length = req.length; try ws(pr.ctx, p.h, &st); - return .rwstat; + return .{ .tag = req.tag, .attr = (try c.info(ref)).attr(ref) }; + } + + /// The engine lets go of a reference: closes the open handle it + /// came with, removes the node on Tremove or ORCLOSE, and clunks + /// the provider handle exactly once. + fn release(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { + if (req.opened and req.handle != no_snapshot) c.slot_used[req.handle] = false; + switch (ref) { + .prov => |p| { + const pr = c.provider(p.idx); + if (req.opened) if (pr.vtable.close) |close| close(pr.ctx, p.h); + var result: anyerror!void = {}; + if (req.remove) { + if (pr.vtable.remove) |rm| result = rm(pr.ctx, p.h) else result = error.Perm; + } + pr.vtable.clunk(pr.ctx, p.h); + try result; + }, + else => if (req.remove) return error.Perm, + } + return .{ .tag = req.tag }; } }; @@ -1321,29 +1198,35 @@ pub fn Server(comptime cfg: Config) type { try testing.expectEqualStrings("9P2000", v.version.version); } - /// One round trip; the result borrows the client input buffer until the next call. - pub fn rpc(h: *Harness, req: cloud9.Client.Request) !cloud9.Client.Result { - _ = try h.client.submit(req); + /// Moves bytes both ways and steps the connection until nothing + /// moves; true when anything did. + pub fn pump(h: *Harness) !bool { + var moved = false; while (true) { - var moved = false; + var again = false; while (h.client.output().len > 0) { const k = h.conn.push(h.client.output()); - h.client.wrote(k); - moved = moved or k > 0; - while (try h.conn.step()) {} - while (h.conn.output().len > 0) { - const n = h.client.push(h.conn.output()); - h.conn.wrote(n); - moved = moved or n > 0; - } if (k == 0) break; + h.client.wrote(k); + again = true; } - while (try h.conn.step()) {} + while (try h.conn.step()) again = true; while (h.conn.output().len > 0) { const n = h.client.push(h.conn.output()); + if (n == 0) break; h.conn.wrote(n); - moved = moved or n > 0; + again = true; } + if (!again) return moved; + moved = true; + } + } + + /// One round trip; the result borrows the client input buffer until the next call. + pub fn rpc(h: *Harness, req: cloud9.Client.Request) !cloud9.Client.Result { + _ = try h.client.submit(req); + while (true) { + const moved = try h.pump(); if (h.client.take()) |done| return done.result; if (!moved) return error.Stuck; } @@ -1449,6 +1332,21 @@ pub fn Server(comptime cfg: Config) type { const testing = std.testing; +// The engine's own Rerror strings, for the conditions it decides itself. +const e_unknown_fid = fs.e_unknown_fid; +const e_fid_in_use = fs.e_fid_in_use; +const e_too_many_fids = fs.e_too_many_fids; +const e_bad_use = fs.e_bad_use; +const e_already_open = fs.e_already_open; +const e_bad_offset = fs.e_bad_offset; +const e_perm = fs.e_perm; +const e_not_dir = fs.e_not_dir; +const e_wstat = fs.e_wstat; +const e_illegal_name = fs.e_illegal_name; +const e_small_msize = fs.e_small_msize; +const e_count_small = fs.e_count_small; +const e_interrupted = fs.e_interrupted; + const TestBuild = struct { pub const zig_version: []const u8 = builtin.zig_version_string; pub const target: []const u8 = "test-target"; @@ -1537,12 +1435,20 @@ const test_cfg: Config = .{ .max_vars = 4, .snapshot_slots = 2, .snapshot_bytes = 512, + .max_parked = 2, }; const TS = Server(test_cfg); +/// Whether `fid` is open, read from the engine's table. +fn isOpen(c: *const TS.Conn, fid: u32) bool { + for (c.engine.fids) |f| if (f.used and !f.orphan and f.fid == fid) return f.open; + return false; +} + /// A small in-memory provider: /prov/{hello,dir/{inner}} with create/remove/wstat, -/// counting every handle reference so tests can check clunk discipline. +/// counting every handle reference so tests can check clunk discipline. A +/// read of `hello` parks (error.Again) while `park` is set. const TestProv = struct { const max_nodes = 16; const Entry = struct { @@ -1565,8 +1471,10 @@ const TestProv = struct { nodes: [max_nodes]Entry = @splat(.{}), total_refs: u32 = 0, clunks: u32 = 0, + reads: u32 = 0, fail_io: bool = false, fail_stat: bool = false, + park: bool = false, fn init() TestProv { var p: TestProv = .{}; @@ -1664,6 +1572,8 @@ const TestProv = struct { fn read(ctx: *anyopaque, h: Provider.Handle, offset: u64, buf: []u8) Provider.Error!usize { const p = self(ctx); const e = try p.node(h); + p.reads += 1; + if (p.park and h == 1) return error.Again; if (offset >= e.len) return 0; const n = @min(buf.len, e.len - @as(usize, @intCast(offset))); @memcpy(buf[0..n], e.data[@intCast(offset)..][0..n]); @@ -1799,16 +1709,21 @@ test "README, /build and the static tree read as expected" { try testing.expectEqual(@as(usize, 7), names.len); // static files are read-only; the static tree admits no creates or removes try x.h.walkTo(1, &.{ "build", "target" }); - try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.owrite } }, "permission denied"); + try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.owrite } }, e_perm); try x.h.expectFail(.{ .remove = .{ .fid = 1 } }, "permission denied"); try x.h.walkTo(2, &.{"build"}); - try x.h.expectFail(.{ .create = .{ .fid = 2, .name = "nope", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied"); - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = stat_dontcare } }, "permission denied"); + try x.h.expectFail(.{ .create = .{ .fid = 2, .name = "nope", .perm = 0o644, .mode = cloud9.owrite } }, e_perm); + // a wstat that changes nothing is answered by the engine without asking + _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = stat_dontcare } }); + var ws = stat_dontcare; + ws.mtime = 5; + try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied"); const st = try x.h.ok(.{ .stat = .{ .fid = 2 } }); try testing.expectEqualStrings("build", st.stat.name); try testing.expectEqualStrings("tester", st.stat.uid); try testing.expect(st.stat.qid.type & cloud9.qtdir != 0); try testing.expectEqual(TS.build_secs, st.stat.mtime); + try testing.expectEqual(TS.build_secs, st.stat.atime); try testing.expectEqual(@as(u32, 0), parseIso8601("1970-01-01T00:00:00Z").?); try testing.expectEqual(@as(?u32, null), parseIso8601("unknown")); } @@ -1873,7 +1788,7 @@ test "runtime/fn calls the function at open and at each read from offset 0" { try x.h.walkTo(4, &.{ "runtime", "fn", "fib30" }); _ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } }); _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - _ = try x.h.ok(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{} } }); + try x.h.walkTo(5, &.{ "runtime", "fn", "fib30" }); _ = try x.h.ok(.{ .open = .{ .fid = 5, .mode = cloud9.oread } }); } @@ -1895,14 +1810,13 @@ test "snapshot slot exhaustion is an Rerror and clunk frees the slot" { _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }); const r = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 100 } }); try testing.expectEqualStrings("7", r.read); - // cloning an open fid onto itself keeps it open and its content - const w = try x.h.ok(.{ .walk = .{ .fid = 3, .newfid = 3, .names = &.{} } }); - try testing.expectEqual(@as(u16, 0), w.walk.nwqid); + // an open fid cannot be walked from, not even cloned, and stays open + try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 3, .names = &.{} } }, e_bad_use); + try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{} } }, e_bad_use); + try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{".."} } }, e_bad_use); const r2 = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 100 } }); try testing.expectEqualStrings("7", r2.read); - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{".."} } }, "file already open"); - _ = try x.h.ok(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{} } }); - try x.h.expectFail(.{ .read = .{ .fid = 4, .offset = 0, .count = 100 } }, "file not open"); + try x.h.expectFail(.{ .read = .{ .fid = 4, .offset = 0, .count = 100 } }, e_unknown_fid); } test "ctl round trip" { @@ -1946,7 +1860,7 @@ test "ctl round trip" { try testing.expectEqual(@as(u64, 0), (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.length); // Tversion resets the ctl fid like any other try x.h.version(4096); - try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid"); + try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, e_unknown_fid); } test "auth is not required and flush is answered" { @@ -1956,7 +1870,7 @@ test "auth is not required and flush is answered" { try x.h.expectFail(.{ .auth = .{ .afid = 5, .uname = "tester" } }, "authentication not required"); const f = try x.h.ok(.{ .flush = .{ .oldtag = 1 } }); try testing.expect(f == .flush); - try x.h.expectFail(.{ .attach = .{ .fid = 0, .uname = "tester" } }, "fid in use"); + try x.h.expectFail(.{ .attach = .{ .fid = 0, .uname = "tester" } }, e_fid_in_use); } test "vars: value/type/size/addr/raw, fields and writes" { @@ -2027,24 +1941,24 @@ test "vars: value/type/size/addr/raw, fields and writes" { try testing.expectEqualStrings("value", a_st.stat.name); // non-scalar values, type/size/addr/raw and directories are read-only try x.h.walkTo(3, &.{ "vars", "state", "value" }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.owrite } }, "permission denied"); + try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.owrite } }, e_perm); _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); try x.h.walkTo(4, &.{ "vars", "state", "f", "name", "value" }); - try x.h.expectFail(.{ .open = .{ .fid = 4, .mode = cloud9.owrite } }, "permission denied"); + try x.h.expectFail(.{ .open = .{ .fid = 4, .mode = cloud9.owrite } }, e_perm); _ = try x.h.ok(.{ .clunk = .{ .fid = 4 } }); try x.h.walkTo(5, &.{ "vars", "state" }); - try x.h.expectFail(.{ .open = .{ .fid = 5, .mode = cloud9.owrite } }, "is a directory"); - try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "z", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied"); + try x.h.expectFail(.{ .open = .{ .fid = 5, .mode = cloud9.owrite } }, e_perm); + try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "z", .perm = 0o644, .mode = cloud9.owrite } }, e_perm); // .. climbs back out of the var tree; unknown names fail const up = try x.h.ok(.{ .walk = .{ .fid = 5, .newfid = 6, .names = &.{ "f", "inner", "..", "..", "..", "..", "README" } } }); try testing.expectEqual(@as(u16, 7), up.walk.nwqid); _ = try x.h.ok(.{ .clunk = .{ .fid = 6 } }); try x.h.walkTo(7, &.{"vars"}); try x.h.expectFail(.{ .walk = .{ .fid = 7, .newfid = 8, .names = &.{"nope"} } }, "file does not exist"); - try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, "file already open"); + try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, e_bad_use); _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); try x.h.walkTo(2, &.{ "vars", "state", "f", "a", "value" }); - try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, "not a directory"); + try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, e_not_dir); } test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and error mapping" { @@ -2083,13 +1997,13 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens); try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].refs); - // permission and kind errors come from the provider + // permission and kind errors: the engine's from the walked mode, the provider's as its own strings try x.h.walkTo(3, &.{ "prov", "locked" }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "permission denied"); + try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, e_perm); try x.h.walkTo(4, &.{ "prov", "hello" }); - try x.h.expectFail(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{"x"} } }, "not a directory"); + try x.h.expectFail(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{"x"} } }, e_not_dir); try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 5, .names = &.{"missing"} } }, "file does not exist"); - try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.owrite } }, "is a directory"); + try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.owrite } }, e_perm); // create in a provider directory: the fid becomes the new open file const cr = try x.h.ok(.{ .create = .{ .fid = 2, .name = "new", .perm = 0o644, .mode = cloud9.ordwr } }); try testing.expectEqual(cloud9.qtfile, cr.create.qid.type); @@ -2099,11 +2013,11 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err try x.h.walkTo(6, &.{"prov"}); try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "new", .perm = 0o644, .mode = cloud9.oread } }, "file already exists"); const long_name = [_]u8{'n'} ** (max_name + 1); - try x.h.expectFail(.{ .create = .{ .fid = 6, .name = &long_name, .perm = 0o644, .mode = cloud9.oread } }, "bad file name"); - try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.owrite } }, "is a directory"); + try x.h.expectFail(.{ .create = .{ .fid = 6, .name = &long_name, .perm = 0o644, .mode = cloud9.oread } }, e_illegal_name); + try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.owrite } }, e_perm); const dr = try x.h.ok(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.oread } }); try testing.expectEqual(cloud9.qtdir, dr.create.qid.type); - // wstat: rename, truncate, mode, mtime; immutable fields are refused + // wstat: rename, truncate, mode, mtime; immutable fields are refused by the engine var ws = stat_dontcare; ws.name = "renamed"; ws.length = 2; @@ -2117,19 +2031,19 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err try testing.expectEqual(@as(u32, 99), st2.stat.mtime); ws = stat_dontcare; ws.uid = "someone-else"; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied"); + try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_wstat); ws = stat_dontcare; ws.mode = cloud9.dmdir | 0o755; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied"); + try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_wstat); ws = stat_dontcare; ws.name = "bad/name"; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "bad file name"); + try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_illegal_name); ws.name = ".."; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "bad file name"); + try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_illegal_name); ws = stat_dontcare; ws.length = 5; try x.h.walkTo(7, &.{ "prov", "d" }); - try x.h.expectFail(.{ .wstat = .{ .fid = 7, .stat = ws } }, "is a directory"); + try x.h.expectFail(.{ .wstat = .{ .fid = 7, .stat = ws } }, e_wstat); ws = stat_dontcare; ws.name = "root2"; // the provider root cannot be renamed try x.h.walkTo(14, &.{"prov"}); @@ -2139,7 +2053,7 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err _ = try x.h.ok(.{ .clunk = .{ .fid = 6 } }); try x.h.walkTo(8, &.{ "prov", "dir" }); try x.h.expectFail(.{ .remove = .{ .fid = 8 } }, "directory not empty"); - try x.h.expectFail(.{ .clunk = .{ .fid = 8 } }, "unknown fid"); + try x.h.expectFail(.{ .clunk = .{ .fid = 8 } }, e_unknown_fid); _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); try x.h.walkTo(9, &.{"prov"}); try x.h.expectFail(.{ .walk = .{ .fid = 9, .newfid = 15, .names = &.{"renamed"} } }, "file does not exist"); @@ -2168,10 +2082,10 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err // a partial walk releases the handles it obtained const part = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 13, .names = &.{ "prov", "dir", "nope" } } }); try testing.expectEqual(@as(u16, 2), part.walk.nwqid); - try x.h.expectFail(.{ .clunk = .{ .fid = 13 } }, "unknown fid"); + try x.h.expectFail(.{ .clunk = .{ .fid = 13 } }, e_unknown_fid); _ = try x.h.ok(.{ .clunk = .{ .fid = 12 } }); - for (x.h.conn.fids) |f| { - if (f.used) _ = try x.h.ok(.{ .clunk = .{ .fid = f.id } }); + for (x.h.conn.engine.fids) |f| { + if (f.used and !f.orphan) _ = try x.h.ok(.{ .clunk = .{ .fid = f.fid } }); } try testing.expectEqual(@as(u32, 0), x.prov.total_refs); } @@ -2184,7 +2098,7 @@ test "directory reads across offsets, bad offset, and records never split" { _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); const first = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); try testing.expect(first.read.len > 0); - try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 5, .count = 4096 } }, "bad offset"); + try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 5, .count = 4096 } }, e_bad_offset); // offset 0 restarts; the same bytes come back const again = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); try testing.expectEqual(first.read.len, again.read.len); @@ -2192,9 +2106,8 @@ test "directory reads across offsets, bad offset, and records never split" { const names = try x.h.listDir(1, 80); defer TS.Harness.freeNames(names); try testing.expectEqual(@as(usize, 7), names.len); - // a count too small for even one record returns nothing rather than splitting it - const tiny = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }); - try testing.expectEqual(@as(usize, 0), tiny.read.len); + // a count too small for even one record is refused rather than splitting it + try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, e_count_small); // the same for provider and var directories const pn = try x.h.listPath(&.{ "prov", "dir" }); defer TS.Harness.freeNames(pn); @@ -2204,7 +2117,7 @@ test "directory reads across offsets, bad offset, and records never split" { const vn = try x.h.listDir(2, 100); defer TS.Harness.freeNames(vn); try testing.expectEqual(@as(usize, 4), vn.len); - try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 1, .count = 100 } }, "bad offset"); + try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 1, .count = 100 } }, e_bad_offset); } test "Tversion mid-session resets fids and clunks every provider handle" { @@ -2226,7 +2139,7 @@ test "Tversion mid-session resets fids and clunks every provider handle" { try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens); try testing.expectEqual(before + 2, x.prov.clunks); try testing.expect(!x.h.conn.slot_used[0] and !x.h.conn.slot_used[1]); - try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid"); + try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, e_unknown_fid); _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } }); try x.h.walkTo(1, &.{ "prov", "hello" }); // hangup does the same @@ -2235,24 +2148,34 @@ test "Tversion mid-session resets fids and clunks every provider handle" { try testing.expectEqual(@as(usize, 0), x.h.conn.fidCount()); } -test "a reply that does not fit msize is an Rerror, not a dead connection" { +test "msize: below the engine's floor the connection dies; at the floor everything that fits is served" { var x: Fixture = .{}; try x.init(); defer x.deinit(); - try x.h.version(64); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "t" } }); - // Rstat of the root is ~70 bytes. - try x.h.expectFail(.{ .stat = .{ .fid = 0 } }, "reply too large for msize"); - // Rwalk with 5 qids is 74 bytes; the walk must not bind newfid. - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ ".", ".", ".", ".", "." } } }, "reply too large for msize"); - try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid"); - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 0, .names = &.{ ".", ".", ".", ".", "." } } }, "reply too large for msize"); - const r = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"README"} } }); - try testing.expectEqual(@as(u16, 1), r.walk.nwqid); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); - const rd = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 40 } }); - try testing.expect(rd.read.len > 0 and rd.read.len <= 64 - cloud9.iohdrsz); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); + try testing.expectError(error.Protocol, x.h.version(64)); + + var y: Fixture = .{}; + try y.init(); + defer y.deinit(); + try y.h.version(fs.msize_min); + _ = try y.h.ok(.{ .attach = .{ .fid = 0, .uname = "t" } }); + const st = try y.h.ok(.{ .stat = .{ .fid = 0 } }); + try testing.expectEqualStrings("/", st.stat.name); + // A full 16-element Rwalk is exactly msize_min. + const w = try y.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &([_][]const u8{"."} ** 16) } }); + try testing.expectEqual(@as(u16, 16), w.walk.nwqid); + _ = try y.h.ok(.{ .clunk = .{ .fid = 1 } }); + // An Rstat that cannot fit is an Rerror, not a dead connection. + try y.h.walkTo(2, &.{"prov"}); + const long_name = [_]u8{'n'} ** 180; // Tcreate fits; the Rstat (61 + 180 bytes) does not + _ = try y.h.ok(.{ .create = .{ .fid = 2, .name = &long_name, .perm = 0o644, .mode = cloud9.oread } }); + try y.h.expectFail(.{ .stat = .{ .fid = 2 } }, e_small_msize); + _ = try y.h.ok(.{ .remove = .{ .fid = 2 } }); + try y.h.walkTo(1, &.{"README"}); + _ = try y.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); + const rd = try y.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = fs.msize_min - cloud9.iohdrsz } }); + try testing.expect(rd.read.len > 0 and rd.read.len <= fs.msize_min - cloud9.iohdrsz); + _ = try y.h.ok(.{ .clunk = .{ .fid = 1 } }); } test "fid table is bounded per connection" { @@ -2263,18 +2186,18 @@ test "fid table is bounded per connection" { while (x.h.conn.fidCount() < test_cfg.max_fids) : (i += 1) { _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }); } - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }, "too many fids"); - try x.h.expectFail(.{ .attach = .{ .fid = i, .uname = "tester" } }, "too many fids"); + try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }, e_too_many_fids); + try x.h.expectFail(.{ .attach = .{ .fid = i, .uname = "tester" } }, e_too_many_fids); // self-walks and clunks still work at the limit _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 0, .names = &.{"build"} } }); _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }); - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{"README"} } }, "fid in use"); - try x.h.expectFail(.{ .walk = .{ .fid = 1234, .newfid = 2, .names = &.{} } }, "unknown fid"); + try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{"README"} } }, e_fid_in_use); + try x.h.expectFail(.{ .walk = .{ .fid = 1234, .newfid = 2, .names = &.{} } }, e_unknown_fid); // a walk into a provider at the limit must not leak the handle _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{ "..", "prov", "hello" } } }); - try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = i + 1, .names = &.{} } }, "too many fids"); + try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = i + 1, .names = &.{} } }, e_too_many_fids); try testing.expectEqual(@as(u32, 1), x.prov.total_refs); } @@ -2295,7 +2218,7 @@ test "Shared refuses more providers or vars than configured" { try testing.expectError(error.Full, shared.expose("v4", &v[4])); } -/// A server with a large fid table for the index tests. +/// A server with a large fid table for the churn test. const big_cfg: Config = .{ .name = "big", .msize = 8192, @@ -2307,8 +2230,8 @@ const big_cfg: Config = .{ }; const BigS = Server(big_cfg); -/// Fid numbers chosen to stress the index: dense low ids, ids with only high -/// bits set, and ids counting down from 2^32-1 (all distinct for i < 2^20). +/// Fid numbers chosen to stress the engine's index: dense low ids, ids with +/// only high bits set, and ids counting down from 2^32-1 (all distinct for i < 2^20). fn adversarialId(i: u32) u32 { return switch (i % 3) { 0 => i * 8192 + 1, @@ -2317,34 +2240,15 @@ fn adversarialId(i: u32) u32 { }; } -/// Every index bucket points at a used fid that finds itself, and every used -/// fid is found: the invariant the hostile fid tests check after each phase. -fn checkFidIndex(c: *BigS.Conn) !void { - var indexed: usize = 0; - for (c.index) |slot| { - if (slot == BigS.no_slot) continue; - indexed += 1; - try testing.expect(c.fids[slot].used); - try testing.expectEqual(&c.fids[slot], c.findFid(c.fids[slot].id).?); - } - var used: usize = 0; - for (c.fids[0..c.high_water]) |*f| if (f.used) { - used += 1; - try testing.expectEqual(f, c.findFid(f.id).?); - }; - for (c.fids[c.high_water..]) |*f| try testing.expect(!f.used); - try testing.expectEqual(indexed, used); - try testing.expectEqual(used, c.nfids); -} - -test "fid index: thousands of fids, clunk in hostile orders, reuse, Tversion" { +test "fid table: thousands of fids, clunk in hostile orders, reuse, Tversion" { var ctx: TestCtx = .{}; var shared: BigS.Shared = .init(&ctx); var prov = TestProv.init(); try shared.addProvider(prov.provider()); const storage = try testing.allocator.create(BigS.Storage); defer testing.allocator.destroy(storage); - var h: BigS.Harness = undefined; + const h = try testing.allocator.create(BigS.Harness); + defer testing.allocator.destroy(h); try h.init(&shared, storage); defer h.deinit(); const n: u32 = big_cfg.max_fids - 1; // fid 0 is the attach @@ -2354,34 +2258,30 @@ test "fid index: thousands of fids, clunk in hostile orders, reuse, Tversion" { } try testing.expectEqual(@as(usize, n + 1), h.conn.fidCount()); try testing.expectEqual(n, prov.total_refs); - try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 0x7FFF_FFFF, .names = &.{} } }, "too many fids"); - try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = adversarialId(5), .names = &.{} } }, "fid in use"); - try h.expectFail(.{ .attach = .{ .fid = adversarialId(7), .uname = "t" } }, "fid in use"); - try testing.expect(h.conn.findFid(0x7FFF_FFFF) == null); - try testing.expect(h.conn.findFid(adversarialId(n)) == null); - try checkFidIndex(&h.conn); - // clunk every third fid, then the rest from the top: backward-shift deletion under churn + try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 0x7FFF_FFFF, .names = &.{} } }, e_too_many_fids); + try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = adversarialId(5), .names = &.{} } }, e_fid_in_use); + try h.expectFail(.{ .attach = .{ .fid = adversarialId(7), .uname = "t" } }, e_fid_in_use); + try h.expectFail(.{ .clunk = .{ .fid = 0x7FFF_FFFF } }, e_unknown_fid); + try h.expectFail(.{ .clunk = .{ .fid = adversarialId(n) } }, e_unknown_fid); + // clunk every third fid, then the rest from the top i = 0; while (i < n) : (i += 3) _ = try h.ok(.{ .clunk = .{ .fid = adversarialId(i) } }); - try checkFidIndex(&h.conn); i = n; while (i > 0) { i -= 1; if (i % 3 == 0) { - try h.expectFail(.{ .clunk = .{ .fid = adversarialId(i) } }, "unknown fid"); + try h.expectFail(.{ .clunk = .{ .fid = adversarialId(i) } }, e_unknown_fid); } else { _ = try h.ok(.{ .clunk = .{ .fid = adversarialId(i) } }); } } try testing.expectEqual(@as(usize, 1), h.conn.fidCount()); try testing.expectEqual(@as(u32, 0), prov.total_refs); - try checkFidIndex(&h.conn); - // the whole table is reusable after the churn, through the free list + // the whole table is reusable after the churn i = 0; while (i < n) : (i += 1) _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = n - i, .names = &.{} } }); - try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = n + 1, .names = &.{} } }, "too many fids"); - try checkFidIndex(&h.conn); - // pseudo-random alloc/free storm with verification + try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = n + 1, .names = &.{} } }, e_too_many_fids); + // pseudo-random alloc/free storm var prng = std.Random.DefaultPrng.init(0x9a11); const rnd = prng.random(); var live: [n + 1]bool = @splat(true); @@ -2395,18 +2295,17 @@ test "fid index: thousands of fids, clunk in hostile orders, reuse, Tversion" { _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = id, .names = &.{"prov"} } }); } live[id] = !live[id]; - if (round % 997 == 0) try checkFidIndex(&h.conn); } - try checkFidIndex(&h.conn); + var expected: usize = 1; + for (live) |l| expected += @intFromBool(l); + try testing.expectEqual(expected, h.conn.fidCount()); // Tversion drops everything and the table starts over, provider refs balanced try h.version(big_cfg.msize); try testing.expectEqual(@as(usize, 0), h.conn.fidCount()); try testing.expectEqual(@as(u32, 0), prov.total_refs); - try testing.expectEqual(@as(u16, 0), h.conn.high_water); - try checkFidIndex(&h.conn); _ = try h.ok(.{ .attach = .{ .fid = 0xFFFF_FFFE, .uname = "t" } }); _ = try h.ok(.{ .walk = .{ .fid = 0xFFFF_FFFE, .newfid = 0, .names = &.{} } }); - try checkFidIndex(&h.conn); + try testing.expectEqual(@as(usize, 2), h.conn.fidCount()); } test "open: a provider stat failure after a successful open closes the file again" { @@ -2418,7 +2317,7 @@ test "open: a provider stat failure after a successful open closes the file agai try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "i/o error"); x.prov.fail_stat = false; try testing.expectEqual(@as(u32, 0), x.prov.nodes[1].opens); - try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, "file not open"); + try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, e_bad_use); _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); try testing.expectEqual(@as(u32, 1), x.prov.nodes[1].opens); // the same for create: a stat failure after the provider created the node releases it @@ -2430,7 +2329,7 @@ test "open: a provider stat failure after a successful open closes the file agai try testing.expectEqual(@as(u32, 0), e.opens); try testing.expectEqual(@as(u32, 0), e.refs); }; - try testing.expect(!x.h.conn.findFid(2).?.open); + try testing.expect(!isOpen(&x.h.conn, 2)); try testing.expectEqual(@as(u32, 1), x.prov.total_refs); // fid 1 only } @@ -2440,11 +2339,12 @@ test "fid state machine: open twice, walk from open, remove/clunk of open provid defer x.deinit(); try x.h.walkTo(1, &.{ "prov", "dir", "inner" }); _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); - try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "file already open"); - try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{"."} } }, "file already open"); - try x.h.expectFail(.{ .create = .{ .fid = 1, .name = "z", .perm = 0o644, .mode = cloud9.oread } }, "file already open"); - // a clone of an open fid is a fresh, unopened reference - _ = try x.h.ok(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{} } }); + try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, e_already_open); + try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{"."} } }, e_bad_use); + try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{} } }, e_bad_use); + try x.h.expectFail(.{ .create = .{ .fid = 1, .name = "z", .perm = 0o644, .mode = cloud9.oread } }, e_already_open); + // a second, unopened reference to the same node + try x.h.walkTo(2, &.{ "prov", "dir", "inner" }); try testing.expectEqual(@as(u32, 2), x.prov.nodes[3].refs); try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].opens); // walking newfid == fid with names on an unopened provider fid swaps the handle, refs balanced @@ -2459,24 +2359,24 @@ test "fid state machine: open twice, walk from open, remove/clunk of open provid _ = try x.h.ok(.{ .remove = .{ .fid = 1 } }); try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens); try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].refs); - try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "unknown fid"); + try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, e_unknown_fid); _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); try testing.expectEqual(@as(u32, 0), x.prov.total_refs); // walking "." on a file fid is "not a directory" at the protocol level, without a provider walk try x.h.walkTo(3, &.{ "prov", "hello" }); const before = x.prov.clunks; - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"."} } }, "not a directory"); + try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"."} } }, e_not_dir); try testing.expectEqual(before, x.prov.clunks); try testing.expectEqual(@as(u32, 1), x.prov.total_refs); // a partial walk through a file releases the handles it took const part = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 5, .names = &.{ "prov", "hello", "x", "y" } } }); try testing.expectEqual(@as(u16, 2), part.walk.nwqid); try testing.expectEqual(@as(u32, 1), x.prov.total_refs); - try x.h.expectFail(.{ .clunk = .{ .fid = 5 } }, "unknown fid"); + try x.h.expectFail(.{ .clunk = .{ .fid = 5 } }, e_unknown_fid); // Tremove is always a clunk, even of a static node or when the provider refuses try x.h.walkTo(6, &.{"README"}); try x.h.expectFail(.{ .remove = .{ .fid = 6 } }, "permission denied"); - try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, "unknown fid"); + try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, e_unknown_fid); _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); try testing.expectEqual(@as(u32, 0), x.prov.total_refs); } @@ -2491,7 +2391,7 @@ test "snapshot slots: exhaust, hold, Tversion frees; reads past the end and at h _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "too many open dynamic files"); - try testing.expect(!x.h.conn.findFid(3).?.open); + try testing.expect(!isOpen(&x.h.conn, 3)); // reads at offsets near 2^64 never trap (counts above msize are a raw-9P // case: the cloud9 client refuses to send them; test/adv_core_hostile.py covers it) const max_count = test_cfg.msize - cloud9.iohdrsz; @@ -2522,23 +2422,26 @@ test "static and var nodes refuse create, remove and wstat; directories refuse w var x: Fixture = .{}; try x.init(); defer x.deinit(); + var ws = stat_dontcare; + ws.mtime = 1; const dirs = [_][]const []const u8{ &.{}, &.{"build"}, &.{"comptime"}, &.{ "comptime", "types" }, &.{ "comptime", "types", "Layout" }, &.{"runtime"}, &.{ "runtime", "fn" }, &.{"vars"}, &.{ "vars", "state" }, &.{ "vars", "state", "f" }, &.{ "vars", "state", "f", "inner" } }; for (dirs, 0..) |d, k| { const fid: u32 = @intCast(10 + k); try x.h.walkTo(fid, d); - try x.h.expectFail(.{ .create = .{ .fid = fid, .name = "x", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied"); - try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } }, "permission denied"); - try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.owrite } }, "is a directory"); - try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.oread | cloud9.otrunc } }, "is a directory"); + try x.h.expectFail(.{ .create = .{ .fid = fid, .name = "x", .perm = 0o644, .mode = cloud9.owrite } }, e_perm); + try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = ws } }, "permission denied"); + _ = try x.h.ok(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } }); + try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.owrite } }, e_perm); + try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.oread | cloud9.otrunc } }, e_perm); try x.h.expectFail(.{ .remove = .{ .fid = fid } }, "permission denied"); - try x.h.expectFail(.{ .clunk = .{ .fid = fid } }, "unknown fid"); + try x.h.expectFail(.{ .clunk = .{ .fid = fid } }, e_unknown_fid); } const files = [_][]const []const u8{ &.{"README"}, &.{ "build", "time" }, &.{ "comptime", "decls" }, &.{ "runtime", "pid" }, &.{ "runtime", "fn", "fib30" }, &.{"ctl"}, &.{ "vars", "state", "value" }, &.{ "vars", "state", "raw" }, &.{ "vars", "state", "f", "a", "value" }, &.{ "vars", "counter", "type" } }; for (files, 0..) |f, k| { const fid: u32 = @intCast(30 + k); try x.h.walkTo(fid, f); - try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } }, "permission denied"); - try x.h.expectFail(.{ .walk = .{ .fid = fid, .newfid = 99, .names = &.{".."} } }, "not a directory"); + try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = ws } }, "permission denied"); + try x.h.expectFail(.{ .walk = .{ .fid = fid, .newfid = 99, .names = &.{".."} } }, e_not_dir); try x.h.expectFail(.{ .remove = .{ .fid = fid } }, "permission denied"); } // writes to a var value at a non-zero offset and with an empty payload @@ -2551,35 +2454,62 @@ test "static and var nodes refuse create, remove and wstat; directories refuse w try testing.expectEqual(@as(u32, 1), x.exposed.a); _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = std.math.maxInt(u64), .data = "77\n" } }); try testing.expectEqual(@as(u32, 77), x.exposed.a); - // reads of a write-only fid are refused; OEXEC reads like OREAD - try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, "file not open"); + // reads of a write-only fid are refused; the engine refuses OEXEC + try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, e_bad_use); try x.h.walkTo(2, &.{"README"}); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oexec } }); + try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.oexec } }, e_perm); + _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); try testing.expect((try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 10 } })).read.len == 10); } -test "msize 24: every request that fits is answered, every reply that cannot fit is an Rerror" { +test "a provider read that is not ready parks and is answered on a later step" { var x: Fixture = .{}; try x.init(); defer x.deinit(); - try x.h.version(24); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "u" } }); // Tattach 20, Rattach 20 - try x.h.expectFail(.{ .stat = .{ .fid = 0 } }, "reply too large"); // Rerror truncated to fit 24 bytes - const w = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"ctl"} } }); // Rwalk 22 - try testing.expectEqual(@as(u16, 1), w.walk.nwqid); - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{ ".", "." } } }, "reply too large"); - try x.h.expectFail(.{ .clunk = .{ .fid = 2 } }, "unknown fid"); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); // Ropen 24 - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "e" } }, "bad command"); // Twrite 24 - // the largest read the client may ask for is msize - iohdrsz = 0 bytes - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 0 } }); - try testing.expectEqual(@as(usize, 0), r.read.len); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try x.h.walkTo(3, &.{"build"}); - _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }); - const d = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 0 } }); - try testing.expectEqual(@as(usize, 0), d.read.len); // no record fits in 0 bytes, nothing is split - try x.h.expectFail(.{ .read = .{ .fid = 3, .offset = 1, .count = 0 } }, "bad offset"); + try x.h.walkTo(1, &.{ "prov", "hello" }); + _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); + x.prov.park = true; + const reads = x.prov.reads; + const tag = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); + _ = try x.h.pump(); + try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take()); + try testing.expect(x.prov.reads > reads); + // the connection keeps serving, and every step asks the provider again + const asked = x.prov.reads; + const st = try x.h.ok(.{ .stat = .{ .fid = 1 } }); + try testing.expectEqualStrings("hello", st.stat.name); + try testing.expect(x.prov.reads > asked); + try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take()); + // a second parked read fills the slots; a third is refused at once + _ = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); + _ = try x.h.pump(); + try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take()); + try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }, fs.e_again); + // ready: the next step answers both, oldest first, with the original tags + x.prov.park = false; + _ = try x.h.pump(); + const first = x.h.client.take() orelse return error.NoReply; + try testing.expectEqual(tag, first.tag); + try testing.expectEqualStrings("hello", first.result.read); + const second = x.h.client.take() orelse return error.NoReply; + try testing.expectEqualStrings("hello", second.result.read); + try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take()); + // a flushed parked read is interrupted; a hangup with one parked pays the provider + x.prov.park = true; + const parked = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); + _ = try x.h.pump(); + _ = try x.h.client.submit(.{ .flush = .{ .oldtag = parked } }); + _ = try x.h.pump(); + const interrupted = x.h.client.take() orelse return error.NoReply; + try testing.expectEqual(parked, interrupted.tag); + try testing.expectEqualStrings(e_interrupted, interrupted.result.fail); + try testing.expect((x.h.client.take() orelse return error.NoReply).result == .flush); + _ = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); + _ = try x.h.pump(); + try testing.expectEqual(@as(u32, 1), x.prov.nodes[1].opens); + x.h.conn.hangup(); + try testing.expectEqual(@as(u32, 0), x.prov.nodes[1].opens); + try testing.expectEqual(@as(u32, 0), x.prov.total_refs); } test "Conn.init clamps the msize cap to [msize_min, cfg.msize]" { @@ -2587,11 +2517,13 @@ test "Conn.init clamps the msize cap to [msize_min, cfg.msize]" { var shared: TS.Shared = .init(&ctx); var storage: TS.Storage = undefined; const lo: TS.Conn = .init(&shared, &storage, 0); - try testing.expectEqual(cloud9.Server.msize_min, lo.msize_cap); + try testing.expectEqual(fs.msize_min, lo.msize_cap); const hi: TS.Conn = .init(&shared, &storage, std.math.maxInt(u32)); try testing.expectEqual(test_cfg.msize, hi.msize_cap); const mid: TS.Conn = .init(&shared, &storage, 4096); try testing.expectEqual(@as(u32, 4096), mid.msize_cap); + // two connections on the same Shared never share a fid-index salt + try testing.expect(lo.engine.hash_seed != hi.engine.hash_seed); } test "parseIso8601 rejects malformed stamps and never traps" { @@ -2611,46 +2543,3 @@ test "parseIso8601 rejects malformed stamps and never traps" { try testing.expectEqual(@as(u32, std.math.maxInt(u32)), parseIso8601("2106-02-07T06:28:15Z").?); try testing.expectEqual(@as(?u32, null), parseIso8601("2106-02-07T06:28:16Z")); } - -/// Multiplicative inverse of an odd 32-bit constant (Newton iteration). -fn inverseMod32(a: u32) u32 { - var x: u32 = a; - for (0..5) |_| x *%= 2 -% a *% x; - return x; -} - -test "fid index: fid numbers crafted to collide under the public hash do not cluster a seeded connection" { - var ctx: TestCtx = .{}; - var shared: BigS.Shared = .init(&ctx); - const storage = try testing.allocator.create(BigS.Storage); - defer testing.allocator.destroy(storage); - var h: BigS.Harness = undefined; - try h.init(&shared, storage); - defer h.deinit(); - // two connections on the same Shared never share a seed - const other: BigS.Conn = .init(&shared, storage, big_cfg.msize); - try testing.expect(other.hash_seed != h.conn.hash_seed); - // ids whose products with the golden ratio share their top bits: all one bucket when unseeded - const inv = inverseMod32(0x9E37_79B1); - try testing.expectEqual(@as(u32, 1), inv *% 0x9E37_79B1); - const n: u32 = big_cfg.max_fids - 1; - const base: u32 = 0x4242_0000; - var i: u32 = 0; - while (i < n) : (i += 1) { - const id = (base + i) *% inv; - try testing.expectEqual(@as(usize, base >> BigS.index_shift), @as(usize, @intCast((id *% 0x9E37_79B1) >> BigS.index_shift))); - _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = id, .names = &.{} } }); - } - try checkFidIndex(&h.conn); - // the longest probe sequence in the seeded table is short; unseeded it would be ~n - var worst: usize = 0; - i = 0; - while (i < n) : (i += 1) { - const id = (base + i) *% inv; - var pos = h.conn.fidHome(id); - var steps: usize = 0; - while (h.conn.fids[h.conn.index[pos]].id != id) : (pos = (pos + 1) & BigS.index_mask) steps += 1; - worst = @max(worst, steps); - } - try testing.expect(worst < 64); -} diff --git a/9proc/src/linux/probe.zig b/9proc/src/linux/probe.zig index 559d981..4db277d 100644 --- a/9proc/src/linux/probe.zig +++ b/9proc/src/linux/probe.zig @@ -427,7 +427,7 @@ pub fn Probe(comptime Srv: type) type { /// Free space in the connection's input buffer (cloud9 keeps one /// msize-sized frame; `push` copies at most this much). fn inputRoom(conn: *const Srv.Conn) usize { - return conn.server.in.len - conn.server.in_len; + return conn.inputRoom(); } fn readClient(p: *Self, i: usize, hup: bool) void { diff --git a/9proc/src/scratch.zig b/9proc/src/scratch.zig index 2163a28..f666afb 100644 --- a/9proc/src/scratch.zig +++ b/9proc/src/scratch.zig @@ -7,6 +7,7 @@ //! the root is handle 0. Not internally synchronized (like `Shared`). const std = @import("std"); const cloud9 = @import("cloud9"); +const fs = cloud9.fs; const core = @import("core.zig"); const Allocator = std.mem.Allocator; const Provider = core.Provider; @@ -374,8 +375,7 @@ const Fixture = struct { } fn nodeOf(x: *Fixture, fid: u32) *Node { - for (x.h.conn.fids) |f| if (f.used and f.id == fid) return x.scratch.node(f.node.prov.h); - unreachable; + return x.scratch.node(x.h.conn.providerHandle(fid).?); } }; @@ -429,7 +429,7 @@ test "scratch create/write/read/rename/truncate/remove" { _ = try x.h.ok(.{ .clunk = .{ .fid = 5 } }); try x.h.walkTo(6, &.{ "scratch", "d" }); try x.h.expectFail(.{ .remove = .{ .fid = 6 } }, "directory not empty"); - try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, "unknown fid"); // remove always clunks + try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, fs.e_unknown_fid); // remove always clunks try x.h.walkTo(7, &.{ "scratch", "d", "inner" }); _ = try x.h.ok(.{ .remove = .{ .fid = 7 } }); try x.h.walkTo(8, &.{ "scratch", "d" }); @@ -469,7 +469,7 @@ test "walk of a missing name and walking a file" { // a walk that fails past the first element is a partial Rwalk that leaves newfid unused const r = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 4, .names = &.{ "scratch", "nope", "x" } } }); try testing.expectEqual(@as(u16, 1), r.walk.nwqid); - try x.h.expectFail(.{ .clunk = .{ .fid = 4 } }, "unknown fid"); + try x.h.expectFail(.{ .clunk = .{ .fid = 4 } }, fs.e_unknown_fid); // .. from a file is not a directory; .. from the scratch root reaches the server root try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 5, .names = &.{".."} } }, "not a directory"); try x.h.walkTo(5, &.{"scratch"}); @@ -504,10 +504,9 @@ test "directory read across consecutive offsets returns every record exactly onc seen[idx] = true; } for (seen) |s| try testing.expect(s); - try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 7, .count = 200 } }, "bad offset"); - // a read that cannot fit even one record returns nothing rather than splitting it - const tiny = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 30 } }); - try testing.expectEqual(@as(usize, 0), tiny.read.len); + try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 7, .count = 200 } }, fs.e_bad_offset); + // a read that cannot fit even one record is refused rather than splitting it + try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 0, .count = 30 } }, fs.e_count_small); _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); // Tversion resets every fid and every reference try x.h.version(8192); @@ -592,13 +591,16 @@ test "wstat with every field equal to the current stat changes nothing" { try x.h.walkTo(1, &.{"scratch"}); _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "same", .perm = 0o640, .mode = cloud9.oread } }); const before = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat; - var copy = before; + // The engine owns type, dev, qid, atime and the owner names: those must + // be "don't care"; name, mode, mtime and length equal to the current + // stat reach the provider and change nothing. + var copy = dontcare; var name_buf: [core.max_name]u8 = undefined; @memcpy(name_buf[0..before.name.len], before.name); copy.name = name_buf[0..before.name.len]; - copy.uid = "tester"; - copy.gid = "tester"; - copy.muid = "tester"; + copy.mode = before.mode; + copy.mtime = before.mtime; + copy.length = before.length; _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = copy } }); const after = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat; try testing.expectEqual(before.qid, after.qid); @@ -619,7 +621,7 @@ test "wstat with every field equal to the current stat changes nothing" { // the mode's directory bit is immutable, mtime is settable st = dontcare; st.mode = cloud9.dmdir | 0o640; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = st } }, "permission denied"); + try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = st } }, fs.e_wstat); st = dontcare; st.mtime = 12345; _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = st } }); @@ -651,7 +653,7 @@ test "ORCLOSE removes on clunk and removed files stay readable through open fids _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); try x.h.walkTo(6, &.{"scratch"}); try x.h.expectFail(.{ .walk = .{ .fid = 6, .newfid = 7, .names = &.{"d"} } }, "file does not exist"); - try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "x", .perm = 0o644, .mode = cloud9.oread } }, "unknown fid"); // remove clunked it + try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "x", .perm = 0o644, .mode = cloud9.oread } }, fs.e_unknown_fid); // remove clunked it } test "qid paths are stable identities, not addresses: remove + recreate differ" { -- cgit v1.3