1use smol_str::{format_smolstr, SmolStr, SmolStrBuilder};
10use std::path::{Path, PathBuf};
11
12pub fn is_url(path: &Path) -> bool {
14 let Some(path) = path.to_str() else {
15 return false;
17 };
18
19 to_url(path).is_some()
20}
21
22fn to_url(path: &str) -> Option<url::Url> {
24 let Ok(url) = url::Url::parse(path) else {
25 return None;
26 };
27 if url.scheme().len() == 1 {
28 None
30 } else {
31 Some(url)
32 }
33}
34
35#[test]
36fn test_to_url() {
37 #[track_caller]
38 fn th(input: &str, expected: bool) {
39 assert_eq!(to_url(input).is_some(), expected);
40 }
41
42 th("https://foo.bar/", true);
43 th("builtin:/foo/bar/", true);
44 th("user://foo/bar.rs", true);
45 th("/foo/bar/", false);
46 th("../foo/bar", false);
47 th("foo/bar", false);
48 th("./foo/bar", false);
49 th("C:\\Documents\\Newsletters\\Summer2018.pdf", false);
51 th("\\Program Files\\Custom Utilities\\StringFinder.exe", false);
52 th("2018\\January.xlsx", false);
53 th("..\\Publications\\TravelBrochure.pdf", false);
54 th("C:\\Projects\\library\\library.sln", false);
55 th("C:Projects\\library\\library.sln", false);
56 th("\\\\system07\\C$\\", false);
57 th("\\\\Server2\\Share\\Test\\Foo.txt", false);
58 th("\\\\.\\C:\\Test\\Foo.txt", false);
59 th("\\\\?\\C:\\Test\\Foo.txt", false);
60 th("\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt", false);
61 th("\\\\?\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt", false);
62 th("C:/Documents/Newsletters/Summer2018.pdf", false);
64 th("/Program Files/Custom Utilities/StringFinder.exe", false);
65 th("2018/January.xlsx", false);
66 th("../Publications/TravelBrochure.pdf", false);
67 th("C:/Projects/library/library.sln", false);
68 th("C:Projects/library/library.sln", false);
69 th("//system07/C$/", false);
70 th("//Server2/Share/Test/Foo.txt", false);
71 th("//./C:/Test/Foo.txt", false);
72 th("//?/C:/Test/Foo.txt", false);
73 th("//./Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt", false);
74 th("//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt", false);
75 th("C:///Documents/Newsletters/Summer2018.pdf", false);
77 th("/http://foo/bar/", false);
78 th("../http://foo/bar", false);
79 th("foo/http://foo/bar", false);
80 th("./http://foo/bar", false);
81 th("", false);
82}
83
84pub fn is_absolute(path: &Path) -> bool {
89 let Some(path) = path.to_str() else {
90 return false;
92 };
93
94 if to_url(path).is_some() {
95 return true;
96 }
97
98 matches!(components(path, 0, &None), Some((PathComponent::Root(_), _, _)))
99}
100
101#[test]
102fn test_is_absolute() {
103 #[track_caller]
104 fn th(input: &str, expected: bool) {
105 let path = PathBuf::from(input);
106 assert_eq!(is_absolute(&path), expected);
107 }
108
109 th("https://foo.bar/", true);
110 th("builtin:/foo/bar/", true);
111 th("user://foo/bar.rs", true);
112 th("/foo/bar/", true);
113 th("../foo/bar", false);
114 th("foo/bar", false);
115 th("./foo/bar", false);
116 th("C:\\Documents\\Newsletters\\Summer2018.pdf", true);
118 th("\\Program Files\\Custom Utilities\\StringFinder.exe", true);
120 th("2018\\January.xlsx", false);
121 th("..\\Publications\\TravelBrochure.pdf", false);
122 th("C:\\Projects\\library\\library.sln", true);
123 th("C:Projects\\library\\library.sln", false);
124 th("\\\\system07\\C$\\", true);
125 th("\\\\Server2\\Share\\Test\\Foo.txt", true);
126 th("\\\\.\\C:\\Test\\Foo.txt", true);
127 th("\\\\?\\C:\\Test\\Foo.txt", true);
128 th("\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt", true);
129 th("\\\\?\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt", true);
130 th("C:/Documents/Newsletters/Summer2018.pdf", true);
132 th("/Program Files/Custom Utilities/StringFinder.exe", true);
134 th("2018/January.xlsx", false);
135 th("../Publications/TravelBrochure.pdf", false);
136 th("C:/Projects/library/library.sln", true);
137 th("C:Projects/library/library.sln", false);
138 th("//system07/C$/", true);
140 th("//Server2/Share/Test/Foo.txt", true);
141 th("//./C:/Test/Foo.txt", true);
142 th("//?/C:/Test/Foo.txt", true);
143 th("//./Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt", true);
144 th("//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt", true);
145 th("C:///Documents/Newsletters/Summer2018.pdf", true);
147 th("C:", false);
148 th("C:\\", true);
149 th("C:/", true);
150 th("", false);
151}
152
153#[derive(Debug, PartialEq)]
154enum PathComponent<'a> {
155 Root(&'a str),
156 Empty,
157 SameDirectory(&'a str),
158 ParentDirectory(&'a str),
159 Directory(&'a str),
160 File(&'a str),
161}
162
163fn find_path_separator(path: &str) -> char {
165 for c in path.chars() {
166 if c == '/' || c == '\\' {
167 return c;
168 }
169 }
170 '/'
171}
172
173fn components<'a>(
177 path: &'a str,
178 offset: usize,
179 separator: &Option<char>,
180) -> Option<(PathComponent<'a>, usize, char)> {
181 use PathComponent as PC;
182
183 if offset >= path.len() {
184 return None;
185 }
186
187 let b = path.as_bytes();
188
189 if offset == 0 {
190 if b.len() >= 3
191 && b[0].is_ascii_alphabetic()
192 && b[1] == b':'
193 && (b[2] == b'\\' || b[2] == b'/')
194 {
195 return Some((PC::Root(&path[0..3]), 3, b[2] as char));
196 }
197 if b.len() >= 2 && b[0] == b'\\' && b[1] == b'\\' {
198 let second_bs = path[2..]
199 .find('\\')
200 .map(|pos| pos + 2 + 1)
201 .and_then(|pos1| path[pos1..].find('\\').map(|pos2| pos2 + pos1));
202 if let Some(end_offset) = second_bs {
203 return Some((PC::Root(&path[0..=end_offset]), end_offset + 1, '\\'));
204 }
205 }
206 if b[0] == b'/' || b[0] == b'\\' {
207 return Some((PC::Root(&path[0..1]), 1, b[0] as char));
208 }
209 }
210
211 let separator = separator.unwrap_or_else(|| find_path_separator(path));
212
213 let next_component = path[offset..].find(separator).map(|p| p + offset).unwrap_or(path.len());
214 if &path[offset..next_component] == "." {
215 return Some((
216 PC::SameDirectory(&path[offset..next_component]),
217 next_component + 1,
218 separator,
219 ));
220 }
221 if &path[offset..next_component] == ".." {
222 return Some((
223 PC::ParentDirectory(&path[offset..next_component]),
224 next_component + 1,
225 separator,
226 ));
227 }
228
229 if next_component == path.len() {
230 Some((PC::File(&path[offset..next_component]), next_component, separator))
231 } else if next_component == offset {
232 Some((PC::Empty, next_component + 1, separator))
233 } else {
234 Some((PC::Directory(&path[offset..next_component]), next_component + 1, separator))
235 }
236}
237
238#[test]
239fn test_components() {
240 use PathComponent as PC;
241
242 #[track_caller]
243 fn th(input: &str, expected: Option<(PathComponent, usize, char)>) {
244 assert_eq!(components(input, 0, &None), expected);
245 }
246
247 th("/foo/bar/", Some((PC::Root("/"), 1, '/')));
248 th("../foo/bar", Some((PC::ParentDirectory(".."), 3, '/')));
249 th("foo/bar", Some((PC::Directory("foo"), 4, '/')));
250 th("./foo/bar", Some((PC::SameDirectory("."), 2, '/')));
251 th("C:\\Documents\\Newsletters\\Summer2018.pdf", Some((PC::Root("C:\\"), 3, '\\')));
253 th("\\Program Files\\Custom Utilities\\StringFinder.exe", Some((PC::Root("\\"), 1, '\\')));
255 th("2018\\January.xlsx", Some((PC::Directory("2018"), 5, '\\')));
256 th("..\\Publications\\TravelBrochure.pdf", Some((PC::ParentDirectory(".."), 3, '\\')));
257 th("C:Projects\\library\\library.sln", Some((PC::Directory("C:Projects"), 11, '\\')));
259 th("\\\\system07\\C$\\", Some((PC::Root("\\\\system07\\C$\\"), 14, '\\')));
260 th("\\\\Server2\\Share\\Test\\Foo.txt", Some((PC::Root("\\\\Server2\\Share\\"), 16, '\\')));
261 th("\\\\.\\C:\\Test\\Foo.txt", Some((PC::Root("\\\\.\\C:\\"), 7, '\\')));
262 th("\\\\?\\C:\\Test\\Foo.txt", Some((PC::Root("\\\\?\\C:\\"), 7, '\\')));
263 th(
264 "\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt",
265 Some((PC::Root("\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\"), 49, '\\')),
266 );
267 th(
268 "\\\\?\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt",
269 Some((PC::Root("\\\\?\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\"), 49, '\\')),
270 );
271 th("C:/Documents/Newsletters/Summer2018.pdf", Some((PC::Root("C:/"), 3, '/')));
273 th("/Program Files/Custom Utilities/StringFinder.exe", Some((PC::Root("/"), 1, '/')));
275 th("//system07/C$/", Some((PC::Root("/"), 1, '/')));
276 th("//Server2/Share/Test/Foo.txt", Some((PC::Root("/"), 1, '/')));
277 th("//./C:/Test/Foo.txt", Some((PC::Root("/"), 1, '/')));
278 th("//?/C:/Test/Foo.txt", Some((PC::Root("/"), 1, '/')));
279 th(
280 "//./Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt",
281 Some((PC::Root("/"), 1, '/')),
282 );
283 th(
284 "//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt",
285 Some((PC::Root("/"), 1, '/')),
286 );
287 th("C:", Some((PC::File("C:"), 2, '/')));
291 th("foo", Some((PC::File("foo"), 3, '/')));
292 th("foo/", Some((PC::Directory("foo"), 4, '/')));
293 th("foo\\", Some((PC::Directory("foo"), 4, '\\')));
294 th("", None);
295}
296
297struct Components<'a> {
298 path: &'a str,
299 offset: usize,
300 separator: Option<char>,
301}
302
303fn component_iter(path: &str) -> Components<'_> {
304 Components { path, offset: 0, separator: None }
305}
306
307impl<'a> Iterator for Components<'a> {
308 type Item = PathComponent<'a>;
309
310 fn next(&mut self) -> Option<Self::Item> {
311 let (result, new_offset, separator) = components(self.path, self.offset, &self.separator)?;
312 self.offset = new_offset;
313 self.separator = Some(separator);
314
315 Some(result)
316 }
317}
318
319fn clean_path_string(path: &str) -> SmolStr {
320 use PathComponent as PC;
321
322 let separator = find_path_separator(path);
323 let path = if separator == '\\' {
324 path.replace('/', &format!("{separator}"))
325 } else {
326 path.replace('\\', "/")
327 };
328
329 let mut clean_components = Vec::new();
330
331 for component in component_iter(&path) {
332 match component {
333 PC::Root(v) => {
334 clean_components = vec![PC::Root(v)];
335 }
336 PC::Empty | PC::SameDirectory(_) => { }
337 PC::ParentDirectory(v) => {
338 match clean_components.last() {
339 Some(PC::Directory(_)) => {
340 clean_components.pop();
341 }
342 Some(PC::File(_)) => unreachable!("Must be the last component"),
343 Some(PC::SameDirectory(_) | PC::Empty) => {
344 unreachable!("Will never be in a the vector")
345 }
346 Some(PC::ParentDirectory(_)) => {
347 clean_components.push(PC::ParentDirectory(v));
348 }
349 Some(PC::Root(_)) => { }
350 None => {
351 clean_components.push(PC::ParentDirectory(v));
352 }
353 };
354 }
355 PC::Directory(v) => clean_components.push(PC::Directory(v)),
356 PC::File(v) => clean_components.push(PC::File(v)),
357 }
358 }
359 if clean_components.is_empty() {
360 SmolStr::new_static(".")
361 } else {
362 let mut result = SmolStrBuilder::default();
363 for c in clean_components {
364 match c {
365 PC::Root(v) => {
366 result.push_str(v);
367 }
368 PC::Empty | PC::SameDirectory(_) => {
369 unreachable!("Never in the vector!")
370 }
371 PC::ParentDirectory(v) => {
372 result.push_str(&format_smolstr!("{v}{separator}"));
373 }
374 PC::Directory(v) => result.push_str(&format_smolstr!("{v}{separator}")),
375 PC::File(v) => {
376 result.push_str(v);
377 }
378 }
379 }
380 result.finish()
381 }
382}
383
384#[test]
385fn test_clean_path_string() {
386 #[track_caller]
387 fn th(input: &str, expected: &str) {
388 let result = clean_path_string(input);
389 assert_eq!(result, expected);
390 }
391
392 th("../../ab/.././hello.txt", "../../hello.txt");
393 th("/../../ab/.././hello.txt", "/hello.txt");
394 th("ab/.././cb/././///./..", ".");
395 th("ab/.././cb/.\\.\\\\\\\\./..", ".");
396 th("ab\\..\\.\\cb\\././///./..", ".");
397}
398
399pub fn clean_path(path: &Path) -> PathBuf {
403 let Some(path_str) = path.to_str() else {
404 return path.to_owned();
405 };
406
407 if let Some(url) = to_url(path_str) {
408 PathBuf::from(url.to_string())
410 } else {
411 PathBuf::from(clean_path_string(path_str).to_string())
412 }
413}
414
415fn dirname_string(path: &str) -> String {
416 let separator = find_path_separator(path);
417 let mut result = String::new();
418
419 for component in component_iter(path) {
420 match component {
421 PathComponent::Root(v) => result = v.to_string(),
422 PathComponent::Empty => result.push(separator),
423 PathComponent::SameDirectory(v)
424 | PathComponent::ParentDirectory(v)
425 | PathComponent::Directory(v) => result += &format!("{v}{separator}"),
426 PathComponent::File(_) => { }
427 };
428 }
429
430 if result.is_empty() {
431 String::from(".")
432 } else {
433 result
434 }
435}
436
437#[test]
438fn test_dirname() {
439 #[track_caller]
440 fn th(input: &str, expected: &str) {
441 let result = dirname_string(input);
442 assert_eq!(result, expected);
443 }
444
445 th("/../../ab/.././", "/../../ab/.././");
446 th("ab/.././cb/./././..", "ab/.././cb/./././../");
447 th("hello.txt", ".");
448 th("../hello.txt", "../");
449 th("/hello.txt", "/");
450}
451
452pub fn dirname(path: &Path) -> PathBuf {
454 let Some(path_str) = path.to_str() else {
455 return path.to_owned();
456 };
457
458 PathBuf::from(dirname_string(path_str))
459}
460
461pub fn join(base: &Path, path: &Path) -> Option<PathBuf> {
466 if is_absolute(path) {
467 return Some(path.to_owned());
468 }
469
470 let Some(base_str) = base.to_str() else {
471 return Some(path.to_owned());
472 };
473 let Some(path_str) = path.to_str() else {
474 return Some(path.to_owned());
475 };
476
477 let path_separator = find_path_separator(path_str);
478
479 if let Some(mut base_url) = to_url(base_str) {
480 let path_str = if path_separator != '/' {
481 path_str.replace(path_separator, "/")
482 } else {
483 path_str.to_string()
484 };
485
486 let base_path = base_url.path();
487 if !base_path.is_empty() && !base_path.ends_with('/') {
488 base_url.set_path(&format_smolstr!("{base_path}/"));
489 }
490
491 Some(PathBuf::from(base_url.join(&path_str).ok()?.to_string()))
492 } else {
493 let base_separator = find_path_separator(base_str);
494 let path_str = if path_separator != base_separator {
495 path_str.replace(path_separator, &base_separator.to_string())
496 } else {
497 path_str.to_string()
498 };
499 let joined = clean_path_string(&format_smolstr!("{base_str}{base_separator}{path_str}"));
500 Some(PathBuf::from(joined.to_string()))
501 }
502}
503
504#[test]
505fn test_join() {
506 #[track_caller]
507 fn th(base: &str, path: &str, expected: Option<&str>) {
508 let base = PathBuf::from(base);
509 let path = PathBuf::from(path);
510 let expected = expected.map(|e| PathBuf::from(e));
511
512 let result = join(&base, &path);
513 assert_eq!(result, expected);
514 }
515
516 th("https://slint.dev/", "/hello.txt", Some("/hello.txt"));
517 th("https://slint.dev/", "../../hello.txt", Some("https://slint.dev/hello.txt"));
518 th("/../../ab/.././", "hello.txt", Some("/hello.txt"));
519 th("ab/.././cb/./././..", "../.././hello.txt", Some("../../hello.txt"));
520 th("builtin:/foo", "..\\bar.slint", Some("builtin:/bar.slint"));
521 th("builtin:/", "..\\bar.slint", Some("builtin:/bar.slint"));
522 th("builtin:/foo/baz", "..\\bar.slint", Some("builtin:/foo/bar.slint"));
523 th("builtin:/foo", "bar.slint", Some("builtin:/foo/bar.slint"));
524 th("builtin:/foo/", "bar.slint", Some("builtin:/foo/bar.slint"));
525 th("builtin:/", "..\\bar.slint", Some("builtin:/bar.slint"));
526}