diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index ca6929b8ca3afa2179b95542aff1316859935560..3658252499ec3ecabe3f9a2e2b4a0bd863995488 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -100,6 +100,18 @@ pub const Lock = enum { pub const OpenFlags = struct { mode: OpenMode = .read_only, + /// Determines the behavior when opening a path that refers to a directory. + /// If set to true, directories may be opened, but `error.IsDir` is still + /// possible in certain scenarios, e.g. attempting to open a directory with + /// write permissions. + /// If set to false, `error.IsDir` will always be returned when opening a directory. + /// + /// When set to false: + /// * On Windows, the behavior is implemented without any extra syscalls. + /// * On other operating systems, the behavior is implemented with an additional + /// `fstat` syscall. + allow_directory: bool = true, + /// Open the file with an advisory lock to coordinate with other processes /// accessing it at the same time. An exclusive lock will prevent other /// processes from acquiring a lock. A shared lock will prevent other @@ -226,7 +238,9 @@ pub const OpenError = error{ /// The file is too large to be opened. This error is unreachable /// for 64-bit targets, as well as when opening directories. FileTooBig, - /// The path refers to directory but the `DIRECTORY` flag was not provided. + /// Either: + /// * The path refers to a directory and write permissions were requested. + /// * The path refers to a directory and `allow_directory` was set to false. IsDir, /// A new path cannot be created because the device has no room for the new file. /// This error is only reachable when the `CREAT` flag is provided. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 36118ca81879917b5b4c0d85ce6c609ef55b6672..e38c56ac1fc4ed82c7775b3a75895f541689fce1 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1621,14 +1621,8 @@ fn dirMakePath( // stat the file and return an error if it's not a directory // this is important because otherwise a dangling symlink // could cause an infinite loop - check_dir: { - // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = dirStatFile(t, dir, component.path, .{}) catch |stat_err| switch (stat_err) { - error.IsDir => break :check_dir, - else => |e| return e, - }; - if (fstat.kind != .directory) return error.NotDir; - } + const fstat = dirStatFile(t, dir, component.path, .{}); + if (fstat.kind != .directory) return error.NotDir; }, error.FileNotFound => |e| { component = it.previous() orelse return e; @@ -1750,16 +1744,10 @@ fn dirMakeOpenPathWindows( // stat the file and return an error if it's not a directory // this is important because otherwise a dangling symlink // could cause an infinite loop - check_dir: { - // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = dirStatFileWindows(t, dir, component.path, .{ - .follow_symlinks = options.follow_symlinks, - }) catch |stat_err| switch (stat_err) { - error.IsDir => break :check_dir, - else => |e| return e, - }; - if (fstat.kind != .directory) return error.NotDir; - } + const fstat = dirStatFileWindows(t, dir, component.path, .{ + .follow_symlinks = options.follow_symlinks, + }); + if (fstat.kind != .directory) return error.NotDir; component = it.next().?; continue; @@ -2791,6 +2779,18 @@ fn dirOpenFilePosix( }; errdefer posix.close(fd); + if (!flags.allow_directory) { + const is_dir = is_dir: { + const stat = fileStat(t, .{ .handle = fd }) catch |err| switch (err) { + // The directory-ness is either unknown or unknowable + error.Streaming => break :is_dir false, + else => |e| return e, + }; + break :is_dir stat.kind == .directory; + }; + if (is_dir) return error.IsDir; + } + if (have_flock and !have_flock_open_flags and flags.lock != .none) { const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; const lock_flags = switch (flags.lock) { @@ -2936,7 +2936,7 @@ pub fn dirOpenFileWtf16( .OPEN, .{ .IO = if (flags.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS, - .NON_DIRECTORY_FILE = true, + .NON_DIRECTORY_FILE = !flags.allow_directory, .OPEN_REPARSE_POINT = !flags.follow_symlinks, }, null, @@ -3052,9 +3052,8 @@ fn dirOpenFileWasi( while (true) { switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) { .SUCCESS => { - errdefer posix.close(fd); current_thread.endSyscall(); - return .{ .handle = fd }; + break; }, .INTR => { try current_thread.checkCancel(); @@ -3088,6 +3087,21 @@ fn dirOpenFileWasi( }, } } + errdefer posix.close(fd); + + if (!flags.allow_directory) { + const is_dir = is_dir: { + const stat = fileStat(t, .{ .handle = fd }) catch |err| switch (err) { + // The directory-ness is either unknown or unknowable + error.Streaming => break :is_dir false, + else => |e| return e, + }; + break :is_dir stat.kind == .directory; + }; + if (is_dir) return error.IsDir; + } + + return .{ .handle = fd }; } const dirOpenDir = switch (native_os) { @@ -4577,7 +4591,7 @@ fn dirSymLinkWindows( .access_mask = w.SYNCHRONIZE | w.GENERIC_READ | w.GENERIC_WRITE, .dir = dir, .creation = w.FILE_CREATE, - .filter = if (flags.is_directory) .dir_only else .file_only, + .filter = if (flags.is_directory) .dir_only else .non_directory_only, }) catch |err| switch (err) { error.IsDir => return error.PathAlreadyExists, error.NotDir => return error.Unexpected, diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 53612e4f960a8442ebdf3f18f7af015f7fd9aef4..0a81efbca5ab8f465b598cbf09141557341df945 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -750,14 +750,26 @@ test "Dir.statFile" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const io = ctx.io; - const test_file_name = try ctx.transformPath("test_file"); + { + const test_file_name = try ctx.transformPath("test_file"); - try expectError(error.FileNotFound, ctx.dir.statFile(io, test_file_name, .{})); + try expectError(error.FileNotFound, ctx.dir.statFile(io, test_file_name, .{})); - try ctx.dir.writeFile(io, .{ .sub_path = test_file_name, .data = "" }); + try ctx.dir.writeFile(io, .{ .sub_path = test_file_name, .data = "" }); - const stat = try ctx.dir.statFile(io, test_file_name, .{}); - try expectEqual(File.Kind.file, stat.kind); + const stat = try ctx.dir.statFile(io, test_file_name, .{}); + try expectEqual(.file, stat.kind); + } + { + const test_dir_name = try ctx.transformPath("test_dir"); + + try expectError(error.FileNotFound, ctx.dir.statFile(io, test_dir_name, .{})); + + try ctx.dir.makeDir(io, test_dir_name); + + const stat = try ctx.dir.statFile(io, test_dir_name, .{}); + try expectEqual(.directory, stat.kind); + } } }.impl); } @@ -840,10 +852,15 @@ test "file operations on directories" { handle.close(io); } else { // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms. - // TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732 try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .mode = .read_write })); } + { + const handle = try ctx.dir.openFile(io, test_dir_name, .{ .allow_directory = true, .mode = .read_only }); + handle.close(io); + } + try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .allow_directory = false, .mode = .read_only })); + if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) { try expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{})); try expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name)); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index ba5282256fd97eebf6257a8ee68b8be006139a7a..7a2b2aaf50a9ac10740a9e86523322f399f0ccb5 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -2310,17 +2310,15 @@ pub const OpenFileOptions = struct { sa: ?*SECURITY_ATTRIBUTES = null, share_access: FILE.SHARE = .VALID_FLAGS, creation: FILE.CREATE_DISPOSITION, - /// If true, tries to open path as a directory. - /// Defaults to false. - filter: Filter = .file_only, + filter: Filter = .non_directory_only, /// If false, tries to open path as a reparse point without dereferencing it. /// Defaults to true. follow_symlinks: bool = true, pub const Filter = enum { /// Causes `OpenFile` to return `error.IsDir` if the opened handle would be a directory. - file_only, - /// Causes `OpenFile` to return `error.NotDir` if the opened handle would be a file. + non_directory_only, + /// Causes `OpenFile` to return `error.NotDir` if the opened handle is not a directory. dir_only, /// `OpenFile` does not discriminate between opening files and directories. any, @@ -2328,10 +2326,10 @@ pub const OpenFileOptions = struct { }; pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HANDLE { - if (mem.eql(u16, sub_path_w, &[_]u16{'.'}) and options.filter == .file_only) { + if (mem.eql(u16, sub_path_w, &[_]u16{'.'}) and options.filter == .non_directory_only) { return error.IsDir; } - if (mem.eql(u16, sub_path_w, &[_]u16{ '.', '.' }) and options.filter == .file_only) { + if (mem.eql(u16, sub_path_w, &[_]u16{ '.', '.' }) and options.filter == .non_directory_only) { return error.IsDir; } @@ -2366,7 +2364,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN options.creation, .{ .DIRECTORY_FILE = options.filter == .dir_only, - .NON_DIRECTORY_FILE = options.filter == .file_only, + .NON_DIRECTORY_FILE = options.filter == .non_directory_only, .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS, .OPEN_REPARSE_POINT = !options.follow_symlinks, }, @@ -3040,7 +3038,7 @@ pub fn CreateSymbolicLink( }, .dir = dir, .creation = .CREATE, - .filter = if (is_directory) .dir_only else .file_only, + .filter = if (is_directory) .dir_only else .non_directory_only, }) catch |err| switch (err) { error.IsDir => return error.PathAlreadyExists, error.NotDir => return error.Unexpected,