From ba996acfcad1698adbf4a1834fe50e73b1c6cab9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sat, 19 Sep 2026 23:28:22 -0300 Subject: Rename programs: 9player -> 9ns, introspect -> 9proc, app -> web (9web) Directories, binaries, build options (-D9ns, -D9proc), step names, module name (9proc), thread and fs names, env var NINEPLAYER_MOUNT -> NINE_MOUNT, docs and test scripts. Browser assets move to web/static. Co-Authored-By: Claude Fable 5.1 --- introspect/src/scratch.zig | 689 --------------------------------------------- 1 file changed, 689 deletions(-) delete mode 100644 introspect/src/scratch.zig (limited to 'introspect/src/scratch.zig') diff --git a/introspect/src/scratch.zig b/introspect/src/scratch.zig deleted file mode 100644 index 2163a28..0000000 --- a/introspect/src/scratch.zig +++ /dev/null @@ -1,689 +0,0 @@ -//! An in-memory read/write tree as a `Provider`: create, write, truncate, -//! rename, remove, mkdir, DMAPPEND, DMEXCL. The one core-level component that -//! takes an `Allocator` (nodes and file contents live on it); it is optional. -//! -//! Nodes are kept alive by `refs` (fids holding a handle) after removal, so a -//! handle stays valid until the core clunks it. Handles are node addresses; -//! the root is handle 0. Not internally synchronized (like `Shared`). -const std = @import("std"); -const cloud9 = @import("cloud9"); -const core = @import("core.zig"); -const Allocator = std.mem.Allocator; -const Provider = core.Provider; -const Handle = Provider.Handle; -const Error = Provider.Error; -const NodeStat = core.NodeStat; - -/// A node of the tree. -pub const Node = struct { - name: []u8, - path: u64, - version: u32 = 0, - mode: u32, - atime: u32, - mtime: u32, - data: std.ArrayList(u8) = .empty, - children: std.ArrayList(*Node) = .empty, - parent: ?*Node, - /// Handles held by the core. - refs: u32 = 0, - /// Fids currently open on this node (DMEXCL admits at most one). - opens: u32 = 0, - removed: bool = false, - - pub fn isDir(n: *const Node) bool { - return n.mode & cloud9.dmdir != 0; - } - - fn find(n: *const Node, name: []const u8) ?*Node { - for (n.children.items) |ch| if (std.mem.eql(u8, ch.name, name)) return ch; - return null; - } -}; - -/// Seconds since the epoch, for atime/mtime; the default clock reports 0. -pub const Clock = *const fn () u32; - -fn zeroClock() u32 { - return 0; -} - -pub const Scratch = struct { - gpa: Allocator, - root: *Node, - /// Qid paths are a counter, never reused: the root is 0 (the provider - /// root handle), so a removed-and-recreated file gets a fresh identity - /// even when the allocator hands back the same address. - next_path: u64 = 0, - /// Sum of all file lengths, bounded by `budget`. - bytes: usize = 0, - /// Largest total of file contents across all files. - budget: usize, - /// Largest single file; defaults to the budget. - max_file: usize, - /// The time source for atime/mtime (a platform layer sets it). - now: Clock = &zeroClock, - - /// The tree's only allocation policy: every node and every file's - /// contents come from `gpa`, and no file content ever exceeds `budget_bytes` - /// in total. - pub fn init(gpa: Allocator, budget_bytes: usize) Allocator.Error!Scratch { - var s: Scratch = .{ .gpa = gpa, .root = undefined, .budget = budget_bytes, .max_file = budget_bytes }; - s.root = try s.newNode("", cloud9.dmdir | 0o777, null); - return s; - } - - pub fn deinit(s: *Scratch) void { - s.destroyTree(s.root); - s.* = undefined; - } - - /// The provider to mount, at `/`. - pub fn provider(s: *Scratch, name: []const u8) Provider { - return .{ .name = name, .ctx = s, .vtable = &vtable }; - } - - pub 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, - }; - - // -- node management -- - - fn destroyTree(s: *Scratch, n: *Node) void { - for (n.children.items) |ch| s.destroyTree(ch); - n.children.clearRetainingCapacity(); - n.removed = true; - if (n.refs == 0 or n == s.root) s.destroyNode(n); - } - - fn destroyNode(s: *Scratch, n: *Node) void { - s.bytes -= n.data.items.len; - s.gpa.free(n.name); - n.data.deinit(s.gpa); - n.children.deinit(s.gpa); - s.gpa.destroy(n); - } - - fn newNode(s: *Scratch, name: []const u8, mode: u32, parent: ?*Node) Allocator.Error!*Node { - const n = try s.gpa.create(Node); - errdefer s.gpa.destroy(n); - const t = s.now(); - n.* = .{ - .name = try s.gpa.dupe(u8, name), - .path = s.next_path, - .mode = mode, - .atime = t, - .mtime = t, - .parent = parent, - }; - errdefer s.gpa.free(n.name); - if (parent) |p| try p.children.append(s.gpa, n); - s.next_path += 1; - return n; - } - - /// Sets a file's length, zero-filling growth and charging the budget. - /// Shrinking releases the memory so a truncated file costs nothing. - fn resizeData(s: *Scratch, n: *Node, new_len: usize) Error!void { - const old = n.data.items.len; - if (new_len > old) { - if (new_len > s.max_file) return error.NoSpace; - if (s.bytes + (new_len - old) > s.budget) return error.NoSpace; - n.data.resize(s.gpa, new_len) catch return error.NoSpace; - @memset(n.data.items[old..new_len], 0); - s.bytes += new_len - old; - } else if (new_len < old) { - n.data.shrinkAndFree(s.gpa, new_len); - s.bytes -= old - new_len; - } - } - - fn touch(s: *Scratch, n: *Node) void { - n.version +%= 1; - n.mtime = s.now(); - } - - fn self(ctx: *anyopaque) *Scratch { - return @ptrCast(@alignCast(ctx)); - } - - fn handle(s: *Scratch, n: *Node) Handle { - return if (n == s.root) Provider.root else @intFromPtr(n); - } - - fn node(s: *Scratch, h: Handle) *Node { - return if (h == Provider.root) s.root else @ptrFromInt(@as(usize, @intCast(h))); - } - - /// A handle the core will clunk exactly once. - fn retain(s: *Scratch, n: *Node) Handle { - if (n != s.root) n.refs += 1; - return s.handle(n); - } - - fn release(s: *Scratch, n: *Node) void { - if (n == s.root) return; - n.refs -= 1; - if (n.refs == 0 and n.removed) s.destroyNode(n); - } - - fn fillStat(n: *const Node, h: Handle, out: *NodeStat) void { - out.* = .{ - .mode = n.mode, - .length = if (n.isDir()) 0 else n.data.items.len, - .atime = n.atime, - .mtime = n.mtime, - .version = n.version, - .name = n.name, - .handle = h, - .path = n.path, - }; - } - - // -- the vtable -- - - fn walk(ctx: *anyopaque, parent: Handle, name: []const u8) Error!Handle { - const s = self(ctx); - const p = s.node(parent); - if (std.mem.eql(u8, name, ".")) return s.retain(p); - if (p.removed) return error.NotFound; - if (!p.isDir()) return error.NotDir; - if (std.mem.eql(u8, name, "..")) return s.retain(p.parent orelse s.root); - return s.retain(p.find(name) orelse return error.NotFound); - } - - fn stat(ctx: *anyopaque, h: Handle, out: *NodeStat) Error!void { - const s = self(ctx); - fillStat(s.node(h), h, out); - } - - fn list(ctx: *anyopaque, dir: Handle, index: usize, out: *NodeStat) Error!bool { - const s = self(ctx); - const d = s.node(dir); - if (!d.isDir()) return error.NotDir; - if (index >= d.children.items.len) return false; - const ch = d.children.items[index]; - fillStat(ch, s.handle(ch), out); - return true; - } - - fn open(ctx: *anyopaque, h: Handle, mode: u8) Error!void { - const s = self(ctx); - const n = s.node(h); - if (n.removed) return error.NotFound; - const acc = mode & 3; - const want_write = acc == cloud9.owrite or acc == cloud9.ordwr; - const want_read = !want_write or acc == cloud9.ordwr; - const trunc = mode & cloud9.otrunc != 0; - if (n.isDir()) { - if (want_write or trunc) return error.IsDir; - if (n.mode & 0o400 == 0) return error.Perm; - } else { - if (want_read and n.mode & 0o400 == 0) return error.Perm; - if ((want_write or trunc) and n.mode & 0o200 == 0) return error.Perm; - if (n.mode & cloud9.dmexcl != 0 and n.opens != 0) return error.Excl; - if (trunc and n.mode & cloud9.dmappend == 0) { - s.resizeData(n, 0) catch unreachable; // shrinking cannot fail - s.touch(n); - } - } - n.opens += 1; - } - - fn close(ctx: *anyopaque, h: Handle) void { - const s = self(ctx); - s.node(h).opens -= 1; - } - - fn read(ctx: *anyopaque, h: Handle, offset: u64, buf: []u8) Error!usize { - const s = self(ctx); - const n = s.node(h); - if (n.isDir()) return error.IsDir; - const src = n.data.items; - if (offset >= src.len) return 0; - const off: usize = @intCast(offset); - const len = @min(buf.len, src.len - off); - @memcpy(buf[0..len], src[off..][0..len]); - return len; - } - - fn write(ctx: *anyopaque, h: Handle, offset: u64, data: []const u8) Error!usize { - const s = self(ctx); - const n = s.node(h); - if (n.isDir()) return error.IsDir; - // A zero-length write changes nothing (and must not extend the file). - if (data.len == 0) return 0; - const off: usize = if (n.mode & cloud9.dmappend != 0) n.data.items.len else @intCast(@min(offset, s.max_file)); - const end = off + data.len; - if (end > s.max_file) return error.NoSpace; - if (end > n.data.items.len) try s.resizeData(n, end); - @memcpy(n.data.items[off..end], data); - s.touch(n); - return data.len; - } - - fn create(ctx: *anyopaque, dir: Handle, name: []const u8, perm: u32, mode: u8) Error!Handle { - const s = self(ctx); - const d = s.node(dir); - if (d.removed) return error.NotFound; - if (!d.isDir()) return error.NotDir; - if (d.mode & 0o200 == 0) return error.Perm; - if (d.find(name) != null) return error.Exists; - const is_dir = perm & cloud9.dmdir != 0; - const inherit: u32 = if (is_dir) d.mode & 0o777 else d.mode & 0o666; - const n = s.newNode(name, perm & (~@as(u32, 0o777) | inherit), d) catch return error.NoSpace; - s.touch(d); - n.opens += 1; - _ = mode; - return s.retain(n); - } - - fn remove(ctx: *anyopaque, h: Handle) Error!void { - const s = self(ctx); - const n = s.node(h); - if (n.removed) return error.NotFound; - const parent = n.parent orelse return error.Perm; - if (parent.mode & 0o200 == 0) return error.Perm; - if (n.isDir() and n.children.items.len != 0) return error.NotEmpty; - const i = std.mem.indexOfScalar(*Node, parent.children.items, n) orelse return error.NotFound; - _ = parent.children.orderedRemove(i); - s.touch(parent); - n.removed = true; - if (n.refs == 0) s.destroyNode(n); - } - - fn wstat(ctx: *anyopaque, h: Handle, st: *const cloud9.Stat) Error!void { - const s = self(ctx); - const n = s.node(h); - if (n.removed) return error.NotFound; - // Validate everything before changing anything. - const rename = st.name.len != 0 and !std.mem.eql(u8, st.name, n.name); - if (rename) { - const parent = n.parent orelse return error.Perm; - if (parent.find(st.name) != null) return error.Exists; - } - const cur_len: u64 = if (n.isDir()) 0 else n.data.items.len; - const set_len = st.length != 0xFFFF_FFFF_FFFF_FFFF and st.length != cur_len; - if (set_len) { - if (n.isDir()) return error.IsDir; - if (st.length > s.max_file) return error.NoSpace; - } - const set_mode = st.mode != 0xFFFF_FFFF and st.mode != n.mode; - if (set_mode and (st.mode & cloud9.dmdir) != (n.mode & cloud9.dmdir)) return error.Perm; - const set_mtime = st.mtime != 0xFFFF_FFFF and st.mtime != n.mtime; - if (!(rename or set_len or set_mode or set_mtime)) return; - const new_name: ?[]u8 = if (rename) s.gpa.dupe(u8, st.name) catch return error.NoSpace else null; - errdefer if (new_name) |nn| s.gpa.free(nn); - if (set_len) try s.resizeData(n, @intCast(st.length)); - // Nothing below can fail. - if (new_name) |nn| { - s.gpa.free(n.name); - n.name = nn; - s.touch(n.parent.?); - } - if (set_mode) n.mode = (n.mode & cloud9.dmdir) | (st.mode & ~cloud9.dmdir); - s.touch(n); - if (set_mtime) n.mtime = st.mtime; - } - - fn clunk(ctx: *anyopaque, h: Handle) void { - const s = self(ctx); - s.release(s.node(h)); - } -}; - -// --------------------------------------------------------------------------- -// Tests: the scratch tree mounted at /scratch of a core server. -// --------------------------------------------------------------------------- - -const testing = std.testing; - -const test_cfg: core.Config = .{ .name = "tester", .msize = 8192, .max_fids = 32 }; -const TS = core.Server(test_cfg); - -const budget: usize = 1 << 20; - -const Fixture = struct { - ctx: u8 = 0, - shared: TS.Shared = undefined, - storage: TS.Storage = undefined, - scratch: Scratch = undefined, - h: TS.Harness = undefined, - - fn init(x: *Fixture) !void { - x.shared = .init(&x.ctx); - x.scratch = try Scratch.init(testing.allocator, budget); - errdefer x.scratch.deinit(); - try x.shared.addProvider(x.scratch.provider("scratch")); - try x.h.init(&x.shared, &x.storage); - } - - fn deinit(x: *Fixture) void { - x.h.deinit(); - x.scratch.deinit(); - } - - fn nodeOf(x: *Fixture, fid: u32) *Node { - for (x.h.conn.fids) |f| if (f.used and f.id == fid) return x.scratch.node(f.node.prov.h); - unreachable; - } -}; - -const dontcare = core.stat_dontcare; - -test "scratch create/write/read/rename/truncate/remove" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - // create + write - const cr = try x.h.ok(.{ .create = .{ .fid = 1, .name = "x", .perm = 0o644, .mode = cloud9.ordwr } }); - try testing.expectEqual(cloud9.qtfile, cr.create.qid.type); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "hello" } }); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 5, .data = " world" } }); - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("hello world", r.read); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - // rename x -> y - try x.h.walkTo(2, &.{ "scratch", "x" }); - var st = dontcare; - st.name = "y"; - _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = st } }); - try x.h.walkTo(3, &.{"scratch"}); - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 30, .names = &.{"x"} } }, "file does not exist"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); - try x.h.walkTo(3, &.{ "scratch", "y" }); - // truncate then extend with zero fill - st = dontcare; - st.length = 2; - _ = try x.h.ok(.{ .wstat = .{ .fid = 3, .stat = st } }); - st.length = 4; - _ = try x.h.ok(.{ .wstat = .{ .fid = 3, .stat = st } }); - const text = try x.h.readAll(3); - defer testing.allocator.free(text); - try testing.expectEqualStrings("he\x00\x00", text); - const s3 = try x.h.ok(.{ .stat = .{ .fid = 3 } }); - try testing.expectEqualStrings("y", s3.stat.name); - try testing.expectEqual(@as(u64, 4), s3.stat.length); - try testing.expectEqualStrings("tester", s3.stat.uid); - _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - // mkdir, nested create, remove rules - try x.h.walkTo(4, &.{"scratch"}); - const dr = try x.h.ok(.{ .create = .{ .fid = 4, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.oread } }); - try testing.expectEqual(cloud9.qtdir, dr.create.qid.type); - _ = try x.h.ok(.{ .clunk = .{ .fid = 4 } }); - try x.h.walkTo(5, &.{ "scratch", "d" }); - _ = try x.h.ok(.{ .create = .{ .fid = 5, .name = "inner", .perm = 0o600, .mode = cloud9.owrite } }); - _ = try x.h.ok(.{ .write = .{ .fid = 5, .offset = 0, .data = "z" } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 5 } }); - try x.h.walkTo(6, &.{ "scratch", "d" }); - try x.h.expectFail(.{ .remove = .{ .fid = 6 } }, "directory not empty"); - try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, "unknown fid"); // remove always clunks - try x.h.walkTo(7, &.{ "scratch", "d", "inner" }); - _ = try x.h.ok(.{ .remove = .{ .fid = 7 } }); - try x.h.walkTo(8, &.{ "scratch", "d" }); - _ = try x.h.ok(.{ .remove = .{ .fid = 8 } }); - try x.h.walkTo(9, &.{ "scratch", "y" }); - _ = try x.h.ok(.{ .remove = .{ .fid = 9 } }); - try x.h.walkTo(10, &.{"scratch"}); - _ = try x.h.ok(.{ .open = .{ .fid = 10, .mode = cloud9.oread } }); - const names = try x.h.listDir(10, 1024); - defer testing.allocator.free(names); - try testing.expectEqual(@as(usize, 0), names.len); - // append-only files ignore the offset - try x.h.walkTo(11, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 11, .name = "log", .perm = cloud9.dmappend | 0o644, .mode = cloud9.ordwr } }); - _ = try x.h.ok(.{ .write = .{ .fid = 11, .offset = 100, .data = "a" } }); - _ = try x.h.ok(.{ .write = .{ .fid = 11, .offset = 0, .data = "b" } }); - const lr = try x.h.ok(.{ .read = .{ .fid = 11, .offset = 0, .count = 10 } }); - try testing.expectEqualStrings("ab", lr.read); - try testing.expect(lr.read.len == 2); - const ls = try x.h.ok(.{ .stat = .{ .fid = 11 } }); - try testing.expect(ls.stat.qid.type & cloud9.qtappend != 0); - // the scratch root cannot be removed - try x.h.walkTo(12, &.{"scratch"}); - try x.h.expectFail(.{ .remove = .{ .fid = 12 } }, "permission denied"); -} - -test "walk of a missing name and walking a file" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{"nope"} } }, "file does not exist"); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "f", .perm = 0o644, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try x.h.walkTo(3, &.{ "scratch", "f" }); - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"x"} } }, "not a directory"); - // a walk that fails past the first element is a partial Rwalk that leaves newfid unused - const r = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 4, .names = &.{ "scratch", "nope", "x" } } }); - try testing.expectEqual(@as(u16, 1), r.walk.nwqid); - try x.h.expectFail(.{ .clunk = .{ .fid = 4 } }, "unknown fid"); - // .. from a file is not a directory; .. from the scratch root reaches the server root - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 5, .names = &.{".."} } }, "not a directory"); - try x.h.walkTo(5, &.{"scratch"}); - const up = try x.h.ok(.{ .walk = .{ .fid = 5, .newfid = 6, .names = &.{ "..", "scratch", "..", "README" } } }); - try testing.expectEqual(@as(u16, 4), up.walk.nwqid); - try testing.expectEqual(@as(u64, 0), up.walk.wqid[1].path); // provider 0, root - try testing.expect(up.walk.wqid[0].type & cloud9.qtdir != 0); -} - -test "directory read across consecutive offsets returns every record exactly once" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - const n = 40; - for (0..n) |i| { - try x.h.walkTo(1, &.{"scratch"}); - var name_buf: [64]u8 = undefined; - const name = try std.fmt.bufPrint(&name_buf, "file-with-a-long-name-{d:0>3}", .{i}); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = name, .perm = 0o644, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - } - try x.h.walkTo(2, &.{"scratch"}); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - // 200 bytes fits two records, so this takes many reads. - const names = try x.h.listDir(2, 200); - defer TS.Harness.freeNames(names); - try testing.expectEqual(@as(usize, n), names.len); - var seen: [n]bool = @splat(false); - for (names) |nm| { - const idx = try std.fmt.parseInt(usize, nm[nm.len - 3 ..], 10); - try testing.expect(!seen[idx]); - seen[idx] = true; - } - for (seen) |s| try testing.expect(s); - try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 7, .count = 200 } }, "bad offset"); - // a read that cannot fit even one record returns nothing rather than splitting it - const tiny = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 30 } }); - try testing.expectEqual(@as(usize, 0), tiny.read.len); - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - // Tversion resets every fid and every reference - try x.h.version(8192); - try testing.expectEqual(@as(usize, 0), x.h.conn.fidCount()); - for (x.scratch.root.children.items) |ch| try testing.expectEqual(@as(u32, 0), ch.refs); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } }); -} - -test "DMEXCL admits one open fid at a time" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - const cr = try x.h.ok(.{ .create = .{ .fid = 1, .name = "lock", .perm = cloud9.dmexcl | 0o644, .mode = cloud9.owrite } }); - try testing.expect(cr.create.qid.type & cloud9.qtexcl != 0); - try x.h.walkTo(2, &.{ "scratch", "lock" }); - try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }, "exclusive use file already open"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - try x.h.walkTo(3, &.{ "scratch", "lock" }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "exclusive use file already open"); - // a Tversion reset drops the open and frees the file for the next session - try x.h.version(8192); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } }); - try x.h.walkTo(4, &.{ "scratch", "lock" }); - _ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } }); - try testing.expectEqual(@as(u32, 1), x.nodeOf(4).opens); - _ = try x.h.ok(.{ .remove = .{ .fid = 4 } }); -} - -test "scratch memory: zero-length writes, truncation frees, global budget" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - x.scratch.max_file = 4096; - try x.h.walkTo(1, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "f", .perm = 0o644, .mode = cloud9.ordwr } }); - // a zero-length write at a huge offset must not extend the file - const w0 = try x.h.ok(.{ .write = .{ .fid = 1, .offset = std.math.maxInt(u64), .data = "" } }); - try testing.expectEqual(@as(u32, 0), w0.write); - var st = try x.h.ok(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqual(@as(u64, 0), st.stat.length); - // growth is charged to the budget; truncation releases it (memory too) - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 1000, .data = "x" } }); - try testing.expectEqual(@as(usize, 1001), x.scratch.bytes); - var ws = dontcare; - ws.length = 10; - _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = ws } }); - try testing.expectEqual(@as(usize, 10), x.scratch.bytes); - try testing.expectEqual(@as(usize, 10), x.nodeOf(1).data.capacity); - // per-file cap and the global budget both answer "no space" - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 4096, .data = "x" } }, "no space left on device"); - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = std.math.maxInt(u64), .data = "x" } }, "no space left on device"); - x.scratch.bytes = budget - 10; // pretend other files hold the rest - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 10, .data = "0123456789A" } }, "no space left on device"); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 10, .data = "0123456789" } }); - try testing.expectEqual(budget, x.scratch.bytes); - ws.length = 4096; - try x.h.expectFail(.{ .wstat = .{ .fid = 1, .stat = ws } }, "no space left on device"); - x.scratch.bytes -= budget - 20; - // OTRUNC releases too - try x.h.walkTo(2, &.{ "scratch", "f" }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.owrite | cloud9.otrunc } }); - try testing.expectEqual(@as(usize, 0), x.scratch.bytes); - st = try x.h.ok(.{ .stat = .{ .fid = 2 } }); - try testing.expectEqual(@as(u64, 0), st.stat.length); - // removing a file with content returns its bytes once the last fid lets go - _ = try x.h.ok(.{ .write = .{ .fid = 2, .offset = 0, .data = "abc" } }); - try testing.expectEqual(@as(usize, 3), x.scratch.bytes); - _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); - try testing.expectEqual(@as(usize, 3), x.scratch.bytes); // fid 1 still holds it - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }); - try testing.expectEqualStrings("abc", r.read); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try testing.expectEqual(@as(usize, 0), x.scratch.bytes); -} - -test "wstat with every field equal to the current stat changes nothing" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "same", .perm = 0o640, .mode = cloud9.oread } }); - const before = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat; - var copy = before; - var name_buf: [core.max_name]u8 = undefined; - @memcpy(name_buf[0..before.name.len], before.name); - copy.name = name_buf[0..before.name.len]; - copy.uid = "tester"; - copy.gid = "tester"; - copy.muid = "tester"; - _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = copy } }); - const after = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat; - try testing.expectEqual(before.qid, after.qid); - try testing.expectEqual(before.mtime, after.mtime); - try testing.expectEqual(before.mode, after.mode); - try testing.expectEqualStrings("same", after.name); - // and a rename to the very same name is also a no-op - var st = dontcare; - st.name = "same"; - _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = st } }); - try testing.expectEqual(before.qid, (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.qid); - // renaming onto an existing sibling is refused - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try x.h.walkTo(2, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 2, .name = "other", .perm = 0o640, .mode = cloud9.oread } }); - st.name = "same"; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = st } }, "file already exists"); - // the mode's directory bit is immutable, mtime is settable - st = dontcare; - st.mode = cloud9.dmdir | 0o640; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = st } }, "permission denied"); - st = dontcare; - st.mtime = 12345; - _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = st } }); - try testing.expectEqual(@as(u32, 12345), (try x.h.ok(.{ .stat = .{ .fid = 2 } })).stat.mtime); - _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); -} - -test "ORCLOSE removes on clunk and removed files stay readable through open fids" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "tmp", .perm = 0o644, .mode = cloud9.ordwr | cloud9.orclose } }); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "gone" } }); - try x.h.walkTo(2, &.{ "scratch", "tmp" }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try x.h.walkTo(3, &.{"scratch"}); - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"tmp"} } }, "file does not exist"); - const r = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 10 } }); - try testing.expectEqualStrings("gone", r.read); - try testing.expectEqual(@as(usize, 4), x.scratch.bytes); - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - try testing.expectEqual(@as(usize, 0), x.scratch.bytes); - // create inside a removed directory fails - _ = try x.h.ok(.{ .create = .{ .fid = 3, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.oread } }); - try x.h.walkTo(5, &.{ "scratch", "d" }); - _ = try x.h.ok(.{ .remove = .{ .fid = 5 } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); - try x.h.walkTo(6, &.{"scratch"}); - try x.h.expectFail(.{ .walk = .{ .fid = 6, .newfid = 7, .names = &.{"d"} } }, "file does not exist"); - try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "x", .perm = 0o644, .mode = cloud9.oread } }, "unknown fid"); // remove clunked it -} - -test "qid paths are stable identities, not addresses: remove + recreate differ" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - const a = try x.h.ok(.{ .create = .{ .fid = 1, .name = "f", .perm = 0o644, .mode = cloud9.oread } }); - const path_a = a.create.qid.path; - try testing.expectEqual(x.nodeOf(1).path, path_a & ((1 << 56) - 1)); - try testing.expect(path_a != 0); - // a rename keeps the identity - var st = dontcare; - st.name = "g"; - _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = st } }); - try testing.expectEqual(path_a, (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.qid.path); - _ = try x.h.ok(.{ .remove = .{ .fid = 1 } }); - // the allocator very likely reuses the freed node's address here - try x.h.walkTo(2, &.{"scratch"}); - const b = try x.h.ok(.{ .create = .{ .fid = 2, .name = "f", .perm = 0o644, .mode = cloud9.oread } }); - try testing.expect(b.create.qid.path != path_a); - _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); - // the scratch root keeps path 0 (its handle), like every provider root - try x.h.walkTo(3, &.{"scratch"}); - try testing.expectEqual(@as(u64, 0), (try x.h.ok(.{ .stat = .{ .fid = 3 } })).stat.qid.path); - // directory listing reports the same identities as walking - _ = try x.h.ok(.{ .create = .{ .fid = 3, .name = "listed", .perm = 0o644, .mode = cloud9.oread } }); - const via_create = (try x.h.ok(.{ .stat = .{ .fid = 3 } })).stat.qid; - try x.h.walkTo(4, &.{"scratch"}); - _ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } }); - const r = try x.h.ok(.{ .read = .{ .fid = 4, .offset = 0, .count = 1024 } }); - const listed = try cloud9.Stat.decode(r.read[0 .. std.mem.readInt(u16, r.read[0..2], .little) + 2]); - try testing.expectEqual(via_create, listed.qid); - _ = try x.h.ok(.{ .remove = .{ .fid = 3 } }); -} -- cgit v1.3