use alloc::borrow::Cow;
#[cfg(target_vendor = "apple")]
use crate::ErrorKind;
use crate::error::ResultKind;
#[cfg(any(test, feature = "__test"))]
use crate::unicode::nfd;
#[cfg(any(
target_os = "windows",
target_vendor = "apple",
test,
feature = "__test"
))]
use crate::utils::cow;
#[cfg(any(target_os = "windows", test, feature = "__test"))]
const WINDOWS_RESERVED: &[&str] = &[
"con",
"prn",
"aux",
"nul",
"com0",
"com1",
"com2",
"com3",
"com4",
"com5",
"com6",
"com7",
"com8",
"com9",
"com\u{00B9}",
"com\u{00B2}",
"com\u{00B3}",
"lpt0",
"lpt1",
"lpt2",
"lpt3",
"lpt4",
"lpt5",
"lpt6",
"lpt7",
"lpt8",
"lpt9",
"lpt\u{00B9}",
"lpt\u{00B2}",
"lpt\u{00B3}",
];
#[cfg(any(target_os = "windows", test, feature = "__test"))]
fn map_windows_forbidden(c: char) -> char {
match c {
'<' => '\u{FF1C}',
'>' => '\u{FF1E}',
':' => '\u{FF1A}',
'"' => '\u{FF02}',
'\\' => '\u{FF3C}',
'|' => '\u{FF5C}',
'?' => '\u{FF1F}',
'*' => '\u{FF0A}',
_ => c,
}
}
#[cfg(any(target_os = "windows", test, feature = "__test"))]
#[must_use]
pub fn is_reserved_on_windows(name: &str) -> bool {
let stem = name.split('.').next().unwrap_or(name);
let lowered = stem.to_ascii_lowercase();
WINDOWS_RESERVED.iter().any(|r| **r == *lowered)
}
#[cfg(any(target_os = "windows", test, feature = "__test"))]
pub fn windows_compatible_from_normalized_cs(s: &str) -> Cow<'_, str> {
let mut result = cow(s.chars().map(map_windows_forbidden), s);
if result.ends_with('.') {
let owned = result.to_mut();
owned.pop();
owned.push('\u{FF0E}');
}
if is_reserved_on_windows(&result) {
let mut owned = result.into_owned();
let first = owned.as_bytes()[0];
debug_assert!(
first.is_ascii_alphabetic(),
"reserved name starts with non-ASCII-letter: {:?}",
first as char,
);
let fullwidth = char::from_u32(u32::from(first) + 0xFEE0)
.expect("reserved name starts with non-ASCII-alphabetic byte");
let mut buf = [0u8; 3];
owned.replace_range(..1, fullwidth.encode_utf8(&mut buf));
result = Cow::Owned(owned);
}
result
}
#[cfg(any(test, feature = "__test"))]
#[must_use]
pub fn apple_compatible_from_normalized_cs_fallback(s: &str) -> Cow<'_, str> {
cow(nfd(s).trim_start_matches('\u{FEFF}').chars(), s)
}
#[cfg(target_vendor = "apple")]
pub fn apple_compatible_from_normalized_cs(s: &str) -> ResultKind<Cow<'_, str>> {
use objc2_core_foundation::CFString;
let cf = CFString::from_str(s);
let max_len = cf.maximum_size_of_file_system_representation();
let mut buf = alloc::vec![0u8; max_len as usize];
let ok = unsafe { cf.file_system_representation(buf.as_mut_ptr().cast(), max_len) };
if ok {
let nul = buf
.iter()
.position(|&b| b == 0)
.ok_or(ErrorKind::GetFileSystemRepresentationError)?;
let result = core::str::from_utf8(&buf[..nul])
.map_err(|_| ErrorKind::GetFileSystemRepresentationError)?;
Ok(cow(result.chars(), s))
} else {
Err(ErrorKind::GetFileSystemRepresentationError)
}
}
#[cfg(all(not(target_vendor = "apple"), any(test, feature = "__test")))]
#[allow(clippy::unnecessary_wraps)]
pub fn apple_compatible_from_normalized_cs(s: &str) -> ResultKind<Cow<'_, str>> {
Ok(apple_compatible_from_normalized_cs_fallback(s))
}
#[cfg(target_os = "windows")]
#[allow(clippy::unnecessary_wraps)]
pub fn os_compatible_from_normalized_cs(s: &str) -> ResultKind<Cow<'_, str>> {
Ok(windows_compatible_from_normalized_cs(s))
}
#[cfg(target_vendor = "apple")]
pub fn os_compatible_from_normalized_cs(s: &str) -> ResultKind<Cow<'_, str>> {
apple_compatible_from_normalized_cs(s)
}
#[cfg(not(any(target_os = "windows", target_vendor = "apple")))]
#[allow(clippy::unnecessary_wraps)]
pub fn os_compatible_from_normalized_cs(s: &str) -> ResultKind<Cow<'_, str>> {
Ok(Cow::Borrowed(s))
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use alloc::format;
#[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use wasm_bindgen_test::wasm_bindgen_test as test;
use super::{
apple_compatible_from_normalized_cs, is_reserved_on_windows,
os_compatible_from_normalized_cs, windows_compatible_from_normalized_cs,
};
use crate::unicode::{case_fold, nfd};
#[test]
fn win_forbidden_chars() {
assert_eq!(
windows_compatible_from_normalized_cs("a<b>c").as_ref(),
"a\u{FF1C}b\u{FF1E}c"
);
}
#[test]
fn win_all_forbidden() {
assert_eq!(
windows_compatible_from_normalized_cs("<>:\"\\|?*").as_ref(),
"\u{FF1C}\u{FF1E}\u{FF1A}\u{FF02}\u{FF3C}\u{FF5C}\u{FF1F}\u{FF0A}"
);
}
#[test]
fn win_trailing_dot() {
assert_eq!(
windows_compatible_from_normalized_cs("file.").as_ref(),
"file\u{FF0E}"
);
}
#[test]
fn win_trailing_dots() {
assert_eq!(
windows_compatible_from_normalized_cs("file..").as_ref(),
"file.\u{FF0E}"
);
}
#[test]
fn win_trailing_space_dot() {
assert_eq!(
windows_compatible_from_normalized_cs("file .").as_ref(),
"file \u{FF0E}"
);
}
#[test]
fn win_reserved_presentation_nul() {
assert_eq!(
windows_compatible_from_normalized_cs("nul").as_ref(),
"\u{FF4E}ul"
);
}
#[test]
fn win_reserved_presentation_with_ext() {
assert_eq!(
windows_compatible_from_normalized_cs("nul.txt").as_ref(),
"\u{FF4E}ul.txt"
);
}
#[test]
fn win_reserved_presentation_com1() {
assert_eq!(
windows_compatible_from_normalized_cs("COM1").as_ref(),
"\u{FF23}OM1"
);
}
#[test]
fn win_normal_unchanged() {
let result = windows_compatible_from_normalized_cs("hello.txt");
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result.as_ref(), "hello.txt");
}
#[test]
fn reserved_basic_names() {
assert!(is_reserved_on_windows("con"));
assert!(is_reserved_on_windows("prn"));
assert!(is_reserved_on_windows("aux"));
assert!(is_reserved_on_windows("nul"));
}
#[test]
fn reserved_case_folded() {
assert!(is_reserved_on_windows("CON"));
assert!(is_reserved_on_windows("Con"));
assert!(is_reserved_on_windows("NUL"));
assert!(is_reserved_on_windows("Aux"));
}
#[test]
fn reserved_com_digits() {
for i in 0..=9 {
assert!(is_reserved_on_windows(&format!("com{i}")));
assert!(is_reserved_on_windows(&format!("COM{i}")));
assert!(is_reserved_on_windows(&format!("lpt{i}")));
assert!(is_reserved_on_windows(&format!("LPT{i}")));
}
}
#[test]
fn reserved_com_superscript() {
assert!(is_reserved_on_windows("COM\u{00B9}"));
assert!(is_reserved_on_windows("COM\u{00B2}"));
assert!(is_reserved_on_windows("COM\u{00B3}"));
assert!(is_reserved_on_windows("LPT\u{00B9}"));
assert!(is_reserved_on_windows("LPT\u{00B2}"));
assert!(is_reserved_on_windows("LPT\u{00B3}"));
}
#[test]
fn reserved_with_extension() {
assert!(is_reserved_on_windows("nul.txt"));
assert!(is_reserved_on_windows("CON.log"));
assert!(is_reserved_on_windows("COM1.dat"));
assert!(is_reserved_on_windows("COM\u{00B3}.txt"));
}
#[test]
fn not_reserved_longer_stem() {
assert!(!is_reserved_on_windows("CONX"));
assert!(!is_reserved_on_windows("nully"));
assert!(!is_reserved_on_windows("com10"));
assert!(!is_reserved_on_windows("lpt10"));
assert!(!is_reserved_on_windows("auxiliary"));
}
#[test]
fn not_reserved_stem_split_by_dot() {
assert!(!is_reserved_on_windows("nu.l"));
}
#[test]
fn not_reserved_normal_files() {
assert!(!is_reserved_on_windows("hello.txt"));
assert!(!is_reserved_on_windows("readme"));
assert!(!is_reserved_on_windows(".gitignore"));
}
#[test]
fn reserved_stable_under_nfd() {
for name in ["con", "nul", "COM1", "COM\u{00B9}", "LPT\u{00B3}"] {
assert_eq!(
is_reserved_on_windows(name),
is_reserved_on_windows(&nfd(name)),
"NFD changed reserved status for {name:?}"
);
}
}
#[test]
fn reserved_stable_under_case_fold() {
for name in ["CON", "Nul", "COM1", "LPT\u{00B2}"] {
assert_eq!(
is_reserved_on_windows(name),
is_reserved_on_windows(&case_fold(name)),
"case_fold changed reserved status for {name:?}"
);
}
}
#[test]
fn apple_nfd_and_remove_bom() {
assert_eq!(
apple_compatible_from_normalized_cs("\u{FEFF}\u{00E9}")
.unwrap()
.as_ref(),
"e\u{0301}"
);
}
#[test]
fn apple_ascii_unchanged() {
let result = apple_compatible_from_normalized_cs("hello").unwrap();
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result.as_ref(), "hello");
}
#[test]
fn apple_bom_removal_borrows() {
let input = "\u{FEFF}hello";
let result = apple_compatible_from_normalized_cs(input).unwrap();
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result.as_ref(), "hello");
assert!(core::ptr::eq(
result.as_ptr(),
input["\u{FEFF}".len()..].as_ptr()
));
}
#[test]
fn os_compatible_from_normalized_cs_ascii_unchanged() {
assert_eq!(
os_compatible_from_normalized_cs("hello.txt")
.unwrap()
.as_ref(),
"hello.txt"
);
}
#[test]
fn os_compatible_from_normalized_cs_forbidden_chars() {
let result = os_compatible_from_normalized_cs("a<b").unwrap();
#[cfg(target_os = "windows")]
assert_eq!(result.as_ref(), "a\u{FF1C}b");
#[cfg(not(target_os = "windows"))]
assert_eq!(result.as_ref(), "a<b");
}
#[test]
fn os_compatible_from_normalized_cs_reserved_name() {
let result = os_compatible_from_normalized_cs("nul").unwrap();
#[cfg(target_os = "windows")]
assert_eq!(result.as_ref(), "\u{FF4E}ul");
#[cfg(not(target_os = "windows"))]
assert_eq!(result.as_ref(), "nul");
}
#[test]
fn os_compatible_from_normalized_cs_nfc_input() {
let result = os_compatible_from_normalized_cs("\u{00E9}").unwrap();
#[cfg(target_vendor = "apple")]
assert_eq!(result.as_ref(), "e\u{0301}");
#[cfg(not(target_vendor = "apple"))]
assert_eq!(result.as_ref(), "\u{00E9}");
}
#[test]
fn os_compatible_from_normalized_cs_bom() {
let result = os_compatible_from_normalized_cs("\u{FEFF}hello").unwrap();
#[cfg(target_vendor = "apple")]
assert_eq!(result.as_ref(), "hello");
#[cfg(not(target_vendor = "apple"))]
assert_eq!(result.as_ref(), "\u{FEFF}hello");
}
}