diff options
Diffstat (limited to 'src/ninep/tree.zig')
| -rw-r--r-- | src/ninep/tree.zig | 197 |
1 files changed, 171 insertions, 26 deletions
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index ad95f6bf..baf8c772 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -18,6 +18,8 @@ pub const events = @import("events.zig"); pub const pane = @import("pane.zig"); pub const addr = @import("addr.zig"); pub const ctl = @import("ctl.zig"); +pub const cols = @import("cols.zig"); +const layout = pardes.layout; pub const pty = @import("pty.zig"); pub const sources = @import("sources.zig"); pub const screen = @import("screen.zig"); @@ -68,9 +70,11 @@ pub const e_shut_down = "window shut down"; /// Tremove closes a pane; nothing else in the tree is created or destroyed by /// the protocol, and wstat stays a truncation. Making a pane is an open of -/// /pane/new, which needs no feature of the engine's, so Tcreate is refused -/// everywhere, as it is in acme (editors/acme/fsys.c, fsyscreate). -pub const features: cloud9.fs.Features = .{ .remove = true }; +/// /pane/new, so Tcreate is refused everywhere, as it is in acme +/// (editors/acme/fsys.c, fsyscreate) -- here rather than by the engine, so +/// a create in a pane that has closed says the pane is gone (a shell's `>` +/// to a closed pane's file creates when its lookup fails). +pub const features: cloud9.fs.Features = .{ .remove = true, .create = true }; /// Whether a request must wait until no step is out in a syscall /// (`pardes.turn`): what would change a pane such a step still points into. @@ -169,7 +173,7 @@ pub fn openOf(p: *Pardes, req: Req) ?*Open { // ---- nodes ---- -pub const TopFile = enum(u4) { +pub const TopFile = enum(u5) { root = 1, README, index, @@ -184,6 +188,9 @@ pub const TopFile = enum(u4) { focus, ctl, commands, + layout, + tag, + col, pub fn fileName(f: TopFile) []const u8 { return if (f == .root) "/" else @tagName(f); @@ -191,14 +198,14 @@ pub const TopFile = enum(u4) { pub fn mode(f: TopFile) u16 { return switch (f) { - .root, .pane => 0o755, - .look, .exec, .log, .focus, .ctl => 0o666, - .README, .index, .status, .screen, .listeners, .new, .commands => 0o444, + .root, .pane, .col => 0o755, + .look, .exec, .log, .focus, .ctl, .tag => 0o666, + .README, .index, .status, .screen, .listeners, .new, .commands, .layout => 0o444, }; } pub fn dir(f: TopFile) bool { - return f == .root or f == .pane; + return f == .root or f == .pane or f == .col; } }; @@ -262,6 +269,21 @@ pub const PaneFile = enum(u5) { } }; +/// /col/<serial> and its tag: a column is named by its serial, which stays +/// while it lives as a pane's does; this bit, past a pane serial's 32, keeps +/// the two apart. +pub const ColFile = enum(u5) { + dir = 0, + tag = 1, + ctl = 2, + exec = 3, + + pub fn fileName(f: ColFile) []const u8 { + return @tagName(f); + } +}; +const col_bit: u59 = 1 << 58; + pub const Node = packed struct(u64) { file: u5 = 0, serial: u59 = 0, @@ -271,11 +293,19 @@ pub const Node = packed struct(u64) { return @bitCast(Node{ .file = @intFromEnum(file), .serial = serial }); } + pub fn ofCol(serial: u32, file: ColFile) u64 { + return @bitCast(Node{ .file = @intFromEnum(file), .serial = col_bit | @as(u59, serial) }); + } + pub fn target(node: u64) ?Target { const n: Node = @bitCast(node); if (n.serial == 0) { return .{ .top = std.enums.fromInt(TopFile, n.file) orelse return null }; } + if (n.serial & col_bit != 0) return .{ .col = .{ + .serial = std.math.cast(u32, n.serial & ~col_bit) orelse return null, + .file = std.enums.fromInt(ColFile, n.file) orelse return null, + } }; return .{ .pane = .{ .serial = std.math.cast(u32, n.serial) orelse return null, .file = std.enums.fromInt(PaneFile, n.file) orelse return null, @@ -286,6 +316,7 @@ pub const Node = packed struct(u64) { pub const Target = union(enum) { top: TopFile, pane: struct { serial: u32, file: PaneFile }, + col: struct { serial: u32, file: ColFile }, }; /// Opening /pane/new makes a pane and reading the open fid answers its @@ -371,6 +402,11 @@ pub fn stagedReply(p: *Pardes, req: Req) Reply { // ---- dispatch ---- pub fn handle(p: *Pardes, req: Req) Reply { + if (req.op == .open and req.create) { + if (Node.target(req.node)) |t| if (t == .pane and p.paneBySerial(t.pane.serial) == null) + return Reply.fail(req.tag, E.NOENT); + return Reply.fail(req.tag, E.PERM); + } const host = req.node == fs.os_root or req.node & fs.os_node != 0; const archive = req.node & sources.archive_node != 0; // Only a pane directory is removed. The host tree and the embedded @@ -381,7 +417,7 @@ pub fn handle(p: *Pardes, req: Req) Reply { 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'), - .top => 'F', + .top, .col => 'F', }; return switch (req.op) { .lookup => lookup(p, req, target), @@ -403,7 +439,7 @@ pub fn handle(p: *Pardes, req: Req) Reply { fn runAttr(p: *Pardes, req: Req, target: Target) ?Reply.Attr { const t = switch (target) { .pane => |t| t, - .top => return null, + .top, .col => return null, }; if (t.file != .pty_run) return null; const run = if (req.handle != 0) openOf(p, req) else for (&p.fs.opens) |*o| { @@ -422,6 +458,23 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr { .mode = f.mode(), .size = topSize(p, f), .mtime = p.fs.started, + .append = f == .tag, + }, + .col => |c| { + if (layout.columnBySerial(p, c.serial) == null) return null; + return .{ + .name = if (c.file == .dir) (std.fmt.bufPrint(&p.fs.node_name, "{d}", .{c.serial}) catch unreachable) else c.file.fileName(), + .node = Node.ofCol(c.serial, c.file), + .dir = c.file == .dir, + .mode = switch (c.file) { + .dir => 0o755, + .ctl, .exec => 0o222, + .tag => 0o666, + }, + .size = if (c.file == .tag) cols.headerText(p, c.serial).?.len + 1 else 0, + .mtime = p.fs.started, + .append = c.file == .tag, + }; }, .pane => |t| { const id = p.paneBySerial(t.serial) orelse return null; @@ -454,7 +507,9 @@ fn topSize(p: *Pardes, f: TopFile) u64 { return switch (f) { // ponytail: /screen has no length until an open renders its frame, // and /pane/new none until an open has a pane to name. - .root, .pane, .screen, .new => 0, + .root, .pane, .col, .screen, .new => 0, + .layout => cols.layoutLen(p), + .tag => cols.headerText(p, null).?.len + 1, .index => pane.indexLen(p), .README => fs.help.len, .status => ctl.statusLen(p), @@ -485,6 +540,7 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply { .pty => Node.of(t.serial, .dir), else => return Reply.fail(req.tag, E.NOTDIR), }, + .col => |c| if (c.file == .dir) @intFromEnum(TopFile.col) else return Reply.fail(req.tag, E.NOTDIR), }; return handle(p, .{ .tag = req.tag, .op = .getattr, .node = parent }); } @@ -507,8 +563,22 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply { if (serial == 0) return Reply.fail(req.tag, E.NOENT); break :pane Node.of(serial, .dir); }, + // A column is its serial, in decimal digits and never 0. + .col => col: { + if (name.len > 10) return Reply.fail(req.tag, E.NOENT); + for (name) |c| if (c < '0' or c > '9') return Reply.fail(req.tag, E.NOENT); + const serial = std.fmt.parseInt(u32, name, 10) catch return Reply.fail(req.tag, E.NOENT); + if (serial == 0) return Reply.fail(req.tag, E.NOENT); + break :col Node.ofCol(serial, .dir); + }, else => return Reply.fail(req.tag, E.NOTDIR), }, + .col => |c| col: { + if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR); + const f = std.meta.stringToEnum(ColFile, name) orelse return Reply.fail(req.tag, E.NOENT); + if (f == .dir) return Reply.fail(req.tag, E.NOENT); + break :col Node.ofCol(c.serial, f); + }, .pane => |t| pane: { _ = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); const f = switch (t.file) { @@ -548,7 +618,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { .root => { // The guide, then what describes the session, then the // files that act, then the directories. - inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| { + inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.layout, TopFile.tag, TopFile.pane, TopFile.col }) |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"); @@ -568,8 +638,24 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { stageDirent(out, p.gpa, Node.of(serial, .dir), true, name); } }, + .col => for (0..p.ncol) |c| { + if (skip > 0) { + skip -= 1; + continue; + } + var buf: [16]u8 = undefined; + const serial = layout.columnSerial(p, c); + stageDirent(out, p.gpa, Node.ofCol(serial, .dir), true, std.fmt.bufPrint(&buf, "{d}", .{serial}) catch unreachable); + }, else => return Reply.fail(req.tag, E.NOTDIR), }, + .col => |c| { + if (layout.columnBySerial(p, c.serial) == null) return Reply.fail(req.tag, E.NOENT); + if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR); + inline for (.{ ColFile.tag, ColFile.ctl, ColFile.exec }) |f| { + if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, Node.ofCol(c.serial, f), false, f.fileName()); + } + }, .pane => |t| { const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); const terminal = p.panes[id].?.isTerminal(); @@ -594,7 +680,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { // pane it made, so reading it twice answers the same one and // closing it leaves the pane alone. .new => { - const slot = p.freeSlot() orelse return Reply.fail(req.tag, E.NFILE); + const slot = p.freeSlot() orelse return failText(req.tag, E.NFILE, std.fmt.bufPrint(&p.fs.ename, "pane/new: " ++ pardes.Messages.no_pane_slots, .{pardes.MAX_PANES}) catch "no pane slots"); p.newScratchFor(p.active, .fs); const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE); return .{ .tag = req.tag, .handle = made.serial }; @@ -603,6 +689,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { .log => .{ .log = .{} }, else => return .{ .tag = req.tag }, }, + .col => |c| return if (layout.columnBySerial(p, c.serial) != null) .{ .tag = req.tag } else Reply.fail(req.tag, E.NOENT), .pane => |t| what: { const pn = p.panes[p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT)].?; if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); @@ -659,6 +746,7 @@ fn remove(p: *Pardes, req: Req) Reply { const t = switch (target) { .pane => |t| t, .top => return Reply.fail(req.tag, E.PERM), + .col => |c| return cols.remove(p, req, c.serial, c.file), }; 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); @@ -676,8 +764,16 @@ fn release(p: *Pardes, req: Req) Reply { /// its releases before the connection's slot is reused, so none outlives /// the connection it came on. fn releaseHandle(p: *Pardes, req: Req) void { - const o = openOf(p, req) orelse return; const t = Node.target(req.node).?; + // A truncating write to `tag` is over when its open goes (pane.zig + // writeTag): a newline it held back is dropped. + if (t == .pane and t.pane.file == .tag) if (p.paneBySerial(t.pane.serial)) |id| { + p.panes[id].?.fs.tag_rewrite = false; + p.panes[id].?.fs.tag_held_newline = false; + }; + if (t == .col and t.col.file == .tag) cols.released(p, t.col.serial); + if (t == .top and t.top == .tag) cols.released(p, null); + const o = openOf(p, req) orelse return; const of: ?*Pane = if (t == .pane) (if (p.paneBySerial(t.pane.serial)) |id| p.panes[id] else null) else null; if (of) |pn| switch (o.what) { .event => |reader| { @@ -712,7 +808,8 @@ fn setattr(p: *Pardes, req: Req, target: Target) Reply { return failText(req.tag, E.INVAL, pane.e_addr_failed); if (pane.truncate(p, p.panes[id].?, t.file) != .ok) return Reply.fail(req.tag, E.NOMEM); }, - .top => {}, + .col => |c| if (c.file == .tag and cols.truncate(p, c.serial) != .ok) return Reply.fail(req.tag, E.NOENT), + .top => |f| if (f == .tag and cols.truncate(p, null) != .ok) return Reply.fail(req.tag, E.NOMEM), }; return .{ .tag = req.tag, .attr = attrOf(p, target) orelse return Reply.fail(req.tag, E.NOENT) }; } @@ -757,8 +854,9 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { var last: u32 = 0; while (nextSerialAfter(p, last)) |serial| { last = serial; - const pn = p.panes[p.paneBySerial(serial).?].?; - out.print(p.gpa, "{d} {s} {d} {s}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), pane.nameOf(pn) }) catch {}; + const id = p.paneBySerial(serial).?; + const pn = p.panes[id].?; + out.print(p.gpa, "{d} {s} {d} {s} {d}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), pane.nameOf(pn), pane.columnOf(p, id) }) catch {}; } break :index stagedReply(p, req); }, @@ -770,6 +868,13 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { .focus => ctl.readFocus(p, req), .ctl => ctl.readRoot(p, req), .commands => ctl.readCommands(p, req), + .layout => layout: { + var text: std.Io.Writer.Allocating = .init(p.scratch.allocator()); + cols.writeLayout(p, &text.writer) catch break :layout Reply.fail(req.tag, E.NOMEM); + p.fs.stage(p.gpa).appendSlice(p.gpa, text.written()) catch break :layout Reply.fail(req.tag, E.NOMEM); + break :layout stagedReply(p, req); + }, + .tag => cols.read(p, req, null), // The serial the open handed this fid, so that two reads of one // fid answer the same pane: the read observes, the open acted. .new => serial: { @@ -778,7 +883,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { break :serial Reply.fail(req.tag, E.NOMEM); break :serial stagedReply(p, req); }, - .root, .pane => Reply.fail(req.tag, E.PERM), + .root, .pane, .col => Reply.fail(req.tag, E.PERM), }, .pane => |t| { // A run's answer outlives the pane it ran in. @@ -794,6 +899,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); return pane.read(p, req, id, pn, t.file); }, + .col => |c| return if (c.file == .tag) cols.read(p, req, c.serial) else Reply.fail(req.tag, E.PERM), } } @@ -804,6 +910,7 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { .log => events.writeLog(p, req), .focus => ctl.writeFocus(p, req), .ctl => ctl.writeRoot(p, req), + .tag => cols.write(p, req, null), else => Reply.fail(req.tag, E.PERM), }, .pane => |t| { @@ -812,6 +919,12 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); return pane.write(p, req, id, pn, t.file); }, + .col => |c| return switch (c.file) { + .tag => cols.write(p, req, c.serial), + .ctl => cols.writeCtl(p, req, c.serial), + .exec => cols.writeExec(p, req, c.serial), + .dir => Reply.fail(req.tag, E.PERM), + }, } } @@ -823,6 +936,7 @@ test { _ = addr; _ = ctl; _ = pty; + _ = cols; _ = sources; _ = screen; } @@ -955,12 +1069,13 @@ test "readdir lists the root and a pane directory without creating anything" { @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 >= 10); - for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| { + for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "layout", "tag", "pane", "col", "os" }, 0..) |name, i| { try testing.expectEqualStrings(name, top[i].name); - try testing.expectEqual(i >= 11, top[i].dir); + try testing.expectEqual(i >= 13, top[i].dir); } - try testing.expectEqual(@intFromEnum(TopFile.pane), top[11].node); - try testing.expectEqual(fs.os_root, top[12].node); + try testing.expectEqual(@intFromEnum(TopFile.pane), top[13].node); + try testing.expectEqual(@intFromEnum(TopFile.col), top[14].node); + try testing.expectEqual(fs.os_root, top[15].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); @@ -1099,10 +1214,11 @@ test "opening /pane/new makes a pane and removing one closes it" { try testing.expect(p.paneBySerial(serial) != null); try testing.expectEqualStrings("hi", p.panes[id].?.file.?.content); - // The tree declares no create, so the engine refuses every Tcreate - // (cloud9 fs.zig: `.tcreate => if (features.create) ... else fail(e_perm)`), - // and `new` is the only name in /pane that is not a serial. - try testing.expect(!features.create); + // Every create is refused, a pane's that has closed as gone, and `new` + // is the only name in /pane that is not a serial. + try testing.expectEqual(E.PERM, call(p, .{ .tag = 6, .op = .open, .node = panes_dir, .create = true, .data = "scratch" }).errno()); + try testing.expectEqual(E.PERM, call(p, .{ .tag = 6, .op = .open, .node = Node.of(serial, .dir), .create = true, .data = "x" }).errno()); + try testing.expectEqual(E.NOENT, call(p, .{ .tag = 6, .op = .open, .node = Node.of(999_999, .dir), .create = true, .data = "exec" }).errno()); try testing.expectEqual(E.NOENT, look_up(p, panes_dir, "scratch").errno()); try testing.expectEqual(E.NOENT, look_up(p, root, new_pane).errno()); // ...and it is listed, because the user asked to see it in `ls`. @@ -1190,3 +1306,32 @@ test "editor paths resolve to the same nodes the wire serves" { try testing.expect(resolveSelf(p, "pane/new/body") == null); try testing.expectEqual(before, p.next_serial); } + +test "at the pane cap, pane/new, look and New each say so, and look reads back empty" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const new = @intFromEnum(TopFile.new); + while (p.freeSlot() != null) try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status); + var want: [64]u8 = undefined; + const said = try std.fmt.bufPrint(&want, "no pane slots: {d} max", .{pardes.MAX_PANES}); + const refused = call(p, .{ .tag = 2, .op = .open, .node = new }); + try testing.expectEqual(E.NFILE, refused.errno()); + try testing.expect(std.mem.endsWith(u8, refused.reply.ename, said)); + try testing.expect(th.logHas(p, said)); + // A look that would open a pane fails its write, and says the same. + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "f\n" }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + var line: [4200]u8 = undefined; + const look_node = @intFromEnum(TopFile.look); + const looked = th.wr(p, look_node, try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{dir})); + try testing.expect(looked.errno() != 0); + try testing.expect(std.mem.indexOf(u8, looked.reply.ename, said) != null); + try testing.expectEqualStrings("", th.rd(p, look_node, 0, 64).bytes); + // New from a pane's ctl fails as well. + const ctl_node = Node.of(p.panes[0].?.serial, .ctl); + const made = th.wr(p, ctl_node, "New\n"); + try testing.expect(std.mem.indexOf(u8, made.reply.ename, said) != null); +} |
