use std::collections::VecDeque;
use std::ffi::OsString;
use std::os::fd::OwnedFd;
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use rustix::fs::{AtFlags, FileType, Mode, OFlags};
use std::path::Component;
use super::{FileError, Located};
const MAX_SYMLINKS: usize = 40;
pub(super) fn components(path: &Path) -> Result<Vec<OsString>, FileError> {
let mut out = Vec::new();
for c in path.components() {
match c {
Component::Normal(part) => out.push(part.to_os_string()),
Component::ParentDir => out.push(OsString::from("..")),
Component::CurDir => {}
Component::RootDir | Component::Prefix(_) => return Err(FileError::Outside),
}
}
Ok(out)
}
struct Walk<'a> {
canonical_root: &'a Path,
dirs: Vec<OwnedFd>,
queue: VecDeque<OsString>,
symlinks: usize,
}
pub(super) fn walk(
root: &OwnedFd,
canonical_root: &Path,
relative: &Path,
) -> Result<Located, FileError> {
let start = open_dir(root, ".")?;
let mut walk = Walk {
canonical_root,
dirs: vec![start],
queue: components(relative)?.into(),
symlinks: 0,
};
while let Some(part) = walk.queue.pop_front() {
if let Some(found) = walk.step(part)? {
return Ok(found);
}
}
Err(FileError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"the path names a directory, not a file",
)))
}
impl Walk<'_> {
fn top(&self) -> &OwnedFd {
&self.dirs[self.dirs.len() - 1]
}
fn step(&mut self, part: OsString) -> Result<Option<Located>, FileError> {
if part == ".." {
if self.dirs.len() == 1 {
return Err(FileError::Outside);
}
drop(self.dirs.pop());
return Ok(None);
}
let stat = rustix::fs::statat(self.top(), &part, AtFlags::SYMLINK_NOFOLLOW)
.map_err(std::io::Error::from)?;
let kind = FileType::from_raw_mode(stat.st_mode);
if kind == FileType::Symlink {
self.follow(&part)?;
return Ok(None);
}
if self.queue.is_empty() {
let dir = open_dir(self.top(), ".")?;
return Ok(Some(Located { dir, name: part }));
}
let next = open_dir(self.top(), &part)?;
self.dirs.push(next);
Ok(None)
}
fn follow(&mut self, part: &OsString) -> Result<(), FileError> {
self.symlinks += 1;
if self.symlinks > MAX_SYMLINKS {
return Err(FileError::Io(std::io::Error::from(rustix::io::Errno::LOOP)));
}
let target =
rustix::fs::readlinkat(self.top(), part, Vec::new()).map_err(std::io::Error::from)?;
let target = PathBuf::from(OsString::from_vec(target.into_bytes()));
let rest = if target.is_absolute() {
let inside = target
.strip_prefix(self.canonical_root)
.map_err(|_| FileError::Outside)?;
self.dirs.truncate(1);
components(inside)?
} else {
components(&target)?
};
for c in rest.into_iter().rev() {
self.queue.push_front(c);
}
Ok(())
}
}
fn open_dir(at: &OwnedFd, name: impl rustix::path::Arg) -> Result<OwnedFd, FileError> {
let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
rustix::fs::openat(at, name, flags, Mode::empty())
.map_err(|e| FileError::Io(std::io::Error::from(e)))
}