summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 09:13:55 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit36241daca2aaf90cc8933dffd95fc3cd6216e2b7 (patch)
treea24873db3188e64f699b828a764b813f29f02de6 /src/ninep
parenta550753fb60be3cdc040c521ddaab9022f5d3248 (diff)
downloadpardes-36241daca2aaf90cc8933dffd95fc3cd6216e2b7.tar.gz
pardes-36241daca2aaf90cc8933dffd95fc3cd6216e2b7.zip
/index shows names as the log does, one line of UTF-8, and a newline in a name as \n
/index printed a name as it was, so a directory with a newline in it split its row in two, and bytes not UTF-8 went through; the log turned the newline into a space, silently another name. Both now show a name one way: a newline `\n`, other controls, DEL and C1 a space, bytes not UTF-8 `\xNN`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/ctl.zig11
-rw-r--r--src/ninep/events.zig48
-rw-r--r--src/ninep/pane.zig3
-rw-r--r--src/ninep/tree.zig6
4 files changed, 54 insertions, 14 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index ccba342e..db37fa49 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1990,6 +1990,17 @@ test "every EINVAL a write gets says why, in its err record too; DEL is a contro
try testing.expect(th.logHas(p, "an empty name"));
}
+test "/index shows a name as the log does: a newline in it is \\n, controls spaces, bytes not UTF-8 \\xNN" {
+ const p = try th.withTerm(testing.allocator);
+ defer p.deinit();
+ const term = p.paneBySerial(serialOf(p)).?;
+ p.setCwd(term, "/tmp/two\nlines\x7f\xff");
+ const index = rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes;
+ try testing.expect(std.mem.indexOf(u8, index, "/tmp/two\\nlines \\xff ") != null);
+ try testing.expectEqual(@as(usize, 1), std.mem.count(u8, index, "\n"));
+ try testing.expectEqual(@as(u64, index.len), call(p, .{ .tag = 1, .op = .getattr, .node = @intFromEnum(tree.TopFile.index) }).reply.attr.size);
+}
+
test "size is monotonic: growing is never refused, and a size once taken is taken again" {
const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 });
defer p.deinit();
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index f58d970e..45569b22 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -145,9 +145,11 @@ pub fn announce(p: *Pardes) void {
/// Records `<kind> <serial> <name>`.
pub fn noteLog(p: *Pardes, kind: LogKind, pane: *Pane) void {
- var buf: [4096 + 64]u8 = undefined;
- const name = pane_files.nameOf(pane);
- pushLog(p, std.fmt.bufPrint(&buf, "{s} {d} {s}\n", .{ @tagName(kind), pane.serial, name[0..@min(name.len, 4096)] }) catch return);
+ var buf: [4 * 4096 + 64]u8 = undefined;
+ var name_buf: [4 * 4096]u8 = undefined;
+ // As /index shows it: a newline in the name is `\n`.
+ const name = shown(pane_files.nameOf(pane), &name_buf);
+ pushLog(p, std.fmt.bufPrint(&buf, "{s} {d} {s}\n", .{ @tagName(kind), pane.serial, name }) catch return);
}
/// Records `msg <serial> <text>` for what the editor said, `-` for no pane.
@@ -342,16 +344,37 @@ fn followerRead(p: *Pardes, seq: u64) bool {
/// The log is one ring that records whether or not anyone reads it. A record
/// is one line: a newline in a message or a name would read as two records.
-/// A record as one line of text: a control character, DEL or a C1 control
-/// (U+0080-U+009F) is a space each, and a byte that is not UTF-8 is
-/// written `\xNN`, so a reader splitting on newlines and decoding UTF-8
-/// never trips. The record's own newline, last, is kept.
+/// A record as one line of text (shown): the record's own newline, last,
+/// is kept.
fn sanitize(record: []const u8, out: []u8) []u8 {
+ // A newline in a message is a space (its words go on); a name's own
+ // newline is escaped before it gets here (noteLog).
+ const w = shownAs(record[0 .. record.len - 1], out[0 .. out.len - 1], false).len;
+ out[w] = '\n';
+ return out[0 .. w + 1];
+}
+
+/// `text` as one line of UTF-8 a reader can split and decode, as /log and
+/// /index show names: a newline is `\n` (a name with one stays readable as
+/// what it is, not run into the next), any other control character, DEL
+/// or C1 control (U+0080-U+009F) a space, and a byte that is not UTF-8
+/// `\xNN`. `out` of 4 bytes a byte of `text` holds it all.
+pub fn shown(text: []const u8, out: []u8) []u8 {
+ return shownAs(text, out, true);
+}
+
+fn shownAs(text: []const u8, out: []u8, escape_newline: bool) []u8 {
var w: usize = 0;
var i: usize = 0;
- const body = record[0 .. record.len - 1];
- while (i < body.len and w + 4 < out.len) {
+ const body = text;
+ while (i < body.len and w + 4 <= out.len) {
const c = body[i];
+ if (c == '\n' and escape_newline) {
+ @memcpy(out[w..][0..2], "\\n");
+ w += 2;
+ i += 1;
+ continue;
+ }
if (c < ' ' or c == 0x7f) {
out[w] = ' ';
w += 1;
@@ -371,13 +394,12 @@ fn sanitize(record: []const u8, out: []u8) []u8 {
i += 2;
continue;
}
- if (w + n + 1 > out.len) break;
+ if (w + n > out.len) break;
@memcpy(out[w..][0..n], body[i..][0..n]);
w += n;
i += n;
}
- out[w] = '\n';
- return out[0 .. w + 1];
+ return out[0..w];
}
fn pushLog(p: *Pardes, raw: []u8) void {
@@ -1285,6 +1307,8 @@ test "a long msg record is cut between words at its cap, with an ellipsis" {
test "a record is one line of UTF-8: DEL and C1 are spaces, bytes not UTF-8 are escaped" {
var out: [64]u8 = undefined;
+ var name: [64]u8 = undefined;
+ try testing.expectEqualStrings("/tmp/two\\nlines", shown("/tmp/two\nlines", &name));
try testing.expectEqualStrings("msg - a b c \\xff d\n", sanitize("msg - a\x7fb\xc2\x85c \xff d\n", &out));
try testing.expectEqualStrings("msg - caf\xc3\xa9\n", sanitize("msg - caf\xc3\xa9\n", &out));
}
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 4c967fc5..d29c8b49 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -298,7 +298,8 @@ pub fn indexLen(p: *Pardes) u64 {
const pane = p.panes[p.paneBySerial(serial).?].?;
var digits: [16]u8 = undefined;
n += (std.fmt.bufPrint(&digits, "{d}", .{serial}) catch unreachable).len;
- n += 1 + kindOf(pane).len + 3 + nameOf(pane).len + 1;
+ var name_buf: [4 * 4096]u8 = undefined;
+ n += 1 + kindOf(pane).len + 3 + events.shown(nameOf(pane), &name_buf).len + 1;
n += 1 + (std.fmt.bufPrint(&digits, "{d}", .{columnOf(p, p.paneBySerial(serial).?)}) catch unreachable).len;
}
return n;
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 391847f5..c089c31d 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -917,7 +917,11 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply {
last = serial;
const id = p.paneBySerial(serial).?;
const pn = p.panes[id].?;
- out.print(p.gpa, "{d} {s} {d} {s} {d}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), pane.nameOf(pn), pane.columnOf(p, id) }) catch {};
+ // A name as /log shows it: one line of UTF-8, a
+ // newline in it `\n` (events.shown).
+ var name_buf: [4 * 4096]u8 = undefined;
+ const name = events.shown(pane.nameOf(pn), &name_buf);
+ out.print(p.gpa, "{d} {s} {d} {s} {d}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), name, pane.columnOf(p, id) }) catch {};
}
break :index stagedReply(p, req);
},