use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub fn create_dir_all_private(path: &Path) -> io::Result<()> {
#[cfg(unix)]
{
use std::fs::DirBuilder;
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
if path.is_dir() {
let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o700));
return Ok(());
}
DirBuilder::new().recursive(true).mode(0o700).create(path)
}
#[cfg(not(unix))]
{
fs::create_dir_all(path)
}
}
pub fn write_bytes_atomically(path: &Path, payload: &[u8]) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Atomic write path has no parent: {path:?}"),
)
})?;
create_dir_all_private(parent)?;
let temp_path = temp_atomic_path(path);
let result =
write_bytes_to_temp(&temp_path, payload).and_then(|_| replace_file(&temp_path, path));
if result.is_err() {
let _ = fs::remove_file(&temp_path);
}
result
}
pub fn write_bytes_owner_only(path: &Path, payload: &[u8]) -> io::Result<()> {
let mut options = OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path)?;
file.write_all(payload)?;
Ok(())
}
fn write_bytes_to_temp(temp_path: &Path, payload: &[u8]) -> io::Result<()> {
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut temp_file = options.open(temp_path)?;
temp_file.write_all(payload)?;
temp_file.sync_all()?;
Ok(())
}
fn replace_file(temp_path: &Path, final_path: &Path) -> io::Result<()> {
match fs::rename(temp_path, final_path) {
Ok(()) => Ok(()),
Err(err) if final_path.exists() => {
fs::remove_file(final_path)?;
fs::rename(temp_path, final_path).map_err(|rename_err| {
io::Error::new(
rename_err.kind(),
format!("{err}; replace retry failed: {rename_err}"),
)
})
}
Err(err) => Err(err),
}
}
fn temp_atomic_path(path: &Path) -> PathBuf {
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("snapshot");
let parent = path.parent().unwrap_or_else(|| Path::new("."));
parent.join(format!(".tmp-{file_name}-{}", Uuid::new_v4()))
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn test_write_bytes_atomically_round_trip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("incremental").join("manifest.json");
write_bytes_atomically(&path, b"hello world").expect("write bytes atomically");
assert_eq!(fs::read(&path).expect("read bytes"), b"hello world");
}
#[cfg(unix)]
#[test]
fn test_write_bytes_atomically_restricts_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let dir = temp_dir.path().join("nested").join("cache");
let path = dir.join("entry.bin");
write_bytes_atomically(&path, b"secret").expect("write bytes atomically");
let file_mode = fs::metadata(&path)
.expect("file metadata")
.permissions()
.mode();
assert_eq!(file_mode & 0o777, 0o600, "cache file must be owner-only");
let dir_mode = fs::metadata(&dir)
.expect("dir metadata")
.permissions()
.mode();
assert_eq!(dir_mode & 0o777, 0o700, "created dir must be owner-only");
}
#[cfg(unix)]
#[test]
fn test_create_dir_all_private_tightens_existing_loose_dir() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let dir = temp_dir.path().join("legacy-cache");
fs::create_dir(&dir).expect("create legacy dir");
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755))
.expect("set loose permissions");
create_dir_all_private(&dir).expect("harden existing dir");
let mode = fs::metadata(&dir)
.expect("dir metadata")
.permissions()
.mode();
assert_eq!(
mode & 0o777,
0o700,
"existing dir must be tightened to owner-only"
);
}
}