summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 11:03:10 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit67be3b6594a60d36723000a1a33a09016b29ff97 (patch)
tree3260b872643bf98f001e59038e5c55e06268b229 /src/ninep/ctl.zig
parenta96c7e873d26686f0e49cf2882c1336046396d37 (diff)
downloadpardes-67be3b6594a60d36723000a1a33a09016b29ff97.tar.gz
pardes-67be3b6594a60d36723000a1a33a09016b29ff97.zip
A control character in a write to any ctl file refuses the whole write, and a refused line is quoted with its control bytes shown as blanks
fs.md already promised that the whole of a write to /ctl, a pane's ctl and a column's ctl is checked first, as it is for exec and look. But a ctl write ran its lines one by one, so a line holding a control byte gave that line's own, misleading reason ("unknown command"). Worse, the reason quoted the raw byte back into the err record. The check now happens in tree.write before anything runs, with the same EINVAL and reason exec gives. A refusal's quoted line shows control bytes as blanks, so an err record never carries a raw escape. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig29
1 files changed, 27 insertions, 2 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 05f93ac5..d6230308 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -372,17 +372,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
@@ -1008,6 +1018,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");