use std::borrow::Cow;
use std::path::{Path, PathBuf};
trait ToChar {
fn to_char(self) -> char;
}
impl ToChar for char {
fn to_char(self) -> char {
self
}
}
impl ToChar for u8 {
fn to_char(self) -> char {
char::from(self)
}
}
impl<T: ToChar + Copy> ToChar for &'_ T {
fn to_char(self) -> char {
(*self).to_char()
}
}
#[inline]
fn is_path_separator<C: ToChar>(c: C) -> bool {
matches!(c.to_char(), '\\' | '/')
}
#[inline]
fn is_windows_separator<C: ToChar>(c: C) -> bool {
is_path_separator(c)
}
#[inline]
fn is_unix_separator<C: ToChar>(c: C) -> bool {
c.to_char() == '/'
}
fn is_windows_unc<P: AsRef<[u8]>>(path: P) -> bool {
let path = path.as_ref();
path.starts_with(b"\\\\") || path.starts_with(b"//")
}
fn is_windows_driveletter<P: AsRef<[u8]>>(path: P) -> bool {
let path = path.as_ref();
if let (Some(drive_letter), Some(b':')) = (path.get(0), path.get(1)) {
if matches!(drive_letter, b'A'..=b'Z' | b'a'..=b'z') {
return path.get(2).map_or(true, is_windows_separator);
}
}
false
}
fn is_absolute_windows_path<P: AsRef<[u8]>>(path: P) -> bool {
let path = path.as_ref();
is_windows_unc(path) || is_windows_driveletter(path)
}
fn is_semi_absolute_windows_path<P: AsRef<[u8]>>(path: P) -> bool {
path.as_ref().get(0).map_or(false, is_windows_separator)
}
fn is_absolute_unix_path<P: AsRef<[u8]>>(path: P) -> bool {
path.as_ref().get(0).map_or(false, is_unix_separator)
}
fn is_windows_path<P: AsRef<[u8]>>(path: P) -> bool {
let path = path.as_ref();
is_absolute_windows_path(path) || path.contains(&b'\\')
}
pub fn join_path(base: &str, other: &str) -> String {
if other.starts_with('<') && other.ends_with('>') {
return other.into();
}
if base.is_empty() || is_absolute_windows_path(other) || is_absolute_unix_path(other) {
return other.into();
}
if other.is_empty() {
return base.into();
}
if is_semi_absolute_windows_path(other) {
if is_absolute_windows_path(base) {
return format!("{}{}", &base[..2], other);
} else {
return other.into();
}
}
let is_windows = is_windows_path(base) || is_windows_path(other);
format!(
"{}{}{}",
base.trim_end_matches(is_path_separator),
if is_windows { '\\' } else { '/' },
other.trim_start_matches(is_path_separator)
)
}
fn pop_path(path: &mut String) -> bool {
if let Some(idx) = path.rfind(is_path_separator) {
path.truncate(idx);
true
} else if !path.is_empty() {
path.truncate(0);
true
} else {
false
}
}
pub fn clean_path(path: &str) -> Cow<'_, str> {
let mut rv = String::with_capacity(path.len());
let main_separator = if is_windows_path(path) { '\\' } else { '/' };
let mut needs_separator = false;
let mut is_past_root = false;
for segment in path.split_terminator(is_path_separator) {
if segment == "." {
continue;
} else if segment == ".." {
if !is_past_root && pop_path(&mut rv) {
if rv.is_empty() {
needs_separator = false;
}
continue;
} else {
if !is_past_root {
needs_separator = false;
is_past_root = true;
}
if needs_separator {
rv.push(main_separator);
}
rv.push_str("..");
needs_separator = true;
continue;
}
}
if needs_separator {
rv.push(main_separator);
} else {
needs_separator = true;
}
rv.push_str(segment);
}
Cow::Owned(rv)
}
pub fn split_path_bytes(path: &[u8]) -> (Option<&[u8]>, &[u8]) {
let path = match path.iter().rposition(|c| !is_path_separator(c)) {
Some(cutoff) => &path[..=cutoff],
None => path,
};
match path.iter().rposition(is_path_separator) {
Some(0) => (Some(&path[..1]), &path[1..]),
Some(pos) => (Some(&path[..pos]), &path[pos + 1..]),
None => (None, path),
}
}
pub fn split_path(path: &str) -> (Option<&str>, &str) {
let (dir, name) = split_path_bytes(path.as_bytes());
unsafe {
(
dir.map(|b| std::str::from_utf8_unchecked(b)),
std::str::from_utf8_unchecked(name),
)
}
}
fn truncate(path: &str, mut length: usize) -> &str {
while !path.is_char_boundary(length) {
length -= 1;
}
path.get(..length).unwrap_or_default()
}
pub fn shorten_path(path: &str, length: usize) -> Cow<'_, str> {
if path.len() <= length {
return Cow::Borrowed(path);
} else if length <= 3 {
return Cow::Borrowed(truncate(path, length));
} else if length <= 10 {
return Cow::Owned(format!("{}...", truncate(path, length - 3)));
}
let mut rv = String::new();
let mut last_idx = 0;
let mut piece_iter = path.match_indices(is_path_separator);
let mut final_sep = "/";
let max_len = length - 4;
while let Some((idx, sep)) = piece_iter.next() {
let slice = &path[last_idx..idx + sep.len()];
rv.push_str(slice);
let done = last_idx > 0;
last_idx = idx + sep.len();
final_sep = sep;
if done {
break;
}
}
let mut final_length = rv.len() as i64;
let mut rest = vec![];
let mut next_idx = path.len();
while let Some((idx, _)) = piece_iter.next_back() {
if idx <= last_idx {
break;
}
let slice = &path[idx + 1..next_idx];
if final_length + (slice.len() as i64) > max_len as i64 {
break;
}
rest.push(slice);
next_idx = idx + 1;
final_length += slice.len() as i64;
}
if rv.len() > max_len || rest.is_empty() {
let basename = path.rsplit(is_path_separator).next().unwrap();
if basename.len() > max_len {
return Cow::Owned(format!("...{}", &basename[basename.len() - max_len + 1..]));
} else {
return Cow::Owned(format!("...{}{}", final_sep, basename));
}
}
rest.reverse();
rv.push_str("...");
rv.push_str(final_sep);
for item in rest {
rv.push_str(&item);
}
Cow::Owned(rv)
}
pub trait DSymPathExt {
fn is_dsym_dir(&self) -> bool;
fn resolve_dsym(&self) -> Option<PathBuf>;
fn dsym_parent(&self) -> Option<&Path>;
}
impl DSymPathExt for Path {
fn is_dsym_dir(&self) -> bool {
self.extension() == Some("dSYM".as_ref()) && self.is_dir()
}
fn resolve_dsym(&self) -> Option<PathBuf> {
if !self.is_dsym_dir() || !self.is_dir() {
return None;
}
let framework = self.file_stem()?;
let mut full_path = self.to_path_buf();
full_path.push("Contents/Resources/DWARF");
full_path.push(framework);
if full_path.is_file() {
Some(full_path)
} else {
None
}
}
fn dsym_parent(&self) -> Option<&Path> {
let framework = self.file_name()?;
let mut parent = self.parent()?;
if !parent.ends_with("Contents/Resources/DWARF") {
return None;
}
for _ in 0..3 {
parent = parent.parent()?;
}
if parent.file_stem() == Some(framework) && parent.is_dsym_dir() {
Some(parent)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use symbolic_testutils::fixture;
#[test]
fn test_join_path() {
assert_eq!(join_path("foo", "C:"), "C:");
assert_eq!(join_path("foo", "C:bar"), "foo/C:bar");
assert_eq!(join_path("C:\\a", "b"), "C:\\a\\b");
assert_eq!(join_path("C:/a", "b"), "C:/a\\b");
assert_eq!(join_path("C:\\a", "b\\c"), "C:\\a\\b\\c");
assert_eq!(join_path("C:/a", "C:\\b"), "C:\\b");
assert_eq!(join_path("a\\b\\c", "d\\e"), "a\\b\\c\\d\\e");
assert_eq!(join_path("\\\\UNC\\", "a"), "\\\\UNC\\a");
assert_eq!(join_path("C:\\foo/bar", "\\baz"), "C:\\baz");
assert_eq!(join_path("\\foo/bar", "\\baz"), "\\baz");
assert_eq!(join_path("/a/b", "\\c"), "\\c");
assert_eq!(join_path("/a/b", "c"), "/a/b/c");
assert_eq!(join_path("/a/b", "c/d"), "/a/b/c/d");
assert_eq!(join_path("/a/b", "/c/d/e"), "/c/d/e");
assert_eq!(join_path("a/b/", "c"), "a/b/c");
assert_eq!(join_path("a/b/", "<stdin>"), "<stdin>");
assert_eq!(
join_path("C:\\test", "<::core::macros::assert_eq macros>"),
"<::core::macros::assert_eq macros>"
);
assert_eq!(
join_path("foo", "아이쿱 조합원 앱카드"),
"foo/아이쿱 조합원 앱카드"
);
}
#[test]
fn test_clean_path() {
assert_eq!(clean_path("/foo/bar/baz/./blah"), "/foo/bar/baz/blah");
assert_eq!(clean_path("/foo/bar/baz/./blah/"), "/foo/bar/baz/blah");
assert_eq!(clean_path("foo/bar/baz/./blah/"), "foo/bar/baz/blah");
assert_eq!(clean_path("foo/bar/baz/../blah/"), "foo/bar/blah");
assert_eq!(clean_path("../../blah/"), "../../blah");
assert_eq!(clean_path("..\\../blah/"), "..\\..\\blah");
assert_eq!(clean_path("foo\\bar\\baz/../blah/"), "foo\\bar\\blah");
assert_eq!(clean_path("foo\\bar\\baz/../../../../blah/"), "..\\blah");
assert_eq!(clean_path("foo/bar/baz/../../../../blah/"), "../blah");
assert_eq!(clean_path("..\\foo"), "..\\foo");
assert_eq!(clean_path("foo"), "foo");
assert_eq!(clean_path("foo\\bar\\baz/../../../blah/"), "blah");
assert_eq!(clean_path("foo/bar/baz/../../../blah/"), "blah");
assert_eq!(clean_path("\\\\foo\\..\\bar"), "\\\\bar");
assert_eq!(
clean_path("foo/bar/../아이쿱 조합원 앱카드"),
"foo/아이쿱 조합원 앱카드"
);
}
#[test]
fn test_shorten_path() {
assert_eq!(shorten_path("/foo/bar/baz/blah/blafasel", 6), "/fo...");
assert_eq!(shorten_path("/foo/bar/baz/blah/blafasel", 2), "/f");
assert_eq!(
shorten_path("/foo/bar/baz/blah/blafasel", 21),
"/foo/.../blafasel"
);
assert_eq!(
shorten_path("/foo/bar/baz/blah/blafasel", 22),
"/foo/.../blah/blafasel"
);
assert_eq!(
shorten_path("C:\\bar\\baz\\blah\\blafasel", 20),
"C:\\bar\\...\\blafasel"
);
assert_eq!(
shorten_path("/foo/blar/baz/blah/blafasel", 27),
"/foo/blar/baz/blah/blafasel"
);
assert_eq!(
shorten_path("/foo/blar/baz/blah/blafasel", 26),
"/foo/.../baz/blah/blafasel"
);
assert_eq!(
shorten_path("/foo/b/baz/blah/blafasel", 23),
"/foo/.../blah/blafasel"
);
assert_eq!(shorten_path("/foobarbaz/blahblah", 16), ".../blahblah");
assert_eq!(shorten_path("/foobarbazblahblah", 12), "...lahblah");
assert_eq!(shorten_path("", 0), "");
assert_eq!(shorten_path("아이쿱 조합원 앱카드", 9), "아...");
assert_eq!(shorten_path("아이쿱 조합원 앱카드", 20), "...ᆸ카드");
}
#[test]
fn test_split_path() {
assert_eq!(split_path("C:\\a\\b"), (Some("C:\\a"), "b"));
assert_eq!(split_path("C:/a\\b"), (Some("C:/a"), "b"));
assert_eq!(split_path("C:\\a\\b\\c"), (Some("C:\\a\\b"), "c"));
assert_eq!(split_path("a\\b\\c\\d\\e"), (Some("a\\b\\c\\d"), "e"));
assert_eq!(split_path("\\\\UNC\\a"), (Some("\\\\UNC"), "a"));
assert_eq!(split_path("/a/b/c"), (Some("/a/b"), "c"));
assert_eq!(split_path("/a/b/c/d"), (Some("/a/b/c"), "d"));
assert_eq!(split_path("a/b/c"), (Some("a/b"), "c"));
assert_eq!(split_path("a"), (None, "a"));
assert_eq!(split_path("a/"), (None, "a"));
assert_eq!(split_path("/a"), (Some("/"), "a"));
assert_eq!(split_path(""), (None, ""));
assert_eq!(
split_path("foo/아이쿱 조합원 앱카드"),
(Some("foo"), "아이쿱 조합원 앱카드")
);
}
#[test]
fn test_split_path_bytes() {
assert_eq!(
split_path_bytes(&b"C:\\a\\b"[..]),
(Some(&b"C:\\a"[..]), &b"b"[..])
);
assert_eq!(
split_path_bytes(&b"C:/a\\b"[..]),
(Some(&b"C:/a"[..]), &b"b"[..])
);
assert_eq!(
split_path_bytes(&b"C:\\a\\b\\c"[..]),
(Some(&b"C:\\a\\b"[..]), &b"c"[..])
);
assert_eq!(
split_path_bytes(&b"a\\b\\c\\d\\e"[..]),
(Some(&b"a\\b\\c\\d"[..]), &b"e"[..])
);
assert_eq!(
split_path_bytes(&b"\\\\UNC\\a"[..]),
(Some(&b"\\\\UNC"[..]), &b"a"[..])
);
assert_eq!(
split_path_bytes(&b"/a/b/c"[..]),
(Some(&b"/a/b"[..]), &b"c"[..])
);
assert_eq!(
split_path_bytes(&b"/a/b/c/d"[..]),
(Some(&b"/a/b/c"[..]), &b"d"[..])
);
assert_eq!(
split_path_bytes(&b"a/b/c"[..]),
(Some(&b"a/b"[..]), &b"c"[..])
);
assert_eq!(split_path_bytes(&b"a"[..]), (None, &b"a"[..]));
assert_eq!(split_path_bytes(&b"a/"[..]), (None, &b"a"[..]));
assert_eq!(split_path_bytes(&b"/a"[..]), (Some(&b"/"[..]), &b"a"[..]));
assert_eq!(split_path_bytes(&b""[..]), (None, &b""[..]));
}
#[test]
fn test_is_dsym_dir() {
assert!(fixture("macos/crash.dSYM").is_dsym_dir());
assert!(!fixture("macos/crash").is_dsym_dir());
}
#[test]
fn test_resolve_dsym() {
let crash_path = fixture("macos/crash.dSYM");
let resolved = crash_path.resolve_dsym().unwrap();
assert!(resolved.exists());
assert!(resolved.ends_with("macos/crash.dSYM/Contents/Resources/DWARF/crash"));
let other_path = fixture("macos/other.dSYM");
assert_eq!(other_path.resolve_dsym(), None);
}
#[test]
fn test_dsym_parent() {
let crash_path = fixture("macos/crash.dSYM/Contents/Resources/DWARF/crash");
let dsym_path = crash_path.dsym_parent().unwrap();
assert!(dsym_path.exists());
assert!(dsym_path.ends_with("macos/crash.dSYM"));
let other_path = fixture("macos/crash.dSYM/Contents/Resources/DWARF/invalid");
assert_eq!(other_path.dsym_parent(), None);
}
}