summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 21:40:57 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitf8710ab27b54bf0241ad2ed629ccc98aaa619709 (patch)
tree0448730ee44282b0adb2ee17fdb92d4451979911
parent4e5719a133030e8ed29799d4959bb0b917092d7c (diff)
downloadpardes-f8710ab27b54bf0241ad2ed629ccc98aaa619709.tar.gz
pardes-f8710ab27b54bf0241ad2ed629ccc98aaa619709.zip
Edit's w and the other left-out sam commands fail EIO in words, never EOPNOTSUPP, and /os refuses a rename or a chmod as not permitted, EACCES
"w is not supported in pardes" read through a mount as EOPNOTSUPP, as if the file system lacked an operation, and a chmod under /os was answered "invalid truncate", EINVAL. An Edit command pardes leaves out now says so in words 9ns reads as EIO, and a rename or mode change of an /os file is "permission denied", EACCES. fs.md's Failure section and /os entry say both. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md8
-rw-r--r--src/fs.zig29
-rw-r--r--src/ninep/ctl.zig1
-rw-r--r--src/sam_edit.zig6
4 files changed, 38 insertions, 6 deletions
diff --git a/docs/fs.md b/docs/fs.md
index c01587a1..15f490ec 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -139,8 +139,9 @@ never a C library string. Through 9ns the kernel sees an errno 9ns reads
from those words (cloud9 `9ns/src/nine.zig`, `enameToErrno`): `control
message`, `invalid` or `bad ` is EINVAL (malformed input); `no such`, `not
found` ENOENT; `in use` EBUSY; `no space` ENOSPC; `denied` EACCES; anything
-else, such as `no match for regexp`, `address out of range` or `Modified`,
-EIO. The `err` record has the words; a shell sees
+else, such as `no match for regexp`, `address out of range`, `Modified` or
+an `Edit` command pardes leaves out (`w`, `e`, `r`, `|`: `w is a sam command
+pardes's Edit leaves out`), EIO; no refusal reads as EOPNOTSUPP. The `err` record has the words; a shell sees
only the errno, most often `Invalid argument` or `Input/output error`.
Not failures: a look that finds nothing (it answers nothing and logs one
@@ -708,7 +709,8 @@ become spaces.
one at its last (a PDF's is its page); a look at a row reopens it there.
- `/status`: `pid`, `version`, `panes`.
- `/os/`: existing regular files take read, write and truncation to zero;
- create, remove, rename and metadata changes are refused; ownership is
+ create, remove, rename and mode changes are refused as not permitted
+ (`permission denied`, EACCES through a mount); ownership is
synthetic. Linux v9fs's truncation `mtime` hint is accepted and dropped.
- `/src/` (and `/shaders` on GUI builds) with `-Dembed-sources=true`;
`EffectCode <effect>` lists an effect's files under `/virtual`.
diff --git a/src/fs.zig b/src/fs.zig
index bc35e1cb..d13e36bd 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -213,6 +213,11 @@ pub fn osHandle(p: *pardes.Pardes, req: Req) Reply {
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } };
},
.read, .write, .setattr => {
+ // A rename or a mode change (`mv`, `chmod`) is refused as not
+ // permitted, EACCES through a mount: /os reads and writes the
+ // files it shows and changes nothing about them.
+ if (req.op == .setattr and (req.set.name or req.set.mode))
+ return tree.failText(req.tag, E.PERM, "permission denied: /os renames nothing and changes no mode");
if (stat.kind != .file) return Reply.fail(req.tag, E.PERM);
var z: [4096]u8 = undefined;
const path_z = std.fmt.bufPrintSentinel(&z, "{s}", .{path}, 0) catch return Reply.fail(req.tag, E.NOENT);
@@ -255,6 +260,30 @@ pub fn validMountName(name: []const u8) bool {
return true;
}
+test "/os refuses a rename or a mode change as not permitted, in words, and still truncates to zero" {
+ if (comptime !pardes.hosted) return error.SkipZigTest;
+ const p = try th.withFile(std.testing.allocator, "x\n");
+ defer p.deinit();
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "kept.txt", .data = "kept\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)];
+ var node: u64 = os_root;
+ var parts = std.mem.tokenizeScalar(u8, dir, '/');
+ while (parts.next()) |part| node = th.look_up(p, node, part).reply.attr.node;
+ node = th.look_up(p, node, "kept.txt").reply.attr.node;
+ for ([_]Req{
+ .{ .tag = 1, .op = .setattr, .node = node, .set = .{ .mode = true } },
+ .{ .tag = 2, .op = .setattr, .node = node, .set = .{ .name = true }, .data = "moved.txt" },
+ }) |req| {
+ const r = th.call(p, req);
+ try std.testing.expectEqual(E.PERM, r.errno());
+ try std.testing.expectStringStartsWith(r.reply.ename, "permission denied");
+ }
+ try std.testing.expectEqual(tree.Status.ok, th.call(p, .{ .tag = 3, .op = .setattr, .node = node, .truncate = true }).reply.status);
+}
+
test "mounts own their names and dials and reject duplicate or reserved names" {
const gpa = std.testing.allocator;
var ns: Namespace = .{};
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 5a199f55..6e09084b 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1854,6 +1854,7 @@ test "an error's words give the errno a mount reads: EINVAL for what is malforme
.{ .text = addressing.e_order, .malformed = false },
.{ .text = addressing.e_slow, .malformed = false },
.{ .text = "/tmp/x.txt: Modified (Exit again to discard)", .malformed = false },
+ .{ .text = "Edit: w is a sam command pardes's Edit leaves out (it has no file or shell commands)", .malformed = false },
}) |c| {
const says_einval = for (einval) |w| {
if (std.ascii.findIgnoreCase(c.text, w) != null) break true;
diff --git a/src/sam_edit.zig b/src/sam_edit.zig
index 94cf6fe0..9b055ef9 100644
--- a/src/sam_edit.zig
+++ b/src/sam_edit.zig
@@ -242,7 +242,7 @@ const Parser = struct {
}
},
'\'' => return fail(ps.why, "can't handle '", .{}),
- '"' => return fail(ps.why, "file addresses are not supported", .{}),
+ '"' => return fail(ps.why, "a file address is no address pardes's Edit takes", .{}),
else => break,
}
}
@@ -410,7 +410,7 @@ const Parser = struct {
if (nest == 0) return fail(ps.why, "right brace with no left brace", .{});
return null;
},
- 'b', 'B', 'D', 'e', 'r', 'w', 'f', 'X', 'Y', '<', '|', '>' => return fail(ps.why, "{c} is not supported in pardes", .{c}),
+ 'b', 'B', 'D', 'e', 'r', 'w', 'f', 'X', 'Y', '<', '|', '>' => return fail(ps.why, "{c} is a sam command pardes's Edit leaves out (it has no file or shell commands)", .{c}),
else => return fail(ps.why, "unknown command {c}", .{c}),
}
return cmd;
@@ -768,7 +768,7 @@ test "an Edit that fails halfway changes nothing, and says why in acme's words"
.{ "}", "right brace with no left brace" },
.{ ",x/foo/{", "unmatched `{'" },
.{ ",x/foo/{\nd", "unmatched `{'" },
- .{ "w /tmp/x", "w is not supported in pardes" },
+ .{ "w /tmp/x", "w is a sam command pardes's Edit leaves out (it has no file or shell commands)" },
.{ ",s/(a)/\\1/", "no \\1: mvzr keeps no submatches" },
.{ "1 m 1,2", "move overlaps itself" },
.{ ",x/(^|\\n)foo/d", "bad regular expression: in a pattern with \\n, ^ can only come first and $ only just before a \\n" },