diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 23:42:32 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | f842a4026348c65370a973e466c2b4f45063bdaf (patch) | |
| tree | e6258955563103db1ad76b1324f3971e279afa06 /src/ninep | |
| parent | 8469def61324feb5e278f88bebad36af7a0d997e (diff) | |
| download | pardes-f842a4026348c65370a973e466c2b4f45063bdaf.tar.gz pardes-f842a4026348c65370a973e466c2b4f45063bdaf.zip | |
A write to a closed pane's file says it is gone, and a terminal says why it takes no name
`echo Del > pane/6/exec` just after pane 6 closed failed EACCES: the shell's
lookup failed, it tried to create the file, and the engine refused every
Tcreate as a permission error. pardes now takes Tcreate itself: refused as
before (acme's fsyscreate refuses it too), ENOENT in a pane that has
closed. Renaming a terminal was refused with only EPERM; it now says a
terminal is named by its shell's directory, worded so a mount still maps
it to EPERM.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/pane.zig | 9 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 22 |
2 files changed, 22 insertions, 9 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 9d20c587..3eafdbce 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -561,7 +561,10 @@ fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { const name = std.mem.trim(u8, req.data, " \t\r\n"); if (name.len == 0) return Reply.fail(req.tag, E.INVAL); for (name) |c| if (c < ' ') return Reply.fail(req.tag, E.INVAL); - if (fileOf(pane) == null) return Reply.fail(req.tag, E.PERM); + if (fileOf(pane) == null) return tree.failText(req.tag, E.PERM, if (pane.isTerminal()) + "rename not allowed: a terminal is named by its shell's directory; cd there, or Tty in another" + else + "rename not allowed: an image or PDF is named by the file it shows"); const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ pardes.Pardes.paneDir(pane), name }) catch return Reply.fail(req.tag, E.NOMEM); if (!std.fs.path.isAbsolute(full) or full.len >= 4096) return Reply.fail(req.tag, E.INVAL); @@ -734,7 +737,9 @@ test "a terminal is listed as term with its directory as name" { try std.fmt.bufPrint(&want, "{d} term 0 /work/dir\n", .{pane.serial}), ) != null); try testing.expectEqualStrings("/work/dir\n", rd(p, Node.of(pane.serial, .name), 0, 4096).bytes); - try testing.expectEqual(E.PERM, wr(p, Node.of(pane.serial, .name), "/elsewhere\n").errno()); + const refused = wr(p, Node.of(pane.serial, .name), "/elsewhere\n"); + try testing.expectEqual(E.PERM, refused.errno()); + try testing.expect(std.mem.startsWith(u8, refused.reply.ename, "rename not allowed: a terminal is named")); } test "body reads at any offset and writes append" { diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index e5320f61..7279b64a 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -68,9 +68,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. @@ -371,6 +373,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 @@ -1105,10 +1112,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`. |
