use std::borrow::Cow; use std::io; use std::path::{Path, PathBuf}; /// Safely copies a file: first copies to a temporary file, then atomically replaces the destination. pub fn copy_with_temp_rename(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { internal_copy_with_temp_rename(src.as_ref(), dst.as_ref()) } fn internal_copy_with_temp_rename(src: &Path, dst: &Path) -> io::Result<()> { // Check if source file exists to avoid later failures if !src.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, format!("source file does not exist: {}", src.display()), )); } // Canonicalize paths and compare; if same file, return early to avoid unnecessary copy // Only canonicalize destination if it exists; if it doesn't exist, skip the comparison let src_canonical = std::fs::canonicalize(src); if dst.exists() { let dst_canonical = std::fs::canonicalize(dst); match (src_canonical, dst_canonical) { (Ok(src_c), Ok(dst_c)) => { if src_c == dst_c { return Ok(()); } } (Err(e), _) | (_, Err(e)) => { return Err(io::Error::new( io::ErrorKind::Other, format!( "failed to canonicalize paths (src: {}, dst: {}): {}", src.display(), dst.display(), e ), )); } } } else { // dst doesn't exist yet, so it cannot be the same file as src // If src canonicalization fails, propagate the error let _ = src_canonical.map_err(|e| { io::Error::new( io::ErrorKind::Other, format!( "failed to canonicalize source path (src: {}): {}", src.display(), e ), ) })?; } // Ensure both source and destination are regular files, preventing accidental operations on directories { let src_meta = src.metadata().map_err(|e| { io::Error::new( e.kind(), format!("failed to get source metadata for {}: {}", src.display(), e), ) })?; if !src_meta.is_file() { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("source is not a regular file: {}", src.display()), )); } if dst.exists() { let dst_meta = dst.metadata().map_err(|e| { io::Error::new( e.kind(), format!( "failed to get destination metadata for {}: {}", dst.display(), e ), ) })?; if !dst_meta.is_file() { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!( "destination exists and is not a regular file: {}", dst.display() ), )); } } } // Ensure the parent directory of the destination exists, creating it if necessary if let Some(parent) = dst.parent() { std::fs::create_dir_all(parent)?; } // Create a temporary file in the destination directory as an intermediate write target let temp_dir = dst.parent().unwrap_or_else(|| Path::new(".")); let base = src .file_name() .map(|f| f.to_string_lossy()) .unwrap_or_else(|| Cow::Borrowed("temp")); let temp_path = atomic_create_temp(temp_dir, &base)?; // Copy source file contents to the temporary file; clean up on failure if let Err(e) = std::fs::copy(src, &temp_path) { let _ = std::fs::remove_file(&temp_path); return Err(e); } // Atomically replace the destination file; clean up temporary file on failure if let Err(e) = atomic_rename(&temp_path, dst) { let _ = std::fs::remove_file(&temp_path); return Err(e); } // Copy source file permissions to the destination let _ = std::fs::set_permissions(dst, src.metadata()?.permissions()); Ok(()) } /// Atomically renames a path, replacing the destination if it exists. fn atomic_rename(src: &Path, dst: &Path) -> io::Result<()> { #[cfg(unix)] { std::fs::rename(src, dst) } #[cfg(not(unix))] { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; // Convert OsStr to a null-terminated UTF-16 vector fn to_wide_null(os_str: &OsStr) -> Vec { os_str.encode_wide().chain(Some(0)).collect() } let src_wide = to_wide_null(src.as_os_str()); let dst_wide = to_wide_null(dst.as_os_str()); // SAFETY: `src_wide` and `dst_wide` are valid, owned `Vec`s, // the pointers returned by `as_ptr()` are valid for their lifetime and are null-terminated. // Declares the Windows API MoveFileExW for atomic renaming. unsafe extern "system" { /// MOVEFILE_REPLACE_EXISTING = 1 fn MoveFileExW( lpExistingFileName: *const u16, lpNewFileName: *const u16, dwFlags: u32, ) -> i32; } const MOVEFILE_REPLACE_EXISTING: u32 = 1; // SAFETY: // 1. `src_wide` and `dst_wide` are null-terminated UTF-16 strings, valid for their lifetime. // 2. `MoveFileExW` is a Windows system API declared via extern "system", // with correct calling convention and ABI. // 3. Return value is checked; errors are handled via `last_os_error()`. let ret = unsafe { MoveFileExW( src_wide.as_ptr(), dst_wide.as_ptr(), MOVEFILE_REPLACE_EXISTING, ) }; if ret == 0 { Err(io::Error::last_os_error()) } else { Ok(()) } } } fn atomic_create_temp(dir: &Path, base: &str) -> io::Result { use std::fs::OpenOptions; // Generate temporary file names; first attempt without index, then incrementing to avoid conflicts let make_name = |i: usize| { let name = if i == 0 { format!(".temp_{}", base) } else { format!(".temp_{}_{}", base, i) }; dir.join(name) }; const MAX_ATTEMPTS: usize = 10000; // Loop to create a new file; create_new ensures the file does not already exist for i in 0..MAX_ATTEMPTS { let path = make_name(i); match OpenOptions::new().create_new(true).write(true).open(&path) { Ok(_) => return Ok(path), Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => continue, Err(e) => return Err(e), } } Err(io::Error::new( io::ErrorKind::Other, "exceeded max attempts to create temp file", )) }