diff options
Diffstat (limited to 'src/ninep/events.zig')
| -rw-r--r-- | src/ninep/events.zig | 115 |
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(); |
