| author | |
| committer | |
| log | d72983da44b7ebe967868ba5236a2c4b05e84510 |
| tree | 21cd22d68e90511d59519047fbb9525c711777fc |
| parent | 1264469a41e89e72635c3b60b4cd3dca17876964 |
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 | 100 | pub const OpenFlags = struct { |
| 101 | 101 | mode: OpenMode = .read_only, |
| 102 | 102 | |
| 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 | 115 | /// Open the file with an advisory lock to coordinate with other processes |
| 104 | 116 | /// accessing it at the same time. An exclusive lock will prevent other |
| 105 | 117 | /// processes from acquiring a lock. A shared lock will prevent other |
| ... | ... | @@ -226,7 +238,9 @@ pub const OpenError = error{ |
| 226 | 238 | /// The file is too large to be opened. This error is unreachable |
| 227 | 239 | /// for 64-bit targets, as well as when opening directories. |
| 228 | 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 | 244 | IsDir, |
| 231 | 245 | /// A new path cannot be created because the device has no room for the new file. |
| 232 | 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 | 1621 | // stat the file and return an error if it's not a directory |
| 1622 | 1622 | // this is important because otherwise a dangling symlink |
| 1623 | 1623 | // 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; | |
| 1632 | 1626 | }, |
| 1633 | 1627 | error.FileNotFound => |e| { |
| 1634 | 1628 | component = it.previous() orelse return e; |
| ... | ... | @@ -1750,16 +1744,10 @@ fn dirMakeOpenPathWindows( |
| 1750 | 1744 | // stat the file and return an error if it's not a directory |
| 1751 | 1745 | // this is important because otherwise a dangling symlink |
| 1752 | 1746 | // 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; | |
| 1763 | 1751 | |
| 1764 | 1752 | component = it.next().?; |
| 1765 | 1753 | continue; |
| ... | ... | @@ -2791,6 +2779,18 @@ fn dirOpenFilePosix( |
| 2791 | 2779 | }; |
| 2792 | 2780 | errdefer posix.close(fd); |
| 2793 | 2781 | |
| 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 | 2794 | if (have_flock and !have_flock_open_flags and flags.lock != .none) { |
| 2795 | 2795 | const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; |
| 2796 | 2796 | const lock_flags = switch (flags.lock) { |
| ... | ... | @@ -2936,7 +2936,7 @@ pub fn dirOpenFileWtf16( |
| 2936 | 2936 | .OPEN, |
| 2937 | 2937 | .{ |
| 2938 | 2938 | .IO = if (flags.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS, |
| 2939 | .NON_DIRECTORY_FILE = true, | |
| 2939 | .NON_DIRECTORY_FILE = !flags.allow_directory, | |
| 2940 | 2940 | .OPEN_REPARSE_POINT = !flags.follow_symlinks, |
| 2941 | 2941 | }, |
| 2942 | 2942 | null, |
| ... | ... | @@ -3052,9 +3052,8 @@ fn dirOpenFileWasi( |
| 3052 | 3052 | while (true) { |
| 3053 | 3053 | switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) { |
| 3054 | 3054 | .SUCCESS => { |
| 3055 | errdefer posix.close(fd); | |
| 3056 | 3055 | current_thread.endSyscall(); |
| 3057 | return .{ .handle = fd }; | |
| 3056 | break; | |
| 3058 | 3057 | }, |
| 3059 | 3058 | .INTR => { |
| 3060 | 3059 | try current_thread.checkCancel(); |
| ... | ... | @@ -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 | } |
| 3092 | 3106 | |
| 3093 | 3107 | const dirOpenDir = switch (native_os) { |
| ... | ... | @@ -4577,7 +4591,7 @@ fn dirSymLinkWindows( |
| 4577 | 4591 | .access_mask = w.SYNCHRONIZE | w.GENERIC_READ | w.GENERIC_WRITE, |
| 4578 | 4592 | .dir = dir, |
| 4579 | 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 | 4595 | }) catch |err| switch (err) { |
| 4582 | 4596 | error.IsDir => return error.PathAlreadyExists, |
| 4583 | 4597 | error.NotDir => return error.Unexpected, |
lib/std/fs/test.zig+23-6| ... | ... | @@ -750,14 +750,26 @@ test "Dir.statFile" { |
| 750 | 750 | try testWithAllSupportedPathTypes(struct { |
| 751 | 751 | fn impl(ctx: *TestContext) !void { |
| 752 | 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"); | |
| 754 | 755 | |
| 755 | try expectError(error.FileNotFound, ctx.dir.statFile(io, test_file_name, .{})); | |
| 756 | try expectError(error.FileNotFound, ctx.dir.statFile(io, test_file_name, .{})); | |
| 756 | 757 | |
| 757 | try ctx.dir.writeFile(io, .{ .sub_path = test_file_name, .data = "" }); | |
| 758 | try ctx.dir.writeFile(io, .{ .sub_path = test_file_name, .data = "" }); | |
| 758 | 759 | |
| 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 | } | |
| 761 | 773 | } |
| 762 | 774 | }.impl); |
| 763 | 775 | } |
| ... | ... | @@ -840,10 +852,15 @@ test "file operations on directories" { |
| 840 | 852 | handle.close(io); |
| 841 | 853 | } else { |
| 842 | 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 | 855 | try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .mode = .read_write })); |
| 845 | 856 | } |
| 846 | 857 | |
| 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 | 864 | if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) { |
| 848 | 865 | try expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{})); |
| 849 | 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 | 2310 | sa: ?*SECURITY_ATTRIBUTES = null, |
| 2311 | 2311 | share_access: FILE.SHARE = .VALID_FLAGS, |
| 2312 | 2312 | 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, | |
| 2316 | 2314 | /// If false, tries to open path as a reparse point without dereferencing it. |
| 2317 | 2315 | /// Defaults to true. |
| 2318 | 2316 | follow_symlinks: bool = true, |
| 2319 | 2317 | |
| 2320 | 2318 | pub const Filter = enum { |
| 2321 | 2319 | /// 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. | |
| 2324 | 2322 | dir_only, |
| 2325 | 2323 | /// `OpenFile` does not discriminate between opening files and directories. |
| 2326 | 2324 | any, |
| ... | ... | @@ -2328,10 +2326,10 @@ pub const OpenFileOptions = struct { |
| 2328 | 2326 | }; |
| 2329 | 2327 | |
| 2330 | 2328 | pub 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 | 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 | 2333 | return error.IsDir; |
| 2336 | 2334 | } |
| 2337 | 2335 | |
| ... | ... | @@ -2366,7 +2364,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN |
| 2366 | 2364 | options.creation, |
| 2367 | 2365 | .{ |
| 2368 | 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 | 2368 | .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS, |
| 2371 | 2369 | .OPEN_REPARSE_POINT = !options.follow_symlinks, |
| 2372 | 2370 | }, |
| ... | ... | @@ -3040,7 +3038,7 @@ pub fn CreateSymbolicLink( |
| 3040 | 3038 | }, |
| 3041 | 3039 | .dir = dir, |
| 3042 | 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 | 3042 | }) catch |err| switch (err) { |
| 3045 | 3043 | error.IsDir => return error.PathAlreadyExists, |
| 3046 | 3044 | error.NotDir => return error.Unexpected, |