authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-12-16 20:02:05-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-23 22:15:10-08:00
logd72983da44b7ebe967868ba5236a2c4b05e84510
tree21cd22d68e90511d59519047fbb9525c711777fc
parent1264469a41e89e72635c3b60b4cd3dca17876964

File.OpenFlags: Add `allow_directory` and default it to true

This is one way of addressing/closing https://github.com/ziglang/zig/issues/16738 Previously, there was a mismatch between the default behaviors on Windows vs other platforms, where Windows was implicitly using .NON_DIRECTORY_FILE for its `openFile` implementation which caused `error.IsDir` when opening a directory, while on other platforms there is no equivalent flag for the `open` syscall. This meant that `openFile` on a path of a directory would fail on Windows but succeed on other platforms. Adding `allow_directory` to `File.OpenFlags` serves two purposes: 1. It provides a cross-platform way to get the `.NON_DIRECTORY_FILE` behavior in the most efficient available way for the platform (on Windows, no extra syscalls are required, on other systems, an extra `fstat` is required) 2. It allows `statFile` to be implemented on top of `openFile` on Windows while still allowing `statFile` to work on directory paths. Before this commit, `statFile` on a directory path on Windows failed with `error.IsDir` Note: The second purpose could have been addressed in different ways (bespoke call to NtCreateFile in the `statFile` implementation to avoid passing `NON_DIRECTORY_FILE`, or just never pass `NON_DIRECTORY_FILE` in the `openFile` implementation), so the first purpose is the more relevant/motivating force behind this change. The default being `true` is intended to cut down on the number of syscalls as much as possible when using the default flags.

4 files changed, 81 insertions(+), 38 deletions(-)

