diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 10:58:24 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | a96c7e873d26686f0e49cf2882c1336046396d37 (patch) | |
| tree | a1771b70cdf30c449a0f025ea45ccf141ce46ced /src/ninep/pane.zig | |
| parent | 048e81e3471c73b9ac690901819976f689f6a4ce (diff) | |
| download | pardes-a96c7e873d26686f0e49cf2882c1336046396d37.tar.gz pardes-a96c7e873d26686f0e49cf2882c1336046396d37.zip | |
Pane, column and workspace tags take one set of write checks, and a refused `>` write leaves the tag as it was
A column's or the workspace's tag took any length, so a write past 4096 bytes went in and a later Dump failed on it (bad dump tag), and a truncating open wiped any tag before the write after it could be refused. The column and workspace tags now refuse what a pane tag refuses: the 4096-byte limit (ENOSPC, tag: no space: over 4096 bytes) as well as the control characters they already did. A truncation of any of the three is held until the write after it is known to fit, and done with it; a refused write drops it; a close or read with no write after it does it then (so `: > tag` still clears). A test runs a truncating write too long, a control character and a bare truncation at each kind, and Dumps after. docs/fs.md says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/pane.zig')
| -rw-r--r-- | src/ninep/pane.zig | 72 |
1 files changed, 50 insertions, 22 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index af8bb52c..eb317655 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -77,6 +77,10 @@ pub const State = struct { /// whole of it is dropped and draws no empty row. tag_rewrite: bool = false, tag_held_newline: bool = false, + /// A truncation of `tag` not yet done: the write after it does it, once + /// the write is known to fit, so a refused `>` leaves the tag as it was; + /// a read or a close with no write does it then. + tag_trunc_pending: bool = false, /// The pty's size as pty/ctl's winsize last set it, until the pane's own /// grid resizes and gives the pty its size again. winsize: ?[2]u16 = null, @@ -396,6 +400,7 @@ pub fn read(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply }, .body => readBody(p, req, id, pane), .tag => tag: { + applyTagTruncation(p, pane); const out = p.fs.stage(p.gpa); out.appendSlice(p.gpa, tagOf(p, pane)) catch {}; break :tag tree.stagedReply(p, req); @@ -516,7 +521,7 @@ pub fn write(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply return switch (file) { .name => writeName(p, req, id, pane), .body => writeBody(p, req, id, pane), - .tag => writeTag(req, pane), + .tag => writeTag(p, req, pane), .ctl => ctl.writePane(p, req, pane), .addr, .dot, .limit => writeRange(req, pane, file), .data, .xdata => writeData(p, req, pane), @@ -630,16 +635,28 @@ pub fn tagFault(data: []const u8) ?[]const u8 { return null; } -fn writeTag(req: Req, pane: *Pane) Reply { +/// A tag write's limit, the same for a pane's, a column's and the +/// workspace's: whole or not at all. +pub const e_tag_over = std.fmt.comptimePrint("tag: no space: over {d} bytes", .{limits.max_tag_tail}); + +fn writeTag(p: *Pardes, req: Req, pane: *Pane) Reply { if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; - if (tagFault(req.data)) |why| return tree.failText(req.tag, E.INVAL, why); + const pf = &pane.fs; + // A refused write after a truncation changes nothing: the truncation + // it would have come with is dropped too. + if (tagFault(req.data)) |why| { + pf.tag_trunc_pending = false; + return tree.failText(req.tag, E.INVAL, why); + } // The tag's own text grows by what is written, newlines and all: a tag // is a text like any other (acme's tag file appends the same way). - const pf = &pane.fs; - 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: no space: over {d} bytes", .{limits.max_tag_tail})); + const had = if (pf.tag_trunc_pending) 0 else tagline.curTail(pane).len; + const room = limits.max_tag_tail -| had -| @intFromBool(pf.tag_held_newline and !pf.tag_trunc_pending); + if (req.data.len > room) { + pf.tag_trunc_pending = false; + return tree.failText(req.tag, E.NOSPC, e_tag_over); + } + applyTagTruncation(p, pane); 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. @@ -656,6 +673,26 @@ fn writeTag(req: Req, pane: *Pane) Reply { return .{ .tag = req.tag, .written = @intCast(take) }; } +/// A pending truncation of the tag, done: its text cleared as `cleartag` +/// clears it, the prefix left. +pub fn applyTagTruncation(p: *Pardes, pane: *Pane) void { + const pf = &pane.fs; + if (!pf.tag_trunc_pending) return; + pf.tag_trunc_pending = false; + const empty = p.gpa.alloc(u8, 0) catch return; + // An edit like a keyboard one: `u` in the tag brings it back. + pane.tag.remember(p.gpa, if (pane.tag.own) |own| .{ .text = own } else null); + if (pane.tag.own) |own| p.gpa.free(own); + pane.tag.own = empty; + // what is left is the prefix; the cursor goes after it + pane.tag.cur_row = 0; + pane.tag.cur_col = if (tagline.pathPrefix(p.scratch.allocator(), pane)) |prefix| @intCast(prefix.len) else |_| 0; + pane.tag.vsel.active = false; + pane.tag.nsel = 0; + pf.tag_rewrite = true; + pf.tag_held_newline = false; +} + /// A write only text takes, to a pane with none: said, as every EINVAL is. pub const e_no_text = "invalid write: this pane has no text (a terminal, an image or a PDF)"; @@ -923,20 +960,8 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { setDot(pane, .{ .q0 = shiftOne(before.q0, q0, q1 - q0, 0), .q1 = shiftOne(before.q1, q0, q1 - q0, 0) }); pf.addr = .{ .q0 = q0, .q1 = q0 }; }, - .tag => { - const empty = p.gpa.alloc(u8, 0) catch return .err; - // An edit like a keyboard one: `u` in the tag brings it back. - pane.tag.remember(p.gpa, if (pane.tag.own) |own| .{ .text = own } else null); - if (pane.tag.own) |own| p.gpa.free(own); - pane.tag.own = empty; - // what is left is the prefix; the cursor goes after it - pane.tag.cur_row = 0; - pane.tag.cur_col = if (tagline.pathPrefix(p.scratch.allocator(), pane)) |prefix| @intCast(prefix.len) else |_| 0; - pane.tag.vsel.active = false; - pane.tag.nsel = 0; - pf.tag_rewrite = true; - pf.tag_held_newline = false; - }, + // Done with the write after it, which may yet be refused. + .tag => pf.tag_trunc_pending = true, // A shell's `>` truncates before it writes: the address written // is evaluated from where the last one left off, as with `>>`. // `0` (or `,`) is how to start over. See State.addr. @@ -1397,12 +1422,15 @@ test "truncating the tag clears its editable tail" { try testing.expect(tagline.curTail(pane).len > 0); const cleared = call(p, .{ .tag = 1, .op = .setattr, .node = tag, .truncate = true }); try testing.expectEqual(Status.ok, cleared.reply.status); + // Done at the first read or write after it (or the close). + _ = rd(p, tag, 0, 4096); try testing.expectEqualStrings("", pane.tag.own.?); try testing.expect(std.mem.indexOf(u8, rd(p, tag, 0, 4096).bytes, " Mine") == null); // The tag keeps its undo: undoing in it brings back what was cleared. pardes.edit.doUndo(p, &pane.tag); try testing.expect(std.mem.endsWith(u8, tagline.curTail(pane), " Mine")); _ = call(p, .{ .tag = 2, .op = .setattr, .node = tag, .truncate = true }); + _ = rd(p, tag, 0, 4096); try testing.expectEqualStrings("", pane.tag.own.?); // Written back without a leading space, the text still stands apart |
