Commit 81e7e9fdbb

Andrew Kelley <andrew@ziglang.org>
2025-10-17 09:52:33
std.Io: add dirOpenDir and WASI impl
1 parent da6b959
Changed files (5)
lib/std/fs/Dir.zig
@@ -1235,28 +1235,10 @@ pub fn setAsCwd(self: Dir) !void {
     try posix.fchdir(self.fd);
 }
 
-pub const OpenOptions = struct {
-    /// `true` means the opened directory can be used as the `Dir` parameter
-    /// for functions which operate based on an open directory handle. When `false`,
-    /// such operations are Illegal Behavior.
-    access_sub_paths: bool = true,
-
-    /// `true` means the opened directory can be scanned for the files and sub-directories
-    /// of the result. It means the `iterate` function can be called.
-    iterate: bool = false,
-
-    /// `true` means it won't dereference the symlinks.
-    no_follow: bool = false,
-};
+/// Deprecated in favor of `Io.Dir.OpenOptions`.
+pub const OpenOptions = Io.Dir.OpenOptions;
 
-/// Opens a directory at the given path. The directory is a system resource that remains
-/// open until `close` is called on the result.
-/// The directory cannot be iterated unless the `iterate` option is set to `true`.
-///
-/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
-/// On WASI, `sub_path` should be encoded as valid UTF-8.
-/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
-/// Asserts that the path parameter has no null bytes.
+/// Deprecated in favor of `Io.Dir.openDir`.
 pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir {
     switch (native_os) {
         .windows => {
@@ -1264,54 +1246,9 @@ pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir
             return self.openDirW(sub_path_w.span().ptr, args);
         },
         .wasi => if (!builtin.link_libc) {
-            var base: std.os.wasi.rights_t = .{
-                .FD_FILESTAT_GET = true,
-                .FD_FDSTAT_SET_FLAGS = true,
-                .FD_FILESTAT_SET_TIMES = true,
-            };
-            if (args.access_sub_paths) {
-                base.FD_READDIR = true;
-                base.PATH_CREATE_DIRECTORY = true;
-                base.PATH_CREATE_FILE = true;
-                base.PATH_LINK_SOURCE = true;
-                base.PATH_LINK_TARGET = true;
-                base.PATH_OPEN = true;
-                base.PATH_READLINK = true;
-                base.PATH_RENAME_SOURCE = true;
-                base.PATH_RENAME_TARGET = true;
-                base.PATH_FILESTAT_GET = true;
-                base.PATH_FILESTAT_SET_SIZE = true;
-                base.PATH_FILESTAT_SET_TIMES = true;
-                base.PATH_SYMLINK = true;
-                base.PATH_REMOVE_DIRECTORY = true;
-                base.PATH_UNLINK_FILE = true;
-            }
-
-            const result = posix.openatWasi(
-                self.fd,
-                sub_path,
-                .{ .SYMLINK_FOLLOW = !args.no_follow },
-                .{ .DIRECTORY = true },
-                .{},
-                base,
-                base,
-            );
-            const fd = result catch |err| switch (err) {
-                error.FileTooBig => unreachable, // can't happen for directories
-                error.IsDir => unreachable, // we're setting DIRECTORY
-                error.NoSpaceLeft => unreachable, // not setting CREAT
-                error.PathAlreadyExists => unreachable, // not setting CREAT
-                error.FileLocksNotSupported => unreachable, // locking folders is not supported
-                error.WouldBlock => unreachable, // can't happen for directories
-                error.FileBusy => unreachable, // can't happen for directories
-                error.SharingViolation => unreachable,
-                error.PipeBusy => unreachable,
-                error.ProcessNotFound => unreachable,
-                error.AntivirusInterference => unreachable,
-
-                else => |e| return e,
-            };
-            return .{ .fd = fd };
+            var threaded: Io.Threaded = .init_single_threaded;
+            const io = threaded.io();
+            return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args));
         },
         else => {},
     }
