summaryrefslogtreecommitdiff
path: root/src/ninep/sources.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/sources.zig')
-rw-r--r--src/ninep/sources.zig152
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());
+}