1use super::encoding::{
13 TCodepoint, char_handling_error, char_to_utf8, hex_char_to_int, parse_next_utf8, parse_utf8,
14};
15use std::collections::HashMap;
16
17fn codepoint_to_escape(cp: TCodepoint) -> Option<&'static str> {
20 Some(match cp {
21 0x27 => "\\'", 0x22 => "\\\"", 0x3F => "\\?", 0x5C => "\\\\", 0x07 => "\\a",
26 0x08 => "\\b",
27 0x0C => "\\f",
28 0x0A => "\\n",
29 0x0D => "\\r",
30 0x09 => "\\t",
31 0x0B => "\\v",
32 0x00 => "\\0",
33 0x1B => "\\e",
34 _ => return None,
35 })
36}
37
38fn escape_to_codepoint(c: u8) -> Option<TCodepoint> {
41 Some(match c {
42 b'\'' => 0x27,
43 b'"' => 0x22,
44 b'?' => 0x3F,
45 b'\\' => 0x5C,
46 b'/' => 0x2F,
49 b'a' => 0x07,
50 b'b' => 0x08,
51 b'f' => 0x0C,
52 b'n' => 0x0A,
53 b'r' => 0x0D,
54 b't' => 0x09,
55 b'v' => 0x0B,
56 b'0' => 0x00,
57 b'e' => 0x1B,
58 _ => return None,
59 })
60}
61
62pub fn print_as_escaped(
69 codepoint: TCodepoint,
70 additional_escape_map: &HashMap<TCodepoint, String>,
71) -> String {
72 if let Some(s) = additional_escape_map.get(&codepoint) {
73 return s.clone();
74 }
75 if let Some(s) = codepoint_to_escape(codepoint) {
76 return s.to_string();
77 }
78 if (0x20..=0x7E).contains(&codepoint) {
79 return (codepoint as u8 as char).to_string();
80 }
81 let (prefix, width) = if codepoint <= 0xFF {
83 ('x', 2)
84 } else if codepoint <= 0xFFFF {
85 ('u', 4)
86 } else {
87 ('U', 8)
88 };
89 format!("\\{}{:0width$x}", prefix, codepoint, width = width)
90}
91
92pub fn print_byte_as_escaped(raw_char: u8) -> String {
95 print_as_escaped(raw_char as TCodepoint, &HashMap::new())
96}
97
98pub fn print_str_as_escaped(raw: &[u8]) -> String {
102 let mut res = String::new();
103 for cp in parse_utf8(raw, true) {
104 res.push_str(&print_as_escaped(cp, &HashMap::new()));
105 }
106 res
107}
108
109pub fn parse_next_escaped(
117 data: &[u8],
118 additional_escape_map: &HashMap<u8, TCodepoint>,
119) -> (TCodepoint, i32) {
120 if data.is_empty() || data[0] != b'\\' {
121 return (char_handling_error::INVALID_ESCAPE, 0);
122 }
123 if data.len() < 2 {
127 return (char_handling_error::INVALID_ESCAPE, 0);
128 }
129 let esc = data[1];
130 if (esc as i32) > 128 {
132 return (char_handling_error::INVALID_ESCAPE, 0);
133 }
134 if let Some(&cp) = additional_escape_map.get(&esc) {
135 return (cp, 2);
136 }
137 if let Some(cp) = escape_to_codepoint(esc) {
138 return (cp, 2);
139 }
140 match esc {
141 b'x' => {
142 let mut len = 0usize;
144 let mut codepoint: TCodepoint = 0;
145 while 2 + len < data.len() {
146 let digit = hex_char_to_int(data[2 + len]);
147 if digit == -1 {
148 break;
149 }
150 codepoint = codepoint * 16 + digit;
151 len += 1;
152 }
153 if len == 0 {
154 return (char_handling_error::INVALID_ESCAPE, 0);
155 }
156 (codepoint, (len + 2) as i32)
157 }
158 b'u' | b'U' => {
159 let len = if esc == b'u' { 4 } else { 8 };
160 let mut codepoint: TCodepoint = 0;
161 for i in 0..len {
162 if 2 + i >= data.len() {
163 return (char_handling_error::INVALID_ESCAPE, 0);
164 }
165 let digit = hex_char_to_int(data[2 + i]);
166 if digit == -1 {
167 return (char_handling_error::INVALID_ESCAPE, 0);
168 }
169 codepoint = codepoint * 16 + digit;
170 }
171 (codepoint, (len + 2) as i32)
172 }
173 _ => (char_handling_error::INVALID_ESCAPE, 0),
174 }
175}
176
177pub fn parse_next_utf8_or_escaped(
184 utf8: &[u8],
185 additional_escape_map: &HashMap<u8, TCodepoint>,
186) -> (TCodepoint, i32) {
187 if utf8.is_empty() || utf8[0] != b'\\' {
188 return parse_next_utf8(utf8);
189 }
190 parse_next_escaped(utf8, additional_escape_map)
191}
192
193pub fn unescape_string(
200 data: &[u8],
201 additional_escape_map: &HashMap<u8, TCodepoint>,
202) -> Result<Vec<u8>, TCodepoint> {
203 let mut out = Vec::new();
204 let mut pos = 0;
205 while pos < data.len() {
206 let (cp, n) = parse_next_utf8_or_escaped(&data[pos..], additional_escape_map);
207 if n == 0 {
208 return Err(cp);
209 }
210 out.extend_from_slice(&char_to_utf8(cp));
211 pos += n as usize;
212 }
213 Ok(out)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::char_handling_error::*;
219 use super::*;
220
221 fn empty_cp_map() -> HashMap<TCodepoint, String> {
222 HashMap::new()
223 }
224 fn empty_esc_map() -> HashMap<u8, TCodepoint> {
225 HashMap::new()
226 }
227
228 #[test]
229 fn escape_c_specials() {
230 assert_eq!(print_as_escaped(0x0A, &empty_cp_map()), "\\n");
231 assert_eq!(print_as_escaped(0x09, &empty_cp_map()), "\\t");
232 assert_eq!(print_as_escaped(0x0D, &empty_cp_map()), "\\r");
233 assert_eq!(print_as_escaped(0x5C, &empty_cp_map()), "\\\\");
234 assert_eq!(print_as_escaped(0x00, &empty_cp_map()), "\\0");
235 assert_eq!(print_as_escaped(0x1B, &empty_cp_map()), "\\e");
236 }
237
238 #[test]
239 fn escape_printable_passthrough() {
240 assert_eq!(print_as_escaped(0x41, &empty_cp_map()), "A");
241 assert_eq!(print_as_escaped(0x20, &empty_cp_map()), " ");
242 assert_eq!(print_as_escaped(0x7E, &empty_cp_map()), "~");
243 }
244
245 #[test]
246 fn escape_hex_forms() {
247 assert_eq!(print_as_escaped(0x01, &empty_cp_map()), "\\x01");
249 assert_eq!(print_as_escaped(0xFF, &empty_cp_map()), "\\xff");
250 assert_eq!(print_as_escaped(0x2603, &empty_cp_map()), "\\u2603");
252 assert_eq!(print_as_escaped(0x1F600, &empty_cp_map()), "\\U0001f600");
254 }
255
256 #[test]
257 fn escape_additional_map_wins() {
258 let mut m = empty_cp_map();
259 m.insert('-' as TCodepoint, "\\-".to_string());
260 assert_eq!(print_as_escaped('-' as TCodepoint, &m), "\\-");
261 }
262
263 #[test]
264 fn escape_byte_overload() {
265 assert_eq!(print_byte_as_escaped(b'A'), "A");
266 assert_eq!(print_byte_as_escaped(0x0A), "\\n");
267 assert_eq!(print_byte_as_escaped(0xFF), "\\xff");
268 }
269
270 #[test]
271 fn escape_whole_string() {
272 assert_eq!(print_str_as_escaped(b"a\nb"), "a\\nb");
273 assert_eq!(print_str_as_escaped(&[0x41, 0x80]), "A\\x80");
275 }
276
277 #[test]
278 fn parse_c_escapes() {
279 assert_eq!(parse_next_escaped(b"\\n", &empty_esc_map()), (0x0A, 2));
280 assert_eq!(parse_next_escaped(b"\\t", &empty_esc_map()), (0x09, 2));
281 assert_eq!(parse_next_escaped(b"\\r", &empty_esc_map()), (0x0D, 2));
282 assert_eq!(parse_next_escaped(b"\\\\", &empty_esc_map()), (0x5C, 2));
283 assert_eq!(parse_next_escaped(b"\\b", &empty_esc_map()), (0x08, 2));
284 assert_eq!(parse_next_escaped(b"\\f", &empty_esc_map()), (0x0C, 2));
285 assert_eq!(parse_next_escaped(b"\\\"", &empty_esc_map()), (0x22, 2));
286 assert_eq!(parse_next_escaped(b"\\/", &empty_esc_map()), (0x2F, 2));
289 }
290
291 #[test]
292 fn parse_hex_x_escape() {
293 assert_eq!(parse_next_escaped(b"\\x41", &empty_esc_map()), (0x41, 4));
294 assert_eq!(
296 parse_next_escaped(b"\\x1F600", &empty_esc_map()),
297 (0x1F600, 7)
298 );
299 assert_eq!(parse_next_escaped(b"\\xABg", &empty_esc_map()), (0xAB, 4));
301 }
302
303 #[test]
304 fn parse_unicode_escapes() {
305 assert_eq!(parse_next_escaped(b"\\u00A9", &empty_esc_map()), (0xA9, 6));
306 assert_eq!(
307 parse_next_escaped(b"\\u2603", &empty_esc_map()),
308 (0x2603, 6)
309 );
310 assert_eq!(
311 parse_next_escaped(b"\\U0001F600", &empty_esc_map()),
312 (0x1F600, 10)
313 );
314 }
315
316 #[test]
317 fn parse_invalid_escapes() {
318 assert_eq!(
320 parse_next_escaped(b"n", &empty_esc_map()),
321 (INVALID_ESCAPE, 0)
322 );
323 assert_eq!(
325 parse_next_escaped(b"\\z", &empty_esc_map()),
326 (INVALID_ESCAPE, 0)
327 );
328 assert_eq!(
330 parse_next_escaped(b"\\", &empty_esc_map()),
331 (INVALID_ESCAPE, 0)
332 );
333 assert_eq!(
335 parse_next_escaped(b"\\xg", &empty_esc_map()),
336 (INVALID_ESCAPE, 0)
337 );
338 assert_eq!(
340 parse_next_escaped(b"\\u12", &empty_esc_map()),
341 (INVALID_ESCAPE, 0)
342 );
343 assert_eq!(
345 parse_next_escaped(b"\\u12zz", &empty_esc_map()),
346 (INVALID_ESCAPE, 0)
347 );
348 }
349
350 #[test]
351 fn parse_additional_escape_map() {
352 let mut m = empty_esc_map();
353 m.insert(b'-', '-' as TCodepoint);
354 assert_eq!(parse_next_escaped(b"\\-", &m), ('-' as TCodepoint, 2));
355 }
356
357 #[test]
358 fn utf8_or_escaped_dispatch() {
359 assert_eq!(
361 parse_next_utf8_or_escaped(b"A", &empty_esc_map()),
362 (0x41, 1)
363 );
364 assert_eq!(
366 parse_next_utf8_or_escaped(b"\\n", &empty_esc_map()),
367 (0x0A, 2)
368 );
369 assert_eq!(
371 parse_next_utf8_or_escaped(b"\\z", &empty_esc_map()),
372 (INVALID_ESCAPE, 0)
373 );
374 assert_eq!(
376 parse_next_utf8_or_escaped(&[0x80], &empty_esc_map()),
377 (INVALID_UTF8, 0)
378 );
379 }
380
381 #[test]
382 fn unescape_full_string() {
383 let out = unescape_string(b"escaped \\\"quotes\\\"", &empty_esc_map()).unwrap();
385 assert_eq!(out, b"escaped \"quotes\"");
386 let out = unescape_string(b"\\n\\r\\t\\\\", &empty_esc_map()).unwrap();
388 assert_eq!(out, b"\n\r\t\\");
389 }
390
391 #[test]
392 fn unescape_unicode_to_utf8() {
393 let out = unescape_string(b"\\u00A9 \\u2603 \\U0001F600", &empty_esc_map()).unwrap();
394 assert_eq!(out, "© ☃ 😀".as_bytes());
395 }
396
397 #[test]
398 fn unescape_propagates_error() {
399 assert_eq!(
400 unescape_string(b"\\z", &empty_esc_map()),
401 Err(INVALID_ESCAPE)
402 );
403 assert_eq!(
404 unescape_string(&[0x80], &empty_esc_map()),
405 Err(INVALID_UTF8)
406 );
407 }
408
409 #[test]
410 fn escape_unescape_round_trip() {
411 for cp in [0x0A_i32, 0x41, 0xA9, 0x2603, 0x1F600, 0x00, 0x1B] {
412 let escaped = print_as_escaped(cp, &empty_cp_map());
413 let (back, _) = parse_next_utf8_or_escaped(escaped.as_bytes(), &empty_esc_map());
414 assert_eq!(back, cp, "round trip failed for {cp:#x}");
415 }
416 }
417}