//! 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()); }