| 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,6 +100,18 @@ pub const Lock = enum { |
| 100 | pub const OpenFlags = struct { | 100 | pub const OpenFlags = struct { |
| 101 | mode: OpenMode = .read_only, | 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 | /// Open the file with an advisory lock to coordinate with other processes | 115 | /// Open the file with an advisory lock to coordinate with other processes |
| 104 | /// accessing it at the same time. An exclusive lock will prevent other | 116 | /// accessing it at the same time. An exclusive lock will prevent other |
| 105 | /// processes from acquiring a lock. A shared lock will prevent other | 117 | /// 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 unreachable | 238 | /// 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 directory | 1621 | // stat the file and return an error if it's not a directory |
| 1622 | // this is important because otherwise a dangling symlink | 1622 | // this is important because otherwise a dangling symlink |
| 1623 | // could cause an infinite loop | 1623 | // 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/16738 | 1625 | 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 directory | 1744 | // stat the file and return an error if it's not a directory |
| 1751 | // this is important because otherwise a dangling symlink | 1745 | // this is important because otherwise a dangling symlink |
| 1752 | // could cause an infinite loop | 1746 | // 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/16738 | 1748 | .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 | } | ||
| 1763 | 1751 | ||
| 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); |
| 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 | 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 | } |
| 3092 | 3106 | ||
| 3093 | const dirOpenDir = switch (native_os) { | 3107 | const 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"); | ||
| 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 | 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 | } |
| 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 | 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, |
| 2319 | 2317 | ||
| 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 | }; |
| 2329 | 2327 | ||
| 2330 | pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HANDLE { | 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 | 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 | } |
| 2337 | 2335 | ||
| ... | @@ -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, |