nu_path/
dots.rs

1#[cfg(windows)]
2use omnipath::WinPathExt;
3use std::path::{Component, Path, PathBuf};
4
5/// Normalize the path, expanding occurrences of n-dots.
6///
7/// It performs the same normalization as `nu_path::components()`, except it also expands n-dots,
8/// such as "..." and "....", into multiple "..".
9///
10/// The resulting path will use platform-specific path separators, regardless of what path separators was used in the input.
11pub fn expand_ndots(path: impl AsRef<Path>) -> PathBuf {
12    // Returns whether a path component is n-dots.
13    fn is_ndots(s: &std::ffi::OsStr) -> bool {
14        s.as_encoded_bytes().iter().all(|c| *c == b'.') && s.len() >= 3
15    }
16
17    let path = path.as_ref();
18
19    let mut result = PathBuf::with_capacity(path.as_os_str().len());
20    for component in crate::components(path) {
21        match component {
22            Component::Normal(s) if is_ndots(s) => {
23                let n = s.len();
24                // Push ".." to the path (n - 1) times.
25                for _ in 0..n - 1 {
26                    result.push("..");
27                }
28            }
29            _ => result.push(component),
30        }
31    }
32
33    result
34}
35
36/// Normalize the path, expanding occurrences of "." and "..".
37///
38/// It performs the same normalization as `nu_path::components()`, except it also expands ".."
39/// when its preceding component is a normal component, ignoring the possibility of symlinks.
40/// In other words, it operates on the lexical structure of the path.
41///
42/// This won't expand "/.." even though the parent directory of "/" is often
43/// considered to be itself.
44///
45/// The resulting path will use platform-specific path separators, regardless of what path separators was used in the input.
46pub fn expand_dots(path: impl AsRef<Path>) -> PathBuf {
47    // Check if the last component of the path is a normal component.
48    fn last_component_is_normal(path: &Path) -> bool {
49        matches!(path.components().last(), Some(Component::Normal(_)))
50    }
51
52    let path = path.as_ref();
53
54    let mut result = PathBuf::with_capacity(path.as_os_str().len());
55    for component in crate::components(path) {
56        match component {
57            Component::ParentDir if last_component_is_normal(&result) => {
58                result.pop();
59            }
60            Component::CurDir if last_component_is_normal(&result) => {
61                // no-op
62            }
63            _ => {
64                let prev_component = result.components().last();
65                if prev_component == Some(Component::RootDir) && component == Component::ParentDir {
66                    continue;
67                }
68                result.push(component)
69            }
70        }
71    }
72
73    simiplified(&result)
74}
75
76/// Expand ndots, but only if it looks like it probably contains them, because there is some lossy
77/// path normalization that happens.
78pub fn expand_ndots_safe(path: impl AsRef<Path>) -> PathBuf {
79    let string = path.as_ref().to_string_lossy();
80
81    // Use ndots if it contains at least `...`, since that's the minimum trigger point.
82    // Don't use it if it contains ://, because that looks like a URL scheme and the path normalization
83    // will mess with that.
84    // Don't use it if it starts with `./`, as to not break golang wildcard syntax
85    // (since generally you're probably not using `./` with ndots)
86    if string.contains("...") && !string.contains("://") && !string.starts_with("./") {
87        expand_ndots(path)
88    } else {
89        path.as_ref().to_owned()
90    }
91}
92
93#[cfg(windows)]
94fn simiplified(path: &std::path::Path) -> PathBuf {
95    path.to_winuser_path()
96        .unwrap_or_else(|_| path.to_path_buf())
97}
98
99#[cfg(not(windows))]
100fn simiplified(path: &std::path::Path) -> PathBuf {
101    path.to_path_buf()
102}
103
104#[cfg(test)]
105mod test_expand_ndots {
106    use super::*;
107    use crate::assert_path_eq;
108
109    #[test]
110    fn empty_path() {
111        let path = Path::new("");
112        assert_path_eq!(expand_ndots(path), "");
113    }
114
115    #[test]
116    fn root_dir() {
117        let path = Path::new("/");
118        let expected = if cfg!(windows) { "\\" } else { "/" };
119        assert_path_eq!(expand_ndots(path), expected);
120    }
121
122    #[test]
123    fn two_dots() {
124        let path = Path::new("..");
125        assert_path_eq!(expand_ndots(path), "..");
126    }
127
128    #[test]
129    fn three_dots() {
130        let path = Path::new("...");
131        let expected = if cfg!(windows) { r"..\.." } else { "../.." };
132        assert_path_eq!(expand_ndots(path), expected);
133    }
134
135    #[test]
136    fn five_dots() {
137        let path = Path::new(".....");
138        let expected = if cfg!(windows) {
139            r"..\..\..\.."
140        } else {
141            "../../../.."
142        };
143        assert_path_eq!(expand_ndots(path), expected);
144    }
145
146    #[test]
147    fn three_dots_with_trailing_slash() {
148        let path = Path::new("/tmp/.../");
149        let expected = if cfg!(windows) {
150            r"\tmp\..\..\"
151        } else {
152            "/tmp/../../"
153        };
154        assert_path_eq!(expand_ndots(path), expected);
155    }
156
157    #[test]
158    fn filenames_with_dots() {
159        let path = Path::new("...foo.../");
160        let expected = if cfg!(windows) {
161            r"...foo...\"
162        } else {
163            "...foo.../"
164        };
165        assert_path_eq!(expand_ndots(path), expected);
166    }
167
168    #[test]
169    fn multiple_ndots() {
170        let path = Path::new("..././...");
171        let expected = if cfg!(windows) {
172            r"..\..\..\.."
173        } else {
174            "../../../.."
175        };
176        assert_path_eq!(expand_ndots(path), expected);
177    }
178
179    #[test]
180    fn trailing_dots() {
181        let path = Path::new("/foo/bar/..");
182        let expected = if cfg!(windows) {
183            r"\foo\bar\.."
184        } else {
185            "/foo/bar/.."
186        };
187        assert_path_eq!(expand_ndots(path), expected);
188    }
189
190    #[test]
191    fn leading_dot_slash() {
192        let path = Path::new("./...");
193        assert_path_eq!(expand_ndots_safe(path), "./...");
194    }
195}
196
197#[cfg(test)]
198mod test_expand_dots {
199    use super::*;
200    use crate::assert_path_eq;
201
202    #[test]
203    fn empty_path() {
204        let path = Path::new("");
205        assert_path_eq!(expand_dots(path), "");
206    }
207
208    #[test]
209    fn single_dot() {
210        let path = Path::new("./");
211        let expected = if cfg!(windows) { r".\" } else { "./" };
212        assert_path_eq!(expand_dots(path), expected);
213    }
214
215    #[test]
216    fn more_single_dots() {
217        let path = Path::new("././.");
218        let expected = ".";
219        assert_path_eq!(expand_dots(path), expected);
220    }
221
222    #[test]
223    fn double_dots() {
224        let path = Path::new("../../..");
225        let expected = if cfg!(windows) {
226            r"..\..\.."
227        } else {
228            "../../.."
229        };
230        assert_path_eq!(expand_dots(path), expected);
231    }
232
233    #[test]
234    fn backtrack_once() {
235        let path = Path::new("/foo/bar/../baz/");
236        let expected = if cfg!(windows) {
237            r"\foo\baz\"
238        } else {
239            "/foo/baz/"
240        };
241        assert_path_eq!(expand_dots(path), expected);
242    }
243
244    #[test]
245    fn backtrack_to_root() {
246        let path = Path::new("/foo/bar/../../../../baz");
247        let expected = if cfg!(windows) { r"\baz" } else { "/baz" };
248        assert_path_eq!(expand_dots(path), expected);
249    }
250}