use std::{
fs::{self, File, Metadata},
io::{ErrorKind, Read},
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
};
use super::super::{
alloc,
error::{
AllocationSite, Error, IoFailure, IoOperation, Result, TraversalDefect, TraversalRefusal,
},
};
pub(crate) const MAX_TREE_ENTRIES: usize = 1024;
const BLOCK: usize = 1 << 20;
pub(crate) fn io(operation: IoOperation, path: &Path) -> impl FnOnce(std::io::Error) -> Error + '_ {
move |e| Error::Io(IoFailure::new(operation, path.to_path_buf(), e))
}
fn refusal(defect: TraversalDefect, path: &Path) -> Error {
Error::Traversal(TraversalRefusal::new(defect, path.to_path_buf()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Kind {
Dir,
File,
Other,
}
#[derive(Debug)]
pub(crate) struct Resolved {
pub(crate) kind: Kind,
pub(crate) metadata: Metadata,
}
impl Resolved {
fn of(metadata: Metadata) -> Self {
let kind = if metadata.is_dir() {
Kind::Dir
} else if metadata.is_file() {
Kind::File
} else {
Kind::Other
};
Self { kind, metadata }
}
pub(crate) fn is_dir(&self) -> bool {
self.kind == Kind::Dir
}
pub(crate) fn is_file(&self) -> bool {
self.kind == Kind::File
}
}
enum Stop {
Broken(PathBuf),
Failed(std::io::Error),
}
fn leads_nowhere(error: &std::io::Error) -> bool {
matches!(error.kind(), ErrorKind::NotFound | ErrorKind::NotADirectory)
|| error.raw_os_error() == Some(libc::ELOOP)
}
fn is_broken_link(path: &Path) -> bool {
fs::symlink_metadata(path).is_ok_and(|entry| entry.file_type().is_symlink())
&& fs::metadata(path).is_err_and(|error| leads_nowhere(&error))
}
fn lookup(path: &Path) -> core::result::Result<Option<Resolved>, Stop> {
match fs::metadata(path) {
Ok(metadata) => Ok(Some(Resolved::of(metadata))),
Err(error) => {
if leads_nowhere(&error) {
if let Some(link) = path.ancestors().find(|step| is_broken_link(step)) {
return Err(Stop::Broken(link.to_path_buf()));
}
if error.kind() == ErrorKind::NotFound {
return Ok(None);
}
}
Err(Stop::Failed(error))
}
}
}
pub(crate) fn resolve(path: &Path) -> Result<Option<Resolved>> {
resolve_or(path, io(IoOperation::Stat, path))
}
pub(crate) fn resolve_or(
path: &Path,
failed: impl FnOnce(std::io::Error) -> Error,
) -> Result<Option<Resolved>> {
lookup(path).map_err(|stop| match stop {
Stop::Broken(link) => refusal(TraversalDefect::BrokenLink, &link),
Stop::Failed(error) => failed(error),
})
}
#[derive(Debug)]
pub(crate) struct Entry {
pub(crate) path: PathBuf,
pub(crate) kind: Kind,
pub(crate) metadata: Metadata,
}
pub(crate) fn list(root: &Path) -> Result<Vec<Entry>> {
let identity = |metadata: &Metadata| (metadata.dev(), metadata.ino());
let top = match resolve(root)? {
Some(top) if top.is_dir() => top,
_ => {
return Err(io(IoOperation::Read, root)(ErrorKind::NotADirectory.into()));
}
};
let mut out: Vec<Entry> = Vec::new();
let mut pending: Vec<(PathBuf, usize, (u64, u64))> = Vec::new();
let mut ancestors: Vec<(u64, u64)> = Vec::new();
alloc::reserve(&mut pending, 1, AllocationSite::StampDigests)?;
pending.push((root.to_path_buf(), 0, identity(&top.metadata)));
while let Some((dir, depth, id)) = pending.pop() {
ancestors.truncate(depth);
alloc::reserve(&mut ancestors, 1, AllocationSite::StampDigests)?;
ancestors.push(id);
for entry in fs::read_dir(&dir).map_err(io(IoOperation::Read, &dir))? {
let path = entry.map_err(io(IoOperation::Read, &dir))?.path();
let Some(found) = resolve(&path)? else {
continue;
};
if found.kind == Kind::Other {
continue;
}
if out.len() == MAX_TREE_ENTRIES {
return Err(refusal(TraversalDefect::TooManyEntries, root));
}
if found.is_dir() {
let child = identity(&found.metadata);
if ancestors.contains(&child) {
return Err(refusal(TraversalDefect::Cycle, &path));
}
alloc::reserve(&mut pending, 1, AllocationSite::StampDigests)?;
pending.push((path.clone(), depth + 1, child));
}
alloc::reserve(&mut out, 1, AllocationSite::StampDigests)?;
out.push(Entry {
path,
kind: found.kind,
metadata: found.metadata,
});
}
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PastCap {
at_least: u64,
held: u64,
cap: u64,
}
impl PastCap {
pub(crate) const fn at_size(size: u64, cap: u64) -> Self {
Self {
at_least: size,
held: size,
cap,
}
}
pub(crate) fn sentence(&self, what: &str) -> String {
if self.at_least > self.cap {
format!(
"{what} holds at least {} bytes, past its {}-byte cap",
self.at_least, self.cap
)
} else {
format!(
"{what} grew past the {} bytes it held when its size was taken, while it was read",
self.held
)
}
}
}
#[derive(Debug)]
pub(crate) enum Refusal {
Absent,
PastCap(PastCap),
Named(Error),
Failed(std::io::Error),
}
#[derive(Debug)]
pub(crate) struct Opened {
pub(crate) file: File,
pub(crate) metadata: Metadata,
}
#[cfg(test)]
pub(crate) mod after_size {
use std::{cell::RefCell, path::Path};
type Hook = Box<dyn FnMut(&Path)>;
std::thread_local! {
static HOOK: RefCell<Option<Hook>> = const { RefCell::new(None) };
}
#[must_use = "the seam clears when the guard drops"]
pub(crate) fn set(hook: impl FnMut(&Path) + 'static) -> Guard {
HOOK.with(|cell| *cell.borrow_mut() = Some(Box::new(hook)));
Guard(())
}
pub(crate) struct Guard(());
impl Drop for Guard {
fn drop(&mut self) {
HOOK.with(|cell| *cell.borrow_mut() = None);
}
}
pub(crate) fn run(path: &Path) {
HOOK.with(|cell| {
if let Some(hook) = cell.borrow_mut().as_mut() {
hook(path);
}
});
}
}
#[cfg(not(test))]
mod after_size {
#[inline(always)]
pub(super) fn run(_: &std::path::Path) {}
}
pub(crate) fn open(path: &Path, cap: Option<u64>) -> core::result::Result<Opened, Refusal> {
let found = match lookup(path) {
Ok(Some(found)) => found,
Ok(None) => return Err(Refusal::Absent),
Err(Stop::Broken(link)) => {
return Err(Refusal::Named(refusal(TraversalDefect::BrokenLink, &link)));
}
Err(Stop::Failed(error)) => return Err(Refusal::Failed(error)),
};
if !found.is_file() {
return Err(Refusal::Failed(std::io::Error::other("not a regular file")));
}
let size = found.metadata.len();
if let Some(cap) = cap
&& size > cap
{
return Err(Refusal::PastCap(PastCap::at_size(size, cap)));
}
after_size::run(path);
let file = File::open(path).map_err(Refusal::Failed)?;
Ok(Opened {
file,
metadata: found.metadata,
})
}
fn count_on(file: &mut File, from: u64, cap: u64) -> core::result::Result<u64, Refusal> {
let mut seen = from;
let mut block = [0u8; 4096];
while seen <= cap {
match file.read(&mut block) {
Ok(0) => break,
Ok(read) => seen += read as u64,
Err(error) if error.kind() == ErrorKind::Interrupted => {}
Err(error) => return Err(Refusal::Failed(error)),
}
}
Ok(seen)
}
pub(crate) fn read(
path: &Path,
cap: u64,
site: AllocationSite,
) -> core::result::Result<Vec<u8>, Refusal> {
let Opened { mut file, metadata } = open(path, Some(cap))?;
let held = metadata.len();
let room = held as usize + 1;
let mut bytes = alloc::with_capacity::<u8>(room, site).map_err(Refusal::Named)?;
bytes.resize(room, 0);
let mut filled = 0;
while filled < room {
match file.read(&mut bytes[filled..]) {
Ok(0) => break,
Ok(read) => filled += read,
Err(error) if error.kind() == ErrorKind::Interrupted => {}
Err(error) => return Err(Refusal::Failed(error)),
}
}
if filled == room {
let at_least = count_on(&mut file, filled as u64, cap)?;
return Err(Refusal::PastCap(PastCap {
at_least,
held,
cap,
}));
}
bytes.truncate(filled);
Ok(bytes)
}
pub(crate) fn stream(
path: &Path,
cap: Option<u64>,
mut each: impl FnMut(&[u8]),
) -> core::result::Result<Metadata, Refusal> {
let Opened { mut file, metadata } = open(path, cap)?;
let held = metadata.len();
let mut block = vec![0u8; BLOCK];
let mut seen = 0u64;
loop {
let read = match file.read(&mut block) {
Ok(0) => break,
Ok(read) => read,
Err(error) if error.kind() == ErrorKind::Interrupted => continue,
Err(error) => return Err(Refusal::Failed(error)),
};
seen += read as u64;
if seen > held {
return Err(Refusal::PastCap(PastCap {
at_least: seen,
held,
cap: cap.unwrap_or(u64::MAX),
}));
}
each(&block[..read]);
}
Ok(metadata)
}
#[cfg(test)]
mod tests;