summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/events.zig6
-rw-r--r--src/ninep/pane.zig6
-rw-r--r--src/ninep/tree.zig99
3 files changed, 107 insertions, 4 deletions
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/pane.zig b/src/ninep/pane.zig
index bb4ebf86..3c1e181e 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -571,7 +571,7 @@ fn writeTag(req: Req, pane: *Pane) Reply {
const had = tagline.curTail(pane).len;
const room = limits.max_tag_tail -| had -| @intFromBool(pf.tag_held_newline);
// Whole or not at all: a write that would pass the limit changes nothing.
- if (req.data.len > room) return tree.failText(req.tag, E.NOSPC, std.fmt.comptimePrint("tag: over {d} bytes", .{limits.max_tag_tail}));
+ if (req.data.len > room) return tree.failText(req.tag, E.NOSPC, std.fmt.comptimePrint("tag: no space: over {d} bytes", .{limits.max_tag_tail}));
const take = wholeUtf8(req.data);
// A truncating write drops ONE trailing newline, its whole text's: a
// newline held from a write before goes in once more text follows it.
@@ -1214,7 +1214,9 @@ test "a tag write past the limit is refused whole, naming the limit" {
const big = "w" ** (limits.max_tag_tail + 10);
const r = wr(p, tag, big);
try testing.expectEqual(E.NOSPC, r.errno());
- try testing.expect(std.mem.startsWith(u8, r.reply.ename, "tag: over "));
+ // `no space` is what 9ns maps to ENOSPC, so a shell through a mount
+ // sees the errno the tree gives.
+ try testing.expect(std.mem.startsWith(u8, r.reply.ename, "tag: no space: over "));
try testing.expectEqualStrings(before, rd(p, tag, 0, 1 << 16).bytes);
}
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 30bf073d..5f3ffc9c 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.
@@ -1578,6 +1628,55 @@ test "editor paths resolve to the same nodes the wire serves" {
try testing.expectEqual(before, p.next_serial);
}
+test "random placements, refused or not, leave no pane under its tag and two rows" {
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "one\ntwo\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var look_line: [4200]u8 = undefined;
+ const looked = try std.fmt.bufPrint(&look_line, "{s}/f.txt\n", .{dir});
+ var seed: u64 = 0;
+ while (seed < 300) : (seed += 1) {
+ var prng = std.Random.DefaultPrng.init(seed);
+ const r = prng.random();
+ const rows = r.intRangeAtMost(u16, 6, 40);
+ const p = try pardes.Pardes.init(testing.allocator, .{ .tty_only = true, .cols = r.intRangeAtMost(u16, 60, 240), .rows = rows });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ _ = try p.setTestFile("x\n");
+ while (p.nextEffect()) |_| {}
+ for (0..r.uintLessThan(usize, 3)) |_| _ = th.wr(p, Node.of(serialOf(p), .exec), "Newcol\n");
+ if (r.boolean()) p.settings.placement = .pardes;
+ for (0..40) |step| {
+ var serials: [pardes.MAX_PANES]u32 = undefined;
+ var n: usize = 0;
+ for (p.panes) |slot| if (slot) |pn| {
+ serials[n] = pn.serial;
+ n += 1;
+ };
+ if (n == 0) break;
+ const at = serials[r.uintLessThan(usize, n)];
+ const action = r.uintLessThan(u8, 8);
+ switch (action) {
+ 0 => _ = th.wr(p, Node.of(at, .exec), "New\n"),
+ 1 => _ = call(p, .{ .tag = 1, .op = .open, .node = @intFromEnum(TopFile.new) }),
+ 2 => _ = th.wr(p, Node.of(at, .exec), "Newcol\n"),
+ 3 => _ = th.wr(p, Node.of(at, .exec), "Edit =\n"),
+ 4 => _ = th.wr(p, Node.of(at, .exec), "Tty\n"),
+ 5 => _ = th.wr(p, Node.of(at, .look), looked),
+ 6 => _ = th.wr(p, Node.of(at, .exec), "Delcol\n"),
+ else => _ = th.wr(p, Node.of(at, .ctl), "delete\n"),
+ }
+ for (0..p.ncol) |c| if (!layout.columnAtMinimums(p, c)) {
+ std.debug.print("seed {d} rows {d} step {d} action {d}: column {d} under its minimums\n", .{ seed, rows, step, action, c });
+ for (p.col_panes[c][0..p.col_n[c]]) |id| std.debug.print(" pane {d} h {d} min {d}\n", .{ id, p.rects[id].h, layout.minRows(p, id) });
+ return error.TestUnexpectedResult;
+ };
+ }
+ }
+}
+
test "at the pane cap, pane/new, look and New each say so, and look reads back empty" {
const p = try th.withFile(testing.allocator, "x\n");
defer p.deinit();