use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
refuse_symlink(path)?;
let parent = path.parent().filter(|p| !p.as_os_str().is_empty());
if let Some(parent) = parent {
std::fs::create_dir_all(parent)?;
}
let tmp = temp_sibling(path);
let mut file = create_staging_file(&tmp)?;
let result = (|| -> io::Result<()> {
file.write_all(bytes)?;
file.sync_all()?;
drop(file);
copy_mode(path, &tmp)?;
std::fs::rename(&tmp, path)
})();
if result.is_err() {
let _ = std::fs::remove_file(&tmp);
return result;
}
#[cfg(unix)]
if let Some(parent) = parent
&& let Ok(dir) = std::fs::File::open(parent)
{
let _ = dir.sync_all();
}
Ok(())
}
fn refuse_symlink(path: &Path) -> io::Result<()> {
match std::fs::symlink_metadata(path) {
Ok(meta) if meta.file_type().is_symlink() => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{} is a symlink — replacing it atomically would sever the link and leave \
its target stale; edit the target file directly",
path.display()
),
)),
_ => Ok(()),
}
}
fn temp_sibling(path: &Path) -> PathBuf {
static NEXT: AtomicU64 = AtomicU64::new(0);
let n = NEXT.fetch_add(1, Ordering::Relaxed);
let mut name = path.as_os_str().to_owned();
name.push(format!(".{}.{n}.tm-tmp", std::process::id()));
PathBuf::from(name)
}
fn create_staging_file(tmp: &Path) -> io::Result<std::fs::File> {
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
options.open(tmp)
}
fn copy_mode(from: &Path, to: &Path) -> io::Result<()> {
match std::fs::metadata(from) {
Ok(meta) => std::fs::set_permissions(to, meta.permissions()),
#[cfg(unix)]
Err(e) if e.kind() == io::ErrorKind::NotFound => {
std::fs::set_permissions(to, default_create_mode(from)?)
}
#[cfg(not(unix))]
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
#[cfg(unix)]
fn default_create_mode(target: &Path) -> io::Result<std::fs::Permissions> {
let probe = temp_sibling(target);
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&probe)?;
let perms = std::fs::metadata(&probe).map(|m| m.permissions());
let _ = std::fs::remove_file(&probe);
perms
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_atomic_replaces_the_contents() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("unit.plist");
std::fs::write(&path, b"old").expect("seed");
write_atomic(&path, b"new").expect("write");
assert_eq!(std::fs::read(&path).expect("read"), b"new");
}
#[cfg(unix)]
#[test]
fn write_atomic_preserves_the_targets_mode() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("unit.plist");
std::fs::write(&path, b"old").expect("seed");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).expect("chmod");
write_atomic(&path, b"new").expect("write");
let mode = std::fs::metadata(&path).expect("stat").permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "mode widened to {mode:o}");
}
fn entries(dir: &Path) -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(dir)
.expect("read_dir")
.map(|e| e.expect("entry").file_name().to_string_lossy().into_owned())
.collect();
names.sort();
names
}
#[cfg(unix)]
#[test]
fn write_atomic_leaves_the_original_intact_when_staging_fails() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("unit.plist");
std::fs::write(&path, b"original").expect("seed");
let Some(_ro) = test_support::ReadOnlyDir::new(dir.path()) else {
return;
};
let err = write_atomic(&path, b"replacement").expect_err("must fail");
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied, "{err}");
assert_eq!(std::fs::read(&path).expect("read"), b"original");
assert_eq!(entries(dir.path()), ["unit.plist"]);
}
#[test]
fn write_atomic_removes_its_temp_file_when_the_rename_fails() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("unit.plist");
std::fs::create_dir(&path).expect("directory at the target");
std::fs::write(path.join("keep"), b"kept").expect("seed");
write_atomic(&path, b"replacement").expect_err("rename over a directory must fail");
assert_eq!(entries(dir.path()), ["unit.plist"]);
assert_eq!(std::fs::read(path.join("keep")).expect("read"), b"kept");
}
#[cfg(unix)]
#[test]
fn staging_file_is_owner_only_and_never_truncates() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tempdir");
let tmp = temp_sibling(&dir.path().join("unit.plist"));
let mut file = create_staging_file(&tmp).expect("create");
file.write_all(b"secret").expect("write");
let mode = std::fs::metadata(&tmp).expect("stat").permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "staging file created at {mode:o}");
let err = create_staging_file(&tmp).expect_err("an existing file must not be reopened");
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists, "{err}");
assert_eq!(std::fs::read(&tmp).expect("read"), b"secret");
}
#[cfg(unix)]
#[test]
fn write_atomic_gives_a_new_target_the_default_create_mode() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tempdir");
let reference = dir.path().join("reference");
std::fs::File::create(&reference).expect("plain create");
let expected = std::fs::metadata(&reference)
.expect("stat")
.permissions()
.mode();
let path = dir.path().join("fresh.json");
write_atomic(&path, b"{}").expect("write");
let mode = std::fs::metadata(&path).expect("stat").permissions().mode();
assert_eq!(mode & 0o777, expected & 0o777, "new target got {mode:o}");
assert_eq!(entries(dir.path()), ["fresh.json", "reference"]);
}
#[test]
fn temp_sibling_is_unique_per_call_and_beside_the_target() {
let path = Path::new("/state/dream_stats.json");
let (a, b) = (temp_sibling(path), temp_sibling(path));
assert_ne!(a, b);
for tmp in [&a, &b] {
assert_eq!(tmp.parent(), path.parent());
let name = tmp.file_name().expect("name").to_string_lossy();
let prefix = format!("dream_stats.json.{}.", std::process::id());
assert!(
name.starts_with(&prefix) && name.ends_with(".tm-tmp"),
"{name}"
);
}
}
#[cfg(unix)]
#[test]
fn write_atomic_publishes_content_mode_and_no_temp_together() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("com.trusty.mpm.plist");
std::fs::write(&path, b"<plist>old</plist>").expect("seed");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).expect("chmod");
write_atomic(&path, b"<plist>new</plist>").expect("write");
let meta = std::fs::symlink_metadata(&path).expect("stat");
assert!(
meta.file_type().is_file(),
"the target stopped being a file"
);
assert_eq!(std::fs::read(&path).expect("read"), b"<plist>new</plist>");
assert_eq!(meta.permissions().mode() & 0o777, 0o600);
let name = path.file_name().expect("name").to_string_lossy();
assert_eq!(entries(dir.path()), [name], "a temp file survived");
}
#[cfg(unix)]
#[test]
fn write_atomic_refuses_a_symlinked_target() {
let dir = tempfile::tempdir().expect("tempdir");
let real = dir.path().join("real.plist");
let link = dir.path().join("com.trusty.mpm.plist");
std::fs::write(&real, b"original").expect("seed");
std::os::unix::fs::symlink(&real, &link).expect("symlink");
let err = write_atomic(&link, b"replacement").expect_err("must refuse");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput, "{err}");
assert!(err.to_string().contains("symlink"), "{err}");
assert!(
std::fs::symlink_metadata(&link)
.expect("stat")
.file_type()
.is_symlink(),
"the link was replaced by a plain file"
);
assert_eq!(std::fs::read(&real).expect("read"), b"original");
assert_eq!(entries(dir.path()).len(), 2, "only the link and its target");
}
#[test]
fn write_atomic_leaves_no_temp_file_behind() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("unit.plist");
write_atomic(&path, b"fresh").expect("write");
assert_eq!(entries(dir.path()), ["unit.plist"]);
}
}
#[cfg(all(test, unix))]
pub(crate) mod test_support {
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
pub(crate) struct ReadOnlyDir(PathBuf);
impl ReadOnlyDir {
pub(crate) fn new(dir: &Path) -> Option<Self> {
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o555))
.expect("chmod 0555");
let guard = Self(dir.to_path_buf());
let probe = dir.join(".read-only-probe");
if std::fs::File::create(&probe).is_ok() {
let _ = std::fs::remove_file(&probe);
eprintln!(
"skipping: {} stays writable at mode 0555 (running as root?)",
dir.display()
);
return None;
}
Some(guard)
}
}
impl Drop for ReadOnlyDir {
fn drop(&mut self) {
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o755));
}
}
}