//! The freestanding 9P2000 introspection tree: a static tree generated at //! comptime from a `Config` (README, /build, /comptime, /runtime/fn, /ctl, //! /vars) plus runtime `Provider`s mounted at the top level, served as a //! backend of `cloud9.fs.Server` (the file-server engine, which owns fids, //! walks, directory cursors, permissions and Tflush). No allocator, no OS, //! no threads: every buffer is caller-owned (`Storage`, `Shared`, `Conn`), //! every table is sized at comptime. See docs/LIBRARY.md. const std = @import("std"); const builtin = @import("builtin"); const cloud9 = @import("cloud9"); const fs = cloud9.fs; const vars = @import("vars.zig"); const Writer = std.Io.Writer; /// 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 = "9proc", /// 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//{name,size,align,fields}. types: []const type = &.{}, /// /comptime/decls lists this type's pub decls (empty when null). decls_of: ?type = null, /// /runtime/fn/: every pub decl is a `fn (ctx: *anyopaque, w: *std.Io.Writer) anyerror!void`. fns: type = struct {}, /// /runtime/: 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, /// Reads a provider has parked with `error.Again`, per connection. max_parked: u8 = 8, }; /// Attributes of a provider node, filled by `VTable.stat` and `VTable.list`. 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). /// /// Answering later: `read` may return `error.Again` when nothing is there /// yet. The request then parks in the engine (up to `Config.max_parked` per /// connection; a further one fails with EAGAIN) and every later `Conn.step` /// asks the provider again, with the same handle and offset, until it /// answers; the fid stays open. A Tflush of a parked read answers it with /// "Interrupted system call"; a hangup or Tversion drops it and closes the /// file as usual. Nothing wakes a connection by itself: the platform layer /// steps it when its transport moves, so a provider that becomes ready has /// to make that happen (out of the core's hands). pub const Provider = struct { name: []const u8, ctx: *anyopaque, 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, Again }; 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, /// `error.Again` parks the read; see `Provider`. read: *const fn (ctx: *anyopaque, h: Handle, offset: u64, buf: []u8) Error!usize, write: *const fn (ctx: *anyopaque, h: Handle, offset: u64, data: []const u8) Error!usize, /// Returns the new node, already open with `mode`. 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 tree or a provider can raise /// (the engine's own refusals carry cloud9.fs's strings). pub fn ename(err: anyerror) []const u8 { return switch (err) { error.NotFound, error.NoFile => "file does not exist", 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", error.Again => "would block", else => "i/o error", }; } /// The engine errno closest to an error, beside its `ename`. fn errno(err: anyerror) u16 { return switch (err) { error.NotFound, error.NoFile => fs.E.NOENT, error.Perm, error.Excl => fs.E.PERM, error.Exists => fs.E.EXIST, error.NotEmpty => fs.E.NOTEMPTY, error.NotDir => fs.E.NOTDIR, error.IsDir => fs.E.ISDIR, error.BadOffset, error.Invalid, error.BadValue, error.BadCommand, error.BadName => fs.E.INVAL, error.NoSpace, error.WriteFailed, error.NoSnapshot => fs.E.NOSPC, error.Unsupported => fs.E.NOSYS, error.OutOfMemory => fs.E.NOMEM, else => fs.E.IO, }; } /// A "don't care" Twstat: every field left as it is. pub const stat_dontcare: cloud9.Stat = .{ .type = 0xFFFF, .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//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.` 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//, pub decls \\/runtime live facts; fn/ calls a Zig function on every read \\/ctl write a command, read the result \\/vars exposed variables: /{{value,type,size,addr,raw,f//...}} \\ \\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 >= fs.msize_min); std.debug.assert(cfg.snapshot_slots > 0 and cfg.max_fids > 0 and cfg.max_parked > 0); // Provider index 0xFE/0xFF would collide with the var/static qid tags. std.debug.assert(cfg.max_providers < 0xFE); } // -- node ids and qid paths ------------------------------------------ const static_tag: u64 = 0xFF << 56; const var_tag: u64 = 0xFE << 56; const handle_mask: u64 = (1 << 56) - 1; /// The engine's root: static node 0. const root_id: u64 = static_tag; // -- storage -------------------------------------------------------- /// Per-connection buffers; the caller places one in static memory. pub const Storage = struct { in: [cfg.msize]u8, out: [@max(cfg.msize, 2 * fs.msize_min)]u8, /// Staging area for read replies (directory records, provider and raw reads). data: [cfg.msize]u8, snapshots: [cfg.snapshot_slots][cfg.snapshot_bytes]u8, }; 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 index. The core mixes in a /// connection counter and buffer addresses; a platform layer with a /// random source may set this once after `init` to make the salt /// 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 `/`. 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` 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; } // -- the engine and its backend --------------------------------------- /// What the engine knows of this tree: the contract types and the /// capabilities it may ask for. The requests themselves are served /// by `Conn.serve`. pub const Backend = struct { pub const Req = fs.Req; pub const Reply = fs.Reply; pub const features: fs.Features = .{ .create = true, .remove = true, .wstat = true, .references = true }; }; /// The file-server engine this tree is a backend of: it owns the fid /// table, permissions, directory cursors, Tflush and the releases a /// dropped connection owes. Names are not kept per fid (`getattr` /// answers them), so a fid costs the same whatever the name length. pub const Engine = fs.Server(Backend, .{ .fid_capacity = cfg.max_fids, .slot_capacity = cfg.max_parked, .park_data_max = 0, .name_capacity = 0, .fid_index = true, }); /// A reference to a node of the tree: the engine's node id decoded. const NodeRef = union(enum) { static: u32, prov: struct { idx: u8, h: Provider.Handle }, @"var": struct { idx: u8, node: u32 }, }; fn nodeId(ref: NodeRef) u64 { return switch (ref) { .static => |idx| static_tag | idx, .@"var" => |v| var_tag | (@as(u64, v.idx) << 32) | v.node, // Provider ids are offset by one: the engine reads node 0 as // "the node asked about", and provider 0's root would be 0. .prov => |p| (@as(u64, p.idx + 1) << 56) | (p.h & handle_mask), }; } /// Everything the engine needs to know about a node for qid/stat. const Info = struct { is_dir: bool, mode: u32, length: u64, atime: u32, mtime: u32, version: u32, /// The qid path. path: u64, name: []const u8, fn attr(i: Info, ref: NodeRef) fs.Attr { const id = nodeId(ref); return .{ .name = i.name, .node = id, .dir = i.is_dir, .size = i.length, .mode = @truncate(i.mode), .mtime = i.mtime, .atime = i.atime, .version = i.version, .path = if (i.path != id) i.path else null, .append = i.mode & cloud9.dmappend != 0, .excl = i.mode & cloud9.dmexcl != 0, }; } }; /// A directory entry as the engine's readdir record wants it. const Entry = struct { path: u64, dir: bool, name: []const u8 }; /// The engine's handle of an open file that holds no snapshot slot. const no_snapshot: u32 = std.math.maxInt(u32); /// One 9P connection: the engine plus this tree's per-connection /// state (snapshot slots). pub const Conn = struct { shared: *Shared, storage: *Storage, engine: Engine, /// Largest msize this connection negotiates. msize_cap: u32, slot_used: [cfg.snapshot_slots]bool = @splat(false), slot_len: [cfg.snapshot_slots]u32 = @splat(0), pub fn init(shared: *Shared, storage: *Storage, msize: u32) Conn { shared.conn_seq +%= 1; const addr = @intFromPtr(storage) ^ (@intFromPtr(shared) << 7); const seed = shared.hash_seed ^ (shared.conn_seq *% 0x9E37_79B1) ^ @as(u32, @truncate(addr)) ^ @as(u32, @truncate(addr >> 16)); const cap = @max(@min(msize, cfg.msize), fs.msize_min); return .{ .shared = shared, .storage = storage, .engine = .init(.{ .in = storage.in[0..cap], .out = &storage.out, .root = root_id, .seed = seed }), .msize_cap = cap, }; } /// Feeds transport bytes; returns how many were taken. pub fn push(c: *Conn, bytes: []const u8) usize { return c.engine.push(bytes); } /// Bytes to send to the client. pub fn output(c: *const Conn) []const u8 { return c.engine.output(); } pub fn wrote(c: *Conn, n: usize) void { c.engine.wrote(n); } /// Free space in the input buffer (one msize-sized frame at most). pub fn inputRoom(c: *const Conn) usize { return c.engine.protocol.in.len - c.engine.protocol.in_len; } /// Drops every fid (telling providers) and kills the session. pub fn hangup(c: *Conn) void { c.engine.hangup(); while (c.engine.next()) |req| c.serve(req); } /// Serves at most one request of the engine (a 9P request is one /// or more of them), after re-asking providers about every parked /// read. Returns false when more input (or output drainage) is /// needed. `error.Protocol` is terminal. pub fn step(c: *Conn) error{Protocol}!bool { if (c.engine.protocol.dead) return error.Protocol; while (c.engine.retry()) |req| c.serve(req); const req = c.engine.next() orelse { if (c.engine.protocol.dead) return error.Protocol; return false; }; c.serve(req); if (c.engine.protocol.dead) return error.Protocol; return true; } /// Number of fids currently held. pub fn fidCount(c: *const Conn) usize { return c.engine.fidCount(); } /// The provider handle a fid holds, if it holds a provider node. pub fn providerHandle(c: *const Conn, fid: u32) ?Provider.Handle { for (c.engine.fids) |f| { if (!f.used or f.orphan or f.fid != fid) continue; return switch (c.refOf(f.node) orelse return null) { .prov => |p| p.h, else => null, }; } return null; } // -- serving the engine -- fn serve(c: *Conn, req: fs.Req) void { var bytes: []const u8 = ""; const reply = c.dispatch(req, &bytes) catch |e| failing(req.tag, e); c.engine.reply(&reply, bytes); } fn failing(tag: u64, e: anyerror) fs.Reply { if (e == error.Again) return .{ .tag = tag, .status = .again }; return .{ .tag = tag, .status = .err, .errno = errno(e), .ename = ename(e) }; } fn dispatch(c: *Conn, req: fs.Req, bytes: *[]const u8) anyerror!fs.Reply { const ref = c.refOf(req.node) orelse return error.NotFound; switch (req.op) { .lookup => { const l = try c.lookup(ref, req.data); return .{ .tag = req.tag, .attr = l.info.attr(l.ref) }; }, .getattr => return .{ .tag = req.tag, .attr = (try c.info(ref)).attr(ref) }, .setattr => return c.setattr(req, ref), .open => return if (req.create) c.create(req, ref) else c.open(req, ref), .read => return c.read(req, ref, bytes), .readdir => return c.readDir(req, ref, bytes), .write => return c.write(req, ref), .release => return c.release(req, ref), } } /// Decodes an engine node id; null for one this tree never issued. fn refOf(c: *const Conn, id: u64) ?NodeRef { const top: u8 = @intCast(id >> 56); switch (top) { 0xFF => { const idx = id & handle_mask; if (idx >= flat_len) return null; return .{ .static = @intCast(idx) }; }, 0xFE => { const vidx: u8 = @truncate(id >> 32); const node: u32 = @truncate(id); if ((id >> 40) & 0xFFFF != 0 or vidx >= c.shared.nvars) return null; if (node >= c.shared.vars[vidx].vt.nodes.len) return null; return .{ .@"var" = .{ .idx = vidx, .node = node } }; }, else => { if (top == 0 or top - 1 >= c.shared.nprov) return null; return .{ .prov = .{ .idx = top - 1, .h = id & handle_mask } }; }, } } // -- 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 provPath(idx: u8, h: Provider.Handle, st: NodeStat) u64 { return (@as(u64, idx) << 56) | ((if (st.path != 0) st.path else h) & handle_mask); } fn provInfo(pr: Provider, idx: u8, h: Provider.Handle, st: NodeStat) Info { return .{ .is_dir = st.isDir(), .mode = st.mode, .length = if (st.isDir()) 0 else st.length, .atime = st.atime, .mtime = st.mtime, .version = st.version, .path = provPath(idx, h, st), .name = if (h == Provider.root) pr.name else st.name, }; } /// Releases a provider handle that no fid holds. fn releaseRef(c: *Conn, ref: NodeRef) void { if (ref == .prov) { const p = c.shared.providers[ref.prov.idx]; p.vtable.clunk(p.ctx, ref.prov.h); } } const Looked = struct { ref: NodeRef, info: Info }; /// Resolves `name` in the directory `ref`. A returned provider ref /// is a fresh handle the engine will release exactly once. 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`, or null past the end. The /// name borrows static memory or the provider's NodeStat. fn entry(c: *Conn, ref: NodeRef, i: usize) !?Entry { switch (ref) { .static => |idx| { const f = flat[idx]; if (f.node.kind == .vars) { if (i >= c.shared.nvars) return null; return .{ .path = var_tag | (@as(u64, i) << 32), .dir = true, .name = c.shared.vars[i].name }; } if (i < f.count) { const ci = f.first + @as(u32, @intCast(i)); return .{ .path = static_tag | ci, .dir = flat[ci].node.isDir(), .name = flat[ci].node.name }; } if (idx == 0) { const pi = i - f.count; if (pi >= c.shared.nprov) return null; return .{ .path = @as(u64, pi) << 56, .dir = true, .name = c.shared.providers[pi].name }; } return null; }, .@"var" => |v| { const vt = c.shared.vars[v.idx].vt; const n = vt.nodes[v.node]; if (i >= n.count) return null; const ci = n.first + @as(u32, @intCast(i)); return .{ .path = var_tag | (@as(u64, v.idx) << 32) | ci, .dir = vt.nodes[ci].isDir(), .name = vt.nodes[ci].name }; }, .prov => |p| { const pr = c.provider(p.idx); var st: NodeStat = .{ .mode = 0 }; if (!try pr.vtable.list(pr.ctx, p.h, i, &st)) return null; return .{ .path = provPath(p.idx, st.handle, st), .dir = st.isDir(), .name = st.name }; }, } } // -- 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 slot `s`. fn generate(c: *Conn, ref: NodeRef, s: u8) !void { var w: Writer = .fixed(&c.storage.snapshots[s]); c.slot_len[s] = 0; switch (ref) { .static => |idx| try flat[idx].node.gen.?(c.shared.ctx, &w), .@"var" => |v| { const n = c.shared.vars[v.idx].vt.nodes[v.node]; 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, }; } fn snapshot(c: *Conn, handle: u32) []const u8 { return c.storage.snapshots[handle][0..c.slot_len[handle]]; } // -- request handlers -- fn open(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { const mode = req.omode; const acc = mode & 3; const want_write = acc == cloud9.owrite or acc == cloud9.ordwr; const trunc = mode & cloud9.otrunc != 0; switch (ref) { .static => |idx| switch (flat[idx].node.kind) { .dir, .vars, .ctl => {}, .static, .dynamic => if (want_write or trunc) return error.Perm, }, .@"var" => |v| { const n = c.shared.vars[v.idx].vt.nodes[v.node]; if (n.isDir()) { if (want_write or trunc) return error.IsDir; } else if ((want_write or trunc) and !n.writable()) return error.Perm; }, .prov => |p| { const pr = c.provider(p.idx); try pr.vtable.open(pr.ctx, p.h, mode); }, } const i = c.info(ref) catch |e| { // The provider's open succeeded but its stat did not: undo the open. if (ref == .prov) { const pr = c.provider(ref.prov.idx); if (pr.vtable.close) |close| close(pr.ctx, ref.prov.h); } return e; }; var handle: u32 = no_snapshot; if (c.isDynamic(ref)) { const s = try c.takeSlot(); c.generate(ref, s) catch |e| { c.slot_used[s] = false; return e; }; handle = s; } return .{ .tag = req.tag, .attr = i.attr(ref), .handle = handle }; } fn create(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { const p = switch (ref) { .prov => |p| p, else => return error.Perm, }; const pr = c.provider(p.idx); const create_fn = pr.vtable.create orelse return error.Perm; try validName(req.data); const h = try create_fn(pr.ctx, p.h, req.data, req.perm, req.omode); const node: NodeRef = .{ .prov = .{ .idx = p.idx, .h = h } }; const i = c.info(node) catch |e| { if (pr.vtable.close) |close| close(pr.ctx, h); pr.vtable.clunk(pr.ctx, h); return e; }; return .{ .tag = req.tag, .attr = i.attr(node), .handle = no_snapshot }; } fn read(c: *Conn, req: fs.Req, ref: NodeRef, bytes: *[]const u8) !fs.Reply { const data = &c.storage.data; const count: usize = @min(req.size, data.len); const src: []const u8 = switch (ref) { .static => |idx| blk: { const n = flat[idx].node; switch (n.kind) { .static => break :blk n.content, .ctl => break :blk c.shared.ctlResult(), .dynamic => { if (req.off == 0) try c.generate(ref, @intCast(req.handle)); break :blk c.snapshot(req.handle); }, .dir, .vars => return error.IsDir, } }, .@"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 (req.off == 0) try c.generate(ref, @intCast(req.handle)); break :blk c.snapshot(req.handle); }, .raw => break :blk c.varBase(v.idx, v.node)[0..n.size], .dir, .fields => return error.IsDir, } }, .prov => |p| { const pr = c.provider(p.idx); const n = try pr.vtable.read(pr.ctx, p.h, req.off, data[0..count]); bytes.* = data[0..@min(n, count)]; return .{ .tag = req.tag }; }, }; if (req.off >= src.len) return .{ .tag = req.tag }; const off: usize = @intCast(req.off); const n = @min(count, src.len - off); if (ref == .@"var" and c.shared.vars[ref.@"var".idx].vt.nodes[ref.@"var".node].kind == .raw) { // Copy out of the variable so the reply does not read live memory twice. @memcpy(data[0..n], src[off..][0..n]); bytes.* = data[0..n]; } else { bytes.* = src[off..][0..n]; } return .{ .tag = req.tag }; } /// Stages `node:u64le dir:u8 len:u8 name` records from entry /// `req.off` on, about as many as the engine can fit in `req.size`. fn readDir(c: *Conn, req: fs.Req, ref: NodeRef, bytes: *[]const u8) !fs.Reply { const data = &c.storage.data; const who = c.engine.uname_len; var n: usize = 0; var est: usize = 0; var i: usize = @intCast(req.off); while (est < req.size) : (i += 1) { const e = (try c.entry(ref, i)) orelse break; if (e.name.len > 255 or n + 10 + e.name.len > data.len) break; std.mem.writeInt(u64, data[n..][0..8], e.path, .little); data[n + 8] = @intFromBool(e.dir); data[n + 9] = @intCast(e.name.len); @memcpy(data[n + 10 ..][0..e.name.len], e.name); n += 10 + e.name.len; est += cloud9.stat_fixed + 2 + e.name.len + 3 * who; } bytes.* = data[0..n]; return .{ .tag = req.tag }; } fn write(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { switch (ref) { .static => |idx| switch (flat[idx].node.kind) { .ctl => try c.ctlCommand(req.data), .dir, .vars => return error.IsDir, else => return error.Perm, }, .@"var" => |v| { const n = c.shared.vars[v.idx].vt.nodes[v.node]; if (n.isDir()) return error.IsDir; const set = n.set orelse return error.Perm; try set(c.varBase(v.idx, v.node), req.data); }, .prov => |p| { const pr = c.provider(p.idx); const n = try pr.vtable.write(pr.ctx, p.h, req.off, req.data); return .{ .tag = req.tag, .written = @intCast(@min(n, req.data.len)) }; }, } return .{ .tag = req.tag, .written = @intCast(req.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; } /// A wstat, or the truncation hint of an OTRUNC open (which the /// provider's `open` performs itself; nothing to do here). fn setattr(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { if (!req.set.any()) return .{ .tag = req.tag }; const p = switch (ref) { .prov => |p| p, else => return error.Perm, }; const pr = c.provider(p.idx); const ws = pr.vtable.wstat orelse return error.Perm; if (req.set.name and p.h == Provider.root) return error.Perm; var st = stat_dontcare; if (req.set.name) st.name = req.data; if (req.set.mode) st.mode = req.perm; if (req.set.mtime) st.mtime = req.mtime; if (req.set.length) st.length = req.length; try ws(pr.ctx, p.h, &st); return .{ .tag = req.tag, .attr = (try c.info(ref)).attr(ref) }; } /// The engine lets go of a reference: closes the open handle it /// came with, removes the node on Tremove or ORCLOSE, and clunks /// the provider handle exactly once. fn release(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply { if (req.opened and req.handle != no_snapshot) c.slot_used[req.handle] = false; switch (ref) { .prov => |p| { const pr = c.provider(p.idx); if (req.opened) if (pr.vtable.close) |close| close(pr.ctx, p.h); var result: anyerror!void = {}; if (req.remove) { if (pr.vtable.remove) |rm| result = rm(pr.ctx, p.h) else result = error.Perm; } pr.vtable.clunk(pr.ctx, p.h); try result; }, else => if (req.remove) return error.Perm, } return .{ .tag = req.tag }; } }; // -- 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); } /// Moves bytes both ways and steps the connection until nothing /// moves; true when anything did. pub fn pump(h: *Harness) !bool { var moved = false; while (true) { var again = false; while (h.client.output().len > 0) { const k = h.conn.push(h.client.output()); if (k == 0) break; h.client.wrote(k); again = true; } while (try h.conn.step()) again = true; while (h.conn.output().len > 0) { const n = h.client.push(h.conn.output()); if (n == 0) break; h.conn.wrote(n); again = true; } if (!again) return moved; moved = true; } } /// One round trip; the result borrows the client input buffer until the next call. pub fn rpc(h: *Harness, req: cloud9.Client.Request) !cloud9.Client.Result { _ = try h.client.submit(req); while (true) { const moved = try h.pump(); if (h.client.take()) |done| return done.result; if (!moved) return error.Stuck; } } 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; // The engine's own Rerror strings, for the conditions it decides itself. const e_unknown_fid = fs.e_unknown_fid; const e_fid_in_use = fs.e_fid_in_use; const e_too_many_fids = fs.e_too_many_fids; const e_bad_use = fs.e_bad_use; const e_already_open = fs.e_already_open; const e_bad_offset = fs.e_bad_offset; const e_perm = fs.e_perm; const e_not_dir = fs.e_not_dir; const e_wstat = fs.e_wstat; const e_illegal_name = fs.e_illegal_name; const e_small_msize = fs.e_small_msize; const e_count_small = fs.e_count_small; const e_interrupted = fs.e_interrupted; const TestBuild = struct { pub const zig_version: []const u8 = builtin.zig_version_string; pub const target: []const u8 = "test-target"; 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, .max_parked = 2, }; const TS = Server(test_cfg); /// Whether `fid` is open, read from the engine's table. fn isOpen(c: *const TS.Conn, fid: u32) bool { for (c.engine.fids) |f| if (f.used and !f.orphan and f.fid == fid) return f.open; return false; } /// A small in-memory provider: /prov/{hello,dir/{inner}} with create/remove/wstat, /// counting every handle reference so tests can check clunk discipline. A /// read of `hello` parks (error.Again) while `park` is set. 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, reads: u32 = 0, fail_io: bool = false, fail_stat: bool = false, park: 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); p.reads += 1; if (p.park and h == 1) return error.Again; if (offset >= e.len) return 0; const n = @min(buf.len, e.len - @as(usize, @intCast(offset))); @memcpy(buf[0..n], e.data[@intCast(offset)..][0..n]); 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 } }, e_perm); try x.h.expectFail(.{ .remove = .{ .fid = 1 } }, "permission denied"); try x.h.walkTo(2, &.{"build"}); try x.h.expectFail(.{ .create = .{ .fid = 2, .name = "nope", .perm = 0o644, .mode = cloud9.owrite } }, e_perm); // a wstat that changes nothing is answered by the engine without asking _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = stat_dontcare } }); var ws = stat_dontcare; ws.mtime = 5; try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied"); const st = try x.h.ok(.{ .stat = .{ .fid = 2 } }); try testing.expectEqualStrings("build", st.stat.name); try testing.expectEqualStrings("tester", st.stat.uid); try testing.expect(st.stat.qid.type & cloud9.qtdir != 0); try testing.expectEqual(TS.build_secs, st.stat.mtime); try testing.expectEqual(TS.build_secs, st.stat.atime); try testing.expectEqual(@as(u32, 0), parseIso8601("1970-01-01T00:00:00Z").?); try testing.expectEqual(@as(?u32, null), parseIso8601("unknown")); } 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.walkTo(5, &.{ "runtime", "fn", "fib30" }); _ = 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); // an open fid cannot be walked from, not even cloned, and stays open try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 3, .names = &.{} } }, e_bad_use); try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{} } }, e_bad_use); try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{".."} } }, e_bad_use); const r2 = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 100 } }); try testing.expectEqualStrings("7", r2.read); try x.h.expectFail(.{ .read = .{ .fid = 4, .offset = 0, .count = 100 } }, e_unknown_fid); } 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 } }, e_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" } }, e_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 } }, e_perm); _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); try x.h.walkTo(4, &.{ "vars", "state", "f", "name", "value" }); try x.h.expectFail(.{ .open = .{ .fid = 4, .mode = cloud9.owrite } }, e_perm); _ = try x.h.ok(.{ .clunk = .{ .fid = 4 } }); try x.h.walkTo(5, &.{ "vars", "state" }); try x.h.expectFail(.{ .open = .{ .fid = 5, .mode = cloud9.owrite } }, e_perm); try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "z", .perm = 0o644, .mode = cloud9.owrite } }, e_perm); // .. climbs back out of the var tree; unknown names fail const up = try x.h.ok(.{ .walk = .{ .fid = 5, .newfid = 6, .names = &.{ "f", "inner", "..", "..", "..", "..", "README" } } }); try testing.expectEqual(@as(u16, 7), up.walk.nwqid); _ = try x.h.ok(.{ .clunk = .{ .fid = 6 } }); try x.h.walkTo(7, &.{"vars"}); try x.h.expectFail(.{ .walk = .{ .fid = 7, .newfid = 8, .names = &.{"nope"} } }, "file does not exist"); try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, e_bad_use); _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); try x.h.walkTo(2, &.{ "vars", "state", "f", "a", "value" }); try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, e_not_dir); } 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: the engine's from the walked mode, the provider's as its own strings try x.h.walkTo(3, &.{ "prov", "locked" }); try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, e_perm); try x.h.walkTo(4, &.{ "prov", "hello" }); try x.h.expectFail(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{"x"} } }, e_not_dir); try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 5, .names = &.{"missing"} } }, "file does not exist"); try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.owrite } }, e_perm); // create in a provider directory: the fid becomes the new open file const cr = try x.h.ok(.{ .create = .{ .fid = 2, .name = "new", .perm = 0o644, .mode = cloud9.ordwr } }); try testing.expectEqual(cloud9.qtfile, cr.create.qid.type); _ = 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 } }, e_illegal_name); try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.owrite } }, e_perm); const dr = try x.h.ok(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.oread } }); try testing.expectEqual(cloud9.qtdir, dr.create.qid.type); // wstat: rename, truncate, mode, mtime; immutable fields are refused by the engine 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 } }, e_wstat); ws = stat_dontcare; ws.mode = cloud9.dmdir | 0o755; try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_wstat); ws = stat_dontcare; ws.name = "bad/name"; try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_illegal_name); ws.name = ".."; try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_illegal_name); ws = stat_dontcare; ws.length = 5; try x.h.walkTo(7, &.{ "prov", "d" }); try x.h.expectFail(.{ .wstat = .{ .fid = 7, .stat = ws } }, e_wstat); 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 } }, e_unknown_fid); _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); try x.h.walkTo(9, &.{"prov"}); try x.h.expectFail(.{ .walk = .{ .fid = 9, .newfid = 15, .names = &.{"renamed"} } }, "file does not exist"); _ = 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 } }, e_unknown_fid); _ = try x.h.ok(.{ .clunk = .{ .fid = 12 } }); for (x.h.conn.engine.fids) |f| { if (f.used and !f.orphan) _ = try x.h.ok(.{ .clunk = .{ .fid = f.fid } }); } try testing.expectEqual(@as(u32, 0), x.prov.total_refs); } 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 } }, e_bad_offset); // offset 0 restarts; the same bytes come back const again = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); try testing.expectEqual(first.read.len, again.read.len); // 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 is refused rather than splitting it try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, e_count_small); // the same for provider and var directories const pn = try x.h.listPath(&.{ "prov", "dir" }); defer TS.Harness.freeNames(pn); 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 } }, e_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 } }, e_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 "msize: below the engine's floor the connection dies; at the floor everything that fits is served" { var x: Fixture = .{}; try x.init(); defer x.deinit(); try testing.expectError(error.Protocol, x.h.version(64)); var y: Fixture = .{}; try y.init(); defer y.deinit(); try y.h.version(fs.msize_min); _ = try y.h.ok(.{ .attach = .{ .fid = 0, .uname = "t" } }); const st = try y.h.ok(.{ .stat = .{ .fid = 0 } }); try testing.expectEqualStrings("/", st.stat.name); // A full 16-element Rwalk is exactly msize_min. const w = try y.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &([_][]const u8{"."} ** 16) } }); try testing.expectEqual(@as(u16, 16), w.walk.nwqid); _ = try y.h.ok(.{ .clunk = .{ .fid = 1 } }); // An Rstat that cannot fit is an Rerror, not a dead connection. try y.h.walkTo(2, &.{"prov"}); const long_name = [_]u8{'n'} ** 180; // Tcreate fits; the Rstat (61 + 180 bytes) does not _ = try y.h.ok(.{ .create = .{ .fid = 2, .name = &long_name, .perm = 0o644, .mode = cloud9.oread } }); try y.h.expectFail(.{ .stat = .{ .fid = 2 } }, e_small_msize); _ = try y.h.ok(.{ .remove = .{ .fid = 2 } }); try y.h.walkTo(1, &.{"README"}); _ = try y.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); const rd = try y.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = fs.msize_min - cloud9.iohdrsz } }); try testing.expect(rd.read.len > 0 and rd.read.len <= fs.msize_min - cloud9.iohdrsz); _ = try y.h.ok(.{ .clunk = .{ .fid = 1 } }); } test "fid table is bounded per connection" { 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 = &.{} } }, e_too_many_fids); try x.h.expectFail(.{ .attach = .{ .fid = i, .uname = "tester" } }, e_too_many_fids); // self-walks and clunks still work at the limit _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 0, .names = &.{"build"} } }); _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }); try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{"README"} } }, e_fid_in_use); try x.h.expectFail(.{ .walk = .{ .fid = 1234, .newfid = 2, .names = &.{} } }, e_unknown_fid); // a walk into a provider at the limit must not leak the handle _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{ "..", "prov", "hello" } } }); try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = i + 1, .names = &.{} } }, e_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 churn test. 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 engine's index: dense low ids, ids with /// only high bits set, and ids counting down from 2^32-1 (all distinct for i < 2^20). fn adversarialId(i: u32) u32 { return switch (i % 3) { 0 => i * 8192 + 1, 1 => 0x8000_0000 | i, else => 0xFFFF_FFFF - i, }; } test "fid table: thousands of fids, clunk in hostile orders, reuse, Tversion" { var ctx: TestCtx = .{}; var shared: BigS.Shared = .init(&ctx); var prov = TestProv.init(); try shared.addProvider(prov.provider()); const storage = try testing.allocator.create(BigS.Storage); defer testing.allocator.destroy(storage); const h = try testing.allocator.create(BigS.Harness); defer testing.allocator.destroy(h); try h.init(&shared, storage); defer h.deinit(); const n: u32 = big_cfg.max_fids - 1; // fid 0 is the attach 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 = &.{} } }, e_too_many_fids); try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = adversarialId(5), .names = &.{} } }, e_fid_in_use); try h.expectFail(.{ .attach = .{ .fid = adversarialId(7), .uname = "t" } }, e_fid_in_use); try h.expectFail(.{ .clunk = .{ .fid = 0x7FFF_FFFF } }, e_unknown_fid); try h.expectFail(.{ .clunk = .{ .fid = adversarialId(n) } }, e_unknown_fid); // clunk every third fid, then the rest from the top i = 0; while (i < n) : (i += 3) _ = try h.ok(.{ .clunk = .{ .fid = adversarialId(i) } }); i = n; while (i > 0) { i -= 1; if (i % 3 == 0) { try h.expectFail(.{ .clunk = .{ .fid = adversarialId(i) } }, e_unknown_fid); } else { _ = try h.ok(.{ .clunk = .{ .fid = adversarialId(i) } }); } } try testing.expectEqual(@as(usize, 1), h.conn.fidCount()); try testing.expectEqual(@as(u32, 0), prov.total_refs); // the whole table is reusable after the churn i = 0; while (i < n) : (i += 1) _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = n - i, .names = &.{} } }); try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = n + 1, .names = &.{} } }, e_too_many_fids); // pseudo-random alloc/free storm var prng = std.Random.DefaultPrng.init(0x9a11); const rnd = prng.random(); var live: [n + 1]bool = @splat(true); 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]; } var expected: usize = 1; for (live) |l| expected += @intFromBool(l); try testing.expectEqual(expected, h.conn.fidCount()); // Tversion drops everything and the table starts over, provider refs balanced try h.version(big_cfg.msize); try testing.expectEqual(@as(usize, 0), h.conn.fidCount()); try testing.expectEqual(@as(u32, 0), prov.total_refs); _ = try h.ok(.{ .attach = .{ .fid = 0xFFFF_FFFE, .uname = "t" } }); _ = try h.ok(.{ .walk = .{ .fid = 0xFFFF_FFFE, .newfid = 0, .names = &.{} } }); try testing.expectEqual(@as(usize, 2), h.conn.fidCount()); } 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 } }, e_bad_use); _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); try testing.expectEqual(@as(u32, 1), x.prov.nodes[1].opens); // the same for create: a stat failure after the provider created the node releases it 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(!isOpen(&x.h.conn, 2)); 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 } }, e_already_open); try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{"."} } }, e_bad_use); try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{} } }, e_bad_use); try x.h.expectFail(.{ .create = .{ .fid = 1, .name = "z", .perm = 0o644, .mode = cloud9.oread } }, e_already_open); // a second, unopened reference to the same node try x.h.walkTo(2, &.{ "prov", "dir", "inner" }); try testing.expectEqual(@as(u32, 2), x.prov.nodes[3].refs); try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].opens); // walking newfid == fid with names on an unopened provider fid swaps the handle, refs balanced 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 } }, e_unknown_fid); _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); try testing.expectEqual(@as(u32, 0), x.prov.total_refs); // walking "." on a file fid is "not a directory" at the protocol level, without a provider walk try x.h.walkTo(3, &.{ "prov", "hello" }); const before = x.prov.clunks; try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"."} } }, e_not_dir); try testing.expectEqual(before, x.prov.clunks); try testing.expectEqual(@as(u32, 1), x.prov.total_refs); // a partial walk through a file releases the handles it took const part = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 5, .names = &.{ "prov", "hello", "x", "y" } } }); try testing.expectEqual(@as(u16, 2), part.walk.nwqid); try testing.expectEqual(@as(u32, 1), x.prov.total_refs); try x.h.expectFail(.{ .clunk = .{ .fid = 5 } }, e_unknown_fid); // Tremove is always a clunk, even of a static node or when the provider refuses try x.h.walkTo(6, &.{"README"}); try x.h.expectFail(.{ .remove = .{ .fid = 6 } }, "permission denied"); try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, e_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(!isOpen(&x.h.conn, 3)); // reads at offsets near 2^64 never trap (counts above msize are a raw-9P // case: the cloud9 client refuses to send them; test/adv_core_hostile.py covers it) const max_count = test_cfg.msize - cloud9.iohdrsz; 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(); var ws = stat_dontcare; ws.mtime = 1; const dirs = [_][]const []const u8{ &.{}, &.{"build"}, &.{"comptime"}, &.{ "comptime", "types" }, &.{ "comptime", "types", "Layout" }, &.{"runtime"}, &.{ "runtime", "fn" }, &.{"vars"}, &.{ "vars", "state" }, &.{ "vars", "state", "f" }, &.{ "vars", "state", "f", "inner" } }; for (dirs, 0..) |d, k| { const fid: u32 = @intCast(10 + k); try x.h.walkTo(fid, d); try x.h.expectFail(.{ .create = .{ .fid = fid, .name = "x", .perm = 0o644, .mode = cloud9.owrite } }, e_perm); try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = ws } }, "permission denied"); _ = try x.h.ok(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } }); try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.owrite } }, e_perm); try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.oread | cloud9.otrunc } }, e_perm); try x.h.expectFail(.{ .remove = .{ .fid = fid } }, "permission denied"); try x.h.expectFail(.{ .clunk = .{ .fid = fid } }, e_unknown_fid); } const files = [_][]const []const u8{ &.{"README"}, &.{ "build", "time" }, &.{ "comptime", "decls" }, &.{ "runtime", "pid" }, &.{ "runtime", "fn", "fib30" }, &.{"ctl"}, &.{ "vars", "state", "value" }, &.{ "vars", "state", "raw" }, &.{ "vars", "state", "f", "a", "value" }, &.{ "vars", "counter", "type" } }; for (files, 0..) |f, k| { const fid: u32 = @intCast(30 + k); try x.h.walkTo(fid, f); try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = ws } }, "permission denied"); try x.h.expectFail(.{ .walk = .{ .fid = fid, .newfid = 99, .names = &.{".."} } }, e_not_dir); try x.h.expectFail(.{ .remove = .{ .fid = fid } }, "permission denied"); } // writes to a var value at a non-zero offset and with an empty payload 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; the engine refuses OEXEC try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, e_bad_use); try x.h.walkTo(2, &.{"README"}); try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.oexec } }, e_perm); _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); try testing.expect((try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 10 } })).read.len == 10); } test "a provider read that is not ready parks and is answered on a later step" { var x: Fixture = .{}; try x.init(); defer x.deinit(); try x.h.walkTo(1, &.{ "prov", "hello" }); _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); x.prov.park = true; const reads = x.prov.reads; const tag = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); _ = try x.h.pump(); try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take()); try testing.expect(x.prov.reads > reads); // the connection keeps serving, and every step asks the provider again const asked = x.prov.reads; const st = try x.h.ok(.{ .stat = .{ .fid = 1 } }); try testing.expectEqualStrings("hello", st.stat.name); try testing.expect(x.prov.reads > asked); try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take()); // a second parked read fills the slots; a third is refused at once _ = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); _ = try x.h.pump(); try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take()); try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }, fs.e_again); // ready: the next step answers both, oldest first, with the original tags x.prov.park = false; _ = try x.h.pump(); const first = x.h.client.take() orelse return error.NoReply; try testing.expectEqual(tag, first.tag); try testing.expectEqualStrings("hello", first.result.read); const second = x.h.client.take() orelse return error.NoReply; try testing.expectEqualStrings("hello", second.result.read); try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take()); // a flushed parked read is interrupted; a hangup with one parked pays the provider x.prov.park = true; const parked = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); _ = try x.h.pump(); _ = try x.h.client.submit(.{ .flush = .{ .oldtag = parked } }); _ = try x.h.pump(); const interrupted = x.h.client.take() orelse return error.NoReply; try testing.expectEqual(parked, interrupted.tag); try testing.expectEqualStrings(e_interrupted, interrupted.result.fail); try testing.expect((x.h.client.take() orelse return error.NoReply).result == .flush); _ = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); _ = try x.h.pump(); try testing.expectEqual(@as(u32, 1), x.prov.nodes[1].opens); x.h.conn.hangup(); try testing.expectEqual(@as(u32, 0), x.prov.nodes[1].opens); try testing.expectEqual(@as(u32, 0), x.prov.total_refs); } test "Conn.init clamps the msize cap to [msize_min, cfg.msize]" { 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(fs.msize_min, lo.msize_cap); const hi: TS.Conn = .init(&shared, &storage, std.math.maxInt(u32)); try testing.expectEqual(test_cfg.msize, hi.msize_cap); const mid: TS.Conn = .init(&shared, &storage, 4096); try testing.expectEqual(@as(u32, 4096), mid.msize_cap); // two connections on the same Shared never share a fid-index salt try testing.expect(lo.engine.hash_seed != hi.engine.hash_seed); } test "parseIso8601 rejects malformed stamps and never traps" { 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")); }