1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::path::{Path, PathBuf};
4
5trait IntoChar {
6 fn into_char(self) -> char;
7}
8
9impl IntoChar for char {
10 fn into_char(self) -> char {
11 self
12 }
13}
14
15impl IntoChar for u8 {
16 fn into_char(self) -> char {
17 char::from(self)
18 }
19}
20
21impl<T: IntoChar + Copy> IntoChar for &'_ T {
22 fn into_char(self) -> char {
23 (*self).into_char()
24 }
25}
26
27#[inline]
29fn is_path_separator<C: IntoChar>(c: C) -> bool {
30 matches!(c.into_char(), '\\' | '/')
31}
32
33#[inline]
35fn is_windows_separator<C: IntoChar>(c: C) -> bool {
36 is_path_separator(c)
37}
38
39#[inline]
41fn is_unix_separator<C: IntoChar>(c: C) -> bool {
42 c.into_char() == '/'
43}
44
45fn is_windows_unc<P: AsRef<[u8]>>(path: P) -> bool {
47 let path = path.as_ref();
48 path.starts_with(b"\\\\") || path.starts_with(b"//")
49}
50
51fn is_windows_driveletter<P: AsRef<[u8]>>(path: P) -> bool {
53 let path = path.as_ref();
54
55 if let (Some(drive_letter), Some(b':')) = (path.first(), path.get(1)) {
56 if drive_letter.is_ascii_alphabetic() {
57 return path.get(2).map_or(true, is_windows_separator);
58 }
59 }
60
61 false
62}
63
64fn is_absolute_windows_path<P: AsRef<[u8]>>(path: P) -> bool {
66 let path = path.as_ref();
67 is_windows_unc(path) || is_windows_driveletter(path)
68}
69
70fn is_semi_absolute_windows_path<P: AsRef<[u8]>>(path: P) -> bool {
72 path.as_ref().first().is_some_and(is_windows_separator)
73}
74
75fn is_absolute_unix_path<P: AsRef<[u8]>>(path: P) -> bool {
76 path.as_ref().first().is_some_and(is_unix_separator)
77}
78
79fn is_windows_path<P: AsRef<[u8]>>(path: P) -> bool {
80 let path = path.as_ref();
81 is_absolute_windows_path(path) || path.contains(&b'\\')
82}
83
84pub fn join_path(base: &str, other: &str) -> String {
110 if other.starts_with('<') && other.ends_with('>') {
112 return other.into();
113 }
114
115 if base.is_empty() || is_absolute_windows_path(other) || is_absolute_unix_path(other) {
117 return other.into();
118 }
119
120 if other.is_empty() {
122 return base.into();
123 }
124
125 if is_semi_absolute_windows_path(other) {
127 if is_absolute_windows_path(base) {
128 return format!("{}{}", &base[..2], other);
129 } else {
130 return other.into();
131 }
132 }
133
134 let is_windows = is_windows_path(base) || is_windows_path(other);
137 format!(
138 "{}{}{}",
139 base.trim_end_matches(is_path_separator),
140 if is_windows { '\\' } else { '/' },
141 other.trim_start_matches(is_path_separator)
142 )
143}
144
145fn pop_path(path: &mut String) -> bool {
146 if let Some(idx) = path.rfind(is_path_separator) {
147 path.truncate(idx);
148 true
149 } else if !path.is_empty() {
150 path.truncate(0);
151 true
152 } else {
153 false
154 }
155}
156
157pub fn clean_path(path: &str) -> Cow<'_, str> {
182 let mut rv = String::with_capacity(path.len());
188 let main_separator = if is_windows_path(path) { '\\' } else { '/' };
189
190 let mut needs_separator = false;
191 let mut is_past_root = false;
192
193 for segment in path.split_terminator(is_path_separator) {
194 if segment == "." {
195 continue;
196 } else if segment == ".." {
197 if !is_past_root && pop_path(&mut rv) {
198 if rv.is_empty() {
199 needs_separator = false;
200 }
201 } else {
202 if !is_past_root {
203 needs_separator = false;
204 is_past_root = true;
205 }
206 if needs_separator {
207 rv.push(main_separator);
208 }
209 rv.push_str("..");
210 needs_separator = true;
211 }
212 continue;
213 }
214 if needs_separator {
215 rv.push(main_separator);
216 } else {
217 needs_separator = true;
218 }
219 rv.push_str(segment);
220 }
221
222 Cow::Owned(rv)
225}
226
227pub fn split_path_bytes(path: &[u8]) -> (Option<&[u8]>, &[u8]) {
260 let path = match path.iter().rposition(|c| !is_path_separator(c)) {
262 Some(cutoff) => &path[..=cutoff],
263 None => path,
264 };
265
266 match path.iter().rposition(is_path_separator) {
269 Some(0) => (Some(&path[..1]), &path[1..]),
270 Some(pos) => (Some(&path[..pos]), &path[pos + 1..]),
271 None => (None, path),
272 }
273}
274
275pub fn split_path(path: &str) -> (Option<&str>, &str) {
302 let (dir, name) = split_path_bytes(path.as_bytes());
303 unsafe {
304 (
305 dir.map(|b| std::str::from_utf8_unchecked(b)),
306 std::str::from_utf8_unchecked(name),
307 )
308 }
309}
310
311fn truncate(path: &str, mut length: usize) -> &str {
313 while !path.is_char_boundary(length) {
316 length -= 1;
317 }
318
319 path.get(..length).unwrap_or_default()
320}
321
322pub fn shorten_path(path: &str, length: usize) -> Cow<'_, str> {
337 if path.len() <= length {
339 return Cow::Borrowed(path);
340 } else if length <= 3 {
341 return Cow::Borrowed(truncate(path, length));
342 } else if length <= 10 {
343 return Cow::Owned(format!("{}...", truncate(path, length - 3)));
344 }
345
346 let mut rv = String::new();
347 let mut last_idx = 0;
348 let mut piece_iter = path.match_indices(is_path_separator);
349 let mut final_sep = "/";
350 let max_len = length - 4;
351
352 for (idx, sep) in &mut piece_iter {
354 let slice = &path[last_idx..idx + sep.len()];
355 rv.push_str(slice);
356 let done = last_idx > 0;
357 last_idx = idx + sep.len();
358 final_sep = sep;
359 if done {
360 break;
361 }
362 }
363
364 let mut final_length = rv.len() as i64;
366 let mut rest = vec![];
367 let mut next_idx = path.len();
368
369 while let Some((idx, _)) = piece_iter.next_back() {
370 if idx <= last_idx {
371 break;
372 }
373 let slice = &path[idx + 1..next_idx];
374 if final_length + (slice.len() as i64) > max_len as i64 {
375 break;
376 }
377
378 rest.push(slice);
379 next_idx = idx + 1;
380 final_length += slice.len() as i64;
381 }
382
383 if rv.len() > max_len || rest.is_empty() {
386 let basename = path.rsplit(is_path_separator).next().unwrap();
387 if basename.len() > max_len {
388 return Cow::Owned(format!("...{}", &basename[basename.len() - max_len + 1..]));
389 } else {
390 return Cow::Owned(format!("...{final_sep}{basename}"));
391 }
392 }
393
394 rest.reverse();
395 rv.push_str("...");
396 rv.push_str(final_sep);
397 for item in rest {
398 rv.push_str(item);
399 }
400
401 Cow::Owned(rv)
402}
403
404pub trait DSymPathExt {
423 fn is_dsym_dir(&self) -> bool;
437
438 fn resolve_dsym(&self) -> Option<PathBuf>;
456
457 fn dsym_parent(&self) -> Option<&Path>;
477}
478
479impl DSymPathExt for Path {
480 fn is_dsym_dir(&self) -> bool {
481 self.extension() == Some("dSYM".as_ref()) && self.is_dir()
482 }
483
484 fn resolve_dsym(&self) -> Option<PathBuf> {
485 if !self.is_dsym_dir() || !self.is_dir() {
486 return None;
487 }
488
489 let framework = self.file_stem()?;
490 let mut full_path = self.to_path_buf();
491 full_path.push("Contents/Resources/DWARF");
492 full_path.push(framework);
493
494 if matches!(full_path.extension(), Some(extension) if extension == "app") {
497 full_path = full_path.with_extension("")
498 }
499
500 if full_path.is_file() {
501 Some(full_path)
502 } else {
503 None
504 }
505 }
506
507 fn dsym_parent(&self) -> Option<&Path> {
508 let framework = self.file_name()?;
509
510 let mut parent = self.parent()?;
511 if !parent.ends_with("Contents/Resources/DWARF") {
512 return None;
513 }
514
515 for _ in 0..3 {
516 parent = parent.parent()?;
517 }
518
519 let stem_matches = parent
522 .file_name()
523 .and_then(|name| Path::new(name).file_stem())
524 .map(|stem| {
525 if stem == framework {
526 return true;
527 }
528 let alt = Path::new(stem);
529 alt.file_stem() == Some(framework)
530 && alt.extension() == Some(OsStr::new("framework"))
531 })
532 .unwrap_or(false);
533 if parent.is_dsym_dir() && stem_matches {
534 Some(parent)
535 } else {
536 None
537 }
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use similar_asserts::assert_eq;
545 use symbolic_testutils::fixture;
546
547 #[test]
548 fn test_join_path() {
549 assert_eq!(join_path("foo", "C:"), "C:");
550 assert_eq!(join_path("foo", "C:bar"), "foo/C:bar");
551 assert_eq!(join_path("C:\\a", "b"), "C:\\a\\b");
552 assert_eq!(join_path("C:/a", "b"), "C:/a\\b");
553 assert_eq!(join_path("C:\\a", "b\\c"), "C:\\a\\b\\c");
554 assert_eq!(join_path("C:/a", "C:\\b"), "C:\\b");
555 assert_eq!(join_path("a\\b\\c", "d\\e"), "a\\b\\c\\d\\e");
556 assert_eq!(join_path("\\\\UNC\\", "a"), "\\\\UNC\\a");
557
558 assert_eq!(join_path("C:\\foo/bar", "\\baz"), "C:\\baz");
559 assert_eq!(join_path("\\foo/bar", "\\baz"), "\\baz");
560 assert_eq!(join_path("/a/b", "\\c"), "\\c");
561
562 assert_eq!(join_path("/a/b", "c"), "/a/b/c");
563 assert_eq!(join_path("/a/b", "c/d"), "/a/b/c/d");
564 assert_eq!(join_path("/a/b", "/c/d/e"), "/c/d/e");
565 assert_eq!(join_path("a/b/", "c"), "a/b/c");
566
567 assert_eq!(join_path("a/b/", "<stdin>"), "<stdin>");
568 assert_eq!(
569 join_path("C:\\test", "<::core::macros::assert_eq macros>"),
570 "<::core::macros::assert_eq macros>"
571 );
572
573 assert_eq!(
574 join_path("foo", "아이쿱 조합원 앱카드"),
575 "foo/아이쿱 조합원 앱카드"
576 );
577 }
578
579 #[test]
580 fn test_clean_path() {
581 assert_eq!(clean_path("/foo/bar/baz/./blah"), "/foo/bar/baz/blah");
582 assert_eq!(clean_path("/foo/bar/baz/./blah/"), "/foo/bar/baz/blah");
583 assert_eq!(clean_path("foo/bar/baz/./blah/"), "foo/bar/baz/blah");
584 assert_eq!(clean_path("foo/bar/baz/../blah/"), "foo/bar/blah");
585 assert_eq!(clean_path("../../blah/"), "../../blah");
586 assert_eq!(clean_path("..\\../blah/"), "..\\..\\blah");
587 assert_eq!(clean_path("foo\\bar\\baz/../blah/"), "foo\\bar\\blah");
588 assert_eq!(clean_path("foo\\bar\\baz/../../../../blah/"), "..\\blah");
589 assert_eq!(clean_path("foo/bar/baz/../../../../blah/"), "../blah");
590 assert_eq!(clean_path("..\\foo"), "..\\foo");
591 assert_eq!(clean_path("foo"), "foo");
592 assert_eq!(clean_path("foo\\bar\\baz/../../../blah/"), "blah");
593 assert_eq!(clean_path("foo/bar/baz/../../../blah/"), "blah");
594 assert_eq!(clean_path("\\\\foo\\..\\bar"), "\\\\bar");
595 assert_eq!(
596 clean_path("foo/bar/../아이쿱 조합원 앱카드"),
597 "foo/아이쿱 조합원 앱카드"
598 );
599
600 }
606
607 #[test]
608 fn test_shorten_path() {
609 assert_eq!(shorten_path("/foo/bar/baz/blah/blafasel", 6), "/fo...");
610 assert_eq!(shorten_path("/foo/bar/baz/blah/blafasel", 2), "/f");
611 assert_eq!(
612 shorten_path("/foo/bar/baz/blah/blafasel", 21),
613 "/foo/.../blafasel"
614 );
615 assert_eq!(
616 shorten_path("/foo/bar/baz/blah/blafasel", 22),
617 "/foo/.../blah/blafasel"
618 );
619 assert_eq!(
620 shorten_path("C:\\bar\\baz\\blah\\blafasel", 20),
621 "C:\\bar\\...\\blafasel"
622 );
623 assert_eq!(
624 shorten_path("/foo/blar/baz/blah/blafasel", 27),
625 "/foo/blar/baz/blah/blafasel"
626 );
627 assert_eq!(
628 shorten_path("/foo/blar/baz/blah/blafasel", 26),
629 "/foo/.../baz/blah/blafasel"
630 );
631 assert_eq!(
632 shorten_path("/foo/b/baz/blah/blafasel", 23),
633 "/foo/.../blah/blafasel"
634 );
635 assert_eq!(shorten_path("/foobarbaz/blahblah", 16), ".../blahblah");
636 assert_eq!(shorten_path("/foobarbazblahblah", 12), "...lahblah");
637 assert_eq!(shorten_path("", 0), "");
638
639 assert_eq!(shorten_path("아이쿱 조합원 앱카드", 9), "아...");
640 assert_eq!(shorten_path("아이쿱 조합원 앱카드", 20), "...ᆸ카드");
641 }
642
643 #[test]
644 fn test_split_path() {
645 assert_eq!(split_path("C:\\a\\b"), (Some("C:\\a"), "b"));
646 assert_eq!(split_path("C:/a\\b"), (Some("C:/a"), "b"));
647 assert_eq!(split_path("C:\\a\\b\\c"), (Some("C:\\a\\b"), "c"));
648 assert_eq!(split_path("a\\b\\c\\d\\e"), (Some("a\\b\\c\\d"), "e"));
649 assert_eq!(split_path("\\\\UNC\\a"), (Some("\\\\UNC"), "a"));
650
651 assert_eq!(split_path("/a/b/c"), (Some("/a/b"), "c"));
652 assert_eq!(split_path("/a/b/c/d"), (Some("/a/b/c"), "d"));
653 assert_eq!(split_path("a/b/c"), (Some("a/b"), "c"));
654
655 assert_eq!(split_path("a"), (None, "a"));
656 assert_eq!(split_path("a/"), (None, "a"));
657 assert_eq!(split_path("/a"), (Some("/"), "a"));
658 assert_eq!(split_path(""), (None, ""));
659
660 assert_eq!(
661 split_path("foo/아이쿱 조합원 앱카드"),
662 (Some("foo"), "아이쿱 조합원 앱카드")
663 );
664 }
665
666 #[test]
667 fn test_split_path_bytes() {
668 assert_eq!(
669 split_path_bytes(&b"C:\\a\\b"[..]),
670 (Some(&b"C:\\a"[..]), &b"b"[..])
671 );
672 assert_eq!(
673 split_path_bytes(&b"C:/a\\b"[..]),
674 (Some(&b"C:/a"[..]), &b"b"[..])
675 );
676 assert_eq!(
677 split_path_bytes(&b"C:\\a\\b\\c"[..]),
678 (Some(&b"C:\\a\\b"[..]), &b"c"[..])
679 );
680 assert_eq!(
681 split_path_bytes(&b"a\\b\\c\\d\\e"[..]),
682 (Some(&b"a\\b\\c\\d"[..]), &b"e"[..])
683 );
684 assert_eq!(
685 split_path_bytes(&b"\\\\UNC\\a"[..]),
686 (Some(&b"\\\\UNC"[..]), &b"a"[..])
687 );
688
689 assert_eq!(
690 split_path_bytes(&b"/a/b/c"[..]),
691 (Some(&b"/a/b"[..]), &b"c"[..])
692 );
693 assert_eq!(
694 split_path_bytes(&b"/a/b/c/d"[..]),
695 (Some(&b"/a/b/c"[..]), &b"d"[..])
696 );
697 assert_eq!(
698 split_path_bytes(&b"a/b/c"[..]),
699 (Some(&b"a/b"[..]), &b"c"[..])
700 );
701
702 assert_eq!(split_path_bytes(&b"a"[..]), (None, &b"a"[..]));
703 assert_eq!(split_path_bytes(&b"a/"[..]), (None, &b"a"[..]));
704 assert_eq!(split_path_bytes(&b"/a"[..]), (Some(&b"/"[..]), &b"a"[..]));
705 assert_eq!(split_path_bytes(&b""[..]), (None, &b""[..]));
706 }
707
708 #[test]
709 fn test_is_dsym_dir() {
710 assert!(fixture("macos/crash.dSYM").is_dsym_dir());
711 assert!(!fixture("macos/crash").is_dsym_dir());
712 }
713
714 #[test]
715 fn test_resolve_dsym() {
716 let crash_path = fixture("macos/crash.dSYM");
717 let resolved = crash_path.resolve_dsym().unwrap();
718 assert!(resolved.exists());
719 assert!(resolved.ends_with("macos/crash.dSYM/Contents/Resources/DWARF/crash"));
720
721 let other_path = fixture("macos/other.dSYM");
722 assert_eq!(other_path.resolve_dsym(), None);
723 }
724
725 #[test]
728 fn test_resolve_dsym_double_extension() {
729 let crash_path = fixture("macos/crash.app.dSYM");
730 let resolved = crash_path.resolve_dsym().unwrap();
731 assert!(resolved.exists());
732 assert!(resolved.ends_with("macos/crash.app.dSYM/Contents/Resources/DWARF/crash"));
733
734 let other_path = fixture("macos/other.dmp.dSYM");
735 assert_eq!(other_path.resolve_dsym(), None);
736 }
737
738 #[test]
739 fn test_dsym_parent() {
740 let crash_path = fixture("macos/crash.dSYM/Contents/Resources/DWARF/crash");
741 let dsym_path = crash_path.dsym_parent().unwrap();
742 assert!(dsym_path.exists());
743 assert!(dsym_path.ends_with("macos/crash.dSYM"));
744
745 let other_path = fixture("macos/crash.dSYM/Contents/Resources/DWARF/invalid");
746 assert_eq!(other_path.dsym_parent(), None);
747 }
748
749 #[test]
750 fn test_dsym_parent_framework() {
751 let dwarf_path = fixture("macos/Example.framework.dSYM/Contents/Resources/DWARF/Example");
752 let dsym_path = dwarf_path.dsym_parent().unwrap();
753 assert!(dsym_path.exists());
754 assert!(dsym_path.ends_with("macos/Example.framework.dSYM"));
755 }
756}