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