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 {...@@ -666,6 +666,7 @@ pub const VTable = struct {
666 dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void,666 dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void,
667 dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File,667 dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File,
668 dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File,668 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,
669 fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat,670 fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat,
670 fileClose: *const fn (?*anyopaque, File) void,671 fileClose: *const fn (?*anyopaque, File) void,
671 fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize,672 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{...@@ -69,6 +69,30 @@ pub const OpenError = error{
69 NetworkNotFound,69 NetworkNotFound,
70} || PathNameError || Io.Cancelable || Io.UnexpectedError;70} || 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
72pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {96pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
73 return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags);97 return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags);
74}98}
lib/std/Io/Threaded.zig+86-1
...@@ -198,6 +198,11 @@ pub fn io(t: *Threaded) Io {...@@ -198,6 +198,11 @@ pub fn io(t: *Threaded) Io {
198 .wasi => dirOpenFileWasi,198 .wasi => dirOpenFileWasi,
199 else => dirOpenFilePosix,199 else => dirOpenFilePosix,
200 },200 },
201 .dirOpenDir = switch (builtin.os.tag) {
202 .windows => @panic("TODO"),
203 .wasi => dirOpenDirWasi,
204 else => dirOpenDirPosix,
205 },
201 .fileClose = fileClose,206 .fileClose = fileClose,
202 .fileWriteStreaming = fileWriteStreaming,207 .fileWriteStreaming = fileWriteStreaming,
203 .fileWritePositional = fileWritePositional,208 .fileWritePositional = fileWritePositional,
...@@ -1429,7 +1434,6 @@ fn dirCreateFileWasi(...@@ -1429,7 +1434,6 @@ fn dirCreateFileWasi(
1429 .CANCELED => return error.Canceled,1434 .CANCELED => return error.Canceled,
14301435
1431 .FAULT => |err| return errnoBug(err),1436 .FAULT => |err| return errnoBug(err),
1432 // Provides INVAL with a linux host on a bad path name, but NOENT on Windows
1433 .INVAL => return error.BadPathName,1437 .INVAL => return error.BadPathName,
1434 .BADF => |err| return errnoBug(err), // File descriptor used after closed.1438 .BADF => |err| return errnoBug(err), // File descriptor used after closed.
1435 .ACCES => return error.AccessDenied,1439 .ACCES => return error.AccessDenied,
...@@ -1656,6 +1660,87 @@ fn dirOpenFileWasi(...@@ -1656,6 +1660,87 @@ fn dirOpenFileWasi(
1656 }1660 }
1657}1661}
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
1659fn fileClose(userdata: ?*anyopaque, file: Io.File) void {1744fn fileClose(userdata: ?*anyopaque, file: Io.File) void {
1660 const t: *Threaded = @ptrCast(@alignCast(userdata));1745 const t: *Threaded = @ptrCast(@alignCast(userdata));
1661 _ = t;1746 _ = t;
lib/std/fs/Dir.zig+13-76
...@@ -1235,28 +1235,10 @@ pub fn setAsCwd(self: Dir) !void {...@@ -1235,28 +1235,10 @@ pub fn setAsCwd(self: Dir) !void {
1235 try posix.fchdir(self.fd);1235 try posix.fchdir(self.fd);
1236}1236}
12371237
1238pub const OpenOptions = struct {1238/// Deprecated in favor of `Io.Dir.OpenOptions`.
1239 /// `true` means the opened directory can be used as the `Dir` parameter1239pub const OpenOptions = Io.Dir.OpenOptions;
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};
12511240
1252/// Opens a directory at the given path. The directory is a system resource that remains1241/// Deprecated in favor of `Io.Dir.openDir`.
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.
1260pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir {1242pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir {
1261 switch (native_os) {1243 switch (native_os) {
1262 .windows => {1244 .windows => {
...@@ -1264,54 +1246,9 @@ pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir...@@ -1264,54 +1246,9 @@ pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir
1264 return self.openDirW(sub_path_w.span().ptr, args);1246 return self.openDirW(sub_path_w.span().ptr, args);
1265 },1247 },
1266 .wasi => if (!builtin.link_libc) {1248 .wasi => if (!builtin.link_libc) {
1267 var base: std.os.wasi.rights_t = .{1249 var threaded: Io.Threaded = .init_single_threaded;
1268 .FD_FILESTAT_GET = true,1250 const io = threaded.io();
1269 .FD_FDSTAT_SET_FLAGS = true,1251 return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args));
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 };
1315 },1252 },
1316 else => {},1253 else => {},
1317 }1254 }
...@@ -1358,12 +1295,12 @@ pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenErr...@@ -1358,12 +1295,12 @@ pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenErr
1358 var symlink_flags: posix.O = switch (native_os) {1295 var symlink_flags: posix.O = switch (native_os) {
1359 .wasi => .{1296 .wasi => .{
1360 .read = true,1297 .read = true,
1361 .NOFOLLOW = args.no_follow,1298 .NOFOLLOW = !args.follow_symlinks,
1362 .DIRECTORY = true,1299 .DIRECTORY = true,
1363 },1300 },
1364 else => .{1301 else => .{
1365 .ACCMODE = .RDONLY,1302 .ACCMODE = .RDONLY,
1366 .NOFOLLOW = args.no_follow,1303 .NOFOLLOW = !args.follow_symlinks,
1367 .DIRECTORY = true,1304 .DIRECTORY = true,
1368 .CLOEXEC = true,1305 .CLOEXEC = true,
1369 },1306 },
...@@ -1384,7 +1321,7 @@ pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenOptions) OpenEr...@@ -1384,7 +1321,7 @@ pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenOptions) OpenEr
1384 w.SYNCHRONIZE | w.FILE_TRAVERSE;1321 w.SYNCHRONIZE | w.FILE_TRAVERSE;
1385 const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;1322 const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
1386 const dir = self.makeOpenDirAccessMaskW(sub_path_w, flags, .{1323 const dir = self.makeOpenDirAccessMaskW(sub_path_w, flags, .{
1387 .no_follow = args.no_follow,1324 .no_follow = !args.follow_symlinks,
1388 .create_disposition = w.FILE_OPEN,1325 .create_disposition = w.FILE_OPEN,
1389 }) catch |err| switch (err) {1326 }) catch |err| switch (err) {
1390 error.ReadOnlyFileSystem => unreachable,1327 error.ReadOnlyFileSystem => unreachable,
...@@ -1923,7 +1860,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {...@@ -1923,7 +1860,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
1923 if (treat_as_dir) {1860 if (treat_as_dir) {
1924 if (stack.unusedCapacitySlice().len >= 1) {1861 if (stack.unusedCapacitySlice().len >= 1) {
1925 var iterable_dir = top.iter.dir.openDir(entry.name, .{1862 var iterable_dir = top.iter.dir.openDir(entry.name, .{
1926 .no_follow = true,1863 .follow_symlinks = false,
1927 .iterate = true,1864 .iterate = true,
1928 }) catch |err| switch (err) {1865 }) catch |err| switch (err) {
1929 error.NotDir => {1866 error.NotDir => {
...@@ -2019,7 +1956,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {...@@ -2019,7 +1956,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
2019 handle_entry: while (true) {1956 handle_entry: while (true) {
2020 if (treat_as_dir) {1957 if (treat_as_dir) {
2021 break :iterable_dir parent_dir.openDir(name, .{1958 break :iterable_dir parent_dir.openDir(name, .{
2022 .no_follow = true,1959 .follow_symlinks = false,
2023 .iterate = true,1960 .iterate = true,
2024 }) catch |err| switch (err) {1961 }) catch |err| switch (err) {
2025 error.NotDir => {1962 error.NotDir => {
...@@ -2125,7 +2062,7 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint...@@ -2125,7 +2062,7 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint
2125 handle_entry: while (true) {2062 handle_entry: while (true) {
2126 if (treat_as_dir) {2063 if (treat_as_dir) {
2127 const new_dir = dir.openDir(entry.name, .{2064 const new_dir = dir.openDir(entry.name, .{
2128 .no_follow = true,2065 .follow_symlinks = false,
2129 .iterate = true,2066 .iterate = true,
2130 }) catch |err| switch (err) {2067 }) catch |err| switch (err) {
2131 error.NotDir => {2068 error.NotDir => {
...@@ -2224,7 +2161,7 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File...@@ -2224,7 +2161,7 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File
2224 handle_entry: while (true) {2161 handle_entry: while (true) {
2225 if (treat_as_dir) {2162 if (treat_as_dir) {
2226 break :iterable_dir self.openDir(sub_path, .{2163 break :iterable_dir self.openDir(sub_path, .{
2227 .no_follow = true,2164 .follow_symlinks = false,
2228 .iterate = true,2165 .iterate = true,
2229 }) catch |err| switch (err) {2166 }) catch |err| switch (err) {
2230 error.NotDir => {2167 error.NotDir => {
lib/std/tar.zig+5-5
...@@ -977,7 +977,7 @@ test pipeToFileSystem {...@@ -977,7 +977,7 @@ test pipeToFileSystem {
977 const data = @embedFile("tar/testdata/example.tar");977 const data = @embedFile("tar/testdata/example.tar");
978 var reader: std.Io.Reader = .fixed(data);978 var reader: std.Io.Reader = .fixed(data);
979979
980 var tmp = testing.tmpDir(.{ .no_follow = true });980 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
981 defer tmp.cleanup();981 defer tmp.cleanup();
982 const dir = tmp.dir;982 const dir = tmp.dir;
983983
...@@ -1010,7 +1010,7 @@ test "pipeToFileSystem root_dir" {...@@ -1010,7 +1010,7 @@ test "pipeToFileSystem root_dir" {
10101010
1011 // with strip_components = 11011 // with strip_components = 1
1012 {1012 {
1013 var tmp = testing.tmpDir(.{ .no_follow = true });1013 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1014 defer tmp.cleanup();1014 defer tmp.cleanup();
1015 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };1015 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1016 defer diagnostics.deinit();1016 defer diagnostics.deinit();
...@@ -1032,7 +1032,7 @@ test "pipeToFileSystem root_dir" {...@@ -1032,7 +1032,7 @@ test "pipeToFileSystem root_dir" {
1032 // with strip_components = 01032 // with strip_components = 0
1033 {1033 {
1034 reader = .fixed(data);1034 reader = .fixed(data);
1035 var tmp = testing.tmpDir(.{ .no_follow = true });1035 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1036 defer tmp.cleanup();1036 defer tmp.cleanup();
1037 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };1037 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1038 defer diagnostics.deinit();1038 defer diagnostics.deinit();
...@@ -1084,7 +1084,7 @@ test "pipeToFileSystem strip_components" {...@@ -1084,7 +1084,7 @@ test "pipeToFileSystem strip_components" {
1084 const data = @embedFile("tar/testdata/example.tar");1084 const data = @embedFile("tar/testdata/example.tar");
1085 var reader: std.Io.Reader = .fixed(data);1085 var reader: std.Io.Reader = .fixed(data);
10861086
1087 var tmp = testing.tmpDir(.{ .no_follow = true });1087 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1088 defer tmp.cleanup();1088 defer tmp.cleanup();
1089 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };1089 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1090 defer diagnostics.deinit();1090 defer diagnostics.deinit();
...@@ -1145,7 +1145,7 @@ test "executable bit" {...@@ -1145,7 +1145,7 @@ test "executable bit" {
1145 for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {1145 for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {
1146 var reader: std.Io.Reader = .fixed(data);1146 var reader: std.Io.Reader = .fixed(data);
11471147
1148 var tmp = testing.tmpDir(.{ .no_follow = true });1148 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1149 //defer tmp.cleanup();1149 //defer tmp.cleanup();
11501150
1151 pipeToFileSystem(tmp.dir, &reader, .{1151 pipeToFileSystem(tmp.dir, &reader, .{