1 use std::borrow::Cow;
2 use std::ffi::OsStr;
3 use std::path::{Path, PathBuf};
4 
5 trait IntoChar {
into_char(self) -> char6     fn into_char(self) -> char;
7 }
8 
9 impl IntoChar for char {
into_char(self) -> char10     fn into_char(self) -> char {
11         self
12     }
13 }
14 
15 impl IntoChar for u8 {
into_char(self) -> char16     fn into_char(self) -> char {
17         char::from(self)
18     }
19 }
20 
21 impl<T: IntoChar + Copy> IntoChar for &'_ T {
into_char(self) -> char22     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]
is_path_separator<C: IntoChar>(c: C) -> bool29 fn 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]
is_windows_separator<C: IntoChar>(c: C) -> bool35 fn 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]
is_unix_separator<C: IntoChar>(c: C) -> bool41 fn 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).
is_windows_unc<P: AsRef<[u8]>>(path: P) -> bool46 fn 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.
is_windows_driveletter<P: AsRef<[u8]>>(path: P) -> bool52 fn 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.get(0), path.get(1)) {
56         if matches!(drive_letter, b'A'..=b'Z' | b'a'..=b'z') {
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.
is_absolute_windows_path<P: AsRef<[u8]>>(path: P) -> bool65 fn 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`
is_semi_absolute_windows_path<P: AsRef<[u8]>>(path: P) -> bool71 fn is_semi_absolute_windows_path<P: AsRef<[u8]>>(path: P) -> bool {
72     path.as_ref().get(0).map_or(false, is_windows_separator)
73 }
74 
is_absolute_unix_path<P: AsRef<[u8]>>(path: P) -> bool75 fn is_absolute_unix_path<P: AsRef<[u8]>>(path: P) -> bool {
76     path.as_ref().get(0).map_or(false, is_unix_separator)
77 }
78 
is_windows_path<P: AsRef<[u8]>>(path: P) -> bool79 fn 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 /// ```
join_path(base: &str, other: &str) -> String109 pub 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 
pop_path(path: &mut String) -> bool145 fn 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 /// ```
clean_path(path: &str) -> Cow<'_, str>181 pub 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
split_path_bytes(path: &[u8]) -> (Option<&[u8]>, &[u8])259 pub 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
split_path(path: &str) -> (Option<&str>, &str)301 pub 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.
truncate(path: &str, mut length: usize) -> &str312 fn 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 /// ```
shorten_path(path: &str, length: usize) -> Cow<'_, str>336 pub 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 /// ```
422 pub 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     /// ```
is_dsym_dir(&self) -> bool436     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
resolve_dsym(&self) -> Option<PathBuf>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     /// ```
dsym_parent(&self) -> Option<&Path>476     fn dsym_parent(&self) -> Option<&Path>;
477 }
478 
479 impl DSymPathExt for Path {
is_dsym_dir(&self) -> bool480     fn is_dsym_dir(&self) -> bool {
481         self.extension() == Some("dSYM".as_ref()) && self.is_dir()
482     }
483 
resolve_dsym(&self) -> Option<PathBuf>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 full_path.is_file() {
495             Some(full_path)
496         } else {
497             None
498         }
499     }
500 
dsym_parent(&self) -> Option<&Path>501     fn dsym_parent(&self) -> Option<&Path> {
502         let framework = self.file_name()?;
503 
504         let mut parent = self.parent()?;
505         if !parent.ends_with("Contents/Resources/DWARF") {
506             return None;
507         }
508 
509         for _ in 0..3 {
510             parent = parent.parent()?;
511         }
512 
513         // Accept both Filename.dSYM and Filename.framework.dSYM as
514         // the bundle directory name.
515         let stem_matches = parent
516             .file_name()
517             .and_then(|name| Path::new(name).file_stem())
518             .map(|stem| {
519                 if stem == framework {
520                     return true;
521                 }
522                 let alt = Path::new(stem);
523                 alt.file_stem() == Some(framework)
524                     && alt.extension() == Some(OsStr::new("framework"))
525             })
526             .unwrap_or(false);
527         if parent.is_dsym_dir() && stem_matches {
528             Some(parent)
529         } else {
530             None
531         }
532     }
533 }
534 
535 #[cfg(test)]
536 mod tests {
537     use super::*;
538     use similar_asserts::assert_eq;
539     use symbolic_testutils::fixture;
540 
541     #[test]
test_join_path()542     fn test_join_path() {
543         assert_eq!(join_path("foo", "C:"), "C:");
544         assert_eq!(join_path("foo", "C:bar"), "foo/C:bar");
545         assert_eq!(join_path("C:\\a", "b"), "C:\\a\\b");
546         assert_eq!(join_path("C:/a", "b"), "C:/a\\b");
547         assert_eq!(join_path("C:\\a", "b\\c"), "C:\\a\\b\\c");
548         assert_eq!(join_path("C:/a", "C:\\b"), "C:\\b");
549         assert_eq!(join_path("a\\b\\c", "d\\e"), "a\\b\\c\\d\\e");
550         assert_eq!(join_path("\\\\UNC\\", "a"), "\\\\UNC\\a");
551 
552         assert_eq!(join_path("C:\\foo/bar", "\\baz"), "C:\\baz");
553         assert_eq!(join_path("\\foo/bar", "\\baz"), "\\baz");
554         assert_eq!(join_path("/a/b", "\\c"), "\\c");
555 
556         assert_eq!(join_path("/a/b", "c"), "/a/b/c");
557         assert_eq!(join_path("/a/b", "c/d"), "/a/b/c/d");
558         assert_eq!(join_path("/a/b", "/c/d/e"), "/c/d/e");
559         assert_eq!(join_path("a/b/", "c"), "a/b/c");
560 
561         assert_eq!(join_path("a/b/", "<stdin>"), "<stdin>");
562         assert_eq!(
563             join_path("C:\\test", "<::core::macros::assert_eq macros>"),
564             "<::core::macros::assert_eq macros>"
565         );
566 
567         assert_eq!(
568             join_path("foo", "아이쿱 조합원 앱카드"),
569             "foo/아이쿱 조합원 앱카드"
570         );
571     }
572 
573     #[test]
574     fn test_clean_path() {
575         assert_eq!(clean_path("/foo/bar/baz/./blah"), "/foo/bar/baz/blah");
576         assert_eq!(clean_path("/foo/bar/baz/./blah/"), "/foo/bar/baz/blah");
577         assert_eq!(clean_path("foo/bar/baz/./blah/"), "foo/bar/baz/blah");
578         assert_eq!(clean_path("foo/bar/baz/../blah/"), "foo/bar/blah");
579         assert_eq!(clean_path("../../blah/"), "../../blah");
580         assert_eq!(clean_path("..\\../blah/"), "..\\..\\blah");
581         assert_eq!(clean_path("foo\\bar\\baz/../blah/"), "foo\\bar\\blah");
582         assert_eq!(clean_path("foo\\bar\\baz/../../../../blah/"), "..\\blah");
583         assert_eq!(clean_path("foo/bar/baz/../../../../blah/"), "../blah");
584         assert_eq!(clean_path("..\\foo"), "..\\foo");
585         assert_eq!(clean_path("foo"), "foo");
586         assert_eq!(clean_path("foo\\bar\\baz/../../../blah/"), "blah");
587         assert_eq!(clean_path("foo/bar/baz/../../../blah/"), "blah");
588         assert_eq!(clean_path("\\\\foo\\..\\bar"), "\\\\bar");
589         assert_eq!(
590             clean_path("foo/bar/../아이쿱 조합원 앱카드"),
591             "foo/아이쿱 조합원 앱카드"
592         );
593 
594         // XXX currently known broken tests:
595         // assert_eq!(clean_path("/foo/../bar"), "/bar");
596         // assert_eq!(clean_path("\\\\foo\\..\\..\\bar"), "\\\\bar");
597         // assert_eq!(clean_path("/../../blah/"), "/blah");
598         // assert_eq!(clean_path("c:\\..\\foo"), "c:\\foo");
599     }
600 
601     #[test]
602     fn test_shorten_path() {
603         assert_eq!(shorten_path("/foo/bar/baz/blah/blafasel", 6), "/fo...");
604         assert_eq!(shorten_path("/foo/bar/baz/blah/blafasel", 2), "/f");
605         assert_eq!(
606             shorten_path("/foo/bar/baz/blah/blafasel", 21),
607             "/foo/.../blafasel"
608         );
609         assert_eq!(
610             shorten_path("/foo/bar/baz/blah/blafasel", 22),
611             "/foo/.../blah/blafasel"
612         );
613         assert_eq!(
614             shorten_path("C:\\bar\\baz\\blah\\blafasel", 20),
615             "C:\\bar\\...\\blafasel"
616         );
617         assert_eq!(
618             shorten_path("/foo/blar/baz/blah/blafasel", 27),
619             "/foo/blar/baz/blah/blafasel"
620         );
621         assert_eq!(
622             shorten_path("/foo/blar/baz/blah/blafasel", 26),
623             "/foo/.../baz/blah/blafasel"
624         );
625         assert_eq!(
626             shorten_path("/foo/b/baz/blah/blafasel", 23),
627             "/foo/.../blah/blafasel"
628         );
629         assert_eq!(shorten_path("/foobarbaz/blahblah", 16), ".../blahblah");
630         assert_eq!(shorten_path("/foobarbazblahblah", 12), "...lahblah");
631         assert_eq!(shorten_path("", 0), "");
632 
633         assert_eq!(shorten_path("아이쿱 조합원 앱카드", 9), "아...");
634         assert_eq!(shorten_path("아이쿱 조합원 앱카드", 20), "...ᆸ카드");
635     }
636 
637     #[test]
638     fn test_split_path() {
639         assert_eq!(split_path("C:\\a\\b"), (Some("C:\\a"), "b"));
640         assert_eq!(split_path("C:/a\\b"), (Some("C:/a"), "b"));
641         assert_eq!(split_path("C:\\a\\b\\c"), (Some("C:\\a\\b"), "c"));
642         assert_eq!(split_path("a\\b\\c\\d\\e"), (Some("a\\b\\c\\d"), "e"));
643         assert_eq!(split_path("\\\\UNC\\a"), (Some("\\\\UNC"), "a"));
644 
645         assert_eq!(split_path("/a/b/c"), (Some("/a/b"), "c"));
646         assert_eq!(split_path("/a/b/c/d"), (Some("/a/b/c"), "d"));
647         assert_eq!(split_path("a/b/c"), (Some("a/b"), "c"));
648 
649         assert_eq!(split_path("a"), (None, "a"));
650         assert_eq!(split_path("a/"), (None, "a"));
651         assert_eq!(split_path("/a"), (Some("/"), "a"));
652         assert_eq!(split_path(""), (None, ""));
653 
654         assert_eq!(
655             split_path("foo/아이쿱 조합원 앱카드"),
656             (Some("foo"), "아이쿱 조합원 앱카드")
657         );
658     }
659 
660     #[test]
661     fn test_split_path_bytes() {
662         assert_eq!(
663             split_path_bytes(&b"C:\\a\\b"[..]),
664             (Some(&b"C:\\a"[..]), &b"b"[..])
665         );
666         assert_eq!(
667             split_path_bytes(&b"C:/a\\b"[..]),
668             (Some(&b"C:/a"[..]), &b"b"[..])
669         );
670         assert_eq!(
671             split_path_bytes(&b"C:\\a\\b\\c"[..]),
672             (Some(&b"C:\\a\\b"[..]), &b"c"[..])
673         );
674         assert_eq!(
675             split_path_bytes(&b"a\\b\\c\\d\\e"[..]),
676             (Some(&b"a\\b\\c\\d"[..]), &b"e"[..])
677         );
678         assert_eq!(
679             split_path_bytes(&b"\\\\UNC\\a"[..]),
680             (Some(&b"\\\\UNC"[..]), &b"a"[..])
681         );
682 
683         assert_eq!(
684             split_path_bytes(&b"/a/b/c"[..]),
685             (Some(&b"/a/b"[..]), &b"c"[..])
686         );
687         assert_eq!(
688             split_path_bytes(&b"/a/b/c/d"[..]),
689             (Some(&b"/a/b/c"[..]), &b"d"[..])
690         );
691         assert_eq!(
692             split_path_bytes(&b"a/b/c"[..]),
693             (Some(&b"a/b"[..]), &b"c"[..])
694         );
695 
696         assert_eq!(split_path_bytes(&b"a"[..]), (None, &b"a"[..]));
697         assert_eq!(split_path_bytes(&b"a/"[..]), (None, &b"a"[..]));
698         assert_eq!(split_path_bytes(&b"/a"[..]), (Some(&b"/"[..]), &b"a"[..]));
699         assert_eq!(split_path_bytes(&b""[..]), (None, &b""[..]));
700     }
701 
702     #[test]
703     fn test_is_dsym_dir() {
704         assert!(fixture("macos/crash.dSYM").is_dsym_dir());
705         assert!(!fixture("macos/crash").is_dsym_dir());
706     }
707 
708     #[test]
709     fn test_resolve_dsym() {
710         let crash_path = fixture("macos/crash.dSYM");
711         let resolved = crash_path.resolve_dsym().unwrap();
712         assert!(resolved.exists());
713         assert!(resolved.ends_with("macos/crash.dSYM/Contents/Resources/DWARF/crash"));
714 
715         let other_path = fixture("macos/other.dSYM");
716         assert_eq!(other_path.resolve_dsym(), None);
717     }
718 
719     #[test]
720     fn test_dsym_parent() {
721         let crash_path = fixture("macos/crash.dSYM/Contents/Resources/DWARF/crash");
722         let dsym_path = crash_path.dsym_parent().unwrap();
723         assert!(dsym_path.exists());
724         assert!(dsym_path.ends_with("macos/crash.dSYM"));
725 
726         let other_path = fixture("macos/crash.dSYM/Contents/Resources/DWARF/invalid");
727         assert_eq!(other_path.dsym_parent(), None);
728     }
729 
730     #[test]
731     fn test_dsym_parent_framework() {
732         let dwarf_path = fixture("macos/Example.framework.dSYM/Contents/Resources/DWARF/Example");
733         let dsym_path = dwarf_path.dsym_parent().unwrap();
734         assert!(dsym_path.exists());
735         assert!(dsym_path.ends_with("macos/Example.framework.dSYM"));
736     }
737 }
738