From c1990b3e6e196ad41379aa432bf3ccca8a65d9f0 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 20 Sep 2026 00:36:50 -0300 Subject: Flatten the 9P control tree and move it out of fs.zig The served tree loses the self/ level: /index /ctl /new /log /screen /listeners /pane//... /os, with /src only in -Dembed-sources=true builds (default off, on for esp32p4). ctl speaks the editor's own language with two lowercase verbs, look TEXT and exec TEXT, plus acme's addr verbs; the new/ factory directory becomes one clone file; cons is gone (exec Msg); name and sel are files; stats report real lengths, modes and mtimes; /log streams pane new/del/rename/save events. The tree code lives in src/ninep/ (tree, pane, ctl, addr, pty, events, screen, sources); fs.zig keeps host access, mounts, resolution and find/grep. Same engine and transports. README (fs-help.txt) and docs rewritten; tests updated and extended. Co-Authored-By: Claude Fable 5.1 --- src/ninep/addr.zig | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 src/ninep/addr.zig (limited to 'src/ninep/addr.zig') diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig new file mode 100644 index 00000000..f7642b9e --- /dev/null +++ b/src/ninep/addr.zig @@ -0,0 +1,276 @@ +//! The address language of a pane's `addr` file: acme's, with mvzr regexps. +const std = @import("std"); +const mvzr = @import("mvzr"); +const modal = @import("../modal.zig"); +const pane_files = @import("pane.zig"); + +pub const Range = pane_files.State.Range; + +fn clip(n: usize) u32 { + return std.math.cast(u32, n) orelse std.math.maxInt(u32); +} + +fn safePattern(pat: []const u8) bool { + var i: usize = 0; + while (i < pat.len) : (i += 1) { + if (pat[i] != '\\') continue; + if (i + 1 >= pat.len) return false; + i += 1; + } + return true; +} + +pub const Addr = struct { + text: []const u8, + lim: ?Range, + expr: []const u8, + i: usize = 0, + depth: u8 = 0, + + const max_depth = 32; + const Size = enum { char, line }; + + pub fn address(a: *Addr, ar_in: Range) ?Range { + const start = a.i; + var ar = ar_in; + var r = ar_in; + var dir: u8 = 0; + var size: Size = .line; + var c: u8 = 0; + while (a.i < a.expr.len) { + const prevc = c; + c = a.expr[a.i]; + a.i += 1; + switch (c) { + ',', ';' => { + if (c == ';') ar = r; + if (prevc == 0) r.q0 = 0; // lhs defaults to 0 + if (a.i >= a.expr.len) { + r.q1 = clip(a.text.len); // rhs defaults to $ + } else { + if (a.depth >= max_depth) return null; + a.depth += 1; + const nr = a.address(ar) orelse return null; + a.depth -= 1; + r.q1 = nr.q1; + } + return r; + }, + '+', '-' => { + if (prevc == '+' or prevc == '-') { + const nc = if (a.i < a.expr.len) a.expr[a.i] else 0; + if (nc != '#' and nc != '/' and nc != '?') + r = a.number(r, 1, prevc, .line) orelse return null; + } + dir = c; + }, + '.', '$' => { + if (a.i != start + 1) { + a.i -= 1; + return r; + } + r = if (c == '.') ar else .{ .q0 = clip(a.text.len), .q1 = clip(a.text.len) }; + dir = if (a.i < a.expr.len) '+' else 0; + }, + '#', '0'...'9' => { + var digit = c; + if (c == '#') { + if (a.i >= a.expr.len or a.expr[a.i] < '0' or a.expr[a.i] > '9') { + a.i -= 1; + return r; + } + digit = a.expr[a.i]; + a.i += 1; + size = .char; + } + var n: u64 = digit - '0'; + while (a.i < a.expr.len) : (a.i += 1) { + const d = a.expr[a.i]; + if (d < '0' or d > '9') break; + n = @min(n * 10 + (d - '0'), std.math.maxInt(u32)); + } + r = a.number(r, @intCast(n), dir, size) orelse return null; + dir = 0; + size = .line; + }, + '/', '?' => { + const back = c == '?'; + r = a.regexp(r, a.pattern(c), back) orelse return null; + dir = 0; + size = .line; + }, + else => { + a.i -= 1; + return r; + }, + } + } + if (dir != 0) r = a.number(r, 1, dir, .line) orelse return null; + return r; + } + + fn pattern(a: *Addr, delim: u8) []const u8 { + const s = a.i; + while (a.i < a.expr.len) { + const c = a.expr[a.i]; + if (c == '\n') break; + a.i += 1; + if (c == '\\') { + if (a.i < a.expr.len) a.i += 1; + continue; + } + if (c == delim) return a.expr[s .. a.i - 1]; + } + return a.expr[s..a.i]; + } + + fn number(a: *Addr, r_in: Range, n: u32, dir: u8, size: Size) ?Range { + var r = r_in; + if (size == .char) { + var off: i64 = n; + if (dir == '+') { + off = @as(i64, r.q1) + n; + } else if (dir == '-') { + if (r.q0 == 0 and n > 0) r.q0 = clip(a.text.len); + off = @as(i64, r.q0) - n; + } + if (off < 0 or off > @as(i64, @intCast(a.text.len))) return null; + const g = clip(modal.graphemeStart(a.text, @intCast(off))); + return .{ .q0 = g, .q1 = g }; + } + var line: i64 = n; + var q0: usize = r.q0; + var q1: usize = r.q1; + switch (dir) { + '-' => { + if (q0 < a.text.len) while (q0 > 0 and a.text[q0 - 1] != '\n') { + q0 -= 1; + }; + q1 = q0; + while (line > 0 and q0 > 0) { + if (a.text[q0 - 1] == '\n') { + line -= 1; + q1 = q0; + } + q0 -= 1; + } + if (line > 1) return null; + while (q0 > 0 and a.text[q0 - 1] != '\n') q0 -= 1; + return .{ .q0 = clip(q0), .q1 = clip(q1) }; + }, + '+' => { + if (q1 > 0) while (q1 < a.text.len and a.text[q1 - 1] != '\n') { + q1 += 1; + }; + q0 = q1; + }, + else => { + q0 = 0; + q1 = 0; + }, + } + while (line > 0 and q1 < a.text.len) { + const ch = a.text[q1]; + q1 += 1; + if (ch == '\n' or q1 == a.text.len) { + line -= 1; + if (line > 0) q0 = q1; + } + } + if (line > 0) return null; + return .{ .q0 = clip(q0), .q1 = clip(q1) }; + } + + fn regexp(a: *Addr, r: Range, pat: []const u8, back: bool) ?Range { + if (pat.len == 0 or !safePattern(pat)) return null; + const re = mvzr.compile(pat) orelse return null; + if (back) { + const hi = @min(@as(usize, r.q0), a.text.len); + var best: ?mvzr.Match = null; + var at: usize = 0; + while (at < hi) { + const m = re.matchPos(at, a.text[0..hi]) orelse break; + best = m; + at = if (m.end > m.start) m.end else m.end + 1; + } + const m = best orelse return null; + return .{ .q0 = clip(m.start), .q1 = clip(m.end) }; + } + const hi = if (a.lim) |l| @min(@as(usize, l.q1), a.text.len) else a.text.len; + const from = @min(@as(usize, r.q1), hi); + const m = re.match(a.text[from..hi]) orelse return null; + return .{ .q0 = clip(from + m.start), .q1 = clip(from + m.end) }; + } +}; + +const testing = std.testing; +const th = @import("testing.zig"); +const tree = @import("tree.zig"); +const Node = tree.Node; +const E = tree.E; +const Status = tree.Status; + +test "the address language, form by form" { + const gpa = testing.allocator; + const p = try th.withFile(gpa, "one\ntwo\nthree\n"); + defer p.deinit(); + const serial = th.serialOf(p); + const addr = Node.of(serial, .addr); + + const Case = struct { expr: []const u8, q0: u32, q1: u32 }; + for ([_]Case{ + .{ .expr = "#0", .q0 = 0, .q1 = 0 }, + .{ .expr = "#5", .q0 = 5, .q1 = 5 }, + .{ .expr = "0", .q0 = 0, .q1 = 0 }, + .{ .expr = "1", .q0 = 0, .q1 = 4 }, + .{ .expr = "2", .q0 = 4, .q1 = 8 }, + .{ .expr = "$", .q0 = 14, .q1 = 14 }, + .{ .expr = ",", .q0 = 0, .q1 = 14 }, + .{ .expr = "1,2", .q0 = 0, .q1 = 8 }, + .{ .expr = "#1,#4", .q0 = 1, .q1 = 4 }, + .{ .expr = "2+1", .q0 = 8, .q1 = 14 }, + .{ .expr = "$-1", .q0 = 8, .q1 = 14 }, + .{ .expr = "/two/", .q0 = 4, .q1 = 7 }, + .{ .expr = "/t.o/", .q0 = 4, .q1 = 7 }, + .{ .expr = "1\n", .q0 = 0, .q1 = 4 }, + }) |c| { + _ = th.wr(p, addr, "#0"); + const w = th.wr(p, addr, c.expr); + try testing.expectEqual(Status.ok, w.reply.status); + const got = th.rd(p, addr, 0, 64); + var want: [32]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&want, "{d:>11} {d:>11} ", .{ c.q0, c.q1 }), + got.bytes, + ); + } + + _ = th.wr(p, addr, "1"); + _ = th.wr(p, addr, "."); + try testing.expectEqual(@as(u32, 0), p.fs.panes[0].addr.q0); + try testing.expectEqual(@as(u32, 4), p.fs.panes[0].addr.q1); + + _ = th.wr(p, addr, "$"); + _ = th.wr(p, addr, "?o?"); + try testing.expectEqual(@as(u32, 6), p.fs.panes[0].addr.q0); // the `o` in "two" + try testing.expectEqual(@as(u32, 7), p.fs.panes[0].addr.q1); + + _ = th.wr(p, addr, "1"); + _ = th.wr(p, Node.of(serial, .ctl), "limit=addr\n"); + _ = th.wr(p, addr, "#0"); + try testing.expectEqual(E.INVAL, th.wr(p, addr, "/three/").errno()); + _ = th.wr(p, Node.of(serial, .ctl), "clean\n"); + _ = th.call(p, .{ .tag = 6, .op = .open, .node = Node.of(serial, .ctl) }); + try testing.expect(p.fs.panes[0].limit == null); + _ = th.wr(p, addr, "#0"); + try testing.expectEqual(Status.ok, th.wr(p, addr, "/three/").reply.status); + + for ([_][]const u8{ "zzz", "#", "//", "/nomatch/", "1 2", "99", "/a\\" }) |bad| { + _ = th.wr(p, addr, "#0"); + try testing.expectEqual(E.INVAL, th.wr(p, addr, bad).errno()); + } + + const nested = "," ** 4096; + _ = th.wr(p, addr, "#0"); + try testing.expectEqual(E.INVAL, th.wr(p, addr, nested).errno()); +} -- cgit v1.3