use anyhow::{Result, anyhow, ensure};
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use super::{sync, validation};
pub fn atomic_write_private_text(path: &Path, content: &str) -> Result<()> {
atomic_write_private_text_with_policy(path, content, usize::MAX, false)
}
pub fn atomic_write_private_text_bounded(
path: &Path,
content: &str,
max_bytes: usize,
) -> Result<()> {
atomic_write_private_text_with_policy(path, content, max_bytes, true)
}
fn atomic_write_private_text_with_policy(
path: &Path,
content: &str,
max_bytes: usize,
require_private_existing_file: bool,
) -> Result<()> {
ensure!(
content.len() <= max_bytes,
"private state file exceeds its bounded size"
);
if require_private_existing_file {
validation::ensure_private_state_parent(path)?;
} else {
validation::ensure_atomic_write_parent(path)?;
}
if let Some(metadata) = validation::state_marker_metadata(path)? {
if require_private_existing_file {
validation::validate_private_file_metadata(&metadata)?;
} else {
validation::validate_private_file_type_and_owner(&metadata)?;
}
}
let tmp = sibling_temp_path(path);
let write_result = (|| -> Result<()> {
let mut options = OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600).custom_flags(nix::libc::O_NOFOLLOW);
}
let mut file = options
.open(&tmp)
.map_err(|_| anyhow!("private state temporary file could not be created"))?;
validation::apply_private_file_permissions(&file, &tmp)?;
file.write_all(content.as_bytes())?;
sync::file(&mut file)?;
validation::validate_open_state_marker(&tmp, &file)?;
drop(file);
rename_into_place(&tmp, path)
.map_err(|_| anyhow!("private state file could not be committed"))?;
let committed = fs::symlink_metadata(path)
.map_err(|_| anyhow!("private state file disappeared after commit"))?;
validation::validate_private_file_metadata(&committed)?;
sync::parent(path)?;
Ok(())
})();
if write_result.is_err() {
let _ = fs::remove_file(&tmp);
}
write_result
}
pub(super) fn rename_into_place(tmp: &Path, path: &Path) -> Result<()> {
validation::validate_regular_file_or_missing_no_follow(tmp, false)?;
validation::validate_regular_file_or_missing_no_follow(path, true)?;
match fs::rename(tmp, path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::CrossesDevices => {
copy_cross_device_then_atomic_replace(tmp, path)
}
Err(error) => Err(error.into()),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommitDurability {
Confirmed,
Unconfirmed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CleanupOutcome {
Removed,
DurabilityUnconfirmed(PathBuf),
Retained(PathBuf),
}
#[derive(Debug)]
pub struct AtomicWriteFailure {
error: anyhow::Error,
cleanup: CleanupOutcome,
}
impl AtomicWriteFailure {
pub fn error(&self) -> &anyhow::Error {
&self.error
}
pub fn cleanup(&self) -> &CleanupOutcome {
&self.cleanup
}
pub fn into_parts(self) -> (anyhow::Error, CleanupOutcome) {
(self.error, self.cleanup)
}
fn not_committed(error: anyhow::Error) -> Self {
Self {
error,
cleanup: CleanupOutcome::Removed,
}
}
fn from_cleanup(error: anyhow::Error, cleanup: CleanupOutcome) -> Self {
Self { error, cleanup }
}
}
fn remove_checked(temp: &Path) -> CleanupOutcome {
remove_with_sync(temp, sync::parent)
}
pub(super) fn remove_with_sync<F>(temp: &Path, mut sync_parent: F) -> CleanupOutcome
where
F: FnMut(&Path) -> Result<()>,
{
match fs::remove_file(temp) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(_) => return CleanupOutcome::Retained(temp.to_path_buf()),
}
match sync_parent(temp) {
Ok(()) => CleanupOutcome::Removed,
Err(_) => CleanupOutcome::DurabilityUnconfirmed(
temp.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or(Path::new("."))
.to_path_buf(),
),
}
}
pub(super) fn commit_with_sync<F>(
temp: &Path,
destination: &Path,
mut sync_parent: F,
) -> Result<CommitDurability>
where
F: FnMut(&Path) -> Result<()>,
{
validation::validate_regular_file_or_missing_no_follow(temp, false)?;
validation::validate_regular_file_or_missing_no_follow(destination, true)?;
let temporary = fs::symlink_metadata(temp)?;
validation::validate_private_file_metadata(&temporary)?;
sync_parent(destination)?;
if fs::rename(temp, destination).is_err() {
return Err(anyhow!("private state file could not be committed"));
}
match sync_parent(destination) {
Ok(()) => Ok(CommitDurability::Confirmed),
Err(_) => Ok(CommitDurability::Unconfirmed),
}
}
pub struct AtomicPrivateFile {
file: fs::File,
temp: PathBuf,
destination: PathBuf,
committed: bool,
}
impl AtomicPrivateFile {
pub fn create(destination: &Path) -> Result<Self, AtomicWriteFailure> {
create_private_file(destination)
}
pub fn file_mut(&mut self) -> &mut fs::File {
&mut self.file
}
pub fn commit(mut self) -> Result<CommitDurability, AtomicWriteFailure> {
if let Err(error) = sync::file(&mut self.file) {
return Err(self.abort(error));
}
if let Err(error) = validation::validate_open_state_marker(&self.temp, &self.file) {
return Err(self.abort(error));
}
match commit_with_sync(&self.temp, &self.destination, sync::parent) {
Ok(durability) => {
self.committed = true;
Ok(durability)
}
Err(error) => Err(self.abort(error)),
}
}
pub fn discard(mut self) -> CleanupOutcome {
self.committed = true;
remove_checked(&self.temp)
}
fn abort(mut self, error: anyhow::Error) -> AtomicWriteFailure {
self.committed = true;
AtomicWriteFailure::from_cleanup(error, remove_checked(&self.temp))
}
}
fn create_private_file(destination: &Path) -> Result<AtomicPrivateFile, AtomicWriteFailure> {
validation::ensure_atomic_write_parent(destination)
.map_err(AtomicWriteFailure::not_committed)?;
validation::validate_regular_file_or_missing_no_follow(destination, true)
.map_err(AtomicWriteFailure::not_committed)?;
let temp = sibling_temp_path(destination);
let mut options = OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600).custom_flags(nix::libc::O_NOFOLLOW);
}
let file = options.open(&temp).map_err(|_| {
AtomicWriteFailure::not_committed(anyhow!(
"private state temporary file could not be created"
))
})?;
let setup = validation::apply_private_file_permissions(&file, &temp)
.and_then(|()| validation::validate_open_state_marker(&temp, &file));
if let Err(error) = setup {
drop(file);
return Err(AtomicWriteFailure::from_cleanup(
error,
remove_checked(&temp),
));
}
Ok(AtomicPrivateFile {
file,
temp,
destination: destination.to_path_buf(),
committed: false,
})
}
impl Drop for AtomicPrivateFile {
fn drop(&mut self) {
if !self.committed {
let _ = fs::remove_file(&self.temp);
}
}
}
fn copy_cross_device_then_atomic_replace(tmp: &Path, path: &Path) -> Result<()> {
let parent = sync::parent_or_current(path)?;
validation::validate_private_path_ancestors(parent)?;
let stage = sibling_temp_path(path);
validation::validate_regular_file_or_missing_no_follow(&stage, true)?;
let source_path_metadata = fs::symlink_metadata(tmp)?;
validation::validate_private_file_type_and_owner(&source_path_metadata)?;
let mut source_options = OpenOptions::new();
source_options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
source_options.custom_flags(nix::libc::O_NOFOLLOW);
}
let mut source = source_options
.open(tmp)
.map_err(|_| anyhow!("atomic replacement source could not be opened"))?;
validation::ensure_same_file(&source_path_metadata, &source.metadata()?)?;
let result = (|| -> Result<()> {
let mut stage_options = OpenOptions::new();
stage_options.create_new(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
stage_options
.mode(0o600)
.custom_flags(nix::libc::O_NOFOLLOW);
}
let mut staged = stage_options
.open(&stage)
.map_err(|_| anyhow!("atomic replacement stage could not be created"))?;
validation::apply_private_file_permissions(&staged, &stage)?;
io::copy(&mut source, &mut staged)?;
sync::file(&mut staged)?;
validation::validate_open_state_marker(&stage, &staged)?;
drop(staged);
validation::validate_private_path_ancestors(parent)?;
validation::validate_regular_file_or_missing_no_follow(path, true)?;
fs::rename(&stage, path).map_err(|_| anyhow!("atomic replacement commit failed"))?;
sync::parent(path)?;
let stable_source = fs::symlink_metadata(tmp)
.map_err(|_| anyhow!("atomic replacement source changed before cleanup"))?;
validation::ensure_same_file(&source_path_metadata, &stable_source)?;
fs::remove_file(tmp)?;
Ok(())
})();
if result.is_err() {
let _ = fs::remove_file(&stage);
}
result
}
fn sibling_temp_path(path: &Path) -> PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let extension = path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("acativa");
path.with_extension(format!(
"{}.tmp-{}-{}",
extension,
std::process::id(),
stamp
))
}