Skip to main content

coding_tools/
atomicfile.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! Atomic sibling-file publication, including replace-existing semantics on
5//! Windows where `std::fs::rename` does not provide it.
6
7use std::path::Path;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10static NEXT: AtomicU64 = AtomicU64::new(0);
11
12pub fn write(path: &Path, bytes: impl AsRef<[u8]>) -> Result<(), String> {
13    let parent = path
14        .parent()
15        .ok_or_else(|| format!("{} has no parent", path.display()))?;
16    std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
17    let tmp = parent.join(format!(
18        ".{}.tmp-{}-{}",
19        path.file_name().and_then(|s| s.to_str()).unwrap_or("state"),
20        std::process::id(),
21        NEXT.fetch_add(1, Ordering::Relaxed)
22    ));
23    std::fs::write(&tmp, bytes).map_err(|e| format!("{}: {e}", tmp.display()))?;
24    replace(&tmp, path).map_err(|e| format!("{} -> {}: {e}", tmp.display(), path.display()))
25}
26
27#[cfg(not(windows))]
28fn replace(from: &Path, to: &Path) -> std::io::Result<()> {
29    std::fs::rename(from, to)
30}
31
32#[cfg(windows)]
33fn replace(from: &Path, to: &Path) -> std::io::Result<()> {
34    use std::os::windows::ffi::OsStrExt;
35    use windows_sys::Win32::Storage::FileSystem::{
36        MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW,
37    };
38
39    let from: Vec<u16> = from.as_os_str().encode_wide().chain(Some(0)).collect();
40    let to: Vec<u16> = to.as_os_str().encode_wide().chain(Some(0)).collect();
41    // SAFETY: both buffers are NUL-terminated and live for the duration of the
42    // call; MoveFileExW does not retain their pointers.
43    let ok = unsafe {
44        MoveFileExW(
45            from.as_ptr(),
46            to.as_ptr(),
47            MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
48        )
49    };
50    if ok == 0 {
51        Err(std::io::Error::last_os_error())
52    } else {
53        Ok(())
54    }
55}