1#[cfg(windows)]
2use omnipath::WinPathExt;
3use std::path::{Component, Path, PathBuf, Prefix};
4
5pub fn expand_ndots(path: impl AsRef<Path>) -> PathBuf {
12 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 let mut has_special_prefix = false;
21 for component in crate::components(path) {
22 match component {
23 Component::Normal(s) if is_ndots(s) => {
24 let n = s.len();
25 for _ in 0..n - 1 {
27 result.push("..");
28 }
29 }
30 Component::Prefix(prefix) => {
31 match prefix.kind() {
32 Prefix::Disk(_) => {
33 }
36 _ => {
37 has_special_prefix = true;
38 }
39 }
40 result.push(component)
41 }
42 Component::RootDir if has_special_prefix => {
43 }
45 _ => result.push(component),
46 }
47 }
48
49 result
50}
51
52pub fn expand_dots(path: impl AsRef<Path>) -> PathBuf {
63 fn last_component_is_normal(path: &Path) -> bool {
65 matches!(path.components().next_back(), Some(Component::Normal(_)))
66 }
67
68 let path = path.as_ref();
69
70 let mut has_special_prefix = false;
71 let mut result = PathBuf::with_capacity(path.as_os_str().len());
72 for component in crate::components(path) {
73 match component {
74 Component::ParentDir if last_component_is_normal(&result) => {
75 result.pop();
76 }
77 Component::CurDir if last_component_is_normal(&result) => {
78 }
80 Component::Prefix(prefix) => {
81 match prefix.kind() {
82 Prefix::Disk(_) => {
83 }
86 _ => {
87 has_special_prefix = true;
88 }
89 }
90 result.push(component)
91 }
92 Component::RootDir if has_special_prefix => {
93 }
95 _ => {
96 let prev_component = result.components().next_back();
97 if prev_component == Some(Component::RootDir) && component == Component::ParentDir {
98 continue;
99 }
100 result.push(component)
101 }
102 }
103 }
104
105 simiplified(&result)
106}
107
108pub fn expand_ndots_safe(path: impl AsRef<Path>) -> PathBuf {
111 let string = path.as_ref().to_string_lossy();
112
113 if string.contains("...") && !string.contains("://") && !string.starts_with("./") {
119 expand_ndots(path)
120 } else {
121 path.as_ref().to_owned()
122 }
123}
124
125#[cfg(windows)]
126fn simiplified(path: &std::path::Path) -> PathBuf {
127 path.to_winuser_path()
128 .unwrap_or_else(|_| path.to_path_buf())
129}
130
131#[cfg(not(windows))]
132fn simiplified(path: &std::path::Path) -> PathBuf {
133 path.to_path_buf()
134}
135
136#[cfg(test)]
137mod test_expand_ndots {
138 use super::*;
139 use crate::assert_path_eq;
140
141 #[test]
142 fn empty_path() {
143 let path = Path::new("");
144 assert_path_eq!(expand_ndots(path), "");
145 }
146
147 #[test]
148 fn root_dir() {
149 let path = Path::new("/");
150 let expected = if cfg!(windows) { "\\" } else { "/" };
151 assert_path_eq!(expand_ndots(path), expected);
152 }
153
154 #[test]
155 fn two_dots() {
156 let path = Path::new("..");
157 assert_path_eq!(expand_ndots(path), "..");
158 }
159
160 #[test]
161 fn three_dots() {
162 let path = Path::new("...");
163 let expected = if cfg!(windows) { r"..\.." } else { "../.." };
164 assert_path_eq!(expand_ndots(path), expected);
165 }
166
167 #[test]
168 fn five_dots() {
169 let path = Path::new(".....");
170 let expected = if cfg!(windows) {
171 r"..\..\..\.."
172 } else {
173 "../../../.."
174 };
175 assert_path_eq!(expand_ndots(path), expected);
176 }
177
178 #[test]
179 fn three_dots_with_trailing_slash() {
180 let path = Path::new("/tmp/.../");
181 let expected = if cfg!(windows) {
182 r"\tmp\..\..\"
183 } else {
184 "/tmp/../../"
185 };
186 assert_path_eq!(expand_ndots(path), expected);
187 }
188
189 #[test]
190 fn filenames_with_dots() {
191 let path = Path::new("...foo.../");
192 let expected = if cfg!(windows) {
193 r"...foo...\"
194 } else {
195 "...foo.../"
196 };
197 assert_path_eq!(expand_ndots(path), expected);
198 }
199
200 #[test]
201 fn multiple_ndots() {
202 let path = Path::new("..././...");
203 let expected = if cfg!(windows) {
204 r"..\..\..\.."
205 } else {
206 "../../../.."
207 };
208 assert_path_eq!(expand_ndots(path), expected);
209 }
210
211 #[test]
212 fn trailing_dots() {
213 let path = Path::new("/foo/bar/..");
214 let expected = if cfg!(windows) {
215 r"\foo\bar\.."
216 } else {
217 "/foo/bar/.."
218 };
219 assert_path_eq!(expand_ndots(path), expected);
220 }
221
222 #[test]
223 fn leading_dot_slash() {
224 let path = Path::new("./...");
225 assert_path_eq!(expand_ndots_safe(path), "./...");
226 }
227
228 #[test]
229 fn unc_share_no_dots() {
230 let path = Path::new(r"\\server\share");
231 assert_path_eq!(expand_ndots(path), path);
232 }
233
234 #[test]
235 fn unc_file_no_dots() {
236 let path = Path::new(r"\\server\share\dir\file.nu");
237 assert_path_eq!(expand_ndots(path), path);
238 }
239
240 #[test]
241 fn verbatim_no_dots() {
242 let path = Path::new(r"\\?\pictures\elephants");
243 assert_path_eq!(expand_ndots(path), path);
244 }
245
246 #[test]
247 fn verbatim_unc_share_no_dots() {
248 let path = Path::new(r"\\?\UNC\server\share");
249 assert_path_eq!(expand_ndots(path), path);
250 }
251
252 #[test]
253 fn verbatim_unc_file_no_dots() {
254 let path = Path::new(r"\\?\UNC\server\share\dir\file.nu");
255 assert_path_eq!(expand_ndots(path), path);
256 }
257
258 #[test]
259 fn verbatim_disk_no_dots() {
260 let path = Path::new(r"\\?\c:\");
261 assert_path_eq!(expand_ndots(path), path);
262 }
263
264 #[test]
265 fn device_path_no_dots() {
266 let path = Path::new(r"\\.\CON");
267 assert_path_eq!(expand_ndots(path), path);
268 }
269
270 #[test]
271 fn disk_no_dots() {
272 let path = Path::new(r"c:\Users\Ellie\nu_scripts");
273 assert_path_eq!(expand_ndots(path), path);
274 }
275}
276
277#[cfg(test)]
278mod test_expand_dots {
279 use super::*;
280 use crate::assert_path_eq;
281
282 #[test]
283 fn empty_path() {
284 let path = Path::new("");
285 assert_path_eq!(expand_dots(path), "");
286 }
287
288 #[test]
289 fn single_dot() {
290 let path = Path::new("./");
291 let expected = if cfg!(windows) { r".\" } else { "./" };
292 assert_path_eq!(expand_dots(path), expected);
293 }
294
295 #[test]
296 fn more_single_dots() {
297 let path = Path::new("././.");
298 let expected = ".";
299 assert_path_eq!(expand_dots(path), expected);
300 }
301
302 #[test]
303 fn double_dots() {
304 let path = Path::new("../../..");
305 let expected = if cfg!(windows) {
306 r"..\..\.."
307 } else {
308 "../../.."
309 };
310 assert_path_eq!(expand_dots(path), expected);
311 }
312
313 #[test]
314 fn backtrack_once() {
315 let path = Path::new("/foo/bar/../baz/");
316 let expected = if cfg!(windows) {
317 r"\foo\baz\"
318 } else {
319 "/foo/baz/"
320 };
321 assert_path_eq!(expand_dots(path), expected);
322 }
323
324 #[test]
325 fn backtrack_to_root() {
326 let path = Path::new("/foo/bar/../../../../baz");
327 let expected = if cfg!(windows) { r"\baz" } else { "/baz" };
328 assert_path_eq!(expand_dots(path), expected);
329 }
330
331 #[test]
332 fn unc_share_no_dots() {
333 let path = Path::new(r"\\server\share");
334 assert_path_eq!(expand_dots(path), path);
335 }
336
337 #[test]
338 fn unc_file_no_dots() {
339 let path = Path::new(r"\\server\share\dir\file.nu");
340 assert_path_eq!(expand_dots(path), path);
341 }
342
343 #[test]
344 #[ignore = "bug in upstream library"]
345 fn verbatim_no_dots() {
346 let path = Path::new(r"\\?\pictures\elephants");
348 assert_path_eq!(expand_dots(path), path);
349 }
350
351 #[cfg_attr(not(windows), ignore = "only for Windows")]
352 #[test]
353 fn verbatim_unc_share_no_dots() {
354 let path = Path::new(r"\\?\UNC\server\share");
355 let expected = Path::new(r"\\server\share");
356 assert_path_eq!(expand_dots(path), expected);
357 }
358
359 #[cfg_attr(not(windows), ignore = "only for Windows")]
360 #[test]
361 fn verbatim_unc_file_no_dots() {
362 let path = Path::new(r"\\?\UNC\server\share\dir\file.nu");
363 let expected = Path::new(r"\\server\share\dir\file.nu");
364 assert_path_eq!(expand_dots(path), expected);
365 }
366
367 #[cfg_attr(not(windows), ignore = "only for Windows")]
368 #[test]
369 fn verbatim_disk_no_dots() {
370 let path = Path::new(r"\\?\C:\");
371 let expected = Path::new(r"C:\");
372 assert_path_eq!(expand_dots(path), expected);
373 }
374
375 #[test]
376 fn device_path_no_dots() {
377 let path = Path::new(r"\\.\CON");
378 assert_path_eq!(expand_dots(path), path);
379 }
380
381 #[test]
382 fn disk_no_dots() {
383 let path = Path::new(r"c:\Users\Ellie\nu_scripts");
384 assert_path_eq!(expand_dots(path), path);
385 }
386}