|
1 | 1 | use super::api::{self, WinError};
|
2 | 2 | use super::{IoResult, to_u16s};
|
| 3 | +use crate::alloc::{alloc, handle_alloc_error}; |
3 | 4 | use crate::borrow::Cow;
|
4 | 5 | use crate::ffi::{OsStr, OsString, c_void};
|
5 | 6 | use crate::io::{self, BorrowedCursor, Error, IoSlice, IoSliceMut, SeekFrom};
|
@@ -1095,7 +1096,149 @@ pub fn unlink(p: &Path) -> io::Result<()> {
|
1095 | 1096 | pub fn rename(old: &Path, new: &Path) -> io::Result<()> {
|
1096 | 1097 | let old = maybe_verbatim(old)?;
|
1097 | 1098 | let new = maybe_verbatim(new)?;
|
1098 |
| - cvt(unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) })?; |
| 1099 | + |
| 1100 | + let new_len_without_nul_in_bytes = (new.len() - 1).try_into().unwrap(); |
| 1101 | + |
| 1102 | + let struct_size = mem::size_of::<c::FILE_RENAME_INFO>() - mem::size_of::<u16>() |
| 1103 | + + new.len() * mem::size_of::<u16>(); |
| 1104 | + |
| 1105 | + let struct_size: u32 = struct_size.try_into().unwrap(); |
| 1106 | + |
| 1107 | + let create_file = |extra_access, extra_flags| { |
| 1108 | + let handle = unsafe { |
| 1109 | + HandleOrInvalid::from_raw_handle(c::CreateFileW( |
| 1110 | + old.as_ptr(), |
| 1111 | + c::SYNCHRONIZE | c::DELETE | extra_access, |
| 1112 | + c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE, |
| 1113 | + ptr::null(), |
| 1114 | + c::OPEN_EXISTING, |
| 1115 | + c::FILE_ATTRIBUTE_NORMAL | c::FILE_FLAG_BACKUP_SEMANTICS | extra_flags, |
| 1116 | + ptr::null_mut(), |
| 1117 | + )) |
| 1118 | + }; |
| 1119 | + |
| 1120 | + OwnedHandle::try_from(handle).map_err(|_| io::Error::last_os_error()) |
| 1121 | + }; |
| 1122 | + |
| 1123 | + // The following code replicates `MoveFileEx`'s behavior as reverse-engineered from its disassembly. |
| 1124 | + // If `old` refers to a mount point, we move it instead of the target. |
| 1125 | + let handle = match create_file(c::FILE_READ_ATTRIBUTES, c::FILE_FLAG_OPEN_REPARSE_POINT) { |
| 1126 | + Ok(handle) => { |
| 1127 | + let mut file_attribute_tag_info: MaybeUninit<c::FILE_ATTRIBUTE_TAG_INFO> = |
| 1128 | + MaybeUninit::uninit(); |
| 1129 | + |
| 1130 | + let result = unsafe { |
| 1131 | + cvt(c::GetFileInformationByHandleEx( |
| 1132 | + handle.as_raw_handle(), |
| 1133 | + c::FileAttributeTagInfo, |
| 1134 | + file_attribute_tag_info.as_mut_ptr().cast(), |
| 1135 | + mem::size_of::<c::FILE_ATTRIBUTE_TAG_INFO>().try_into().unwrap(), |
| 1136 | + )) |
| 1137 | + }; |
| 1138 | + |
| 1139 | + if let Err(err) = result { |
| 1140 | + if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) |
| 1141 | + || err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _) |
| 1142 | + { |
| 1143 | + // `GetFileInformationByHandleEx` documents that not all underlying drivers support all file information classes. |
| 1144 | + // Since we know we passed the correct arguments, this means the underlying driver didn't understand our request; |
| 1145 | + // `MoveFileEx` proceeds by reopening the file without inhibiting reparse point behavior. |
| 1146 | + None |
| 1147 | + } else { |
| 1148 | + Some(Err(err)) |
| 1149 | + } |
| 1150 | + } else { |
| 1151 | + // SAFETY: The struct has been initialized by GetFileInformationByHandleEx |
| 1152 | + let file_attribute_tag_info = unsafe { file_attribute_tag_info.assume_init() }; |
| 1153 | + |
| 1154 | + if file_attribute_tag_info.FileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0 |
| 1155 | + && file_attribute_tag_info.ReparseTag != c::IO_REPARSE_TAG_MOUNT_POINT |
| 1156 | + { |
| 1157 | + // The file is not a mount point: Reopen the file without inhibiting reparse point behavior. |
| 1158 | + None |
| 1159 | + } else { |
| 1160 | + // The file is a mount point: Don't reopen the file so that the mount point gets renamed. |
| 1161 | + Some(Ok(handle)) |
| 1162 | + } |
| 1163 | + } |
| 1164 | + } |
| 1165 | + // The underlying driver may not support `FILE_FLAG_OPEN_REPARSE_POINT`: Retry without it. |
| 1166 | + Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None, |
| 1167 | + Err(err) => Some(Err(err)), |
| 1168 | + } |
| 1169 | + .unwrap_or_else(|| create_file(0, 0))?; |
| 1170 | + |
| 1171 | + // The last field of FILE_RENAME_INFO, the file name, is unsized. |
| 1172 | + // Therefore we need to subtract the size of one wide char. |
| 1173 | + let layout = core::alloc::Layout::from_size_align( |
| 1174 | + struct_size as _, |
| 1175 | + mem::align_of::<c::FILE_RENAME_INFO>(), |
| 1176 | + ) |
| 1177 | + .unwrap(); |
| 1178 | + |
| 1179 | + let file_rename_info = unsafe { alloc(layout) } as *mut c::FILE_RENAME_INFO; |
| 1180 | + |
| 1181 | + if file_rename_info.is_null() { |
| 1182 | + handle_alloc_error(layout); |
| 1183 | + } |
| 1184 | + |
| 1185 | + // SAFETY: file_rename_info is a non-null pointer pointing to memory allocated by the global allocator. |
| 1186 | + let mut file_rename_info = unsafe { Box::from_raw(file_rename_info) }; |
| 1187 | + |
| 1188 | + // SAFETY: We have allocated enough memory for a full FILE_RENAME_INFO struct and a filename. |
| 1189 | + unsafe { |
| 1190 | + (&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 { |
| 1191 | + // Don't bother with FileRenameInfo on Windows 7 since it doesn't exist. |
| 1192 | + #[cfg(not(target_vendor = "win7"))] |
| 1193 | + Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS, |
| 1194 | + #[cfg(target_vendor = "win7")] |
| 1195 | + ReplaceIfExists: 1, |
| 1196 | + }); |
| 1197 | + |
| 1198 | + (&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut()); |
| 1199 | + (&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes); |
| 1200 | + |
| 1201 | + new.as_ptr() |
| 1202 | + .copy_to_nonoverlapping((&raw mut (*file_rename_info).FileName) as *mut u16, new.len()); |
| 1203 | + } |
| 1204 | + |
| 1205 | + #[cfg(not(target_vendor = "win7"))] |
| 1206 | + const FileInformationClass: c::FILE_INFO_BY_HANDLE_CLASS = c::FileRenameInfoEx; |
| 1207 | + #[cfg(target_vendor = "win7")] |
| 1208 | + const FileInformationClass: c::FILE_INFO_BY_HANDLE_CLASS = c::FileRenameInfo; |
| 1209 | + |
| 1210 | + // We don't use `set_file_information_by_handle` here as `FILE_RENAME_INFO` is used for both `FileRenameInfo` and `FileRenameInfoEx`. |
| 1211 | + let result = unsafe { |
| 1212 | + cvt(c::SetFileInformationByHandle( |
| 1213 | + handle.as_raw_handle(), |
| 1214 | + FileInformationClass, |
| 1215 | + (&raw const *file_rename_info).cast::<c_void>(), |
| 1216 | + struct_size, |
| 1217 | + )) |
| 1218 | + }; |
| 1219 | + |
| 1220 | + #[cfg(not(target_vendor = "win7"))] |
| 1221 | + if let Err(err) = result { |
| 1222 | + if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) { |
| 1223 | + // FileRenameInfoEx and FILE_RENAME_FLAG_POSIX_SEMANTICS were added in Windows 10 1607; retry with FileRenameInfo. |
| 1224 | + file_rename_info.Anonymous.ReplaceIfExists = 1; |
| 1225 | + |
| 1226 | + cvt(unsafe { |
| 1227 | + c::SetFileInformationByHandle( |
| 1228 | + handle.as_raw_handle(), |
| 1229 | + c::FileRenameInfo, |
| 1230 | + (&raw const *file_rename_info).cast::<c_void>(), |
| 1231 | + struct_size, |
| 1232 | + ) |
| 1233 | + })?; |
| 1234 | + } else { |
| 1235 | + return Err(err); |
| 1236 | + } |
| 1237 | + } |
| 1238 | + |
| 1239 | + #[cfg(target_vendor = "win7")] |
| 1240 | + result?; |
| 1241 | + |
1099 | 1242 | Ok(())
|
1100 | 1243 | }
|
1101 | 1244 |
|
|
0 commit comments