authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-10-17 00:52:33-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-10-29 06:20:50-07:00
log81e7e9fdbbb822c413649479dd572ffcd244543a
treebb0b3a9262df12afd28a46fd7326dffe0b2d452b
parentda6b959f647f62aeaf96f380ad2828c16142f23f

std.Io: add dirOpenDir and WASI impl


5 files changed, 129 insertions(+), 82 deletions(-)

lib/std/Io.zig+1
......@@ -666,6 +666,7 @@ pub const VTable = struct {
666666 dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void,
667667 dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File,
668668 dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File,
669 dirOpenDir: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.OpenError!Dir,
669670 fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat,
670671 fileClose: *const fn (?*anyopaque, File) void,
671672 fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize,
lib/std/Io/Dir.zig+24
......@@ -69,6 +69,30 @@ pub const OpenError = error{
6969 NetworkNotFound,
7070} || PathNameError || Io.Cancelable || Io.UnexpectedError;
7171
72pub const OpenOptions = struct {
73 /// `true` means the opened directory can be used as the `Dir` parameter
74 /// for functions which operate based on an open directory handle. When `false`,
75 /// such operations are Illegal Behavior.
76 access_sub_paths: bool = true,
77 /// `true` means the opened directory can be scanned for the files and sub-directories
78 /// of the result. It means the `iterate` function can be called.
79 iterate: bool = false,
80 /// `false` means it won't dereference the symlinks.
81 follow_symlinks: bool = true,
82};
83
84/// Opens a directory at the given path. The directory is a system resource that remains
85/// open until `close` is called on the result.
86///
87/// The directory cannot be iterated unless the `iterate` option is set to `true`.
88///
89/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
90/// On WASI, `sub_path` should be encoded as valid UTF-8.
91/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
92pub fn openDir(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) OpenError!Dir {
93 return io.vtable.dirOpenDir(io.userdata, dir, sub_path, options);
94}
95
7296pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
7397 return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags);
7498}
lib/std/Io/Threaded.zig+86-1
......@@ -198,6 +198,11 @@ pub fn io(t: *Threaded) Io {
198198 .wasi => dirOpenFileWasi,
199199 else => dirOpenFilePosix,
200200 },
201 .dirOpenDir = switch (builtin.os.tag) {
202 .windows => @panic("TODO"),
203 .wasi => dirOpenDirWasi,
204 else => dirOpenDirPosix,
205 },
201206 .fileClose = fileClose,
202207 .fileWriteStreaming = fileWriteStreaming,
203208 .fileWritePositional = fileWritePositional,
......@@ -1429,7 +1434,6 @@ fn dirCreateFileWasi(
14291434 .CANCELED => return error.Canceled,
14301435
14311436 .FAULT => |err| return errnoBug(err),
1432 // Provides INVAL with a linux host on a bad path name, but NOENT on Windows
14331437 .INVAL => return error.BadPathName,
14341438 .BADF => |err| return errnoBug(err), // File descriptor used after closed.
14351439 .ACCES => return error.AccessDenied,
......@@ -1656,6 +1660,87 @@ fn dirOpenFileWasi(
16561660 }
16571661}
16581662
1663fn dirOpenDirPosix(
1664 userdata: ?*anyopaque,
1665 dir: Io.Dir,
1666 sub_path: []const u8,
1667 options: Io.Dir.OpenOptions,
1668) Io.Dir.OpenError!Io.Dir {
1669 const t: *Threaded = @ptrCast(@alignCast(userdata));
1670
1671 _ = t;
1672 _ = dir;
1673 _ = sub_path;
1674 _ = options;
1675 @panic("TODO");
1676}
1677
1678fn dirOpenDirWasi(
1679 userdata: ?*anyopaque,
1680 dir: Io.Dir,
1681 sub_path: []const u8,
1682 options: Io.Dir.OpenOptions,
1683) Io.Dir.OpenError!Io.Dir {
1684 if (builtin.link_libc) return dirOpenDirPosix(userdata, dir, sub_path, options);
1685 const t: *Threaded = @ptrCast(@alignCast(userdata));
1686 const wasi = std.os.wasi;
1687
1688 var base: std.os.wasi.rights_t = .{
1689 .FD_FILESTAT_GET = true,
1690 .FD_FDSTAT_SET_FLAGS = true,
1691 .FD_FILESTAT_SET_TIMES = true,
1692 };
1693 if (options.access_sub_paths) {
1694 base.FD_READDIR = true;
1695 base.PATH_CREATE_DIRECTORY = true;
1696 base.PATH_CREATE_FILE = true;
1697 base.PATH_LINK_SOURCE = true;
1698 base.PATH_LINK_TARGET = true;
1699 base.PATH_OPEN = true;
1700 base.PATH_READLINK = true;
1701 base.PATH_RENAME_SOURCE = true;
1702 base.PATH_RENAME_TARGET = true;
1703 base.PATH_FILESTAT_GET = true;
1704 base.PATH_FILESTAT_SET_SIZE = true;
1705 base.PATH_FILESTAT_SET_TIMES = true;
1706 base.PATH_SYMLINK = true;
1707 base.PATH_REMOVE_DIRECTORY = true;
1708 base.PATH_UNLINK_FILE = true;
1709 }
1710
1711 const lookup_flags: wasi.lookupflags_t = .{ .SYMLINK_FOLLOW = options.follow_symlinks };
1712 const oflags: wasi.oflags_t = .{ .DIRECTORY = true };
1713 const fdflags: wasi.fdflags_t = .{};
1714 var fd: posix.fd_t = undefined;
1715
1716 while (true) {
1717 try t.checkCancel();
1718 switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, base, fdflags, &fd)) {
1719 .SUCCESS => return .{ .handle = fd },
1720 .INTR => continue,
1721 .CANCELED => return error.Canceled,
1722
1723 .FAULT => |err| return errnoBug(err),
1724 .INVAL => return error.BadPathName,
1725 .BADF => |err| return errnoBug(err), // File descriptor used after closed.
1726 .ACCES => return error.AccessDenied,
1727 .LOOP => return error.SymLinkLoop,
1728 .MFILE => return error.ProcessFdQuotaExceeded,
1729 .NAMETOOLONG => return error.NameTooLong,
1730 .NFILE => return error.SystemFdQuotaExceeded,
1731 .NODEV => return error.NoDevice,
1732 .NOENT => return error.FileNotFound,
1733 .NOMEM => return error.SystemResources,
1734 .NOTDIR => return error.NotDir,
1735 .PERM => return error.PermissionDenied,
1736 .BUSY => return error.DeviceBusy,
1737 .NOTCAPABLE => return error.AccessDenied,
1738 .ILSEQ => return error.BadPathName,
1739 else => |err| return posix.unexpectedErrno(err),
1740 }
1741 }
1742}
1743
16591744fn fileClose(userdata: ?*anyopaque, file: Io.File) void {
16601745 const t: *Threaded = @ptrCast(@alignCast(userdata));
16611746 _ = t;
lib/std/fs/Dir.zig+13-76
......@@ -1235,28 +1235,10 @@ pub fn setAsCwd(self: Dir) !void {
12351235 try posix.fchdir(self.fd);
12361236}
12371237
1238pub const OpenOptions = struct {
1239 /// `true` means the opened directory can be used as the `Dir` parameter
1240 /// for functions which operate based on an open directory handle. When `false`,
1241 /// such operations are Illegal Behavior.
1242 access_sub_paths: bool = true,
1243
1244 /// `true` means the opened directory can be scanned for the files and sub-directories
1245 /// of the result. It means the `iterate` function can be called.
1246 iterate: bool = false,
1247
1248 /// `true` means it won't dereference the symlinks.
1249 no_follow: bool = false,
1250};
1238/// Deprecated in favor of `Io.Dir.OpenOptions`.
1239pub const OpenOptions = Io.Dir.OpenOptions;
12511240
1252/// Opens a directory at the given path. The directory is a system resource that remains
1253/// open until `close` is called on the result.
1254/// The directory cannot be iterated unless the `iterate` option is set to `true`.
1255///
1256/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
1257/// On WASI, `sub_path` should be encoded as valid UTF-8.
1258/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
1259/// Asserts that the path parameter has no null bytes.
1241/// Deprecated in favor of `Io.Dir.openDir`.
12601242pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir {
12611243 switch (native_os) {
12621244 .windows => {
......@@ -1264,54 +1246,9 @@ pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir
12641246 return self.openDirW(sub_path_w.span().ptr, args);
12651247 },
12661248 .wasi => if (!builtin.link_libc) {
1267 var base: std.os.wasi.rights_t = .{
1268 .FD_FILESTAT_GET = true,
1269 .FD_FDSTAT_SET_FLAGS = true,
1270 .FD_FILESTAT_SET_TIMES = true,
1271 };
1272 if (args.access_sub_paths) {
1273 base.FD_READDIR = true;
1274 base.PATH_CREATE_DIRECTORY = true;
1275 base.PATH_CREATE_FILE = true;
1276 base.PATH_LINK_SOURCE = true;
1277 base.PATH_LINK_TARGET = true;
1278 base.PATH_OPEN = true;
1279 base.PATH_READLINK = true;
1280 base.PATH_RENAME_SOURCE = true;
1281 base.PATH_RENAME_TARGET = true;
1282 base.PATH_FILESTAT_GET = true;
1283 base.PATH_FILESTAT_SET_SIZE = true;
1284 base.PATH_FILESTAT_SET_TIMES = true;
1285 base.PATH_SYMLINK = true;
1286 base.PATH_REMOVE_DIRECTORY = true;
1287 base.PATH_UNLINK_FILE = true;
1288 }
1289
1290 const result = posix.openatWasi(
1291 self.fd,
1292 sub_path,
1293 .{ .SYMLINK_FOLLOW = !args.no_follow },
1294 .{ .DIRECTORY = true },
1295 .{},
1296 base,
1297 base,
1298 );
1299 const fd = result catch |err| switch (err) {
1300 error.FileTooBig => unreachable, // can't happen for directories
1301 error.IsDir => unreachable, // we're setting DIRECTORY
1302 error.NoSpaceLeft => unreachable, // not setting CREAT
1303 error.PathAlreadyExists => unreachable, // not setting CREAT
1304 error.FileLocksNotSupported => unreachable, // locking folders is not supported
1305 error.WouldBlock => unreachable, // can't happen for directories
1306 error.FileBusy => unreachable, // can't happen for directories
1307 error.SharingViolation => unreachable,
1308 error.PipeBusy => unreachable,
1309 error.ProcessNotFound => unreachable,
1310 error.AntivirusInterference => unreachable,
1311
1312 else => |e| return e,
1313 };
1314 return .{ .fd = fd };
1249 var threaded: Io.Threaded = .init_single_threaded;
1250 const io = threaded.io();
1251 return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args));
13151252 },
13161253 else => {},
13171254 }
......@@ -1358,12 +1295,12 @@ pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenErr
13581295 var symlink_flags: posix.O = switch (native_os) {
13591296 .wasi => .{
13601297 .read = true,
1361 .NOFOLLOW = args.no_follow,
1298 .NOFOLLOW = !args.follow_symlinks,
13621299 .DIRECTORY = true,
13631300 },
13641301 else => .{
13651302 .ACCMODE = .RDONLY,
1366 .NOFOLLOW = args.no_follow,
1303 .NOFOLLOW = !args.follow_symlinks,
13671304 .DIRECTORY = true,
13681305 .CLOEXEC = true,
13691306 },
......@@ -1384,7 +1321,7 @@ pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenOptions) OpenEr
13841321 w.SYNCHRONIZE | w.FILE_TRAVERSE;
13851322 const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
13861323 const dir = self.makeOpenDirAccessMaskW(sub_path_w, flags, .{
1387 .no_follow = args.no_follow,
1324 .no_follow = !args.follow_symlinks,
13881325 .create_disposition = w.FILE_OPEN,
13891326 }) catch |err| switch (err) {
13901327 error.ReadOnlyFileSystem => unreachable,
......@@ -1923,7 +1860,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
19231860 if (treat_as_dir) {
19241861 if (stack.unusedCapacitySlice().len >= 1) {
19251862 var iterable_dir = top.iter.dir.openDir(entry.name, .{
1926 .no_follow = true,
1863 .follow_symlinks = false,
19271864 .iterate = true,
19281865 }) catch |err| switch (err) {
19291866 error.NotDir => {
......@@ -2019,7 +1956,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
20191956 handle_entry: while (true) {
20201957 if (treat_as_dir) {
20211958 break :iterable_dir parent_dir.openDir(name, .{
2022 .no_follow = true,
1959 .follow_symlinks = false,
20231960 .iterate = true,
20241961 }) catch |err| switch (err) {
20251962 error.NotDir => {
......@@ -2125,7 +2062,7 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint
21252062 handle_entry: while (true) {
21262063 if (treat_as_dir) {
21272064 const new_dir = dir.openDir(entry.name, .{
2128 .no_follow = true,
2065 .follow_symlinks = false,
21292066 .iterate = true,
21302067 }) catch |err| switch (err) {
21312068 error.NotDir => {
......@@ -2224,7 +2161,7 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File
22242161 handle_entry: while (true) {
22252162 if (treat_as_dir) {
22262163 break :iterable_dir self.openDir(sub_path, .{
2227 .no_follow = true,
2164 .follow_symlinks = false,
22282165 .iterate = true,
22292166 }) catch |err| switch (err) {
22302167 error.NotDir => {
lib/std/tar.zig+5-5
......@@ -977,7 +977,7 @@ test pipeToFileSystem {
977977 const data = @embedFile("tar/testdata/example.tar");
978978 var reader: std.Io.Reader = .fixed(data);
979979
980 var tmp = testing.tmpDir(.{ .no_follow = true });
980 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
981981 defer tmp.cleanup();
982982 const dir = tmp.dir;
983983
......@@ -1010,7 +1010,7 @@ test "pipeToFileSystem root_dir" {
10101010
10111011 // with strip_components = 1
10121012 {
1013 var tmp = testing.tmpDir(.{ .no_follow = true });
1013 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
10141014 defer tmp.cleanup();
10151015 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
10161016 defer diagnostics.deinit();
......@@ -1032,7 +1032,7 @@ test "pipeToFileSystem root_dir" {
10321032 // with strip_components = 0
10331033 {
10341034 reader = .fixed(data);
1035 var tmp = testing.tmpDir(.{ .no_follow = true });
1035 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
10361036 defer tmp.cleanup();
10371037 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
10381038 defer diagnostics.deinit();
......@@ -1084,7 +1084,7 @@ test "pipeToFileSystem strip_components" {
10841084 const data = @embedFile("tar/testdata/example.tar");
10851085 var reader: std.Io.Reader = .fixed(data);
10861086
1087 var tmp = testing.tmpDir(.{ .no_follow = true });
1087 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
10881088 defer tmp.cleanup();
10891089 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
10901090 defer diagnostics.deinit();
......@@ -1145,7 +1145,7 @@ test "executable bit" {
11451145 for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {
11461146 var reader: std.Io.Reader = .fixed(data);
11471147
1148 var tmp = testing.tmpDir(.{ .no_follow = true });
1148 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
11491149 //defer tmp.cleanup();
11501150
11511151 pipeToFileSystem(tmp.dir, &reader, .{