use std::fmt;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use prikk_error::{PrikkError, Result};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use rustix::fd::OwnedFd;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use rustix::fs::{self, Mode, OFlags};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use super::failpoints;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use super::io_error;
pub(super) trait PlatformAuthority: Sized {
fn bind(path: &Path) -> Result<Self>;
fn same_as(&self, self_path: &Arc<PathBuf>, other: &Self, other_path: &Arc<PathBuf>) -> bool;
fn ensure_child(&self, relative: &Path) -> Result<Self>;
fn open_child(&self, relative: &Path) -> Result<Self>;
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
type Authority = Arc<AnchoredDirectory>;
#[cfg(target_os = "windows")]
type Authority = Arc<super::windows_authority::WindowsAuthority>;
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
type Authority = PathOnlyAuthority;
#[derive(Clone)]
pub(crate) struct MutationRoot {
path: Arc<PathBuf>,
authority: Authority,
}
impl fmt::Debug for MutationRoot {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("MutationRoot")
.finish_non_exhaustive()
}
}
impl MutationRoot {
pub(crate) fn same_authority(&self, other: &Self) -> bool {
self.authority
.same_as(&self.path, &other.authority, &other.path)
}
pub(crate) fn open(path: &Path) -> Result<Self> {
Ok(Self {
path: Arc::new(path.to_path_buf()),
authority: Authority::bind(path)?,
})
}
pub(crate) fn ensure_root(&self, relative: &Path) -> Result<Self> {
Ok(Self {
path: Arc::new(self.fallback_path(relative)?),
authority: self.authority.ensure_child(relative)?,
})
}
pub(crate) fn open_root(&self, relative: &Path) -> Result<Self> {
Ok(Self {
path: Arc::new(self.fallback_path(relative)?),
authority: self.authority.open_child(relative)?,
})
}
pub(super) fn fallback_path(&self, relative: &Path) -> Result<PathBuf> {
validate_relative(relative)?;
Ok(self.path.join(relative))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) fn duplicate_directory(&self) -> Result<AnchoredDirectory> {
let fd = rustix::io::dup(&self.authority.fd).map_err(io_error)?;
Ok(AnchoredDirectory { fd })
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) struct AnchoredDirectory {
pub(super) fd: OwnedFd,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl PlatformAuthority for Arc<AnchoredDirectory> {
fn bind(path: &Path) -> Result<Self> {
Ok(Arc::new(AnchoredDirectory::open(path)?))
}
fn same_as(&self, _self_path: &Arc<PathBuf>, other: &Self, _other_path: &Arc<PathBuf>) -> bool {
Arc::ptr_eq(self, other)
}
fn ensure_child(&self, relative: &Path) -> Result<Self> {
let mut current = dup(self)?;
for component in relative_components(relative)? {
current = current.ensure_child(component)?;
}
Ok(Arc::new(current))
}
fn open_child(&self, relative: &Path) -> Result<Self> {
let mut current = dup(self)?;
for component in relative_components(relative)? {
current = current.open_validated_child(component)?;
}
Ok(Arc::new(current))
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn dup(directory: &AnchoredDirectory) -> Result<AnchoredDirectory> {
let fd = rustix::io::dup(&directory.fd).map_err(io_error)?;
Ok(AnchoredDirectory { fd })
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
#[derive(Clone)]
pub(super) struct PathOnlyAuthority;
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
impl PlatformAuthority for PathOnlyAuthority {
fn bind(_path: &Path) -> Result<Self> {
Ok(Self)
}
fn same_as(&self, self_path: &Arc<PathBuf>, _other: &Self, other_path: &Arc<PathBuf>) -> bool {
Arc::ptr_eq(self_path, other_path)
}
fn ensure_child(&self, relative: &Path) -> Result<Self> {
let _ = relative;
super::unsupported_mutation()
}
fn open_child(&self, relative: &Path) -> Result<Self> {
let _ = relative;
Ok(Self)
}
}
#[cfg(target_os = "windows")]
pub(super) fn prepare_windows_directory_required(
root: &MutationRoot,
relative: &Path,
) -> Result<PathBuf> {
root.authority.resolve_prepared(relative)
}
#[cfg(target_os = "windows")]
pub(super) fn open_existing_windows_directory_required(
root: &MutationRoot,
relative: &Path,
) -> Result<PathBuf> {
root.authority.resolve_existing(relative)
}
#[cfg(target_os = "windows")]
pub(super) fn open_existing_windows_directory_for_read(
root: &MutationRoot,
relative: &Path,
) -> Result<Option<PathBuf>> {
root.authority.resolve_existing_for_read(relative)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl AnchoredDirectory {
fn open(path: &Path) -> Result<Self> {
failpoints::required_open()?;
let fd = fs::open(
path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(io_error)?;
Ok(Self { fd })
}
fn open_child(&self, name: &std::ffi::OsStr) -> Result<Self> {
failpoints::required_open()?;
let fd = fs::openat(
&self.fd,
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(io_error)?;
Ok(Self { fd })
}
fn open_child_for_read(&self, name: &std::ffi::OsStr) -> Result<Option<Self>> {
failpoints::required_open()?;
match fs::openat(
&self.fd,
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
) {
Ok(fd) => Ok(Some(Self { fd })),
Err(rustix::io::Errno::NOENT) => Ok(None),
Err(error) => Err(io_error(error)),
}
}
fn open_validated_child(&self, name: &std::ffi::OsStr) -> Result<Self> {
let child = self.open_child(name)?;
failpoints::observed_directory_parent_sync()?;
self.sync()?;
Ok(child)
}
fn ensure_child(&self, name: &std::ffi::OsStr) -> Result<Self> {
failpoints::required_open()?;
match fs::openat(
&self.fd,
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
) {
Ok(fd) => {
failpoints::observed_directory_parent_sync()?;
self.sync()?;
Ok(Self { fd })
}
Err(rustix::io::Errno::NOENT) => {
failpoints::directory_create()?;
failpoints::wait_at_directory_create();
match fs::mkdirat(&self.fd, name, Mode::from_raw_mode(0o755)) {
Ok(()) => {
failpoints::created_directory_parent_sync()?;
self.sync()?;
self.open_child(name)
}
Err(rustix::io::Errno::EXIST) => self.open_validated_child(name),
Err(error) => Err(io_error(error)),
}
}
Err(error) => Err(io_error(error)),
}
}
pub(super) fn sync(&self) -> Result<()> {
#[cfg(target_os = "linux")]
{
fs::fsync(&self.fd).map_err(io_error)
}
#[cfg(target_os = "macos")]
{
fs::fcntl_fullfsync(&self.fd).map_err(io_error)
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) fn prepare_directory_required(
root: &MutationRoot,
relative: &Path,
) -> Result<AnchoredDirectory> {
let mut current = root.duplicate_directory()?;
for component in relative_components(relative)? {
current = current.ensure_child(component)?;
}
Ok(current)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) fn open_existing_directory_required(
root: &MutationRoot,
relative: &Path,
) -> Result<AnchoredDirectory> {
let mut current = root.duplicate_directory()?;
for component in relative_components(relative)? {
current = current.open_validated_child(component)?;
}
Ok(current)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) fn open_existing_directory_for_read(
root: &MutationRoot,
relative: &Path,
) -> Result<Option<AnchoredDirectory>> {
let mut current = root.duplicate_directory()?;
for component in relative_components(relative)? {
let Some(child) = current.open_child_for_read(component)? else {
return Ok(None);
};
current = child;
}
Ok(Some(current))
}
pub(super) fn relative_components(path: &Path) -> Result<Vec<&std::ffi::OsStr>> {
validate_relative(path)?;
let mut components = Vec::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::Normal(value) => components.push(value),
Component::RootDir | Component::ParentDir | Component::Prefix(_) => {
return Err(PrikkError::Io(
"path must be relative to its authority root".to_string(),
));
}
}
}
Ok(components)
}
fn validate_relative(path: &Path) -> Result<()> {
for component in path.components() {
if matches!(
component,
Component::RootDir | Component::ParentDir | Component::Prefix(_)
) {
return Err(PrikkError::Io(
"path must be relative to its authority root".to_string(),
));
}
}
Ok(())
}