use std::{ffi::OsStr, fs, io, path::Path};
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
use rustix::{
fd::AsFd,
fs::{self as unix_fs, Mode, OFlags, RenameFlags},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DirectoryCommitOutcome {
Published,
Recovered,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DirectoryCommitStep {
BeforePublication,
BeforeParentSync,
PublicationDurable,
}
#[derive(Debug, thiserror::Error)]
pub enum DirectoryCommitError<E> {
#[error("directory commit requires distinct sibling paths")]
InvalidPaths,
#[error("directory commit has two occupied paths")]
Conflict,
#[error("directory commit has no existing path")]
Missing,
#[error("directory preparation failed: {0}")]
Prepare(#[source] E),
#[error("directory publication failed: {0}")]
BeforePublication(#[source] io::Error),
#[error("directory publication may be visible: {0}")]
AfterPublication(#[source] io::Error),
}
pub fn commit_existing_directory_with<E>(
staging: &Path,
canonical: &Path,
prepare: impl FnOnce(&fs::File, &Path) -> Result<(), E>,
mut at_step: impl FnMut(DirectoryCommitStep, &Path) -> io::Result<()>,
) -> Result<DirectoryCommitOutcome, DirectoryCommitError<E>> {
let (parent, staging_name, canonical_name) = sibling_paths(staging, canonical)?;
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
{
let staging_exists =
exists_without_following(staging).map_err(DirectoryCommitError::BeforePublication)?;
let canonical_exists =
exists_without_following(canonical).map_err(DirectoryCommitError::BeforePublication)?;
let parent_fd = unix_fs::open(
parent,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(io::Error::from)
.map_err(DirectoryCommitError::BeforePublication)?;
let (selected_name, selected_path, publish) = match (staging_exists, canonical_exists) {
(true, false) => (staging_name, staging, true),
(false, true) => (canonical_name, canonical, false),
(true, true) => return Err(DirectoryCommitError::Conflict),
(false, false) => return Err(DirectoryCommitError::Missing),
};
let selected = open_directory_at(&parent_fd, selected_name)
.map_err(DirectoryCommitError::BeforePublication)?;
prepare(&selected, selected_path).map_err(DirectoryCommitError::Prepare)?;
if publish {
at_step(DirectoryCommitStep::BeforePublication, canonical)
.map_err(DirectoryCommitError::BeforePublication)?;
unix_fs::renameat_with(
&parent_fd,
staging_name,
&parent_fd,
canonical_name,
RenameFlags::NOREPLACE,
)
.map_err(io::Error::from)
.map_err(DirectoryCommitError::BeforePublication)?;
}
at_step(DirectoryCommitStep::BeforeParentSync, parent)
.map_err(DirectoryCommitError::AfterPublication)?;
unix_fs::fsync(&parent_fd)
.map_err(io::Error::from)
.map_err(DirectoryCommitError::AfterPublication)?;
at_step(DirectoryCommitStep::PublicationDurable, canonical)
.map_err(DirectoryCommitError::AfterPublication)?;
Ok(if publish {
DirectoryCommitOutcome::Published
} else {
DirectoryCommitOutcome::Recovered
})
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
{
let _ = (parent, staging_name, canonical_name, prepare, at_step);
Err(DirectoryCommitError::BeforePublication(io::Error::from(
io::ErrorKind::Unsupported,
)))
}
}
fn sibling_paths<'a, E>(
staging: &'a Path,
canonical: &'a Path,
) -> Result<(&'a Path, &'a OsStr, &'a OsStr), DirectoryCommitError<E>> {
let staging_parent = staging.parent().ok_or(DirectoryCommitError::InvalidPaths)?;
let canonical_parent = canonical
.parent()
.ok_or(DirectoryCommitError::InvalidPaths)?;
let staging_name = staging
.file_name()
.ok_or(DirectoryCommitError::InvalidPaths)?;
let canonical_name = canonical
.file_name()
.ok_or(DirectoryCommitError::InvalidPaths)?;
if staging == canonical || staging_parent != canonical_parent {
return Err(DirectoryCommitError::InvalidPaths);
}
Ok((staging_parent, staging_name, canonical_name))
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn exists_without_following(path: &Path) -> io::Result<bool> {
match fs::symlink_metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(error),
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn open_directory_at(parent: &impl AsFd, name: &OsStr) -> io::Result<fs::File> {
let fd = unix_fs::openat(
parent,
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(io::Error::from)?;
Ok(fs::File::from(fd))
}