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 {...@@ -100,6 +100,18 @@ pub const Lock = enum {
100pub const OpenFlags = struct {100pub const OpenFlags = struct {
101 mode: OpenMode = .read_only,101 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
103 /// Open the file with an advisory lock to coordinate with other processes115 /// Open the file with an advisory lock to coordinate with other processes
104 /// accessing it at the same time. An exclusive lock will prevent other116 /// accessing it at the same time. An exclusive lock will prevent other
105 /// processes from acquiring a lock. A shared lock will prevent other117 /// processes from acquiring a lock. A shared lock will prevent other
...@@ -226,7 +238,9 @@ pub const OpenError = error{...@@ -226,7 +238,9 @@ pub const OpenError = error{
226 /// The file is too large to be opened. This error is unreachable238 /// The file is too large to be opened. This error is unreachable
227 /// for 64-bit targets, as well as when opening directories.239 /// for 64-bit targets, as well as when opening directories.
228 FileTooBig,240 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.
230 IsDir,244 IsDir,
231 /// A new path cannot be created because the device has no room for the new file.245 /// A new path cannot be created because the device has no room for the new file.
232 /// This error is only reachable when the `CREAT` flag is provided.246 /// This error is only reachable when the `CREAT` flag is provided.
lib/std/Io/Threaded.zig+36-22
...@@ -1621,14 +1621,8 @@ fn dirMakePath(...@@ -1621,14 +1621,8 @@ fn dirMakePath(
1621 // stat the file and return an error if it's not a directory1621 // stat the file and return an error if it's not a directory
1622 // this is important because otherwise a dangling symlink1622 // this is important because otherwise a dangling symlink
1623 // could cause an infinite loop1623 // could cause an infinite loop
1624 check_dir: {1624 const fstat = dirStatFile(t, dir, component.path, .{});
1625 // workaround for windows, see https://github.com/ziglang/zig/issues/167381625 if (fstat.kind != .directory) return error.NotDir;
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 }
1632 },1626 },
1633 error.FileNotFound => |e| {1627 error.FileNotFound => |e| {
1634 component = it.previous() orelse return e;1628 component = it.previous() orelse return e;
...@@ -1750,16 +1744,10 @@ fn dirMakeOpenPathWindows(...@@ -1750,16 +1744,10 @@ fn dirMakeOpenPathWindows(
1750 // stat the file and return an error if it's not a directory1744 // stat the file and return an error if it's not a directory
1751 // this is important because otherwise a dangling symlink1745 // this is important because otherwise a dangling symlink
1752 // could cause an infinite loop1746 // could cause an infinite loop
1753 check_dir: {1747 const fstat = dirStatFileWindows(t, dir, component.path, .{
1754 // workaround for windows, see https://github.com/ziglang/zig/issues/167381748 .follow_symlinks = options.follow_symlinks,
1755 const fstat = dirStatFileWindows(t, dir, component.path, .{1749 });
1756 .follow_symlinks = options.follow_symlinks,1750 if (fstat.kind != .directory) return error.NotDir;
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 }
17631751
1764 component = it.next().?;1752 component = it.next().?;
1765 continue;1753 continue;
...@@ -2791,6 +2779,18 @@ fn dirOpenFilePosix(...@@ -2791,6 +2779,18 @@ fn dirOpenFilePosix(
2791 };2779 };
2792 errdefer posix.close(fd);2780 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
2794 if (have_flock and !have_flock_open_flags and flags.lock != .none) {2794 if (have_flock and !have_flock_open_flags and flags.lock != .none) {
2795 const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;2795 const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;
2796 const lock_flags = switch (flags.lock) {2796 const lock_flags = switch (flags.lock) {
...@@ -2936,7 +2936,7 @@ pub fn dirOpenFileWtf16(...@@ -2936,7 +2936,7 @@ pub fn dirOpenFileWtf16(
2936 .OPEN,2936 .OPEN,
2937 .{2937 .{
2938 .IO = if (flags.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,2938 .IO = if (flags.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,
2939 .NON_DIRECTORY_FILE = true,2939 .NON_DIRECTORY_FILE = !flags.allow_directory,
2940 .OPEN_REPARSE_POINT = !flags.follow_symlinks,2940 .OPEN_REPARSE_POINT = !flags.follow_symlinks,
2941 },2941 },
2942 null,2942 null,
...@@ -3052,9 +3052,8 @@ fn dirOpenFileWasi(...@@ -3052,9 +3052,8 @@ fn dirOpenFileWasi(
3052 while (true) {3052 while (true) {
3053 switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) {3053 switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) {
3054 .SUCCESS => {3054 .SUCCESS => {
3055 errdefer posix.close(fd);
3056 current_thread.endSyscall();3055 current_thread.endSyscall();
3057 return .{ .handle = fd };3056 break;
3058 },3057 },
3059 .INTR => {3058 .INTR => {
3060 try current_thread.checkCancel();3059 try current_thread.checkCancel();
...@@ -3088,6 +3087,21 @@ fn dirOpenFileWasi(...@@ -3088,6 +3087,21 @@ fn dirOpenFileWasi(
3088 },3087 },
3089 }3088 }
3090 }3089 }
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 };
3091}3105}
30923106
3093const dirOpenDir = switch (native_os) {3107const dirOpenDir = switch (native_os) {
...@@ -4577,7 +4591,7 @@ fn dirSymLinkWindows(...@@ -4577,7 +4591,7 @@ fn dirSymLinkWindows(
4577 .access_mask = w.SYNCHRONIZE | w.GENERIC_READ | w.GENERIC_WRITE,4591 .access_mask = w.SYNCHRONIZE | w.GENERIC_READ | w.GENERIC_WRITE,
4578 .dir = dir,4592 .dir = dir,
4579 .creation = w.FILE_CREATE,4593 .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,
4581 }) catch |err| switch (err) {4595 }) catch |err| switch (err) {
4582 error.IsDir => return error.PathAlreadyExists,4596 error.IsDir => return error.PathAlreadyExists,
4583 error.NotDir => return error.Unexpected,4597 error.NotDir => return error.Unexpected,
lib/std/fs/test.zig+23-6
...@@ -750,14 +750,26 @@ test "Dir.statFile" {...@@ -750,14 +750,26 @@ test "Dir.statFile" {
750 try testWithAllSupportedPathTypes(struct {750 try testWithAllSupportedPathTypes(struct {
751 fn impl(ctx: *TestContext) !void {751 fn impl(ctx: *TestContext) !void {
752 const io = ctx.io;752 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 const stat = try ctx.dir.statFile(io, test_file_name, .{});
760 try expectEqual(File.Kind.file, stat.kind);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 }
761 }773 }
762 }.impl);774 }.impl);
763}775}
...@@ -840,10 +852,15 @@ test "file operations on directories" {...@@ -840,10 +852,15 @@ test "file operations on directories" {
840 handle.close(io);852 handle.close(io);
841 } else {853 } else {
842 // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.854 // 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
844 try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .mode = .read_write }));855 try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .mode = .read_write }));
845 }856 }
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
847 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {864 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
848 try expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{}));865 try expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{}));
849 try expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name));866 try expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name));
lib/std/os/windows.zig+7-9
...@@ -2310,17 +2310,15 @@ pub const OpenFileOptions = struct {...@@ -2310,17 +2310,15 @@ pub const OpenFileOptions = struct {
2310 sa: ?*SECURITY_ATTRIBUTES = null,2310 sa: ?*SECURITY_ATTRIBUTES = null,
2311 share_access: FILE.SHARE = .VALID_FLAGS,2311 share_access: FILE.SHARE = .VALID_FLAGS,
2312 creation: FILE.CREATE_DISPOSITION,2312 creation: FILE.CREATE_DISPOSITION,
2313 /// If true, tries to open path as a directory.2313 filter: Filter = .non_directory_only,
2314 /// Defaults to false.
2315 filter: Filter = .file_only,
2316 /// If false, tries to open path as a reparse point without dereferencing it.2314 /// If false, tries to open path as a reparse point without dereferencing it.
2317 /// Defaults to true.2315 /// Defaults to true.
2318 follow_symlinks: bool = true,2316 follow_symlinks: bool = true,
23192317
2320 pub const Filter = enum {2318 pub const Filter = enum {
2321 /// Causes `OpenFile` to return `error.IsDir` if the opened handle would be a directory.2319 /// Causes `OpenFile` to return `error.IsDir` if the opened handle would be a directory.
2322 file_only,2320 non_directory_only,
2323 /// Causes `OpenFile` to return `error.NotDir` if the opened handle would be a file.2321 /// Causes `OpenFile` to return `error.NotDir` if the opened handle is not a directory.
2324 dir_only,2322 dir_only,
2325 /// `OpenFile` does not discriminate between opening files and directories.2323 /// `OpenFile` does not discriminate between opening files and directories.
2326 any,2324 any,
...@@ -2328,10 +2326,10 @@ pub const OpenFileOptions = struct {...@@ -2328,10 +2326,10 @@ pub const OpenFileOptions = struct {
2328};2326};
23292327
2330pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HANDLE {2328pub 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) {
2332 return error.IsDir;2330 return error.IsDir;
2333 }2331 }
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) {
2335 return error.IsDir;2333 return error.IsDir;
2336 }2334 }
23372335
...@@ -2366,7 +2364,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN...@@ -2366,7 +2364,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN
2366 options.creation,2364 options.creation,
2367 .{2365 .{
2368 .DIRECTORY_FILE = options.filter == .dir_only,2366 .DIRECTORY_FILE = options.filter == .dir_only,
2369 .NON_DIRECTORY_FILE = options.filter == .file_only,2367 .NON_DIRECTORY_FILE = options.filter == .non_directory_only,
2370 .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,2368 .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,
2371 .OPEN_REPARSE_POINT = !options.follow_symlinks,2369 .OPEN_REPARSE_POINT = !options.follow_symlinks,
2372 },2370 },
...@@ -3040,7 +3038,7 @@ pub fn CreateSymbolicLink(...@@ -3040,7 +3038,7 @@ pub fn CreateSymbolicLink(
3040 },3038 },
3041 .dir = dir,3039 .dir = dir,
3042 .creation = .CREATE,3040 .creation = .CREATE,
3043 .filter = if (is_directory) .dir_only else .file_only,3041 .filter = if (is_directory) .dir_only else .non_directory_only,
3044 }) catch |err| switch (err) {3042 }) catch |err| switch (err) {
3045 error.IsDir => return error.PathAlreadyExists,3043 error.IsDir => return error.PathAlreadyExists,
3046 error.NotDir => return error.Unexpected,3044 error.NotDir => return error.Unexpected,