@@ -1358,12 +1295,12 @@ pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenErr
     var symlink_flags: posix.O = switch (native_os) {
         .wasi => .{
             .read = true,
-            .NOFOLLOW = args.no_follow,
+            .NOFOLLOW = !args.follow_symlinks,
             .DIRECTORY = true,
         },
         else => .{
             .ACCMODE = .RDONLY,
-            .NOFOLLOW = args.no_follow,
+            .NOFOLLOW = !args.follow_symlinks,
             .DIRECTORY = true,
             .CLOEXEC = true,
         },
@@ -1384,7 +1321,7 @@ pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenOptions) OpenEr
         w.SYNCHRONIZE | w.FILE_TRAVERSE;
     const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
     const dir = self.makeOpenDirAccessMaskW(sub_path_w, flags, .{
-        .no_follow = args.no_follow,
+        .no_follow = !args.follow_symlinks,
         .create_disposition = w.FILE_OPEN,
     }) catch |err| switch (err) {
         error.ReadOnlyFileSystem => unreachable,
@@ -1923,7 +1860,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
                 if (treat_as_dir) {
                     if (stack.unusedCapacitySlice().len >= 1) {
                         var iterable_dir = top.iter.dir.openDir(entry.name, .{
-                            .no_follow = true,
+                            .follow_symlinks = false,
                             .iterate = true,
                         }) catch |err| switch (err) {
                             error.NotDir => {
@@ -2019,7 +1956,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
                 handle_entry: while (true) {
                     if (treat_as_dir) {
                         break :iterable_dir parent_dir.openDir(name, .{
-                            .no_follow = true,
+                            .follow_symlinks = false,
                             .iterate = true,
                         }) catch |err| switch (err) {
                             error.NotDir => {
@@ -2125,7 +2062,7 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint
                 handle_entry: while (true) {
                     if (treat_as_dir) {
                         const new_dir = dir.openDir(entry.name, .{
-                            .no_follow = true,
+                            .follow_symlinks = false,
                             .iterate = true,
                         }) catch |err| switch (err) {
                             error.NotDir => {
@@ -2224,7 +2161,7 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File
         handle_entry: while (true) {
             if (treat_as_dir) {
                 break :iterable_dir self.openDir(sub_path, .{
-                    .no_follow = true,
+                    .follow_symlinks = false,
                     .iterate = true,
                 }) catch |err| switch (err) {
                     error.NotDir => {
lib/std/Io/Dir.zig
@@ -69,6 +69,30 @@ pub const OpenError = error{
     NetworkNotFound,
 } || PathNameError || Io.Cancelable || Io.UnexpectedError;
 
+pub const OpenOptions = struct {
+    /// `true` means the opened directory can be used as the `Dir` parameter
+    /// for functions which operate based on an open directory handle. When `false`,
+    /// such operations are Illegal Behavior.
+    access_sub_paths: bool = true,
+    /// `true` means the opened directory can be scanned for the files and sub-directories
+    /// of the result. It means the `iterate` function can be called.
+    iterate: bool = false,
+    /// `false` means it won't dereference the symlinks.
+    follow_symlinks: bool = true,
+};
+
+/// Opens a directory at the given path. The directory is a system resource that remains
+/// open until `close` is called on the result.
+///
+/// The directory cannot be iterated unless the `iterate` option is set to `true`.
+///
+/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
+/// On WASI, `sub_path` should be encoded as valid UTF-8.
+/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
+pub fn openDir(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) OpenError!Dir {
+    return io.vtable.dirOpenDir(io.userdata, dir, sub_path, options);
+}
+
 pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
     return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags);
 }
lib/std/Io/Threaded.zig
@@ -198,6 +198,11 @@ pub fn io(t: *Threaded) Io {
                 .wasi => dirOpenFileWasi,
                 else => dirOpenFilePosix,
             },
+            .dirOpenDir = switch (builtin.os.tag) {
+                .windows => @panic("TODO"),
+                .wasi => dirOpenDirWasi,
+                else => dirOpenDirPosix,
+            },
             .fileClose = fileClose,
             .fileWriteStreaming = fileWriteStreaming,
             .fileWritePositional = fileWritePositional,
@@ -1429,7 +1434,6 @@ fn dirCreateFileWasi(
             .CANCELED => return error.Canceled,
 
             .FAULT => |err| return errnoBug(err),
-            // Provides INVAL with a linux host on a bad path name, but NOENT on Windows
             .INVAL => return error.BadPathName,
             .BADF => |err| return errnoBug(err), // File descriptor used after closed.
             .ACCES => return error.AccessDenied,
@@ -1656,6 +1660,87 @@ fn dirOpenFileWasi(
     }
 }
 
+fn dirOpenDirPosix(
+    userdata: ?*anyopaque,
+    dir: Io.Dir,
+    sub_path: []const u8,
+    options: Io.Dir.OpenOptions,
+) Io.Dir.OpenError!Io.Dir {
+    const t: *Threaded = @ptrCast(@alignCast(userdata));
+
+    _ = t;
+    _ = dir;
+    _ = sub_path;
+    _ = options;
+    @panic("TODO");
+}
+
+fn dirOpenDirWasi(
+    userdata: ?*anyopaque,
+    dir: Io.Dir,
+    sub_path: []const u8,
+    options: Io.Dir.OpenOptions,
+) Io.Dir.OpenError!Io.Dir {
+    if (builtin.link_libc) return dirOpenDirPosix(userdata, dir, sub_path, options);
+    const t: *Threaded = @ptrCast(@alignCast(userdata));
+    const wasi = std.os.wasi;
+
+    var base: std.os.wasi.rights_t = .{
+        .FD_FILESTAT_GET = true,
+        .FD_FDSTAT_SET_FLAGS = true,
+        .FD_FILESTAT_SET_TIMES = true,
+    };
+    if (options.access_sub_paths) {
+        base.FD_READDIR = true;
+        base.PATH_CREATE_DIRECTORY = true;
+        base.PATH_CREATE_FILE = true;
+        base.PATH_LINK_SOURCE = true;
+        base.PATH_LINK_TARGET = true;
+        base.PATH_OPEN = true;
+        base.PATH_READLINK = true;
+        base.PATH_RENAME_SOURCE = true;
+        base.PATH_RENAME_TARGET = true;
+        base.PATH_FILESTAT_GET = true;
+        base.PATH_FILESTAT_SET_SIZE = true;
+        base.PATH_FILESTAT_SET_TIMES = true;
+        base.PATH_SYMLINK = true;
+        base.PATH_REMOVE_DIRECTORY = true;
+        base.PATH_UNLINK_FILE = true;
+    }
+
+    const lookup_flags: wasi.lookupflags_t = .{ .SYMLINK_FOLLOW = options.follow_symlinks };
+    const oflags: wasi.oflags_t = .{ .DIRECTORY = true };
+    const fdflags: wasi.fdflags_t = .{};
+    var fd: posix.fd_t = undefined;
+
+    while (true) {
+        try t.checkCancel();
+        switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, base, fdflags, &fd)) {
+            .SUCCESS => return .{ .handle = fd },
+            .INTR => continue,
+            .CANCELED => return error.Canceled,
+
+            .FAULT => |err| return errnoBug(err),
+            .INVAL => return error.BadPathName,
+            .BADF => |err| return errnoBug(err), // File descriptor used after closed.
+            .ACCES => return error.AccessDenied,
+            .LOOP => return error.SymLinkLoop,
+            .MFILE => return error.ProcessFdQuotaExceeded,
+            .NAMETOOLONG => return error.NameTooLong,
+            .NFILE => return error.SystemFdQuotaExceeded,
+            .NODEV => return error.NoDevice,
+            .NOENT => return error.FileNotFound,
+            .NOMEM => return error.SystemResources,
+            .NOTDIR => return error.NotDir,
+            .PERM => return error.PermissionDenied,
+            .BUSY => return error.DeviceBusy,
+            .NOTCAPABLE => return error.AccessDenied,
+            .ILSEQ => return error.BadPathName,
+            else => |err| return posix.unexpectedErrno(err),
+        }
+    }
+}
+
 fn fileClose(userdata: ?*anyopaque, file: Io.File) void {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
     _ = t;
lib/std/Io.zig
@@ -666,6 +666,7 @@ pub const VTable = struct {
     dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void,
     dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File,
     dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File,
+    dirOpenDir: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.OpenError!Dir,
     fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat,
     fileClose: *const fn (?*anyopaque, File) void,
     fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize,
lib/std/tar.zig
@@ -977,7 +977,7 @@ test pipeToFileSystem {
     const data = @embedFile("tar/testdata/example.tar");
     var reader: std.Io.Reader = .fixed(data);
 
-    var tmp = testing.tmpDir(.{ .no_follow = true });
+    var tmp = testing.tmpDir(.{ .follow_symlinks = false });
     defer tmp.cleanup();
     const dir = tmp.dir;
 
@@ -1010,7 +1010,7 @@ test "pipeToFileSystem root_dir" {
 
     // with strip_components = 1
     {
-        var tmp = testing.tmpDir(.{ .no_follow = true });
+        var tmp = testing.tmpDir(.{ .follow_symlinks = false });
         defer tmp.cleanup();
         var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
         defer diagnostics.deinit();
@@ -1032,7 +1032,7 @@ test "pipeToFileSystem root_dir" {
     // with strip_components = 0
     {
         reader = .fixed(data);
-        var tmp = testing.tmpDir(.{ .no_follow = true });
+        var tmp = testing.tmpDir(.{ .follow_symlinks = false });
         defer tmp.cleanup();
         var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
         defer diagnostics.deinit();
@@ -1084,7 +1084,7 @@ test "pipeToFileSystem strip_components" {
     const data = @embedFile("tar/testdata/example.tar");
     var reader: std.Io.Reader = .fixed(data);
 
-    var tmp = testing.tmpDir(.{ .no_follow = true });
+    var tmp = testing.tmpDir(.{ .follow_symlinks = false });
     defer tmp.cleanup();
     var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
     defer diagnostics.deinit();
@@ -1145,7 +1145,7 @@ test "executable bit" {
     for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {
         var reader: std.Io.Reader = .fixed(data);
 
-        var tmp = testing.tmpDir(.{ .no_follow = true });
+        var tmp = testing.tmpDir(.{ .follow_symlinks = false });
         //defer tmp.cleanup();
 
         pipeToFileSystem(tmp.dir, &reader, .{