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/cols.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/cols.zig')
| -rw-r--r-- | src/ninep/cols.zig | 60 |
1 files changed, 59 insertions, 1 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index cc583c93..c049e0f3 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -7,6 +7,7 @@ const std = @import("std"); const pardes = @import("../pardes.zig"); const Pardes = pardes.Pardes; const tree = @import("tree.zig"); +const limits = @import("../memory.zig").limits; const tagline = pardes.tagline; const layout = pardes.layout; const Text = pardes.panes.Text; @@ -65,6 +66,7 @@ pub fn headerText(p: *Pardes, serial: ?u32) ?[]const u8 { } pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply { + applyTruncation(p, serial); const text = headerText(p, serial) orelse return Reply.fail(req.tag, E.NOENT); const out = p.fs.stage(p.gpa); out.appendSlice(p.gpa, text) catch return Reply.fail(req.tag, E.NOMEM); @@ -78,7 +80,19 @@ pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply { pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply { const t = (header(p, serial) orelse return Reply.fail(req.tag, E.NOENT)).text; if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; - if (pardes.ctlfs.pane.tagFault(req.data)) |why| return tree.failText(req.tag, E.INVAL, why); + const pending = p.fs.header_trunc_pending == rewriteKey(serial); + // The same checks as a pane tag's, whole or not at all; a refused + // write drops the truncation it would have come with. + if (pardes.ctlfs.pane.tagFault(req.data)) |why| { + if (pending) p.fs.header_trunc_pending = null; + return tree.failText(req.tag, E.INVAL, why); + } + const had = if (pending) 0 else headerText(p, serial).?.len; + if (req.data.len > limits.max_tag_tail -| had) { + if (pending) p.fs.header_trunc_pending = null; + return tree.failText(req.tag, E.NOSPC, pardes.ctlfs.pane.e_tag_over); + } + applyTruncation(p, serial); const was = headerText(p, serial).?; var data = req.data; const rewriting = p.fs.header_rewrite == rewriteKey(serial); @@ -99,6 +113,20 @@ pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply { /// Truncating clears it, as a pane tag's `cleartag`: its default words go /// too, and `u` in it brings them back. pub fn truncate(p: *Pardes, serial: ?u32) tree.Status { + if (header(p, serial) == null) return .err; + // Done with the write after it, which may yet be refused. + p.fs.header_trunc_pending = rewriteKey(serial); + return .ok; +} + +/// A pending truncation of this header, done. +pub fn applyTruncation(p: *Pardes, serial: ?u32) void { + if (p.fs.header_trunc_pending != rewriteKey(serial)) return; + p.fs.header_trunc_pending = null; + _ = clear(p, serial); +} + +fn clear(p: *Pardes, serial: ?u32) tree.Status { const t = (header(p, serial) orelse return .err).text; const empty = p.gpa.alloc(u8, 0) catch return .err; t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null); @@ -118,6 +146,7 @@ fn rewriteKey(serial: ?u32) u32 { } pub fn released(p: *Pardes, serial: ?u32) void { + applyTruncation(p, serial); if (p.fs.header_rewrite != rewriteKey(serial)) return; p.fs.header_rewrite = null; p.fs.header_held = false; @@ -356,6 +385,35 @@ test "a focus write makes its pane's column the active one: layout says so and p } } +test "a refused write after a truncation leaves every kind of tag as it was, and Dump still works" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = th.serialOf(p); + const col = layout.columnSerial(p, 0); + const big = "w" ** (limits.max_tag_tail + 1); + // Each tag: a truncating open, then a write too long: nothing changes. + for ([_]u64{ Node.of(serial, .tag), Node.ofCol(col, .tag), @intFromEnum(tree.TopFile.tag) }) |node| { + _ = th.wr(p, node, " Mine"); + const before = try testing.allocator.dupe(u8, th.rd(p, node, 0, 1 << 16).bytes); + defer testing.allocator.free(before); + _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = node, .truncate = true }); + const h = th.call(p, .{ .tag = 2, .op = .open, .node = node, .omode = 1 }).reply.handle; + const r = th.call(p, .{ .tag = 3, .op = .write, .node = node, .handle = h, .data = big }); + try testing.expectEqual(E.NOSPC, r.errno()); + try testing.expectEqualStrings("tag: no space: over 4096 bytes", r.reply.ename); + _ = th.call(p, .{ .tag = 4, .op = .release, .node = node, .handle = h, .opened = true }); + try testing.expectEqualStrings(before, th.rd(p, node, 0, 1 << 16).bytes); + // A control character is refused the same way on every kind. + try testing.expectEqual(E.INVAL, th.wr(p, node, "a\x01b").errno()); + // A truncation with no write after it clears at the close. + _ = th.call(p, .{ .tag = 5, .op = .setattr, .node = node, .truncate = true }); + const e = th.call(p, .{ .tag = 6, .op = .open, .node = node, .omode = 1 }).reply.handle; + _ = th.call(p, .{ .tag = 7, .op = .release, .node = node, .handle = e, .opened = true }); + try testing.expect(std.mem.indexOf(u8, th.rd(p, node, 0, 1 << 16).bytes, "Mine") == null); + } + try pardes.dump.dumpState(p); +} + test "a column's ctl and exec act on it as its tag would, and rmdir closes it only empty" { const p = try th.withFile(testing.allocator, "x\n"); defer p.deinit(); |
