summaryrefslogtreecommitdiff
path: root/introspect/src/core.zig
diff options
context:
space:
mode:
Diffstat (limited to 'introspect/src/core.zig')
-rw-r--r--introspect/src/core.zig2656
1 files changed, 2656 insertions, 0 deletions
diff --git a/introspect/src/core.zig b/introspect/src/core.zig
new file mode 100644
index 0000000..2707af2
--- /dev/null
+++ b/introspect/src/core.zig
@@ -0,0 +1,2656 @@
+//! The freestanding 9P2000 introspection engine: 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.
+const std = @import("std");
+const builtin = @import("builtin");
+const cloud9 = @import("cloud9");
+const vars = @import("vars.zig");
+const Writer = std.Io.Writer;
+
+/// Longest file name accepted in a create or rename.
+pub const max_name: usize = 255;
+
+/// A dynamic-file generator (`Config.fns`, `Config.runtime`): writes the file's
+/// content into `w` at open time (and again at each read from offset 0).
+pub const Gen = *const fn (ctx: *anyopaque, w: *Writer) anyerror!void;
+/// The /ctl command handler: `cmd` is the written text, `out` receives the
+/// result that later reads of /ctl return.
+pub const Ctl = *const fn (ctx: *anyopaque, cmd: []const u8, out: *Writer) anyerror!void;
+
+pub const Config = struct {
+ /// Appears in /README and as uid/gid/muid of every Stat.
+ name: []const u8 = "introspect",
+ /// A type whose pub decls `zig_version`, `target`, `optimize`, `time`
+ /// and `change` (all `[]const u8`) become the files of /build. `null`
+ /// omits /build.
+ build: ?type = null,
+ /// /comptime/types/<short name>/{name,size,align,fields}.
+ types: []const type = &.{},
+ /// /comptime/decls lists this type's pub decls (empty when null).
+ decls_of: ?type = null,
+ /// /runtime/fn/<name>: every pub decl is a `fn (ctx: *anyopaque, w: *std.Io.Writer) anyerror!void`.
+ fns: type = struct {},
+ /// /runtime/<name>: same signature as `fns`, one level up (pid, uptime, ...).
+ runtime: type = struct {},
+ /// The /ctl handler; `null` omits /ctl.
+ ctl: ?Ctl = null,
+ /// Where /ctl lives: the root or /runtime/ctl.
+ ctl_dir: enum { root, runtime } = .root,
+ /// Capacity of the ctl result (in `Shared`).
+ ctl_bytes: u32 = 4096,
+ /// Largest negotiable msize; sizes `Storage.in/out/data`.
+ msize: u32 = 8192,
+ max_fids: u16 = 64,
+ max_providers: u8 = 8,
+ max_vars: u8 = 32,
+ /// Dynamic file contents are generated at open time into per-fid snapshot
+ /// slots so that reads at arbitrary offsets are consistent.
+ snapshot_slots: u8 = 8,
+ snapshot_bytes: u32 = 16 * 1024,
+};
+
+/// Attributes of a provider node, filled by `VTable.stat` and `VTable.list`.
+pub const NodeStat = struct {
+ /// Permission bits plus `cloud9.dmdir`/`dmappend`/`dmexcl`.
+ mode: u32,
+ length: u64 = 0,
+ atime: u32 = 0,
+ mtime: u32 = 0,
+ /// Becomes the qid version.
+ version: u32 = 0,
+ /// The entry name (`list`) or the node's own name (`stat`; ignored for the
+ /// provider root, whose name is the mount name). Must stay valid until the
+ /// provider's next call.
+ name: []const u8 = "",
+ /// Filled by `list`: the entry's handle. Not retained by the core.
+ handle: Provider.Handle = 0,
+ /// A stable identity for the qid path (low 56 bits), for providers whose
+ /// handles are not stable across the node's life (e.g. memory addresses
+ /// that an allocator may reuse). 0 means "the handle is the path".
+ path: u64 = 0,
+
+ pub fn isDir(s: NodeStat) bool {
+ return s.mode & cloud9.dmdir != 0;
+ }
+};
+
+/// A runtime subtree mounted at a top-level name.
+///
+/// Handle lifetime: every handle returned by `walk` or `create` is released by
+/// the core with exactly one `clunk` (after `close` if the fid was open). The
+/// root handle 0 is never obtained through `walk`, so providers must treat
+/// `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).
+pub const Provider = struct {
+ name: []const u8,
+ ctx: *anyopaque,
+ vtable: *const VTable,
+
+ /// Provider-defined node id; 0 = provider root.
+ 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 VTable = struct {
+ walk: *const fn (ctx: *anyopaque, parent: Handle, name: []const u8) Error!Handle,
+ stat: *const fn (ctx: *anyopaque, h: Handle, out: *NodeStat) Error!void,
+ /// 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,
+ 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`.
+ create: ?*const fn (ctx: *anyopaque, dir: Handle, name: []const u8, perm: u32, mode: u8) Error!Handle = null,
+ remove: ?*const fn (ctx: *anyopaque, h: Handle) Error!void = null,
+ /// Only name, length, mode and mtime can differ from the current stat
+ /// (the core has already checked the immutable fields and the name).
+ wstat: ?*const fn (ctx: *anyopaque, h: Handle, st: *const cloud9.Stat) Error!void = null,
+ /// An open fid on `h` was released (before `clunk`).
+ close: ?*const fn (ctx: *anyopaque, h: Handle) void = null,
+ /// A fid holding `h` was released (also on hangup and Tversion).
+ clunk: *const fn (ctx: *anyopaque, h: Handle) void,
+ };
+};
+
+/// The Plan 9 error string for any error the engine or a provider can raise.
+pub fn ename(err: anyerror) []const u8 {
+ return switch (err) {
+ error.NotFound, error.NoFile => "file does not exist",
+ error.Perm => "permission denied",
+ error.Exists => "file already exists",
+ error.NotEmpty => "directory not empty",
+ error.NotDir => "not a directory",
+ error.IsDir => "is a directory",
+ error.BadOffset => "bad offset",
+ error.NoSpace => "no space left on device",
+ error.Io => "i/o error",
+ error.Unsupported => "not supported",
+ error.Excl => "exclusive use file already open",
+ error.FidInUse => "fid in use",
+ error.UnknownFid => "unknown fid",
+ error.NotOpen => "file not open",
+ error.AlreadyOpen => "file already open",
+ error.AuthNotRequired => "authentication not required",
+ error.BadCommand => "bad command",
+ error.BadName => "bad file name",
+ error.Invalid, error.BadValue => "bad value",
+ error.TooManyFids => "too many fids",
+ error.NoSnapshot => "too many open dynamic files",
+ error.WriteFailed => "no space in buffer",
+ error.ReplyTooLarge => "reply too large for msize",
+ error.OutOfMemory => "out of memory",
+ else => "i/o error",
+ };
+}
+
+/// A "don't care" Twstat: every field left as it is.
+pub const stat_dontcare: cloud9.Stat = .{
+ .type = 0xFFFF,
+ .dev = 0xFFFF_FFFF,
+ .qid = .{ .type = 0xFF, .version = 0xFFFF_FFFF, .path = 0xFFFF_FFFF_FFFF_FFFF },
+ .mode = 0xFFFF_FFFF,
+ .atime = 0xFFFF_FFFF,
+ .mtime = 0xFFFF_FFFF,
+ .length = 0xFFFF_FFFF_FFFF_FFFF,
+ .name = "",
+ .uid = "",
+ .gid = "",
+ .muid = "",
+};
+
+pub fn validName(name: []const u8) error{BadName}!void {
+ if (name.len == 0 or name.len > max_name) return error.BadName;
+ if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return error.BadName;
+ if (std.mem.indexOfAny(u8, name, "/\x00") != null) return error.BadName;
+}
+
+/// "YYYY-MM-DDTHH:MM:SSZ" as unix seconds, or null.
+pub fn parseIso8601(s: []const u8) ?u32 {
+ if (s.len != 20 or s[4] != '-' or s[7] != '-' or s[10] != 'T' or s[13] != ':' or s[16] != ':' or s[19] != 'Z') return null;
+ const y = std.fmt.parseInt(i64, s[0..4], 10) catch return null;
+ const mo = std.fmt.parseInt(i64, s[5..7], 10) catch return null;
+ const d = std.fmt.parseInt(i64, s[8..10], 10) catch return null;
+ const h = std.fmt.parseInt(i64, s[11..13], 10) catch return null;
+ const mi = std.fmt.parseInt(i64, s[14..16], 10) catch return null;
+ const sec = std.fmt.parseInt(i64, s[17..19], 10) catch return null;
+ if (mo < 1 or mo > 12 or d < 1 or d > 31 or h > 23 or mi > 59 or sec > 60) return null;
+ // Howard Hinnant's days_from_civil.
+ const yy = if (mo <= 2) y - 1 else y;
+ const era = @divFloor(yy, 400);
+ const yoe = yy - era * 400;
+ const mp = if (mo > 2) mo - 3 else mo + 9;
+ const doy = @divFloor(153 * mp + 2, 5) + d - 1;
+ const doe = yoe * 365 + @divFloor(yoe, 4) - @divFloor(yoe, 100) + doy;
+ const days = era * 146097 + doe - 719468;
+ const total = days * 86400 + h * 3600 + mi * 60 + sec;
+ if (total < 0 or total > std.math.maxInt(u32)) return null;
+ return @intCast(total);
+}
+
+/// The last component of @typeName(T): "wire.Qid" -> "Qid".
+pub fn shortTypeName(comptime T: type) []const u8 {
+ const full = @typeName(T);
+ const dot = std.mem.lastIndexOfScalar(u8, full, '.') orelse return full;
+ return full[dot + 1 ..];
+}
+
+/// The /comptime/types/<T>/fields text: "name: type @offset" per line.
+pub fn fieldsText(comptime T: type) []const u8 {
+ comptime {
+ @setEvalBranchQuota(200_000);
+ var s: []const u8 = "";
+ switch (@typeInfo(T)) {
+ .@"struct" => |info| for (info.fields) |f| {
+ if (info.layout == .@"packed") {
+ s = s ++ std.fmt.comptimePrint("{s}: {s} @{d}b\n", .{ f.name, @typeName(f.type), @bitOffsetOf(T, f.name) });
+ } else if (f.is_comptime) {
+ s = s ++ std.fmt.comptimePrint("{s}: {s} (comptime)\n", .{ f.name, @typeName(f.type) });
+ } else {
+ s = s ++ std.fmt.comptimePrint("{s}: {s} @{d}\n", .{ f.name, @typeName(f.type), @offsetOf(T, f.name) });
+ }
+ },
+ .@"union" => |info| for (info.fields) |f| {
+ s = s ++ f.name ++ ": " ++ @typeName(f.type) ++ "\n";
+ },
+ .@"enum" => |info| for (info.fields) |f| {
+ s = s ++ std.fmt.comptimePrint("{s} = {d}\n", .{ f.name, f.value });
+ },
+ else => s = @typeName(T) ++ "\n",
+ }
+ return s;
+ }
+}
+
+fn declsText(comptime T: type) []const u8 {
+ comptime {
+ @setEvalBranchQuota(20_000);
+ const decls = switch (@typeInfo(T)) {
+ inline .@"struct", .@"union", .@"enum", .@"opaque" => |info| info.decls,
+ else => &[_]std.builtin.Type.Declaration{},
+ };
+ var s: []const u8 = "";
+ for (decls) |d| s = s ++ d.name ++ "\n";
+ return s;
+ }
+}
+
+/// Wraps `Fns.<name>` in a function of exactly the `Gen` signature.
+fn genFor(comptime Fns: type, comptime name: []const u8) Gen {
+ return &struct {
+ fn g(ctx: *anyopaque, w: *Writer) anyerror!void {
+ return @field(Fns, name)(ctx, w);
+ }
+ }.g;
+}
+
+/// A node of the static tree, described at comptime.
+pub const Node = struct {
+ name: []const u8,
+ kind: Kind,
+ children: []const Node = &.{},
+ content: []const u8 = "",
+ gen: ?Gen = null,
+
+ pub const Kind = enum(u8) { dir, static, dynamic, ctl, vars };
+
+ fn isDir(n: Node) bool {
+ return n.kind == .dir or n.kind == .vars;
+ }
+};
+
+fn genNodes(comptime Fns: type) [@typeInfo(Fns).@"struct".decls.len]Node {
+ const decls = @typeInfo(Fns).@"struct".decls;
+ var arr: [decls.len]Node = undefined;
+ for (decls, 0..) |d, i| arr[i] = .{ .name = d.name, .kind = .dynamic, .gen = genFor(Fns, d.name) };
+ return arr;
+}
+
+pub fn Server(comptime cfg: Config) type {
+ return struct {
+ const Self = @This();
+
+ // -- the static tree ------------------------------------------------
+
+ pub const readme_text = std.fmt.comptimePrint(
+ \\{s}: a 9P2000 introspection server (built on cloud9).
+ \\
+ \\/build facts baked in at build time (zig version, target, optimize, time, change id)
+ \\/comptime facts computed by the Zig compiler: type layouts under types/<T>/, pub decls
+ \\/runtime live facts; fn/<name> calls a Zig function on every read
+ \\/ctl write a command, read the result
+ \\/vars exposed variables: <name>/{{value,type,size,addr,raw,f/<field>/...}}
+ \\
+ \\Other top-level directories are providers mounted at runtime.
+ \\
+ , .{cfg.name});
+
+ fn typeDir(comptime T: type) Node {
+ return .{ .name = shortTypeName(T), .kind = .dir, .children = &.{
+ .{ .name = "name", .kind = .static, .content = @typeName(T) },
+ .{ .name = "size", .kind = .static, .content = std.fmt.comptimePrint("{d}", .{@sizeOf(T)}) },
+ .{ .name = "align", .kind = .static, .content = std.fmt.comptimePrint("{d}", .{@alignOf(T)}) },
+ .{ .name = "fields", .kind = .static, .content = fieldsText(T) },
+ } };
+ }
+
+ const type_dirs: [cfg.types.len]Node = blk: {
+ @setEvalBranchQuota(200_000);
+ var arr: [cfg.types.len]Node = undefined;
+ for (cfg.types, 0..) |T, i| arr[i] = typeDir(T);
+ for (arr, 0..) |a, i| for (arr[i + 1 ..]) |b| {
+ if (std.mem.eql(u8, a.name, b.name)) @compileError("duplicate short type name " ++ a.name);
+ };
+ break :blk arr;
+ };
+
+ const decls_text: []const u8 = if (cfg.decls_of) |T| declsText(T) else "";
+ const fn_nodes = genNodes(cfg.fns);
+ const runtime_nodes = genNodes(cfg.runtime);
+ const ctl_node: Node = .{ .name = "ctl", .kind = .ctl };
+
+ const build_nodes: []const Node = if (cfg.build) |B| &[_]Node{
+ .{ .name = "zig_version", .kind = .static, .content = B.zig_version },
+ .{ .name = "target", .kind = .static, .content = B.target },
+ .{ .name = "optimize", .kind = .static, .content = B.optimize },
+ .{ .name = "time", .kind = .static, .content = B.time },
+ .{ .name = "change", .kind = .static, .content = B.change },
+ } else &.{};
+
+ /// Build time as unix seconds (for static atime/mtime), or 0.
+ pub const build_secs: u32 = if (cfg.build) |B| (parseIso8601(B.time) orelse 0) else 0;
+
+ const runtime_children: []const Node = blk: {
+ var list: []const Node = &runtime_nodes;
+ list = list ++ &[_]Node{.{ .name = "fn", .kind = .dir, .children = &fn_nodes }};
+ if (cfg.ctl != null and cfg.ctl_dir == .runtime) list = list ++ &[_]Node{ctl_node};
+ break :blk list;
+ };
+
+ const root_children: []const Node = blk: {
+ var list: []const Node = &[_]Node{.{ .name = "README", .kind = .static, .content = readme_text }};
+ if (cfg.build != null) list = list ++ &[_]Node{.{ .name = "build", .kind = .dir, .children = build_nodes }};
+ list = list ++ &[_]Node{
+ .{ .name = "comptime", .kind = .dir, .children = &.{
+ .{ .name = "types", .kind = .dir, .children = &type_dirs },
+ .{ .name = "decls", .kind = .static, .content = decls_text },
+ } },
+ .{ .name = "runtime", .kind = .dir, .children = runtime_children },
+ };
+ if (cfg.ctl != null and cfg.ctl_dir == .root) list = list ++ &[_]Node{ctl_node};
+ list = list ++ &[_]Node{.{ .name = "vars", .kind = .vars }};
+ break :blk list;
+ };
+
+ pub const root_node: Node = .{ .name = "/", .kind = .dir, .children = root_children };
+
+ /// The static tree flattened so nodes can be referenced by index; the
+ /// children of a node occupy consecutive slots `first..first+count`.
+ const Flat = struct { node: Node, parent: u32, first: u32, count: u32 };
+
+ fn countNodes(n: Node) usize {
+ var c: usize = 1;
+ for (n.children) |ch| c += countNodes(ch);
+ return c;
+ }
+
+ fn fillFlat(arr: []Flat, next: *usize, idx: usize, n: Node, parent: u32) void {
+ const first = next.*;
+ next.* += n.children.len;
+ arr[idx] = .{ .node = n, .parent = parent, .first = @intCast(first), .count = @intCast(n.children.len) };
+ for (n.children, 0..) |ch, i| fillFlat(arr, next, first + i, ch, @intCast(idx));
+ }
+
+ pub const flat_len = countNodes(root_node);
+ pub const flat: [flat_len]Flat = blk: {
+ @setEvalBranchQuota(100_000);
+ var arr: [flat_len]Flat = undefined;
+ var next: usize = 1;
+ fillFlat(&arr, &next, 0, root_node, 0);
+ break :blk arr;
+ };
+ const vars_idx: u32 = blk: {
+ for (flat, 0..) |f, i| if (f.node.kind == .vars) break :blk @intCast(i);
+ @compileError("no vars node");
+ };
+
+ 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);
+ // Provider index 0xFE/0xFF would collide with the var/static qid tags.
+ std.debug.assert(cfg.max_providers < 0xFE);
+ }
+
+ // -- qid paths ------------------------------------------------------
+
+ const static_tag: u64 = 0xFF << 56;
+ const var_tag: u64 = 0xFE << 56;
+ const handle_mask: u64 = (1 << 56) - 1;
+
+ // -- storage --------------------------------------------------------
+
+ /// Per-connection buffers; the caller places one in static memory.
+ pub const Storage = struct {
+ in: [cfg.msize]u8,
+ out: [cfg.msize]u8,
+ /// Staging area for read replies (directory records, provider and raw reads).
+ data: [cfg.msize]u8,
+ snapshots: [cfg.snapshot_slots][cfg.snapshot_bytes]u8,
+ };
+
+ const Var = struct {
+ name: []const u8,
+ ptr: *anyopaque,
+ vt: *const vars.VTable,
+ };
+
+ /// State common to all connections: providers, exposed variables, the
+ /// ctl result. Not internally synchronized: one thread serves all
+ /// connections (or the caller serializes).
+ pub const Shared = struct {
+ ctx: *anyopaque,
+ providers: [cfg.max_providers]Provider = undefined,
+ nprov: u8 = 0,
+ vars: [cfg.max_vars]Var = undefined,
+ nvars: u8 = 0,
+ /// The ctl result is double-buffered: a command writes into the
+ /// buffer that is not current and commits it only on success, so
+ /// a failed command leaves the previous result intact.
+ ctl_bufs: [2][cfg.ctl_bytes]u8 = undefined,
+ ctl_cur: u1 = 0,
+ /// 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
+ /// connection counter and buffer addresses; a platform layer with a
+ /// random source may set this once after `init` to make the seed
+ /// unpredictable even where addresses are static.
+ hash_seed: u32 = 0,
+ conn_seq: u32 = 0,
+
+ /// `ctx` is passed to every `fns`/`runtime` generator and to `ctl`.
+ pub fn init(ctx: *anyopaque) Shared {
+ return .{ .ctx = ctx };
+ }
+
+ /// Mounts `p` at `/<p.name>`. The name must not collide with a
+ /// static entry or another provider.
+ pub fn addProvider(s: *Shared, p: Provider) error{Full}!void {
+ if (s.nprov == cfg.max_providers) return error.Full;
+ std.debug.assert(validName(p.name) != error.BadName);
+ std.debug.assert(s.findProvider(p.name) == null);
+ std.debug.assert(staticChild(0, p.name) == null);
+ s.providers[s.nprov] = p;
+ s.nprov += 1;
+ }
+
+ /// Publishes `ptr.*` as /vars/<name>. `name` and the pointee must
+ /// outlive the server.
+ pub fn expose(s: *Shared, name: []const u8, ptr: anytype) error{Full}!void {
+ const P = @TypeOf(ptr);
+ const info = @typeInfo(P);
+ if (info != .pointer or info.pointer.size != .one or info.pointer.is_const) @compileError("expose wants a *T, got " ++ @typeName(P));
+ if (s.nvars == cfg.max_vars) return error.Full;
+ std.debug.assert(validName(name) != error.BadName);
+ std.debug.assert(s.findVar(name) == null);
+ s.vars[s.nvars] = .{ .name = name, .ptr = @ptrCast(ptr), .vt = vars.vtableFor(info.pointer.child) };
+ s.nvars += 1;
+ }
+
+ /// The result of the last successful ctl command.
+ pub fn ctlResult(s: *const Shared) []const u8 {
+ return s.ctl_bufs[s.ctl_cur][0..s.ctl_len];
+ }
+
+ fn findProvider(s: *const Shared, name: []const u8) ?u8 {
+ for (s.providers[0..s.nprov], 0..) |p, i| if (std.mem.eql(u8, p.name, name)) return @intCast(i);
+ return null;
+ }
+
+ fn findVar(s: *const Shared, name: []const u8) ?u8 {
+ for (s.vars[0..s.nvars], 0..) |v, i| if (std.mem.eql(u8, v.name, name)) return @intCast(i);
+ return null;
+ }
+ };
+
+ fn staticChild(idx: u32, name: []const u8) ?u32 {
+ const f = flat[idx];
+ for (f.first..f.first + f.count) |ci| {
+ if (std.mem.eql(u8, flat[ci].node.name, name)) return @intCast(ci);
+ }
+ return null;
+ }
+
+ // -- connection -----------------------------------------------------
+
+ 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;
+ }
+
+ /// Everything the engine needs to know about a node for qid/stat.
+ const Info = struct {
+ is_dir: bool,
+ mode: u32,
+ length: u64,
+ atime: u32,
+ mtime: u32,
+ version: u32,
+ 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 {
+ 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,
+ };
+ }
+ };
+
+ /// One 9P connection: a cloud9.Server plus a fid table and snapshot slots.
+ pub const Conn = struct {
+ shared: *Shared,
+ storage: *Storage,
+ server: cloud9.Server,
+ /// 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)));
+ 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,
+ };
+ }
+
+ /// 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);
+ }
+
+ /// Bytes to send to the client.
+ pub fn output(c: *const Conn) []const u8 {
+ return c.server.output();
+ }
+
+ pub fn wrote(c: *Conn, n: usize) void {
+ c.server.wrote(n);
+ }
+
+ /// Drops every fid (telling providers) and kills the session.
+ pub fn hangup(c: *Conn) void {
+ c.resetFids();
+ c.server.hangup();
+ }
+
+ /// Handles at most one request. 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,
+ };
+ },
+ }
+ return true;
+ }
+
+ /// Number of fids currently held.
+ pub fn fidCount(c: *const Conn) usize {
+ return c.nfids;
+ }
+
+ 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;
+ }
+ }
+
+ 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;
+ }
+
+ /// 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 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 resetFids(c: *Conn) void {
+ for (c.fids[0..c.high_water]) |*f| {
+ if (f.used) c.dropContents(f);
+ f.* = .{};
+ }
+ @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);
+ }
+ }
+
+ // -- node helpers --
+
+ fn provider(c: *Conn, idx: u8) Provider {
+ return c.shared.providers[idx];
+ }
+
+ fn varBase(c: *Conn, idx: u8, node: u32) [*]u8 {
+ const v = c.shared.vars[idx];
+ return @as([*]u8, @ptrCast(v.ptr)) + v.vt.nodes[node].offset;
+ }
+
+ fn info(c: *Conn, ref: NodeRef) !Info {
+ switch (ref) {
+ .static => |idx| {
+ const n = flat[idx].node;
+ return .{
+ .is_dir = n.isDir(),
+ .mode = switch (n.kind) {
+ .dir, .vars => cloud9.dmdir | 0o555,
+ .ctl => 0o666,
+ else => 0o444,
+ },
+ .length = switch (n.kind) {
+ .static => n.content.len,
+ .ctl => c.shared.ctl_len,
+ else => 0,
+ },
+ .atime = build_secs,
+ .mtime = build_secs,
+ .version = if (n.kind == .ctl) c.shared.ctl_version else 0,
+ .path = static_tag | idx,
+ .name = n.name,
+ };
+ },
+ .@"var" => |v| {
+ const sv = c.shared.vars[v.idx];
+ const n = sv.vt.nodes[v.node];
+ return .{
+ .is_dir = n.isDir(),
+ .mode = if (n.isDir()) cloud9.dmdir | 0o555 else if (n.writable()) 0o644 else 0o444,
+ .length = switch (n.kind) {
+ .type_name, .size => n.content.len,
+ .raw => n.size,
+ else => 0,
+ },
+ .atime = 0,
+ .mtime = 0,
+ .version = 0,
+ .path = var_tag | (@as(u64, v.idx) << 32) | v.node,
+ .name = if (v.node == 0) sv.name else n.name,
+ };
+ },
+ .prov => |p| {
+ const pr = c.provider(p.idx);
+ var st: NodeStat = .{ .mode = 0 };
+ try pr.vtable.stat(pr.ctx, p.h, &st);
+ return provInfo(pr, p.idx, p.h, st);
+ },
+ }
+ }
+
+ fn provInfo(pr: Provider, idx: u8, h: Provider.Handle, st: NodeStat) Info {
+ return .{
+ .is_dir = st.isDir(),
+ .mode = st.mode,
+ .length = if (st.isDir()) 0 else st.length,
+ .atime = st.atime,
+ .mtime = st.mtime,
+ .version = st.version,
+ .path = (@as(u64, idx) << 56) | ((if (st.path != 0) st.path else h) & handle_mask),
+ .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;
+ }
+
+ 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.
+ fn lookup(c: *Conn, ref: NodeRef, name: []const u8) !Looked {
+ switch (ref) {
+ .static => |idx| {
+ const dot = std.mem.eql(u8, name, ".");
+ const dotdot = std.mem.eql(u8, name, "..");
+ var next: NodeRef = undefined;
+ if (dot) {
+ next = ref;
+ } else if (dotdot) {
+ next = .{ .static = flat[idx].parent };
+ } else if (flat[idx].node.kind == .vars) {
+ const vi = c.shared.findVar(name) orelse return error.NotFound;
+ next = .{ .@"var" = .{ .idx = vi, .node = 0 } };
+ } else if (staticChild(idx, name)) |ci| {
+ next = .{ .static = ci };
+ } else if (idx == 0) {
+ const pi = c.shared.findProvider(name) orelse return error.NotFound;
+ next = .{ .prov = .{ .idx = pi, .h = Provider.root } };
+ } else return error.NotFound;
+ return .{ .ref = next, .info = try c.info(next) };
+ },
+ .@"var" => |v| {
+ const vt = c.shared.vars[v.idx].vt;
+ var next = ref;
+ if (std.mem.eql(u8, name, ".")) {
+ // unchanged
+ } else if (std.mem.eql(u8, name, "..")) {
+ next = if (v.node == 0) .{ .static = vars_idx } else .{ .@"var" = .{ .idx = v.idx, .node = vt.nodes[v.node].parent } };
+ } else {
+ const ci = vt.child(v.node, name) orelse return error.NotFound;
+ next = .{ .@"var" = .{ .idx = v.idx, .node = ci } };
+ }
+ return .{ .ref = next, .info = try c.info(next) };
+ },
+ .prov => |p| {
+ if (p.h == Provider.root and std.mem.eql(u8, name, "..")) {
+ const next: NodeRef = .{ .static = 0 };
+ return .{ .ref = next, .info = try c.info(next) };
+ }
+ const pr = c.provider(p.idx);
+ const h = try pr.vtable.walk(pr.ctx, p.h, name);
+ const next: NodeRef = .{ .prov = .{ .idx = p.idx, .h = h } };
+ errdefer c.releaseRef(next);
+ return .{ .ref = next, .info = try c.info(next) };
+ },
+ }
+ }
+
+ /// 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 {
+ 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();
+ }
+ 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 null;
+ },
+ .@"var" => |v| {
+ const n = c.shared.vars[v.idx].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();
+ },
+ .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();
+ },
+ }
+ }
+
+ // -- snapshots --
+
+ fn takeSlot(c: *Conn) !u8 {
+ for (&c.slot_used, 0..) |*u, i| if (!u.*) {
+ u.* = true;
+ return @intCast(i);
+ };
+ 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.?;
+ var w: Writer = .fixed(&c.storage.snapshots[s]);
+ c.slot_len[s] = 0;
+ switch (f.node) {
+ .static => |idx| try flat[idx].node.gen.?(c.shared.ctx, &w),
+ .@"var" => |v| {
+ const n = c.shared.vars[v.idx].vt.nodes[v.node];
+ const base = c.varBase(v.idx, v.node);
+ switch (n.kind) {
+ .value => try n.render.?(base, &w),
+ .addr => try w.print("0x{x}", .{@intFromPtr(base)}),
+ else => unreachable,
+ }
+ },
+ .prov => unreachable,
+ }
+ c.slot_len[s] = @intCast(w.buffered().len);
+ }
+
+ fn isDynamic(c: *Conn, ref: NodeRef) bool {
+ return switch (ref) {
+ .static => |idx| flat[idx].node.kind == .dynamic,
+ .@"var" => |v| switch (c.shared.vars[v.idx].vt.nodes[v.node].kind) {
+ .value, .addr => true,
+ else => false,
+ },
+ .prov => false,
+ };
+ }
+
+ // -- 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 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;
+ }
+
+ 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;
+ 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) {
+ .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;
+ },
+ .prov => |p| {
+ const pr = c.provider(p.idx);
+ try pr.vtable.open(pr.ctx, p.h, m.mode);
+ },
+ }
+ const qid = (c.info(f.node) 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);
+ }
+ 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;
+ return e;
+ };
+ }
+ 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 } };
+ }
+
+ 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) {
+ .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);
+ const node: NodeRef = .{ .prov = .{ .idx = p.idx, .h = h } };
+ const qid = (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 } };
+ }
+
+ 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);
+ const data = &c.storage.data;
+ const src: []const u8 = switch (f.node) {
+ .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.?]];
+ },
+ .dir, .vars => unreachable,
+ }
+ },
+ .@"var" => |v| blk: {
+ const n = c.shared.vars[v.idx].vt.nodes[v.node];
+ 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.?]];
+ },
+ .raw => break :blk c.varBase(v.idx, v.node)[0..n.size],
+ .dir, .fields => unreachable,
+ }
+ },
+ .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)] } };
+ },
+ };
+ if (m.offset >= src.len) return .{ .rread = .{ .data = "" } };
+ const off: usize = @intCast(m.offset);
+ 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) {
+ // 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] } };
+ }
+ return .{ .rread = .{ .data = src[off..][0..n] } };
+ }
+
+ 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;
+ 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;
+ }
+ f.dir_offset += used;
+ f.dir_index = i;
+ return .{ .rread = .{ .data = data[0..used] } };
+ }
+
+ 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) {
+ .static => |idx| switch (flat[idx].node.kind) {
+ .ctl => try c.ctlCommand(m.data),
+ else => return error.Perm,
+ },
+ .@"var" => |v| {
+ const n = c.shared.vars[v.idx].vt.nodes[v.node];
+ const set = n.set orelse return error.Perm;
+ try set(c.varBase(v.idx, v.node), m.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)) } };
+ },
+ }
+ return .{ .rwrite = .{ .count = @intCast(m.data.len) } };
+ }
+
+ /// Runs `cfg.ctl`; on success its output becomes the ctl result.
+ /// On failure the previous result (and its qid version) survive:
+ /// the handler writes into the staging half of `ctl_bufs`.
+ fn ctlCommand(c: *Conn, line: []const u8) !void {
+ const s = c.shared;
+ const next = s.ctl_cur ^ 1;
+ var w: Writer = .fixed(&s.ctl_bufs[next]);
+ try cfg.ctl.?(s.ctx, line, &w);
+ s.ctl_cur = next;
+ s.ctl_len = @intCast(w.buffered().len);
+ 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) {
+ .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;
+ try ws(pr.ctx, p.h, &st);
+ return .rwstat;
+ }
+ };
+
+ // -- in-memory test harness -------------------------------------------
+
+ /// Drives a `Conn` with a `cloud9.Client` in memory. Test-only (uses
+ /// std.testing.allocator); never referenced by non-test code.
+ pub const Harness = struct {
+ shared: *Shared,
+ storage: *Storage,
+ conn: Conn,
+ client: cloud9.Client,
+ cin: []u8,
+ cout: []u8,
+
+ pub fn init(h: *Harness, shared: *Shared, storage: *Storage) !void {
+ h.shared = shared;
+ h.storage = storage;
+ h.conn = .init(shared, storage, cfg.msize);
+ h.cin = try testing.allocator.alloc(u8, cfg.msize);
+ errdefer testing.allocator.free(h.cin);
+ h.cout = try testing.allocator.alloc(u8, cfg.msize);
+ errdefer testing.allocator.free(h.cout);
+ h.client = .init(.{ .in = h.cin, .out = h.cout });
+ try h.version(cfg.msize);
+ _ = try h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } });
+ }
+
+ pub fn deinit(h: *Harness) void {
+ h.conn.hangup();
+ testing.allocator.free(h.cin);
+ testing.allocator.free(h.cout);
+ }
+
+ pub fn version(h: *Harness, msize: u32) !void {
+ const v = try h.rpc(.{ .version = .{ .msize = msize } });
+ try testing.expectEqual(msize, v.version.msize);
+ 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);
+ while (true) {
+ var moved = 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;
+ }
+ 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 (h.client.take()) |done| return done.result;
+ if (!moved) return error.Stuck;
+ }
+ }
+
+ pub fn ok(h: *Harness, req: cloud9.Client.Request) !cloud9.Client.Result {
+ const r = try h.rpc(req);
+ if (r == .fail) {
+ std.debug.print("unexpected Rerror: {s}\n", .{r.fail});
+ return error.Rerror;
+ }
+ return r;
+ }
+
+ pub fn expectFail(h: *Harness, req: cloud9.Client.Request, msg: []const u8) !void {
+ const r = try h.rpc(req);
+ if (r != .fail) return error.ExpectedRerror;
+ try testing.expectEqualStrings(msg, r.fail);
+ }
+
+ pub fn walkTo(h: *Harness, newfid: u32, names: []const []const u8) !void {
+ const r = try h.ok(.{ .walk = .{ .fid = 0, .newfid = newfid, .names = names } });
+ try testing.expectEqual(@as(u16, @intCast(names.len)), r.walk.nwqid);
+ }
+
+ /// Opens `fid` for reading and reads it whole (across consecutive offsets); caller frees.
+ pub fn readAll(h: *Harness, fid: u32) ![]u8 {
+ _ = try h.ok(.{ .open = .{ .fid = fid, .mode = cloud9.oread } });
+ return h.readOpen(fid);
+ }
+
+ pub fn readOpen(h: *Harness, fid: u32) ![]u8 {
+ var acc: std.ArrayList(u8) = .empty;
+ errdefer acc.deinit(testing.allocator);
+ while (true) {
+ const r = try h.ok(.{ .read = .{ .fid = fid, .offset = acc.items.len, .count = 1024 } });
+ if (r.read.len == 0) break;
+ try acc.appendSlice(testing.allocator, r.read);
+ }
+ return acc.toOwnedSlice(testing.allocator);
+ }
+
+ pub fn readPath(h: *Harness, names: []const []const u8) ![]u8 {
+ try h.walkTo(99, names);
+ defer _ = h.rpc(.{ .clunk = .{ .fid = 99 } }) catch {};
+ return h.readAll(99);
+ }
+
+ pub fn writePath(h: *Harness, names: []const []const u8, data: []const u8) !void {
+ try h.walkTo(98, names);
+ defer _ = h.rpc(.{ .clunk = .{ .fid = 98 } }) catch {};
+ _ = try h.ok(.{ .open = .{ .fid = 98, .mode = cloud9.owrite } });
+ const w = try h.ok(.{ .write = .{ .fid = 98, .offset = 0, .data = data } });
+ try testing.expectEqual(@as(u32, @intCast(data.len)), w.write);
+ }
+
+ /// Reads a whole directory in `count`-byte reads at consecutive offsets; returns owned names.
+ pub fn listDir(h: *Harness, fid: u32, count: u32) ![][]u8 {
+ var names: std.ArrayList([]u8) = .empty;
+ errdefer {
+ for (names.items) |n| testing.allocator.free(n);
+ names.deinit(testing.allocator);
+ }
+ var offset: u64 = 0;
+ while (true) {
+ const r = try h.ok(.{ .read = .{ .fid = fid, .offset = offset, .count = count } });
+ if (r.read.len == 0) break;
+ offset += r.read.len;
+ var rest = r.read;
+ while (rest.len > 0) {
+ const n = std.mem.readInt(u16, rest[0..2], .little) + 2;
+ const st = try cloud9.Stat.decode(rest[0..n]);
+ try names.append(testing.allocator, try testing.allocator.dupe(u8, st.name));
+ rest = rest[n..];
+ }
+ }
+ return names.toOwnedSlice(testing.allocator);
+ }
+
+ pub fn listPath(h: *Harness, names: []const []const u8) ![][]u8 {
+ try h.walkTo(97, names);
+ defer _ = h.rpc(.{ .clunk = .{ .fid = 97 } }) catch {};
+ _ = try h.ok(.{ .open = .{ .fid = 97, .mode = cloud9.oread } });
+ return h.listDir(97, 1024);
+ }
+
+ pub fn freeNames(names: [][]u8) void {
+ for (names) |n| testing.allocator.free(n);
+ testing.allocator.free(names);
+ }
+
+ pub fn hasName(names: []const []const u8, want: []const u8) bool {
+ for (names) |n| if (std.mem.eql(u8, n, want)) return true;
+ return false;
+ }
+ };
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+const testing = std.testing;
+
+const TestBuild = struct {
+ pub const zig_version: []const u8 = builtin.zig_version_string;
+ pub const target: []const u8 = "test-target";
+ pub const optimize: []const u8 = "Debug";
+ pub const time: []const u8 = "2023-11-14T22:13:20Z";
+ pub const change: []const u8 = "abc123";
+};
+
+const Layout = struct { a: u8, b: u32, c: u64 };
+const Decls = struct {
+ pub const one = 1;
+ pub const two = 2;
+ pub fn three() void {}
+};
+
+/// The context every generator and the ctl handler receive in tests.
+const TestCtx = struct {
+ calls: u32 = 0,
+ ctl_state: i64 = 0,
+};
+
+const TestFns = struct {
+ pub fn counter(ctx: *anyopaque, w: *Writer) anyerror!void {
+ const t: *TestCtx = @ptrCast(@alignCast(ctx));
+ t.calls += 1;
+ try w.print("{d}", .{t.calls});
+ }
+ pub fn fib30(_: *anyopaque, w: *Writer) anyerror!void {
+ try w.print("{d}", .{fib(30)});
+ }
+ pub fn failing(_: *anyopaque, _: *Writer) anyerror!void {
+ return error.BadCommand;
+ }
+ pub fn huge(_: *anyopaque, w: *Writer) anyerror!void {
+ try w.splatByteAll('x', 1 << 20);
+ }
+};
+
+const TestRuntime = struct {
+ pub fn pid(_: *anyopaque, w: *Writer) anyerror!void {
+ try w.writeAll("4242");
+ }
+};
+
+fn fib(n: u32) u64 {
+ if (n == 0) return 0;
+ var a: u64 = 0;
+ var b: u64 = 1;
+ for (1..n) |_| {
+ const c = a + b;
+ a = b;
+ b = c;
+ }
+ return b;
+}
+
+fn testCtl(ctx: *anyopaque, cmd: []const u8, out: *Writer) anyerror!void {
+ const t: *TestCtx = @ptrCast(@alignCast(ctx));
+ const line = std.mem.trim(u8, cmd, " \t\r\n\x00");
+ var it = std.mem.tokenizeScalar(u8, line, ' ');
+ const verb = it.next() orelse return error.BadCommand;
+ if (std.mem.eql(u8, verb, "echo")) {
+ try out.writeAll(std.mem.trimStart(u8, line[verb.len..], " \t"));
+ } else if (std.mem.eql(u8, verb, "add")) {
+ const a = std.fmt.parseInt(i64, it.next() orelse return error.BadCommand, 10) catch return error.BadCommand;
+ const b = std.fmt.parseInt(i64, it.next() orelse return error.BadCommand, 10) catch return error.BadCommand;
+ t.ctl_state = a +% b;
+ try out.print("{d}", .{t.ctl_state});
+ } else if (std.mem.eql(u8, verb, "partial")) {
+ try out.writeAll("half-written");
+ return error.BadCommand;
+ } else return error.BadCommand;
+}
+
+const test_cfg: Config = .{
+ .name = "tester",
+ .build = TestBuild,
+ .types = &.{ Layout, cloud9.Qid },
+ .decls_of = Decls,
+ .fns = TestFns,
+ .runtime = TestRuntime,
+ .ctl = &testCtl,
+ .msize = 8192,
+ .max_fids = 8,
+ .max_providers = 2,
+ .max_vars = 4,
+ .snapshot_slots = 2,
+ .snapshot_bytes = 512,
+};
+
+const TS = Server(test_cfg);
+
+/// A small in-memory provider: /prov/{hello,dir/{inner}} with create/remove/wstat,
+/// counting every handle reference so tests can check clunk discipline.
+const TestProv = struct {
+ const max_nodes = 16;
+ const Entry = struct {
+ used: bool = false,
+ name: [max_name]u8 = undefined,
+ name_len: u8 = 0,
+ parent: u32 = 0,
+ is_dir: bool = false,
+ mode: u32 = 0o644,
+ data: [64]u8 = undefined,
+ len: usize = 0,
+ refs: u32 = 0,
+ opens: u32 = 0,
+ mtime: u32 = 0,
+
+ fn nameSlice(e: *const Entry) []const u8 {
+ return e.name[0..e.name_len];
+ }
+ };
+ nodes: [max_nodes]Entry = @splat(.{}),
+ total_refs: u32 = 0,
+ clunks: u32 = 0,
+ fail_io: bool = false,
+ fail_stat: bool = false,
+
+ fn init() TestProv {
+ var p: TestProv = .{};
+ p.nodes[0] = .{ .used = true, .is_dir = true, .mode = cloud9.dmdir | 0o755 };
+ _ = p.add(0, "hello", false, 0o644);
+ p.nodes[1].len = 5;
+ @memcpy(p.nodes[1].data[0..5], "hello");
+ const d = p.add(0, "dir", true, cloud9.dmdir | 0o755);
+ _ = p.add(d, "inner", false, 0o600);
+ _ = p.add(0, "locked", false, 0o000);
+ return p;
+ }
+
+ fn add(p: *TestProv, parent: u32, name: []const u8, is_dir: bool, mode: u32) u32 {
+ for (&p.nodes, 0..) |*e, i| if (!e.used) {
+ e.* = .{ .used = true, .parent = parent, .is_dir = is_dir, .mode = mode };
+ @memcpy(e.name[0..name.len], name);
+ e.name_len = @intCast(name.len);
+ return @intCast(i);
+ };
+ unreachable;
+ }
+
+ fn self(ctx: *anyopaque) *TestProv {
+ return @ptrCast(@alignCast(ctx));
+ }
+
+ fn node(p: *TestProv, h: Provider.Handle) Provider.Error!*Entry {
+ if (h >= max_nodes or !p.nodes[h].used) return error.NotFound;
+ return &p.nodes[h];
+ }
+
+ fn retain(p: *TestProv, h: Provider.Handle) Provider.Handle {
+ if (h != 0) {
+ p.nodes[h].refs += 1;
+ p.total_refs += 1;
+ }
+ return h;
+ }
+
+ fn walk(ctx: *anyopaque, parent: Provider.Handle, name: []const u8) Provider.Error!Provider.Handle {
+ const p = self(ctx);
+ if (p.fail_io) return error.Io;
+ const d = try p.node(parent);
+ if (std.mem.eql(u8, name, ".")) return p.retain(parent);
+ if (!d.is_dir) return error.NotDir;
+ if (std.mem.eql(u8, name, "..")) return p.retain(d.parent);
+ for (p.nodes[0..], 0..) |*e, i| {
+ if (e.used and e.parent == parent and i != 0 and std.mem.eql(u8, e.nameSlice(), name)) return p.retain(@intCast(i));
+ }
+ return error.NotFound;
+ }
+
+ fn fillStat(e: *const Entry, h: Provider.Handle, out: *NodeStat) void {
+ out.* = .{ .mode = e.mode, .length = e.len, .mtime = e.mtime, .name = e.nameSlice(), .handle = h };
+ }
+
+ fn stat(ctx: *anyopaque, h: Provider.Handle, out: *NodeStat) Provider.Error!void {
+ const p = self(ctx);
+ if (p.fail_stat) return error.Io;
+ fillStat(try p.node(h), h, out);
+ }
+
+ fn list(ctx: *anyopaque, dir: Provider.Handle, index: usize, out: *NodeStat) Provider.Error!bool {
+ const p = self(ctx);
+ const d = try p.node(dir);
+ if (!d.is_dir) return error.NotDir;
+ var k: usize = 0;
+ for (p.nodes[0..], 0..) |*e, i| {
+ if (!e.used or e.parent != dir or i == 0) continue;
+ if (k == index) {
+ fillStat(e, @intCast(i), out);
+ return true;
+ }
+ k += 1;
+ }
+ return false;
+ }
+
+ fn open(ctx: *anyopaque, h: Provider.Handle, mode: u8) Provider.Error!void {
+ const p = self(ctx);
+ const e = try p.node(h);
+ const acc = mode & 3;
+ if (acc != cloud9.owrite and e.mode & 0o400 == 0) return error.Perm;
+ if (acc != cloud9.oread and e.mode & 0o200 == 0) return error.Perm;
+ if (mode & cloud9.otrunc != 0) e.len = 0;
+ e.opens += 1;
+ }
+
+ fn close(ctx: *anyopaque, h: Provider.Handle) void {
+ const p = self(ctx);
+ p.nodes[h].opens -= 1;
+ }
+
+ fn read(ctx: *anyopaque, h: Provider.Handle, offset: u64, buf: []u8) Provider.Error!usize {
+ const p = self(ctx);
+ const e = try p.node(h);
+ 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]);
+ return n;
+ }
+
+ fn write(ctx: *anyopaque, h: Provider.Handle, offset: u64, data: []const u8) Provider.Error!usize {
+ const p = self(ctx);
+ const e = try p.node(h);
+ if (offset + data.len > e.data.len) return error.NoSpace;
+ const off: usize = @intCast(offset);
+ @memcpy(e.data[off..][0..data.len], data);
+ e.len = @max(e.len, off + data.len);
+ e.mtime += 1;
+ return data.len;
+ }
+
+ fn create(ctx: *anyopaque, dir: Provider.Handle, name: []const u8, perm: u32, mode: u8) Provider.Error!Provider.Handle {
+ const p = self(ctx);
+ const d = try p.node(dir);
+ if (!d.is_dir) return error.NotDir;
+ for (p.nodes[0..]) |*e| if (e.used and e.parent == dir and std.mem.eql(u8, e.nameSlice(), name)) return error.Exists;
+ var free: ?u32 = null;
+ for (p.nodes[0..], 0..) |*e, i| if (!e.used) {
+ free = @intCast(i);
+ break;
+ };
+ const idx = free orelse return error.NoSpace;
+ const h = p.add(@intCast(dir), name, perm & cloud9.dmdir != 0, perm);
+ std.debug.assert(h == idx);
+ p.nodes[h].opens = 1;
+ _ = mode;
+ return p.retain(h);
+ }
+
+ fn remove(ctx: *anyopaque, h: Provider.Handle) Provider.Error!void {
+ const p = self(ctx);
+ const e = try p.node(h);
+ if (h == 0) return error.Perm;
+ for (p.nodes[0..]) |*c| if (c.used and c.parent == h) return error.NotEmpty;
+ e.used = false; // refs still keep the slot "alive" for clunk accounting
+ e.used = true;
+ e.parent = std.math.maxInt(u32); // unlinked
+ }
+
+ fn wstat(ctx: *anyopaque, h: Provider.Handle, st: *const cloud9.Stat) Provider.Error!void {
+ const p = self(ctx);
+ const e = try p.node(h);
+ if (st.name.len != 0) {
+ @memcpy(e.name[0..st.name.len], st.name);
+ e.name_len = @intCast(st.name.len);
+ }
+ if (st.length != 0xFFFF_FFFF_FFFF_FFFF) {
+ if (st.length > e.data.len) return error.NoSpace;
+ e.len = @intCast(st.length);
+ }
+ if (st.mode != 0xFFFF_FFFF) e.mode = st.mode;
+ if (st.mtime != 0xFFFF_FFFF) e.mtime = st.mtime;
+ }
+
+ fn clunk(ctx: *anyopaque, h: Provider.Handle) void {
+ const p = self(ctx);
+ p.clunks += 1;
+ if (h != 0) {
+ p.nodes[h].refs -= 1;
+ p.total_refs -= 1;
+ }
+ }
+
+ const vtable: Provider.VTable = .{
+ .walk = &walk,
+ .stat = &stat,
+ .list = &list,
+ .open = &open,
+ .read = &read,
+ .write = &write,
+ .create = &create,
+ .remove = &remove,
+ .wstat = &wstat,
+ .close = &close,
+ .clunk = &clunk,
+ };
+
+ fn provider(p: *TestProv) Provider {
+ return .{ .name = "prov", .ctx = p, .vtable = &vtable };
+ }
+};
+
+const Inner = struct { x: f32 };
+const Exposed = struct { a: u32, b: bool, name: []const u8, inner: Inner };
+
+/// Everything a core test needs, in one place; `harness.init` runs version+attach.
+const Fixture = struct {
+ ctx: TestCtx = .{},
+ shared: TS.Shared = undefined,
+ storage: TS.Storage = undefined,
+ prov: TestProv = undefined,
+ exposed: Exposed = .{ .a = 1, .b = true, .name = "hello", .inner = .{ .x = 0.5 } },
+ counter: u64 = 7,
+ h: TS.Harness = undefined,
+
+ fn init(x: *Fixture) !void {
+ x.shared = .init(&x.ctx);
+ x.prov = TestProv.init();
+ try x.shared.addProvider(x.prov.provider());
+ try x.shared.expose("state", &x.exposed);
+ try x.shared.expose("counter", &x.counter);
+ try x.h.init(&x.shared, &x.storage);
+ }
+
+ fn deinit(x: *Fixture) void {
+ x.h.deinit();
+ }
+};
+
+test "README, /build and the static tree read as expected" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ const readme = try x.h.readPath(&.{"README"});
+ defer testing.allocator.free(readme);
+ try testing.expect(std.mem.startsWith(u8, readme, "tester: a 9P2000 introspection server"));
+ const zv = try x.h.readPath(&.{ "build", "zig_version" });
+ defer testing.allocator.free(zv);
+ try testing.expectEqualStrings(builtin.zig_version_string, zv);
+ const ch = try x.h.readPath(&.{ "build", "change" });
+ defer testing.allocator.free(ch);
+ try testing.expectEqualStrings("abc123", ch);
+ try testing.expectEqual(@as(u32, 1_700_000_000), TS.build_secs);
+ const names = try x.h.listPath(&.{});
+ defer TS.Harness.freeNames(names);
+ for ([_][]const u8{ "README", "build", "comptime", "runtime", "ctl", "vars", "prov" }) |n| try testing.expect(TS.Harness.hasName(names, n));
+ 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(.{ .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");
+ 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(@as(u32, 0), parseIso8601("1970-01-01T00:00:00Z").?);
+ try testing.expectEqual(@as(?u32, null), parseIso8601("unknown"));
+}
+
+test "comptime/types fields carry @offsetOf and comptime/decls lists pub decls" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ const names = try x.h.listPath(&.{ "comptime", "types" });
+ defer TS.Harness.freeNames(names);
+ try testing.expectEqual(@as(usize, 2), names.len);
+ try testing.expect(TS.Harness.hasName(names, "Layout"));
+ try testing.expect(TS.Harness.hasName(names, "Qid"));
+ const fields = try x.h.readPath(&.{ "comptime", "types", "Layout", "fields" });
+ defer testing.allocator.free(fields);
+ var expect_buf: [128]u8 = undefined;
+ const expect = try std.fmt.bufPrint(&expect_buf, "a: u8 @{d}\nb: u32 @{d}\nc: u64 @{d}\n", .{ @offsetOf(Layout, "a"), @offsetOf(Layout, "b"), @offsetOf(Layout, "c") });
+ try testing.expectEqualStrings(expect, fields);
+ const size = try x.h.readPath(&.{ "comptime", "types", "Layout", "size" });
+ defer testing.allocator.free(size);
+ try testing.expectEqualStrings(std.fmt.comptimePrint("{d}", .{@sizeOf(Layout)}), size);
+ const name = try x.h.readPath(&.{ "comptime", "types", "Qid", "name" });
+ defer testing.allocator.free(name);
+ try testing.expectEqualStrings(@typeName(cloud9.Qid), name);
+ const decls = try x.h.readPath(&.{ "comptime", "decls" });
+ defer testing.allocator.free(decls);
+ try testing.expectEqualStrings("one\ntwo\nthree\n", decls);
+}
+
+test "runtime/fn calls the function at open and at each read from offset 0" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ const names = try x.h.listPath(&.{ "runtime", "fn" });
+ defer TS.Harness.freeNames(names);
+ try testing.expectEqual(@typeInfo(TestFns).@"struct".decls.len, names.len);
+ const fib_text = try x.h.readPath(&.{ "runtime", "fn", "fib30" });
+ defer testing.allocator.free(fib_text);
+ try testing.expectEqualStrings("832040", fib_text);
+ const pid = try x.h.readPath(&.{ "runtime", "pid" });
+ defer testing.allocator.free(pid);
+ try testing.expectEqualStrings("4242", pid);
+ // the generator runs at open, then again at each read from offset 0, not at offset > 0
+ try x.h.walkTo(1, &.{ "runtime", "fn", "counter" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } });
+ try testing.expectEqual(@as(u32, 1), x.ctx.calls);
+ const r1 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ try testing.expectEqualStrings("2", r1.read);
+ const r2 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 1, .count = 100 } });
+ try testing.expectEqualStrings("", r2.read);
+ try testing.expectEqual(@as(u32, 2), x.ctx.calls);
+ // stat of a dynamic file reports length 0
+ const st = try x.h.ok(.{ .stat = .{ .fid = 1 } });
+ try testing.expectEqual(@as(u64, 0), st.stat.length);
+ try testing.expectEqual(@as(u32, 0o444), st.stat.mode);
+ // a generator error is the file's Rerror; a generator that overflows the slot too
+ try x.h.walkTo(2, &.{ "runtime", "fn", "failing" });
+ try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }, "bad command");
+ try x.h.walkTo(3, &.{ "runtime", "fn", "huge" });
+ try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "no space in buffer");
+ // a failed open frees its slot: two more dynamic opens still succeed
+ 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.ok(.{ .open = .{ .fid = 5, .mode = cloud9.oread } });
+}
+
+test "snapshot slot exhaustion is an Rerror and clunk frees the slot" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ try x.h.walkTo(1, &.{ "runtime", "fn", "fib30" });
+ try x.h.walkTo(2, &.{ "runtime", "fn", "fib30" });
+ try x.h.walkTo(3, &.{ "vars", "counter", "value" });
+ _ = 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");
+ // static and provider files need no slot
+ const t = try x.h.readPath(&.{ "vars", "counter", "type" });
+ defer testing.allocator.free(t);
+ try testing.expectEqualStrings("u64", t);
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } });
+ _ = 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);
+ 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");
+}
+
+test "ctl round trip" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ try x.h.walkTo(1, &.{"ctl"});
+ _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } });
+ const w = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "add 2 3\n" } });
+ try testing.expectEqual(@as(u32, 8), w.write);
+ const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ try testing.expectEqualStrings("5", r.read);
+ try testing.expectEqual(@as(i64, 5), x.ctx.ctl_state);
+ const st = try x.h.ok(.{ .stat = .{ .fid = 1 } });
+ try testing.expectEqual(@as(u64, 1), st.stat.length);
+ try testing.expectEqualStrings("ctl", st.stat.name);
+ try testing.expectEqual(@as(u32, 0o666), st.stat.mode);
+ const v1 = st.stat.qid.version;
+ _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "echo hello world" } });
+ const r2 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ try testing.expectEqualStrings("hello world", r2.read);
+ const r3 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 6, .count = 100 } });
+ try testing.expectEqualStrings("world", r3.read);
+ try testing.expect((try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.qid.version != v1);
+ const v2 = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.qid.version;
+ try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "frobnicate" } }, "bad command");
+ // a failed command leaves the previous result, length and version in place
+ const r4 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ try testing.expectEqualStrings("hello world", r4.read);
+ const st4 = try x.h.ok(.{ .stat = .{ .fid = 1 } });
+ try testing.expectEqual(@as(u64, 11), st4.stat.length);
+ try testing.expectEqual(v2, st4.stat.qid.version);
+ // even when the handler wrote part of a result before failing
+ try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "partial" } }, "bad command");
+ const r5 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ try testing.expectEqualStrings("hello world", r5.read);
+ try testing.expectEqualStrings("hello world", x.shared.ctlResult());
+ // the empty result is a legitimate result too
+ _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "echo" } });
+ try testing.expectEqualStrings("", (try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } })).read);
+ 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");
+}
+
+test "auth is not required and flush is answered" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ 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");
+}
+
+test "vars: value/type/size/addr/raw, fields and writes" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ const names = try x.h.listPath(&.{"vars"});
+ defer TS.Harness.freeNames(names);
+ try testing.expectEqual(@as(usize, 2), names.len);
+ try testing.expectEqualStrings("state", names[0]);
+ const entries = try x.h.listPath(&.{ "vars", "state" });
+ defer TS.Harness.freeNames(entries);
+ for ([_][]const u8{ "value", "type", "size", "addr", "raw", "f" }) |n| try testing.expect(TS.Harness.hasName(entries, n));
+ try testing.expectEqual(@as(usize, 6), entries.len);
+ const value = try x.h.readPath(&.{ "vars", "state", "value" });
+ defer testing.allocator.free(value);
+ try testing.expectEqualStrings("a: 1\nb: true\nname: \"hello\"\ninner:\n x: 0.5\n", value);
+ const tn = try x.h.readPath(&.{ "vars", "state", "type" });
+ defer testing.allocator.free(tn);
+ try testing.expectEqualStrings(@typeName(Exposed), tn);
+ const size = try x.h.readPath(&.{ "vars", "state", "size" });
+ defer testing.allocator.free(size);
+ try testing.expectEqualStrings(std.fmt.comptimePrint("{d}", .{@sizeOf(Exposed)}), size);
+ const addr = try x.h.readPath(&.{ "vars", "state", "addr" });
+ defer testing.allocator.free(addr);
+ var addr_buf: [32]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&addr_buf, "0x{x}", .{@intFromPtr(&x.exposed)}), addr);
+ const raw = try x.h.readPath(&.{ "vars", "state", "raw" });
+ defer testing.allocator.free(raw);
+ try testing.expectEqualSlices(u8, std.mem.asBytes(&x.exposed), raw);
+ try x.h.walkTo(1, &.{ "vars", "state", "raw" });
+ const raw_st = try x.h.ok(.{ .stat = .{ .fid = 1 } });
+ try testing.expectEqual(@as(u64, @sizeOf(Exposed)), raw_st.stat.length);
+ try testing.expectEqual(@as(u32, 0o444), raw_st.stat.mode);
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } });
+ // fields
+ const fnames = try x.h.listPath(&.{ "vars", "state", "f" });
+ defer TS.Harness.freeNames(fnames);
+ try testing.expectEqual(@as(usize, 4), fnames.len);
+ const a_value = try x.h.readPath(&.{ "vars", "state", "f", "a", "value" });
+ defer testing.allocator.free(a_value);
+ try testing.expectEqualStrings("1", a_value);
+ const a_type = try x.h.readPath(&.{ "vars", "state", "f", "a", "type" });
+ defer testing.allocator.free(a_type);
+ try testing.expectEqualStrings("u32", a_type);
+ const xv = try x.h.readPath(&.{ "vars", "state", "f", "inner", "f", "x", "value" });
+ defer testing.allocator.free(xv);
+ try testing.expectEqualStrings("0.5", xv);
+ const b_raw = try x.h.readPath(&.{ "vars", "state", "f", "b", "raw" });
+ defer testing.allocator.free(b_raw);
+ try testing.expectEqualSlices(u8, &.{1}, b_raw);
+ // writes
+ try x.h.writePath(&.{ "vars", "state", "f", "a", "value" }, "42");
+ try testing.expectEqual(@as(u32, 42), x.exposed.a);
+ try x.h.writePath(&.{ "vars", "state", "f", "b", "value" }, "false\n");
+ try testing.expect(!x.exposed.b);
+ try x.h.writePath(&.{ "vars", "state", "f", "inner", "f", "x", "value" }, "2.25");
+ try testing.expectEqual(@as(f32, 2.25), x.exposed.inner.x);
+ try x.h.writePath(&.{ "vars", "counter", "value" }, "0x10");
+ try testing.expectEqual(@as(u64, 16), x.counter);
+ try x.h.walkTo(2, &.{ "vars", "state", "f", "a", "value" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.ordwr } });
+ try x.h.expectFail(.{ .write = .{ .fid = 2, .offset = 0, .data = "abc" } }, "bad value");
+ const rd = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 100 } });
+ try testing.expectEqualStrings("42", rd.read);
+ const a_st = try x.h.ok(.{ .stat = .{ .fid = 2 } });
+ try testing.expectEqual(@as(u32, 0o644), a_st.stat.mode);
+ 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.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.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");
+ // .. 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.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");
+}
+
+test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and error mapping" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ const names = try x.h.listPath(&.{"prov"});
+ defer TS.Harness.freeNames(names);
+ try testing.expectEqual(@as(usize, 3), names.len);
+ try testing.expect(TS.Harness.hasName(names, "hello") and TS.Harness.hasName(names, "dir") and TS.Harness.hasName(names, "locked"));
+ const hello = try x.h.readPath(&.{ "prov", "hello" });
+ defer testing.allocator.free(hello);
+ try testing.expectEqualStrings("hello", hello);
+ try testing.expectEqual(@as(u32, 0), x.prov.total_refs); // every temp handle was clunked
+ // stat and qid scheme
+ try x.h.walkTo(1, &.{ "prov", "dir", "inner" });
+ const st = try x.h.ok(.{ .stat = .{ .fid = 1 } });
+ try testing.expectEqualStrings("inner", st.stat.name);
+ try testing.expectEqual(@as(u32, 0o600), st.stat.mode);
+ try testing.expectEqual(@as(u64, 3), st.stat.qid.path); // provider 0, handle 3
+ try testing.expectEqualStrings("tester", st.stat.gid);
+ try x.h.walkTo(2, &.{"prov"});
+ const root_st = try x.h.ok(.{ .stat = .{ .fid = 2 } });
+ try testing.expectEqualStrings("prov", root_st.stat.name);
+ try testing.expect(root_st.stat.qid.type & cloud9.qtdir != 0);
+ try testing.expectEqual(@as(u64, 0), root_st.stat.qid.path);
+ try testing.expectEqual(@as(u32, 1), x.prov.total_refs); // fid 1 holds inner; fid 2 holds root (unref'd)
+ // write then read back; opens are tracked through close
+ _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } });
+ try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].opens);
+ _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "abc" } });
+ _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 3, .data = "def" } });
+ const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 1, .count = 100 } });
+ try testing.expectEqualStrings("bcdef", r.read);
+ try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 100, .data = "z" } }, "no space left on device");
+ _ = 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
+ try x.h.walkTo(3, &.{ "prov", "locked" });
+ try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "permission denied");
+ 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 = 2, .newfid = 5, .names = &.{"missing"} } }, "file does not exist");
+ try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.owrite } }, "is a directory");
+ // 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);
+ _ = try x.h.ok(.{ .write = .{ .fid = 2, .offset = 0, .data = "fresh" } });
+ const rr = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 100 } });
+ try testing.expectEqualStrings("fresh", rr.read);
+ 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");
+ 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
+ var ws = stat_dontcare;
+ ws.name = "renamed";
+ ws.length = 2;
+ ws.mode = 0o600;
+ ws.mtime = 99;
+ _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = ws } });
+ const st2 = try x.h.ok(.{ .stat = .{ .fid = 2 } });
+ try testing.expectEqualStrings("renamed", st2.stat.name);
+ try testing.expectEqual(@as(u64, 2), st2.stat.length);
+ try testing.expectEqual(@as(u32, 0o600), st2.stat.mode);
+ 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");
+ ws = stat_dontcare;
+ ws.mode = cloud9.dmdir | 0o755;
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied");
+ ws = stat_dontcare;
+ ws.name = "bad/name";
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "bad file name");
+ ws.name = "..";
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "bad file 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");
+ ws = stat_dontcare;
+ ws.name = "root2"; // the provider root cannot be renamed
+ try x.h.walkTo(14, &.{"prov"});
+ try x.h.expectFail(.{ .wstat = .{ .fid = 14, .stat = ws } }, "permission denied");
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 14 } });
+ // remove always clunks; a non-empty directory refuses
+ _ = 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.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");
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 9 } });
+ // an i/o error from the provider maps to "i/o error"
+ try x.h.walkTo(9, &.{"prov"});
+ x.prov.fail_io = true;
+ try x.h.expectFail(.{ .walk = .{ .fid = 9, .newfid = 15, .names = &.{"hello"} } }, "i/o error");
+ x.prov.fail_io = false;
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 9 } });
+ // ORCLOSE removes on clunk
+ try x.h.walkTo(9, &.{"prov"});
+ _ = try x.h.ok(.{ .create = .{ .fid = 9, .name = "tmp", .perm = 0o644, .mode = cloud9.owrite | cloud9.orclose } });
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 9 } });
+ try x.h.walkTo(9, &.{"prov"});
+ try x.h.expectFail(.{ .walk = .{ .fid = 9, .newfid = 15, .names = &.{"tmp"} } }, "file does not exist");
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 9 } });
+ // walking .. out of the provider root and cloning provider fids keeps refs balanced
+ const up = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 10, .names = &.{ "prov", "dir", "..", "..", "build" } } });
+ try testing.expectEqual(@as(u16, 5), up.walk.nwqid);
+ try x.h.walkTo(11, &.{ "prov", "dir", "inner" });
+ _ = try x.h.ok(.{ .walk = .{ .fid = 11, .newfid = 12, .names = &.{} } });
+ try testing.expectEqual(@as(u32, 2), x.prov.nodes[3].refs);
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 11 } });
+ try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].refs);
+ // 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.ok(.{ .clunk = .{ .fid = 12 } });
+ for (x.h.conn.fids) |f| {
+ if (f.used) _ = try x.h.ok(.{ .clunk = .{ .fid = f.id } });
+ }
+ try testing.expectEqual(@as(u32, 0), x.prov.total_refs);
+}
+
+test "directory reads across offsets, bad offset, and records never split" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ try x.h.walkTo(1, &.{});
+ _ = 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");
+ // 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);
+ // small reads at consecutive offsets return every record exactly once
+ 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);
+ // the same for provider and var directories
+ const pn = try x.h.listPath(&.{ "prov", "dir" });
+ defer TS.Harness.freeNames(pn);
+ try testing.expectEqual(@as(usize, 1), pn.len);
+ try x.h.walkTo(2, &.{ "vars", "state", "f" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } });
+ 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");
+}
+
+test "Tversion mid-session resets fids and clunks every provider handle" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ try x.h.walkTo(1, &.{ "prov", "hello" });
+ try x.h.walkTo(2, &.{ "prov", "dir", "inner" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } });
+ try x.h.walkTo(3, &.{ "runtime", "fn", "fib30" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } });
+ try testing.expectEqual(@as(u32, 2), x.prov.total_refs);
+ try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].opens);
+ try testing.expectEqual(@as(usize, 4), x.h.conn.fidCount());
+ const before = x.prov.clunks;
+ try x.h.version(4096);
+ try testing.expectEqual(@as(usize, 0), x.h.conn.fidCount());
+ try testing.expectEqual(@as(u32, 0), x.prov.total_refs);
+ 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.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } });
+ try x.h.walkTo(1, &.{ "prov", "hello" });
+ // hangup does the same
+ x.h.conn.hangup();
+ try testing.expectEqual(@as(u32, 0), x.prov.total_refs);
+ 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" {
+ 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 } });
+}
+
+test "fid table is bounded per connection" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ var i: u32 = 1;
+ 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");
+ // 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");
+ // 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 testing.expectEqual(@as(u32, 1), x.prov.total_refs);
+}
+
+test "Shared refuses more providers or vars than configured" {
+ var ctx: TestCtx = .{};
+ var shared: TS.Shared = .init(&ctx);
+ var p1 = TestProv.init();
+ var p2 = TestProv.init();
+ var p3 = TestProv.init();
+ try shared.addProvider(.{ .name = "a", .ctx = &p1, .vtable = &TestProv.vtable });
+ try shared.addProvider(.{ .name = "b", .ctx = &p2, .vtable = &TestProv.vtable });
+ try testing.expectError(error.Full, shared.addProvider(.{ .name = "c", .ctx = &p3, .vtable = &TestProv.vtable }));
+ var v: [5]u32 = @splat(0);
+ try shared.expose("v0", &v[0]);
+ try shared.expose("v1", &v[1]);
+ try shared.expose("v2", &v[2]);
+ try shared.expose("v3", &v[3]);
+ try testing.expectError(error.Full, shared.expose("v4", &v[4]));
+}
+
+/// A server with a large fid table for the index tests.
+const big_cfg: Config = .{
+ .name = "big",
+ .msize = 8192,
+ .max_fids = 4096,
+ .max_providers = 1,
+ .max_vars = 1,
+ .snapshot_slots = 1,
+ .snapshot_bytes = 256,
+};
+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).
+fn adversarialId(i: u32) u32 {
+ return switch (i % 3) {
+ 0 => i * 8192 + 1,
+ 1 => 0x8000_0000 | i,
+ else => 0xFFFF_FFFF - i,
+ };
+}
+
+/// 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" {
+ 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;
+ try h.init(&shared, storage);
+ defer h.deinit();
+ const n: u32 = big_cfg.max_fids - 1; // fid 0 is the attach
+ var i: u32 = 0;
+ while (i < n) : (i += 1) {
+ _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = adversarialId(i), .names = &.{ "prov", "hello" } } });
+ }
+ 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
+ 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");
+ } 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
+ 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
+ var prng = std.Random.DefaultPrng.init(0x9a11);
+ const rnd = prng.random();
+ var live: [n + 1]bool = @splat(true);
+ live[0] = false; // never touch the attach fid
+ var round: usize = 0;
+ while (round < 20_000) : (round += 1) {
+ const id = 1 + rnd.uintLessThan(u32, n);
+ if (live[id]) {
+ _ = try h.ok(.{ .clunk = .{ .fid = id } });
+ } else {
+ _ = 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);
+ // 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);
+}
+
+test "open: a provider stat failure after a successful open closes the file again" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ try x.h.walkTo(1, &.{ "prov", "hello" });
+ x.prov.fail_stat = true;
+ 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.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
+ try x.h.walkTo(2, &.{"prov"});
+ x.prov.fail_stat = true;
+ try x.h.expectFail(.{ .create = .{ .fid = 2, .name = "born", .perm = 0o644, .mode = cloud9.owrite } }, "i/o error");
+ x.prov.fail_stat = false;
+ for (x.prov.nodes) |e| if (e.used and std.mem.eql(u8, e.nameSlice(), "born")) {
+ 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.expectEqual(@as(u32, 1), x.prov.total_refs); // fid 1 only
+}
+
+test "fid state machine: open twice, walk from open, remove/clunk of open provider fids" {
+ var x: Fixture = .{};
+ try x.init();
+ 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 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
+ try x.h.walkTo(7, &.{ "prov", "dir" });
+ try testing.expectEqual(@as(u32, 1), x.prov.nodes[2].refs);
+ _ = try x.h.ok(.{ .walk = .{ .fid = 7, .newfid = 7, .names = &.{ "..", "dir", "inner", "..", "..", "dir" } } });
+ try testing.expectEqual(@as(u32, 1), x.prov.nodes[2].refs);
+ try testing.expectEqual(@as(u32, 2), x.prov.nodes[3].refs);
+ _ = try x.h.ok(.{ .clunk = .{ .fid = 7 } });
+ try testing.expectEqual(@as(u32, 0), x.prov.nodes[2].refs);
+ // remove of an open fid: close, then remove, then clunk; refs and opens return to zero
+ _ = 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.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 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");
+ // 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.ok(.{ .clunk = .{ .fid = 3 } });
+ try testing.expectEqual(@as(u32, 0), x.prov.total_refs);
+}
+
+test "snapshot slots: exhaust, hold, Tversion frees; reads past the end and at huge offsets" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ try x.h.walkTo(1, &.{ "runtime", "fn", "fib30" });
+ try x.h.walkTo(2, &.{ "vars", "state", "value" });
+ try x.h.walkTo(3, &.{ "vars", "state", "addr" });
+ _ = 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);
+ // 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;
+ const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = std.math.maxInt(u64), .count = max_count } });
+ try testing.expectEqualStrings("", r.read);
+ const r2 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 1 << 63, .count = 0 } });
+ try testing.expectEqualStrings("", r2.read);
+ const r3 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = max_count } });
+ try testing.expectEqualStrings("832040", r3.read);
+ // raw beyond @sizeOf is empty; a partial raw read at the tail is bounded
+ try x.h.walkTo(4, &.{ "vars", "state", "raw" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } });
+ const raw_end = try x.h.ok(.{ .read = .{ .fid = 4, .offset = @sizeOf(Exposed), .count = 100 } });
+ try testing.expectEqualStrings("", raw_end.read);
+ const raw_tail = try x.h.ok(.{ .read = .{ .fid = 4, .offset = @sizeOf(Exposed) - 1, .count = 100 } });
+ try testing.expectEqual(@as(usize, 1), raw_tail.read.len);
+ const raw_huge = try x.h.ok(.{ .read = .{ .fid = 4, .offset = std.math.maxInt(u64) - 1, .count = 100 } });
+ try testing.expectEqualStrings("", raw_huge.read);
+ // Tversion releases the held slots
+ try x.h.version(test_cfg.msize);
+ try testing.expect(!x.h.conn.slot_used[0] and !x.h.conn.slot_used[1]);
+ _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } });
+ try x.h.walkTo(3, &.{ "vars", "state", "addr" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } });
+}
+
+test "static and var nodes refuse create, remove and wstat; directories refuse writes" {
+ var x: Fixture = .{};
+ try x.init();
+ defer x.deinit();
+ 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(.{ .remove = .{ .fid = fid } }, "permission denied");
+ try x.h.expectFail(.{ .clunk = .{ .fid = fid } }, "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(.{ .remove = .{ .fid = fid } }, "permission denied");
+ }
+ // writes to a var value at a non-zero offset and with an empty payload
+ try x.h.walkTo(1, &.{ "vars", "state", "f", "a", "value" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.owrite | cloud9.otrunc } });
+ try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "" } }, "bad value");
+ try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "-1" } }, "bad value");
+ try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "1e3" } }, "bad value");
+ try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "99999999999999999999" } }, "bad value");
+ 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");
+ try x.h.walkTo(2, &.{"README"});
+ _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oexec } });
+ 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" {
+ 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");
+}
+
+test "Conn.init clamps the msize cap to [msize_min, cfg.msize]" {
+ var ctx: TestCtx = .{};
+ 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);
+ 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);
+}
+
+test "parseIso8601 rejects malformed stamps and never traps" {
+ try testing.expectEqual(@as(?u32, null), parseIso8601(""));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("2023-11-14T22:13:20"));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("2023-13-14T22:13:20Z"));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("2023-11-32T22:13:20Z"));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("2023-11-14T24:13:20Z"));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("2023-11-14T22:60:20Z"));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("1969-12-31T23:59:59Z"));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("9999-12-31T23:59:59Z"));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("20x3-11-14T22:13:20Z"));
+ try testing.expectEqual(@as(?u32, null), parseIso8601("0000-01-01T00:00:00Z"));
+ try testing.expectEqual(@as(u32, 1_700_000_000), parseIso8601("2023-11-14T22:13:20Z").?);
+ try testing.expectEqual(@as(u32, 951_782_400), parseIso8601("2000-02-29T00:00:00Z").?);
+ try testing.expectEqual(@as(u32, 4_102_444_799), parseIso8601("2099-12-31T23:59:59Z").?);
+ 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);
+}