Skip to content

Commit 5d9f508

Browse files
committed
stdlib std.os: Improve wasi-libc parity for WASI CWD emulation
Two major changes here: 1. We store the CWD as a simple `[]const u8` and lookup Preopens for every absolute or CWD-referenced file operation, based on the Preopen with the longest match (i.e. most specific path) 2. Preorders are normalized to POSIX absolute paths at init time. Behavior depends on the "cwd_root" parameter of `initPreopensWasi`: `cwd_root` is used for any Preopens that start with "." For example: "./foo/bar" - inits to -> "{cwd_root}/foo/bar" "foo/bar" - inits to -> "/foo/bar" "/foo/bar" - inits to -> "/foo/bar" `cwd_root` must be an absolute path. Using "/" as `cwd_root` gives behavior similar to wasi-libc.
1 parent 908f41a commit 5d9f508

File tree

7 files changed

+159
-199
lines changed

7 files changed

+159
-199
lines changed

doc/langref.html.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11270,7 +11270,7 @@ pub fn main() !void {
1127011270
var preopens = PreopenList.init(gpa);
1127111271
defer preopens.deinit();
1127211272

11273-
try preopens.populate();
11273+
try preopens.populate(null);
1127411274

1127511275
for (preopens.asSlice()) |preopen, i| {
1127611276
std.debug.print("{}: {}\n", .{ i, preopen });

lib/std/fs/path.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) ![]u8 {
735735

736736
test "resolve" {
737737
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
738-
if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
738+
if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
739739

740740
const cwd = try process.getCwdAlloc(testing.allocator);
741741
defer testing.allocator.free(cwd);
@@ -756,7 +756,7 @@ test "resolveWindows" {
756756
return error.SkipZigTest;
757757
}
758758
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
759-
if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
759+
if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
760760
if (native_os == .windows) {
761761
const cwd = try process.getCwdAlloc(testing.allocator);
762762
defer testing.allocator.free(cwd);
@@ -802,7 +802,7 @@ test "resolveWindows" {
802802

803803
test "resolvePosix" {
804804
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
805-
if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
805+
if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
806806

807807
try testResolvePosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c");
808808
try testResolvePosix(&[_][]const u8{ "/a/b", "c", "//d", "e///" }, "/d/e");
@@ -1216,7 +1216,7 @@ test "relative" {
12161216
return error.SkipZigTest;
12171217
}
12181218
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
1219-
if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
1219+
if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
12201220

12211221
try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
12221222
try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");

lib/std/fs/test.zig

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
4747

4848
test "accessAbsolute" {
4949
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
50-
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
50+
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
5151

5252
var tmp = tmpDir(.{});
5353
defer tmp.cleanup();
@@ -66,7 +66,7 @@ test "accessAbsolute" {
6666

6767
test "openDirAbsolute" {
6868
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
69-
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
69+
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
7070

7171
var tmp = tmpDir(.{});
7272
defer tmp.cleanup();
@@ -103,7 +103,7 @@ test "openDir cwd parent .." {
103103

104104
test "readLinkAbsolute" {
105105
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
106-
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
106+
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
107107

108108
var tmp = tmpDir(.{});
109109
defer tmp.cleanup();
@@ -512,7 +512,7 @@ test "rename" {
512512

513513
test "renameAbsolute" {
514514
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
515-
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
515+
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
516516

517517
var tmp_dir = tmpDir(.{});
518518
defer tmp_dir.cleanup();
@@ -947,7 +947,7 @@ test "open file with exclusive nonblocking lock twice (absolute paths)" {
947947

948948
test "walker" {
949949
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
950-
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
950+
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
951951

952952
var tmp = tmpDir(.{ .iterate = true });
953953
defer tmp.cleanup();
@@ -998,7 +998,7 @@ test "walker" {
998998

999999
test ". and .. in fs.Dir functions" {
10001000
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
1001-
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
1001+
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
10021002

10031003
var tmp = tmpDir(.{});
10041004
defer tmp.cleanup();
@@ -1027,7 +1027,7 @@ test ". and .. in fs.Dir functions" {
10271027

10281028
test ". and .. in absolute functions" {
10291029
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
1030-
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, ".");
1030+
if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/");
10311031

10321032
var tmp = tmpDir(.{});
10331033
defer tmp.cleanup();

lib/std/fs/wasi.zig

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const builtin = @import("builtin");
33
const os = std.os;
44
const mem = std.mem;
55
const math = std.math;
6+
const fs = std.fs;
7+
const assert = std.debug.assert;
68
const Allocator = mem.Allocator;
79
const wasi = std.os.wasi;
810
const fd_t = wasi.fd_t;
@@ -34,16 +36,27 @@ pub const PreopenType = union(PreopenTypeTag) {
3436

3537
// Checks whether `other` refers to a subdirectory of `self` and, if so,
3638
// returns the relative path to `other` from `self`
39+
//
40+
// Expects `other` to be a canonical path, not containing "." or ".."
3741
pub fn getRelativePath(self: Self, other: PreopenType) ?[]const u8 {
3842
if (std.meta.activeTag(self) != std.meta.activeTag(other)) return null;
3943

4044
switch (self) {
41-
PreopenTypeTag.Dir => |this_path| {
45+
PreopenTypeTag.Dir => |self_path| {
4246
const other_path = other.Dir;
43-
if (mem.indexOfDiff(u8, this_path, other_path)) |index| {
44-
if (index < this_path.len) return null;
47+
if (mem.indexOfDiff(u8, self_path, other_path)) |index| {
48+
if (index < self_path.len) return null;
49+
}
50+
51+
const rel_path = other_path[self_path.len..];
52+
if (rel_path.len == 0) {
53+
return rel_path;
54+
} else if (rel_path[0] == '/') {
55+
return rel_path[1..];
56+
} else {
57+
if (self_path[self_path.len - 1] != '/') return null;
58+
return rel_path;
4559
}
46-
return other_path[this_path.len..];
4760
},
4861
}
4962
}
@@ -130,7 +143,22 @@ pub const PreopenList = struct {
130143
/// the preopen list still contains all valid preopened file descriptors that are valid
131144
/// for use. Therefore, it is fine to call `find`, `asSlice`, or `toOwnedSlice`. Finally,
132145
/// `deinit` still must be called!
133-
pub fn populate(self: *Self) Error!void {
146+
///
147+
/// Usage of `cwd_root`:
148+
/// If provided, `cwd_root` is inserted as prefix for any Preopens that
149+
/// begin with "." and all paths are normalized as POSIX-style absolute
150+
/// paths. `cwd_root` must be an absolute path.
151+
///
152+
/// For example:
153+
/// "./foo/bar" -> "{cwd_root}/foo/bar"
154+
/// "foo/bar" -> "/foo/bar"
155+
/// "/foo/bar" -> "/foo/bar"
156+
///
157+
/// If `cwd_root` is not provided, all preopen directories are unmodified.
158+
///
159+
pub fn populate(self: *Self, cwd_root: ?[]const u8) Error!void {
160+
if (cwd_root) |root| assert(fs.path.isAbsolute(root));
161+
134162
// Clear contents if we're being called again
135163
for (self.toOwnedSlice()) |preopen| {
136164
switch (preopen.@"type") {
@@ -140,6 +168,7 @@ pub const PreopenList = struct {
140168
errdefer self.deinit();
141169
var fd: fd_t = 3; // start fd has to be beyond stdio fds
142170

171+
var path_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
143172
while (true) {
144173
var buf: prestat_t = undefined;
145174
switch (wasi.fd_prestat_get(fd, &buf)) {
@@ -156,42 +185,57 @@ pub const PreopenList = struct {
156185
else => |err| return os.unexpectedErrno(err),
157186
}
158187
const preopen_len = buf.u.dir.pr_name_len;
159-
const path_buf = try self.buffer.allocator.alloc(u8, preopen_len);
160-
mem.set(u8, path_buf, 0);
161-
switch (wasi.fd_prestat_dir_name(fd, path_buf.ptr, preopen_len)) {
188+
189+
mem.set(u8, path_buf[0..preopen_len], 0);
190+
switch (wasi.fd_prestat_dir_name(fd, &path_buf, preopen_len)) {
162191
.SUCCESS => {},
163192
else => |err| return os.unexpectedErrno(err),
164193
}
165194

166-
const preopen = Preopen.new(fd, PreopenType{ .Dir = path_buf });
195+
// Unfortunately, WASI runtimes (e.g. wasmer) are not consistent about whether the
196+
// NULL sentinel is included in the reported Preopen name_len
197+
const raw_path = if (path_buf[preopen_len - 1] == 0) blk: {
198+
break :blk path_buf[0 .. preopen_len - 1];
199+
} else path_buf[0..preopen_len];
200+
201+
// If we were provided a CWD root to resolve against, we try to treat Preopen dirs as
202+
// POSIX paths, relative to "/" or `cwd_root` depending on whether they start with "."
203+
const path = if (cwd_root) |cwd| blk: {
204+
const resolve_paths: [][]const u8 = if (raw_path[0] == '.') &.{ cwd, raw_path } else &.{ "/", raw_path };
205+
break :blk fs.path.resolve(self.buffer.allocator, resolve_paths) catch |err| switch (err) {
206+
error.CurrentWorkingDirectoryUnlinked => unreachable, // root is absolute, so CWD not queried
207+
else => |e| return e,
208+
};
209+
} else blk: {
210+
// If we were provided no CWD root, we preserve the preopen dir without resolving
211+
break :blk try self.buffer.allocator.dupe(u8, raw_path);
212+
};
213+
errdefer self.buffer.allocator.free(path);
214+
const preopen = Preopen.new(fd, .{ .Dir = path });
215+
167216
try self.buffer.append(preopen);
168217
fd = try math.add(fd_t, fd, 1);
169218
}
170219
}
171220

172221
/// Find a preopen which includes access to `preopen_type`.
173222
///
174-
/// If the preopen exists, `relative_path` is updated to point to the relative
175-
/// portion of `preopen_type` and the matching Preopen is returned. If multiple
176-
/// preopens match the provided resource, the most recent one is used.
223+
/// If multiple preopens match the provided resource, the most specific
224+
/// match is returned. More recent preopens take priority, as well.
177225
pub fn findContaining(self: Self, preopen_type: PreopenType) ?PreopenUri {
178-
// Search in reverse, so that most recently added preopens take precedence
179-
var k: usize = self.buffer.items.len;
180-
while (k > 0) {
181-
k -= 1;
182-
183-
const preopen = self.buffer.items[k];
184-
if (preopen.@"type".getRelativePath(preopen_type)) |rel_path_orig| {
185-
var rel_path = rel_path_orig;
186-
while (rel_path.len > 0 and rel_path[0] == '/') rel_path = rel_path[1..];
187-
188-
return PreopenUri{
189-
.base = preopen,
190-
.relative_path = if (rel_path.len == 0) "." else rel_path,
191-
};
226+
var best_match: ?PreopenUri = null;
227+
228+
for (self.buffer.items) |preopen| {
229+
if (preopen.@"type".getRelativePath(preopen_type)) |rel_path| {
230+
if (best_match == null or rel_path.len <= best_match.?.relative_path.len) {
231+
best_match = PreopenUri{
232+
.base = preopen,
233+
.relative_path = if (rel_path.len == 0) "." else rel_path,
234+
};
235+
}
192236
}
193237
}
194-
return null;
238+
return best_match;
195239
}
196240

197241
/// Find preopen by fd. If the preopen exists, return it.
@@ -233,8 +277,20 @@ test "extracting WASI preopens" {
233277
var preopens = PreopenList.init(std.testing.allocator);
234278
defer preopens.deinit();
235279

236-
try preopens.populate();
280+
try preopens.populate(null);
237281

238282
const preopen = preopens.find(PreopenType{ .Dir = "." }) orelse unreachable;
239283
try std.testing.expect(preopen.@"type".eql(PreopenType{ .Dir = "." }));
284+
285+
const po_type1 = PreopenType{ .Dir = "/" };
286+
try std.testing.expect(std.mem.eql(u8, po_type1.getRelativePath(.{ .Dir = "/" }).?, ""));
287+
try std.testing.expect(std.mem.eql(u8, po_type1.getRelativePath(.{ .Dir = "/test/foobar" }).?, "test/foobar"));
288+
289+
const po_type2 = PreopenType{ .Dir = "/test/foo" };
290+
try std.testing.expect(po_type2.getRelativePath(.{ .Dir = "/test/foobar" }) == null);
291+
292+
const po_type3 = PreopenType{ .Dir = "/test" };
293+
try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test" }).?, ""));
294+
try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test/" }).?, ""));
295+
try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test/foo/bar" }).?, "foo/bar"));
240296
}

0 commit comments

Comments
 (0)