//! 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 } }); }