use std::io;
use std::path::{Path, PathBuf};
pub fn physical(path: &Path) -> io::Result<PathBuf> {
let mut missing = Vec::new();
let mut current = path;
loop {
match std::fs::canonicalize(current) {
Ok(resolved) => {
let resolved = simplified(resolved);
return Ok(missing
.iter()
.rev()
.fold(resolved, |acc, part: &&std::ffi::OsStr| acc.join(part)));
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
if std::fs::symlink_metadata(current).is_ok() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{} is a dangling symlink", current.display()),
));
}
let (Some(parent), Some(name)) = (current.parent(), current.file_name()) else {
return Err(error);
};
missing.push(name);
current = parent;
}
Err(error) => return Err(error),
}
}
}
#[cfg(windows)]
fn simplified(path: PathBuf) -> PathBuf {
let text = path.to_string_lossy();
if let Some(rest) = text.strip_prefix(r"\\?\")
&& rest.as_bytes().get(1) == Some(&b':')
&& rest.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
{
return PathBuf::from(rest);
}
path
}
#[cfg(not(windows))]
const fn simplified(path: PathBuf) -> PathBuf {
path
}
#[must_use]
pub fn git_root(physical: &Path) -> Option<PathBuf> {
physical
.ancestors()
.find(|dir| dir.join(".git").symlink_metadata().is_ok())
.map(Path::to_path_buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(windows)]
#[test]
fn a_physical_path_is_written_as_people_write_it() {
let dir = tempfile::tempdir().expect("dir");
let place = physical(&dir.path().join("skills")).expect("physical");
assert!(
!place.to_string_lossy().starts_with(r"\\?\"),
"{}",
place.display()
);
}
#[cfg(not(windows))]
#[test]
fn a_physical_path_resolves_symlinks_only() {
let dir = tempfile::tempdir().expect("dir");
let resolved = std::fs::canonicalize(dir.path()).expect("canonical");
assert_eq!(
physical(&dir.path().join("skills")).expect("physical"),
resolved.join("skills")
);
}
}