diff options
Diffstat (limited to 'src/ninep/cols.zig')
| -rw-r--r-- | src/ninep/cols.zig | 89 |
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" { |
