1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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());
}
|