use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
pub fn open_private_truncate(path: &Path) -> io::Result<File> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)
}
#[cfg(not(unix))]
{
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
}
}
pub fn append_private(path: &Path) -> io::Result<File> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
OpenOptions::new()
.create(true)
.append(true)
.mode(0o600)
.open(path)
}
#[cfg(not(unix))]
{
OpenOptions::new().create(true).append(true).open(path)
}
}
pub fn write_private(path: &Path, data: &[u8]) -> io::Result<()> {
let mut f = open_private_truncate(path)?;
f.write_all(data)?;
f.flush()
}
pub fn atomic_write_private(path: &Path, data: &[u8]) -> io::Result<()> {
let tmp = path.with_added_extension("tmp");
let _ = std::fs::remove_file(&tmp);
let write_result = (|| -> io::Result<()> {
let mut f = open_private_exclusive(&tmp)?;
f.write_all(data)?;
f.flush()?;
f.sync_all()?;
Ok(())
})();
if let Err(e) = write_result {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
std::fs::rename(&tmp, path).inspect_err(|_| {
let _ = std::fs::remove_file(&tmp);
})?;
if let Some(parent) = path.parent()
&& let Ok(dir) = File::open(parent)
{
let _ = dir.sync_all();
}
Ok(())
}
fn open_private_exclusive(path: &Path) -> io::Result<File> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
}
#[cfg(not(unix))]
{
OpenOptions::new().write(true).create_new(true).open(path)
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt as _;
fn mode(path: &Path) -> u32 {
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}
#[test]
fn write_private_creates_0600() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("secret.txt");
write_private(&p, b"hello").unwrap();
assert_eq!(mode(&p), 0o600);
assert_eq!(std::fs::read(&p).unwrap(), b"hello");
}
#[test]
fn atomic_write_private_overwrites_with_0600() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("state.json");
{
use std::os::unix::fs::OpenOptionsExt as _;
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o644)
.open(&p)
.unwrap()
.write_all(b"old")
.unwrap();
}
atomic_write_private(&p, b"new").unwrap();
assert_eq!(mode(&p), 0o600);
assert_eq!(std::fs::read(&p).unwrap(), b"new");
}
#[test]
fn atomic_write_private_preserves_extension_appends_tmp() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("vault.age");
let tmp = p.with_added_extension("tmp");
assert_eq!(tmp.file_name().unwrap(), "vault.age.tmp");
atomic_write_private(&p, b"data").unwrap();
assert!(p.exists());
assert!(!tmp.exists(), "tmp must be cleaned up after success");
}
#[test]
fn atomic_write_private_cleans_tmp_on_success() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("data.json");
atomic_write_private(&p, b"{}").unwrap();
let tmp = p.with_added_extension("tmp");
assert!(!tmp.exists());
}
#[test]
fn atomic_write_private_errors_on_unwritable_dir() {
use std::os::unix::fs::PermissionsExt as _;
let outer = tempfile::tempdir().unwrap();
let inner = outer.path().join("sub");
std::fs::create_dir(&inner).unwrap();
let p = inner.join("data.json");
atomic_write_private(&p, b"first").unwrap();
std::fs::set_permissions(&inner, std::fs::Permissions::from_mode(0o500)).unwrap();
let result = atomic_write_private(&p, b"second");
std::fs::set_permissions(&inner, std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(result.is_err(), "write to read-only dir must fail");
assert_eq!(std::fs::read(&p).unwrap(), b"first");
}
#[test]
fn atomic_write_private_stale_tmp_removed_before_write() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("state.json");
let tmp = p.with_added_extension("tmp");
std::fs::write(&tmp, b"stale").unwrap();
atomic_write_private(&p, b"fresh").unwrap();
assert_eq!(std::fs::read(&p).unwrap(), b"fresh");
assert!(!tmp.exists());
}
#[test]
fn append_private_creates_0600_on_new_file() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("audit.jsonl");
{
let mut f = append_private(&p).unwrap();
writeln!(f, "line1").unwrap();
}
assert_eq!(mode(&p), 0o600);
}
#[test]
fn append_private_preserves_mode_on_reopen() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("audit.jsonl");
{
let mut f = append_private(&p).unwrap();
writeln!(f, "line1").unwrap();
}
{
let mut f = append_private(&p).unwrap();
writeln!(f, "line2").unwrap();
}
assert_eq!(mode(&p), 0o600);
let content = std::fs::read_to_string(&p).unwrap();
assert!(content.contains("line1") && content.contains("line2"));
}
}