From daf833bccf5aca958593738ecc59fd58dbb697b6 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 17:35:15 -0300 Subject: name refuses a directory, EISDIR, when written, not at the Save that could only fail `name ~`, `name /` or `name foo/` renamed the pane to a directory, and only its Save failed later. A name ending in `/`, the root, or one naming a directory that is there is now refused at once, with words a mount reads as EISDIR (`name: is a directory, not a file`). The ctl word name and a forwarded `pardes ~` go through the same check. fs.md says so. Co-Authored-By: Claude Opus 5.5 --- src/ninep/pane.zig | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'src/ninep') diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index d3c1b907..78bec09f 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -883,6 +883,11 @@ fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ pardes.Pardes.paneDir(pane), fs.expandHome(name, &home_buf) }) catch return Reply.fail(req.tag, E.NOMEM); if (!std.fs.path.isAbsolute(full) or full.len >= 4096) return tree.failText(req.tag, E.INVAL, "invalid file name: longer than a path may be"); + // A directory (`~`, `/`, `foo/`) is no file to name a pane after: a + // Save could only fail. Said now, EISDIR. + const is_dir = name[name.len - 1] == '/' or std.mem.eql(u8, full, "/") or + (if (comptime pardes.hosted) (if (fs.localPath(full)) |local| exec.isDirectory(local) else false) else false); + if (is_dir) return tree.failText(req.tag, E.ISDIR, std.fmt.bufPrint(&p.fs.ename, "name: {s} is a directory, not a file", .{full[0..@min(full.len, 256)]}) catch "name: is a directory, not a file"); nameBuffer(p, id, full, false) catch |err| return Reply.fail(req.tag, switch (err) { error.OutOfMemory => E.NOMEM, else => E.INVAL, @@ -1415,6 +1420,20 @@ test "an addr pair out of order or out of range is refused as the address form i try testing.expectEqual(State.Range{ .q0 = 2, .q1 = 4 }, p.panes[p.paneBySerial(serial).?].?.fs.addr); } +test "name refuses a directory, EISDIR, and leaves the name as it was" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + const node = Node.of(serial, .name); + for ([_][]const u8{ "/\n", "/tmp/\n", "sub/\n", "/tmp\n" }) |dir| { + if (comptime !pardes.hosted) if (std.mem.eql(u8, dir, "/tmp\n")) continue; + const r = wr(p, node, dir); + try testing.expectEqual(E.ISDIR, r.errno()); + try testing.expect(std.mem.indexOf(u8, r.reply.ename, "is a directory") != null); + } + try testing.expectEqualStrings("/test.txt", nameOf(p, p.panes[p.paneBySerial(serial).?].?)); +} + test "a write of two lines to name is refused EINVAL, on a held open or not" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); -- cgit v1.3