diff options
Diffstat (limited to 'src/ninep/tree.zig')
| -rw-r--r-- | src/ninep/tree.zig | 207 |
1 files changed, 132 insertions, 75 deletions
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 56c3112d..46609649 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -4,8 +4,10 @@ //! extended here with the editor's reply payload; fs.zig keeps host access //! and mounts. //! -//! /README /index /ctl /new /log /screen /listeners -//! /pane/<serial>/{name,body,tag,ctl,addr,data,xdata,sel,errors,event,pty/} +//! /README /index /status /look /exec /log /screen /listeners +//! /pane/ mkdir makes a pane, rmdir closes it +//! /pane/<serial>/{name,body,tag,ctl,addr,dot,limit,data,xdata,sel, +//! dirty,mark,scroll,errors,event,look,exec,pty/} //! /os/... the host filesystem /src/... embedded sources (opt-in) const std = @import("std"); const cloud9 = @import("cloud9"); @@ -47,11 +49,16 @@ pub const Payload = union(enum) { pub const Reply = cloud9.fs.ReplyWith(Payload); +/// Tcreate opens a pane and Tremove closes one; nothing else in the tree is +/// created or destroyed by the protocol, and wstat stays a truncation. +pub const features: cloud9.fs.Features = .{ .create = true, .remove = true }; + pub fn changesPane(req: Req) bool { return switch (req.op) { .write, .setattr => true, - .open => req.node == @intFromEnum(TopFile.new), - .lookup, .getattr, .read, .release, .readdir => false, + .open => req.create, + .release => req.remove, + .lookup, .getattr, .read, .readdir => false, }; } @@ -63,8 +70,9 @@ pub const TopFile = enum(u4) { root = 1, README, index, - ctl, - new, + status, + look, + exec, log, screen, listeners, @@ -77,8 +85,8 @@ pub const TopFile = enum(u4) { pub fn mode(f: TopFile) u16 { return switch (f) { .root, .pane => 0o755, - .ctl => 0o666, - .README, .index, .new, .log, .screen, .listeners => 0o444, + .look, .exec => 0o666, + .README, .index, .status, .log, .screen, .listeners => 0o444, }; } @@ -89,18 +97,26 @@ pub const TopFile = enum(u4) { pub const root: u64 = @intFromEnum(TopFile.root); -pub const PaneFile = enum(u4) { +/// Declaration order is readdir order. +pub const PaneFile = enum(u5) { dir = 0, name, body, tag, ctl, addr, + dot, + limit, data, xdata, sel, + dirty, + mark, + scroll, errors, event, + look, + exec, pty, pty_ctl, pty_status, @@ -138,8 +154,8 @@ pub const PaneFile = enum(u4) { }; pub const Node = packed struct(u64) { - file: u4 = 0, - serial: u60 = 0, + file: u5 = 0, + serial: u59 = 0, pub fn of(serial: u32, file: PaneFile) u64 { std.debug.assert(serial != 0); @@ -240,8 +256,13 @@ pub fn stagedReply(p: *Pardes, req: Req) Reply { // ---- dispatch ---- pub fn handle(p: *Pardes, req: Req) Reply { - if (req.node == fs.os_root or req.node & fs.os_node != 0) return fs.osHandle(p, req); - if (req.node & sources.archive_node != 0) return sources.handle(p, req); + const host = req.node == fs.os_root or req.node & fs.os_node != 0; + const archive = req.node & sources.archive_node != 0; + // Only /pane is created in and removed from. The host tree and the + // embedded sources say so, rather than quietly doing nothing. + if ((host or archive) and (req.create or req.remove)) return Reply.fail(req.tag, E.PERM); + if (host) return fs.osHandle(p, req); + if (archive) return sources.handle(p, req); const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT); if (req.op == .write or req.op == .setattr) p.fs.origin = switch (target) { .pane => |t| @as(u8, if (t.file == .body or t.file == .tag) 'E' else 'F'), @@ -280,6 +301,7 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr { .mode = t.file.mode(), .size = pane.fileSize(p, id, t.file), .mtime = pane.mtimeOf(p, pn), + .version = pane.versionOf(pn, t.file), }; }, } @@ -292,13 +314,14 @@ fn attrReply(p: *Pardes, tag: u64, target: Target) Reply { fn topSize(p: *Pardes, f: TopFile) u64 { return switch (f) { - .root, .pane, .new, .log, .screen, .listeners => 0, + // ponytail: /screen has no length until an open renders its frame. + .root, .pane, .screen => 0, .index => pane.indexLen(p), .README => fs.help.len, - .ctl => ctl: { - var buf: [ctl.root_text_cap]u8 = undefined; - break :ctl ctl.rootText(p, &buf).len; - }, + .status => ctl.statusLen(p), + .look, .exec => ctl.resultsLen(p), + .log => events.pending(&p.fs.log), + .listeners => screen.listenersLen(p), }; } @@ -366,7 +389,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { switch (target) { .top => |f| switch (f) { .root => { - inline for (.{ TopFile.README, TopFile.index, TopFile.ctl, TopFile.new, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| { + inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| { if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName()); } if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os"); @@ -403,21 +426,27 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } }; } -fn newPane(p: *Pardes) ?u32 { - const slot = p.freeSlot() orelse return null; +/// Tcreate in /pane opens a pane, the way mkdir opens a directory. The name +/// asked for is ignored: a pane is named by the serial the editor gives it, +/// which the reply carries back and /index lists last. +fn create(p: *Pardes, req: Req, target: Target) Reply { + switch (target) { + .top => |f| if (f != .pane) return Reply.fail(req.tag, E.PERM), + .pane => return Reply.fail(req.tag, E.PERM), + } + if (req.perm & cloud9.dmdir == 0) return Reply.fail(req.tag, E.PERM); + const slot = p.freeSlot() orelse return Reply.fail(req.tag, E.NFILE); p.newScratchBelow(p.active); - const pn = p.panes[slot] orelse return null; - return pn.serial; + const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE); + const attr = attrOf(p, .{ .pane = .{ .serial = made.serial, .file = .dir } }) orelse + return Reply.fail(req.tag, E.NFILE); + return .{ .tag = req.tag, .handle = 1, .attr = attr }; } fn open(p: *Pardes, req: Req, target: Target) Reply { + if (req.create) return create(p, req, target); switch (target) { .top => |f| switch (f) { - // The clone idiom: each open makes one empty pane; reads answer its serial. - .new => { - const serial = newPane(p) orelse return Reply.fail(req.tag, E.NFILE); - return .{ .tag = req.tag, .handle = serial }; - }, .screen => return screen.openSnapshot(p, req, true), .log => p.fs.log_readers +|= 1, else => {}, @@ -429,11 +458,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); switch (t.file) { .body => if (pn.isTerminal()) return screen.openSnapshot(p, req, false), - .ctl => pf.limit = null, - .addr => { - pf.addr = .{}; - pf.limit = null; - }, + .addr => pf.addr = .{}, .event => { pf.readers +|= 1; p.fs.listeners +|= 1; @@ -446,7 +471,27 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { return .{ .tag = req.tag, .handle = 1 }; } +/// Tremove closes a pane. Nothing else in the tree can be removed, and this +/// is also where an ORCLOSE open lands, so every node is checked here. +fn remove(p: *Pardes, req: Req) Reply { + const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT); + const t = switch (target) { + .pane => |t| t, + .top => return Reply.fail(req.tag, E.PERM), + }; + if (t.file != .dir) return Reply.fail(req.tag, E.PERM); + const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); + p.removePane(id) catch return Reply.fail(req.tag, E.IO); + return .{ .tag = req.tag }; +} + fn release(p: *Pardes, req: Req) Reply { + // The handle's bookkeeping runs whether or not the removal is allowed. + const done = releaseHandle(p, req); + return if (req.remove) remove(p, req) else done; +} + +fn releaseHandle(p: *Pardes, req: Req) Reply { if (screen.releaseSnapshot(p, req)) return .{ .tag = req.tag }; const target = Node.target(req.node) orelse return .{ .tag = req.tag }; switch (target) { @@ -502,12 +547,8 @@ fn read(p: *Pardes, req: Req, target: Target) Reply { } break :index stagedReply(p, req); }, - .ctl => ctl.readRoot(p, req), - .new => new: { - if (req.handle == 0) break :new Reply.fail(req.tag, E.INVAL); - p.fs.stage(p.gpa).print(p.gpa, "{d}\n", .{req.handle}) catch break :new Reply.fail(req.tag, E.NOMEM); - break :new stagedReply(p, req); - }, + .status => ctl.readStatus(p, req), + .look, .exec => ctl.readResults(p, req), .log => events.readQueue(p, req, &p.fs.log), .screen => screen.readSnapshot(p, req, null), .listeners => screen.readListeners(p, req), @@ -525,7 +566,7 @@ fn read(p: *Pardes, req: Req, target: Target) Reply { fn write(p: *Pardes, req: Req, target: Target) Reply { switch (target) { .top => |f| return switch (f) { - .ctl => ctl.writeRoot(p, req), + .look, .exec => ctl.command(p, req, null, f == .exec), else => Reply.fail(req.tag, E.PERM), }, .pane => |t| { @@ -587,8 +628,8 @@ test "filesystem inspection preserves pending and displayed Look hover" { const preview = p.look_hover_preview; const requests = [_]Req{ .{ .tag = 1, .op = .lookup, .node = root, .data = "screen" }, - .{ .tag = 1, .op = .lookup, .node = root, .data = "new" }, - .{ .tag = 1, .op = .getattr, .node = @intFromEnum(TopFile.new) }, + .{ .tag = 1, .op = .lookup, .node = root, .data = "status" }, + .{ .tag = 1, .op = .getattr, .node = @intFromEnum(TopFile.pane) }, .{ .tag = 2, .op = .getattr, .node = body }, .{ .tag = 3, .op = .open, .node = body }, .{ .tag = 4, .op = .read, .node = body, .size = 5 }, @@ -614,7 +655,7 @@ test "filesystem inspection preserves pending and displayed Look hover" { test "filesystem pane creation and truncation cancel Look hover" { const requests = [_]Req{ - .{ .tag = 2, .op = .open, .node = @intFromEnum(TopFile.new) }, + .{ .tag = 2, .op = .open, .node = @intFromEnum(TopFile.pane), .data = "x", .create = true, .perm = cloud9.dmdir | 0o755 }, .{ .tag = 3, .op = .setattr, .node = 0, .truncate = true }, }; for (requests) |request| { @@ -644,13 +685,13 @@ test "readdir lists the root and a pane directory without creating anything" { var top_bytes: [4096]u8 = undefined; @memcpy(top_bytes[0..top_reply.bytes.len], top_reply.bytes); const top = th.dirents(top_bytes[0..top_reply.bytes.len], &buf); - try testing.expect(top.len >= 9); - for ([_][]const u8{ "README", "index", "ctl", "new", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| { + try testing.expect(top.len >= 10); + for ([_][]const u8{ "README", "index", "status", "look", "exec", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| { try testing.expectEqualStrings(name, top[i].name); - try testing.expectEqual(i >= 7, top[i].dir); + try testing.expectEqual(i >= 8, top[i].dir); } - try testing.expectEqual(@intFromEnum(TopFile.pane), top[7].node); - try testing.expectEqual(fs.os_root, top[8].node); + try testing.expectEqual(@intFromEnum(TopFile.pane), top[8].node); + try testing.expectEqual(fs.os_root, top[9].node); var idbuf: [16]u8 = undefined; try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null); try testing.expect(th.nameAt(top, "self") == null); @@ -674,13 +715,18 @@ test "readdir lists the root and a pane directory without creating anything" { } const panes_dir = rdir(p, @intFromEnum(TopFile.pane), 0); const listed = th.dirents(panes_dir.bytes, &buf); - try testing.expectEqual(@as(usize, 1), listed.len); - try testing.expectEqual(Node.of(serial, .dir), listed[0].node); + var idname: [16]u8 = undefined; + const own = th.nameAt(listed, try std.fmt.bufPrint(&idname, "{d}", .{serial})).?; + try testing.expectEqual(Node.of(serial, .dir), own.node); + try testing.expect(own.dir); const dir = rdir(p, Node.of(serial, .dir), 0); const files = th.dirents(dir.bytes, &buf); - try testing.expectEqual(@as(usize, 10), files.len); - for ([_][]const u8{ "name", "body", "tag", "ctl", "addr", "data", "xdata", "sel", "errors", "event" }, 0..) |name, i| + try testing.expectEqual(@as(usize, 17), files.len); + for ([_][]const u8{ + "name", "body", "tag", "ctl", "addr", "dot", "limit", "data", "xdata", + "sel", "dirty", "mark", "scroll", "errors", "event", "look", "exec", + }, 0..) |name, i| try testing.expectEqualStrings(name, files[i].name); try testing.expect(th.nameAt(files, ".") == null); try testing.expect(th.nameAt(files, "rdsel") == null); @@ -703,8 +749,10 @@ test "lookup resolves top files, pane serials and pane files" { const serial = serialOf(p); try testing.expectEqual(@as(u64, @intFromEnum(TopFile.index)), look_up(p, root, "index").reply.attr.node); - try testing.expect(!look_up(p, root, "new").reply.attr.dir); - try testing.expectEqual(@as(u16, 0o444), look_up(p, root, "new").reply.attr.mode); + try testing.expect(!look_up(p, root, "status").reply.attr.dir); + try testing.expectEqual(@as(u16, 0o444), look_up(p, root, "status").reply.attr.mode); + try testing.expectEqual(@as(u16, 0o666), look_up(p, root, "look").reply.attr.mode); + try testing.expectEqual(E.NOENT, look_up(p, root, "new").errno()); try testing.expect(look_up(p, root, "pane").reply.attr.dir); try testing.expect(look_up(p, root, "os").reply.attr.dir); try testing.expectEqual(E.NOENT, look_up(p, root, "nosuchthing").errno()); @@ -732,38 +780,46 @@ test "lookup resolves top files, pane serials and pane files" { try testing.expectEqual(E.NOTDIR, look_up(p, Node.of(serial, .body), "x").errno()); } -test "each open of new creates exactly one pane whose serial the read answers" { +test "creating in the pane directory opens a pane and removing one closes it" { const gpa = testing.allocator; const p = try withFile(gpa, "first\n"); defer p.deinit(); const before = p.next_serial; - const new = @intFromEnum(TopFile.new); + const panes_dir = @intFromEnum(TopFile.pane); - try testing.expectEqual(Status.ok, look_up(p, root, "new").reply.status); - try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .getattr, .node = new }).reply.status); + // Browsing /pane creates nothing; only a create does. + try testing.expectEqual(Status.ok, rdir(p, panes_dir, 0).reply.status); try testing.expectEqual(before, p.next_serial); - const opened = call(p, .{ .tag = 1, .op = .open, .node = new }); - try testing.expectEqual(Status.ok, opened.reply.status); + const made = th.mkdir(p, "scratch"); + try testing.expectEqual(Status.ok, made.reply.status); try testing.expectEqual(before + 1, p.next_serial); - try testing.expectEqual(before + 1, opened.reply.handle); - try testing.expectEqual(@as(u64, 0), opened.reply.attr.node); + const serial = before + 1; + try testing.expectEqual(Node.of(serial, .dir), made.reply.attr.node); + try testing.expect(made.reply.attr.dir); var expected: [16]u8 = undefined; - const line = try std.fmt.bufPrint(&expected, "{d}\n", .{opened.reply.handle}); - try testing.expectEqualStrings(line, call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .size = 64 }).bytes); - try testing.expectEqualStrings(line[1..], call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .off = 1, .size = 64 }).bytes); - try testing.expectEqualStrings("", call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .off = line.len, .size = 64 }).bytes); - try testing.expectEqual(before + 1, p.next_serial); - _ = call(p, .{ .tag = 3, .op = .release, .node = new, .handle = opened.reply.handle }); + try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}", .{serial}), made.reply.attr.name); - const id = p.paneBySerial(opened.reply.handle).?; + const id = p.paneBySerial(serial).?; try testing.expectEqualStrings("", p.panes[id].?.file.?.content); - _ = wr(p, Node.of(opened.reply.handle, .body), "hi"); - const second = call(p, .{ .tag = 4, .op = .open, .node = new }); + _ = wr(p, Node.of(serial, .body), "hi"); + const second = th.mkdir(p, "another"); try testing.expectEqual(Status.ok, second.reply.status); - try testing.expect(second.reply.handle != opened.reply.handle); + try testing.expect(second.reply.attr.node != made.reply.attr.node); try testing.expectEqualStrings("hi", p.panes[id].?.file.?.content); - try testing.expectEqual(E.INVAL, call(p, .{ .tag = 5, .op = .read, .node = new, .size = 64 }).errno()); + + // A plain file, and a create anywhere else, are refused. + try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = panes_dir, .data = "f", .create = true, .perm = 0o666 }).errno()); + try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = root, .data = "d", .create = true, .perm = cloud9.dmdir | 0o755 }).errno()); + try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = Node.of(serial, .dir), .data = "d", .create = true, .perm = cloud9.dmdir | 0o755 }).errno()); + + // Tremove closes the pane it names, and nothing else in the tree. + try testing.expectEqual(E.PERM, th.rmdir(p, Node.of(serial, .body)).errno()); + try testing.expectEqual(E.PERM, th.rmdir(p, @intFromEnum(TopFile.index)).errno()); + try testing.expect(p.paneBySerial(serial) != null); + try testing.expectEqual(Status.ok, th.rmdir(p, Node.of(serial, .dir)).reply.status); + try testing.expect(p.paneBySerial(serial) == null); + try testing.expectEqual(E.NOENT, th.rmdir(p, Node.of(serial, .dir)).errno()); } test "directories walk to their parents" { @@ -791,14 +847,15 @@ test "editor paths resolve to the same nodes the wire serves" { try testing.expectEqual(root, resolveSelf(p, "").?); try testing.expectEqual(root, resolveSelf(p, "/./pane/../").?); try testing.expectEqual(@intFromEnum(TopFile.index), resolveSelf(p, "index").?); - try testing.expectEqual(@intFromEnum(TopFile.new), resolveSelf(p, "new").?); + try testing.expectEqual(@intFromEnum(TopFile.status), resolveSelf(p, "status").?); + try testing.expect(resolveSelf(p, "new") == null); try testing.expectEqual(@intFromEnum(TopFile.pane), resolveSelf(p, "pane").?); try testing.expectEqual(Node.of(serial, .dir), resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}", .{serial})).?); try testing.expectEqual(Node.of(serial, .sel), resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/sel", .{serial})).?); try testing.expect(resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/pty/ctl", .{serial})) == null); try testing.expect(resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/rdsel", .{serial})) == null); try testing.expect(resolveSelf(p, "self/index") == null); - try testing.expect(resolveSelf(p, "new/ctl") == null); + try testing.expect(resolveSelf(p, "status/ctl") == null); try testing.expect(resolveSelf(p, "cons") == null); try testing.expect(resolveSelf(p, "pane/0") == null); } |
