use core::{iter::repeat_n, slice};
use std::{
borrow::Cow,
env::current_dir,
fs,
path::{Component, MAIN_SEPARATOR_STR, Path, PathBuf, Prefix},
};
pub trait RelativePath {
fn relative_to<P: AsRef<Path>>(&self, source: P) -> PathBuf;
fn relative_to_current_dir(&self) -> PathBuf;
fn opinionated_display(&self) -> String;
}
impl<P: AsRef<Path>> RelativePath for P {
fn relative_to<P2: AsRef<Path>>(&self, source: P2) -> PathBuf {
fn naive_normalize(path: &Path) -> Vec<Component<'_>> {
let mut output = Vec::new();
for component in path.components() {
match &component {
Component::ParentDir => {
output.pop();
}
Component::CurDir => {}
_ => output.push(component),
}
}
output
}
let source =
fs::canonicalize(source.as_ref()).unwrap_or_else(|_| source.as_ref().to_path_buf());
let target =
fs::canonicalize(self.as_ref()).unwrap_or_else(|_| self.as_ref().to_path_buf());
let source_components = naive_normalize(&source);
let target_components = naive_normalize(&target);
let diverge_index = {
let mut index = 0;
for source_component in source_components.iter() {
let Some(target_component) = target_components.get(index) else {
break;
};
if source_component != target_component {
break;
}
index += 1;
}
index
};
let output_components: Vec<_> = repeat_n(
&Component::ParentDir,
source_components.len() - diverge_index,
)
.chain(target_components.get(diverge_index..).unwrap_or_default())
.collect();
if output_components.is_empty() {
PathBuf::from_iter(&[Component::CurDir])
} else {
PathBuf::from_iter(output_components)
}
}
fn relative_to_current_dir(&self) -> PathBuf {
let Ok(current_dir) = current_dir() else {
return self.as_ref().to_path_buf();
};
self.relative_to(current_dir)
}
fn opinionated_display(&self) -> String {
if self.as_ref() == Path::new("") {
".".to_string()
} else {
let path = self.as_ref();
let has_prefix = path
.components()
.any(|component| matches!(component, Component::Prefix(_)));
let separator = if has_prefix { MAIN_SEPARATOR_STR } else { "/" };
self.as_ref()
.components()
.filter_map(|component| match component {
Component::Prefix(prefix_component) => match prefix_component.kind() {
Prefix::VerbatimUNC(hostname, share) | Prefix::UNC(hostname, share) => {
Some(Cow::Owned(format!(
"\\\\{}\\{}",
hostname.to_string_lossy(),
share.to_string_lossy()
)))
}
Prefix::VerbatimDisk(disk) | Prefix::Disk(disk) => {
let letter = str::from_utf8(slice::from_ref(&disk)).unwrap_or("C");
Some(Cow::Owned(format!("{letter}:")))
}
Prefix::DeviceNS(namespace) => Some(Cow::Owned(format!(
"\\\\.\\{}",
namespace.to_string_lossy()
))),
Prefix::Verbatim(os_str) => Some(os_str.to_string_lossy()),
},
Component::RootDir => {
if has_prefix {
None
} else {
Some(Cow::Borrowed(""))
}
}
Component::CurDir => Some(Cow::Borrowed(".")),
Component::ParentDir => Some(Cow::Borrowed("..")),
Component::Normal(os_str) => Some(os_str.to_string_lossy()),
})
.collect::<Vec<_>>()
.join(separator)
}
}
}
#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};
use crate::path::RelativePath;
#[test]
fn relative_to() {
let source = Path::new("/root/dir-a/dir-b");
let target = Path::new("/root/dir-c/dir-d");
assert_eq!(
PathBuf::from("../../dir-c/dir-d"),
target.relative_to(source)
);
let source = Path::new("dir-a/dir-b");
let target = Path::new("dir-a/dir-b");
assert_eq!(PathBuf::from("."), target.relative_to(source));
let source = Path::new("../dir-a/dir-b");
let target = Path::new("./dir-a/./dir-b");
assert_eq!(PathBuf::from("."), target.relative_to(source));
}
}