//! macOS Unicode Private Range Mapping for SMB //! //! macOS SMB client maps NTFS illegal characters to Unicode private range. //! Reference: Samba vfs_catia.c and vfs_fruit.c encoding handling //! //! Full mapping table (Samba catia standard): //! U+F001 → / (0x2F) //! U+F002 → : (0x3A) //! U+F003 → * (0x2A) //! U+F004 → ? (0x3F) //! U+F005 → " (0x22) //! U+F006 → < (0x3C) //! U+F007 → > (0x3E) //! U+F008 → | (0x7C) //! U+F009 → \ (0x5C) //! U+F02A → : (0x3A) — macOS Finder uses this for colon pub const FRUIT_ENC_NATIVE: bool = true; pub const FRUIT_ENC_PRIVATE: bool = false; // Apple private range code points (vfs_catia mapping) const APPLE_SLASH: u16 = 0xF001; const APPLE_COLON_ALT: u16 = 0xF002; const APPLE_ASTERISK: u16 = 0xF003; const APPLE_QUESTION: u16 = 0xF004; const APPLE_QUOTE: u16 = 0xF005; const APPLE_LESS_THAN: u16 = 0xF006; const APPLE_GREATER_THAN: u16 = 0xF007; const APPLE_PIPE: u16 = 0xF008; const APPLE_BACKSLASH: u16 = 0xF009; const APPLE_COLON: u16 = 0xF02A; // macOS Finder specific const ASCII_SLASH: u16 = '/' as u16; const ASCII_COLON: u16 = ':' as u16; const ASCII_ASTERISK: u16 = '*' as u16; const ASCII_QUESTION: u16 = '?' as u16; const ASCII_QUOTE: u16 = '"' as u16; const ASCII_LESS_THAN: u16 = '<' as u16; const ASCII_GREATER_THAN: u16 = '>' as u16; const ASCII_PIPE: u16 = '|' as u16; const ASCII_BACKSLASH: u16 = '\\' as u16; /// Check if a UTF-16 code unit is in the macOS private range. pub fn is_private_range_char(u: u16) -> bool { matches!(u, APPLE_SLASH | APPLE_COLON_ALT | APPLE_ASTERISK | APPLE_QUESTION | APPLE_QUOTE | APPLE_LESS_THAN | APPLE_GREATER_THAN | APPLE_PIPE | APPLE_BACKSLASH | APPLE_COLON ) } pub fn map_private_to_ascii(units: &[u16]) -> Vec { units.iter().map(|u| { match *u { APPLE_SLASH => ASCII_SLASH, APPLE_COLON | APPLE_COLON_ALT => ASCII_COLON, APPLE_ASTERISK => ASCII_ASTERISK, APPLE_QUESTION => ASCII_QUESTION, APPLE_QUOTE => ASCII_QUOTE, APPLE_LESS_THAN => ASCII_LESS_THAN, APPLE_GREATER_THAN => ASCII_GREATER_THAN, APPLE_PIPE => ASCII_PIPE, APPLE_BACKSLASH => ASCII_BACKSLASH, _ => *u, } }).collect() } pub fn map_ascii_to_private(units: &[u16]) -> Vec { units.iter().map(|u| { match *u { ASCII_SLASH => APPLE_SLASH, ASCII_COLON => APPLE_COLON, ASCII_ASTERISK => APPLE_ASTERISK, ASCII_QUESTION => APPLE_QUESTION, ASCII_QUOTE => APPLE_QUOTE, ASCII_LESS_THAN => APPLE_LESS_THAN, ASCII_GREATER_THAN => APPLE_GREATER_THAN, ASCII_PIPE => APPLE_PIPE, ASCII_BACKSLASH => APPLE_BACKSLASH, _ => *u, } }).collect() } pub fn has_private_range_chars(units: &[u16]) -> bool { units.iter().any(|u| is_private_range_char(*u)) } pub fn has_ntfs_illegal_chars(units: &[u16]) -> bool { units.iter().any(|u| { matches!(*u, ASCII_SLASH | ASCII_COLON | ASCII_ASTERISK | ASCII_QUESTION | ASCII_QUOTE | ASCII_LESS_THAN | ASCII_GREATER_THAN | ASCII_PIPE | ASCII_BACKSLASH ) }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_map_private_to_ascii() { let input = [APPLE_SLASH, APPLE_COLON, APPLE_QUESTION]; let output = map_private_to_ascii(&input); assert_eq!(output, [ASCII_SLASH, ASCII_COLON, ASCII_QUESTION]); } #[test] fn test_map_private_to_ascii_all() { let input = [ APPLE_SLASH, APPLE_COLON_ALT, APPLE_ASTERISK, APPLE_QUESTION, APPLE_QUOTE, APPLE_LESS_THAN, APPLE_GREATER_THAN, APPLE_PIPE, APPLE_BACKSLASH, APPLE_COLON, ]; let output = map_private_to_ascii(&input); assert_eq!(output, [ ASCII_SLASH, ASCII_COLON, ASCII_ASTERISK, ASCII_QUESTION, ASCII_QUOTE, ASCII_LESS_THAN, ASCII_GREATER_THAN, ASCII_PIPE, ASCII_BACKSLASH, ASCII_COLON, ]); } #[test] fn test_map_ascii_to_private() { let input = [ASCII_SLASH, ASCII_COLON, ASCII_ASTERISK]; let output = map_ascii_to_private(&input); assert_eq!(output, [APPLE_SLASH, APPLE_COLON, APPLE_ASTERISK]); } #[test] fn test_map_ascii_to_private_all() { let input = [ ASCII_SLASH, ASCII_COLON, ASCII_ASTERISK, ASCII_QUESTION, ASCII_QUOTE, ASCII_LESS_THAN, ASCII_GREATER_THAN, ASCII_PIPE, ASCII_BACKSLASH, ]; let output = map_ascii_to_private(&input); assert_eq!(output, [ APPLE_SLASH, APPLE_COLON, APPLE_ASTERISK, APPLE_QUESTION, APPLE_QUOTE, APPLE_LESS_THAN, APPLE_GREATER_THAN, APPLE_PIPE, APPLE_BACKSLASH, ]); } #[test] fn test_roundtrip() { let original = [ASCII_SLASH, ASCII_COLON, 'a' as u16]; let to_private = map_ascii_to_private(&original); let back_to_ascii = map_private_to_ascii(&to_private); assert_eq!(back_to_ascii, original); } #[test] fn test_has_private_range_chars() { assert!(has_private_range_chars(&[APPLE_SLASH, 'a' as u16])); assert!(!has_private_range_chars(&[ASCII_SLASH, 'a' as u16])); } #[test] fn test_has_ntfs_illegal_chars() { assert!(has_ntfs_illegal_chars(&[ASCII_SLASH, 'a' as u16])); assert!(!has_ntfs_illegal_chars(&['a' as u16, 'b' as u16])); } #[test] fn test_preserve_non_mapped() { let input = ['a' as u16, 'b' as u16, 'c' as u16]; let output = map_private_to_ascii(&input); assert_eq!(output, input); } #[test] fn test_is_private_range_char() { assert!(is_private_range_char(APPLE_SLASH)); assert!(is_private_range_char(APPLE_COLON)); assert!(!is_private_range_char('a' as u16)); } }