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