use super::{ArtifactError, check_metadata};
use rustix::fs::{AtFlags, FileType, Mode, OFlags, openat, statat};
use std::{
fmt,
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)]
pub enum OpenEntryError {
Io(io::Error),
UnsupportedEntry {
kind: FileType,
},
}
impl fmt::Display for OpenEntryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "root-relative opening failed: {error}"),
Self::UnsupportedEntry { kind } => write!(formatter, "unsupported entry: {kind:?}"),
}
}
}
impl std::error::Error for OpenEntryError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
Self::UnsupportedEntry { .. } => None,
}
}
}
impl From<io::Error> for OpenEntryError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
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 stat = statat(directory, name, AtFlags::SYMLINK_NOFOLLOW).map_err(io::Error::from)?;
let observed = FileType::from_raw_mode(stat.st_mode);
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 flags = OFlags::RDONLY
| OFlags::NOFOLLOW
| OFlags::NONBLOCK
| OFlags::CLOEXEC
| if !final_component || observed == FileType::Directory {
OFlags::DIRECTORY
} else {
OFlags::empty()
};
let opened = openat(directory, name, flags, Mode::empty()).map_err(io::Error::from)?;
if final_component {
let actual = FileType::from_raw_mode(
rustix::fs::fstat(&opened).map_err(io::Error::from)?.st_mode,
);
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) => {
check_metadata(&file.metadata()?, max_bytes)?;
Ok(file)
}
OpenedEntry::Directory(_) => Err(ArtifactError::NotRegularFile),
}
}
#[cfg(test)]
mod tests;