symbolic_common/
path.rs

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/// Returns `true` if the given character is any valid directory separator.
28#[inline]
29fn is_path_separator<C: IntoChar>(c: C) -> bool {
30    matches!(c.into_char(), '\\' | '/')
31}
32
33/// Returns `true` if the given character is a valid Windows directory separator.
34#[inline]
35fn is_windows_separator<C: IntoChar>(c: C) -> bool {
36    is_path_separator(c)
37}
38
39/// Returns `true` if the given character is a valid UNIX directory separator.
40#[inline]
41fn is_unix_separator<C: IntoChar>(c: C) -> bool {
42    c.into_char() == '/'
43}
44
45/// Returns `true` if this is a Windows Universal Naming Convention path (UNC).
46fn 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
51/// Returns `true` if this is an absolute Windows path starting with a drive letter.
52fn 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
64/// Returns `true` if this is an absolute Windows path.
65fn 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
70/// Returns `true`
71fn 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
84/// Joins paths of various platforms.
85///
86/// This attempts to detect Windows or Unix paths and joins with the correct directory separator.
87/// Also, trailing directory separators are detected in the base string and empty paths are handled
88/// correctly.
89///
90/// # Examples
91///
92/// Join a relative UNIX path:
93///
94/// ```
95/// assert_eq!(symbolic_common::join_path("/a/b", "c/d"), "/a/b/c/d");
96/// ```
97///
98/// Join a Windows drive letter path path:
99///
100/// ```
101/// assert_eq!(symbolic_common::join_path("C:\\a", "b\\c"), "C:\\a\\b\\c");
102/// ```
103///
104/// If the right-hand side is an absolute path, it replaces the left-hand side:
105///
106/// ```
107/// assert_eq!(symbolic_common::join_path("/a/b", "/c/d"), "/c/d");
108/// ```
109pub fn join_path(base: &str, other: &str) -> String {
110    // special case for things like <stdin> or others.
111    if other.starts_with('<') && other.ends_with('>') {
112        return other.into();
113    }
114
115    // absolute paths
116    if base.is_empty() || is_absolute_windows_path(other) || is_absolute_unix_path(other) {
117        return other.into();
118    }
119
120    // other weird cases
121    if other.is_empty() {
122        return base.into();
123    }
124
125    // C:\test + \bar -> C:\bar
126    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    // Always trim by both separators, since as soon as the path is Windows, slashes also count as
135    // valid path separators. However, use the main separator for joining.
136    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
157/// Simplifies paths by stripping redundant components.
158///
159/// This removes redundant `../` or `./` path components. However, this function does not operate on
160/// the file system. Since it does not resolve symlinks, this is a potentially lossy operation.
161///
162/// # Examples
163///
164/// Remove `./` components:
165///
166/// ```
167/// assert_eq!(symbolic_common::clean_path("/a/./b"), "/a/b");
168/// ```
169///
170/// Remove path components followed by `../`:
171///
172/// ```
173/// assert_eq!(symbolic_common::clean_path("/a/b/../c"), "/a/c");
174/// ```
175///
176/// Note that when the path is relative, the parent dir components may exceed the top-level:
177///
178/// ```
179/// assert_eq!(symbolic_common::clean_path("/foo/../../b"), "../b");
180/// ```
181pub fn clean_path(path: &str) -> Cow<'_, str> {
182    // TODO: This function has a number of problems (see broken tests):
183    //  - It does not collapse consequtive directory separators
184    //  - Parent-directory directives may leave an absolute path
185    //  - A path is converted to relative when the parent directory hits top-level
186
187    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    // For now, always return an owned string.
223    // This can be optimized later.
224    Cow::Owned(rv)
225}
226
227/// Splits off the last component of a path given as bytes.
228///
229/// The path should be a path to a file, and not a directory with a trailing directory separator. If
230/// this path is a directory or the root path, the result is undefined.
231///
232/// This attempts to detect Windows or Unix paths and split off the last component of the path
233/// accordingly. Note that for paths with mixed slash and backslash separators this might not lead
234/// to the desired results.
235///
236/// **Note**: This is the same as [`split_path`], except that it operates on byte slices.
237///
238/// # Examples
239///
240/// Split the last component of a UNIX path:
241///
242/// ```
243/// assert_eq!(
244///     symbolic_common::split_path_bytes(b"/a/b/c"),
245///     (Some("/a/b".as_bytes()), "c".as_bytes())
246/// );
247/// ```
248///
249/// Split the last component of a Windows path:
250///
251/// ```
252/// assert_eq!(
253///     symbolic_common::split_path_bytes(b"C:\\a\\b"),
254///     (Some("C:\\a".as_bytes()), "b".as_bytes())
255/// );
256/// ```
257///
258/// [`split_path`]: fn.split_path.html
259pub fn split_path_bytes(path: &[u8]) -> (Option<&[u8]>, &[u8]) {
260    // Trim directory separators at the end, if any.
261    let path = match path.iter().rposition(|c| !is_path_separator(c)) {
262        Some(cutoff) => &path[..=cutoff],
263        None => path,
264    };
265
266    // Split by all path separators. On Windows, both are valid and a path is considered a
267    // Windows path as soon as it has a backslash inside.
268    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
275/// Splits off the last component of a path.
276///
277/// The path should be a path to a file, and not a directory. If this path is a directory or the
278/// root path, the result is undefined.
279///
280/// This attempts to detect Windows or Unix paths and split off the last component of the path
281/// accordingly. Note that for paths with mixed slash and backslash separators this might not lead
282/// to the desired results.
283///
284/// **Note**: For a version that operates on byte slices, see [`split_path_bytes`].
285///
286/// # Examples
287///
288/// Split the last component of a UNIX path:
289///
290/// ```
291/// assert_eq!(symbolic_common::split_path("/a/b/c"), (Some("/a/b"), "c"));
292/// ```
293///
294/// Split the last component of a Windows path:
295///
296/// ```
297/// assert_eq!(symbolic_common::split_path("C:\\a\\b"), (Some("C:\\a"), "b"));
298/// ```
299///
300/// [`split_path_bytes`]: fn.split_path_bytes.html
301pub 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
311/// Truncates the given string at character boundaries.
312fn truncate(path: &str, mut length: usize) -> &str {
313    // Backtrack to the last code point. There is a unicode point at least at the beginning of the
314    // string before the first character, which is why this cannot underflow.
315    while !path.is_char_boundary(length) {
316        length -= 1;
317    }
318
319    path.get(..length).unwrap_or_default()
320}
321
322/// Trims a path to a given length.
323///
324/// This attempts to not completely destroy the path in the process by trimming off the middle path
325/// segments. In the process, this tries to determine whether the path is a Windows or Unix path and
326/// handle directory separators accordingly.
327///
328/// # Examples
329///
330/// ```
331/// assert_eq!(
332///     symbolic_common::shorten_path("/foo/bar/baz/blah/blafasel", 21),
333///     "/foo/.../blafasel"
334/// );
335/// ```
336pub fn shorten_path(path: &str, length: usize) -> Cow<'_, str> {
337    // trivial cases
338    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    // make sure we get two segments at the start.
353    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    // collect the rest of the segments into a temporary we can then reverse.
365    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 at this point already we're too long we just take the last element
384    // of the path and strip it.
385    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
404/// Extensions to `Path` for handling `dSYM` directories.
405///
406/// # dSYM Files
407///
408/// `dSYM` files are actually folder structures that store debugging information on Apple platforms.
409/// They are also referred to as debug companion. At the core of this structure is a `MachO` file
410/// containing the actual debug information.
411///
412/// A full `dSYM` folder structure looks like this:
413///
414/// ```text
415/// MyApp.dSYM
416/// └── Contents
417///     ├── Info.plist
418///     └── Resources
419///         └── DWARF
420///             └── MyApp
421/// ```
422pub trait DSymPathExt {
423    /// Returns `true` if this path points to an existing directory with a `.dSYM` extension.
424    ///
425    /// Note that this does not check if a full `dSYM` structure is contained within this folder.
426    ///
427    /// # Examples
428    ///
429    /// ```no_run
430    /// use std::path::Path;
431    /// use symbolic_common::DSymPathExt;
432    ///
433    /// assert!(Path::new("Foo.dSYM").is_dsym_dir());
434    /// assert!(!Path::new("Foo").is_dsym_dir());
435    /// ```
436    fn is_dsym_dir(&self) -> bool;
437
438    /// Resolves the path of the debug file in a `dSYM` directory structure.
439    ///
440    /// Returns `Some(path)` if this path is a dSYM directory according to [`is_dsym_dir`], and a
441    /// file of the same name is located at `Contents/Resources/DWARF/`.
442    ///
443    /// # Examples
444    ///
445    /// ```no_run
446    /// use std::path::Path;
447    /// use symbolic_common::DSymPathExt;
448    ///
449    /// let path = Path::new("Foo.dSYM");
450    /// let dsym_path = path.resolve_dsym().unwrap();
451    /// assert_eq!(dsym_path, Path::new("Foo.dSYM/Contents/Resources/DWARF/Foo"));
452    /// ```
453    ///
454    /// [`is_dsym_dir`]: trait.DSymPathExt.html#tymethod.is_dsym_dir
455    fn resolve_dsym(&self) -> Option<PathBuf>;
456
457    /// Resolves the `dSYM` parent directory if this file is a dSYM.
458    ///
459    /// If this path points to the MachO file in a `dSYM` directory structure, this function returns
460    /// the path to the dSYM directory. Returns `None` if the parent does not exist or the file name
461    /// does not match.
462    ///
463    /// # Examples
464    ///
465    /// ```no_run
466    /// use std::path::Path;
467    /// use symbolic_common::DSymPathExt;
468    ///
469    /// let path = Path::new("Foo.dSYM/Contents/Resources/DWARF/Foo");
470    /// let parent = path.dsym_parent().unwrap();
471    /// assert_eq!(parent, Path::new("Foo.dSYM"));
472    ///
473    /// let path = Path::new("Foo.dSYM/Contents/Resources/DWARF/Bar");
474    /// assert_eq!(path.dsym_parent(), None);
475    /// ```
476    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        // XCode produces [appName].app.dSYM files where the debug file's name is just [appName],
495        // so strip .app if it's present.
496        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        // Accept both Filename.dSYM and Filename.framework.dSYM as
520        // the bundle directory name.
521        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        // XXX currently known broken tests:
601        // assert_eq!(clean_path("/foo/../bar"), "/bar");
602        // assert_eq!(clean_path("\\\\foo\\..\\..\\bar"), "\\\\bar");
603        // assert_eq!(clean_path("/../../blah/"), "/blah");
604        // assert_eq!(clean_path("c:\\..\\foo"), "c:\\foo");
605    }
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    // XCode and other tools (e.g. dwarfdump) produce a dSYM that includes the .app
726    // suffix, which needs to be stripped.
727    #[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}