use super::{
ArtifactError, admit_file,
entry::{entry_type, open_child, opened_type},
};
use rustix::fs::{FileType, Mode, OFlags};
use std::{
fs::File,
io,
os::fd::{AsFd, BorrowedFd, OwnedFd},
path::{Component, Path},
};
pub fn open_directory_no_follow(path: &Path) -> io::Result<File> {
rustix::fs::open(
path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
Mode::empty(),
)
.map(File::from)
.map_err(io::Error::from)
}
#[derive(Debug)]
pub enum OpenedEntry {
RegularFile(File),
Directory(File),
}
#[derive(Debug, thiserror::Error)]
pub enum OpenEntryError {
#[error("root-relative opening failed: {0}")]
Io(#[from] io::Error),
#[error("unsupported entry: {kind:?}")]
UnsupportedEntry {
kind: FileType,
},
}
pub fn open_entry_beneath(
root: BorrowedFd<'_>,
relative: &Path,
) -> Result<OpenedEntry, OpenEntryError> {
let mut parts = relative.components().peekable();
if parts.peek().is_none() || parts.clone().any(|p| !matches!(p, Component::Normal(_))) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"relative normal components required",
)
.into());
}
let mut parent: Option<OwnedFd> = None;
while let Some(Component::Normal(name)) = parts.next() {
let directory = parent.as_ref().map_or(root, AsFd::as_fd);
let observed = entry_type(directory, name)?;
if observed == FileType::Symlink {
return Err(
io::Error::new(io::ErrorKind::InvalidInput, "symlink component refused").into(),
);
}
let final_component = parts.peek().is_none();
if final_component && !matches!(observed, FileType::RegularFile | FileType::Directory) {
return Err(OpenEntryError::UnsupportedEntry { kind: observed });
}
let opened = open_child(
directory,
name,
!final_component || observed == FileType::Directory,
)?;
if final_component {
let actual = opened_type(&opened)?;
let file = File::from(opened);
return match (observed, actual) {
(FileType::RegularFile, FileType::RegularFile) => {
Ok(OpenedEntry::RegularFile(file))
}
(FileType::Directory, FileType::Directory) => Ok(OpenedEntry::Directory(file)),
_ => Err(OpenEntryError::UnsupportedEntry { kind: actual }),
};
}
parent = Some(opened);
}
unreachable!("validated nonempty normal path");
}
pub fn open_regular_file_beneath(
root: BorrowedFd<'_>,
relative: &Path,
max_bytes: u64,
) -> Result<File, ArtifactError> {
let opened = open_entry_beneath(root, relative).map_err(|error| match error {
OpenEntryError::Io(error) => ArtifactError::Io(error),
OpenEntryError::UnsupportedEntry { .. } => ArtifactError::NotRegularFile,
})?;
match opened {
OpenedEntry::RegularFile(file) => {
admit_file(&file, max_bytes)?;
Ok(file)
}
OpenedEntry::Directory(_) => Err(ArtifactError::NotRegularFile),
}
}
#[cfg(test)]
mod tests;