summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 23:29:17 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:18 -0300
commitafaa2428b346f6dd1d165abe1396dce9150fa05c (patch)
tree8c7f14cd5f720e61f0eb0104d862fe4f0cc17d4e
parentafe51fc6c3675a6aa8ab2f5caf64796b65597c2c (diff)
downloadpardes-afaa2428b346f6dd1d165abe1396dce9150fa05c.tar.gz
pardes-afaa2428b346f6dd1d165abe1396dce9150fa05c.zip
Forwarding pardes FILE for a new name in a directory it may not read or write is refused with words, not a pane that only fails at Save
A new name forwarded into a directory that is there but locked opened an empty pane named for it, and the failure came only at its Save, long after the launch. The directory is checked first: one that cannot be read, written and entered is refused, exit 1, no pane made. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/main.zig10
-rw-r--r--test/fs.py13
2 files changed, 23 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig
index 3218fef1..9dce559f 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -106,6 +106,7 @@ fn forwardWords(err: anyerror, buf: []u8) []const u8 {
error.NotOneLine => "a file name is one line, and this one holds a newline",
error.NotAFileName => "names a directory, not a file",
error.NoWorkingDirectory => "this shell's working directory is gone",
+ error.DirectoryNotWritable => "its directory may not be read or written, so it could never be saved",
error.NotAPaneAddress => "not a pane address: @p and a pane's number",
error.NoSuchPane => "this session has no such pane",
else => if (pardes.Messages.dialReason(err)) |why| why else words: {
@@ -483,6 +484,15 @@ fn nativeMain(init: std.process.Init) !void {
const cwd = std.mem.sliceTo(&cwd_buf, 0);
break :named std.fs.path.resolvePosix(arena, &.{ cwd, target.path }) catch |err| Refuse.with(init.io, word, err);
};
+ // A directory that is there but may not be read or written: a
+ // pane for the name could only fail at its Save, so refused now.
+ var dir_z_buf: [4096]u8 = undefined;
+ if (std.fmt.bufPrintSentinel(&dir_z_buf, "{s}", .{dir.path}, 0)) |dir_z| {
+ const R_OK = 4;
+ const W_OK = 2;
+ const X_OK = 1;
+ if (std.c.access(dir_z.ptr, R_OK | W_OK | X_OK) != 0) Refuse.with(init.io, word, error.DirectoryNotWritable);
+ } else |_| {}
break :named std.fmt.bufPrint(&newbuf, "{s}/{s}", .{ std.mem.trimEnd(u8, dir.path, "/"), base }) catch |err| Refuse.with(init.io, word, err);
};
var made_serial: ?u32 = null;
diff --git a/test/fs.py b/test/fs.py
index 8d830c48..0821b117 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -1403,6 +1403,19 @@ def test(binary, quic=False):
# A pane address the session has not is refused in words, exit
# 1, no pane made for a file of that name; one it has is looked
# at. A name with a newline is said in words, not an error name.
+ # A new name in a directory there but not writable: refused now,
+ # not a pane that only fails at Save.
+ shut = root / 'shut-dir'
+ shut.mkdir()
+ shut.chmod(0o500)
+ try:
+ refused = subprocess.run([binary, 'shut-dir/new.txt'], cwd=root, env=env,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
+ assert refused.returncode == 1, refused
+ assert b'may not be read or written' in refused.stderr, refused.stderr
+ assert serials() == before, (serials(), before)
+ finally:
+ shut.chmod(0o755)
for word, said in (('@p999:1', b'pardes: @p999:1: this session has no such pane'),
('two\nlines.txt', b'a file name is one line')):
refused = subprocess.run([binary, word], cwd=root, env=env,