use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
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 result = (|| -> io::Result<()> {
let mut file = std::fs::File::create(&tmp)?;
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 {
let mut name = path.as_os_str().to_owned();
name.push(".tm-tmp");
PathBuf::from(name)
}
fn copy_mode(from: &Path, to: &Path) -> io::Result<()> {
match std::fs::metadata(from) {
Ok(meta) => std::fs::set_permissions(to, meta.permissions()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
#[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}");
}
#[test]
fn write_atomic_leaves_the_original_intact_when_the_rename_fails() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("unit.plist");
std::fs::write(&path, b"original").expect("seed");
std::fs::create_dir(temp_sibling(&path)).expect("block the temp path");
let err = write_atomic(&path, b"replacement").expect_err("must fail");
assert!(!matches!(err.kind(), io::ErrorKind::NotFound), "{err}");
assert_eq!(std::fs::read(&path).expect("read"), b"original");
}
#[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);
assert!(!temp_sibling(&path).exists(), "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!(!temp_sibling(&link).exists());
}
#[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!(!temp_sibling(&path).exists());
}
}