summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 23:20:19 -0300
committerGabriel Schneider <[email protected]>2026-10-01 23:22:39 -0300
commitdd6a3c2c367cee12a8e2da81e7da6a2c8f3103d5 (patch)
tree3c2acaf6836af9c52b7cfbf201a2c1b533512d10 /src/ninep/ctl.zig
parente4562384ac910b78254ec7aaaecad8b74717f2ee (diff)
downloadpardes-dd6a3c2c367cee12a8e2da81e7da6a2c8f3103d5.tar.gz
pardes-dd6a3c2c367cee12a8e2da81e7da6a2c8f3103d5.zip
Round 39's small ones: a name write past its one line is refused by that write; Get and Edit's e load over a scratch under 100 bytes unasked, as Del closes it; Putall saves a Zerox pair once; bare Tab on the root ctl says the width as from a pane, and a ctl check of Tab no longer sets it; a directory read is "is a directory" (EISDIR), an unreadable file "permission denied" (EACCES), Incl of a file "not a directory" (ENOTDIR), and a device "not a regular file", never read to the stream limit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig72
1 files changed, 70 insertions, 2 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index c6b76d51..dc5e8386 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -322,6 +322,10 @@ pub fn failureErrno(failure: []const u8) u16 {
// word 9ns reads as it.
if (std.mem.indexOf(u8, failure, "busy: ") != null) return E.BUSY;
if (std.mem.indexOf(u8, failure, "no such") != null or std.mem.indexOf(u8, failure, "not found") != null) return E.NOENT;
+ // As 9ns reads the same words (cloud9.fs.enameErrno).
+ if (std.mem.indexOf(u8, failure, "not a dir") != null) return E.NOTDIR;
+ if (std.mem.indexOf(u8, failure, "is a dir") != null) return E.ISDIR;
+ if (std.mem.indexOf(u8, failure, "permission denied") != null) return 13; // EACCES
// A pattern refused (Edit's too) is malformed input, as 9ns reads it;
// so is a word this build has not (`invalid: acme's Put ...`).
if (std.mem.indexOf(u8, failure, "bad regular expression") != null or std.mem.indexOf(u8, failure, "invalid") != null or
@@ -548,6 +552,8 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?
return refuse(p, req, std.fmt.bufPrint(&why, head ++ "{s}" ++ tail, .{pardes.colors.themesNear(near[0..@min(room, near.len)], arg)}) catch "bad value in control message", arg);
},
.font => config.Runtime.FontSpec.parse(arg) != null,
+ // Bare, it says the width, as from a pane's exec.
+ .tab => arg.len == 0 or (std.fmt.parseInt(u16, arg, 10) catch 0) -% 1 < 16,
else => probe: {
var probe = p.settings;
break :probe probe.apply(setting, if (arg.len > 0) arg else null);
@@ -1158,6 +1164,9 @@ const e_locked = tree.e_in_use ++ ": another open of this ctl holds the lock";
pub fn getRefused(p: *Pardes, pane: *Pane, word: []const u8) ?[]const u8 {
const f = pane_files.fileOf(pane) orelse return null;
if (!panes.Output.fileTraits(f.output).saves or f.revision == f.saved_revision) return null;
+ // A scratch under 100 bytes is not asked about, as Del and Exit do not
+ // ask (builtins.warnModifiedIn): nothing worth a question to lose.
+ if (f.output != null and f.content.len < 100) return null;
// Dirty by a rename alone is no text to lose: only edits are asked about.
const saved = f.saved_hash orelse return null;
if (std.hash.Wyhash.hash(0, f.content) == saved) return null;
@@ -2106,6 +2115,64 @@ test "Undo to unsaved text under its old name says nothing changed on disk when
}
}
+test "round 39's small refusals: one name an open, small scratches unasked, twins saved once, bare Tab on ctl, and errnos in words a mount reads" {
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "f\n" });
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "noread.txt", .data = "secret\n" });
+ try tmp.dir.createDirPath(testing.io, "sub");
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const pane = p.panes[0].?;
+ var line: [4300]u8 = undefined;
+ // One name an open: a second line, in the write or after, is refused.
+ try testing.expectEqual(E.INVAL, wr(p, Node.of(serial, .name), "/tmp/pardes-a\nb").errno());
+ const before = try testing.allocator.dupe(u8, pane.file.?.path);
+ defer testing.allocator.free(before);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = Node.of(serial, .name), .omode = 1 }).reply.handle;
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = Node.of(serial, .name), .handle = h, .data = "/tmp/pardes-one.txt\n" }).reply.status);
+ try testing.expectEqual(E.INVAL, call(p, .{ .tag = 3, .op = .write, .node = Node.of(serial, .name), .handle = h, .data = "b" }).errno());
+ _ = call(p, .{ .tag = 4, .op = .release, .node = Node.of(serial, .name), .handle = h, .opened = true });
+ tree.runClosedLines(p);
+ try testing.expectEqualStrings("/tmp/pardes-one.txt", pane.file.?.path);
+ // Bare Tab on the root ctl says the width, as from a pane.
+ try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Tab\n").reply.status);
+ // Errnos a mount reads from the words.
+ _ = wr(p, Node.of(serial, .name), try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{dir}));
+ try testing.expect(p.executeBuiltinLine(0, "Get"));
+ try testing.expectEqual(E.ISDIR, wr(p, Node.of(serial, .exec), "Get sub\n").errno());
+ if (comptime pardes.hosted) {
+ var noread_buf: [4300]u8 = undefined;
+ _ = std.c.chmod(try std.fmt.bufPrintSentinel(&noread_buf, "{s}/noread.txt", .{dir}, 0), 0);
+ try testing.expectEqual(@as(u16, 13), wr(p, Node.of(serial, .exec), "Get noread.txt\n").errno());
+ try testing.expectEqual(E.NOTDIR, wr(p, Node.of(serial, .exec), try std.fmt.bufPrint(&line, "Incl {s}/f.txt\n", .{dir})).errno());
+ const zero = wr(p, Node.of(serial, .look), "/dev/zero\n");
+ _ = zero;
+ try testing.expect(th.logHas(p, "not a regular file"));
+ }
+ // Twins saved once by Putall.
+ _ = wr(p, Node.of(serial, .body), "edited\n");
+ try testing.expect(p.executeBuiltinLine(0, "Zerox"));
+ while (p.nextEffect()) |_| {}
+ try testing.expect(p.executeBuiltinLine(0, "Putall"));
+ var saves: usize = 0;
+ while (p.nextEffect()) |e| if (e == .save_file) {
+ saves += 1;
+ };
+ try testing.expectEqual(@as(usize, 1), saves);
+ // A scratch of 2 bytes is loaded over unasked.
+ try testing.expect(p.executeBuiltinLine(0, "New"));
+ const scratch = p.panes[p.active].?;
+ _ = pane_files.spliceBody(p, scratch, 0, 0, "hi") orelse return error.NoScratch;
+ const failures = p.fs.failures;
+ try testing.expect(p.executeBuiltinLine(p.active, try std.fmt.bufPrint(&line, "Get {s}/f.txt", .{dir})));
+ try testing.expectEqual(failures, p.fs.failures);
+ try testing.expectEqualStrings("f\n", scratch.file.?.content);
+}
+
test "a body write to a terminal is typed input, not a paste, even to a program that asked for bracketed paste" {
const p = try Pardes.init(testing.allocator, .{ .cols = 90, .rows = 30 });
defer p.deinit();
@@ -2761,7 +2828,8 @@ test "Shell refuses a path that is no executable, and bare it goes back to the d
try testing.expectEqual(Status.err, refused.reply.status);
try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "Shell: shell \"/nonexistent/zzsh\" not found") != null);
try testing.expect(th.logHas(p, "shell \"/nonexistent/zzsh\" not found"));
- try testing.expectEqual(E.IO, wr(p, root_ctl, "Shell /etc\n").errno());
+ // Its words say a directory, which a mount reads as EISDIR too.
+ try testing.expectEqual(E.ISDIR, wr(p, root_ctl, "Shell /etc\n").errno());
try testing.expect(th.logHas(p, "Shell: not a shell: /etc is a directory"));
try testing.expectEqualStrings("", p.settings.shell.requested.get());
try testing.expectEqual(Status.ok, wr(p, root_ctl, "Shell /bin/sh\n").reply.status);
@@ -3073,7 +3141,7 @@ test "pty/ctl exec in a directory that is gone fails ENOENT; a shell that cannot
// The host's own failure is the waiting write's (late_failure).
p.fs.late_failure_len = 0;
p.shellFailed(@intCast(p.paneBySerial(serial).?), error.AccessDenied);
- try testing.expectEqualStrings("shell: access denied", p.fs.late_failure[0..p.fs.late_failure_len]);
+ try testing.expectEqualStrings("shell: permission denied", p.fs.late_failure[0..p.fs.late_failure_len]);
}
test "a script whose interpreter is not there: Tty refuses it up front, only an err logged, and pty/ctl exec keeps the running shell" {