summaryrefslogtreecommitdiff
path: root/src/ninep/cols.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/cols.zig')
-rw-r--r--src/ninep/cols.zig89
1 files changed, 84 insertions, 5 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index b00ecdba..aae6d364 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -78,6 +78,7 @@ pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply {
pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply {
const t = (header(p, serial) orelse return Reply.fail(req.tag, E.NOENT)).text;
if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 };
+ if (pardes.ctlfs.pane.tagFault(req.data)) |why| return tree.failText(req.tag, E.INVAL, why);
const was = headerText(p, serial).?;
var data = req.data;
const rewriting = p.fs.header_rewrite == rewriteKey(serial);
@@ -126,6 +127,8 @@ pub fn released(p: *Pardes, serial: ?u32) void {
/// Delcol, Joincol, New, Tty -- one a line, each run as from that tag. What
/// they say is the session's in the log (`msg -`), not the focused pane's.
pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply {
+ // What this write touched, alone, reads back (runBuiltin notes it).
+ p.fs.results_len = 0;
p.fs.session_write = true;
defer p.fs.session_write = false;
var lines = std.mem.splitScalar(u8, req.data, '\n');
@@ -164,8 +167,14 @@ pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply {
while (lines.next()) |raw| {
const line = std.mem.trim(u8, raw, " \t\r");
if (line.len == 0) continue;
- for (line) |c| if (c < ' ' and c != '\t') return tree.failText(req.tag, E.INVAL, pardes.ctlfs.ctl.e_control);
+ for (line) |c| if ((c < ' ' and c != '\t') or c == 0x7f) return tree.failText(req.tag, E.INVAL, pardes.ctlfs.ctl.e_control);
if (pardes.ctlfs.ctl.tooLong(req, line)) |refusal| return refusal;
+ // A pane's word (Undo, Msg, Save) is no word of a tag no pane owns:
+ // refused as the root's ctl refuses it, never done at whichever
+ // pane has the keyboard. The tag's own words (New, Tty, Find, Grep
+ // in a column's) are its to run.
+ if (paneWord(line, if (serial == null) pardes.config.topbar_str else tagline.column_tag_default))
+ return pardes.ctlfs.ctl.refuseTo(p, req, "not a session control message", line, "pane/<n>/ctl");
const col = if (serial) |s| layout.columnBySerial(p, s) orelse return Reply.fail(req.tag, E.NOENT) else null;
if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT);
p.exec_column = col;
@@ -189,6 +198,75 @@ test "New written to a column's exec makes its scratch in the session's director
try testing.expectEqualStrings("/project/src/+New", p.panes[p.active].?.file.?.path);
}
+test "New written to a column's ctl reads back the new pane alone, as its exec does" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const root_exec = @intFromEnum(tree.TopFile.exec);
+ _ = th.wr(p, root_exec, "Msg before\n");
+ const ctl_node = tree.Node.ofCol(layout.columnSerial(p, 0), .ctl);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, ctl_node, "New\n").reply.status);
+ var want: [16]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{p.panes[p.active].?.serial}), th.rd(p, root_exec, 0, 64).bytes);
+}
+
+/// Whether `line` names a builtin that acts on a pane (every one of pane
+/// scope, Edit too, which the root's ctl takes at the active pane) and is
+/// not one of `own`, the words its tag holds by default: those a tag no
+/// pane owns refuses, never doing them at the pane with the keyboard.
+fn paneWord(line: []const u8, own: []const u8) bool {
+ const cmd = pardes.exec.commandText(line);
+ const word = cmd[0 .. std.mem.indexOfAny(u8, cmd, " \t+") orelse cmd.len];
+ const b = std.meta.stringToEnum(pardes.builtins.registry.Builtin(), word) orelse return false;
+ if (pardes.builtins.registry.scope(b) == .session) return false;
+ var words = std.mem.tokenizeScalar(u8, own, ' ');
+ while (words.next()) |w| if (std.mem.eql(u8, w, word)) return false;
+ return true;
+}
+
+test "a pane's word written to /tagexec or a column's exec is refused, and acts on no pane" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const col_exec = tree.Node.ofCol(layout.columnSerial(p, 0), .exec);
+ for ([_]u64{ @intFromEnum(tree.TopFile.tagexec), col_exec }) |node| {
+ for ([_][]const u8{ "Undo\n", "Msg hi\n", "Redo\n" }) |line| {
+ const refused = th.wr(p, node, line);
+ try testing.expectEqual(E.INVAL, refused.errno());
+ try testing.expectStringStartsWith(refused.reply.ename, "not a session control message");
+ }
+ }
+ // The column tag's own words still run there.
+ try testing.expectEqual(tree.Status.ok, th.wr(p, col_exec, "New\n").reply.status);
+}
+
+test "every pane word a tag no pane owns does not hold is refused there, Edit included, and the pane is untouched" {
+ const p = try th.withFile(testing.allocator, "keep me\n");
+ defer p.deinit();
+ const Builtin = pardes.builtins.registry.Builtin();
+ const col_exec = tree.Node.ofCol(layout.columnSerial(p, 0), .exec);
+ for ([_]struct { node: u64, own: []const u8 }{
+ .{ .node = @intFromEnum(tree.TopFile.tagexec), .own = pardes.config.topbar_str },
+ .{ .node = col_exec, .own = tagline.column_tag_default },
+ }) |tag_file| {
+ var checked: usize = 0;
+ for (std.enums.values(Builtin)) |b| {
+ if (pardes.builtins.registry.scope(b) == .session) continue;
+ var owned = false;
+ var words = std.mem.tokenizeScalar(u8, tag_file.own, ' ');
+ while (words.next()) |w| owned = owned or std.mem.eql(u8, w, @tagName(b));
+ if (owned) continue;
+ var line: [64]u8 = undefined;
+ const text = try std.fmt.bufPrint(&line, "{s}{s}\n", .{ @tagName(b), if (b == .Edit) " ,d" else if (pardes.builtins.registry.requiresArg(b)) " x" else "" });
+ const refused = th.wr(p, tag_file.node, text);
+ try testing.expectEqual(E.INVAL, refused.errno());
+ try testing.expectStringStartsWith(refused.reply.ename, "not a session control message");
+ checked += 1;
+ }
+ try testing.expect(checked > 20);
+ }
+ // `Edit ,d` wiped the pane with the keyboard: nothing touched it.
+ try testing.expectEqualStrings("keep me\n", p.panes[0].?.file.?.content);
+}
+
/// rmdir of a column closes it when it is empty; one with panes is
/// refused, saying so (close them, or Delcol on its ctl, which asks first).
pub fn remove(p: *Pardes, req: Req, serial: u32, file: tree.ColFile) Reply {
@@ -323,8 +401,10 @@ test "tagexec runs a word as a click in the workspace tag; exec files read back
try testing.expectEqual(tree.Status.ok, th.look_up(p, tree.root, "tagexec").reply.status);
try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, "Newcol\n").reply.status);
try testing.expectEqual(@as(usize, 2), p.ncol);
- try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, "Msg from the top\n").reply.status);
- try testing.expect(th.logHas(p, "msg - from the top\n"));
+ // What it says is the session's (Newcol announcing itself); a pane's
+ // word such as Msg is refused there.
+ try testing.expect(th.logHas(p, "msg - Newcol\n"));
+ try testing.expectEqual(E.INVAL, th.wr(p, tagexec, "Msg from the top\n").errno());
// A column's exec makes a pane there, reads it back, and its Msg is
// the session's too.
const right = layout.columnSerial(p, 1);
@@ -333,8 +413,7 @@ test "tagexec runs a word as a click in the workspace tag; exec files read back
var want: [16]u8 = undefined;
try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), th.rd(p, Node.ofCol(right, .exec), 0, 64).bytes);
try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), th.rd(p, tagexec, 0, 64).bytes);
- try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(right, .exec), "Msg from a column\n").reply.status);
- try testing.expect(th.logHas(p, "msg - from a column\n"));
+ try testing.expect(th.logHas(p, "msg - New\n"));
}
test "a word run from the workspace's or a column's tag runs in the session's directory, a pane's in its own" {