From 8d25618ea575611e5b1cbe48f4ac48532f2e32be Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 17:21:09 -0300 Subject: An addr, dot or limit pair out of order or past the text is refused as the address form is, not clamped `#5,#2` and `#2,#99` were refused (addresses out of order, address out of range), but the pair form `5 2` became 5..5 and `2 99` became 2..end, silently a different range from the one asked for. A pair is now checked the same way: past the text is out of range, and an end before its start is out of order. fs.md says so. Co-Authored-By: Claude Opus 5.5 --- docs/fs.md | 4 +++- src/ninep/pane.zig | 28 ++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/fs.md b/docs/fs.md index 337e31eb..0ec88876 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -447,7 +447,9 @@ warns once. `addr`, `dot` and `limit` read the pair of byte offsets they take, so copying one onto another (`cp $p/addr $p/dot`) is acme's `dot=addr`. A -write is a pair or an address expression. `addr` names where `data` reads +write is a pair or an address expression; a pair is checked as an address +is, `addresses out of order` (`5 2`) or `address out of range` (past the +text), never clamped. `addr` names where `data` reads (to the end of text) and the range `xdata` reads; `dot` is the selection (moving it scrolls the pane there); `limit` bounds the end of a forward search and reads empty until set. Truncating `dot` empties it, truncating diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index d5f2f20a..d3c1b907 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -760,7 +760,7 @@ fn pairOf(text: []const u8) ?State.Range { const q0 = std.fmt.parseInt(u32, it.next() orelse return null, 10) catch return null; const q1 = std.fmt.parseInt(u32, it.next() orelse return null, 10) catch return null; if (it.next() != null) return null; - return .{ .q0 = q0, .q1 = @max(q0, q1) }; + return .{ .q0 = q0, .q1 = q1 }; } pub const e_addr_failed = "no address: the last one written to addr failed"; @@ -779,9 +779,16 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply { if (file == .addr and pf.addr_failed and expr.len > 0 and std.mem.indexOfScalar(u8, ".+-", expr[0]) != null) return tree.failText(req.tag, E.INVAL, e_addr_failed); var a: addressing.Addr = .{ .text = text, .lim = pf.limit, .expr = expr }; - var r = if (pairOf(expr)) |pair| - State.Range{ .q0 = @min(pair.q0, clip(text.len)), .q1 = @min(pair.q1, clip(text.len)) } - else if (a.address(pf.addr)) |found| (if (a.i < expr.len) null else found) else null; + // A pair is refused as the address form refuses it, never clamped: one + // past the text is out of range, one ending before it starts out of + // order (below). + var r = if (pairOf(expr)) |pair| pair: { + if (pair.q0 > clip(text.len) or pair.q1 > clip(text.len)) { + a.err = addressing.e_range; + break :pair null; + } + break :pair pair; + } else if (a.address(pf.addr)) |found| (if (a.i < expr.len) null else found) else null; // sam's check, which acme leaves out: `#100,#50` names no range. if (r) |found| if (found.q0 > found.q1) { a.err = addressing.e_order; @@ -1395,6 +1402,19 @@ test "a 9P write moves dot only as acme's textinsert does, and scrolls only the try testing.expectEqualStrings("abYYef\nmore\n", fileOf(pane).?.content); } +test "an addr pair out of order or out of range is refused as the address form is, not clamped" { + const p = try withFile(testing.allocator, "abcdef\n"); + defer p.deinit(); + const serial = serialOf(p); + const addr = Node.of(serial, .addr); + try testing.expectEqualStrings(addressing.e_order, wr(p, addr, "5 2").reply.ename); + try testing.expectEqualStrings(addressing.e_order, wr(p, addr, "#5,#2").reply.ename); + try testing.expectEqualStrings(addressing.e_range, wr(p, addr, "2 99").reply.ename); + try testing.expectEqualStrings(addressing.e_range, wr(p, addr, "#2,#99").reply.ename); + try testing.expectEqual(Status.ok, wr(p, addr, "2 4").reply.status); + try testing.expectEqual(State.Range{ .q0 = 2, .q1 = 4 }, p.panes[p.paneBySerial(serial).?].?.fs.addr); +} + 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(); -- cgit v1.3