diff options
Diffstat (limited to 'lib/std')
| -rw-r--r-- | lib/std/c/wasi.zig | 10 | ||||
| -rw-r--r-- | lib/std/child_process.zig | 1 | ||||
| -rw-r--r-- | lib/std/fs.zig | 72 | ||||
| -rw-r--r-- | lib/std/fs/path.zig | 13 | ||||
| -rw-r--r-- | lib/std/fs/test.zig | 22 | ||||
| -rw-r--r-- | lib/std/fs/wasi.zig | 66 | ||||
| -rw-r--r-- | lib/std/os.zig | 550 | ||||
| -rw-r--r-- | lib/std/os/test.zig | 133 | ||||
| -rw-r--r-- | lib/std/os/wasi.zig | 5 | ||||
| -rw-r--r-- | lib/std/zig/system/NativeTargetInfo.zig | 2 |
10 files changed, 778 insertions, 96 deletions
diff --git a/lib/std/c/wasi.zig b/lib/std/c/wasi.zig index c3635784dd..e1940054b6 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 236ce45619..e8e348d67b 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 758a2bd132..7a41bdf6a1 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 fbf5bd99ee..bdd1c53ed5 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, "."); 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, "."); 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, "."); 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, "."); 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 b00b0609b2..69fbe5449f 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, "."); 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, "."); 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, "."); 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, "."); 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, "."); 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, "."); 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, "."); var tmp = tmpDir(.{}); defer tmp.cleanup(); diff --git a/lib/std/fs/wasi.zig b/lib/std/fs/wasi.zig index 1a033653d3..4754ded630 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,49 @@ 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 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 { @@ -173,8 +235,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); } diff --git a/lib/std/os.zig b/lib/std/os.zig index 3e230b773d..241c38dedf 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,17 @@ pub const LOG = struct { pub const DEBUG = 7; }; +/// 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`. + 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 +1259,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 +1323,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 +1336,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 +1374,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 +1414,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 +1423,204 @@ 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: ?RelativePathWasi = 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`. +/// 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. +/// +/// `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 = RelativePathWasi{ + .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) !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) !RelativePathWasi { + 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 (mem.startsWith(u8, path, "/preopens/fd/")) { + // "/preopens/fd/<N>" 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 RelativePathWasi{ + .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| { + if (preopen) |p| p.* = po.base; + return RelativePathWasi{ + .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 = 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 RelativePathWasi{ + .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); } +/// 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, + 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 +1660,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 +1708,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 +1976,15 @@ 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 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: { @@ -1809,11 +2027,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 +2042,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 +2072,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 +2117,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 +2156,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 +2181,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 +2201,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 +2233,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: 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) { + 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: 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; + + 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 +2316,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 +2334,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 +2373,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 +2423,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 +2483,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 +2501,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 +2547,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: 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) { + 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 +2575,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: 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, .PERM => return error.AccessDenied, @@ -2290,6 +2612,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 +2704,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 +2744,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 +2804,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 +2822,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 +2879,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 +2898,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 +2946,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 +2975,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 +3007,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 +3056,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 +3077,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 +3101,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 +3149,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 +4103,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 +4141,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 +4443,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 +4456,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 +4499,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 = RelativePathWasi{ .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 +4548,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 +5077,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,19 +5095,55 @@ 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) { + return realpathWasi(pathname, out_buffer); } const pathname_c = try toPosixPath(pathname); return realpathZ(&pathname_c, out_buffer); } +/// 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/<N>/" 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 { + // 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; + } +} + /// Same as `realpath` except `pathname` is null-terminated. pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { 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 +5151,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 cc62e6161c..348f5ffaa8 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, "."); // 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, "."); // 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, "."); 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, "."); + } + }, .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, "."), .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, "."); + + 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, "."); + + 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 0b2538cb88..ec2c577de1 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/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index af41fc7905..36f6207677 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, |
