use std::fs;
use std::io;
use std::path::Path;
use tempfile::NamedTempFile;
use crate::{Error, buffer::Buffer};
pub fn write_atomic(buffer: Buffer, target: &Path, append: bool) -> Result<(), Error> {
let parent = target
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let mut tempfile: NamedTempFile = tempfile::Builder::new()
.prefix(".rusty-sponge-")
.tempfile_in(parent)?;
if append {
if let Ok(mut existing) = fs::File::open(target) {
io::copy(&mut existing, tempfile.as_file_mut())?;
}
}
buffer.write_to(tempfile.as_file_mut())?;
tempfile.as_file_mut().sync_data().ok();
if let Ok(existing_meta) = fs::symlink_metadata(target) {
if existing_meta.file_type().is_file() {
preserve_perms_from(&existing_meta, tempfile.path())?;
}
}
tempfile.persist(target).map_err(|e| Error::Io(e.error))?;
Ok(())
}
#[cfg(unix)]
fn preserve_perms_from(meta: &fs::Metadata, tempfile_path: &Path) -> Result<(), Error> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let mode = meta.mode();
let mut perms = fs::metadata(tempfile_path)?.permissions();
perms.set_mode(mode);
fs::set_permissions(tempfile_path, perms)?;
Ok(())
}
#[cfg(windows)]
fn preserve_perms_from(meta: &fs::Metadata, tempfile_path: &Path) -> Result<(), Error> {
let was_readonly = meta.permissions().readonly();
let mut perms = fs::metadata(tempfile_path)?.permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(was_readonly);
fs::set_permissions(tempfile_path, perms)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::path::PathBuf;
fn empty_buffer() -> Buffer {
Buffer::new()
}
fn buffer_from(bytes: &[u8]) -> Buffer {
let mut b = Buffer::new();
let tmpdir = tempfile::tempdir().unwrap();
b.drain_reader(Cursor::new(bytes), 1 << 30, tmpdir.path())
.unwrap();
b
}
fn target_in(tmpdir: &Path, name: &str) -> PathBuf {
tmpdir.join(name)
}
#[test]
fn writes_buffer_atomically_to_new_target() {
let tmpdir = tempfile::tempdir().unwrap();
let target = target_in(tmpdir.path(), "out.txt");
write_atomic(buffer_from(b"hello\n"), &target, false).unwrap();
assert_eq!(fs::read(&target).unwrap(), b"hello\n");
}
#[test]
fn empty_buffer_creates_zero_byte_target() {
let tmpdir = tempfile::tempdir().unwrap();
let target = target_in(tmpdir.path(), "empty.txt");
write_atomic(empty_buffer(), &target, false).unwrap();
assert!(target.exists());
assert_eq!(fs::metadata(&target).unwrap().len(), 0);
}
#[test]
fn binary_bytes_passthrough_unchanged() {
let tmpdir = tempfile::tempdir().unwrap();
let target = target_in(tmpdir.path(), "bin.dat");
let bytes: &[u8] = &[0x00, 0xFE, 0xFF, 0xC3, 0x28, 0xA0, 0xA1];
write_atomic(buffer_from(bytes), &target, false).unwrap();
assert_eq!(fs::read(&target).unwrap(), bytes);
}
#[test]
fn replaces_existing_target() {
let tmpdir = tempfile::tempdir().unwrap();
let target = target_in(tmpdir.path(), "replace.txt");
fs::write(&target, b"OLD\n").unwrap();
write_atomic(buffer_from(b"NEW\n"), &target, false).unwrap();
assert_eq!(fs::read(&target).unwrap(), b"NEW\n");
}
#[test]
fn append_mode_concatenates_existing_and_stdin() {
let tmpdir = tempfile::tempdir().unwrap();
let target = target_in(tmpdir.path(), "append.txt");
fs::write(&target, b"original\n").unwrap();
write_atomic(buffer_from(b"appended\n"), &target, true).unwrap();
assert_eq!(fs::read(&target).unwrap(), b"original\nappended\n");
}
#[test]
fn append_mode_missing_target_treats_as_empty() {
let tmpdir = tempfile::tempdir().unwrap();
let target = target_in(tmpdir.path(), "missing.txt");
write_atomic(buffer_from(b"first\n"), &target, true).unwrap();
assert_eq!(fs::read(&target).unwrap(), b"first\n");
}
#[test]
fn append_mode_empty_stdin_preserves_existing() {
let tmpdir = tempfile::tempdir().unwrap();
let target = target_in(tmpdir.path(), "preserve.txt");
fs::write(&target, b"keep me\n").unwrap();
write_atomic(empty_buffer(), &target, true).unwrap();
assert_eq!(fs::read(&target).unwrap(), b"keep me\n");
}
#[cfg(unix)]
#[test]
fn unix_mode_bits_preserved_on_replacement() {
use std::os::unix::fs::PermissionsExt;
let tmpdir = tempfile::tempdir().unwrap();
let target = target_in(tmpdir.path(), "perms.txt");
fs::write(&target, b"old\n").unwrap();
fs::set_permissions(&target, fs::Permissions::from_mode(0o640)).unwrap();
write_atomic(buffer_from(b"new\n"), &target, false).unwrap();
let mode = fs::metadata(&target).unwrap().permissions().mode() & 0o777;
assert_eq!(
mode, 0o640,
"prior mode must be preserved on atomic replace"
);
}
}