lisette_semantics/
path.rs1use std::path::{Component, Path};
2
3pub fn relative_to_cwd(path: &Path) -> Option<String> {
6 relative_to_cwd_with(path, std::env::current_dir().ok().as_deref())
7}
8
9pub fn relative_to_cwd_with(path: &Path, cwd: Option<&Path>) -> Option<String> {
10 let cwd = cwd?;
11 let absolute = if path.is_absolute() {
12 path.to_path_buf()
13 } else {
14 cwd.join(path)
15 };
16
17 if let (Ok(base), Ok(target)) = (cwd.canonicalize(), absolute.canonicalize()) {
18 return relativize(target.strip_prefix(&base).ok()?);
19 }
20 relativize(absolute.strip_prefix(cwd).ok()?)
21}
22
23fn relativize(rel: &Path) -> Option<String> {
24 let mut segments = Vec::new();
25 for component in rel.components() {
26 match component {
27 Component::CurDir => {}
28 Component::Normal(segment) => segments.push(segment.to_str()?),
29 _ => return None,
30 }
31 }
32 (!segments.is_empty()).then(|| segments.join("/"))
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38 use std::fs as stdfs;
39
40 #[test]
41 fn plain_path_inside_cwd() {
42 let tmp = tempfile::tempdir().unwrap();
43 let cwd = tmp.path();
44 assert_eq!(
45 relative_to_cwd_with(&cwd.join("src/main.lis"), Some(cwd)),
46 Some("src/main.lis".to_string())
47 );
48 }
49
50 #[test]
51 fn file_at_cwd_root() {
52 let tmp = tempfile::tempdir().unwrap();
53 let cwd = tmp.path();
54 assert_eq!(
55 relative_to_cwd_with(&cwd.join("main.lis"), Some(cwd)),
56 Some("main.lis".to_string())
57 );
58 }
59
60 #[test]
61 fn strips_leading_dot_slash() {
62 let tmp = tempfile::tempdir().unwrap();
63 let cwd = tmp.path();
64 assert_eq!(
65 relative_to_cwd_with(&cwd.join("./src/main.lis"), Some(cwd)),
66 Some("src/main.lis".to_string())
67 );
68 }
69
70 #[test]
71 fn strips_mid_path_dot() {
72 let tmp = tempfile::tempdir().unwrap();
73 let cwd = tmp.path();
74 assert_eq!(
75 relative_to_cwd_with(&cwd.join("src/./main.lis"), Some(cwd)),
76 Some("src/main.lis".to_string())
77 );
78 }
79
80 #[test]
81 fn path_outside_cwd_returns_none() {
82 let tmp = tempfile::tempdir().unwrap();
83 let other = tempfile::tempdir().unwrap();
84 assert_eq!(
85 relative_to_cwd_with(&other.path().join("main.lis"), Some(tmp.path())),
86 None
87 );
88 }
89
90 #[test]
91 fn unknown_cwd_returns_none() {
92 assert_eq!(
93 relative_to_cwd_with(Path::new("/any/path/main.lis"), None),
94 None
95 );
96 }
97
98 #[cfg(unix)]
99 #[test]
100 fn absolute_path_under_symlinked_cwd_strips() {
101 let tmp = tempfile::tempdir().unwrap();
102 let real = tmp.path().join("real");
103 stdfs::create_dir_all(real.join("src")).unwrap();
104 stdfs::write(real.join("src/main.lis"), "").unwrap();
105 let link = tmp.path().join("link");
106 std::os::unix::fs::symlink(&real, &link).unwrap();
107
108 assert_eq!(
109 relative_to_cwd_with(&real.join("src/main.lis"), Some(&link)),
110 Some("src/main.lis".to_string())
111 );
112 }
113}