Skip to content

Commit 710f9af

Browse files
committed
Win: Fix std::fs::rename fallback handling
In addition to ERROR_INVALID_PARAMETER for Windows versions older than Windows 10 1607, some Windows versions may also fail with ERROR_INVALID_FUNCTION or ERROR_NOT_SUPPORTED to indicate that atomic rename is not supported. Fallback to FileRenameInfo in those cases.
1 parent aa5832b commit 710f9af

File tree

1 file changed

+127
-54
lines changed

1 file changed

+127
-54
lines changed

library/std/src/sys/fs/windows.rs

Lines changed: 127 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,74 +1233,147 @@ pub fn unlink(path: &WCStr) -> io::Result<()> {
12331233
}
12341234

12351235
pub fn rename(old: &WCStr, new: &WCStr) -> io::Result<()> {
1236-
if unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) } == 0 {
1237-
let err = api::get_last_error();
1238-
// if `MoveFileExW` fails with ERROR_ACCESS_DENIED then try to move
1239-
// the file while ignoring the readonly attribute.
1240-
// This is accomplished by calling `SetFileInformationByHandle` with `FileRenameInfoEx`.
1241-
if err == WinError::ACCESS_DENIED {
1242-
let mut opts = OpenOptions::new();
1243-
opts.access_mode(c::DELETE);
1244-
opts.custom_flags(c::FILE_FLAG_OPEN_REPARSE_POINT | c::FILE_FLAG_BACKUP_SEMANTICS);
1245-
let Ok(f) = File::open_native(&old, &opts) else { return Err(err).io_result() };
1246-
1247-
// Calculate the layout of the `FILE_RENAME_INFO` we pass to `SetFileInformation`
1248-
// This is a dynamically sized struct so we need to get the position of the last field to calculate the actual size.
1249-
let Ok(new_len_without_nul_in_bytes): Result<u32, _> =
1250-
((new.count_bytes() - 1) * 2).try_into()
1251-
else {
1252-
return Err(err).io_result();
1236+
// Calculate the layout of the `FILE_RENAME_INFO` we pass to `SetFileInformation`
1237+
// This is a dynamically sized struct so we need to get the position of the last field to calculate the actual size.
1238+
let Ok(new_len_without_nul_in_bytes): Result<u32, _> = ((new.count_bytes() - 1) * 2).try_into()
1239+
else {
1240+
return Err(WinError::INVALID_PARAMETER).io_result();
1241+
};
1242+
let offset: u32 = offset_of!(c::FILE_RENAME_INFO, FileName).try_into().unwrap();
1243+
let struct_size = offset + new_len_without_nul_in_bytes + 2;
1244+
let layout =
1245+
Layout::from_size_align(struct_size as usize, align_of::<c::FILE_RENAME_INFO>()).unwrap();
1246+
1247+
let create_file = |extra_access, extra_flags| {
1248+
let handle = unsafe {
1249+
HandleOrInvalid::from_raw_handle(c::CreateFileW(
1250+
old.as_ptr(),
1251+
c::SYNCHRONIZE | c::DELETE | extra_access,
1252+
c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE,
1253+
ptr::null(),
1254+
c::OPEN_EXISTING,
1255+
c::FILE_ATTRIBUTE_NORMAL | c::FILE_FLAG_BACKUP_SEMANTICS | extra_flags,
1256+
ptr::null_mut(),
1257+
))
1258+
};
1259+
1260+
OwnedHandle::try_from(handle).map_err(|_| io::Error::last_os_error())
1261+
};
1262+
1263+
// The following code replicates `MoveFileEx`'s behavior as reverse-engineered from its disassembly.
1264+
// If `old` refers to a mount point, we move it instead of the target.
1265+
let f = match create_file(c::FILE_READ_ATTRIBUTES, c::FILE_FLAG_OPEN_REPARSE_POINT) {
1266+
Ok(handle) => {
1267+
let mut file_attribute_tag_info: MaybeUninit<c::FILE_ATTRIBUTE_TAG_INFO> =
1268+
MaybeUninit::uninit();
1269+
1270+
let result = unsafe {
1271+
cvt(c::GetFileInformationByHandleEx(
1272+
handle.as_raw_handle(),
1273+
c::FileAttributeTagInfo,
1274+
file_attribute_tag_info.as_mut_ptr().cast(),
1275+
mem::size_of::<c::FILE_ATTRIBUTE_TAG_INFO>().try_into().unwrap(),
1276+
))
12531277
};
1254-
let offset: u32 = offset_of!(c::FILE_RENAME_INFO, FileName).try_into().unwrap();
1255-
let struct_size = offset + new_len_without_nul_in_bytes + 2;
1256-
let layout =
1257-
Layout::from_size_align(struct_size as usize, align_of::<c::FILE_RENAME_INFO>())
1258-
.unwrap();
1259-
1260-
// SAFETY: We allocate enough memory for a full FILE_RENAME_INFO struct and a filename.
1261-
let file_rename_info;
1262-
unsafe {
1263-
file_rename_info = alloc(layout).cast::<c::FILE_RENAME_INFO>();
1264-
if file_rename_info.is_null() {
1265-
return Err(io::ErrorKind::OutOfMemory.into());
1278+
1279+
if let Err(err) = result {
1280+
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _)
1281+
|| err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _)
1282+
{
1283+
// `GetFileInformationByHandleEx` documents that not all underlying drivers support all file information classes.
1284+
// Since we know we passed the correct arguments, this means the underlying driver didn't understand our request;
1285+
// `MoveFileEx` proceeds by reopening the file without inhibiting reparse point behavior.
1286+
None
1287+
} else {
1288+
Some(Err(err))
1289+
}
1290+
} else {
1291+
// SAFETY: The struct has been initialized by GetFileInformationByHandleEx
1292+
let file_attribute_tag_info = unsafe { file_attribute_tag_info.assume_init() };
1293+
let file_type = FileType::new(
1294+
file_attribute_tag_info.FileAttributes,
1295+
file_attribute_tag_info.ReparseTag,
1296+
);
1297+
1298+
if file_type.is_symlink() {
1299+
// The file is a mount point, junction point or symlink so
1300+
// don't reopen the file so that the link gets renamed.
1301+
Some(Ok(handle))
1302+
} else {
1303+
// Otherwise reopen the file without inhibiting reparse point behavior.
1304+
None
12661305
}
1306+
}
1307+
}
1308+
// The underlying driver may not support `FILE_FLAG_OPEN_REPARSE_POINT`: Retry without it.
1309+
Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None,
1310+
Err(err) => Some(Err(err)),
1311+
}
1312+
.unwrap_or_else(|| create_file(0, 0))?;
12671313

1268-
(&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
1269-
Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS
1270-
| c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
1271-
});
1314+
// SAFETY: We allocate enough memory for a full FILE_RENAME_INFO struct and a filename.
1315+
let file_rename_info;
1316+
unsafe {
1317+
file_rename_info = alloc(layout).cast::<c::FILE_RENAME_INFO>();
1318+
if file_rename_info.is_null() {
1319+
return Err(io::ErrorKind::OutOfMemory.into());
1320+
}
12721321

1273-
(&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
1274-
// Don't include the NULL in the size
1275-
(&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
1322+
(&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
1323+
Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
1324+
});
12761325

1277-
new.as_ptr().copy_to_nonoverlapping(
1278-
(&raw mut (*file_rename_info).FileName).cast::<u16>(),
1279-
new.count_bytes(),
1280-
);
1326+
(&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
1327+
// Don't include the NULL in the size
1328+
(&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
1329+
1330+
new.as_ptr().copy_to_nonoverlapping(
1331+
(&raw mut (*file_rename_info).FileName).cast::<u16>(),
1332+
new.count_bytes(),
1333+
);
1334+
}
1335+
1336+
let result = cvt(unsafe {
1337+
c::SetFileInformationByHandle(
1338+
f.as_raw_handle(),
1339+
c::FileRenameInfoEx,
1340+
file_rename_info.cast::<c_void>(),
1341+
struct_size,
1342+
)
1343+
});
1344+
1345+
let result = match result {
1346+
Ok(i) => Ok(i),
1347+
// Windows version older than Windows 10 1607 don't support FileRenameInfoEx and fail with ERROR_INVALID_PARAMETER.
1348+
// On Windows 10 1607, it may fail with STATUS_VOLUME_NOT_UPGRADED, which gets mapped to ERROR_INVALID_FUNCTION.
1349+
// On Windows Server 2022, it may fail on ReFS with ERROR_NOT_SUPPORTED.
1350+
Err(err)
1351+
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _)
1352+
|| err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _)
1353+
|| err.raw_os_error() == Some(c::ERROR_NOT_SUPPORTED as _) =>
1354+
{
1355+
unsafe {
1356+
(&raw mut (*file_rename_info).Anonymous)
1357+
.write(c::FILE_RENAME_INFO_0 { ReplaceIfExists: true });
12811358
}
12821359

1283-
let result = unsafe {
1360+
cvt(unsafe {
12841361
c::SetFileInformationByHandle(
12851362
f.as_raw_handle(),
1286-
c::FileRenameInfoEx,
1363+
c::FileRenameInfo,
12871364
file_rename_info.cast::<c_void>(),
12881365
struct_size,
12891366
)
1290-
};
1291-
unsafe { dealloc(file_rename_info.cast::<u8>(), layout) };
1292-
if result == 0 {
1293-
if api::get_last_error() == WinError::DIR_NOT_EMPTY {
1294-
return Err(WinError::DIR_NOT_EMPTY).io_result();
1295-
} else {
1296-
return Err(err).io_result();
1297-
}
1298-
}
1299-
} else {
1300-
return Err(err).io_result();
1367+
})
13011368
}
1369+
Err(err) => Err(err),
1370+
};
1371+
1372+
unsafe {
1373+
dealloc(file_rename_info.cast::<u8>(), layout);
13021374
}
1303-
Ok(())
1375+
1376+
result.map(|_| ())
13041377
}
13051378

13061379
pub fn rmdir(p: &WCStr) -> io::Result<()> {

0 commit comments

Comments
 (0)