Skip to content

Commit bf25816

Browse files
committed
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.
1 parent 17ecc77 commit bf25816

3 files changed

Lines changed: 129 additions & 124 deletions

File tree

lib/std/os/windows.zig

Lines changed: 126 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,21 +1084,135 @@ pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFil
10841084
}
10851085
}
10861086

1087-
pub const MoveFileError = error{ FileNotFound, AccessDenied, Unexpected };
1087+
pub 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

1089-
pub fn MoveFileEx(old_path: []const u8, new_path: []const u8, flags: DWORD) (MoveFileError || Wtf8ToPrefixedFileWError)!void {
1090-
const old_path_w = try sliceToPrefixedFileW(null, old_path);
1091-
const new_path_w = try sliceToPrefixedFileW(null, new_path);
1092-
return MoveFileExW(old_path_w.span().ptr, new_path_w.span().ptr, flags);
1093-
}
1103+
pub fn RenameFile(
1104+
/// May only be `null` if `old_path_w` is a fully-qualified absolute path.
1105+
old_dir_fd: ?HANDLE,
1106+
old_path_w: []const u16,
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

1095-
pub fn MoveFileExW(old_path: [*:0]const u16, new_path: [*:0]const u16, flags: DWORD) MoveFileError!void {
1096-
if (kernel32.MoveFileExW(old_path, new_path, flags) == 0) {
1097-
switch (GetLastError()) {
1098-
.FILE_NOT_FOUND => return error.FileNotFound,
1099-
.ACCESS_DENIED => return error.AccessDenied,
1100-
else => |err| return unexpectedError(err),
1125+
var rc: NTSTATUS = undefined;
1126+
// FileRenameInformationEx has varying levels of support:
1127+
// - FILE_RENAME_INFORMATION_EX requires >= win10_rs1
1128+
// (INVALID_INFO_CLASS is returned if not supported)
1129+
// - Requires the NTFS filesystem
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,
11011173
}
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),
11021216
}
11031217
}
11041218

lib/std/os/windows/kernel32.zig

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,6 @@ pub extern "kernel32" fn GetStdHandle(
9191
nStdHandle: DWORD,
9292
) callconv(.winapi) ?HANDLE;
9393

94-
pub extern "kernel32" fn MoveFileExW(
95-
lpExistingFileName: LPCWSTR,
96-
lpNewFileName: LPCWSTR,
97-
dwFlags: DWORD,
98-
) callconv(.winapi) BOOL;
99-
10094
// TODO: Wrapper around NtSetInformationFile + `FILE_POSITION_INFORMATION`.
10195
// `FILE_STANDARD_INFORMATION` is also used if dwMoveMethod is `FILE_END`
10296
pub extern "kernel32" fn SetFilePointerEx(

lib/std/posix.zig

Lines changed: 3 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,8 +2470,8 @@ pub fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) RenameError!voi
24702470
/// Same as `rename` except the parameters are null-terminated and WTF16LE encoded.
24712471
/// Assumes target is Windows.
24722472
pub 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;
2474-
return windows.MoveFileExW(old_path, new_path, flags);
2473+
const cwd_handle = std.fs.cwd().fd;
2474+
return windows.RenameFile(cwd_handle, mem.span(old_path), cwd_handle, mem.span(new_path), true);
24752475
}
24762476

24772477
/// Change the name or location of a file based on an open directory handle.
@@ -2588,110 +2588,7 @@ pub fn renameatW(
25882588
new_path_w: []const u16,
25892589
ReplaceIfExists: windows.BOOLEAN,
25902590
) RenameError!void {
2591-
const src_fd = windows.OpenFile(old_path_w, .{
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-
}
2591+
return windows.RenameFile(old_dir_fd, old_path_w, new_dir_fd, new_path_w, ReplaceIfExists != 0);
26952592
}
26962593

26972594
/// On Windows, `sub_dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).

0 commit comments

Comments
 (0)