authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-11-23 20:02:16-08:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-11-23 23:38:01-08:00
logbf25816067844efbdb7fe26e8ca7a96341fb919a
tree19e2bd872fde1c705f413352d4f78a1e229388a7
parent17ecc77fc46727f3979abe904ec00b79f036270e

Move Windows rename implementation from std.posix to windows.RenameFile

This also unifies the rename implementations, since previously `posix.renameW` used `MoveFileEx` while `posix.renameatW` used `NtOpenFile`/`NtSetInformationFile`. This, in turn, allows the `MoveFileEx` bindings to be deleted as `posix.renameW` was the only usage.

3 files changed, 129 insertions(+), 124 deletions(-)

lib/std/os/windows.zig+126-12
...@@ -1084,21 +1084,135 @@ pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFil...@@ -1084,21 +1084,135 @@ pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFil
1084 }1084 }
1085}1085}
10861086
1087pub const MoveFileError = error{ FileNotFound, AccessDenied, Unexpected };1087pub const RenameError = error{
1088 IsDir,
1089 NotDir,
1090 FileNotFound,
1091 NoDevice,
1092 AccessDenied,
1093 PipeBusy,
1094 PathAlreadyExists,
1095 Unexpected,
1096 NameTooLong,
1097 NetworkNotFound,
1098 AntivirusInterference,
1099 BadPathName,
1100 RenameAcrossMountPoints,
1101} || UnexpectedError;
10881102
1089pub fn MoveFileEx(old_path: []const u8, new_path: []const u8, flags: DWORD) (MoveFileError || Wtf8ToPrefixedFileWError)!void {1103pub fn RenameFile(
1090 const old_path_w = try sliceToPrefixedFileW(null, old_path);1104 /// May only be `null` if `old_path_w` is a fully-qualified absolute path.
1091 const new_path_w = try sliceToPrefixedFileW(null, new_path);1105 old_dir_fd: ?HANDLE,
1092 return MoveFileExW(old_path_w.span().ptr, new_path_w.span().ptr, flags);1106 old_path_w: []const u16,
1093}1107 /// May only be `null` if `new_path_w` is a fully-qualified absolute path,
1108 /// or if the file is not being moved to a different directory.
1109 new_dir_fd: ?HANDLE,
1110 new_path_w: []const u16,
1111 replace_if_exists: bool,
1112) RenameError!void {
1113 const src_fd = OpenFile(old_path_w, .{
1114 .dir = old_dir_fd,
1115 .access_mask = SYNCHRONIZE | GENERIC_WRITE | DELETE,
1116 .creation = FILE_OPEN,
1117 .filter = .any, // This function is supposed to rename both files and directories.
1118 .follow_symlinks = false,
1119 }) catch |err| switch (err) {
1120 error.WouldBlock => unreachable, // Not possible without `.share_access_nonblocking = true`.
1121 else => |e| return e,
1122 };
1123 defer CloseHandle(src_fd);
10941124
1095pub fn MoveFileExW(old_path: [*:0]const u16, new_path: [*:0]const u16, flags: DWORD) MoveFileError!void {1125 var rc: NTSTATUS = undefined;
1096 if (kernel32.MoveFileExW(old_path, new_path, flags) == 0) {1126 // FileRenameInformationEx has varying levels of support:
1097 switch (GetLastError()) {1127 // - FILE_RENAME_INFORMATION_EX requires >= win10_rs1
1098 .FILE_NOT_FOUND => return error.FileNotFound,1128 // (INVALID_INFO_CLASS is returned if not supported)
1099 .ACCESS_DENIED => return error.AccessDenied,1129 // - Requires the NTFS filesystem
1100 else => |err| return unexpectedError(err),1130 // (on filesystems like FAT32, INVALID_PARAMETER is returned)
1131 // - FILE_RENAME_POSIX_SEMANTICS requires >= win10_rs1
1132 // - FILE_RENAME_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
1133 // (NOT_SUPPORTED is returned if a flag is unsupported)
1134 //
1135 // The strategy here is just to try using FileRenameInformationEx and fall back to
1136 // FileRenameInformation if the return value lets us know that some aspect of it is not supported.
1137 const need_fallback = need_fallback: {
1138 const struct_buf_len = @sizeOf(FILE_RENAME_INFORMATION_EX) + (PATH_MAX_WIDE * 2);
1139 var rename_info_buf: [struct_buf_len]u8 align(@alignOf(FILE_RENAME_INFORMATION_EX)) = undefined;
1140 const struct_len = @sizeOf(FILE_RENAME_INFORMATION_EX) + new_path_w.len * 2;
1141 if (struct_len > struct_buf_len) return error.NameTooLong;
1142
1143 const rename_info: *FILE_RENAME_INFORMATION_EX = @ptrCast(&rename_info_buf);
1144 var io_status_block: IO_STATUS_BLOCK = undefined;
1145
1146 var flags: ULONG = FILE_RENAME_POSIX_SEMANTICS | FILE_RENAME_IGNORE_READONLY_ATTRIBUTE;
1147 if (replace_if_exists) flags |= FILE_RENAME_REPLACE_IF_EXISTS;
1148 rename_info.* = .{
1149 .Flags = flags,
1150 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir_fd,
1151 .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
1152 .FileName = undefined,
1153 };
1154 @memcpy((&rename_info.FileName).ptr, new_path_w);
1155 rc = ntdll.NtSetInformationFile(
1156 src_fd,
1157 &io_status_block,
1158 rename_info,
1159 @intCast(struct_len), // already checked for error.NameTooLong
1160 .FileRenameInformationEx,
1161 );
1162 switch (rc) {
1163 .SUCCESS => return,
1164 // The filesystem does not support FileDispositionInformationEx
1165 .INVALID_PARAMETER,
1166 // The operating system does not support FileDispositionInformationEx
1167 .INVALID_INFO_CLASS,
1168 // The operating system does not support one of the flags
1169 .NOT_SUPPORTED,
1170 => break :need_fallback true,
1171 // For all other statuses, fall down to the switch below to handle them.
1172 else => break :need_fallback false,
1101 }1173 }
1174 };
1175
1176 if (need_fallback) {
1177 const struct_buf_len = @sizeOf(FILE_RENAME_INFORMATION) + (PATH_MAX_WIDE * 2);
1178 var rename_info_buf: [struct_buf_len]u8 align(@alignOf(FILE_RENAME_INFORMATION)) = undefined;
1179 const struct_len = @sizeOf(FILE_RENAME_INFORMATION) + new_path_w.len * 2;
1180 if (struct_len > struct_buf_len) return error.NameTooLong;
1181
1182 const rename_info: *FILE_RENAME_INFORMATION = @ptrCast(&rename_info_buf);
1183 var io_status_block: IO_STATUS_BLOCK = undefined;
1184
1185 rename_info.* = .{
1186 .Flags = @intFromBool(replace_if_exists),
1187 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir_fd,
1188 .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
1189 .FileName = undefined,
1190 };
1191 @memcpy((&rename_info.FileName).ptr, new_path_w);
1192
1193 rc = ntdll.NtSetInformationFile(
1194 src_fd,
1195 &io_status_block,
1196 rename_info,
1197 @intCast(struct_len), // already checked for error.NameTooLong
1198 .FileRenameInformation,
1199 );
1200 }
1201
1202 switch (rc) {
1203 .SUCCESS => {},
1204 .INVALID_HANDLE => unreachable,
1205 .INVALID_PARAMETER => unreachable,
1206 .OBJECT_PATH_SYNTAX_BAD => unreachable,
1207 .ACCESS_DENIED => return error.AccessDenied,
1208 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
1209 .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
1210 .NOT_SAME_DEVICE => return error.RenameAcrossMountPoints,
1211 .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
1212 .DIRECTORY_NOT_EMPTY => return error.PathAlreadyExists,
1213 .FILE_IS_A_DIRECTORY => return error.IsDir,
1214 .NOT_A_DIRECTORY => return error.NotDir,
1215 else => return unexpectedStatus(rc),
1102 }1216 }
1103}1217}
11041218
lib/std/os/windows/kernel32.zig-6
...@@ -91,12 +91,6 @@ pub extern "kernel32" fn GetStdHandle(...@@ -91,12 +91,6 @@ pub extern "kernel32" fn GetStdHandle(
91 nStdHandle: DWORD,91 nStdHandle: DWORD,
92) callconv(.winapi) ?HANDLE;92) callconv(.winapi) ?HANDLE;
9393
94pub extern "kernel32" fn MoveFileExW(
95 lpExistingFileName: LPCWSTR,
96 lpNewFileName: LPCWSTR,
97 dwFlags: DWORD,
98) callconv(.winapi) BOOL;
99
100// TODO: Wrapper around NtSetInformationFile + `FILE_POSITION_INFORMATION`.94// TODO: Wrapper around NtSetInformationFile + `FILE_POSITION_INFORMATION`.
101// `FILE_STANDARD_INFORMATION` is also used if dwMoveMethod is `FILE_END`95// `FILE_STANDARD_INFORMATION` is also used if dwMoveMethod is `FILE_END`
102pub extern "kernel32" fn SetFilePointerEx(96pub extern "kernel32" fn SetFilePointerEx(
lib/std/posix.zig+3-106
...@@ -2470,8 +2470,8 @@ pub fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) RenameError!voi...@@ -2470,8 +2470,8 @@ pub fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) RenameError!voi
2470/// Same as `rename` except the parameters are null-terminated and WTF16LE encoded.2470/// Same as `rename` except the parameters are null-terminated and WTF16LE encoded.
2471/// Assumes target is Windows.2471/// Assumes target is Windows.
2472pub fn renameW(old_path: [*:0]const u16, new_path: [*:0]const u16) RenameError!void {2472pub fn renameW(old_path: [*:0]const u16, new_path: [*:0]const u16) RenameError!void {
2473 const flags = windows.MOVEFILE_REPLACE_EXISTING | windows.MOVEFILE_WRITE_THROUGH;2473 const cwd_handle = std.fs.cwd().fd;
2474 return windows.MoveFileExW(old_path, new_path, flags);2474 return windows.RenameFile(cwd_handle, mem.span(old_path), cwd_handle, mem.span(new_path), true);
2475}2475}
24762476
2477/// Change the name or location of a file based on an open directory handle.2477/// Change the name or location of a file based on an open directory handle.
...@@ -2588,110 +2588,7 @@ pub fn renameatW(...@@ -2588,110 +2588,7 @@ pub fn renameatW(
2588 new_path_w: []const u16,2588 new_path_w: []const u16,
2589 ReplaceIfExists: windows.BOOLEAN,2589 ReplaceIfExists: windows.BOOLEAN,
2590) RenameError!void {2590) RenameError!void {
2591 const src_fd = windows.OpenFile(old_path_w, .{2591 return windows.RenameFile(old_dir_fd, old_path_w, new_dir_fd, new_path_w, ReplaceIfExists != 0);
2592 .dir = old_dir_fd,
2593 .access_mask = windows.SYNCHRONIZE | windows.GENERIC_WRITE | windows.DELETE,
2594 .creation = windows.FILE_OPEN,
2595 .filter = .any, // This function is supposed to rename both files and directories.
2596 .follow_symlinks = false,
2597 }) catch |err| switch (err) {
2598 error.WouldBlock => unreachable, // Not possible without `.share_access_nonblocking = true`.
2599 else => |e| return e,
2600 };
2601 defer windows.CloseHandle(src_fd);
2602
2603 var rc: windows.NTSTATUS = undefined;
2604 // FileRenameInformationEx has varying levels of support:
2605 // - FILE_RENAME_INFORMATION_EX requires >= win10_rs1
2606 // (INVALID_INFO_CLASS is returned if not supported)
2607 // - Requires the NTFS filesystem
2608 // (on filesystems like FAT32, INVALID_PARAMETER is returned)
2609 // - FILE_RENAME_POSIX_SEMANTICS requires >= win10_rs1
2610 // - FILE_RENAME_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
2611 // (NOT_SUPPORTED is returned if a flag is unsupported)
2612 //
2613 // The strategy here is just to try using FileRenameInformationEx and fall back to
2614 // FileRenameInformation if the return value lets us know that some aspect of it is not supported.
2615 const need_fallback = need_fallback: {
2616 const struct_buf_len = @sizeOf(windows.FILE_RENAME_INFORMATION_EX) + (max_path_bytes - 1);
2617 var rename_info_buf: [struct_buf_len]u8 align(@alignOf(windows.FILE_RENAME_INFORMATION_EX)) = undefined;
2618 const struct_len = @sizeOf(windows.FILE_RENAME_INFORMATION_EX) - 1 + new_path_w.len * 2;
2619 if (struct_len > struct_buf_len) return error.NameTooLong;
2620
2621 const rename_info: *windows.FILE_RENAME_INFORMATION_EX = @ptrCast(&rename_info_buf);
2622 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
2623
2624 var flags: windows.ULONG = windows.FILE_RENAME_POSIX_SEMANTICS | windows.FILE_RENAME_IGNORE_READONLY_ATTRIBUTE;
2625 if (ReplaceIfExists == windows.TRUE) flags |= windows.FILE_RENAME_REPLACE_IF_EXISTS;
2626 rename_info.* = .{
2627 .Flags = flags,
2628 .RootDirectory = if (fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir_fd,
2629 .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
2630 .FileName = undefined,
2631 };
2632 @memcpy((&rename_info.FileName).ptr, new_path_w);
2633 rc = windows.ntdll.NtSetInformationFile(
2634 src_fd,
2635 &io_status_block,
2636 rename_info,
2637 @intCast(struct_len), // already checked for error.NameTooLong
2638 .FileRenameInformationEx,
2639 );
2640 switch (rc) {
2641 .SUCCESS => return,
2642 // The filesystem does not support FileDispositionInformationEx
2643 .INVALID_PARAMETER,
2644 // The operating system does not support FileDispositionInformationEx
2645 .INVALID_INFO_CLASS,
2646 // The operating system does not support one of the flags
2647 .NOT_SUPPORTED,
2648 => break :need_fallback true,
2649 // For all other statuses, fall down to the switch below to handle them.
2650 else => break :need_fallback false,
2651 }
2652 };
2653
2654 if (need_fallback) {
2655 const struct_buf_len = @sizeOf(windows.FILE_RENAME_INFORMATION) + (max_path_bytes - 1);
2656 var rename_info_buf: [struct_buf_len]u8 align(@alignOf(windows.FILE_RENAME_INFORMATION)) = undefined;
2657 const struct_len = @sizeOf(windows.FILE_RENAME_INFORMATION) - 1 + new_path_w.len * 2;
2658 if (struct_len > struct_buf_len) return error.NameTooLong;
2659
2660 const rename_info: *windows.FILE_RENAME_INFORMATION = @ptrCast(&rename_info_buf);
2661 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
2662
2663 rename_info.* = .{
2664 .Flags = ReplaceIfExists,
2665 .RootDirectory = if (fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir_fd,
2666 .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
2667 .FileName = undefined,
2668 };
2669 @memcpy((&rename_info.FileName).ptr, new_path_w);
2670
2671 rc = windows.ntdll.NtSetInformationFile(
2672 src_fd,
2673 &io_status_block,
2674 rename_info,
2675 @intCast(struct_len), // already checked for error.NameTooLong
2676 .FileRenameInformation,
2677 );
2678 }
2679
2680 switch (rc) {
2681 .SUCCESS => {},
2682 .INVALID_HANDLE => unreachable,
2683 .INVALID_PARAMETER => unreachable,
2684 .OBJECT_PATH_SYNTAX_BAD => unreachable,
2685 .ACCESS_DENIED => return error.AccessDenied,
2686 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
2687 .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
2688 .NOT_SAME_DEVICE => return error.RenameAcrossMountPoints,
2689 .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
2690 .DIRECTORY_NOT_EMPTY => return error.PathAlreadyExists,
2691 .FILE_IS_A_DIRECTORY => return error.IsDir,
2692 .NOT_A_DIRECTORY => return error.NotDir,
2693 else => return windows.unexpectedStatus(rc),
2694 }
2695}2592}
26962593
2697/// On Windows, `sub_dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).2594/// On Windows, `sub_dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).