use std::fs::{File, FileType};
use std::io;
use std::path::Path;
use cap_fs_ext::{OpenOptionsFollowExt, OpenOptionsMaybeDirExt};
use cap_primitives::fs::{FollowSymlinks, OpenOptions};
use cap_std::ambient_authority;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(not(unix), allow(dead_code))]
pub(crate) enum UnsupportedEntry {
Socket,
Fifo,
BlockDevice,
CharacterDevice,
Unknown,
}
impl UnsupportedEntry {
pub(crate) fn of(file_type: &FileType) -> Self {
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if file_type.is_socket() {
return Self::Socket;
}
if file_type.is_fifo() {
return Self::Fifo;
}
if file_type.is_block_device() {
return Self::BlockDevice;
}
if file_type.is_char_device() {
return Self::CharacterDevice;
}
}
#[cfg(not(unix))]
let _ = file_type;
Self::Unknown
}
}
pub(crate) trait TraversalPolicy {
type Error;
const ROOT_INVARIANT: &'static str;
const ALIASED_ROOT_IS_REFUSED: bool = false;
fn io(&self, path: &Path, source: io::Error) -> Self::Error;
fn alias(&self, path: &Path) -> Self::Error;
fn unsupported_entry(&self, path: &Path, entry: UnsupportedEntry) -> Self::Error;
}
pub(crate) fn open_without_following<P: TraversalPolicy>(
policy: &P,
root: &Path,
path: &Path,
) -> Result<File, P::Error> {
let relative = path.strip_prefix(root).expect(P::ROOT_INVARIANT);
let mut components = relative.components().peekable();
let mut current = root.to_owned();
let mut directory = open_root(policy, root)?;
while let Some(component) = components.next() {
current.push(component.as_os_str());
let name = Path::new(component.as_os_str());
if components.peek().is_none() {
let file = cap_primitives::fs::open(&directory, name, &selected_file_options())
.map_err(|error| classify(policy, ¤t, error))?;
return validate_opened_file(policy, file, ¤t);
}
directory = cap_primitives::fs::open_dir_nofollow(&directory, name)
.map_err(|error| classify(policy, ¤t, error))?;
}
unreachable!("{}", P::ROOT_INVARIANT)
}
fn open_root<P: TraversalPolicy>(policy: &P, root: &Path) -> Result<File, P::Error> {
if !P::ALIASED_ROOT_IS_REFUSED {
return cap_primitives::fs::open_ambient_dir(root, ambient_authority())
.map_err(|error| policy.io(root, error));
}
let mut options = OpenOptions::new();
options
.read(true)
.follow(FollowSymlinks::No)
.maybe_dir(true);
cap_primitives::fs::open_ambient(root, &options, ambient_authority())
.map_err(|error| classify(policy, root, error))
}
fn selected_file_options() -> OpenOptions {
let mut options = OpenOptions::new();
options
.read(true)
.follow(FollowSymlinks::No)
.maybe_dir(true);
#[cfg(unix)]
{
use cap_std::fs::OpenOptionsExt;
options.custom_flags(libc::O_NONBLOCK);
}
options
}
fn classify<P: TraversalPolicy>(policy: &P, path: &Path, error: io::Error) -> P::Error {
if is_alias(path) {
return policy.alias(path);
}
policy.io(path, error)
}
fn validate_opened_file<P: TraversalPolicy>(
policy: &P,
file: File,
path: &Path,
) -> Result<File, P::Error> {
let metadata = file.metadata().map_err(|error| policy.io(path, error))?;
if metadata.file_type().is_symlink() {
return Err(policy.alias(path));
}
if !metadata.file_type().is_file() {
return Err(policy.unsupported_entry(path, UnsupportedEntry::of(&metadata.file_type())));
}
Ok(file)
}
fn is_alias(path: &Path) -> bool {
std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink())
}