1use std::borrow::Cow;
20
21const RAW_BASE: u32 = 0x0010_FF00;
23
24pub fn decode(bytes: &[u8]) -> String {
26 let mut out = String::with_capacity(bytes.len());
27 let mut rest = bytes;
28 loop {
29 match std::str::from_utf8(rest) {
30 Ok(s) => {
31 out.push_str(s);
32 return out;
33 }
34 Err(e) => {
35 let (good, bad) = rest.split_at(e.valid_up_to());
36 out.push_str(std::str::from_utf8(good).unwrap_or_default());
37 let n = e.error_len().unwrap_or(bad.len());
38 out.extend(bad[..n].iter().map(|&b| placeholder(b)));
39 rest = &bad[n..];
40 }
41 }
42 }
43}
44
45pub fn encode(name: &str) -> Cow<'_, [u8]> {
47 if !name.chars().any(|c| raw_byte(c).is_some()) {
48 return Cow::Borrowed(name.as_bytes());
49 }
50 let mut out = Vec::with_capacity(name.len());
51 let mut buf = [0u8; 4];
52 for c in name.chars() {
53 match raw_byte(c) {
54 Some(b) => out.push(b),
55 None => out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes()),
56 }
57 }
58 Cow::Owned(out)
59}
60
61pub fn raw_byte(c: char) -> Option<u8> {
63 let v = u32::from(c);
64 (RAW_BASE..=RAW_BASE + 0xFF)
65 .contains(&v)
66 .then(|| (v - RAW_BASE) as u8)
67}
68
69fn placeholder(b: u8) -> char {
70 char::from_u32(RAW_BASE + u32::from(b)).expect("Private Use Area-B is valid")
71}
72
73pub fn display(name: &str) -> Cow<'_, str> {
75 if !name.chars().any(|c| raw_byte(c).is_some()) {
76 return Cow::Borrowed(name);
77 }
78 let mut out = String::with_capacity(name.len());
79 for c in name.chars() {
80 match raw_byte(c) {
81 Some(b) => out.push_str(&format!("\\x{b:02X}")),
82 None => out.push(c),
83 }
84 }
85 Cow::Owned(out)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn utf8_names_pass_through_untouched() {
94 assert_eq!(decode(b"NeXTSTEP_3.3"), "NeXTSTEP_3.3");
95 assert_eq!(decode("caf\u{e9}".as_bytes()), "caf\u{e9}");
96 assert!(matches!(encode("plain"), Cow::Borrowed(_)));
97 }
98
99 #[test]
101 fn nextstep_and_euc_bytes_round_trip() {
102 for raw in [
103 &b"Steroidgrundger\xf6st.lookMol"[..],
104 &b"Eject\xbcE.tiff"[..],
105 &b"01OmniWeb \xa4\xd8\xa4\xe8\xa4\xa6\xa4\xb3\xa4\xbd.wdesk"[..],
106 &b"\xff\xfe"[..],
107 ] {
108 let name = decode(raw);
109 assert_eq!(&*encode(&name), raw, "{name:?}");
110 }
111 }
112}