summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md5
-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
5 files changed, 58 insertions, 15 deletions
diff --git a/docs/fs.md b/docs/fs.md
index ffce848e..8ca23c85 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -100,6 +100,7 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
```
/README this guide, also src/fs-help.txt
/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column serial
+ (the name as the log shows it: one line of UTF-8, a newline in it `\n`)
/status pid, version and pane count
/look write a line: a right click on it at the active pane; read: the serials the last
look, exec or ctl write touched (made, else acted at)
@@ -824,7 +825,9 @@ takes \`follow\` or \`follow new\``, `invalid write: this pane has no
text`), never a bare `Invalid argument`.
Control characters, DEL and C1 controls (U+0080-U+009F) in a record become
spaces, and a byte that is not UTF-8 is written `\xNN`, so a record is one
-line of UTF-8.
+line of UTF-8; a pane's name, in a record and in `/index` alike, shows a
+newline in it as `\n` rather than a space, so a directory named with one
+reads back as what it is.
(An `event` record is not: acme's `<origin><action><q0> <q1> <flag> <n>
<text>\n`, whose text may hold newlines. The origin is `E` (a 9P write to body or tag), `F` (other
files, the editor's own lines), `K` (the keyboard) or `M` (the mouse); the
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);
},