tinymist_std/
path.rs

1//! Path utilities.
2
3use std::borrow::Cow;
4use std::path::{Component, Path, PathBuf};
5
6pub use path_clean::PathClean;
7
8/// Gets the path cleaned as a unix-style string.
9pub fn unix_slash(root: &Path) -> String {
10    let mut res = String::with_capacity(root.as_os_str().len());
11    let mut parent_norm = false;
12    for comp in root.components() {
13        match comp {
14            Component::Prefix(p) => {
15                res.push_str(&p.as_os_str().to_string_lossy());
16                parent_norm = false;
17            }
18            Component::RootDir => {
19                res.push('/');
20                parent_norm = false;
21            }
22            Component::CurDir => {
23                parent_norm = false;
24            }
25            Component::ParentDir => {
26                if parent_norm {
27                    res.push('/');
28                }
29                res.push_str("..");
30                parent_norm = true;
31            }
32            Component::Normal(p) => {
33                if parent_norm {
34                    res.push('/');
35                }
36                res.push_str(&p.to_string_lossy());
37                parent_norm = true;
38            }
39        }
40    }
41
42    if res.is_empty() {
43        res.push('.');
44    }
45
46    res
47}
48
49/// Gets the path cleaned as a platform-style string.
50pub use path_clean::clean;
51
52/// Construct a relative path from a provided base directory path to the
53/// provided path.
54pub fn diff(path: &Path, base: &Path) -> Option<PathBuf> {
55    // Because of <https://github.com/Manishearth/pathdiff/issues/8>, we have to clean the path
56    // before diff.
57    fn clean_for_diff(p: &Path) -> Cow<'_, Path> {
58        if p.components()
59            .any(|c| matches!(c, Component::ParentDir | Component::CurDir))
60        {
61            Cow::Owned(p.clean())
62        } else {
63            Cow::Borrowed(p)
64        }
65    }
66
67    pathdiff::diff_paths(clean_for_diff(path).as_ref(), clean_for_diff(base).as_ref())
68}
69
70#[cfg(test)]
71mod test {
72    use std::path::{Path, PathBuf};
73
74    use super::{clean as inner_path_clean, unix_slash, PathClean};
75
76    pub fn clean<P: AsRef<Path>>(path: P) -> String {
77        unix_slash(&inner_path_clean(path))
78    }
79
80    #[test]
81    fn test_unix_slash() {
82        if cfg!(target_os = "windows") {
83            // windows group
84            assert_eq!(
85                unix_slash(std::path::Path::new("C:\\Users\\a\\b\\c")),
86                "C:/Users/a/b/c"
87            );
88            assert_eq!(
89                unix_slash(std::path::Path::new("C:\\Users\\a\\b\\c\\")),
90                "C:/Users/a/b/c"
91            );
92            assert_eq!(unix_slash(std::path::Path::new("a\\b\\c")), "a/b/c");
93            assert_eq!(unix_slash(std::path::Path::new("C:\\")), "C:/");
94            assert_eq!(unix_slash(std::path::Path::new("C:\\\\")), "C:/");
95            assert_eq!(unix_slash(std::path::Path::new("C:")), "C:");
96            assert_eq!(unix_slash(std::path::Path::new("C:\\a")), "C:/a");
97            assert_eq!(unix_slash(std::path::Path::new("C:\\a\\")), "C:/a");
98            assert_eq!(unix_slash(std::path::Path::new("C:\\a\\b")), "C:/a/b");
99            assert_eq!(
100                unix_slash(std::path::Path::new("C:\\Users\\a\\..\\b\\c")),
101                "C:/Users/a/../b/c"
102            );
103            assert_eq!(
104                unix_slash(std::path::Path::new("C:\\Users\\a\\..\\b\\c\\")),
105                "C:/Users/a/../b/c"
106            );
107            assert_eq!(
108                unix_slash(std::path::Path::new("C:\\Users\\a\\..\\..")),
109                "C:/Users/a/../.."
110            );
111            assert_eq!(
112                unix_slash(std::path::Path::new("C:\\Users\\a\\..\\..\\")),
113                "C:/Users/a/../.."
114            );
115        }
116        // unix group
117        assert_eq!(unix_slash(std::path::Path::new("/a/b/c")), "/a/b/c");
118        assert_eq!(unix_slash(std::path::Path::new("/a/b/c/")), "/a/b/c");
119        assert_eq!(unix_slash(std::path::Path::new("/")), "/");
120        assert_eq!(unix_slash(std::path::Path::new("//")), "/");
121        assert_eq!(unix_slash(std::path::Path::new("a")), "a");
122        assert_eq!(unix_slash(std::path::Path::new("a/")), "a");
123        assert_eq!(unix_slash(std::path::Path::new("a/b")), "a/b");
124        assert_eq!(unix_slash(std::path::Path::new("a/b/")), "a/b");
125        assert_eq!(unix_slash(std::path::Path::new("a/..")), "a/..");
126        assert_eq!(unix_slash(std::path::Path::new("a/../")), "a/..");
127        assert_eq!(unix_slash(std::path::Path::new("a/../..")), "a/../..");
128        assert_eq!(unix_slash(std::path::Path::new("a/../../")), "a/../..");
129        assert_eq!(unix_slash(std::path::Path::new("a/./b")), "a/b");
130        assert_eq!(unix_slash(std::path::Path::new("a/./b/")), "a/b");
131        assert_eq!(unix_slash(std::path::Path::new(".")), ".");
132        assert_eq!(unix_slash(std::path::Path::new("./")), ".");
133        assert_eq!(unix_slash(std::path::Path::new("./a")), "a");
134        assert_eq!(unix_slash(std::path::Path::new("./a/")), "a");
135        assert_eq!(unix_slash(std::path::Path::new("./a/b")), "a/b");
136        assert_eq!(unix_slash(std::path::Path::new("./a/b/")), "a/b");
137        assert_eq!(unix_slash(std::path::Path::new("./a/./b/")), "a/b");
138    }
139
140    #[test]
141    fn test_path_clean_empty_path_is_current_dir() {
142        assert_eq!(clean(""), ".");
143    }
144
145    #[test]
146    fn test_path_clean_clean_paths_dont_change() {
147        let tests = vec![(".", "."), ("..", ".."), ("/", "/")];
148
149        for test in tests {
150            assert_eq!(clean(test.0), test.1);
151        }
152    }
153
154    #[test]
155    fn test_path_clean_replace_multiple_slashes() {
156        let tests = vec![
157            ("/", "/"),
158            ("//", "/"),
159            ("///", "/"),
160            (".//", "."),
161            ("//..", "/"),
162            ("..//", ".."),
163            ("/..//", "/"),
164            ("/.//./", "/"),
165            ("././/./", "."),
166            ("path//to///thing", "path/to/thing"),
167            ("/path//to///thing", "/path/to/thing"),
168        ];
169
170        for test in tests {
171            assert_eq!(clean(test.0), test.1);
172        }
173    }
174
175    #[test]
176    fn test_path_clean_eliminate_current_dir() {
177        let tests = vec![
178            ("./", "."),
179            ("/./", "/"),
180            ("./test", "test"),
181            ("./test/./path", "test/path"),
182            ("/test/./path/", "/test/path"),
183            ("test/path/.", "test/path"),
184        ];
185
186        for test in tests {
187            assert_eq!(clean(test.0), test.1);
188        }
189    }
190
191    #[test]
192    fn test_path_clean_eliminate_parent_dir() {
193        let tests = vec![
194            ("/..", "/"),
195            ("/../test", "/test"),
196            ("test/..", "."),
197            ("test/path/..", "test"),
198            ("test/../path", "path"),
199            ("/test/../path", "/path"),
200            ("test/path/../../", "."),
201            ("test/path/../../..", ".."),
202            ("/test/path/../../..", "/"),
203            ("/test/path/../../../..", "/"),
204            ("test/path/../../../..", "../.."),
205            ("test/path/../../another/path", "another/path"),
206            ("test/path/../../another/path/..", "another"),
207            ("../test", "../test"),
208            ("../test/", "../test"),
209            ("../test/path", "../test/path"),
210            ("../test/..", ".."),
211        ];
212
213        for test in tests {
214            assert_eq!(clean(test.0), test.1);
215        }
216    }
217
218    #[test]
219    fn test_path_clean_pathbuf_trait() {
220        assert_eq!(
221            unix_slash(&PathBuf::from("/test/../path/").clean()),
222            "/path"
223        );
224    }
225
226    #[test]
227    fn test_path_clean_path_trait() {
228        assert_eq!(unix_slash(&Path::new("/test/../path/").clean()), "/path");
229    }
230
231    #[test]
232    #[cfg(target_os = "windows")]
233    fn test_path_clean_windows_paths() {
234        let tests = vec![
235            ("\\..", "/"),
236            ("\\..\\test", "/test"),
237            ("test\\..", "."),
238            ("test\\path\\..\\..\\..", ".."),
239            ("test\\path/..\\../another\\path", "another/path"), // Mixed
240            ("test\\path\\my/path", "test/path/my/path"),        // Mixed 2
241            ("/dir\\../otherDir/test.json", "/otherDir/test.json"), // User example
242            ("c:\\test\\..", "c:/"),                             // issue #12
243            ("c:/test/..", "c:/"),                               // issue #12
244        ];
245
246        for test in tests {
247            assert_eq!(clean(test.0), test.1);
248        }
249    }
250}