summaryrefslogtreecommitdiff
path: root/src/ninep/events.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/events.zig')
-rw-r--r--src/ninep/events.zig115
1 files changed, 100 insertions, 15 deletions
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 98799774..690efbe5 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -101,7 +101,7 @@ pub const Queue = struct {
/// One record per read; `.again` holds the read until a record arrives.
pub fn readQueue(p: *Pardes, req: Req, q: *Queue) Reply {
const record = q.peek() orelse return .{ .tag = req.tag, .status = .again };
- if (req.size < record.len) return Reply.fail(req.tag, E.INVAL);
+ if (req.size < record.len) return tree.failText(req.tag, E.INVAL, "invalid read: shorter than the record waiting");
const out = p.fs.stage(p.gpa);
out.appendSlice(p.gpa, record) catch return Reply.fail(req.tag, E.NOMEM);
q.pop();
@@ -140,14 +140,18 @@ pub fn announce(p: *Pardes) void {
if (!pane.fs.unannounced) continue;
pane.fs.unannounced = false;
noteLog(p, .new, pane);
+ // A file opened (a look, a Restore, a boot): first in Recent.
+ if (pane.file) |f| if (f.output == null) p.recent.opened(p.recentGpa(), f.path);
}
}
/// 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.
@@ -214,9 +218,12 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
};
}
var buf: [256]u8 = undefined;
+ // A reason that names its file already (`look: no space...` from a
+ // look) is not prefixed twice: `err 3 look: no space...`.
+ const reason = if (why.len > file.len + 2 and std.ascii.startsWithIgnoreCase(why, file) and std.mem.startsWith(u8, why[file.len..], ": ")) why[file.len + 2 ..] else why;
// A long reason is cut between words, with an ellipsis, never mid-word.
- const kept = pardes.Messages.clip(why, 200);
- const record = std.fmt.bufPrint(&buf, "err {s} {s}: {s}{s}\n", .{ serial, file, kept, if (kept.len < why.len) "…" else "" }) catch return;
+ const kept = pardes.Messages.clip(reason, 200);
+ const record = std.fmt.bufPrint(&buf, "err {s} {s}: {s}{s}\n", .{ serial, file, kept, if (kept.len < reason.len) "…" else "" }) catch return;
for (record[0 .. record.len - 1]) |*c| if (c.* < ' ') {
c.* = ' ';
};
@@ -292,11 +299,13 @@ pub fn dropMessage(p: *Pardes, text: []const u8) void {
/// counts that one instead: `<record> (x2)`, the count being every time it
/// was said. One a follower has already read is not rewritten: the repeat
/// is a new line carrying the running count, `(x3)`.
-fn pushCounting(p: *Pardes, record: []u8) void {
+fn pushCounting(p: *Pardes, raw: []u8) void {
// A client retrying a write that fails the same way would fill the ring
// with one line, so a repeat of the newest record is that record counted,
// `(x3)`, as +Messages counts its repeats; unless a follower has read it
// already and so waits on the repeat as a line of its own.
+ var clean: [4 * 4096 + 256]u8 = undefined;
+ const record = sanitize(raw, &clean);
const last = newest(p) orelse return pushLog(p, record);
var said = last.text;
var times: u32 = 1;
@@ -340,10 +349,75 @@ 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.
-fn pushLog(p: *Pardes, record: []u8) void {
- for (record[0 .. record.len - 1]) |*c| if (c.* < ' ') {
- c.* = ' ';
- };
+/// 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 backslash is `\\`, a newline `\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 = text;
+ while (i < body.len and w + 4 <= out.len) {
+ const c = body[i];
+ // A name's own backslash is `\\`, so `\n` and `\xNN` read back as
+ // the escapes they are, never as a name that held them.
+ if (c == '\\' and escape_newline) {
+ @memcpy(out[w..][0..2], "\\\\");
+ w += 2;
+ i += 1;
+ continue;
+ }
+ 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;
+ i += 1;
+ continue;
+ }
+ const n = std.unicode.utf8ByteSequenceLength(c) catch 0;
+ if (n == 0 or i + n > body.len or !std.unicode.utf8ValidateSlice(body[i .. i + n])) {
+ _ = std.fmt.bufPrint(out[w..], "\\x{x:0>2}", .{c}) catch break;
+ w += 4;
+ i += 1;
+ continue;
+ }
+ if (n == 2 and c == 0xC2 and body[i + 1] <= 0x9F) {
+ out[w] = ' ';
+ w += 1;
+ i += 2;
+ continue;
+ }
+ if (w + n > out.len) break;
+ @memcpy(out[w..][0..n], body[i..][0..n]);
+ w += n;
+ i += n;
+ }
+ return out[0..w];
+}
+
+fn pushLog(p: *Pardes, raw: []u8) void {
+ var clean: [4 * 4096 + 256]u8 = undefined;
+ const record = sanitize(raw, &clean);
// One record larger than the ring would push every other out and then
// not fit itself; cut it to what fits instead, on a character boundary.
var end = @min(record.len, p.fs.log.cap - 4) - 1;
@@ -438,7 +512,7 @@ pub fn writeLog(p: *Pardes, req: Req) Reply {
// what was there and waits for what comes after, as tail -n0 -f does.
if (std.mem.eql(u8, word, "follow new")) {
slot.pos = slot.bytes.len;
- } else if (!std.mem.eql(u8, word, "follow")) return Reply.fail(req.tag, E.INVAL);
+ } else if (!std.mem.eql(u8, word, "follow")) return tree.failText(req.tag, E.INVAL, "invalid write to log: it takes `follow` or `follow new`");
slot.follow = true;
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
@@ -654,14 +728,14 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply {
while (check.next()) |r| {
switch (r.action) {
.body_look, .tag_look, .body_exec, .tag_exec => {},
- else => return tree.failText(req.tag, E.INVAL, tree.e_bad_event),
+ else => return tree.failText(req.tag, E.INVAL, tree.e_bad_event ++ ": only a look or exec (L l X x) is written back"),
}
const n = if (r.action.onTag()) tag.len else body.len;
- if (r.q0 > r.q1) return tree.failText(req.tag, E.INVAL, tree.e_bad_event);
+ if (r.q0 > r.q1) return tree.failText(req.tag, E.INVAL, tree.e_bad_event ++ ": its range ends before it starts");
// Well formed, but not a range of this text: say which.
if (r.q1 > n) return tree.failText(req.tag, E.INVAL, if (r.action.onTag()) "range past end of tag" else "range past end of body");
}
- if (check.i != req.data.len) return tree.failText(req.tag, E.INVAL, tree.e_bad_event);
+ if (check.i != req.data.len) return tree.failText(req.tag, E.INVAL, tree.e_bad_event ++ ": a record is <origin><action><q0> <q1> <flag> <count> <text>, a line each");
}
var run: EventReader = .{ .data = req.data };
while (run.next()) |r| {
@@ -1244,6 +1318,17 @@ test "a long msg record is cut between words at its cap, with an ellipsis" {
_ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h });
}
+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));
+ // A backslash of the name's own is doubled: `\n` in a name differs from
+ // a newline in one.
+ try testing.expectEqualStrings("/tmp/a\\\\nb", shown("/tmp/a\\nb", &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));
+}
+
test "a long err record is cut between words, with an ellipsis" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();