diff --git a/lib/std/fs.zig b/lib/std/fs.zig index d14feedb5..57c1534e9 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1117,7 +1117,7 @@ pub const Dir = struct { pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); - return self.deleteFileW(sub_path_w.span().ptr); + return self.deleteFileW(sub_path_w.span()); } else if (builtin.os.tag == .wasi) { os.unlinkatWasi(self.fd, sub_path, 0) catch |err| switch (err) { error.DirNotEmpty => unreachable, // not passing AT_REMOVEDIR @@ -1151,7 +1151,7 @@ pub const Dir = struct { } /// Same as `deleteFile` except the parameter is WTF-16 encoded. - pub fn deleteFileW(self: Dir, sub_path_w: [*:0]const u16) DeleteFileError!void { + pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void { os.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) { error.DirNotEmpty => unreachable, // not passing AT_REMOVEDIR else => |e| return e, @@ -1180,7 +1180,7 @@ pub const Dir = struct { pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); - return self.deleteDirW(sub_path_w.span().ptr); + return self.deleteDirW(sub_path_w.span()); } else if (builtin.os.tag == .wasi) { os.unlinkat(self.fd, sub_path, os.AT_REMOVEDIR) catch |err| switch (err) { error.IsDir => unreachable, // not possible since we pass AT_REMOVEDIR @@ -1202,7 +1202,7 @@ pub const Dir = struct { /// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed. /// This function is Windows-only. - pub fn deleteDirW(self: Dir, sub_path_w: [*:0]const u16) DeleteDirError!void { + pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void { os.unlinkatW(self.fd, sub_path_w, os.AT_REMOVEDIR) catch |err| switch (err) { error.IsDir => unreachable, // not possible since we pass AT_REMOVEDIR else => |e| return e, @@ -1939,7 +1939,20 @@ pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker { return walker; } -pub const OpenSelfExeError = os.OpenError || os.windows.CreateFileError || SelfExePathError || os.FlockError; +pub const OpenSelfExeError = error{ + SharingViolation, + PathAlreadyExists, + FileNotFound, + AccessDenied, + PipeBusy, + NameTooLong, + /// On Windows, file paths must be valid Unicode. + InvalidUtf8, + /// On Windows, file paths cannot contain these characters: + /// '/', '*', '?', '"', '<', '>', '|' + BadPathName, + Unexpected, +} || os.OpenError || SelfExePathError || os.FlockError; pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { if (builtin.os.tag == .linux) { diff --git a/lib/std/fs/file.zig b/lib/std/fs/file.zig index cffc8cf87..3b79e4e01 100644 --- a/lib/std/fs/file.zig +++ b/lib/std/fs/file.zig @@ -47,7 +47,20 @@ pub const File = struct { else => 0o666, }; - pub const OpenError = windows.CreateFileError || os.OpenError || os.FlockError; + pub const OpenError = error{ + SharingViolation, + PathAlreadyExists, + FileNotFound, + AccessDenied, + PipeBusy, + NameTooLong, + /// On Windows, file paths must be valid Unicode. + InvalidUtf8, + /// On Windows, file paths cannot contain these characters: + /// '/', '*', '?', '"', '<', '>', '|' + BadPathName, + Unexpected, + } || os.OpenError || os.FlockError; pub const Lock = enum { None, Shared, Exclusive }; diff --git a/lib/std/fs/watch.zig b/lib/std/fs/watch.zig index a753bdd24..635818b70 100644 --- a/lib/std/fs/watch.zig +++ b/lib/std/fs/watch.zig @@ -379,7 +379,7 @@ pub fn Watch(comptime V: type) type { .access_mask = windows.FILE_LIST_DIRECTORY, .creation = windows.FILE_OPEN, .io_mode = .blocking, - .expect_dir = true, + .open_dir = true, }); var dir_handle_consumed = false; defer if (!dir_handle_consumed) windows.CloseHandle(dir_handle); diff --git a/lib/std/os.zig b/lib/std/os.zig index 664b6c35b..0a21c9880 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1683,7 +1683,7 @@ pub fn unlink(file_path: []const u8) UnlinkError!void { @compileError("unlink is not supported in WASI; use unlinkat instead"); } else if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); - return windows.DeleteFileW(file_path_w.span().ptr); + return unlinkW(file_path_w.span()); } else { const file_path_c = try toPosixPath(file_path); return unlinkZ(&file_path_c); @@ -1696,7 +1696,7 @@ pub const unlinkC = @compileError("deprecated: renamed to unlinkZ"); pub fn unlinkZ(file_path: [*:0]const u8) UnlinkError!void { if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path); - return windows.DeleteFileW(file_path_w.span().ptr); + return unlinkW(file_path_w.span()); } switch (errno(system.unlink(file_path))) { 0 => return, @@ -1717,6 +1717,11 @@ pub fn unlinkZ(file_path: [*:0]const u8) UnlinkError!void { } } +/// Windows-only. Same as `unlink` except the parameter is null-terminated, WTF16 encoded. +pub fn unlinkW(file_path_w: []const u16) UnlinkError!void { + return windows.DeleteFile(file_path_w, .{ .dir = std.fs.cwd().fd }); +} + pub const UnlinkatError = UnlinkError || error{ /// When passing `AT_REMOVEDIR`, this error occurs when the named directory is not empty. DirNotEmpty, @@ -1727,7 +1732,7 @@ pub const UnlinkatError = UnlinkError || error{ pub fn unlinkat(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatError!void { if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); - return unlinkatW(dirfd, file_path_w.span().ptr, flags); + return unlinkatW(dirfd, file_path_w.span(), flags); } else if (builtin.os.tag == .wasi) { return unlinkatWasi(dirfd, file_path, flags); } else { @@ -1774,7 +1779,7 @@ pub fn unlinkatWasi(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatErro pub fn unlinkatZ(dirfd: fd_t, file_path_c: [*:0]const u8, flags: u32) UnlinkatError!void { if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path_c); - return unlinkatW(dirfd, file_path_w.span().ptr, flags); + return unlinkatW(dirfd, file_path_w.span(), flags); } switch (errno(system.unlinkat(dirfd, file_path_c, flags))) { 0 => return, @@ -1800,67 +1805,9 @@ pub fn unlinkatZ(dirfd: fd_t, file_path_c: [*:0]const u8, flags: u32) UnlinkatEr } /// Same as `unlinkat` but `sub_path_w` is UTF16LE, NT prefixed. Windows only. -pub fn unlinkatW(dirfd: fd_t, sub_path_w: [*:0]const u16, flags: u32) UnlinkatError!void { - const w = windows; - - const want_rmdir_behavior = (flags & AT_REMOVEDIR) != 0; - const create_options_flags = if (want_rmdir_behavior) - @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT) - else - @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_NON_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT); // would we ever want to delete the target instead? - - const path_len_bytes = @intCast(u16, mem.lenZ(sub_path_w) * 2); - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - // The Windows API makes this mutable, but it will not mutate here. - .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)), - }; - - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - // Windows does not recognize this, but it does work with empty string. - nt_name.Length = 0; - } - if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { - // Can't remove the parent directory with an open handle. - return error.FileBusy; - } - - var attr = w.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else dirfd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var io: w.IO_STATUS_BLOCK = undefined; - var tmp_handle: w.HANDLE = undefined; - var rc = w.ntdll.NtCreateFile( - &tmp_handle, - w.SYNCHRONIZE | w.DELETE, - &attr, - &io, - null, - 0, - w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, - w.FILE_OPEN, - create_options_flags, - null, - 0, - ); - if (rc == .SUCCESS) { - rc = w.ntdll.NtClose(tmp_handle); - } - switch (rc) { - .SUCCESS => return, - .OBJECT_NAME_INVALID => unreachable, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .INVALID_PARAMETER => unreachable, - .FILE_IS_A_DIRECTORY => return error.IsDir, - .NOT_A_DIRECTORY => return error.NotDir, - else => return w.unexpectedStatus(rc), - } +pub fn unlinkatW(dirfd: fd_t, sub_path_w: []const u16, flags: u32) UnlinkatError!void { + const remove_dir = (flags & AT_REMOVEDIR) != 0; + return windows.DeleteFile(sub_path_w, .{ .dir = dirfd, .remove_dir = remove_dir }); } const RenameError = error{ @@ -2256,7 +2203,7 @@ pub fn rmdir(dir_path: []const u8) DeleteDirError!void { @compileError("rmdir is not supported in WASI; use unlinkat instead"); } else if (builtin.os.tag == .windows) { const dir_path_w = try windows.sliceToPrefixedFileW(dir_path); - return windows.RemoveDirectoryW(dir_path_w.span().ptr); + return rmdirW(dir_path_w.span()); } else { const dir_path_c = try toPosixPath(dir_path); return rmdirZ(&dir_path_c); @@ -2269,7 +2216,7 @@ pub const rmdirC = @compileError("deprecated: renamed to rmdirZ"); pub fn rmdirZ(dir_path: [*:0]const u8) DeleteDirError!void { if (builtin.os.tag == .windows) { const dir_path_w = try windows.cStrToPrefixedFileW(dir_path); - return windows.RemoveDirectoryW(dir_path_w.span().ptr); + return rmdirW(dir_path_w.span()); } switch (errno(system.rmdir(dir_path))) { 0 => return, @@ -2290,6 +2237,14 @@ pub fn rmdirZ(dir_path: [*:0]const u8) DeleteDirError!void { } } +/// Windows-only. Same as `rmdir` except the parameter is null-terminated, WTF16 encoded. +pub fn rmdirW(dir_path_w: []const u16) DeleteDirError!void { + return windows.DeleteFile(dir_path_w, .{ .dir = std.fs.cwd().fd, .remove_dir = true }) catch |err| switch (err) { + error.IsDir => unreachable, + else => |e| return e, + }; +} + pub const ChangeCurDirError = error{ AccessDenied, FileSystem, diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 64998bfbb..9c00f2d4c 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -25,30 +25,6 @@ pub usingnamespace @import("windows/bits.zig"); pub const self_process_handle = @intToPtr(HANDLE, maxInt(usize)); -pub const CreateFileError = error{ - SharingViolation, - PathAlreadyExists, - - /// When any of the path components can not be found or the file component can not - /// be found. Some operating systems distinguish between path components not found and - /// file components not found, but they are collapsed into FileNotFound to gain - /// consistency across operating systems. - FileNotFound, - - AccessDenied, - PipeBusy, - NameTooLong, - - /// On Windows, file paths must be valid Unicode. - InvalidUtf8, - - /// On Windows, file paths cannot contain these characters: - /// '/', '*', '?', '"', '<', '>', '|' - BadPathName, - - Unexpected, -}; - pub const OpenError = error{ IsDir, FileNotFound, @@ -729,24 +705,69 @@ pub const DeleteFileError = error{ NameTooLong, FileBusy, Unexpected, + NotDir, + IsDir, }; -pub fn DeleteFile(filename: []const u8) DeleteFileError!void { - const filename_w = try sliceToPrefixedFileW(filename); - return DeleteFileW(filename_w.span().ptr); -} +pub const DeleteFileOptions = struct { + dir: ?HANDLE, + remove_dir: bool = false, +}; -pub fn DeleteFileW(filename: [*:0]const u16) DeleteFileError!void { - if (kernel32.DeleteFileW(filename) == 0) { - switch (kernel32.GetLastError()) { - .FILE_NOT_FOUND => return error.FileNotFound, - .PATH_NOT_FOUND => return error.FileNotFound, - .ACCESS_DENIED => return error.AccessDenied, - .FILENAME_EXCED_RANGE => return error.NameTooLong, - .INVALID_PARAMETER => return error.NameTooLong, - .SHARING_VIOLATION => return error.FileBusy, - else => |err| return unexpectedError(err), - } +pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFileError!void { + const create_options_flags: ULONG = if (options.remove_dir) + FILE_DELETE_ON_CLOSE | FILE_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT + else + FILE_DELETE_ON_CLOSE | FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT; // would we ever want to delete the target instead? + + const path_len_bytes = @intCast(u16, sub_path_w.len * 2); + var nt_name = UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + // The Windows API makes this mutable, but it will not mutate here. + .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w.ptr)), + }; + + if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { + // Windows does not recognize this, but it does work with empty string. + nt_name.Length = 0; + } + if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { + // Can't remove the parent directory with an open handle. + return error.FileBusy; + } + + var attr = OBJECT_ATTRIBUTES{ + .Length = @sizeOf(OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else options.dir, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + var io: IO_STATUS_BLOCK = undefined; + var tmp_handle: HANDLE = undefined; + var rc = ntdll.NtCreateFile( + &tmp_handle, + SYNCHRONIZE | DELETE, + &attr, + &io, + null, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + create_options_flags, + null, + 0, + ); + switch (rc) { + .SUCCESS => return CloseHandle(tmp_handle), + .OBJECT_NAME_INVALID => unreachable, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .INVALID_PARAMETER => unreachable, + .FILE_IS_A_DIRECTORY => return error.IsDir, + .NOT_A_DIRECTORY => return error.NotDir, + else => return unexpectedStatus(rc), } } @@ -766,29 +787,6 @@ pub fn MoveFileExW(old_path: [*:0]const u16, new_path: [*:0]const u16, flags: DW } } -pub const RemoveDirectoryError = error{ - FileNotFound, - DirNotEmpty, - Unexpected, - NotDir, -}; - -pub fn RemoveDirectory(dir_path: []const u8) RemoveDirectoryError!void { - const dir_path_w = try sliceToPrefixedFileW(dir_path); - return RemoveDirectoryW(dir_path_w.span().ptr); -} - -pub fn RemoveDirectoryW(dir_path_w: [*:0]const u16) RemoveDirectoryError!void { - if (kernel32.RemoveDirectoryW(dir_path_w) == 0) { - switch (kernel32.GetLastError()) { - .PATH_NOT_FOUND => return error.FileNotFound, - .DIR_NOT_EMPTY => return error.DirNotEmpty, - .DIRECTORY => return error.NotDir, - else => |err| return unexpectedError(err), - } - } -} - pub const GetStdHandleError = error{ NoStandardHandleAttached, Unexpected,