From b85f9e56fa04291fc1791bdceae7b0f2c284114c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 04:20:34 -0300 Subject: A column's ctl refuses as the other ctls do, and a bare answer says what it takes A column's ctl refused an unknown word in words 9ns maps to EIO, and took New extra as New. It now refuses the word as unknown control message (EINVAL, as the root and pane ctls) and any word but Tty with an argument as wrong #args. A bare answer on a pane's ctl says answer takes a choice or -, not unknown control message. Co-Authored-By: Claude Opus 5.5 --- src/ninep/cols.zig | 24 ++++++++++++++++++++++-- src/ninep/ctl.zig | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index ef2e6cbd..ebe3279a 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -136,7 +136,12 @@ pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply { const takes = for ([_][]const u8{ "Delcol", "Joincol", "New", "Tty" }) |w| { if (std.mem.eql(u8, w, word)) break true; } else false; - if (!takes) return tree.failText(req.tag, E.INVAL, "a column's ctl takes Delcol, Joincol, New or Tty"); + // Plan 9's words for it, so a mount's errno is EINVAL as the root's + // and a pane's ctl give (9ns reads "control message"). + if (!takes) return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "unknown control message \"{s}\": a column's ctl takes Delcol, Joincol, New or Tty", .{line[0..@min(line.len, 64)]}) catch "unknown control message"); + // Only Tty takes a word (its shell); `New extra` is no New. + if (!std.mem.eql(u8, word, "Tty") and word.len != line.len) + return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "wrong #args in control message \"{s}\"", .{line[0..@min(line.len, 64)]}) catch "wrong #args in control message"); const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT); p.exec_column = col; defer p.exec_column = null; @@ -272,7 +277,7 @@ test "a column's ctl and exec act on it as its tag would, and rmdir closes it on try testing.expectEqual(@as(usize, 1), p.col_n[1]); try testing.expectEqual(E.INVAL, th.wr(p, Node.ofCol(empty, .ctl), "Exit\n").errno()); var rec: [64]u8 = undefined; - try testing.expect(th.logHas(p, try std.fmt.bufPrint(&rec, "err - col/{d}/ctl: a column's ctl takes", .{empty}))); + try testing.expect(th.logHas(p, try std.fmt.bufPrint(&rec, "err - col/{d}/ctl: unknown control message \"Exit\"", .{empty}))); // rmdir refuses a column with a pane, and says why. const full = th.call(p, .{ .tag = 1, .op = .release, .node = Node.ofCol(empty, .dir), .remove = true }); try testing.expectEqual(E.NOTEMPTY, full.errno()); @@ -356,3 +361,18 @@ test "a command run from a column's tag gets its pane in that column, not the la try testing.expect(other != made); try testing.expectEqual(@as(usize, 1), layout.findPane(p, other).?.col); } + +test "a column's ctl refuses an unknown word and an argument as the other ctls do" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const ctl_node = Node.ofCol(layout.columnSerial(p, 0), .ctl); + const unknown = th.wr(p, ctl_node, "Bogus\n"); + try testing.expectEqual(E.INVAL, unknown.errno()); + try testing.expect(std.mem.startsWith(u8, unknown.reply.ename, "unknown control message \"Bogus\"")); + const extra = th.wr(p, ctl_node, "New extra\n"); + try testing.expectEqual(E.INVAL, extra.errno()); + try testing.expect(std.mem.startsWith(u8, extra.reply.ename, "wrong #args")); + try testing.expectEqual(@as(usize, 1), p.col_n[0]); + // A pane's bare answer and a winsize of no size say what they take. + try testing.expectEqualStrings("answer takes a choice or -", th.wr(p, Node.of(p.panes[0].?.serial, .ctl), "answer\n").reply.ename); +} diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 1b1288a1..424245eb 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -660,6 +660,8 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { if (!held) return tree.failText(req.tag, E.INVAL, e_not_locked); held = false; if (apply) pane.fs.lock = null; + } else if (std.mem.eql(u8, line, "answer")) { + return tree.failText(req.tag, E.INVAL, "answer takes a choice or -"); } else if (std.mem.startsWith(u8, line, "answer ")) { // The pane's question (its `ask` in the log), answered. const choice = std.mem.trim(u8, line["answer ".len..], " "); -- cgit v1.3