1use std::fmt;
2use std::ops::{Range, RangeInclusive};
3
4use rowan::TextSize;
5
6use crate::decoded_text::DecodedText;
7
8pub enum UnicodeEscapeKind {
9 Extended,
10 Short,
11}
12
13impl UnicodeEscapeKind {
14 fn count(&self) -> u32 {
15 match self {
16 UnicodeEscapeKind::Extended => 6,
17 UnicodeEscapeKind::Short => 4,
18 }
19 }
20}
21
22pub enum UnicodeEscError {
23 InvalidEscape,
24 InvalidSurrogatePair,
25 OutOfRange,
26 RequiresHexDigits {
27 kind: UnicodeEscapeKind,
28 escape_char: char,
29 },
30}
31
32impl fmt::Display for UnicodeEscError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::InvalidEscape => f.write_str("Invalid Unicode escape sequence"),
36 Self::InvalidSurrogatePair => f.write_str("Invalid Unicode surrogate pair"),
37 Self::OutOfRange => f.write_str("Unicode escape value out of range"),
38 Self::RequiresHexDigits { kind, escape_char } => {
39 let required = kind.count();
40 let plus = match kind {
41 UnicodeEscapeKind::Extended => "+",
42 UnicodeEscapeKind::Short => "",
43 };
44 let xs = "X".repeat(required as usize);
45 write!(
46 f,
47 "Unicode escape requires {required} hex digits: {escape_char}{plus}{xs}"
48 )
49 }
50 }
51 }
52}
53
54pub fn escape_unicode_esc_str<F>(text: &str, escape_char: char, mut callback: F)
55where
56 F: FnMut(Range<usize>, Result<char, UnicodeEscError>),
57{
58 const HIGH_SURROGATE: RangeInclusive<u32> = 0xD800..=0xDBFF;
59 const LOW_SURROGATE: RangeInclusive<u32> = 0xDC00..=0xDFFF;
60 const MAX_CODEPOINT: u32 = 0x10FFFF;
61
62 let mut chars = text.char_indices().peekable();
63 let mut high_surrogate: Option<(Range<usize>, u32)> = None;
64
65 while let Some((escape_start, c)) = chars.next() {
66 if c != escape_char {
67 if let Some((hi_range, _)) = high_surrogate.take() {
68 callback(hi_range, Err(UnicodeEscError::InvalidSurrogatePair));
69 }
70 callback(escape_start..escape_start + c.len_utf8(), Ok(c));
71 continue;
72 }
73 let kind = match chars.peek() {
74 Some(&(_, c)) if c == escape_char => {
75 chars.next();
76 if let Some((hi_range, _)) = high_surrogate.take() {
77 callback(hi_range, Err(UnicodeEscError::InvalidSurrogatePair));
78 }
79 let end = escape_start + escape_char.len_utf8() * 2;
80 callback(escape_start..end, Ok(escape_char));
81 continue;
82 }
83 Some(&(_, '+')) => {
84 chars.next();
85 UnicodeEscapeKind::Extended
86 }
87 Some(&(_, c)) if c.is_ascii_hexdigit() => UnicodeEscapeKind::Short,
88 _ => {
89 let end = chars
90 .next()
91 .map(|(i, c)| i + c.len_utf8())
92 .unwrap_or(text.len());
93 if let Some((hi_range, _)) = high_surrogate.take() {
94 callback(hi_range, Err(UnicodeEscError::InvalidSurrogatePair));
95 }
96 callback(escape_start..end, Err(UnicodeEscError::InvalidEscape));
97 continue;
98 }
99 };
100 let mut codepoint: u32 = 0;
101 let mut got_all = true;
102 let mut last_end = chars.peek().map(|&(i, _)| i).unwrap_or(text.len());
103 for _ in 0..kind.count() {
104 let radix = 16;
105 let Some(&(i, ch)) = chars.peek() else {
106 got_all = false;
107 break;
108 };
109 let Some(d) = ch.to_digit(radix) else {
110 got_all = false;
111 break;
112 };
113 chars.next();
114 codepoint = codepoint * radix + d;
115 last_end = i + ch.len_utf8();
116 }
117 if !got_all {
118 if let Some((hi_range, _)) = high_surrogate.take() {
119 callback(hi_range, Err(UnicodeEscError::InvalidSurrogatePair));
120 }
121 callback(
122 escape_start..last_end,
123 Err(UnicodeEscError::RequiresHexDigits { kind, escape_char }),
124 );
125 continue;
126 }
127 if let Some((hi_range, hi_cp)) = high_surrogate.take() {
128 if LOW_SURROGATE.contains(&codepoint) {
129 let combined = 0x10000 + ((hi_cp - 0xD800) << 10) + (codepoint - 0xDC00);
130 let ch = char::from_u32(combined).unwrap();
131 callback(hi_range.start..last_end, Ok(ch));
132 continue;
133 }
134 callback(
135 hi_range.start..last_end,
136 Err(UnicodeEscError::InvalidSurrogatePair),
137 );
138 continue;
139 }
140 if codepoint > MAX_CODEPOINT {
141 callback(escape_start..last_end, Err(UnicodeEscError::OutOfRange));
142 } else if HIGH_SURROGATE.contains(&codepoint) {
143 high_surrogate = Some((escape_start..last_end, codepoint));
144 } else if LOW_SURROGATE.contains(&codepoint) {
145 callback(
146 escape_start..last_end,
147 Err(UnicodeEscError::InvalidSurrogatePair),
148 );
149 } else {
150 let ch = char::from_u32(codepoint).unwrap();
151 callback(escape_start..last_end, Ok(ch));
152 }
153 }
154 if let Some((range, _)) = high_surrogate {
155 callback(range, Err(UnicodeEscError::InvalidSurrogatePair));
156 }
157}
158
159const fn is_valid_uescape_char(byte: u8) -> bool {
161 !byte.is_ascii_hexdigit()
162 && byte != b'+'
163 && byte != b'\''
164 && byte != b'"'
165 && !matches!(
166 byte,
167 b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C
168 )
169}
170
171pub fn uescape_char(text: &str) -> Option<char> {
172 let inner = text.strip_prefix('\'')?.strip_suffix('\'')?;
173 let &[byte] = inner.as_bytes() else {
174 return None;
175 };
176 is_valid_uescape_char(byte).then(|| char::from(byte))
177}
178
179pub fn decode_plain_string(inner: &str, start_pos: TextSize, out: &mut DecodedText) {
180 let mut chars = inner.char_indices().peekable();
181 while let Some((i, c)) = chars.next() {
182 let pos = start_pos + TextSize::new(i as u32);
183 if c == '\'' && chars.peek().is_some_and(|&(_, next)| next == '\'') {
184 chars.next();
185 }
186 out.push_char(c, pos);
187 }
188}
189
190struct EscBuffer {
191 bytes: Vec<u8>,
192 pos: TextSize,
193}
194
195impl EscBuffer {
196 fn new(pos: TextSize) -> Self {
197 Self { bytes: vec![], pos }
198 }
199
200 fn push(&mut self, byte: u8, pos: TextSize) {
201 if self.bytes.is_empty() {
202 self.pos = pos;
203 }
204 self.bytes.push(byte);
205 }
206
207 fn push_char(&mut self, c: char, pos: TextSize) {
208 if self.bytes.is_empty() {
209 self.pos = pos;
210 }
211 let mut buf = [0; 4];
212 self.bytes
213 .extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
214 }
215
216 fn drain(&mut self, out: &mut DecodedText) {
217 if self.bytes.is_empty() {
218 return;
219 }
220 match std::str::from_utf8(&self.bytes) {
221 Ok(text) => out.push_str(text, self.pos),
222 Err(err) if err.error_len().is_some() => {
223 out.push_str(&String::from_utf8_lossy(&self.bytes), self.pos);
224 }
225 Err(_) => return,
226 }
227 self.bytes.clear();
228 }
229
230 fn flush(&mut self, out: &mut DecodedText) {
231 if self.bytes.is_empty() {
232 return;
233 }
234 out.push_str(&String::from_utf8_lossy(&self.bytes), self.pos);
235 self.bytes.clear();
236 }
237}
238
239pub fn decode_esc_string(inner: &str, start_pos: TextSize, out: &mut DecodedText) {
240 let mut chars = inner.char_indices().peekable();
241 let mut esc = EscBuffer::new(start_pos);
242
243 while let Some((i, c)) = chars.next() {
244 let pos = start_pos + TextSize::new(i as u32);
245
246 if c == '\'' && chars.peek().is_some_and(|&(_, next)| next == '\'') {
247 chars.next();
248 esc.flush(out);
249 out.push_char('\'', pos);
250 continue;
251 }
252 if c != '\\' {
253 esc.flush(out);
254 out.push_char(c, pos);
255 continue;
256 }
257 let Some(&(_, next)) = chars.peek() else {
258 esc.flush(out);
259 out.push_char('\\', pos);
260 break;
261 };
262 match next {
263 'b' => {
264 chars.next();
265 esc.push(b'\x08', pos);
266 }
267 'f' => {
268 chars.next();
269 esc.push(b'\x0C', pos);
270 }
271 'n' => {
272 chars.next();
273 esc.push(b'\n', pos);
274 }
275 'r' => {
276 chars.next();
277 esc.push(b'\r', pos);
278 }
279 't' => {
280 chars.next();
281 esc.push(b'\t', pos);
282 }
283 '0'..='7' => {
284 let mut value: u32 = 0;
285 for _ in 0..3 {
286 match chars.peek() {
287 Some(&(_, d)) if ('0'..='7').contains(&d) => {
288 chars.next();
289 value = value * 8 + d.to_digit(8).unwrap();
290 }
291 _ => break,
292 }
293 }
294 if value != 0 {
295 esc.push(value as u8, pos);
296 }
297 }
298 'x' => {
299 chars.next();
300 let mut value: u8 = 0;
301 let mut got_any = false;
302 for _ in 0..2 {
303 match chars.peek() {
304 Some(&(_, d)) if d.is_ascii_hexdigit() => {
305 chars.next();
306 value = value * 16 + d.to_digit(16).unwrap() as u8;
307 got_any = true;
308 }
309 _ => break,
310 }
311 }
312 if got_any {
313 if value != 0 {
314 esc.push(value, pos);
315 }
316 } else {
317 esc.push(b'x', pos);
318 }
319 }
320 'u' | 'U' => {
321 chars.next();
322 let required = if next == 'u' { 4 } else { 8 };
323 let mut value: u32 = 0;
324 let mut got_all = true;
325 for _ in 0..required {
326 match chars.peek() {
327 Some(&(_, d)) if d.is_ascii_hexdigit() => {
328 chars.next();
329 value = value * 16 + d.to_digit(16).unwrap();
330 }
331 _ => {
332 got_all = false;
333 break;
334 }
335 }
336 }
337 if got_all
338 && let Some(ch) = char::from_u32(value)
339 && ch != '\0'
340 {
341 esc.push_char(ch, pos);
342 }
343 }
344 _ => {
345 chars.next();
346 esc.push_char(next, pos);
347 }
348 }
349 esc.drain(out);
350 }
351
352 esc.flush(out);
353}
354
355pub fn decode_unicode_esc_string(
356 inner: &str,
357 start_pos: TextSize,
358 escape_char: char,
359 out: &mut DecodedText,
360) {
361 let mut dequoted = DecodedText::new(start_pos);
362 decode_plain_string(inner, start_pos, &mut dequoted);
363
364 escape_unicode_esc_str(dequoted.text(), escape_char, |range, result| {
365 if let Ok(ch) = result {
366 out.push_char(ch, dequoted.source_pos(TextSize::new(range.start as u32)));
367 }
368 });
369}
370
371#[cfg(test)]
372mod tests {
373 use insta::assert_snapshot;
374
375 use super::*;
376
377 fn unicode_escape_events(text: &str, escape_char: char) -> String {
378 let mut events = vec![];
379
380 escape_unicode_esc_str(text, escape_char, |range, result| {
381 let entry = match result {
382 Ok(ch) => format!("{}..{} ok {ch:?}", range.start, range.end),
383 Err(err) => format!("{}..{} err {err}", range.start, range.end),
384 };
385 events.push(entry);
386 });
387
388 events.join("\n")
389 }
390
391 fn decode_escape_string(inner: &str) -> String {
392 let mut out = DecodedText::new(TextSize::new(0));
393 decode_esc_string(inner, TextSize::new(0), &mut out);
394 out.into_text()
395 }
396
397 fn decode_unicode_escape_string(inner: &str, escape_char: char) -> String {
398 let mut out = DecodedText::new(TextSize::new(0));
399 decode_unicode_esc_string(inner, TextSize::new(0), escape_char, &mut out);
400 out.into_text()
401 }
402
403 #[test]
404 fn ok() {
405 assert_snapshot!(unicode_escape_events(r"hello world", '\\'), @"
406 0..1 ok 'h'
407 1..2 ok 'e'
408 2..3 ok 'l'
409 3..4 ok 'l'
410 4..5 ok 'o'
411 5..6 ok ' '
412 6..7 ok 'w'
413 7..8 ok 'o'
414 8..9 ok 'r'
415 9..10 ok 'l'
416 10..11 ok 'd'
417 ");
418 }
419
420 #[test]
421 fn incomplete_unicode_escape_breaks_surrogate_pairing() {
422 assert_snapshot!(unicode_escape_events(r"\D800\006\DC00", '\\'), @r"
423 0..5 err Invalid Unicode surrogate pair
424 5..9 err Unicode escape requires 4 hex digits: \XXXX
425 9..14 err Invalid Unicode surrogate pair
426 ");
427 }
428
429 #[test]
430 fn invalid_unicode_escape_breaks_surrogate_pairing() {
431 assert_snapshot!(unicode_escape_events(r"\D800\Q\DC00", '\\'), @r"
432 0..5 err Invalid Unicode surrogate pair
433 5..7 err Invalid Unicode escape sequence
434 7..12 err Invalid Unicode surrogate pair
435 ");
436 }
437
438 #[test]
439 fn invalid_unicode_escape_does_not_emit_literal_char() {
440 assert_snapshot!(unicode_escape_events(r"\0061\Q\0062", '\\'), @r"
441 0..5 ok 'a'
442 5..7 err Invalid Unicode escape sequence
443 7..12 ok 'b'
444 ");
445 }
446
447 #[test]
448 fn invalid_unicode_escape_works_with_custom_escape_char() {
449 assert_snapshot!(unicode_escape_events("!0061!Q!0062", '!'), @r"
450 0..5 ok 'a'
451 5..7 err Invalid Unicode escape sequence
452 7..12 ok 'b'
453 ");
454 }
455
456 #[test]
457 fn valid_unicode_escape_after_high_surrogate_only_emits_error() {
458 assert_snapshot!(unicode_escape_events(r"\D800\0061", '\\'), @r"
459 0..10 err Invalid Unicode surrogate pair
460 ");
461 }
462
463 #[test]
464 fn decode_escape_string_hex_bytes_as_utf8() {
465 assert_snapshot!(decode_escape_string(r"\xC3\xA9"), @"é");
466 }
467
468 #[test]
469 fn decode_escape_string_skips_nul_byte() {
470 assert_snapshot!(decode_escape_string(r"a\000b"), @"ab");
471 }
472
473 #[test]
474 fn escape_string_incomplete_byte_escape() {
475 assert_snapshot!(decode_escape_string(r"\xc3a"), @"�a");
476 assert_snapshot!(decode_escape_string(r"\xc3"), @"�");
477 }
478
479 #[test]
480 fn escape_string_trailing_backslash() {
481 assert_snapshot!(decode_escape_string(r"a\"), @r"a\");
482 assert_snapshot!(decode_escape_string(r"\xC3\"), @r"�\");
483 }
484
485 #[test]
486 fn decode_unicode_string_collapses_doubled_quotes() {
487 assert_snapshot!(decode_unicode_escape_string("a''b", '\\'), @"a'b");
488 }
489}