lib/std/Io/File.zig+15-1
......@@ -100,6 +100,18 @@ pub const Lock = enum {
100100pub const OpenFlags = struct {
101101 mode: OpenMode = .read_only,
102102
103 /// Determines the behavior when opening a path that refers to a directory.
104 /// If set to true, directories may be opened, but `error.IsDir` is still
105 /// possible in certain scenarios, e.g. attempting to open a directory with
106 /// write permissions.
107 /// If set to false, `error.IsDir` will always be returned when opening a directory.
108 ///
109 /// When set to false:
110 /// * On Windows, the behavior is implemented without any extra syscalls.
111 /// * On other operating systems, the behavior is implemented with an additional
112 /// `fstat` syscall.
113 allow_directory: bool = true,
114
103115 /// Open the file with an advisory lock to coordinate with other processes
104116 /// accessing it at the same time. An exclusive lock will prevent other
105117 /// processes from acquiring a lock. A shared lock will prevent other
......@@ -226,7 +238,9 @@ pub const OpenError = error{
226238 /// The file is too large to be opened. This error is unreachable
227239 /// for 64-bit targets, as well as when opening directories.
228240 FileTooBig,
229 /// The path refers to directory but the `DIRECTORY` flag was not provided.
241 /// Either:
242 /// * The path refers to a directory and write permissions were requested.
243 /// * The path refers to a directory and `allow_directory` was set to false.
230244 IsDir,
231245 /// A new path cannot be created because the device has no room for the new file.
232246 /// This error is only reachable when the `CREAT` flag is provided.
lib/std/Io/Threaded.zig+36-22
......@@ -1621,14 +1621,8 @@ fn dirMakePath(
16211621 // stat the file and return an error if it's not a directory
16221622 // this is important because otherwise a dangling symlink
16231623 // could cause an infinite loop
1624 check_dir: {
1625 // workaround for windows, see https://github.com/ziglang/zig/issues/16738
1626 const fstat = dirStatFile(t, dir, component.path, .{}) catch |stat_err| switch (stat_err) {
1627 error.IsDir => break :check_dir,
1628 else => |e| return e,
1629 };
1630 if (fstat.kind != .directory) return error.NotDir;
1631 }
1624 const fstat = dirStatFile(t, dir, component.path, .{});
1625 if (fstat.kind != .directory) return error.NotDir;
16321626 },
16331627 error.FileNotFound => |e| {
16341628 component = it.previous() orelse return e;
......@@ -1750,16 +1744,10 @@ fn dirMakeOpenPathWindows(
17501744 // stat the file and return an error if it's not a directory
17511745 // this is important because otherwise a dangling symlink
17521746 // could cause an infinite loop
1753 check_dir: {
1754 // workaround for windows, see https://github.com/ziglang/zig/issues/16738
1755 const fstat = dirStatFileWindows(t, dir, component.path, .{
1756 .follow_symlinks = options.follow_symlinks,
1757 }) catch |stat_err| switch (stat_err) {
1758 error.IsDir => break :check_dir,
1759 else => |e| return e,
1760 };
1761 if (fstat.kind != .directory) return error.NotDir;
1762 }
1747 const fstat = dirStatFileWindows(t, dir, component.path, .{
1748 .follow_symlinks = options.follow_symlinks,
1749 });
1750 if (fstat.kind != .directory) return error.NotDir;
17631751
17641752 component = it.next().?;
17651753 continue;
......@@ -2791,6 +2779,18 @@ fn dirOpenFilePosix(
27912779 };
27922780 errdefer posix.close(fd);
27932781
2782 if (!flags.allow_directory) {
2783 const is_dir = is_dir: {
2784 const stat = fileStat(t, .{ .handle = fd }) catch |err| switch (err) {
2785 // The directory-ness is either unknown or unknowable
2786 error.Streaming => break :is_dir false,
2787 else => |e| return e,
2788 };
2789 break :is_dir stat.kind == .directory;
2790 };
2791 if (is_dir) return error.IsDir;
2792 }
2793
27942794 if (have_flock and !have_flock_open_flags and flags.lock != .none) {
27952795 const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;
27962796 const lock_flags = switch (flags.lock) {
......@@ -2936,7 +2936,7 @@ pub fn dirOpenFileWtf16(
29362936 .OPEN,
29372937 .{
29382938 .IO = if (flags.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,
2939 .NON_DIRECTORY_FILE = true,
2939 .NON_DIRECTORY_FILE = !flags.allow_directory,
29402940 .OPEN_REPARSE_POINT = !flags.follow_symlinks,
29412941 },
29422942 null,
......@@ -3052,9 +3052,8 @@ fn dirOpenFileWasi(
30523052 while (true) {
30533053 switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) {
30543054 .SUCCESS => {
3055 errdefer posix.close(fd);
30563055 current_thread.endSyscall();
3057 return .{ .handle = fd };
3056 break;
30583057 },
30593058 .INTR => {
30603059 try current_thread.checkCancel();
......@@ -3088,6 +3087,21 @@ fn dirOpenFileWasi(
30883087 },
30893088 }
30903089 }
3090 errdefer posix.close(fd);
3091
3092 if (!flags.allow_directory) {
3093 const is_dir = is_dir: {
3094 const stat = fileStat(t, .{ .handle = fd }) catch |err| switch (err) {
3095 // The directory-ness is either unknown or unknowable
3096 error.Streaming => break :is_dir false,
3097 else => |e| return e,
3098 };
3099 break :is_dir stat.kind == .directory;
3100 };
3101 if (is_dir) return error.IsDir;
3102 }
3103
3104 return .{ .handle = fd };
30913105}
30923106
30933107const dirOpenDir = switch (native_os) {
......@@ -4577,7 +4591,7 @@ fn dirSymLinkWindows(
45774591 .access_mask = w.SYNCHRONIZE | w.GENERIC_READ | w.GENERIC_WRITE,
45784592 .dir = dir,
45794593 .creation = w.FILE_CREATE,
4580 .filter = if (flags.is_directory) .dir_only else .file_only,
4594 .filter = if (flags.is_directory) .dir_only else .non_directory_only,
45814595 }) catch |err| switch (err) {
45824596 error.IsDir => return error.PathAlreadyExists,
45834597 error.NotDir => return error.Unexpected,
lib/std/fs/test.zig+23-6
......@@ -750,14 +750,26 @@ test "Dir.statFile" {
750750 try testWithAllSupportedPathTypes(struct {
751751 fn impl(ctx: *TestContext) !void {
752752 const io = ctx.io;
753 const test_file_name = try ctx.transformPath("test_file");
753 {
754 const test_file_name = try ctx.transformPath("test_file");
754755
755 try expectError(error.FileNotFound, ctx.dir.statFile(io, test_file_name, .{}));
756 try expectError(error.FileNotFound, ctx.dir.statFile(io, test_file_name, .{}));
756757
757 try ctx.dir.writeFile(io, .{ .sub_path = test_file_name, .data = "" });
758 try ctx.dir.writeFile(io, .{ .sub_path = test_file_name, .data = "" });
758759
759 const stat = try ctx.dir.statFile(io, test_file_name, .{});
760 try expectEqual(File.Kind.file, stat.kind);
760 const stat = try ctx.dir.statFile(io, test_file_name, .{});
761 try expectEqual(.file, stat.kind);
762 }
763 {
764 const test_dir_name = try ctx.transformPath("test_dir");
765
766 try expectError(error.FileNotFound, ctx.dir.statFile(io, test_dir_name, .{}));
767
768 try ctx.dir.makeDir(io, test_dir_name);
769
770 const stat = try ctx.dir.statFile(io, test_dir_name, .{});
771 try expectEqual(.directory, stat.kind);
772 }
761773 }
762774 }.impl);
763775}
......@@ -840,10 +852,15 @@ test "file operations on directories" {
840852 handle.close(io);
841853 } else {
842854 // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.
843 // TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732
844855 try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .mode = .read_write }));
845856 }
846857
858 {
859 const handle = try ctx.dir.openFile(io, test_dir_name, .{ .allow_directory = true, .mode = .read_only });
860 handle.close(io);
861 }
862 try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .allow_directory = false, .mode = .read_only }));
863
847864 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
848865 try expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{}));
849866 try expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name));
lib/std/os/windows.zig+7-9
......@@ -2310,17 +2310,15 @@ pub const OpenFileOptions = struct {
23102310 sa: ?*SECURITY_ATTRIBUTES = null,
23112311 share_access: FILE.SHARE = .VALID_FLAGS,
23122312 creation: FILE.CREATE_DISPOSITION,
2313 /// If true, tries to open path as a directory.
2314 /// Defaults to false.
2315 filter: Filter = .file_only,
2313 filter: Filter = .non_directory_only,
23162314 /// If false, tries to open path as a reparse point without dereferencing it.
23172315 /// Defaults to true.
23182316 follow_symlinks: bool = true,
23192317
23202318 pub const Filter = enum {
23212319 /// Causes `OpenFile` to return `error.IsDir` if the opened handle would be a directory.
2322 file_only,
2323 /// Causes `OpenFile` to return `error.NotDir` if the opened handle would be a file.
2320 non_directory_only,
2321 /// Causes `OpenFile` to return `error.NotDir` if the opened handle is not a directory.
23242322 dir_only,
23252323 /// `OpenFile` does not discriminate between opening files and directories.
23262324 any,
......@@ -2328,10 +2326,10 @@ pub const OpenFileOptions = struct {
23282326};
23292327
23302328pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HANDLE {
2331 if (mem.eql(u16, sub_path_w, &[_]u16{'.'}) and options.filter == .file_only) {
2329 if (mem.eql(u16, sub_path_w, &[_]u16{'.'}) and options.filter == .non_directory_only) {
23322330 return error.IsDir;
23332331 }
2334 if (mem.eql(u16, sub_path_w, &[_]u16{ '.', '.' }) and options.filter == .file_only) {
2332 if (mem.eql(u16, sub_path_w, &[_]u16{ '.', '.' }) and options.filter == .non_directory_only) {
23352333 return error.IsDir;
23362334 }
23372335
......@@ -2366,7 +2364,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN
23662364 options.creation,
23672365 .{
23682366 .DIRECTORY_FILE = options.filter == .dir_only,
2369 .NON_DIRECTORY_FILE = options.filter == .file_only,
2367 .NON_DIRECTORY_FILE = options.filter == .non_directory_only,
23702368 .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,
23712369 .OPEN_REPARSE_POINT = !options.follow_symlinks,
23722370 },
......@@ -3040,7 +3038,7 @@ pub fn CreateSymbolicLink(
30403038 },
30413039 .dir = dir,
30423040 .creation = .CREATE,
3043 .filter = if (is_directory) .dir_only else .file_only,
3041 .filter = if (is_directory) .dir_only else .non_directory_only,
30443042 }) catch |err| switch (err) {
30453043 error.IsDir => return error.PathAlreadyExists,
30463044 error.NotDir => return error.Unexpected,