summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md6
-rw-r--r--src/exec.zig37
-rw-r--r--src/ninep/ctl.zig43
3 files changed, 78 insertions, 8 deletions
diff --git a/docs/fs.md b/docs/fs.md
index e988b87f..4fb15eb8 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -258,7 +258,11 @@ takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir
<the directory in effect>`, `LocationsConfig ...`), so writing what it reads
back changes nothing; a setting the frontend cannot show (`Lift`,
`GripWidth` on a terminal) is refused as `Lift is GUI-only, invalid here`
-(EINVAL: a request this build cannot take), and so is a builtin only the
+(EINVAL: a request this build cannot take); acme's own words run as
+pardes's where it has one -- `Put` is `Save`, `Delete` a `Del` that does
+not ask -- and the rest (`Get`, `Putall`, `Snarf`, `Cut`, `Paste`, `Zerox`,
+`Sort`, `Load`, `ID`, `Send`) are refused, EINVAL, `invalid: acme's Snarf
+is not a pardes builtin`, never run as shell commands; and so is a builtin only the
GUI has (`Fonts`), written to an `exec` too, where it would otherwise run as
a shell command; a `Shell` or `Tty` naming no
shell says `shell "x" not found` (ENOENT); a `DumpDir` whose last
diff --git a/src/exec.zig b/src/exec.zig
index 06e5d375..87f6da40 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -811,9 +811,42 @@ pub fn namesBuiltin(txt: []const u8) bool {
return std.meta.stringToEnum(Builtin, cmd[0..end]) != null;
}
+/// acme's words, run by their pardes names (Put is Save; Delete is a Del
+/// that does not ask), or refused, never run as a shell command: a Put that
+/// exited 127 looked like a save that worked.
+pub const acme_words = [_]struct { acme: []const u8, pardes: ?[]const u8, hint: []const u8 = "" }{
+ .{ .acme = "Put", .pardes = "Save" },
+ .{ .acme = "Delete", .pardes = "Del" },
+ .{ .acme = "Get", .pardes = null, .hint = ": write get to the pane's ctl" },
+ .{ .acme = "Putall", .pardes = null, .hint = ": Save each pane" },
+ .{ .acme = "Snarf", .pardes = null, .hint = ": y yanks the selection" },
+ .{ .acme = "Cut", .pardes = null, .hint = ": d deletes the selection into a register" },
+ .{ .acme = "Paste", .pardes = null, .hint = ": p puts a register" },
+ .{ .acme = "Zerox", .pardes = null },
+ .{ .acme = "Sort", .pardes = null },
+ .{ .acme = "Load", .pardes = null, .hint = ": Restore loads a dump" },
+ .{ .acme = "ID", .pardes = null },
+ .{ .acme = "Send", .pardes = null },
+};
+
pub fn executeBuiltinLine(p: *Pardes, id: usize, txt: []const u8) bool {
- const cmd = commandText(txt);
- if (cmd.len == 0 or p.exec_depth >= max_exec_depth) return false;
+ const raw = commandText(txt);
+ if (raw.len == 0 or p.exec_depth >= max_exec_depth) return false;
+ const word_end = std.mem.indexOfAny(u8, raw, " \t") orelse raw.len;
+ var renamed: [1024 + 16]u8 = undefined;
+ const cmd = for (acme_words) |w| {
+ if (!std.mem.eql(u8, raw[0..word_end], w.acme)) continue;
+ const now = w.pardes orelse {
+ var said: [128]u8 = undefined;
+ p.reportFailure(id, std.fmt.bufPrint(&said, "invalid: acme's {s} is not a pardes builtin{s}", .{ w.acme, w.hint }) catch "invalid: an acme word pardes has not");
+ return true;
+ };
+ // Delete: Del, the text thrown away unasked.
+ if (std.mem.eql(u8, w.acme, "Delete")) if (p.panes[id]) |pane| if (pane.file) |f| {
+ pane.discard_warned = .{ .revision = f.revision, .by = .Del };
+ };
+ break std.fmt.bufPrint(&renamed, "{s}{s}", .{ now, raw[word_end..] }) catch raw;
+ } else raw;
// `Word arg`, or for a word that says so, `Word+arg` (`Tty+fish`).
const plus = std.mem.indexOfScalar(u8, cmd, '+');
var sp = std.mem.indexOfAny(u8, cmd, " \t");
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();