Skip to main content

lisette_semantics/
path.rs

1use std::path::{Component, Path};
2
3/// Returns path relative to the cwd as a forward-slash string.
4/// Returns None if the cwd is unknown or the path lies outside it.
5pub 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}