use crate::inspect::FsKind;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, clap::ValueEnum)]
pub enum PathStyle {
#[default]
Unix,
Native,
}
fn native_separator(kind: FsKind) -> char {
match kind {
FsKind::Hfs | FsKind::HfsPlus => ':',
FsKind::Fat32 | FsKind::Exfat | FsKind::Ntfs => '\\',
_ => '/',
}
}
fn in_name_swap(kind: FsKind) -> bool {
matches!(kind, FsKind::Hfs | FsKind::HfsPlus)
}
pub fn to_canonical(user: &str, kind: FsKind, style: PathStyle) -> String {
if style == PathStyle::Unix {
return user.to_string();
}
let sep = native_separator(kind);
if sep == '/' {
return user.to_string();
}
if user.is_empty() || user == "/" {
return "/".to_string();
}
let absolute = user.starts_with(sep);
let swap = in_name_swap(kind);
let comps: Vec<String> = user
.split(sep)
.filter(|s| !s.is_empty())
.map(|s| {
if swap {
s.replace('/', ":")
} else {
s.to_string()
}
})
.collect();
if comps.is_empty() {
return "/".to_string();
}
let joined = comps.join("/");
if absolute {
format!("/{joined}")
} else {
joined
}
}
pub fn display_name(name: &str, kind: FsKind, style: PathStyle) -> String {
if style == PathStyle::Native && in_name_swap(kind) {
name.replace(':', "/")
} else {
name.to_string()
}
}
pub fn display_path(path: &str, kind: FsKind, style: PathStyle) -> String {
if style == PathStyle::Unix {
return path.to_string();
}
let sep = native_separator(kind);
if sep == '/' {
return path.to_string();
}
let swap = in_name_swap(kind);
let comps: Vec<String> = path
.split('/')
.filter(|s| !s.is_empty())
.map(|s| {
if swap {
s.replace(':', "/")
} else {
s.to_string()
}
})
.collect();
format!("{sep}{}", comps.join(&sep.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
const CANON: &str = "/Apps/A:ROSE Includes";
const NATIVE_HFS: &str = ":Apps:A/ROSE Includes";
#[test]
fn unix_style_is_identity_for_every_kind() {
for kind in [FsKind::Hfs, FsKind::HfsPlus, FsKind::Fat32, FsKind::Ext] {
assert_eq!(to_canonical(CANON, kind, PathStyle::Unix), CANON);
assert_eq!(display_path(CANON, kind, PathStyle::Unix), CANON);
assert_eq!(
display_name("A:ROSE Includes", kind, PathStyle::Unix),
"A:ROSE Includes"
);
}
}
#[test]
fn hfs_native_round_trip() {
assert_eq!(
to_canonical(NATIVE_HFS, FsKind::Hfs, PathStyle::Native),
CANON
);
assert_eq!(
display_path(CANON, FsKind::Hfs, PathStyle::Native),
NATIVE_HFS
);
assert_eq!(
to_canonical(
&display_path(CANON, FsKind::Hfs, PathStyle::Native),
FsKind::Hfs,
PathStyle::Native
),
CANON
);
assert_eq!(
display_name("A:ROSE Includes", FsKind::Hfs, PathStyle::Native),
"A/ROSE Includes"
);
}
#[test]
fn hfs_plus_behaves_like_hfs() {
assert_eq!(
to_canonical(NATIVE_HFS, FsKind::HfsPlus, PathStyle::Native),
CANON
);
assert_eq!(
display_path(CANON, FsKind::HfsPlus, PathStyle::Native),
NATIVE_HFS
);
}
#[test]
fn root_maps_to_bare_separator() {
assert_eq!(to_canonical("/", FsKind::Hfs, PathStyle::Native), "/");
assert_eq!(to_canonical(":", FsKind::Hfs, PathStyle::Native), "/");
assert_eq!(to_canonical("", FsKind::Hfs, PathStyle::Native), "/");
assert_eq!(display_path("/", FsKind::Hfs, PathStyle::Native), ":");
assert_eq!(display_path("/", FsKind::Fat32, PathStyle::Native), "\\");
}
#[test]
fn relative_paths_stay_relative() {
assert_eq!(
to_canonical("System Folder", FsKind::Hfs, PathStyle::Native),
"System Folder"
);
assert_eq!(
to_canonical("Apps:Tool", FsKind::Hfs, PathStyle::Native),
"Apps/Tool"
);
}
#[test]
fn fat_native_uses_backslash_without_in_name_swap() {
assert_eq!(
to_canonical("\\Windows\\System32", FsKind::Fat32, PathStyle::Native),
"/Windows/System32"
);
assert_eq!(
display_path("/Windows/System32", FsKind::Fat32, PathStyle::Native),
"\\Windows\\System32"
);
assert_eq!(display_name("a:b", FsKind::Fat32, PathStyle::Native), "a:b");
}
#[test]
fn non_hfs_unix_separator_kinds_are_identity_in_native() {
for kind in [FsKind::Ext, FsKind::Xfs, FsKind::Tar, FsKind::Iso9660] {
assert_eq!(to_canonical("/a/b", kind, PathStyle::Native), "/a/b");
assert_eq!(display_path("/a/b", kind, PathStyle::Native), "/a/b");
assert_eq!(display_name("b", kind, PathStyle::Native), "b");
}
}
}