From 58f961f4cb9875bbce3070969438ecf08f392c9f Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Tue, 1 Mar 2022 10:17:05 -0700 Subject: [PATCH 1/3] stdlib: Add emulated CWD to std.os for WASI targets This adds a special CWD file descriptor, AT.FDCWD (-2), to refer to the current working directory. The `*at(...)` functions look for this and resolve relative paths against the stored CWD. Absolute paths are dynamically matched against the stored Preopens. "os.initPreopensWasi()" must be called before std.os functions will resolve relative or absolute paths correctly. This is asserted at runtime. Support has been added for: `open`, `rename`, `mkdir`, `rmdir`, `chdir`, `fchdir`, `link`, `symlink`, `unlink`, `readlink`, `fstatat`, `access`, and `faccessat`. This also includes limited support for `getcwd()` and `realpath()`. These return an error if the CWD does not correspond to a Preopen with an absolute path. They also do not currently expand symlinks. --- lib/std/build.zig | 2 + lib/std/c/wasi.zig | 10 +- lib/std/child_process.zig | 1 + lib/std/fs.zig | 72 ++-- lib/std/fs/path.zig | 13 +- lib/std/fs/test.zig | 22 +- lib/std/fs/wasi.zig | 59 ++- lib/std/os.zig | 533 ++++++++++++++++++++++-- lib/std/os/test.zig | 133 +++++- lib/std/os/wasi.zig | 5 + lib/std/testing.zig | 4 +- lib/std/zig/system/NativeTargetInfo.zig | 2 + src/test.zig | 1 + 13 files changed, 757 insertions(+), 100 deletions(-) diff --git a/lib/std/build.zig b/lib/std/build.zig index 07e9055b1d5c..2c81f9dcd42e 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -2707,6 +2707,8 @@ pub const LibExeObjStep = struct { try zig_args.append("--test-cmd"); try zig_args.append("--dir=."); try zig_args.append("--test-cmd"); + try zig_args.append("--mapdir=/cwd::."); + try zig_args.append("--test-cmd"); try zig_args.append("--allow-unknown-exports"); // TODO: Remove when stage2 is default compiler try zig_args.append("--test-cmd-bin"); } else { diff --git a/lib/std/c/wasi.zig b/lib/std/c/wasi.zig index c3635784dddd..e1940054b6db 100644 --- a/lib/std/c/wasi.zig +++ b/lib/std/c/wasi.zig @@ -62,21 +62,21 @@ pub const Stat = extern struct { /// https://github.com/WebAssembly/wasi-libc/blob/main/expected/wasm32-wasi/predefined-macros.txt pub const O = struct { pub const ACCMODE = (EXEC | RDWR | SEARCH); - pub const APPEND = FDFLAG.APPEND; + pub const APPEND = @as(u32, FDFLAG.APPEND); pub const CLOEXEC = (0); pub const CREAT = ((1 << 0) << 12); // = __WASI_OFLAGS_CREAT << 12 pub const DIRECTORY = ((1 << 1) << 12); // = __WASI_OFLAGS_DIRECTORY << 12 - pub const DSYNC = FDFLAG.DSYNC; + pub const DSYNC = @as(u32, FDFLAG.DSYNC); pub const EXCL = ((1 << 2) << 12); // = __WASI_OFLAGS_EXCL << 12 pub const EXEC = (0x02000000); pub const NOCTTY = (0); pub const NOFOLLOW = (0x01000000); - pub const NONBLOCK = (1 << FDFLAG.NONBLOCK); + pub const NONBLOCK = @as(u32, FDFLAG.NONBLOCK); pub const RDONLY = (0x04000000); pub const RDWR = (RDONLY | WRONLY); - pub const RSYNC = (1 << FDFLAG.RSYNC); + pub const RSYNC = @as(u32, FDFLAG.RSYNC); pub const SEARCH = (0x08000000); - pub const SYNC = (1 << FDFLAG.SYNC); + pub const SYNC = @as(u32, FDFLAG.SYNC); pub const TRUNC = ((1 << 3) << 12); // = __WASI_OFLAGS_TRUNC << 12 pub const TTY_INIT = (0); pub const WRONLY = (0x10000000); diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index 236ce45619e8..e8e348d67ba2 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -563,6 +563,7 @@ pub const ChildProcess = struct { error.DeviceBusy => unreachable, error.FileLocksNotSupported => unreachable, error.BadPathName => unreachable, // Windows-only + error.InvalidHandle => unreachable, // WASI-only error.WouldBlock => unreachable, else => |e| return e, } diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 758a2bd13259..7a41bdf6a1e5 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -923,6 +923,7 @@ pub const Dir = struct { pub const OpenError = error{ FileNotFound, NotDir, + InvalidHandle, AccessDenied, SymLinkLoop, ProcessFdQuotaExceeded, @@ -981,6 +982,13 @@ pub const Dir = struct { w.RIGHT.FD_FILESTAT_SET_TIMES | w.RIGHT.FD_FILESTAT_SET_SIZE; } + if (self.fd == os.wasi.AT.FDCWD or path.isAbsolute(sub_path)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var resolved_path_buf: [MAX_PATH_BYTES]u8 = undefined; + const resolved_path = try os.resolvePathWasi(sub_path, &resolved_path_buf); + const fd = try os.openatWasi(resolved_path.dir_fd, resolved_path.relative_path, 0x0, 0x0, fdflags, base, 0x0); + return File{ .handle = fd }; + } const fd = try os.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); return File{ .handle = fd }; } @@ -1145,6 +1153,13 @@ pub const Dir = struct { if (flags.exclusive) { oflags |= w.O.EXCL; } + if (self.fd == os.wasi.AT.FDCWD or path.isAbsolute(sub_path)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var resolved_path_buf: [MAX_PATH_BYTES]u8 = undefined; + const resolved_path = try os.resolvePathWasi(sub_path, &resolved_path_buf); + const fd = try os.openatWasi(resolved_path.dir_fd, resolved_path.relative_path, 0x0, oflags, 0x0, base, 0x0); + return File{ .handle = fd }; + } const fd = try os.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); return File{ .handle = fd }; } @@ -1330,7 +1345,19 @@ pub const Dir = struct { /// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { if (builtin.os.tag == .wasi) { - @compileError("realpath is unsupported in WASI"); + if (self.fd == os.wasi.AT.FDCWD or path.isAbsolute(pathname)) { + var buffer: [MAX_PATH_BYTES]u8 = undefined; + const out_path = try os.realpath(pathname, &buffer); + if (out_path.len > out_buffer.len) { + return error.NameTooLong; + } + mem.copy(u8, out_buffer, out_path); + return out_buffer[0..out_path.len]; + } else { + // Unfortunately, we have no ability to look up the path for an fd_t + // on WASI, so we have to give up here. + return error.InvalidHandle; + } } if (builtin.os.tag == .windows) { const pathname_w = try os.windows.sliceToPrefixedFileW(pathname); @@ -1507,7 +1534,16 @@ pub const Dir = struct { // TODO do we really need all the rights here? const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN; - const result = os.openatWasi(self.fd, sub_path, symlink_flags, w.O.DIRECTORY, 0x0, base, inheriting); + const result = blk: { + if (self.fd == os.wasi.AT.FDCWD or path.isAbsolute(sub_path)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var resolved_path_buf: [MAX_PATH_BYTES]u8 = undefined; + const resolved_path = try os.resolvePathWasi(sub_path, &resolved_path_buf); + break :blk os.openatWasi(resolved_path.dir_fd, resolved_path.relative_path, symlink_flags, w.O.DIRECTORY, 0x0, base, inheriting); + } else { + break :blk os.openatWasi(self.fd, sub_path, symlink_flags, w.O.DIRECTORY, 0x0, base, inheriting); + } + }; const fd = result catch |err| switch (err) { error.FileTooBig => unreachable, // can't happen for directories error.IsDir => unreachable, // we're providing O.DIRECTORY @@ -1622,7 +1658,7 @@ pub const Dir = struct { const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); return self.deleteFileW(sub_path_w.span()); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - os.unlinkatWasi(self.fd, sub_path, 0) catch |err| switch (err) { + os.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) { error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR else => |e| return e, }; @@ -1761,7 +1797,7 @@ pub const Dir = struct { sym_link_path: []const u8, _: SymLinkFlags, ) !void { - return os.symlinkatWasi(target_path, self.fd, sym_link_path); + return os.symlinkat(target_path, self.fd, sym_link_path); } /// Same as `symLink`, except the pathname parameters are null-terminated. @@ -1807,7 +1843,7 @@ pub const Dir = struct { /// WASI-only. Same as `readLink` except targeting WASI. pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { - return os.readlinkatWasi(self.fd, sub_path, buffer); + return os.readlinkat(self.fd, sub_path, buffer); } /// Same as `readLink`, except the `pathname` parameter is null-terminated. @@ -1870,6 +1906,7 @@ pub const Dir = struct { } pub const DeleteTreeError = error{ + InvalidHandle, AccessDenied, FileTooBig, SymLinkLoop, @@ -1935,6 +1972,7 @@ pub const Dir = struct { continue :start_over; }, + error.InvalidHandle, error.AccessDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, @@ -2002,6 +2040,7 @@ pub const Dir = struct { continue :scan_dir; }, + error.InvalidHandle, error.AccessDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, @@ -2272,8 +2311,6 @@ pub const Dir = struct { pub fn cwd() Dir { if (builtin.os.tag == .windows) { return Dir{ .fd = os.windows.peb().ProcessParameters.CurrentDirectory.Handle }; - } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("WASI doesn't have a concept of cwd(); use std.fs.wasi.PreopenList to get available Dir handles instead"); } else { return Dir{ .fd = os.AT.FDCWD }; } @@ -2285,26 +2322,17 @@ pub fn cwd() Dir { /// /// Asserts that the path parameter has no null bytes. pub fn openDirAbsolute(absolute_path: []const u8, flags: Dir.OpenDirOptions) File.OpenError!Dir { - if (builtin.os.tag == .wasi) { - @compileError("WASI doesn't have the concept of an absolute directory; use openDir instead for WASI."); - } assert(path.isAbsolute(absolute_path)); return cwd().openDir(absolute_path, flags); } /// Same as `openDirAbsolute` but the path parameter is null-terminated. pub fn openDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenDirOptions) File.OpenError!Dir { - if (builtin.os.tag == .wasi) { - @compileError("WASI doesn't have the concept of an absolute directory; use openDir instead for WASI."); - } assert(path.isAbsoluteZ(absolute_path_c)); return cwd().openDirZ(absolute_path_c, flags); } /// Same as `openDirAbsolute` but the path parameter is null-terminated. pub fn openDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenDirOptions) File.OpenError!Dir { - if (builtin.os.tag == .wasi) { - @compileError("WASI doesn't have the concept of an absolute directory; use openDir instead for WASI."); - } assert(path.isAbsoluteWindowsW(absolute_path_c)); return cwd().openDirW(absolute_path_c, flags); } @@ -2339,25 +2367,16 @@ pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) Fi /// open it and handle the error for file not found. /// See `accessAbsoluteZ` for a function that accepts a null-terminated path. pub fn accessAbsolute(absolute_path: []const u8, flags: File.OpenFlags) Dir.AccessError!void { - if (builtin.os.tag == .wasi) { - @compileError("WASI doesn't have the concept of an absolute path; use access instead for WASI."); - } assert(path.isAbsolute(absolute_path)); try cwd().access(absolute_path, flags); } /// Same as `accessAbsolute` but the path parameter is null-terminated. pub fn accessAbsoluteZ(absolute_path: [*:0]const u8, flags: File.OpenFlags) Dir.AccessError!void { - if (builtin.os.tag == .wasi) { - @compileError("WASI doesn't have the concept of an absolute path; use access instead for WASI."); - } assert(path.isAbsoluteZ(absolute_path)); try cwd().accessZ(absolute_path, flags); } /// Same as `accessAbsolute` but the path parameter is WTF-16 encoded. pub fn accessAbsoluteW(absolute_path: [*:0]const 16, flags: File.OpenFlags) Dir.AccessError!void { - if (builtin.os.tag == .wasi) { - @compileError("WASI doesn't have the concept of an absolute path; use access instead for WASI."); - } assert(path.isAbsoluteWindowsW(absolute_path)); try cwd().accessW(absolute_path, flags); } @@ -2458,9 +2477,6 @@ pub const SymLinkFlags = struct { /// If `sym_link_path` exists, it will not be overwritten. /// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`. pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymLinkFlags) !void { - if (builtin.os.tag == .wasi) { - @compileError("symLinkAbsolute is not supported in WASI; use Dir.symLinkWasi instead"); - } assert(path.isAbsolute(target_path)); assert(path.isAbsolute(sym_link_path)); if (builtin.os.tag == .windows) { diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index fbf5bd99eeb8..07ca298927c8 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -8,6 +8,7 @@ const fmt = std.fmt; const Allocator = mem.Allocator; const math = std.math; const windows = std.os.windows; +const os = std.os; const fs = std.fs; const process = std.process; const native_os = builtin.target.os.tag; @@ -733,7 +734,8 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) ![]u8 { } test "resolve" { - if (native_os == .wasi) return error.SkipZigTest; + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); const cwd = try process.getCwdAlloc(testing.allocator); defer testing.allocator.free(cwd); @@ -753,7 +755,8 @@ test "resolveWindows" { // TODO https://github.com/ziglang/zig/issues/3288 return error.SkipZigTest; } - if (native_os == .wasi) return error.SkipZigTest; + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); if (native_os == .windows) { const cwd = try process.getCwdAlloc(testing.allocator); defer testing.allocator.free(cwd); @@ -798,7 +801,8 @@ test "resolveWindows" { } test "resolvePosix" { - if (native_os == .wasi) return error.SkipZigTest; + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); try testResolvePosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c"); try testResolvePosix(&[_][]const u8{ "/a/b", "c", "//d", "e///" }, "/d/e"); @@ -1211,7 +1215,8 @@ test "relative" { // TODO https://github.com/ziglang/zig/issues/3288 return error.SkipZigTest; } - if (native_os == .wasi) return error.SkipZigTest; + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games"); try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", ".."); diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index b00b0609b26b..9791165eae71 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1,6 +1,7 @@ const std = @import("../std.zig"); const builtin = @import("builtin"); const testing = std.testing; +const os = std.os; const fs = std.fs; const mem = std.mem; const wasi = std.os.wasi; @@ -45,7 +46,8 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo } test "accessAbsolute" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; + if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -63,7 +65,8 @@ test "accessAbsolute" { } test "openDirAbsolute" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; + if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -99,7 +102,8 @@ test "openDir cwd parent .." { } test "readLinkAbsolute" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; + if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -507,7 +511,8 @@ test "rename" { } test "renameAbsolute" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; + if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); @@ -941,7 +946,8 @@ test "open file with exclusive nonblocking lock twice (absolute paths)" { } test "walker" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; + if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); @@ -991,7 +997,8 @@ test "walker" { } test ". and .. in fs.Dir functions" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; + if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1019,7 +1026,8 @@ test ". and .. in fs.Dir functions" { } test ". and .. in absolute functions" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; + if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); var tmp = tmpDir(.{}); defer tmp.cleanup(); diff --git a/lib/std/fs/wasi.zig b/lib/std/fs/wasi.zig index 1a033653d3d5..061a745855c7 100644 --- a/lib/std/fs/wasi.zig +++ b/lib/std/fs/wasi.zig @@ -25,13 +25,29 @@ pub const PreopenType = union(PreopenTypeTag) { const Self = @This(); pub fn eql(self: Self, other: PreopenType) bool { - if (!mem.eql(u8, @tagName(self), @tagName(other))) return false; + if (std.meta.activeTag(self) != std.meta.activeTag(other)) return false; switch (self) { PreopenTypeTag.Dir => |this_path| return mem.eql(u8, this_path, other.Dir), } } + // Checks whether `other` refers to a subdirectory of `self` and, if so, + // returns the relative path to `other` from `self` + pub fn getRelativePath(self: Self, other: PreopenType) ?[]const u8 { + if (std.meta.activeTag(self) != std.meta.activeTag(other)) return null; + + switch (self) { + PreopenTypeTag.Dir => |this_path| { + const other_path = other.Dir; + if (mem.indexOfDiff(u8, this_path, other_path)) |index| { + if (index < this_path.len) return null; + } + return other_path[this_path.len..]; + }, + } + } + pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { _ = fmt; _ = options; @@ -62,6 +78,15 @@ pub const Preopen = struct { } }; +/// WASI resource identifier struct. This is effectively a path within +/// a WASI Preopen. +pub const PreopenUri = struct { + /// WASI Preopen containing the resource. + base: Preopen, + /// Path to resource within `base`. + relative_path: []const u8, +}; + /// Dynamically-sized array list of WASI preopens. This struct is a /// convenience wrapper for issuing `std.os.wasi.fd_prestat_get` and /// `std.os.wasi.fd_prestat_dir_name` syscalls to the WASI runtime, and @@ -137,12 +162,38 @@ pub const PreopenList = struct { .SUCCESS => {}, else => |err| return os.unexpectedErrno(err), } + const preopen = Preopen.new(fd, PreopenType{ .Dir = path_buf }); try self.buffer.append(preopen); fd = try math.add(fd_t, fd, 1); } } + /// Find a preopen which includes access to `preopen_type`. + /// + /// If the preopen exists, `relative_path` is updated to point to the relative + /// portion of `preopen_type` and the matching Preopen is returned. If multiple + /// preopens match the provided resource, the most recent one is used. + pub fn findContaining(self: Self, preopen_type: PreopenType) ?PreopenUri { + // Search in reverse, so that most recently added preopens take precedence + var k: usize = self.buffer.items.len; + while (k > 0) { + k -= 1; + + const preopen = self.buffer.items[k]; + if (preopen.@"type".getRelativePath(preopen_type)) |rel_path_orig| { + var rel_path = rel_path_orig; + while (rel_path.len > 0 and rel_path[0] == '/') rel_path = rel_path[1..]; + + return PreopenUri{ + .base = preopen, + .relative_path = if (rel_path.len == 0) "." else rel_path, + }; + } + } + return null; + } + /// Find preopen by type. If the preopen exists, return it. /// Otherwise, return `null`. pub fn find(self: Self, preopen_type: PreopenType) ?*const Preopen { @@ -173,8 +224,6 @@ test "extracting WASI preopens" { try preopens.populate(); - try std.testing.expectEqual(@as(usize, 1), preopens.asSlice().len); - const preopen = preopens.find(PreopenType{ .Dir = "." }) orelse unreachable; - try std.testing.expect(preopen.@"type".eql(PreopenType{ .Dir = "." })); - try std.testing.expectEqual(@as(i32, 3), preopen.fd); + const preopen = preopens.find(PreopenType{ .Dir = "/cwd" }) orelse unreachable; + try std.testing.expect(preopen.@"type".eql(PreopenType{ .Dir = "/cwd" })); } diff --git a/lib/std/os.zig b/lib/std/os.zig index 3e230b773db1..5b904f6905f7 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -21,9 +21,13 @@ const assert = std.debug.assert; const math = std.math; const mem = std.mem; const elf = std.elf; +const fs = std.fs; const dl = @import("dynamic_library.zig"); const MAX_PATH_BYTES = std.fs.MAX_PATH_BYTES; const is_windows = builtin.os.tag == .windows; +const Allocator = std.mem.Allocator; +const Preopen = std.fs.wasi.Preopen; +const PreopenList = std.fs.wasi.PreopenList; pub const darwin = std.c; pub const dragonfly = std.c; @@ -93,7 +97,12 @@ pub const MAX_ADDR_LEN = system.MAX_ADDR_LEN; pub const MMAP2_UNIT = system.MMAP2_UNIT; pub const MSG = system.MSG; pub const NAME_MAX = system.NAME_MAX; -pub const O = system.O; +pub const O = switch (builtin.os.tag) { + // We want to expose the POSIX-like OFLAGS, so we use std.c.wasi.O instead + // of std.os.wasi.O, which is for non-POSIX-like `wasi.path_open`, etc. + .wasi => std.c.O, + else => system.O, +}; pub const PATH_MAX = system.PATH_MAX; pub const POLL = system.POLL; pub const POSIX_FADV = system.POSIX_FADV; @@ -210,6 +219,13 @@ pub const LOG = struct { pub const DEBUG = 7; }; +pub const RelativePath = struct { + /// Handle to directory + dir_fd: fd_t, + /// Path to resource within `dir_fd`. + relative_path: []const u8, +}; + pub const socket_t = if (builtin.os.tag == .windows) windows.ws2_32.SOCKET else fd_t; /// See also `getenv`. Populated by startup code before main(). @@ -1239,6 +1255,9 @@ pub fn pwritev(fd: fd_t, iov: []const iovec_const, offset: u64) PWriteError!usiz } pub const OpenError = error{ + /// In WASI, this error may occur when the provided file handle is invalid. + InvalidHandle, + /// In WASI, this error may occur when the file descriptor does /// not hold the required rights to open a new resource relative to it. AccessDenied, @@ -1300,6 +1319,8 @@ pub fn open(file_path: []const u8, flags: u32, perm: mode_t) OpenError!fd_t { if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); return openW(file_path_w.span(), flags, perm); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return openat(wasi.AT.FDCWD, file_path, flags, perm); } const file_path_c = try toPosixPath(file_path); return openZ(&file_path_c, flags, perm); @@ -1311,6 +1332,8 @@ pub fn openZ(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path); return openW(file_path_w.span(), flags, perm); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return open(mem.sliceTo(file_path, 0), flags, perm); } const open_sym = if (builtin.os.tag == .linux and builtin.link_libc) @@ -1347,7 +1370,7 @@ pub fn openZ(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t } } -fn openOptionsFromFlags(flags: u32) windows.OpenFileOptions { +fn openOptionsFromFlagsWindows(flags: u32) windows.OpenFileOptions { const w = windows; var access_mask: w.ULONG = w.READ_CONTROL | w.FILE_WRITE_ATTRIBUTES | w.SYNCHRONIZE; @@ -1387,7 +1410,7 @@ fn openOptionsFromFlags(flags: u32) windows.OpenFileOptions { /// or makes use of perm argument. pub fn openW(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t { _ = perm; - var options = openOptionsFromFlags(flags); + var options = openOptionsFromFlagsWindows(flags); options.dir = std.fs.cwd().fd; return windows.OpenFile(file_path_w, options) catch |err| switch (err) { error.WouldBlock => unreachable, @@ -1396,21 +1419,187 @@ pub fn openW(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t }; } +var wasi_cwd = if (builtin.os.tag == .wasi and !builtin.link_libc) struct { + // List of available Preopens + preopens: ?PreopenList = null, + // Memory buffer for storing the relative portion of the CWD + path_buffer: [MAX_PATH_BYTES]u8 = undefined, + // Current Working Directory, stored as an fd_t and a relative path + cwd: ?RelativePath = null, + // Preopen associated with `cwd`, if any + cwd_preopen: ?Preopen = null, +}{} else undefined; + +/// Initialize the available Preopen list on WASI and set the CWD to `cwd_init`. +/// +/// This must be called before using any relative or absolute paths with `std.os` +/// functions, if you are on WASI without linking libc. +/// +/// `alloc` must not be a temporary or leak-detecting allocator, since `std.os` +/// retains ownership of allocations internally and may never call free(). +pub fn initPreopensWasi(alloc: Allocator, cwd_init: ?[]const u8) !void { + if (builtin.os.tag == .wasi) { + if (!builtin.link_libc) { + if (wasi_cwd.preopens == null) { + var preopen_list = PreopenList.init(alloc); + try preopen_list.populate(); + wasi_cwd.preopens = preopen_list; + } + if (cwd_init) |cwd| { + const preopen = wasi_cwd.preopens.?.findContaining(.{ .Dir = cwd }); + if (preopen) |po| { + wasi_cwd.cwd_preopen = po.base; + wasi_cwd.cwd = RelativePath{ + .dir_fd = po.base.fd, + .relative_path = po.relative_path, + }; + } else { + // No matching preopen found + return error.FileNotFound; + } + } + } else { + if (cwd_init) |cwd| try chdir(cwd); + } + } +} + +/// Resolve a relative or absolute path to an handle (`fd_t`) and a relative subpath. +/// +/// For absolute paths, this automatically searches among available Preopens to find +/// a match. For relative paths, it uses the "emulated" CWD. +pub fn resolvePathWasi(path: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) !RelativePath { + // Note: Due to WASI's "sandboxed" file handles, operations with this RelativePath + // will fail if the relative path navigates outside of `dir_fd` using ".." + return resolvePathAndGetWasiPreopen(path, null, out_buffer); +} + +fn resolvePathAndGetWasiPreopen(path: []const u8, preopen: ?*?Preopen, out_buffer: *[MAX_PATH_BYTES]u8) !RelativePath { + var allocator = std.heap.FixedBufferAllocator.init(out_buffer); + var alloc = allocator.allocator(); + + if (fs.path.isAbsolute(path) or wasi_cwd.cwd == null) { + if (wasi_cwd.preopens == null) @panic("On WASI, `initPreopensWasi` must be called to initialize preopens " ++ + "before using any CWD-relative or absolute paths.\n"); + + // If the path is absolute, we need to lookup a containing Preopen + const abs_path = std.fs.path.resolve(alloc, &.{ "/", path }) catch return error.NameTooLong; + const preopen_uri = wasi_cwd.preopens.?.findContaining(.{ .Dir = abs_path }); + + if (preopen_uri) |po| { + if (preopen) |p| p.* = po.base; + return RelativePath{ + .dir_fd = po.base.fd, + .relative_path = po.relative_path, + }; + } else { + // No matching preopen found + return error.AccessDenied; + } + } else { + const cwd = wasi_cwd.cwd.?; + + // If the path is empty or "." or "./", return CWD + if (std.mem.eql(u8, path, ".") or std.mem.eql(u8, path, "./")) { + return cwd; + } + + // First resolve a combined path, where the "/" corresponds to `cwd.dir_fd` + // not the true filesystem root + const paths = &.{ "/", cwd.relative_path, path }; + const resolved_path = std.fs.path.resolve(alloc, paths) catch return error.NameTooLong; + + // Strip off the fake root to get the relative path w.r.t. `cwd.dir_fd` + const resolved_relative_path = resolved_path[1..]; + + if (preopen) |p| p.* = wasi_cwd.cwd_preopen; + return RelativePath{ + .dir_fd = cwd.dir_fd, + .relative_path = resolved_relative_path, + }; + } +} + /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// See also `openatZ`. pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) OpenError!fd_t { - if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("use openatWasi instead"); - } if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); return openatW(dir_fd, file_path_w.span(), flags, mode); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + // `mode` is ignored on WASI, which does not support unix-style file permissions + const fd = if (dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(file_path)) blk: { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var path_buf: [MAX_PATH_BYTES]u8 = undefined; + const path = try resolvePathWasi(file_path, &path_buf); + + const opts = try openOptionsFromFlagsWasi(path.dir_fd, flags); + break :blk try openatWasi(path.dir_fd, path.relative_path, opts.lookup_flags, opts.oflags, opts.fs_flags, opts.fs_rights_base, opts.fs_rights_inheriting); + } else blk: { + const opts = try openOptionsFromFlagsWasi(dir_fd, flags); + break :blk try openatWasi(dir_fd, file_path, opts.lookup_flags, opts.oflags, opts.fs_flags, opts.fs_rights_base, opts.fs_rights_inheriting); + }; + errdefer close(fd); + + const info = try fstat(fd); + if (flags & O.WRONLY != 0 and info.filetype == .DIRECTORY) + return error.IsDir; + + return fd; } const file_path_c = try toPosixPath(file_path); return openatZ(dir_fd, &file_path_c, flags, mode); } +const WasiOpenOptions = struct { + oflags: wasi.oflags_t, + lookup_flags: wasi.lookupflags_t, + fs_rights_base: wasi.rights_t, + fs_rights_inheriting: wasi.rights_t, + fs_flags: wasi.fdflags_t, +}; + +/// Compute rights + flags corresponding to the provided POSIX access mode. +fn openOptionsFromFlagsWasi(fd: fd_t, oflag: u32) OpenError!WasiOpenOptions { + const w = std.os.wasi; + + // First, discover the rights that we can derive from `fd` + var fsb_cur: wasi.fdstat_t = undefined; + _ = switch (w.fd_fdstat_get(fd, &fsb_cur)) { + .SUCCESS => .{}, + .BADF => return error.InvalidHandle, + else => |err| return unexpectedErrno(err), + }; + + // Next, calculate the read/write rights to request, depending on the + // provided POSIX access mode + var rights: w.rights_t = 0; + if (oflag & O.RDONLY != 0) { + rights |= w.RIGHT.FD_READ | w.RIGHT.FD_READDIR; + } + if (oflag & O.WRONLY != 0) { + rights |= w.RIGHT.FD_DATASYNC | w.RIGHT.FD_WRITE | + w.RIGHT.FD_ALLOCATE | w.RIGHT.FD_FILESTAT_SET_SIZE; + } + + // Request all other rights unconditionally + rights |= ~(w.RIGHT.FD_DATASYNC | w.RIGHT.FD_READ | + w.RIGHT.FD_WRITE | w.RIGHT.FD_ALLOCATE | + w.RIGHT.FD_READDIR | w.RIGHT.FD_FILESTAT_SET_SIZE); + + // But only take rights that we can actually inherit + rights &= fsb_cur.fs_rights_inheriting; + + return WasiOpenOptions{ + .oflags = @truncate(w.oflags_t, (oflag >> 12)) & 0xfff, + .lookup_flags = if (oflag & O.NOFOLLOW == 0) w.LOOKUP_SYMLINK_FOLLOW else 0, + .fs_rights_base = rights, + .fs_rights_inheriting = fsb_cur.fs_rights_inheriting, + .fs_flags = @truncate(w.fdflags_t, oflag & 0xfff), + }; +} + /// Open and possibly create a file in WASI. pub fn openatWasi(dir_fd: fd_t, file_path: []const u8, lookup_flags: lookupflags_t, oflags: oflags_t, fdflags: fdflags_t, base: rights_t, inheriting: rights_t) OpenError!fd_t { while (true) { @@ -1450,6 +1639,8 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path); return openatW(dir_fd, file_path_w.span(), flags, mode); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return openat(dir_fd, mem.sliceTo(file_path, 0), flags, mode); } const openat_sym = if (builtin.os.tag == .linux and builtin.link_libc) @@ -1496,7 +1687,7 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) /// or makes use of perm argument. pub fn openatW(dir_fd: fd_t, file_path_w: []const u16, flags: u32, mode: mode_t) OpenError!fd_t { _ = mode; - var options = openOptionsFromFlags(flags); + var options = openOptionsFromFlagsWindows(flags); options.dir = dir_fd; return windows.OpenFile(file_path_w, options) catch |err| switch (err) { error.WouldBlock => unreachable, @@ -1764,9 +1955,29 @@ pub const GetCwdError = error{ pub fn getcwd(out_buffer: []u8) GetCwdError![]u8 { if (builtin.os.tag == .windows) { return windows.GetCurrentDirectory(out_buffer); - } - if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("WASI doesn't have a concept of cwd(); use std.fs.wasi.PreopenList to get available Dir handles instead"); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + var allocator = std.heap.FixedBufferAllocator.init(out_buffer); + var alloc = allocator.allocator(); + if (wasi_cwd.cwd) |cwd| { + if (wasi_cwd.cwd_preopen) |po| { + var base_cwd_dir = switch (po.@"type") { + .Dir => |dir| dir, + }; + if (!fs.path.isAbsolute(base_cwd_dir)) { + // This preopen is not based on an absolute path, so we have + // no way to know the absolute path of the CWD + return error.CurrentWorkingDirectoryUnlinked; + } + const paths = &.{ base_cwd_dir, cwd.relative_path }; + return std.fs.path.resolve(alloc, paths) catch return error.NameTooLong; + } else { + // The CWD is not rooted to an existing Preopen, + // so we have no way to know its absolute path + return error.CurrentWorkingDirectoryUnlinked; + } + } else { + return alloc.dupe(u8, "/") catch return error.NameTooLong; + } } const err = if (builtin.link_libc) blk: { @@ -1809,11 +2020,10 @@ pub const SymLinkError = error{ /// If `sym_link_path` exists, it will not be overwritten. /// See also `symlinkZ. pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError!void { - if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("symlink is not supported in WASI; use symlinkat instead"); - } if (builtin.os.tag == .windows) { @compileError("symlink is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return symlinkat(target_path, wasi.AT.FDCWD, sym_link_path); } const target_path_c = try toPosixPath(target_path); const sym_link_path_c = try toPosixPath(sym_link_path); @@ -1825,6 +2035,8 @@ pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError! pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLinkError!void { if (builtin.os.tag == .windows) { @compileError("symlink is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return symlink(mem.sliceTo(target_path, 0), mem.sliceTo(sym_link_path, 0)); } switch (errno(system.symlink(target_path, sym_link_path))) { .SUCCESS => return, @@ -1853,11 +2065,16 @@ pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLin /// If `sym_link_path` exists, it will not be overwritten. /// See also `symlinkatWasi`, `symlinkatZ` and `symlinkatW`. pub fn symlinkat(target_path: []const u8, newdirfd: fd_t, sym_link_path: []const u8) SymLinkError!void { - if (builtin.os.tag == .wasi and !builtin.link_libc) { - return symlinkatWasi(target_path, newdirfd, sym_link_path); - } if (builtin.os.tag == .windows) { @compileError("symlinkat is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + if (newdirfd == wasi.AT.FDCWD or fs.path.isAbsolute(target_path)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var path_buf: [MAX_PATH_BYTES]u8 = undefined; + const path = try resolvePathWasi(sym_link_path, &path_buf); + return symlinkatWasi(target_path, path.dir_fd, path.relative_path); + } + return symlinkatWasi(target_path, newdirfd, sym_link_path); } const target_path_c = try toPosixPath(target_path); const sym_link_path_c = try toPosixPath(sym_link_path); @@ -1893,6 +2110,8 @@ pub fn symlinkatWasi(target_path: []const u8, newdirfd: fd_t, sym_link_path: []c pub fn symlinkatZ(target_path: [*:0]const u8, newdirfd: fd_t, sym_link_path: [*:0]const u8) SymLinkError!void { if (builtin.os.tag == .windows) { @compileError("symlinkat is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return symlinkat(mem.sliceTo(target_path, 0), newdirfd, mem.sliceTo(sym_link_path, 0)); } switch (errno(system.symlinkat(target_path, newdirfd, sym_link_path))) { .SUCCESS => return, @@ -1930,6 +2149,9 @@ pub const LinkError = UnexpectedError || error{ }; pub fn linkZ(oldpath: [*:0]const u8, newpath: [*:0]const u8, flags: i32) LinkError!void { + if (builtin.os.tag == .wasi and !builtin.link_libc) { + return link(mem.sliceTo(oldpath, 0), mem.sliceTo(newpath, 0), flags); + } switch (errno(system.link(oldpath, newpath, flags))) { .SUCCESS => return, .ACCES => return error.AccessDenied, @@ -1952,6 +2174,12 @@ pub fn linkZ(oldpath: [*:0]const u8, newpath: [*:0]const u8, flags: i32) LinkErr } pub fn link(oldpath: []const u8, newpath: []const u8, flags: i32) LinkError!void { + if (builtin.os.tag == .wasi and !builtin.link_libc) { + return linkat(wasi.AT.FDCWD, oldpath, wasi.AT.FDCWD, newpath, flags) catch |err| switch (err) { + error.NotDir => unreachable, // link() does not support directories + else => |e| return e, + }; + } const old = try toPosixPath(oldpath); const new = try toPosixPath(newpath); return try linkZ(&old, &new, flags); @@ -1966,6 +2194,9 @@ pub fn linkatZ( newpath: [*:0]const u8, flags: i32, ) LinkatError!void { + if (builtin.os.tag == .wasi and !builtin.link_libc) { + return linkat(olddir, mem.sliceTo(oldpath, 0), newdir, mem.sliceTo(newpath, 0), flags); + } switch (errno(system.linkat(olddir, oldpath, newdir, newpath, flags))) { .SUCCESS => return, .ACCES => return error.AccessDenied, @@ -1995,11 +2226,62 @@ pub fn linkat( newpath: []const u8, flags: i32, ) LinkatError!void { + if (builtin.os.tag == .wasi and !builtin.link_libc) { + var resolve_olddir: bool = (olddir == wasi.AT.FDCWD or fs.path.isAbsolute(oldpath)); + var resolve_newdir: bool = (newdir == wasi.AT.FDCWD or fs.path.isAbsolute(newpath)); + + var old: RelativePath = .{ .dir_fd = olddir, .relative_path = oldpath }; + var new: RelativePath = .{ .dir_fd = newdir, .relative_path = newpath }; + + // Resolve absolute or CWD-relative paths to a path within a Preopen + if (resolve_olddir or resolve_newdir) { + var buf_old: [MAX_PATH_BYTES]u8 = undefined; + var buf_new: [MAX_PATH_BYTES]u8 = undefined; + + if (resolve_olddir) + old = try resolvePathWasi(oldpath, &buf_old); + + if (resolve_newdir) + new = try resolvePathWasi(newpath, &buf_new); + + return linkatWasi(old, new, flags); + } + return linkatWasi(old, new, flags); + } const old = try toPosixPath(oldpath); const new = try toPosixPath(newpath); return try linkatZ(olddir, &old, newdir, &new, flags); } +/// WASI-only. The same as `linkat` but targeting WASI. +/// See also `linkat`. +pub fn linkatWasi(old: RelativePath, new: RelativePath, flags: i32) LinkatError!void { + var old_flags: wasi.lookupflags_t = 0; + // TODO: Why is this not defined in wasi-libc? + if (flags & linux.AT.SYMLINK_FOLLOW != 0) old_flags |= wasi.LOOKUP_SYMLINK_FOLLOW; + + switch (wasi.path_link(old.dir_fd, old_flags, old.relative_path.ptr, old.relative_path.len, new.dir_fd, new.relative_path.ptr, new.relative_path.len)) { + .SUCCESS => return, + .ACCES => return error.AccessDenied, + .DQUOT => return error.DiskQuota, + .EXIST => return error.PathAlreadyExists, + .FAULT => unreachable, + .IO => return error.FileSystem, + .LOOP => return error.SymLinkLoop, + .MLINK => return error.LinkQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .PERM => return error.AccessDenied, + .ROFS => return error.ReadOnlyFileSystem, + .XDEV => return error.NotSameFileSystem, + .INVAL => unreachable, + else => |err| return unexpectedErrno(err), + } +} + pub const UnlinkError = error{ FileNotFound, @@ -2027,7 +2309,10 @@ pub const UnlinkError = error{ /// See also `unlinkZ`. pub fn unlink(file_path: []const u8) UnlinkError!void { if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("unlink is not supported in WASI; use unlinkat instead"); + return unlinkat(wasi.AT.FDCWD, file_path, 0) catch |err| switch (err) { + error.DirNotEmpty => unreachable, // only occurs when targeting directories + else => |e| return e, + }; } else if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); return unlinkW(file_path_w.span()); @@ -2042,6 +2327,8 @@ pub fn unlinkZ(file_path: [*:0]const u8) UnlinkError!void { if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path); return unlinkW(file_path_w.span()); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return unlink(mem.sliceTo(file_path, 0)); } switch (errno(system.unlink(file_path))) { .SUCCESS => return, @@ -2079,6 +2366,12 @@ pub fn unlinkat(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatError!vo const file_path_w = try windows.sliceToPrefixedFileW(file_path); return unlinkatW(dirfd, file_path_w.span(), flags); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(file_path)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var path_buf: [MAX_PATH_BYTES]u8 = undefined; + const path = try resolvePathWasi(file_path, &path_buf); + return unlinkatWasi(path.dir_fd, path.relative_path, flags); + } return unlinkatWasi(dirfd, file_path, flags); } else { const file_path_c = try toPosixPath(file_path); @@ -2123,6 +2416,8 @@ pub fn unlinkatZ(dirfd: fd_t, file_path_c: [*:0]const u8, flags: u32) UnlinkatEr if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path_c); return unlinkatW(dirfd, file_path_w.span(), flags); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return unlinkat(dirfd, mem.sliceTo(file_path_c, 0), flags); } switch (errno(system.unlinkat(dirfd, file_path_c, flags))) { .SUCCESS => return, @@ -2181,7 +2476,7 @@ pub const RenameError = error{ /// Change the name or location of a file. pub fn rename(old_path: []const u8, new_path: []const u8) RenameError!void { if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("rename is not supported in WASI; use renameat instead"); + return renameat(wasi.AT.FDCWD, old_path, wasi.AT.FDCWD, new_path); } else if (builtin.os.tag == .windows) { const old_path_w = try windows.sliceToPrefixedFileW(old_path); const new_path_w = try windows.sliceToPrefixedFileW(new_path); @@ -2199,6 +2494,8 @@ pub fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) RenameError!voi const old_path_w = try windows.cStrToPrefixedFileW(old_path); const new_path_w = try windows.cStrToPrefixedFileW(new_path); return renameW(old_path_w.span().ptr, new_path_w.span().ptr); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return rename(mem.sliceTo(old_path, 0), mem.sliceTo(new_path, 0)); } switch (errno(system.rename(old_path, new_path))) { .SUCCESS => return, @@ -2243,7 +2540,25 @@ pub fn renameat( const new_path_w = try windows.sliceToPrefixedFileW(new_path); return renameatW(old_dir_fd, old_path_w.span(), new_dir_fd, new_path_w.span(), windows.TRUE); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - return renameatWasi(old_dir_fd, old_path, new_dir_fd, new_path); + var resolve_old: bool = (old_dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(old_path)); + var resolve_new: bool = (new_dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(new_path)); + + var old: RelativePath = .{ .dir_fd = old_dir_fd, .relative_path = old_path }; + var new: RelativePath = .{ .dir_fd = new_dir_fd, .relative_path = new_path }; + + // Resolve absolute or CWD-relative paths to a path within a Preopen + if (resolve_old or resolve_new) { + var buf_old: [MAX_PATH_BYTES]u8 = undefined; + var buf_new: [MAX_PATH_BYTES]u8 = undefined; + + if (resolve_old) + old = try resolvePathWasi(old_path, &buf_old); + if (resolve_new) + new = try resolvePathWasi(new_path, &buf_new); + + return renameatWasi(old, new); + } + return renameatWasi(old, new); } else { const old_path_c = try toPosixPath(old_path); const new_path_c = try toPosixPath(new_path); @@ -2253,8 +2568,8 @@ pub fn renameat( /// WASI-only. Same as `renameat` expect targeting WASI. /// See also `renameat`. -pub fn renameatWasi(old_dir_fd: fd_t, old_path: []const u8, new_dir_fd: fd_t, new_path: []const u8) RenameError!void { - switch (wasi.path_rename(old_dir_fd, old_path.ptr, old_path.len, new_dir_fd, new_path.ptr, new_path.len)) { +pub fn renameatWasi(old: RelativePath, new: RelativePath) RenameError!void { + switch (wasi.path_rename(old.dir_fd, old.relative_path.ptr, old.relative_path.len, new.dir_fd, new.relative_path.ptr, new.relative_path.len)) { .SUCCESS => return, .ACCES => return error.AccessDenied, .PERM => return error.AccessDenied, @@ -2290,6 +2605,8 @@ pub fn renameatZ( const old_path_w = try windows.cStrToPrefixedFileW(old_path); const new_path_w = try windows.cStrToPrefixedFileW(new_path); return renameatW(old_dir_fd, old_path_w.span(), new_dir_fd, new_path_w.span(), windows.TRUE); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return renameat(old_dir_fd, mem.sliceTo(old_path, 0), new_dir_fd, mem.sliceTo(new_path, 0)); } switch (errno(system.renameat(old_dir_fd, old_path, new_dir_fd, new_path))) { @@ -2380,6 +2697,12 @@ pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: u32) MakeDirError!v const sub_dir_path_w = try windows.sliceToPrefixedFileW(sub_dir_path); return mkdiratW(dir_fd, sub_dir_path_w.span(), mode); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + if (dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(sub_dir_path)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var path_buf: [MAX_PATH_BYTES]u8 = undefined; + const path = try resolvePathWasi(sub_dir_path, &path_buf); + return mkdiratWasi(path.dir_fd, path.relative_path, mode); + } return mkdiratWasi(dir_fd, sub_dir_path, mode); } else { const sub_dir_path_c = try toPosixPath(sub_dir_path); @@ -2414,6 +2737,8 @@ pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: u32) MakeDirErr if (builtin.os.tag == .windows) { const sub_dir_path_w = try windows.cStrToPrefixedFileW(sub_dir_path); return mkdiratW(dir_fd, sub_dir_path_w.span().ptr, mode); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return mkdirat(dir_fd, mem.sliceTo(sub_dir_path, 0), mode); } switch (errno(system.mkdirat(dir_fd, sub_dir_path, mode))) { .SUCCESS => return, @@ -2472,10 +2797,10 @@ pub const MakeDirError = error{ } || UnexpectedError; /// Create a directory. -/// `mode` is ignored on Windows. +/// `mode` is ignored on Windows and WASI. pub fn mkdir(dir_path: []const u8, mode: u32) MakeDirError!void { if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("mkdir is not supported in WASI; use mkdirat instead"); + return mkdirat(wasi.AT.FDCWD, dir_path, mode); } else if (builtin.os.tag == .windows) { const dir_path_w = try windows.sliceToPrefixedFileW(dir_path); return mkdirW(dir_path_w.span(), mode); @@ -2490,6 +2815,8 @@ pub fn mkdirZ(dir_path: [*:0]const u8, mode: u32) MakeDirError!void { if (builtin.os.tag == .windows) { const dir_path_w = try windows.cStrToPrefixedFileW(dir_path); return mkdirW(dir_path_w.span(), mode); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return mkdir(mem.sliceTo(dir_path, 0), mode); } switch (errno(system.mkdir(dir_path, mode))) { .SUCCESS => return, @@ -2545,7 +2872,11 @@ pub const DeleteDirError = error{ /// Deletes an empty directory. pub fn rmdir(dir_path: []const u8) DeleteDirError!void { if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("rmdir is not supported in WASI; use unlinkat instead"); + return unlinkat(wasi.AT.FDCWD, dir_path, AT.REMOVEDIR) catch |err| switch (err) { + error.FileSystem => unreachable, // only occurs when targeting files + error.IsDir => unreachable, // only occurs when targeting files + else => |e| return e, + }; } else if (builtin.os.tag == .windows) { const dir_path_w = try windows.sliceToPrefixedFileW(dir_path); return rmdirW(dir_path_w.span()); @@ -2560,6 +2891,8 @@ pub fn rmdirZ(dir_path: [*:0]const u8) DeleteDirError!void { if (builtin.os.tag == .windows) { const dir_path_w = try windows.cStrToPrefixedFileW(dir_path); return rmdirW(dir_path_w.span()); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return rmdir(mem.sliceTo(dir_path, 0)); } switch (errno(system.rmdir(dir_path))) { .SUCCESS => return, @@ -2606,7 +2939,17 @@ pub const ChangeCurDirError = error{ /// `dir_path` is recommended to be a UTF-8 encoded string. pub fn chdir(dir_path: []const u8) ChangeCurDirError!void { if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("chdir is not supported in WASI"); + var preopen: ?Preopen = null; + const path = try resolvePathAndGetWasiPreopen(dir_path, &preopen, &wasi_cwd.path_buffer); + + const dirinfo = try fstatat(path.dir_fd, path.relative_path, 0); + if (dirinfo.filetype != .DIRECTORY) { + return error.NotDir; + } + + wasi_cwd.cwd_preopen = preopen; + wasi_cwd.cwd = path; + return; } else if (builtin.os.tag == .windows) { var utf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; const len = try std.unicode.utf8ToUtf16Le(utf16_dir_path[0..], dir_path); @@ -2625,6 +2968,8 @@ pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { const len = try std.unicode.utf8ToUtf16Le(utf16_dir_path[0..], dir_path); if (len > utf16_dir_path.len) return error.NameTooLong; return chdirW(utf16_dir_path[0..len]); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return chdir(mem.sliceTo(dir_path, 0)); } switch (errno(system.chdir(dir_path))) { .SUCCESS => return, @@ -2655,15 +3000,29 @@ pub const FchdirError = error{ } || UnexpectedError; pub fn fchdir(dirfd: fd_t) FchdirError!void { - while (true) { - switch (errno(system.fchdir(dirfd))) { - .SUCCESS => return, - .ACCES => return error.AccessDenied, - .BADF => unreachable, - .NOTDIR => return error.NotDir, - .INTR => continue, - .IO => return error.FileSystem, - else => |err| return unexpectedErrno(err), + if (builtin.os.tag == .wasi) { + // Check that this is a directory + const dirinfo = fstatat(dirfd, ".", 0) catch unreachable; + if (dirinfo.filetype != .DIRECTORY) { + return error.NotDir; + } + + wasi_cwd.cwd = .{ + .dir_fd = dirfd, + .relative_path = ".", + }; + wasi_cwd.cwd_preopen = null; + } else { + while (true) { + switch (errno(system.fchdir(dirfd))) { + .SUCCESS => return, + .ACCES => return error.AccessDenied, + .BADF => unreachable, + .NOTDIR => return error.NotDir, + .INTR => continue, + .IO => return error.FileSystem, + else => |err| return unexpectedErrno(err), + } } } } @@ -2690,7 +3049,7 @@ pub const ReadLinkError = error{ /// The return value is a slice of `out_buffer` from index 0. pub fn readlink(file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 { if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("readlink is not supported in WASI; use readlinkat instead"); + return readlinkat(wasi.AT.FDCWD, file_path, out_buffer); } else if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); return readlinkW(file_path_w.span(), out_buffer); @@ -2711,6 +3070,8 @@ pub fn readlinkZ(file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToWin32PrefixedFileW(file_path); return readlinkW(file_path_w.span(), out_buffer); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return readlink(mem.sliceTo(file_path, 0), out_buffer); } const rc = system.readlink(file_path, out_buffer.ptr, out_buffer.len); switch (errno(rc)) { @@ -2733,6 +3094,12 @@ pub fn readlinkZ(file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 /// See also `readlinkatWasi`, `realinkatZ` and `realinkatW`. pub fn readlinkat(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 { if (builtin.os.tag == .wasi and !builtin.link_libc) { + if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(file_path)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var path_buf: [MAX_PATH_BYTES]u8 = undefined; + var path = try resolvePathWasi(file_path, &path_buf); + return readlinkatWasi(path.dir_fd, path.relative_path, out_buffer); + } return readlinkatWasi(dirfd, file_path, out_buffer); } if (builtin.os.tag == .windows) { @@ -2775,6 +3142,8 @@ pub fn readlinkatZ(dirfd: fd_t, file_path: [*:0]const u8, out_buffer: []u8) Read if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path); return readlinkatW(dirfd, file_path_w.span(), out_buffer); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return readlinkat(dirfd, mem.sliceTo(file_path, 0), out_buffer); } const rc = system.readlinkat(dirfd, file_path, out_buffer.ptr, out_buffer.len); switch (errno(rc)) { @@ -3727,7 +4096,14 @@ pub const FStatAtError = FStatError || error{ NameTooLong, FileNotFound, SymLink /// See also `fstatatZ` and `fstatatWasi`. pub fn fstatat(dirfd: fd_t, pathname: []const u8, flags: u32) FStatAtError!Stat { if (builtin.os.tag == .wasi and !builtin.link_libc) { - return fstatatWasi(dirfd, pathname, flags); + const wasi_flags = if (flags & linux.AT.SYMLINK_NOFOLLOW == 0) wasi.LOOKUP_SYMLINK_FOLLOW else 0; + if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(pathname)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var path_buf: [MAX_PATH_BYTES]u8 = undefined; + const path = try resolvePathWasi(pathname, &path_buf); + return fstatatWasi(path.dir_fd, path.relative_path, wasi_flags); + } + return fstatatWasi(dirfd, pathname, wasi_flags); } else if (builtin.os.tag == .windows) { @compileError("fstatat is not yet implemented on Windows"); } else { @@ -3758,6 +4134,10 @@ pub fn fstatatWasi(dirfd: fd_t, pathname: []const u8, flags: u32) FStatAtError!S /// Same as `fstatat` but `pathname` is null-terminated. /// See also `fstatat`. pub fn fstatatZ(dirfd: fd_t, pathname: [*:0]const u8, flags: u32) FStatAtError!Stat { + if (builtin.os.tag == .wasi and !builtin.link_libc) { + return fstatatWasi(dirfd, mem.sliceTo(pathname), flags); + } + const fstatat_sym = if (builtin.os.tag == .linux and builtin.link_libc) system.fstatat64 else @@ -4056,6 +4436,8 @@ pub fn access(path: []const u8, mode: u32) AccessError!void { const path_w = try windows.sliceToPrefixedFileW(path); _ = try windows.GetFileAttributesW(path_w.span().ptr); return; + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return faccessat(wasi.AT.FDCWD, path, mode, 0); } const path_c = try toPosixPath(path); return accessZ(&path_c, mode); @@ -4067,6 +4449,8 @@ pub fn accessZ(path: [*:0]const u8, mode: u32) AccessError!void { const path_w = try windows.cStrToPrefixedFileW(path); _ = try windows.GetFileAttributesW(path_w.span().ptr); return; + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return access(mem.sliceTo(path, 0), mode); } switch (errno(system.access(path, mode))) { .SUCCESS => return, @@ -4108,6 +4492,45 @@ pub fn faccessat(dirfd: fd_t, path: []const u8, mode: u32, flags: u32) AccessErr if (builtin.os.tag == .windows) { const path_w = try windows.sliceToPrefixedFileW(path); return faccessatW(dirfd, path_w.span().ptr, mode, flags); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + var resolved = RelativePath{ .dir_fd = dirfd, .relative_path = path }; + + const file = blk: { + if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(path)) { + // Resolve absolute or CWD-relative paths to a path within a Preopen + var path_buf: [MAX_PATH_BYTES]u8 = undefined; + resolved = resolvePathWasi(path, &path_buf) catch |err| break :blk @as(FStatAtError!Stat, err); + break :blk fstatat(resolved.dir_fd, resolved.relative_path, flags); + } + break :blk fstatat(dirfd, path, flags); + } catch |err| switch (err) { + error.AccessDenied => return error.PermissionDenied, + else => |e| return e, + }; + + if (mode != F_OK) { + var directory: wasi.fdstat_t = undefined; + if (wasi.fd_fdstat_get(resolved.dir_fd, &directory) != .SUCCESS) { + return error.PermissionDenied; + } + + var rights: wasi.rights_t = 0; + if (mode & R_OK != 0) { + rights |= if (file.filetype == .DIRECTORY) + wasi.RIGHT.FD_READDIR + else + wasi.RIGHT.FD_READ; + } + if (mode & W_OK != 0) { + rights |= wasi.RIGHT.FD_WRITE; + } + // No validation for X_OK + + if ((rights & directory.fs_rights_inheriting) != rights) { + return error.PermissionDenied; + } + } + return; } const path_c = try toPosixPath(path); return faccessatZ(dirfd, &path_c, mode, flags); @@ -4118,6 +4541,8 @@ pub fn faccessatZ(dirfd: fd_t, path: [*:0]const u8, mode: u32, flags: u32) Acces if (builtin.os.tag == .windows) { const path_w = try windows.cStrToPrefixedFileW(path); return faccessatW(dirfd, path_w.span().ptr, mode, flags); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return faccessat(dirfd, mem.sliceTo(path, 0), mode, flags); } switch (errno(system.faccessat(dirfd, path, mode, flags))) { .SUCCESS => return, @@ -4645,6 +5070,9 @@ pub const RealPathError = error{ SharingViolation, PipeBusy, + /// On WASI, the current CWD may not be associated with an absolute path. + InvalidHandle, + /// On Windows, file paths must be valid Unicode. InvalidUtf8, @@ -4660,9 +5088,33 @@ pub fn realpath(pathname: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealPathE if (builtin.os.tag == .windows) { const pathname_w = try windows.sliceToPrefixedFileW(pathname); return realpathW(pathname_w.span(), out_buffer); - } - if (builtin.os.tag == .wasi and !builtin.link_libc) { - @compileError("Use std.fs.wasi.PreopenList to obtain valid Dir handles instead of using absolute paths"); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + // NOTE: This emulation is incomplete. Symbolic links are not + // currently expanded during path canonicalization. + var alloc = std.heap.FixedBufferAllocator.init(out_buffer); + if (fs.path.isAbsolute(pathname)) + return try fs.path.resolve(alloc.allocator(), &.{pathname}) catch error.NameTooLong; + if (wasi_cwd.cwd) |cwd| { + if (wasi_cwd.cwd_preopen) |po| { + var base_cwd_dir = switch (po.@"type") { + .Dir => |dir| dir, + }; + if (!fs.path.isAbsolute(base_cwd_dir)) { + // This preopen is not based on an absolute path, so we have + // no way to know the absolute path of the CWD + return error.InvalidHandle; + } + + const paths = &.{ base_cwd_dir, cwd.relative_path, pathname }; + return fs.path.resolve(alloc.allocator(), paths) catch error.NameTooLong; + } else { + // The CWD is not rooted to an existing Preopen, + // so we have no way to know its absolute path + return error.InvalidHandle; + } + } else { + return try fs.path.resolve(alloc.allocator(), &.{ "/", pathname }) catch error.NameTooLong; + } } const pathname_c = try toPosixPath(pathname); return realpathZ(&pathname_c, out_buffer); @@ -4673,6 +5125,8 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP if (builtin.os.tag == .windows) { const pathname_w = try windows.cStrToPrefixedFileW(pathname); return realpathW(pathname_w.span(), out_buffer); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return realpath(mem.sliceTo(pathname, 0), out_buffer); } if (!builtin.link_libc) { const flags = if (builtin.os.tag == .linux) O.PATH | O.NONBLOCK | O.CLOEXEC else O.NONBLOCK | O.CLOEXEC; @@ -4680,6 +5134,7 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP error.FileLocksNotSupported => unreachable, error.WouldBlock => unreachable, error.FileBusy => unreachable, // not asking for write permissions + error.InvalidHandle => unreachable, // WASI-only else => |e| return e, }; defer close(fd); diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index cc62e6161c13..479794d53a94 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -22,7 +22,7 @@ const Dir = std.fs.Dir; const ArenaAllocator = std.heap.ArenaAllocator; test "chdir smoke test" { - if (native_os == .wasi) return error.SkipZigTest; + if (native_os == .wasi) return error.SkipZigTest; // WASI doesn't allow navigating outside of a preopen // Get current working directory path var old_cwd_buf: [fs.MAX_PATH_BYTES]u8 = undefined; @@ -48,7 +48,8 @@ test "chdir smoke test" { } test "open smoke test" { - if (native_os == .wasi) return error.SkipZigTest; + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); // TODO verify file attributes using `fstat` @@ -102,7 +103,8 @@ test "open smoke test" { } test "openat smoke test" { - if (native_os == .wasi) return error.SkipZigTest; + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); // TODO verify file attributes using `fstatat` @@ -138,7 +140,8 @@ test "openat smoke test" { } test "symlink with relative paths" { - if (native_os == .wasi) return error.SkipZigTest; + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); const cwd = fs.cwd(); cwd.deleteFile("file.txt") catch {}; @@ -190,6 +193,13 @@ fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { test "link with relative paths" { switch (native_os) { + .wasi => { + if (builtin.link_libc) { + return error.SkipZigTest; + } else { + try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + } + }, .linux, .solaris => {}, else => return error.SkipZigTest, } @@ -212,14 +222,14 @@ test "link with relative paths" { const nstat = try os.fstat(nfd.handle); try testing.expectEqual(estat.ino, nstat.ino); - try testing.expectEqual(@as(usize, 2), nstat.nlink); + try testing.expectEqual(@as(@TypeOf(nstat.nlink), 2), nstat.nlink); } try os.unlink("new.txt"); { const estat = try os.fstat(efd.handle); - try testing.expectEqual(@as(usize, 1), estat.nlink); + try testing.expectEqual(@as(@TypeOf(estat.nlink), 1), estat.nlink); } try cwd.deleteFile("example.txt"); @@ -227,6 +237,7 @@ test "link with relative paths" { test "linkat with different directories" { switch (native_os) { + .wasi => if (!builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"), .linux, .solaris => {}, else => return error.SkipZigTest, } @@ -250,14 +261,14 @@ test "linkat with different directories" { const nstat = try os.fstat(nfd.handle); try testing.expectEqual(estat.ino, nstat.ino); - try testing.expectEqual(@as(usize, 2), nstat.nlink); + try testing.expectEqual(@as(@TypeOf(nstat.nlink), 2), nstat.nlink); } try os.unlinkat(tmp.dir.fd, "new.txt", 0); { const estat = try os.fstat(efd.handle); - try testing.expectEqual(@as(usize, 1), estat.nlink); + try testing.expectEqual(@as(@TypeOf(estat.nlink), 1), estat.nlink); } try cwd.deleteFile("example.txt"); @@ -388,8 +399,6 @@ test "getrandom" { } test "getcwd" { - if (native_os == .wasi) return error.SkipZigTest; - // at least call it so it gets compiled var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; _ = os.getcwd(&buf) catch undefined; @@ -878,3 +887,107 @@ test "POSIX file locking with fcntl" { try expect(result.status == 0 * 256); } } + +test "rename smoke test" { + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + // Get base abs path + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const base_path = blk: { + const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp.sub_path[0..] }); + break :blk try fs.realpathAlloc(allocator, relative_path); + }; + + var file_path: []u8 = undefined; + var fd: os.fd_t = undefined; + const mode: os.mode_t = if (native_os == .windows) 0 else 0o666; + + // Create some file using `open`. + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" }); + fd = try os.open(file_path, os.O.RDWR | os.O.CREAT | os.O.EXCL, mode); + os.close(fd); + + // Rename the file + var new_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_file" }); + try os.rename(file_path, new_file_path); + + // Try opening renamed file + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_file" }); + fd = try os.open(file_path, os.O.RDWR, mode); + os.close(fd); + + // Try opening original file - should fail with error.FileNotFound + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" }); + try expectError(error.FileNotFound, os.open(file_path, os.O.RDWR, mode)); + + // Create some directory + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" }); + try os.mkdir(file_path, mode); + + // Rename the directory + new_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_dir" }); + try os.rename(file_path, new_file_path); + + // Try opening renamed directory + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_dir" }); + fd = try os.open(file_path, os.O.RDONLY | os.O.DIRECTORY, mode); + os.close(fd); + + // Try opening original directory - should fail with error.FileNotFound + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" }); + try expectError(error.FileNotFound, os.open(file_path, os.O.RDONLY | os.O.DIRECTORY, mode)); +} + +test "access smoke test" { + if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + // Get base abs path + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const base_path = blk: { + const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp.sub_path[0..] }); + break :blk try fs.realpathAlloc(allocator, relative_path); + }; + + var file_path: []u8 = undefined; + var fd: os.fd_t = undefined; + const mode: os.mode_t = if (native_os == .windows) 0 else 0o666; + + // Create some file using `open`. + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" }); + fd = try os.open(file_path, os.O.RDWR | os.O.CREAT | os.O.EXCL, mode); + os.close(fd); + + // Try to access() the file + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" }); + if (builtin.os.tag == .windows) { + try os.access(file_path, os.F_OK); + } else { + try os.access(file_path, os.F_OK | os.W_OK | os.R_OK); + } + + // Try to access() a non-existent file - should fail with error.FileNotFound + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_file" }); + try expectError(error.FileNotFound, os.access(file_path, os.F_OK)); + + // Create some directory + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" }); + try os.mkdir(file_path, mode); + + // Try to access() the directory + file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" }); + try os.access(file_path, os.F_OK); +} diff --git a/lib/std/os/wasi.zig b/lib/std/os/wasi.zig index 0b2538cb88f8..ec2c577de133 100644 --- a/lib/std/os/wasi.zig +++ b/lib/std/os/wasi.zig @@ -15,6 +15,11 @@ comptime { // assert(@alignOf(u64) == 8); } +pub const F_OK = 0; +pub const X_OK = 1; +pub const W_OK = 2; +pub const R_OK = 4; + pub const iovec_t = std.os.iovec; pub const ciovec_t = std.os.iovec_const; diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 8d0879280173..8e5a75feb721 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -327,8 +327,8 @@ fn getCwdOrWasiPreopen() std.fs.Dir { defer preopens.deinit(); preopens.populate() catch @panic("unable to make tmp dir for testing: unable to populate preopens"); - const preopen = preopens.find(std.fs.wasi.PreopenType{ .Dir = "." }) orelse - @panic("unable to make tmp dir for testing: didn't find '.' in the preopens"); + const preopen = preopens.find(std.fs.wasi.PreopenType{ .Dir = "/cwd" }) orelse + @panic("unable to make tmp dir for testing: didn't find '/cwd' in the preopens"); return std.fs.Dir{ .fd = preopen.fd }; } else { diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index af41fc790579..36f6207677ec 100644 --- a/lib/std/zig/system/NativeTargetInfo.zig +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -369,6 +369,7 @@ fn detectAbiAndDynamicLinker( error.IsDir, error.NotDir, + error.InvalidHandle, error.AccessDenied, error.NoDevice, error.FileNotFound, @@ -670,6 +671,7 @@ pub fn abiAndDynamicLinkerFromFile( error.FileNotFound, error.NotDir, + error.InvalidHandle, error.AccessDenied, error.NoDevice, => continue, diff --git a/src/test.zig b/src/test.zig index a540d566eb98..e291db09dbde 100644 --- a/src/test.zig +++ b/src/test.zig @@ -1188,6 +1188,7 @@ pub const TestContext = struct { .wasmtime => |wasmtime_bin_name| if (enable_wasmtime) { try argv.append(wasmtime_bin_name); try argv.append("--dir=."); + try argv.append("--mapdir=/cwd::."); try argv.append(exe_path); } else { return; // wasmtime not available; pass test. From ade2d0c6a28009ac505b4863079ede3ba64fcdd1 Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Wed, 2 Mar 2022 23:21:55 -0700 Subject: [PATCH 2/3] stdlib WASI: Add realpath() support for non-absolute Preopens --- lib/std/build.zig | 2 - lib/std/fs/path.zig | 8 ++-- lib/std/fs/test.zig | 14 +++--- lib/std/fs/wasi.zig | 15 +++++- lib/std/os.zig | 112 ++++++++++++++++++++++++-------------------- lib/std/os/test.zig | 14 +++--- lib/std/testing.zig | 4 +- src/test.zig | 1 - 8 files changed, 95 insertions(+), 75 deletions(-) diff --git a/lib/std/build.zig b/lib/std/build.zig index 2c81f9dcd42e..07e9055b1d5c 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -2707,8 +2707,6 @@ pub const LibExeObjStep = struct { try zig_args.append("--test-cmd"); try zig_args.append("--dir=."); try zig_args.append("--test-cmd"); - try zig_args.append("--mapdir=/cwd::."); - try zig_args.append("--test-cmd"); try zig_args.append("--allow-unknown-exports"); // TODO: Remove when stage2 is default compiler try zig_args.append("--test-cmd-bin"); } else { diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index 07ca298927c8..bdd1c53ed5df 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -735,7 +735,7 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) ![]u8 { test "resolve" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); const cwd = try process.getCwdAlloc(testing.allocator); defer testing.allocator.free(cwd); @@ -756,7 +756,7 @@ test "resolveWindows" { return error.SkipZigTest; } if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); if (native_os == .windows) { const cwd = try process.getCwdAlloc(testing.allocator); defer testing.allocator.free(cwd); @@ -802,7 +802,7 @@ test "resolveWindows" { test "resolvePosix" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); try testResolvePosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c"); try testResolvePosix(&[_][]const u8{ "/a/b", "c", "//d", "e///" }, "/d/e"); @@ -1216,7 +1216,7 @@ test "relative" { return error.SkipZigTest; } if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games"); try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", ".."); diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 9791165eae71..69fbe5449f7e 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -47,7 +47,7 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo test "accessAbsolute" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -66,7 +66,7 @@ test "accessAbsolute" { test "openDirAbsolute" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -103,7 +103,7 @@ test "openDir cwd parent .." { test "readLinkAbsolute" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -512,7 +512,7 @@ test "rename" { test "renameAbsolute" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); @@ -947,7 +947,7 @@ test "open file with exclusive nonblocking lock twice (absolute paths)" { test "walker" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); @@ -998,7 +998,7 @@ test "walker" { test ". and .. in fs.Dir functions" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1027,7 +1027,7 @@ test ". and .. in fs.Dir functions" { test ". and .. in absolute functions" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (builtin.os.tag == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp = tmpDir(.{}); defer tmp.cleanup(); diff --git a/lib/std/fs/wasi.zig b/lib/std/fs/wasi.zig index 061a745855c7..4754ded630d1 100644 --- a/lib/std/fs/wasi.zig +++ b/lib/std/fs/wasi.zig @@ -194,6 +194,17 @@ pub const PreopenList = struct { return null; } + /// Find preopen by fd. If the preopen exists, return it. + /// Otherwise, return `null`. + pub fn findByFd(self: Self, fd: fd_t) ?Preopen { + for (self.buffer.items) |preopen| { + if (preopen.fd == fd) { + return preopen; + } + } + return null; + } + /// Find preopen by type. If the preopen exists, return it. /// Otherwise, return `null`. pub fn find(self: Self, preopen_type: PreopenType) ?*const Preopen { @@ -224,6 +235,6 @@ test "extracting WASI preopens" { try preopens.populate(); - const preopen = preopens.find(PreopenType{ .Dir = "/cwd" }) orelse unreachable; - try std.testing.expect(preopen.@"type".eql(PreopenType{ .Dir = "/cwd" })); + const preopen = preopens.find(PreopenType{ .Dir = "." }) orelse unreachable; + try std.testing.expect(preopen.@"type".eql(PreopenType{ .Dir = "." })); } diff --git a/lib/std/os.zig b/lib/std/os.zig index 5b904f6905f7..5c443934be08 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1431,6 +1431,8 @@ var wasi_cwd = if (builtin.os.tag == .wasi and !builtin.link_libc) struct { }{} else undefined; /// Initialize the available Preopen list on WASI and set the CWD to `cwd_init`. +/// Note that `cwd_init` corresponds to a Preopen directory, not necessarily +/// a POSIX path. For example, "." matches a Preopen provided with `--dir=.` /// /// This must be called before using any relative or absolute paths with `std.os` /// functions, if you are on WASI without linking libc. @@ -1482,8 +1484,22 @@ fn resolvePathAndGetWasiPreopen(path: []const u8, preopen: ?*?Preopen, out_buffe if (wasi_cwd.preopens == null) @panic("On WASI, `initPreopensWasi` must be called to initialize preopens " ++ "before using any CWD-relative or absolute paths.\n"); - // If the path is absolute, we need to lookup a containing Preopen - const abs_path = std.fs.path.resolve(alloc, &.{ "/", path }) catch return error.NameTooLong; + if (mem.startsWith(u8, path, "/preopens/fd/")) { + // "/preopens/fd/" is a special prefix, which refers to a Preopen directly by fd + const fd_start = "/preopens/fd/".len; + const fd_end = mem.indexOfScalarPos(u8, path, fd_start, '/') orelse path.len; + const fd = std.fmt.parseUnsigned(fd_t, path[fd_start..fd_end], 10) catch unreachable; + const rel_path = if (path.len > fd_end + 1) path[fd_end + 1 ..] else "."; + + if (preopen) |p| p.* = wasi_cwd.preopens.?.findByFd(fd); + return RelativePath{ + .dir_fd = fd, + .relative_path = alloc.dupe(u8, rel_path) catch return error.NameTooLong, + }; + } + + // For any other absolute path, we need to lookup a containing Preopen + const abs_path = fs.path.resolve(alloc, &.{ "/", path }) catch return error.NameTooLong; const preopen_uri = wasi_cwd.preopens.?.findContaining(.{ .Dir = abs_path }); if (preopen_uri) |po| { @@ -1507,7 +1523,7 @@ fn resolvePathAndGetWasiPreopen(path: []const u8, preopen: ?*?Preopen, out_buffe // First resolve a combined path, where the "/" corresponds to `cwd.dir_fd` // not the true filesystem root const paths = &.{ "/", cwd.relative_path, path }; - const resolved_path = std.fs.path.resolve(alloc, paths) catch return error.NameTooLong; + const resolved_path = fs.path.resolve(alloc, paths) catch return error.NameTooLong; // Strip off the fake root to get the relative path w.r.t. `cwd.dir_fd` const resolved_relative_path = resolved_path[1..]; @@ -1956,28 +1972,14 @@ pub fn getcwd(out_buffer: []u8) GetCwdError![]u8 { if (builtin.os.tag == .windows) { return windows.GetCurrentDirectory(out_buffer); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - var allocator = std.heap.FixedBufferAllocator.init(out_buffer); - var alloc = allocator.allocator(); - if (wasi_cwd.cwd) |cwd| { - if (wasi_cwd.cwd_preopen) |po| { - var base_cwd_dir = switch (po.@"type") { - .Dir => |dir| dir, - }; - if (!fs.path.isAbsolute(base_cwd_dir)) { - // This preopen is not based on an absolute path, so we have - // no way to know the absolute path of the CWD - return error.CurrentWorkingDirectoryUnlinked; - } - const paths = &.{ base_cwd_dir, cwd.relative_path }; - return std.fs.path.resolve(alloc, paths) catch return error.NameTooLong; - } else { - // The CWD is not rooted to an existing Preopen, - // so we have no way to know its absolute path - return error.CurrentWorkingDirectoryUnlinked; - } - } else { - return alloc.dupe(u8, "/") catch return error.NameTooLong; - } + var buf: [MAX_PATH_BYTES]u8 = undefined; + const path = realpathWasi(".", &buf) catch |err| switch (err) { + error.NameTooLong => return error.NameTooLong, + error.InvalidHandle => return error.CurrentWorkingDirectoryUnlinked, + }; + if (out_buffer.len < path.len) return error.NameTooLong; + std.mem.copy(u8, out_buffer, path); + return out_buffer[0..path.len]; } const err = if (builtin.link_libc) blk: { @@ -5089,35 +5091,45 @@ pub fn realpath(pathname: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealPathE const pathname_w = try windows.sliceToPrefixedFileW(pathname); return realpathW(pathname_w.span(), out_buffer); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - // NOTE: This emulation is incomplete. Symbolic links are not - // currently expanded during path canonicalization. - var alloc = std.heap.FixedBufferAllocator.init(out_buffer); - if (fs.path.isAbsolute(pathname)) - return try fs.path.resolve(alloc.allocator(), &.{pathname}) catch error.NameTooLong; - if (wasi_cwd.cwd) |cwd| { - if (wasi_cwd.cwd_preopen) |po| { - var base_cwd_dir = switch (po.@"type") { - .Dir => |dir| dir, - }; - if (!fs.path.isAbsolute(base_cwd_dir)) { - // This preopen is not based on an absolute path, so we have - // no way to know the absolute path of the CWD - return error.InvalidHandle; - } + return realpathWasi(pathname, out_buffer); + } + const pathname_c = try toPosixPath(pathname); + return realpathZ(&pathname_c, out_buffer); +} - const paths = &.{ base_cwd_dir, cwd.relative_path, pathname }; - return fs.path.resolve(alloc.allocator(), paths) catch error.NameTooLong; - } else { - // The CWD is not rooted to an existing Preopen, - // so we have no way to know its absolute path - return error.InvalidHandle; - } +/// Return an emulated canonicalized absolute pathname on WASI. +/// +/// NOTE: This emulation is incomplete. Symbolic links are not +/// currently expanded during path canonicalization. +fn realpathWasi(pathname: []const u8, out_buffer: []u8) ![]u8 { + var alloc = std.heap.FixedBufferAllocator.init(out_buffer); + if (fs.path.isAbsolute(pathname)) + return try fs.path.resolve(alloc.allocator(), &.{pathname}) catch error.NameTooLong; + if (wasi_cwd.cwd) |cwd| { + if (wasi_cwd.cwd_preopen) |po| { + var base_cwd_dir = switch (po.@"type") { + .Dir => |dir| dir, + }; + const paths: [][]const u8 = if (fs.path.isAbsolute(base_cwd_dir)) blk: { + break :blk &.{ base_cwd_dir, cwd.relative_path, pathname }; + } else blk: { + // No absolute path is associated with this preopen, so + // instead we use a special "/preopens/fd//" prefix + var buf: [16]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + std.fmt.formatInt(po.fd, 10, .lower, .{}, fbs.writer()) catch return error.NameTooLong; + break :blk &.{ "/preopens/fd/", fbs.getWritten(), cwd.relative_path, pathname }; + }; + + return fs.path.resolve(alloc.allocator(), paths) catch error.NameTooLong; } else { - return try fs.path.resolve(alloc.allocator(), &.{ "/", pathname }) catch error.NameTooLong; + // The CWD is not rooted to an existing Preopen, + // so we have no way to know its absolute path + return error.InvalidHandle; } + } else { + return try fs.path.resolve(alloc.allocator(), &.{ "/", pathname }) catch error.NameTooLong; } - const pathname_c = try toPosixPath(pathname); - return realpathZ(&pathname_c, out_buffer); } /// Same as `realpath` except `pathname` is null-terminated. diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 479794d53a94..348f5ffaa825 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -49,7 +49,7 @@ test "chdir smoke test" { test "open smoke test" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); // TODO verify file attributes using `fstat` @@ -104,7 +104,7 @@ test "open smoke test" { test "openat smoke test" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); // TODO verify file attributes using `fstatat` @@ -141,7 +141,7 @@ test "openat smoke test" { test "symlink with relative paths" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); const cwd = fs.cwd(); cwd.deleteFile("file.txt") catch {}; @@ -197,7 +197,7 @@ test "link with relative paths" { if (builtin.link_libc) { return error.SkipZigTest; } else { - try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + try os.initPreopensWasi(std.heap.page_allocator, "."); } }, .linux, .solaris => {}, @@ -237,7 +237,7 @@ test "link with relative paths" { test "linkat with different directories" { switch (native_os) { - .wasi => if (!builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"), + .wasi => if (!builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."), .linux, .solaris => {}, else => return error.SkipZigTest, } @@ -890,7 +890,7 @@ test "POSIX file locking with fcntl" { test "rename smoke test" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -947,7 +947,7 @@ test "rename smoke test" { test "access smoke test" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; - if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "/cwd"); + if (native_os == .wasi and !builtin.link_libc) try os.initPreopensWasi(std.heap.page_allocator, "."); var tmp = tmpDir(.{}); defer tmp.cleanup(); diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 8e5a75feb721..8d0879280173 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -327,8 +327,8 @@ fn getCwdOrWasiPreopen() std.fs.Dir { defer preopens.deinit(); preopens.populate() catch @panic("unable to make tmp dir for testing: unable to populate preopens"); - const preopen = preopens.find(std.fs.wasi.PreopenType{ .Dir = "/cwd" }) orelse - @panic("unable to make tmp dir for testing: didn't find '/cwd' in the preopens"); + const preopen = preopens.find(std.fs.wasi.PreopenType{ .Dir = "." }) orelse + @panic("unable to make tmp dir for testing: didn't find '.' in the preopens"); return std.fs.Dir{ .fd = preopen.fd }; } else { diff --git a/src/test.zig b/src/test.zig index e291db09dbde..a540d566eb98 100644 --- a/src/test.zig +++ b/src/test.zig @@ -1188,7 +1188,6 @@ pub const TestContext = struct { .wasmtime => |wasmtime_bin_name| if (enable_wasmtime) { try argv.append(wasmtime_bin_name); try argv.append("--dir=."); - try argv.append("--mapdir=/cwd::."); try argv.append(exe_path); } else { return; // wasmtime not available; pass test. From aafcd8eab3c6046a915f26aed766090ee219d920 Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Wed, 2 Mar 2022 23:49:49 -0700 Subject: [PATCH 3/3] stdlib std.os: Rename `RelativePath` to `RelativePathWasi` --- lib/std/os.zig | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 5c443934be08..241c38dedffd 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -219,7 +219,11 @@ pub const LOG = struct { pub const DEBUG = 7; }; -pub const RelativePath = struct { +/// An fd-relative file path +/// +/// This is currently only used for WASI-specific functionality, but the concept +/// is the same as the dirfd/pathname pairs in the `*at(...)` POSIX functions. +pub const RelativePathWasi = struct { /// Handle to directory dir_fd: fd_t, /// Path to resource within `dir_fd`. @@ -1425,7 +1429,7 @@ var wasi_cwd = if (builtin.os.tag == .wasi and !builtin.link_libc) struct { // Memory buffer for storing the relative portion of the CWD path_buffer: [MAX_PATH_BYTES]u8 = undefined, // Current Working Directory, stored as an fd_t and a relative path - cwd: ?RelativePath = null, + cwd: ?RelativePathWasi = null, // Preopen associated with `cwd`, if any cwd_preopen: ?Preopen = null, }{} else undefined; @@ -1451,7 +1455,7 @@ pub fn initPreopensWasi(alloc: Allocator, cwd_init: ?[]const u8) !void { const preopen = wasi_cwd.preopens.?.findContaining(.{ .Dir = cwd }); if (preopen) |po| { wasi_cwd.cwd_preopen = po.base; - wasi_cwd.cwd = RelativePath{ + wasi_cwd.cwd = RelativePathWasi{ .dir_fd = po.base.fd, .relative_path = po.relative_path, }; @@ -1470,13 +1474,13 @@ pub fn initPreopensWasi(alloc: Allocator, cwd_init: ?[]const u8) !void { /// /// For absolute paths, this automatically searches among available Preopens to find /// a match. For relative paths, it uses the "emulated" CWD. -pub fn resolvePathWasi(path: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) !RelativePath { - // Note: Due to WASI's "sandboxed" file handles, operations with this RelativePath +pub fn resolvePathWasi(path: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) !RelativePathWasi { + // Note: Due to WASI's "sandboxed" file handles, operations with this RelativePathWasi // will fail if the relative path navigates outside of `dir_fd` using ".." return resolvePathAndGetWasiPreopen(path, null, out_buffer); } -fn resolvePathAndGetWasiPreopen(path: []const u8, preopen: ?*?Preopen, out_buffer: *[MAX_PATH_BYTES]u8) !RelativePath { +fn resolvePathAndGetWasiPreopen(path: []const u8, preopen: ?*?Preopen, out_buffer: *[MAX_PATH_BYTES]u8) !RelativePathWasi { var allocator = std.heap.FixedBufferAllocator.init(out_buffer); var alloc = allocator.allocator(); @@ -1492,7 +1496,7 @@ fn resolvePathAndGetWasiPreopen(path: []const u8, preopen: ?*?Preopen, out_buffe const rel_path = if (path.len > fd_end + 1) path[fd_end + 1 ..] else "."; if (preopen) |p| p.* = wasi_cwd.preopens.?.findByFd(fd); - return RelativePath{ + return RelativePathWasi{ .dir_fd = fd, .relative_path = alloc.dupe(u8, rel_path) catch return error.NameTooLong, }; @@ -1504,7 +1508,7 @@ fn resolvePathAndGetWasiPreopen(path: []const u8, preopen: ?*?Preopen, out_buffe if (preopen_uri) |po| { if (preopen) |p| p.* = po.base; - return RelativePath{ + return RelativePathWasi{ .dir_fd = po.base.fd, .relative_path = po.relative_path, }; @@ -1529,7 +1533,7 @@ fn resolvePathAndGetWasiPreopen(path: []const u8, preopen: ?*?Preopen, out_buffe const resolved_relative_path = resolved_path[1..]; if (preopen) |p| p.* = wasi_cwd.cwd_preopen; - return RelativePath{ + return RelativePathWasi{ .dir_fd = cwd.dir_fd, .relative_path = resolved_relative_path, }; @@ -1568,6 +1572,7 @@ pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) Ope return openatZ(dir_fd, &file_path_c, flags, mode); } +/// A struct to contain all lookup/rights flags accepted by `wasi.path_open` const WasiOpenOptions = struct { oflags: wasi.oflags_t, lookup_flags: wasi.lookupflags_t, @@ -2232,8 +2237,8 @@ pub fn linkat( var resolve_olddir: bool = (olddir == wasi.AT.FDCWD or fs.path.isAbsolute(oldpath)); var resolve_newdir: bool = (newdir == wasi.AT.FDCWD or fs.path.isAbsolute(newpath)); - var old: RelativePath = .{ .dir_fd = olddir, .relative_path = oldpath }; - var new: RelativePath = .{ .dir_fd = newdir, .relative_path = newpath }; + var old: RelativePathWasi = .{ .dir_fd = olddir, .relative_path = oldpath }; + var new: RelativePathWasi = .{ .dir_fd = newdir, .relative_path = newpath }; // Resolve absolute or CWD-relative paths to a path within a Preopen if (resolve_olddir or resolve_newdir) { @@ -2257,7 +2262,7 @@ pub fn linkat( /// WASI-only. The same as `linkat` but targeting WASI. /// See also `linkat`. -pub fn linkatWasi(old: RelativePath, new: RelativePath, flags: i32) LinkatError!void { +pub fn linkatWasi(old: RelativePathWasi, new: RelativePathWasi, flags: i32) LinkatError!void { var old_flags: wasi.lookupflags_t = 0; // TODO: Why is this not defined in wasi-libc? if (flags & linux.AT.SYMLINK_FOLLOW != 0) old_flags |= wasi.LOOKUP_SYMLINK_FOLLOW; @@ -2545,8 +2550,8 @@ pub fn renameat( var resolve_old: bool = (old_dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(old_path)); var resolve_new: bool = (new_dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(new_path)); - var old: RelativePath = .{ .dir_fd = old_dir_fd, .relative_path = old_path }; - var new: RelativePath = .{ .dir_fd = new_dir_fd, .relative_path = new_path }; + var old: RelativePathWasi = .{ .dir_fd = old_dir_fd, .relative_path = old_path }; + var new: RelativePathWasi = .{ .dir_fd = new_dir_fd, .relative_path = new_path }; // Resolve absolute or CWD-relative paths to a path within a Preopen if (resolve_old or resolve_new) { @@ -2570,7 +2575,7 @@ pub fn renameat( /// WASI-only. Same as `renameat` expect targeting WASI. /// See also `renameat`. -pub fn renameatWasi(old: RelativePath, new: RelativePath) RenameError!void { +pub fn renameatWasi(old: RelativePathWasi, new: RelativePathWasi) RenameError!void { switch (wasi.path_rename(old.dir_fd, old.relative_path.ptr, old.relative_path.len, new.dir_fd, new.relative_path.ptr, new.relative_path.len)) { .SUCCESS => return, .ACCES => return error.AccessDenied, @@ -4495,7 +4500,7 @@ pub fn faccessat(dirfd: fd_t, path: []const u8, mode: u32, flags: u32) AccessErr const path_w = try windows.sliceToPrefixedFileW(path); return faccessatW(dirfd, path_w.span().ptr, mode, flags); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - var resolved = RelativePath{ .dir_fd = dirfd, .relative_path = path }; + var resolved = RelativePathWasi{ .dir_fd = dirfd, .relative_path = path }; const file = blk: { if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(path)) {