ifc_lite_core/
step_encoding.rs1use std::borrow::Cow;
21
22pub fn decode_ifc_string(s: &str) -> Cow<'_, str> {
32 if !s.as_bytes().contains(&b'\\') {
33 return Cow::Borrowed(s);
34 }
35
36 let bytes = s.as_bytes();
37 let n = bytes.len();
38 let mut out = String::with_capacity(n);
39 let mut i = 0;
40
41 while i < n {
42 if bytes[i] != b'\\' {
43 let ch = s[i..].chars().next().unwrap();
46 out.push(ch);
47 i += ch.len_utf8();
48 continue;
49 }
50
51 if i + 3 < n && bytes[i + 1] == b'P' && bytes[i + 3] == b'\\' {
53 i += 4;
54 continue;
55 }
56
57 if i + 3 < n && bytes[i + 1] == b'S' && bytes[i + 2] == b'\\' {
61 let c = s[i + 3..].chars().next().unwrap();
62 let code = c as u32 + 128;
63 out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
64 i += 3 + c.len_utf8();
65 continue;
66 }
67
68 if i + 4 < n && bytes[i + 1] == b'X' && bytes[i + 2] == b'\\' {
70 if let (Some(hi), Some(lo)) = (hex_val(bytes[i + 3]), hex_val(bytes[i + 4])) {
71 let code = ((hi << 4) | lo) as u32;
72 out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
73 i += 5;
74 continue;
75 }
76 }
77
78 if starts_with(bytes, i, b"\\X2\\") {
81 if let Some(end) = find(bytes, i + 4, b"\\X0\\") {
82 let hex = &s[i + 4..end];
83 if !hex.is_empty()
84 && hex.len().is_multiple_of(4)
85 && hex.bytes().all(|c| c.is_ascii_hexdigit())
86 {
87 let units: Vec<u16> = (0..hex.len())
88 .step_by(4)
89 .map(|j| u16::from_str_radix(&hex[j..j + 4], 16).unwrap())
90 .collect();
91 out.push_str(&String::from_utf16_lossy(&units));
92 i = end + 4;
93 continue;
94 }
95 }
96 }
97
98 if starts_with(bytes, i, b"\\X4\\") {
100 if let Some(end) = find(bytes, i + 4, b"\\X0\\") {
101 let hex = &s[i + 4..end];
102 if !hex.is_empty()
103 && hex.len().is_multiple_of(8)
104 && hex.bytes().all(|c| c.is_ascii_hexdigit())
105 {
106 for j in (0..hex.len()).step_by(8) {
107 let v = u32::from_str_radix(&hex[j..j + 8], 16).unwrap();
108 out.push(char::from_u32(v).unwrap_or('\u{FFFD}'));
109 }
110 i = end + 4;
111 continue;
112 }
113 }
114 }
115
116 out.push('\\');
118 i += 1;
119 }
120
121 Cow::Owned(out)
122}
123
124pub fn encode_ifc_string(s: &str) -> Cow<'_, str> {
131 if s.bytes().all(|b| (0x20..=0x7E).contains(&b) && b != b'\\') {
132 return Cow::Borrowed(s);
133 }
134
135 let mut out = String::with_capacity(s.len());
136 for ch in s.chars() {
137 let cp = ch as u32;
138 if (0x20..=0x7E).contains(&cp) && ch != '\\' {
139 out.push(ch);
140 } else if cp <= 0xFF {
141 out.push_str(&format!("\\X\\{cp:02X}"));
142 } else if cp <= 0xFFFF {
143 out.push_str(&format!("\\X2\\{cp:04X}\\X0\\"));
144 } else {
145 out.push_str(&format!("\\X4\\{cp:08X}\\X0\\"));
146 }
147 }
148 Cow::Owned(out)
149}
150
151#[inline]
152fn hex_val(b: u8) -> Option<u8> {
153 match b {
154 b'0'..=b'9' => Some(b - b'0'),
155 b'a'..=b'f' => Some(b - b'a' + 10),
156 b'A'..=b'F' => Some(b - b'A' + 10),
157 _ => None,
158 }
159}
160
161#[inline]
162fn starts_with(bytes: &[u8], at: usize, pat: &[u8]) -> bool {
163 bytes.len() >= at + pat.len() && &bytes[at..at + pat.len()] == pat
164}
165
166fn find(bytes: &[u8], from: usize, pat: &[u8]) -> Option<usize> {
167 if pat.is_empty() || from + pat.len() > bytes.len() {
168 return None;
169 }
170 bytes[from..]
171 .windows(pat.len())
172 .position(|w| w == pat)
173 .map(|p| from + p)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn no_backslash_is_borrowed_and_unchanged() {
182 assert!(matches!(decode_ifc_string("Hello World"), Cow::Borrowed(_)));
183 assert_eq!(decode_ifc_string("3Bvg7$qHb0gP37$Qz2vN1k"), "3Bvg7$qHb0gP37$Qz2vN1k");
185 }
186
187 #[test]
188 fn decodes_x2_bmp() {
189 assert_eq!(decode_ifc_string(r"Br\X2\00FC\X0\cke"), "Br\u{FC}cke");
190 }
191
192 #[test]
193 fn decodes_x2_surrogate_pair() {
194 assert_eq!(decode_ifc_string(r"\X2\D83DDE00\X0\"), "\u{1F600}");
195 }
196
197 #[test]
198 fn decodes_x4_astral() {
199 assert_eq!(decode_ifc_string(r"\X4\0001F600\X0\"), "\u{1F600}");
200 }
201
202 #[test]
203 fn decodes_x_and_s() {
204 assert_eq!(decode_ifc_string(r"\X\E9"), "\u{E9}");
205 assert_eq!(decode_ifc_string(r"\S\a"), "\u{E1}");
206 }
207
208 #[test]
209 fn drops_code_page_directive() {
210 assert_eq!(decode_ifc_string(r"\PA\Hello"), "Hello");
211 }
212
213 #[test]
214 fn keeps_unknown_escape() {
215 assert_eq!(decode_ifc_string(r"a\Qb"), r"a\Qb");
216 assert_eq!(decode_ifc_string(r"\X2\00FC"), r"\X2\00FC");
218 }
219
220 #[test]
221 fn s_escape_before_multibyte_char_does_not_panic() {
222 let _ = decode_ifc_string("\\S\\\u{00E9}tail");
226 let _ = decode_ifc_string("x\\S\\\u{1F600}y");
227 assert_eq!(decode_ifc_string(r"\S\a"), "\u{E1}");
229 }
230
231 #[test]
232 fn round_trips_through_encode() {
233 for s in ["plain", "Br\u{FC}cke", "\u{1F600}", "a\u{E9}b"] {
234 assert_eq!(decode_ifc_string(&encode_ifc_string(s)), s);
235 }
236 }
237}