use std::{fmt, fs::File, io, path::Path};
mod publish;
pub use publish::{
DirectoryCommitError, DirectoryCommitOutcome, DirectoryCommitStep,
commit_existing_directory_with,
};
#[derive(Debug)]
pub enum DirectoryCreationError {
BeforeCreation(io::Error),
AfterCreation(io::Error),
}
impl fmt::Display for DirectoryCreationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BeforeCreation(_) => f.write_str("private directory creation refused"),
Self::AfterCreation(_) => f.write_str("created private directory admission failed"),
}
}
}
impl std::error::Error for DirectoryCreationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
let (Self::BeforeCreation(error) | Self::AfterCreation(error)) = self;
Some(error)
}
}
pub fn create_private_directory(path: &Path) -> Result<File, DirectoryCreationError> {
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
{
use std::os::unix::fs::DirBuilderExt as _;
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
for directory in [
path.to_path_buf(),
parent
.canonicalize()
.map_err(DirectoryCreationError::BeforeCreation)?,
] {
if directory
.as_os_str()
.as_encoded_bytes()
.iter()
.any(|b| matches!(b, b'\r' | b'\n'))
{
return Err(DirectoryCreationError::BeforeCreation(
io::ErrorKind::InvalidInput.into(),
));
}
}
std::fs::DirBuilder::new()
.mode(0o700)
.create(path)
.map_err(DirectoryCreationError::BeforeCreation)?;
let finish = || -> io::Result<File> {
use rustix::fs::{Mode, OFlags, open};
File::open(parent)?.sync_all()?;
let file = File::from(open(
path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)?);
admit_directory(path, &file)?;
Ok(file)
};
finish().map_err(DirectoryCreationError::AfterCreation)
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
{
let _ = path;
Err(DirectoryCreationError::BeforeCreation(
io::ErrorKind::Unsupported.into(),
))
}
}
pub fn admit_directory(path: &Path, directory: &File) -> io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt as _;
let current = std::fs::symlink_metadata(path)?;
let held = directory.metadata()?;
if !current.is_dir()
|| !held.is_dir()
|| current.dev() != held.dev()
|| current.ino() != held.ino()
{
return Err(io::ErrorKind::InvalidInput.into());
}
Ok(())
}
#[cfg(not(unix))]
{
let _ = (path, directory);
Err(io::ErrorKind::Unsupported.into())
}
}
#[cfg(test)]
mod tests;