summaryrefslogtreecommitdiff
path: root/introspect/src/scratch.zig
diff options
context:
space:
mode:
Diffstat (limited to 'introspect/src/scratch.zig')
-rw-r--r--introspect/src/scratch.zig689
1 files changed, 0 insertions, 689 deletions
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 `/<name>`.
- 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 } });
-}