1use core::iter::repeat_n;
4use std::{
5 env::current_dir,
6 fs,
7 path::{Component, Path, PathBuf},
8};
9
10use crate::NormalizePath;
11
12pub trait RelativePath {
14 fn relative_to(&self, source: &Path) -> PathBuf;
16
17 fn relative_to_cwd(&self) -> PathBuf {
19 let current_dur = current_dir().unwrap_or_else(|_| PathBuf::from("./"));
20 self.relative_to(¤t_dur)
21 }
22}
23
24impl<P: AsRef<Path>> RelativePath for P {
25 fn relative_to(&self, source: &Path) -> PathBuf {
26 relative_path(source, self.as_ref())
27 }
28}
29
30pub fn relative_path(source: &Path, target: &Path) -> PathBuf {
32 let source = fs::canonicalize(source)
33 .unwrap_or_else(|_| source.to_path_buf())
34 .normalized();
35 let source: Vec<_> = source.components().collect();
36
37 let target = fs::canonicalize(target)
38 .unwrap_or_else(|_| target.to_path_buf())
39 .normalized();
40 let target: Vec<_> = target.components().collect();
41
42 let diverge_index = {
43 let mut index = 0;
44
45 for source_component in source.iter() {
46 let Some(target_component) = target.get(index) else {
47 break;
48 };
49
50 if source_component != target_component {
51 break;
52 }
53
54 index += 1;
55 }
56
57 index
58 };
59
60 let output_components: Vec<_> = repeat_n(&Component::ParentDir, source.len() - diverge_index)
61 .chain(target.get(diverge_index..).unwrap_or_default())
62 .collect();
63
64 if output_components.is_empty() {
65 PathBuf::from_iter(&[Component::CurDir])
66 } else {
67 PathBuf::from_iter(output_components)
68 }
69}
70
71#[cfg(test)]
72mod test {
73 use std::path::{Path, PathBuf};
74
75 use crate::relative::RelativePath;
76
77 #[test]
78 fn handles_relative() {
79 let source = Path::new("/root/dir-a/dir-b");
80 let target = Path::new("/root/dir-c/dir-d");
81 assert_eq!(
82 PathBuf::from("../../dir-c/dir-d"),
83 target.relative_to(source)
84 );
85
86 let source = Path::new("dir-a/dir-b");
87 let target = Path::new("dir-a/dir-b");
88 assert_eq!(PathBuf::from("."), target.relative_to(source));
89
90 let source = Path::new("../dir-a/dir-b");
91 let target = Path::new("./dir-a/./dir-b");
92 assert_eq!(
93 PathBuf::from("../../../dir-a/dir-b"),
94 target.relative_to(source)
95 );
96 }
97
98 #[test]
99 fn handles_current_dir() {
100 let target = Path::new("../ts-ansi/src/lib.rs")
101 .canonicalize()
102 .expect("canonicalize to succeed");
103 assert_eq!(Path::new("../ts-ansi/src/lib.rs"), target.relative_to_cwd());
104 }
105}