summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig72
1 files changed, 68 insertions, 4 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 5ed03c58..0bbc0319 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -93,6 +93,12 @@ pub fn run(p: *Pardes, id: usize, exec: bool, text: []const u8, in_tag: bool) vo
// touched.
if (p.fs.refilled) |serial| if (p.paneBySerial(serial) != null) return noteResult(p, serial);
if (went) |dst| if (p.panes[dst]) |pane| return noteResult(p, pane.serial);
+ // A word in a column's or the workspace's tag ran at no pane: it
+ // answers one only if it focused one (Newcol answers nothing).
+ if (p.exec_header) {
+ if (p.active != id) if (p.panes[p.active]) |pane| noteResult(p, pane.serial);
+ return;
+ }
noteResult(p, if (exec) context else (p.panes[p.active] orelse return).serial);
}
@@ -233,6 +239,14 @@ pub fn captured(p: *Pardes, req: Req, id: usize, exec: bool, text: []const u8, i
p.fs.results_len = 0;
return refuse(p, req, "wrong #args in control message", cmd);
};
+ // ...and one that takes none, written with one (`Config extra`):
+ // refused as a ctl's is, not run as a shell command of that name.
+ if (std.mem.indexOfAny(u8, cmd, " \t")) |sp| if (std.meta.stringToEnum(Builtin, cmd[0..sp])) |b| {
+ if (!builtins.registry.takesArg(b) and config.Runtime.find(cmd[0..sp]) == null) {
+ p.fs.results_len = 0;
+ return refuse(p, req, "wrong #args in control message", cmd);
+ }
+ };
}
p.fs.capturing = true;
p.fs.failure_len = 0;
@@ -372,17 +386,27 @@ const Builtin = builtins.registry.Builtin();
/// (kernel/misc/parse.c:82): `unknown control message "Bogus 3"`.
fn refuse(p: *Pardes, req: Req, why: []const u8, line: []const u8) Reply {
const room = p.fs.ename.len -| (why.len + 3);
- const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\"", .{ why, line[0..@min(line.len, room)] }) catch why;
+ var shown: [320]u8 = undefined;
+ const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\"", .{ why, blanked(line[0..@min(line.len, room)], &shown) }) catch why;
return tree.failText(req.tag, E.INVAL, text);
}
/// `refuse`, saying which ctl takes the message instead.
pub fn refuseTo(p: *Pardes, req: Req, why: []const u8, line: []const u8, ctl: []const u8) Reply {
const room = p.fs.ename.len -| (why.len + ctl.len + 18);
- const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\": write it to {s}", .{ why, line[0..@min(line.len, room)], ctl }) catch why;
+ var shown: [320]u8 = undefined;
+ const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\": write it to {s}", .{ why, blanked(line[0..@min(line.len, room)], &shown), ctl }) catch why;
return tree.failText(req.tag, E.INVAL, text);
}
+/// A quoted line as a refusal shows it: a control byte (a tab, the one a
+/// line may hold) is a blank, so the reason reads as one line of words.
+fn blanked(line: []const u8, buf: *[320]u8) []const u8 {
+ const n = @min(line.len, buf.len);
+ for (line[0..n], buf[0..n]) |c, *o| o.* = if (c < ' ' or c == 0x7f) ' ' else c;
+ return buf[0..n];
+}
+
/// Checks one line written to a ctl as a builtin of `scope` before any line
/// of the write runs: a word the registry knows, of this ctl's scope, given
/// an argument if and only if it takes or requires one, and for a setting a
@@ -886,8 +910,10 @@ fn acmeCtl(p: *Pardes, req: Req, serial: u32, line: []const u8, apply: bool) Acm
const File = tree.PaneFile;
// A word written to one of the pane's own files, as acme's is to its.
const Into = struct { file: File, data: []const u8 };
+ // `name` takes all after its one blank, as the name file takes what is
+ // written: a blank at its start is refused there, not trimmed here.
const into: ?Into = if (std.mem.eql(u8, word, "name") and rest.len > 0)
- .{ .file = .name, .data = rest }
+ .{ .file = .name, .data = line[word.len + 1 ..] }
else if (std.mem.eql(u8, line, "clean"))
.{ .file = .dirty, .data = "0" }
else if (std.mem.eql(u8, line, "dirty"))
@@ -983,6 +1009,7 @@ fn get(p: *Pardes, pane: *Pane, failed: *anyerror) u16 {
panes.File.setContent(p, f, bytes);
f.saved_revision = f.revision;
f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ f.saved_len = f.content.len;
f.disk_newer = null;
f.disk_gone = false;
if (discarded) {
@@ -1007,6 +1034,21 @@ const root_status = @intFromEnum(tree.TopFile.status);
const root_look = @intFromEnum(tree.TopFile.look);
const root_exec = @intFromEnum(tree.TopFile.exec);
+test "a control character in a write to any ctl refuses the whole write, and a quoted line shows a tab as a blank" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const col = layout.columnSerial(p, 0);
+ for ([_]u64{ @intFromEnum(tree.TopFile.ctl), Node.of(serialOf(p), .ctl), Node.ofCol(col, .ctl) }) |node| {
+ const r = wr(p, node, "Wrap off\nbo\x01gus\n");
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expectEqualStrings(e_control, r.reply.ename);
+ }
+ const quoted = wr(p, Node.of(serialOf(p), .ctl), "bogus\tword\n");
+ try testing.expectEqual(E.INVAL, quoted.errno());
+ try testing.expect(std.mem.indexOf(u8, quoted.reply.ename, "bogus word") != null);
+ try testing.expect(std.mem.indexOfScalar(u8, quoted.reply.ename, '\t') == null);
+}
+
test "pane ctl read is acme's fields -- index's five, width in cells, font, tab width, undo, redo -- then whether it is current" {
const gpa = testing.allocator;
const p = try withFile(gpa, "x\n");
@@ -2403,11 +2445,33 @@ 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 "the ctl word name refuses a leading blank, as the name file does" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const r = wr(p, Node.of(serial, .ctl), "name /tmp/lead.txt\n");
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expectEqualStrings("bad character in file name: a blank at its start", r.reply.ename);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "name /tmp/ok.txt\n").reply.status);
+ try testing.expectEqualStrings("/tmp/ok.txt", pane_files.nameOf(p, p.panes[0].?));
+}
+
+test "a builtin that takes no argument, written with one to an exec, is refused wrong #args, as at a ctl" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ for ([_]u64{ @intFromEnum(tree.TopFile.exec), Node.of(serial, .exec) }) |node| for ([_][]const u8{ "Config extra\n", "DumpConfig extra\n" }) |line| {
+ const r = wr(p, node, line);
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expectStringStartsWith(r.reply.ename, "wrong #args in control message");
+ };
+}
+
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 ([_][]const u8{ "Snarf\n", "Putall\n", "Zerox\n", "Get\n", "Tab\n", "Indent\n", "Local\n", "Incl\n", "Abort\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());