diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 13:48:35 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | ca362e190825862d5c0a1451d163d3c00be68314 (patch) | |
| tree | 41ba9702527092b4e8cbc482c79d6651d6918fa0 | |
| parent | a2e466a874502b665807f620eb8edefb37be1e8c (diff) | |
| download | pardes-ca362e190825862d5c0a1451d163d3c00be68314.tar.gz pardes-ca362e190825862d5c0a1451d163d3c00be68314.zip | |
A second line written to name is refused EINVAL, in one write or on the same open
bash writes printf 'a\nb\n' a line at a time, so each line arrived as a whole name on the held open and renamed the pane twice. An open that has named its pane now refuses a further line, as a two-line write already was.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/fs.md | 4 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 18 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 10 |
3 files changed, 31 insertions, 1 deletions
@@ -559,7 +559,9 @@ directory. A name alone is no edit: the pane's `dirty` stays what its text made Restore or Del, which ask only about text edited), and `Save` writes it under the new name all the same. An open's writes are one name: held until its newline, or its close when it has none, then applied once, however -the writes cut it; nothing else is trimmed. A blank inside a name is taken +the writes cut it; nothing else is trimmed. Two lines are refused, EINVAL, +in one write or as a second line on the same open (bash's `printf +'a\nb\n' > name` writes a line at a time: the first names it). A blank inside a name is taken (`two words.zig`); refused, EINVAL, in acme's words (xfid.c:650) and why, are a blank at either end (not quietly cut off), `bad character in file name: a blank at its end`, a second line, `...: a newline (a name is one diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index d48c8e71..66e3dad8 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -1124,6 +1124,24 @@ test "after a failed address, one that goes from it says there is none" { try testing.expectEqual(Status.ok, wr(p, addr, ".").reply.status); } +test "a write of two lines to name is refused EINVAL, on a held open or not" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + const node = Node.of(serial, .name); + try testing.expectEqual(E.INVAL, wr(p, node, "/tmp/a\n/tmp/b\n").errno()); + const h = call(p, .{ .tag = 1, .op = .open, .node = node, .omode = 1 }).reply.handle; + try testing.expectEqual(E.INVAL, call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h, .data = "/tmp/a\n/tmp/b\n" }).errno()); + _ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h, .opened = true }); + try testing.expectEqualStrings("/test.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); + // Line by line, as bash writes it: the second line is refused. + const h2 = call(p, .{ .tag = 1, .op = .open, .node = node, .omode = 1 }).reply.handle; + try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h2, .data = "/tmp/pardes-a\n" }).reply.status); + try testing.expectEqual(E.INVAL, call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h2, .data = "/tmp/pardes-b\n" }).errno()); + _ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h2, .opened = true }); + try testing.expectEqualStrings("/tmp/pardes-a", nameOf(p.panes[p.paneBySerial(serial).?].?)); +} + test "a name cut across writes is one name, applied once at its newline or its close" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 60778b9c..8c0e74a6 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -162,6 +162,9 @@ pub const Open = struct { /// answered from it again: bash's `read` takes a chunk, keeps a line and /// lseeks back to just past it, and a stream has already moved on. replay: ?Replay = null, + /// A `name` open has named its pane: one name an open, so a second line + /// on it (bash writes `printf 'a\nb\n'` a line at a time) is refused. + named: bool = false, pub const Replay = struct { off: u64, bytes: []u8 }; @@ -1071,6 +1074,13 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { o.pending.clearRetainingCapacity(); return failText(req.tag, E.INVAL, "a line or Edit block over 1 MiB"); } + if (target == .pane and target.pane.file == .name) { + if (o.named or std.mem.count(u8, o.pending.items[0..end], "\n") > 1) { + o.pending.clearRetainingCapacity(); + return failText(req.tag, E.INVAL, "invalid file name: one name a write, on one line"); + } + o.named = true; + } const whole = p.scratch.allocator().dupe(u8, o.pending.items[0..end]) catch return Reply.fail(req.tag, E.NOMEM); const rest = o.pending.items.len - end; std.mem.copyForwards(u8, o.pending.items[0..rest], o.pending.items[end..]); |
