diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 23:22:02 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | e3cf9c1ffb54a64ee519f02afec17e6859ef8242 (patch) | |
| tree | 5c719091bd3cb04272f1f6b86d0c13ef218ca0d4 /src/ninep | |
| parent | 9dc93a8d31f08f5aec81e7c076220b598db37cbb (diff) | |
| download | pardes-e3cf9c1ffb54a64ee519f02afec17e6859ef8242.tar.gz pardes-e3cf9c1ffb54a64ee519f02afec17e6859ef8242.zip | |
A ctl refusal quotes the offending word alone, everywhere: wrong #args in control message "Newcol", not "Newcol 2"
Some refusals quoted the whole line, arguments and all ("Newcol 2",
"Verbose maybe", "lock x"), others the word alone ("Theme", "Mount"),
so a script matching one could not match the other. Every ctl
refusal, the root's, a pane's and a column's, now quotes the message's
word alone, and one too long to quote whole gives up its middle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/cols.zig | 4 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 59 | ||||
| -rw-r--r-- | src/ninep/events.zig | 2 |
3 files changed, 41 insertions, 24 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index b2e62d28..e3b456ba 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -167,10 +167,10 @@ pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply { } else false; // 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"); + 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", .{pardes.ctlfs.ctl.firstWord(line)[0..@min(pardes.ctlfs.ctl.firstWord(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"); + return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "wrong #args in control message \"{s}\"", .{pardes.ctlfs.ctl.firstWord(line)[0..@min(pardes.ctlfs.ctl.firstWord(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; diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 4a0f94c2..f8391672 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -422,23 +422,39 @@ const Builtin = builtins.registry.Builtin(); /// Refuses a control message, quoting it the way Plan 9's cmderror does /// (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); - 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); + const word = firstWord(line); + // A word too long to quote whole gives up its middle (fitErr), so the + // refusal keeps both its ends. + var shown: [4096]u8 = undefined; + var whole: [4096 + 320]u8 = undefined; + const full = std.fmt.bufPrint(&whole, "{s} \"{s}\"", .{ why, blanked(word[0..@min(word.len, shown.len)], &shown) }) catch why; + // Into the session's own buffer: `whole` is this frame's. + if (full.len <= p.fs.ename.len) { + @memcpy(p.fs.ename[0..full.len], full); + return tree.failText(req.tag, E.INVAL, p.fs.ename[0..full.len]); + } + return tree.failText(req.tag, E.INVAL, tree.fitErr(full, &p.fs.ename)); +} + +/// The word a refusal quotes: a control message's first, its offending +/// word, never the line with its arguments (one rule everywhere: `wrong +/// #args in control message "Newcol"`). +pub fn firstWord(line: []const u8) []const u8 { + const trimmed = std.mem.trim(u8, line, " \t\r\n"); + return trimmed[0 .. std.mem.indexOfAny(u8, trimmed, " \t") orelse trimmed.len]; } /// `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); 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; + const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\": write it to {s}", .{ why, blanked(firstWord(line)[0..@min(firstWord(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 { +fn blanked(line: []const u8, buf: []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]; @@ -1087,7 +1103,8 @@ test "a control character in a write to any ctl refuses the whole write, and a q } 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); + // The offending word alone is quoted: a tab ends it. + try testing.expect(std.mem.indexOf(u8, quoted.reply.ename, "\"bogus\"") != null); try testing.expect(std.mem.indexOfScalar(u8, quoted.reply.ename, '\t') == null); } @@ -1209,11 +1226,11 @@ test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the r const refused = wr(p, ctl_node, bad); try testing.expectEqual(E.INVAL, refused.errno()); var want: [64]u8 = undefined; - try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "unknown control message \"{s}\"", .{bad}), refused.reply.ename); + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "unknown control message \"{s}\"", .{firstWord(bad)}), refused.reply.ename); } try testing.expectEqualStrings("not a window control message \"Newcol\": write it to /ctl", wr(p, ctl_node, "Newcol").reply.ename); - try testing.expectEqualStrings("not a window control message \"Verbose off\": write it to /ctl", wr(p, ctl_node, "Verbose off").reply.ename); - try testing.expectEqualStrings("wrong #args in control message \"Collapse now\"", wr(p, ctl_node, "Collapse now").reply.ename); + try testing.expectEqualStrings("not a window control message \"Verbose\": write it to /ctl", wr(p, ctl_node, "Verbose off").reply.ename); + try testing.expectEqualStrings("wrong #args in control message \"Collapse\"", wr(p, ctl_node, "Collapse now").reply.ename); // Checked whole first: a bad line anywhere and nothing ran. try testing.expectEqual(E.INVAL, wr(p, ctl_node, "Msg first\nbogus").errno()); try testing.expect(p.panes[0].?.msg_len == 0 or !std.mem.eql(u8, p.panes[0].?.msg[0..p.panes[0].?.msg_len], "first")); @@ -1265,18 +1282,18 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi try testing.expectEqual(columns + 1, p.ncol); try testing.expectEqualStrings("not a session control message \"Del\": write it to pane/<n>/ctl", wr(p, root_ctl, "Del").reply.ename); - try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename); - try testing.expectEqualStrings("bad value in control message; takes on, off \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename); - try testing.expectEqualStrings("bad value in control message; takes acme, pardes \"Placement east\"", wr(p, root_ctl, "Placement east").reply.ename); + try testing.expectEqualStrings("unknown control message \"Nonsense\"", wr(p, root_ctl, "Nonsense 1").reply.ename); + try testing.expectEqualStrings("bad value in control message; takes on, off \"Verbose\"", wr(p, root_ctl, "Verbose maybe").reply.ename); + try testing.expectEqualStrings("bad value in control message; takes acme, pardes \"Placement\"", wr(p, root_ctl, "Placement east").reply.ename); // A number's refusal names its range, a path's what it takes. - try testing.expectEqualStrings("bad value in control message; takes 0-100 (a percentage) \"InactiveDim 200\"", wr(p, root_ctl, "InactiveDim 200").reply.ename); - try testing.expectEqualStrings("bad value in control message; takes 0-60000 (milliseconds) \"MessageLinger x\"", wr(p, root_ctl, "MessageLinger x").reply.ename); + try testing.expectEqualStrings("bad value in control message; takes 0-100 (a percentage) \"InactiveDim\"", wr(p, root_ctl, "InactiveDim 200").reply.ename); + try testing.expectEqualStrings("bad value in control message; takes 0-60000 (milliseconds) \"MessageLinger\"", wr(p, root_ctl, "MessageLinger x").reply.ename); for (config.Runtime.settings) |setting| if (setting.action != .theme) try testing.expect(config.Runtime.takes(setting.action) != null); const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename; try testing.expectStringStartsWith(no_theme, "bad value in control message; like it: n"); - try testing.expect(std.mem.endsWith(u8, no_theme, "; Themes lists all \"Theme no-such-theme\"")); - try testing.expectEqualStrings("wrong #args in control message \"Newcol 2\"", wr(p, root_ctl, "Newcol 2").reply.ename); + try testing.expect(std.mem.endsWith(u8, no_theme, "; Themes lists all \"Theme\"")); + try testing.expectEqualStrings("wrong #args in control message \"Newcol\"", wr(p, root_ctl, "Newcol 2").reply.ename); try testing.expectEqualStrings("wrong #args in control message \"Theme\"", wr(p, root_ctl, "Theme").reply.ename); // A bare required word fails the check, so the line before never runs. try testing.expectEqual(E.INVAL, wr(p, root_ctl, "Verbose on\nMount").errno()); @@ -1989,7 +2006,7 @@ test "Joincol with no column to the right and Theme with no such theme say so" { try testing.expectEqual(E.INVAL, themed.errno()); try testing.expectStringStartsWith(themed.reply.ename, "bad value in control message; like it: "); try testing.expect(std.mem.indexOf(u8, themed.reply.ename, ": dr") != null); - try testing.expect(std.mem.endsWith(u8, themed.reply.ename, "\"Theme drak\"")); + try testing.expect(std.mem.endsWith(u8, themed.reply.ename, "\"Theme\"")); // A click on the word says it on the message row. _ = wr(p, Node.of(serialOf(p), .exec), "Theme drak\n"); const pane = p.panes[p.active].?; @@ -2127,8 +2144,8 @@ test "a setting this frontend cannot show says GUI-only, and DumpDir reads back defer p.deinit(); const root_ctl = @intFromEnum(tree.TopFile.ctl); if (!builtins.capabilities.lift) { - try testing.expectEqualStrings("Lift is GUI-only, invalid here \"Lift on\"", wr(p, root_ctl, "Lift on\n").reply.ename); - try testing.expectEqualStrings("GripWidth is GUI-only, invalid here \"GripWidth 120\"", wr(p, root_ctl, "GripWidth 120\n").reply.ename); + try testing.expectEqualStrings("Lift is GUI-only, invalid here \"Lift\"", wr(p, root_ctl, "Lift on\n").reply.ename); + try testing.expectEqualStrings("GripWidth is GUI-only, invalid here \"GripWidth\"", wr(p, root_ctl, "GripWidth 120\n").reply.ename); // A builtin the GUI alone has, written to a ctl or an exec, never // runs as a shell command. if (comptime !pardes.font_picker) { @@ -2138,7 +2155,7 @@ test "a setting this frontend cannot show says GUI-only, and DumpDir reads back try testing.expectEqualStrings("Fonts is GUI-only, invalid here", ran.reply.ename); } } - try testing.expectEqualStrings("unknown control message \"Liftt on\"", wr(p, root_ctl, "Liftt on\n").reply.ename); + try testing.expectEqualStrings("unknown control message \"Liftt\"", wr(p, root_ctl, "Liftt on\n").reply.ename); var buf: [900]u8 = undefined; if (pardes.dump.directory(&buf, "")) |dir| { var want: [920]u8 = undefined; diff --git a/src/ninep/events.zig b/src/ninep/events.zig index f5631adf..c0b52d03 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -1417,7 +1417,7 @@ test "a record is one line of UTF-8: DEL and C1 are spaces, bytes not UTF-8 are test "a long err record keeps its end, its middle given up to an ellipsis" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); - _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus " ++ "abcdefgh " ** 40 ++ "\n"); + _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus" ++ "abcdefgh" ** 40 ++ "\n"); const log = @intFromEnum(tree.TopFile.log); const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes; |
