summaryrefslogtreecommitdiff
path: root/src/ninep/cols.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 10:58:24 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commita96c7e873d26686f0e49cf2882c1336046396d37 (patch)
treea1771b70cdf30c449a0f025ea45ccf141ce46ced /src/ninep/cols.zig
parent048e81e3471c73b9ac690901819976f689f6a4ce (diff)
downloadpardes-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.zig60
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();