@@ -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
0 commit comments