From cb1e82682f90f92db3301192babb2849fa62fdb0 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 16:34:56 -0300 Subject: A tag write past the limit is refused whole, naming it A write to tag longer than the room left was cut and applied in part. It now changes nothing and fails ENOSPC, tag: over 4096 bytes; the docs give the limit. Co-Authored-By: Claude Opus 5.5 --- docs/fs.md | 4 +++- src/ninep/pane.zig | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/fs.md b/docs/fs.md index f3602dfc..f35bb3f6 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -806,7 +806,9 @@ a command pane ends in its own time, told by its `exit` record. page (no mark for unsaved text: the grip shows that, and `dirty` says it), then the text you may edit. An image's tag begins with its mode words, not its path: `img petscii:off palette:commodore ascii:on ` by default, the modes -it is drawn in, then the file. A write appends to that text, +it is drawn in, then the file. The editable text is 4096 bytes at most: a +write that would pass that is refused whole, ENOSPC, `tag: over 4096 +bytes`. A write appends to that text, newlines included, and a tag with more than one line takes a row per line on screen; truncating `tag` clears it, as acme's `cleartag` does -- the default words (`Save Tty Collapse Del` and the rest) with it, since they are that text until diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index ec6e8551..4327c403 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -570,8 +570,9 @@ fn writeTag(req: Req, pane: *Pane) Reply { const pf = &pane.fs; const had = tagline.curTail(pane).len; const room = limits.max_tag_tail -| had -| @intFromBool(pf.tag_held_newline); - if (room == 0) return Reply.fail(req.tag, E.NOSPC); - const take = wholeUtf8(req.data[0..@min(req.data.len, room)]); + // 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})); + 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. if (pf.tag_held_newline) { @@ -1189,6 +1190,19 @@ test "an open's writes in a row are held and go in as one edit, seen by the next try testing.expectEqualStrings("abone\n", rd(p, body, 0, 64).bytes); } +test "a tag write past the limit is refused whole, naming the limit" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const tag = Node.of(serialOf(p), .tag); + const before = try testing.allocator.dupe(u8, rd(p, tag, 0, 1 << 16).bytes); + defer testing.allocator.free(before); + 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 ")); + try testing.expectEqualStrings(before, rd(p, tag, 0, 1 << 16).bytes); +} + test "two opens writing one body are two undo steps, however their writes interleave" { const p = try withFile(testing.allocator, "one\n"); defer p.deinit(); -- cgit v1.3