From 16d5415c508fea0b5dc90dd2cc40866beecf216b Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 16:50:04 -0300 Subject: acme's words run as pardes's or are refused, never as shell commands: Put saves Put, Get, Delete, Snarf, Cut, Paste, Zerox, Sort, Putall, Load, ID and Send written to exec or tagexec each ran as a shell command that exited 127, the write answering 0 -- a Put that looked like a save. Put now runs Save and Delete a Del that does not ask; the rest are refused, EINVAL, invalid: acme's X is not a pardes builtin (with where pardes does it, where it does), in exec and ctl alike. Co-Authored-By: Claude Opus 5.5 --- src/ninep/ctl.zig | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) (limited to 'src/ninep') diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index d2ff4dab..a8d4e8e0 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -251,8 +251,9 @@ pub fn captured(p: *Pardes, req: Req, id: usize, exec: bool, text: []const u8, i /// EIO. fn failureErrno(failure: []const u8) u16 { if (std.mem.indexOf(u8, failure, "no such") != null or std.mem.indexOf(u8, failure, "not found") != null) return E.NOENT; - // A pattern refused (Edit's too) is malformed input, as 9ns reads it. - if (std.mem.indexOf(u8, failure, "bad regular expression") != null) return E.INVAL; + // 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.startsWith(u8, failure, "invalid")) return E.INVAL; return E.IO; } @@ -387,7 +388,13 @@ pub fn refuseTo(p: *Pardes, req: Req, why: []const u8, line: []const u8, ctl: [] /// as on a tag; acme's lowercase verbs are the pane ctl's and alias none. fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?Reply { const sp = std.mem.indexOfAny(u8, line, " \t"); - const word = line[0 .. sp orelse line.len]; + const typed = line[0 .. sp orelse line.len]; + // acme's words: checked as the pardes word they run as, or refused. + const word = for (pardes.exec.acme_words) |w| { + if (!std.mem.eql(u8, typed, w.acme)) continue; + const now = w.pardes orelse return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "invalid: acme's {s} is not a pardes builtin{s}", .{ w.acme, w.hint }) catch "invalid: an acme word pardes has not"); + break now; + } else typed; const arg = if (sp) |s| std.mem.trim(u8, line[s + 1 ..], " \t") else ""; const b = std.meta.stringToEnum(Builtin, word) orelse { // A setting this build's frontend cannot show (Lift, GripWidth on a @@ -1035,11 +1042,12 @@ test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the r const serial = serialOf(p); const ctl_node = Node.of(serial, .ctl); // acme's other ctl words have files of their own here, and a builtin is - // its own capitalised word: `Get`, `DEL` and `del` are none of them. + // its own capitalised word: `GET`, `DEL` and `del` are none of them + // (acme's own `Get` is refused as acme's). for ([_][]const u8{ "menu", "nomenu", "dump echo hi", "font Go Mono", "lock x", "bogus", "DEL", "put", "del", "delete", "clean", "dirty", "show", "mark", - "Get", "limit=addr", "get x", "look /tmp", "exec Del", + "GET", "limit=addr", "get x", "look /tmp", "exec Del", }) |bad| { const refused = wr(p, ctl_node, bad); try testing.expectEqual(E.INVAL, refused.errno()); @@ -2269,6 +2277,31 @@ test "a look that misses file:/re/ names the file, and is logged as its pane's" try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "err {d} look: {s}/f.txt: no match for regexp", .{ f_serial, dir }))); } +test "acme's words run as pardes's (Put is Save, Delete a Del that does not ask) or are refused, never run as commands" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + for ([_][]const u8{ "Snarf\n", "Putall\n", "Zerox\n", "Get\n" }) |line| { + for ([_]u64{ Node.of(serial, .ctl), Node.of(serial, .exec) }) |node| { + const r = wr(p, node, line); + try testing.expectEqual(E.INVAL, r.errno()); + try testing.expect(std.mem.indexOf(u8, r.reply.ename, "is not a pardes builtin") != null); + } + } + // Put saves, as Save does. + _ = wr(p, Node.of(serial, .name), "/tmp/pardes-put.txt\n"); + const put = wr(p, Node.of(serial, .exec), "Put\n"); + try testing.expectEqual(Status.ok, put.reply.status); + try testing.expect(put.saved); + // Delete closes an edited pane without asking. + const made = try th.newPane(p); + _ = wr(p, Node.of(made, .name), "/tmp/pardes-delete.txt\n"); + _ = wr(p, Node.of(made, .body), "edited\n"); + try testing.expectEqual(Status.ok, wr(p, Node.of(made, .ctl), "Delete\n").reply.status); + p.sync(); + try testing.expect(p.paneBySerial(made) == null); +} + test "every EINVAL a write gets says why, in its err record too; DEL is a control character in a line" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); -- cgit v1.3