use std::ffi::OsString;
use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};
fn clean(path: &Path) -> PathBuf {
let mut parts: Vec<Component<'_>> = Vec::new();
for c in path.components() {
match c {
Component::CurDir => {}
Component::ParentDir => match parts.last() {
Some(Component::Normal(_)) => {
parts.pop();
}
Some(Component::RootDir | Component::Prefix(_)) => {}
_ => parts.push(c),
},
other => parts.push(other),
}
}
let mut out = PathBuf::new();
for c in parts {
out.push(c.as_os_str());
}
if out.as_os_str().is_empty() {
out.push(".");
}
out
}
pub(crate) fn absolute(path: &Path) -> io::Result<PathBuf> {
if path.is_absolute() {
return Ok(clean(path));
}
Ok(clean(&std::env::current_dir()?.join(path)))
}
pub(crate) fn home() -> io::Result<PathBuf> {
std::env::home_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "the home directory is not known"))
}
pub(crate) fn same_dir(a: &Path, b: &Path) -> bool {
a == b || matches!((fs::canonicalize(a), fs::canonicalize(b)), (Ok(ra), Ok(rb)) if ra == rb)
}
pub(crate) fn same_file(a: &Path, b: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt as _;
match (fs::metadata(a), fs::metadata(b)) {
(Ok(x), Ok(y)) => x.dev() == y.dev() && x.ino() == y.ino(),
_ => false,
}
}
#[cfg(not(unix))]
{
matches!((fs::canonicalize(a), fs::canonicalize(b)), (Ok(x), Ok(y)) if x == y)
}
}
const MAX_LINK_HOPS: u32 = 64;
pub(crate) fn real(path: &Path) -> io::Result<PathBuf> {
let abs = absolute(path)?;
let mut at = abs.clone();
let mut rest: Vec<OsString> = Vec::new();
let mut hops = 0;
loop {
match fs::canonicalize(&at) {
Ok(resolved) => {
return Ok(rest.iter().rev().fold(resolved, |acc, part| acc.join(part)));
}
Err(e) if e.kind() != io::ErrorKind::NotFound => return Err(e),
Err(_) => {}
}
if let Ok(target) = fs::read_link(&at) {
hops += 1;
if hops > MAX_LINK_HOPS {
return Err(io::Error::other(format!(
"{}: too many symbolic links",
path.display()
)));
}
at = if target.is_absolute() {
clean(&target)
} else {
clean(&at.parent().unwrap_or(Path::new("/")).join(target))
};
continue;
}
let Some(parent) = at.parent() else {
return Ok(abs);
};
let Some(name) = at.file_name() else {
return Ok(abs);
};
rest.push(name.to_owned());
at = parent.to_path_buf();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clean_resolves_parents_lexically() {
assert_eq!(clean(Path::new("/a/b/../c")), Path::new("/a/c"));
assert_eq!(clean(Path::new("/a/./b/")), Path::new("/a/b"));
assert_eq!(clean(Path::new("/../..")), Path::new("/"));
assert_eq!(clean(Path::new("a/../..")), Path::new(".."));
assert_eq!(clean(Path::new("")), Path::new("."));
}
#[test]
fn starts_with_is_component_wise() {
assert!(!Path::new("/a/project-notes").starts_with("/a/project"));
assert!(Path::new("/a/project/x").starts_with("/a/project"));
}
}