diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-20 00:36:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | c1990b3e6e196ad41379aa432bf3ccca8a65d9f0 (patch) | |
| tree | d7ad5afe559f7f56464b14c3122242fa3f16399b /src/ninep/sources.zig | |
| parent | 717afaf3177a2e0925b18ae3445119efe808b129 (diff) | |
| download | pardes-c1990b3e6e196ad41379aa432bf3ccca8a65d9f0.tar.gz pardes-c1990b3e6e196ad41379aa432bf3ccca8a65d9f0.zip | |
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/<n>/... /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 <[email protected]>
Diffstat (limited to 'src/ninep/sources.zig')
| -rw-r--r-- | src/ninep/sources.zig | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/src/ninep/sources.zig b/src/ninep/sources.zig new file mode 100644 index 00000000..b0408594 --- /dev/null +++ b/src/ninep/sources.zig @@ -0,0 +1,152 @@ +//! The editor's embedded sources, served read-only from the root when the +//! build carries them (-Dembed-sources=true). +const std = @import("std"); +const pardes = @import("../pardes.zig"); +const fs = @import("../fs.zig"); +const tree = @import("tree.zig"); + +const Pardes = pardes.Pardes; +const Req = tree.Req; +const Reply = tree.Reply; +const E = tree.E; +const sources = fs.sources; + +pub const archive_node: u64 = 1 << 61; + +/// The node of an archive path, or null when nothing embedded lives there. +/// The empty path is the archive root, which is never listed by itself. +pub fn node(path: []const u8) ?u64 { + if (path.len == 0) return archive_node; + if (path.len >= 4096) return null; + for (sources, 0..) |source, i| { + if (std.mem.eql(u8, source.path, path) or + (source.path.len > path.len and source.path[path.len] == '/' and std.mem.startsWith(u8, source.path, path))) + return archive_node | (@as(u64, i) << 12) | path.len; + } + return null; +} + +fn info(n: u64) ?struct { path: []const u8, contents: []const u8, dir: bool } { + if (n == archive_node) return .{ .path = "", .contents = "", .dir = true }; + const index = (n & ~archive_node) >> 12; + if (index >= sources.len) return null; + const source = sources[@intCast(index)]; + const len: usize = @intCast(n & 4095); + if (len > source.path.len) return null; + return .{ .path = source.path[0..len], .contents = source.contents, .dir = len < source.path.len }; +} + +/// Stages the children of one archive directory; "" lists the top-level +/// directories the served root shows. +pub fn stage(p: *Pardes, out: *std.ArrayList(u8), path: []const u8, skip: *u64) void { + var seen: [sources.len][]const u8 = undefined; + var count: usize = 0; + for (sources, 0..) |source, i| { + if (path.len != 0 and + (source.path.len <= path.len or source.path[path.len] != '/' or !std.mem.startsWith(u8, source.path, path))) continue; + const start = if (path.len == 0) 0 else path.len + 1; + const rest = source.path[start..]; + const len = std.mem.indexOfScalar(u8, rest, '/') orelse rest.len; + const name = rest[0..len]; + var duplicate = false; + for (seen[0..count]) |previous| if (std.mem.eql(u8, name, previous)) { + duplicate = true; + break; + }; + if (duplicate) continue; + seen[count] = name; + count += 1; + if (skip.* > 0) { + skip.* -= 1; + continue; + } + const child = archive_node | (@as(u64, i) << 12) | (start + len); + tree.stageDirent(out, p.gpa, child, len < rest.len, name); + } +} + +pub fn handle(p: *Pardes, req: Req) Reply { + const found = info(req.node) orelse return Reply.fail(req.tag, E.NOENT); + const attr: Reply.Attr = .{ + .node = req.node, + .name = std.fs.path.basename(found.path), + .dir = found.dir, + .size = if (found.dir) 0 else found.contents.len, + .mode = if (found.dir) 0o555 else 0o444, + .mtime = p.fs.started, + }; + switch (req.op) { + .getattr => return .{ .tag = req.tag, .attr = attr }, + .open => return .{ .tag = req.tag, .handle = 1 }, + .release => return .{ .tag = req.tag }, + .lookup => { + if (!found.dir) return Reply.fail(req.tag, E.NOTDIR); + if (std.mem.eql(u8, req.data, "..")) { + const parent = std.fs.path.dirname(found.path) orelse ""; + if (parent.len == 0) return tree.handle(p, .{ .tag = req.tag, .op = .getattr, .node = tree.root }); + const parent_node = node(parent) orelse return Reply.fail(req.tag, E.NOENT); + return handle(p, .{ .tag = req.tag, .op = .getattr, .node = parent_node }); + } + if (req.data.len == 0 or std.mem.indexOfAny(u8, req.data, "/\x00") != null) return Reply.fail(req.tag, E.NOENT); + var buf: [4096]u8 = undefined; + const path = if (found.path.len == 0) req.data else std.fmt.bufPrint(&buf, "{s}/{s}", .{ found.path, req.data }) catch return Reply.fail(req.tag, E.NOENT); + const child = node(path) orelse return Reply.fail(req.tag, E.NOENT); + return handle(p, .{ .tag = req.tag, .op = .getattr, .node = child }); + }, + .readdir => { + if (!found.dir) return Reply.fail(req.tag, E.NOTDIR); + const out = p.fs.stage(p.gpa); + var skip = req.off; + stage(p, out, found.path, &skip); + return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } }; + }, + .read => { + if (found.dir) return Reply.fail(req.tag, E.PERM); + const off: usize = @intCast(@min(req.off, found.contents.len)); + const bytes = found.contents[off..][0..@min(req.size, found.contents.len - off)]; + const out = p.fs.stage(p.gpa); + out.appendSlice(p.gpa, bytes) catch return Reply.fail(req.tag, E.NOMEM); + return .{ .tag = req.tag, .payload = .{ .staged = @intCast(bytes.len) } }; + }, + else => return Reply.fail(req.tag, E.PERM), + } +} + +const testing = std.testing; +const th = @import("testing.zig"); +const limits = @import("../memory.zig").limits; + +test "embedded sources appear under the root only when the build carries them" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + var buf: [64]th.Dirent = undefined; + const top = th.dirents(th.rdir(p, tree.root, 0).bytes, &buf); + const listed = th.nameAt(top, "src"); + try testing.expectEqual(limits.embedded_sources, listed != null); + const looked = th.look_up(p, tree.root, "src"); + try testing.expectEqual(limits.embedded_sources, looked.reply.status == .ok); + try testing.expect(tree.resolveSelf(p, "shaders/../src/../") == tree.root); + if (!limits.embedded_sources) { + try testing.expect(tree.resolveSelf(p, "src/pardes.zig") == null); + try testing.expect(tree.resolveSelf(p, "src") == null); + try testing.expect(th.nameAt(top, "build.zig") == null); + return; + } + try testing.expect(looked.reply.attr.dir); + try testing.expectEqual(@as(u16, 0o555), looked.reply.attr.mode); + try testing.expectEqual(listed.?.node, looked.reply.attr.node); + try testing.expect(th.nameAt(top, "build.zig") == null); + try testing.expect(th.nameAt(top, "build.zig.zon") == null); + const file = th.look_up(p, looked.reply.attr.node, "pardes.zig"); + try testing.expectEqual(tree.Status.ok, file.reply.status); + try testing.expectEqual(@as(u16, 0o444), file.reply.attr.mode); + try testing.expectEqual(@as(u64, fs.sourceBytes("src/pardes.zig").?.len), file.reply.attr.size); + try testing.expectEqual(file.reply.attr.node, tree.resolveSelf(p, "src/pardes.zig").?); + const opened = th.call(p, .{ .tag = 1, .op = .open, .node = file.reply.attr.node }); + const head = th.call(p, .{ .tag = 2, .op = .read, .node = file.reply.attr.node, .handle = opened.reply.handle, .size = 16 }); + try testing.expectEqualStrings(fs.sourceBytes("src/pardes.zig").?[0..16], head.bytes); + const parent = th.look_up(p, looked.reply.attr.node, ".."); + try testing.expectEqual(tree.root, parent.reply.attr.node); + try testing.expectEqual(E.NOENT, th.look_up(p, tree.root, "build.zig").errno()); + try testing.expectEqual(E.NOENT, th.look_up(p, looked.reply.attr.node, "missing.zig").errno()); +} |
