use std::{
collections::HashSet,
fs, io,
path::{Component, Path, PathBuf},
};
#[cfg(all(test, unix))]
mod tests;
enum ResolutionStep {
Component(PathBuf),
EndSymlink(PathBuf),
RequireDirectory,
}
pub fn canonicalize_allow_missing(
path: &Path,
base: &Path,
max_symlink_depth: usize,
) -> io::Result<PathBuf> {
let absolute = if path.is_absolute() {
path.to_owned()
} else if base.is_absolute() {
base.join(path)
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"relative path requires an absolute base",
));
};
match absolute.canonicalize() {
Ok(canonical) => return Ok(canonical),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
let mut resolved = PathBuf::new();
let mut missing_depth = 0_usize;
let mut pending = Vec::new();
let mut active_symlinks = HashSet::new();
push_steps(&absolute, &mut pending);
while let Some(step) = pending.pop() {
let path = match step {
ResolutionStep::Component(path) => path,
ResolutionStep::EndSymlink(path) => {
active_symlinks.remove(&path);
continue;
}
ResolutionStep::RequireDirectory => {
if missing_depth == 0 {
resolved = resolved.join(".").canonicalize()?;
}
continue;
}
};
for component in path.components() {
match component {
Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
let candidate = resolved.join(component.as_os_str());
if missing_depth == 0 && matches!(component, Component::Normal(_)) {
match candidate.canonicalize() {
Ok(canonical) => resolved = canonical,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
match fs::symlink_metadata(&candidate) {
Ok(metadata) if metadata.is_symlink() => {
if active_symlinks.len() >= max_symlink_depth
|| !active_symlinks.insert(candidate.clone())
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"missing-target symlink cycle or depth limit",
));
}
let target = fs::read_link(&candidate)?;
pending.push(ResolutionStep::EndSymlink(candidate));
push_steps(&target, &mut pending);
}
Err(error) if error.kind() != io::ErrorKind::NotFound => {
return Err(error);
}
_ => {
resolved = candidate;
missing_depth = 1;
}
}
}
Err(error) => return Err(error),
}
} else {
resolved = candidate;
if matches!(component, Component::Normal(_)) && missing_depth > 0 {
missing_depth += 1;
}
}
}
Component::CurDir => {}
Component::ParentDir => {
if missing_depth == 0 {
resolved = resolved.join(component.as_os_str()).canonicalize()?;
} else {
resolved.pop();
missing_depth -= 1;
}
}
}
}
}
Ok(resolved)
}
fn push_steps(path: &Path, pending: &mut Vec<ResolutionStep>) {
let bytes = path.as_os_str().as_encoded_bytes();
let separator = std::path::MAIN_SEPARATOR as u8;
if bytes.ends_with(b"/")
|| bytes.ends_with(b"/.")
|| bytes.ends_with(&[separator])
|| bytes.ends_with(&[separator, b'.'])
{
pending.push(ResolutionStep::RequireDirectory);
}
pending.extend(
path.components()
.rev()
.map(|component| ResolutionStep::Component(PathBuf::from(component.as_os_str()))),
);
}