use std::fs::OpenOptions;
use std::path::Path;
use crate::{Error, buffer::Buffer};
pub fn write_through(buffer: Buffer, target: &Path, append: bool) -> Result<(), Error> {
let mut file = if append {
OpenOptions::new().create(true).append(true).open(target)?
} else {
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(target)?
};
buffer.write_to(&mut file)?;
file.sync_data().ok();
Ok(())
}
pub fn requires_write_through(target: &Path) -> bool {
let Ok(meta) = std::fs::symlink_metadata(target) else {
return false;
};
let ft = meta.file_type();
if ft.is_symlink() {
return true;
}
if !ft.is_file() && !ft.is_dir() {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
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
}
#[test]
fn write_through_to_new_file_creates_with_content() {
let tmpdir = tempfile::tempdir().unwrap();
let target = tmpdir.path().join("new.txt");
write_through(buffer_from(b"hello\n"), &target, false).unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"hello\n");
}
#[test]
fn write_through_truncates_existing_file() {
let tmpdir = tempfile::tempdir().unwrap();
let target = tmpdir.path().join("preexisting.txt");
std::fs::write(&target, b"OLD_CONTENT\n").unwrap();
write_through(buffer_from(b"NEW\n"), &target, false).unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"NEW\n");
}
#[test]
fn write_through_append_mode_concatenates() {
let tmpdir = tempfile::tempdir().unwrap();
let target = tmpdir.path().join("append.txt");
std::fs::write(&target, b"first\n").unwrap();
write_through(buffer_from(b"second\n"), &target, true).unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"first\nsecond\n");
}
#[test]
fn requires_write_through_false_for_missing_target() {
let tmpdir = tempfile::tempdir().unwrap();
let missing = tmpdir.path().join("does-not-exist");
assert!(!requires_write_through(&missing));
}
#[test]
fn requires_write_through_false_for_regular_file() {
let tmpdir = tempfile::tempdir().unwrap();
let f = tmpdir.path().join("regular.txt");
std::fs::write(&f, b"x").unwrap();
assert!(!requires_write_through(&f));
}
#[cfg(unix)]
#[test]
fn requires_write_through_true_for_unix_symlink() {
let tmpdir = tempfile::tempdir().unwrap();
let realfile = tmpdir.path().join("real.txt");
std::fs::write(&realfile, b"linked\n").unwrap();
let link = tmpdir.path().join("via.link");
std::os::unix::fs::symlink(&realfile, &link).unwrap();
assert!(requires_write_through(&link));
}
#[cfg(unix)]
#[test]
fn write_through_unix_symlink_updates_linked_file_keeps_link() {
let tmpdir = tempfile::tempdir().unwrap();
let realfile = tmpdir.path().join("real.txt");
std::fs::write(&realfile, b"original\n").unwrap();
let link = tmpdir.path().join("via.link");
std::os::unix::fs::symlink(&realfile, &link).unwrap();
write_through(buffer_from(b"via the link\n"), &link, false).unwrap();
assert_eq!(std::fs::read(&realfile).unwrap(), b"via the link\n");
let link_meta = std::fs::symlink_metadata(&link).unwrap();
assert!(
link_meta.file_type().is_symlink(),
"FR-010: the symlink itself MUST be preserved (not replaced)"
);
}
}