Skip to content

Commit 9c064c1

Browse files
host_env: os.replace for Windows (#8212)
`rename` and `replace` are the same for Unixes but different for Windows in Python. POSIX's `rename` atomically replaces its target; there is no `replace` in POSIX. `renameat2` with `RENAME_NOREPLACE` atomically checks if a file exists and renames if it doesn't, but Python's `rename` and `replace` predate `renameat2`. For Windows, `rename` acts like the `RENAME_NOREPLACE` flag while `replace` functions like `rename`. I implemented both using the Windows API. Both implementations follow CPython's code by using `MoveFileExW`. There are modern Windows APIs that may be worth using in the future. Rust's standard library prefers the modern APIs but falls back to `MoveFileExW` for deprecated Windows versions.
1 parent 238bed6 commit 9c064c1

7 files changed

Lines changed: 220 additions & 92 deletions

File tree

crates/host_env/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ pub mod posix;
5656
#[cfg(windows)]
5757
#[path = "posix_windows.rs"]
5858
pub mod posix;
59+
#[cfg(any(unix, target_os = "wasi"))]
60+
pub mod posix_unix_like;
5961
#[cfg(unix)]
6062
pub mod pwd;
6163
#[cfg(unix)]

crates/host_env/src/os.rs

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ use core::ffi::CStr;
99
use core::str::Utf8Error;
1010
#[cfg(windows)]
1111
use core::time::Duration;
12-
#[cfg(unix)]
13-
use rustix::fd::AsFd;
1412
use std::{
1513
env,
1614
ffi::{OsStr, OsString},
@@ -267,41 +265,6 @@ pub fn copy_file_range(
267265
rustix::fs::copy_file_range(src, offset_src, dst, offset_dst, count)
268266
}
269267

270-
#[cfg(not(unix))]
271-
pub fn rename(
272-
from: impl AsRef<std::path::Path>,
273-
from_fd: Option<crt_fd::Borrowed<'_>>,
274-
to: impl AsRef<std::path::Path>,
275-
to_fd: Option<crt_fd::Borrowed<'_>>,
276-
) -> io::Result<()> {
277-
if from_fd.is_none() && to_fd.is_none() {
278-
// TODO: Rust's implementation always overwrites the file so ensure consistency between
279-
// operating systems. We need to use windows-sys directly to distinguish between
280-
// os.rename and os.replace.
281-
std::fs::rename(from, to)
282-
} else {
283-
core::hint::cold_path();
284-
Err(io::Error::new(
285-
io::ErrorKind::Unsupported,
286-
"renameat is not available on this platform",
287-
))
288-
}
289-
}
290-
291-
#[cfg(unix)]
292-
pub fn rename(
293-
from: impl AsRef<std::path::Path>,
294-
from_fd: Option<crt_fd::Borrowed<'_>>,
295-
to: impl AsRef<std::path::Path>,
296-
to_fd: Option<crt_fd::Borrowed<'_>>,
297-
) -> io::Result<()> {
298-
let from = from.as_ref();
299-
let from_fd = from_fd.as_ref().map_or(rustix::fs::CWD, AsFd::as_fd);
300-
let to = to.as_ref();
301-
let to_fd = to_fd.as_ref().map_or(rustix::fs::CWD, AsFd::as_fd);
302-
rustix::fs::renameat(from_fd, from, to_fd, to).map_err(Into::into)
303-
}
304-
305268
#[cfg(windows)]
306269
pub fn seek_fd(
307270
fd: crt_fd::Borrowed<'_>,

crates/host_env/src/posix.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::os::fd::FromRawFd;
1010
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd};
1111
use std::path::Path;
1212

13-
use crate::crt_fd;
13+
pub use super::posix_unix_like::*;
1414

1515
pub use libc::{c_char, pid_t};
1616

@@ -178,16 +178,6 @@ pub fn fcopyfile(in_fd: i32, out_fd: i32, flags: u32) -> std::io::Result<()> {
178178
}
179179
}
180180

181-
#[cfg(any(unix, target_os = "wasi"))]
182-
pub fn make_dir(
183-
dir_fd: Option<crt_fd::Borrowed<'_>>,
184-
path: &impl AsRef<Path>,
185-
mode: libc::mode_t,
186-
) -> std::io::Result<()> {
187-
let dir_fd = dir_fd.as_ref().map_or(rustix::fs::CWD, AsFd::as_fd);
188-
rustix::fs::mkdirat(dir_fd, path.as_ref(), mode.into()).map_err(Into::into)
189-
}
190-
191181
#[cfg(unix)]
192182
pub fn link_paths(src: &CStr, dst: &CStr, follow_symlinks: bool) -> std::io::Result<()> {
193183
let flags = if follow_symlinks {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//! Common POSIX implementations across Unix-likes.
2+
3+
use std::{io, path::Path};
4+
5+
use rustix::{fd::AsFd, fs};
6+
7+
pub use rustix::fs::RawMode;
8+
9+
use crate::crt_fd;
10+
11+
/// https://pubs.opengroup.org/onlinepubs/9799919799/functions/mkdir.html
12+
pub fn make_dir(
13+
dir_fd: Option<crt_fd::Borrowed<'_>>,
14+
path: impl AsRef<Path>,
15+
mode: fs::RawMode,
16+
) -> io::Result<()> {
17+
let dir_fd = dir_fd.as_ref().map_or(fs::CWD, AsFd::as_fd);
18+
fs::mkdirat(dir_fd, path.as_ref(), mode.into()).map_err(Into::into)
19+
}
20+
21+
/// https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html
22+
pub fn rename(
23+
from: impl AsRef<Path>,
24+
from_fd: Option<crt_fd::Borrowed<'_>>,
25+
to: impl AsRef<Path>,
26+
to_fd: Option<crt_fd::Borrowed<'_>>,
27+
) -> io::Result<()> {
28+
let from = from.as_ref();
29+
let from_fd = from_fd.as_ref().map_or(fs::CWD, AsFd::as_fd);
30+
let to = to.as_ref();
31+
let to_fd = to_fd.as_ref().map_or(fs::CWD, AsFd::as_fd);
32+
fs::renameat(from_fd, from, to_fd, to).map_err(Into::into)
33+
}
34+
35+
/// https://docs.python.org/3/library/os.html#os.replace
36+
///
37+
/// Atomically replace `to` with `from`.
38+
/// POSIX's rename already atomically replaces targets, so this function just forwards to [`rename`].
39+
#[inline]
40+
pub fn replace(
41+
from: impl AsRef<Path>,
42+
from_fd: Option<crt_fd::Borrowed<'_>>,
43+
to: impl AsRef<Path>,
44+
to_fd: Option<crt_fd::Borrowed<'_>>,
45+
) -> io::Result<()> {
46+
rename(from, from_fd, to, to_fd)
47+
}

crates/host_env/src/posix_wasi.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,9 @@ use core::{ffi::CStr, time::Duration};
33
use rustix::fd::AsFd;
44
use std::{ffi::OsStr, io, path::Path};
55

6-
use crate::{crt_fd, os::CheckLibcResult};
6+
pub use super::posix_unix_like::*;
77

8-
pub fn make_dir(
9-
dir_fd: Option<crt_fd::Borrowed<'_>>,
10-
path: &impl AsRef<Path>,
11-
mode: libc::mode_t,
12-
) -> std::io::Result<()> {
13-
let dir_fd = dir_fd.as_ref().map_or(rustix::fs::CWD, AsFd::as_fd);
14-
rustix::fs::mkdirat(dir_fd, path.as_ref(), mode.into()).map_err(Into::into)
15-
}
8+
use crate::{crt_fd, os::CheckLibcResult};
169

1710
pub fn remove_dir_at(dir_fd: i32, path: &CStr) -> io::Result<()> {
1811
unsafe { libc::unlinkat(dir_fd, path.as_ptr(), libc::AT_REMOVEDIR) }.check_libc_neg()?;

crates/host_env/src/posix_windows.rs

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,96 @@
44
//! these syscalls, but they can be emulated with a mix of the Windows API and the Rust standard
55
//! library, the latter of which calls the former.
66
7+
use core::hint::cold_path;
78
use std::{fs, io, path::Path};
89

10+
use widestring::WideCString;
11+
use windows_sys::Win32::{
12+
Foundation::FALSE,
13+
Storage::FileSystem::{MOVE_FILE_FLAGS, MOVEFILE_REPLACE_EXISTING, MoveFileExW},
14+
};
15+
916
use crate::crt_fd;
1017

11-
#[expect(non_camel_case_types)]
12-
pub type mode_t = u32;
18+
pub type RawMode = u32;
1319

1420
pub fn make_dir(
1521
dir_fd: Option<crt_fd::Borrowed<'_>>,
16-
path: &impl AsRef<Path>,
17-
_mode: mode_t,
22+
path: impl AsRef<Path>,
23+
_mode: RawMode,
1824
) -> io::Result<()> {
1925
debug_assert!(dir_fd.is_none());
2026
// TODO: On Windows, Python has an override if the mode is 0o700
2127
fs::create_dir(path)
2228
}
29+
30+
/// https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html
31+
#[inline]
32+
pub fn rename(
33+
from: impl AsRef<Path>,
34+
#[cfg_attr(not(debug_assertions), expect(unused_variables))] from_fd: Option<
35+
crt_fd::Borrowed<'_>,
36+
>,
37+
to: impl AsRef<Path>,
38+
#[cfg_attr(not(debug_assertions), expect(unused_variables))] to_fd: Option<
39+
crt_fd::Borrowed<'_>,
40+
>,
41+
) -> io::Result<()> {
42+
debug_assert!(from_fd.is_none());
43+
debug_assert!(to_fd.is_none());
44+
45+
rename_impl(from, to, 0)
46+
}
47+
48+
/// https://docs.python.org/3/library/os.html#os.replace
49+
///
50+
/// Atomically replace `to` with `from`.
51+
#[inline]
52+
pub fn replace(
53+
from: impl AsRef<Path>,
54+
#[cfg_attr(not(debug_assertions), expect(unused_variables))] from_fd: Option<
55+
crt_fd::Borrowed<'_>,
56+
>,
57+
to: impl AsRef<Path>,
58+
#[cfg_attr(not(debug_assertions), expect(unused_variables))] to_fd: Option<
59+
crt_fd::Borrowed<'_>,
60+
>,
61+
) -> io::Result<()> {
62+
debug_assert!(from_fd.is_none());
63+
debug_assert!(to_fd.is_none());
64+
65+
rename_impl(from, to, MOVEFILE_REPLACE_EXISTING)
66+
}
67+
68+
fn rename_impl(
69+
from: impl AsRef<Path>,
70+
to: impl AsRef<Path>,
71+
flags: MOVE_FILE_FLAGS,
72+
) -> io::Result<()> {
73+
let from = WideCString::from_os_str(from.as_ref())
74+
.map_err(io::Error::other)?
75+
.into_vec_with_nul();
76+
let to = WideCString::from_os_str(to.as_ref())
77+
.map_err(io::Error::other)?
78+
.into_vec_with_nul();
79+
80+
// SAFETY:
81+
// * from and to are NUL terminated wide strings
82+
let success = unsafe {
83+
// Rust's [`std::fs::rename`] is more complicated than CPython's. Rust attempts to use modern APIs
84+
// where available, such as `FileRenameInfoEx`, which better map to POSIX. CPython simply
85+
// calls MoveFileExW so we'll do that for parity. However, it may be better to use the new
86+
// APIs and fall back if possible, especially if they're faster.
87+
//
88+
// Unlike POSIX's rename, MoveFileExW does not automatically move between volumes.
89+
// This is expected behavior in CPython.
90+
MoveFileExW(from.as_ptr(), to.as_ptr(), flags)
91+
};
92+
93+
if success != FALSE {
94+
Ok(())
95+
} else {
96+
cold_path();
97+
Err(io::Error::last_os_error())
98+
}
99+
}

0 commit comments

Comments
 (0)