diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 19:57:19 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | d0efcd6d63d8e918d66a1c9493870e78586e9567 (patch) | |
| tree | 4c8a253bcb61c1227cd44605e46dcdc2e274b0ab /src | |
| parent | 4442bf09a377297a33b91066bfb5dd8eedb964f0 (diff) | |
| download | pardes-d0efcd6d63d8e918d66a1c9493870e78586e9567.tar.gz pardes-d0efcd6d63d8e918d66a1c9493870e78586e9567.zip | |
A 9P error past 128 bytes keeps its reason: the path in it gives up its middle to …
An Rerror carries 128 bytes (Plan 9's ERRMAX) and the engine cut longer text there, and a builtin's failure was first clipped between words to 256 with ...: either way the end, which says why, was what went. tree.handle now fits a long reason by cutting the middle of its longest path, else of the whole text, on a character boundary, and reportFailure fills its buffer the same way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/Messages.zig | 14 | ||||
| -rw-r--r-- | src/builtins.zig | 3 | ||||
| -rw-r--r-- | src/ninep/events.zig | 6 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 50 |
4 files changed, 61 insertions, 12 deletions
diff --git a/src/Messages.zig b/src/Messages.zig index 7342f726..42200863 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -503,18 +503,14 @@ pub fn clip(text: []const u8, max: usize) []const u8 { /// reportError with the words already chosen. pub fn reportFailure(p: *Pardes, id: usize, text: []const u8) void { p.fs.failures +%= 1; - // A builtin a ctl write runs: its first error is also the write's, cut - // between words. + // A builtin a ctl write runs: its first error is also the write's, its + // path shortened in the middle if it must be, never its reason. const failing_write = p.fs.no_prompt or p.fs.capturing or p.fs.write_waits; if (failing_write and p.fs.failure_len == 0) { - const kept = if (text.len > p.fs.failure.len) clip(text, p.fs.failure.len - 3) else text; + var room: @TypeOf(p.fs.failure) = undefined; + const kept = pardes.ctlfs.fitErr(text, &room); @memcpy(p.fs.failure[0..kept.len], kept); - var n = kept.len; - if (kept.len < text.len) { - @memcpy(p.fs.failure[n..][0..3], "..."); - n += 3; - } - p.fs.failure_len = @intCast(n); + p.fs.failure_len = @intCast(kept.len); } // The write fails with it, and its err record says it: no msg for it, // so the same failure again is the same record again, counted diff --git a/src/builtins.zig b/src/builtins.zig index e47afeb4..8a01b3d6 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -574,7 +574,8 @@ fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool { std.fmt.bufPrint(&said_buf, "{s}: Modified ({s} again to discard)", .{ one, @tagName(asking) }) else std.fmt.bufPrint(&said_buf, "{d} unsaved panes: Modified ({s} again to discard)", .{ count, @tagName(asking) })) catch "unsaved panes: Modified"; - const kept = @import("Messages.zig").clip(said, c.p.fs.failure.len); + var room: @TypeOf(c.p.fs.failure) = undefined; + const kept = pardes.ctlfs.fitErr(said, &room); @memcpy(c.p.fs.failure[0..kept.len], kept); c.p.fs.failure_len = @intCast(kept.len); } diff --git a/src/ninep/events.zig b/src/ninep/events.zig index c0bf4a9a..fe7af77e 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -1289,7 +1289,7 @@ test "a record is one line of UTF-8: DEL and C1 are spaces, bytes not UTF-8 are 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" { +test "a long err record keeps its end, its middle given up to an ellipsis" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus " ++ "abcdefgh " ** 40 ++ "\n"); @@ -1298,7 +1298,9 @@ test "a long err record is cut between words, with an ellipsis" { const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes; const at = std.mem.lastIndexOf(u8, text, "err - ctl: ").?; const record = text[at .. std.mem.indexOfScalarPos(u8, text, at, '\n').? + 1]; - try testing.expect(std.mem.endsWith(u8, record, "abcdefgh…\n")); + try testing.expect(std.mem.indexOf(u8, record, "…") != null); + try testing.expect(std.mem.startsWith(u8, record, "err - ctl: ")); + try testing.expect(std.mem.endsWith(u8, record, "abcdefgh\"\n") or std.mem.endsWith(u8, record, "abcdefgh \"\n")); _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); } diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 30bf073d..f6cb115a 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -456,9 +456,59 @@ pub fn stagedReply(p: *Pardes, req: Req) Reply { pub fn handle(p: *Pardes, req: Req) Reply { var reply = serve(p, req); if (reply.status == .err and reply.ename.len == 0) reply.ename = errWords(reply.errno); + if (reply.status == .err and reply.ename.len > cloud9.fs.errmax) + if (p.scratch.allocator().alloc(u8, cloud9.fs.errmax)) |room| { + reply.ename = fitErr(reply.ename, room); + } else |_| {}; return reply; } +/// A reason longer than an Rerror carries (Plan 9's ERRMAX, 128 bytes) +/// keeps its end, which says why: the longest path in it -- else the whole +/// text -- gives up its middle to `…`. +pub fn fitErr(text: []const u8, out: []u8) []const u8 { + const ell = "…"; + if (text.len <= out.len) return text; + const over = text.len - out.len + ell.len; + var from: usize = 0; + var to: usize = text.len; + var words = std.mem.tokenizeAny(u8, text, " \t"); + var longest: usize = 0; + while (words.next()) |w| if (std.mem.indexOfScalar(u8, w, '/') != null and w.len > longest and w.len > over + 2) { + longest = w.len; + from = @intFromPtr(w.ptr) - @intFromPtr(text.ptr); + to = from + w.len; + }; + var a = from + (to - from - over) / 2; + var b = a + over; + while (a > from and text[a] & 0xC0 == 0x80) a -= 1; + while (b < to and text[b] & 0xC0 == 0x80) b += 1; + return std.fmt.bufPrint(out, "{s}" ++ ell ++ "{s}", .{ text[0..a], text[b..] }) catch text[0..out.len]; +} + +test "a reason past 128 bytes keeps its end: the path in it gives up its middle" { + var out: [128]u8 = undefined; + const long = "Save /home/someone/projects/" ++ "deep/" ** 30 ++ "file.txt: no such directory"; + const fit = fitErr(long, &out); + try testing.expect(fit.len <= 128); + try testing.expect(std.mem.startsWith(u8, fit, "Save /home/someone/")); + try testing.expect(std.mem.endsWith(u8, fit, "file.txt: no such directory")); + try testing.expect(std.mem.indexOf(u8, fit, "…") != null); + try testing.expect(std.unicode.utf8ValidateSlice(fit)); + // No path: the whole text's middle goes, the end stays. + const words = "why " ** 40 ++ "the reason"; + try testing.expect(std.mem.endsWith(u8, fitErr(words, &out), "the reason")); + try testing.expectEqualStrings("short", fitErr("short", &out)); + // Through the tree: a look at a long ./ name not there says why at the end. + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + var line: [512]u8 = undefined; + const r = th.wr(p, Node.of(serialOf(p), .look), try std.fmt.bufPrint(&line, "./{s}x.txt\n", .{"no-such-dir/" ** 20})); + try testing.expectEqual(Status.err, r.reply.status); + try testing.expect(r.reply.ename.len <= 128); + try testing.expect(std.mem.endsWith(u8, r.reply.ename, "no such file") or std.mem.endsWith(u8, r.reply.ename, "no such directory")); +} + /// A refusal with no reason of its own said in Plan 9's words, not the C /// library's (`Operation not permitted`); each maps back to its errno in /// 9ns (enameToErrno), EPERM's to EACCES as Plan 9's does. |
