Skip to main content

solar_parse/lexer/unescape/
mod.rs

1//! Utilities for validating string and char literals and turning them into values they represent.
2
3use alloy_primitives::hex;
4use solar_ast::Span;
5use solar_data_structures::trustme;
6use solar_interface::{BytePos, Session};
7use std::{borrow::Cow, ops::Range, slice, str::Chars};
8
9mod errors;
10pub use errors::EscapeError;
11pub(crate) use errors::emit_unescape_error;
12
13pub use solar_ast::StrKind;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16enum UnescapedUnit {
17    Byte(u8),
18    CodePoint(u32),
19}
20
21impl UnescapedUnit {
22    fn code_point(self) -> u32 {
23        match self {
24            Self::Byte(byte) => byte as u32,
25            Self::CodePoint(code) => code,
26        }
27    }
28
29    fn write(self, dst: &mut [u8]) -> usize {
30        match self {
31            Self::Byte(byte) => {
32                dst[0] = byte;
33                1
34            }
35            // NOTE: We can't use `char::encode_utf8` because `code` can be an invalid unicode
36            // code.
37            Self::CodePoint(code) => super::utf8::encode_utf8_raw(code, dst).len(),
38        }
39    }
40}
41
42pub fn parse_string_literal<'a>(
43    src: &'a str,
44    kind: StrKind,
45    lit_span: Span,
46    sess: &Session,
47) -> (Cow<'a, [u8]>, bool) {
48    let mut has_error = false;
49    let content_start = lit_span.lo() + BytePos(1) + BytePos(kind.prefix().len() as u32);
50    let bytes = try_parse_string_literal(src, kind, |range, error| {
51        has_error = true;
52        let (start, end) = (range.start as u32, range.end as u32);
53        let lo = content_start + BytePos(start);
54        let hi = lo + BytePos(end - start);
55        let span = Span::new(lo, hi);
56        emit_unescape_error(&sess.dcx, src, span, range, error);
57    });
58    (bytes, has_error)
59}
60
61/// Parses a string literal (without quotes) into a byte array.
62/// `f` is called for each escape error.
63#[instrument(name = "parse_string_literal", level = "trace", skip_all)]
64pub fn try_parse_string_literal<F>(src: &str, kind: StrKind, f: F) -> Cow<'_, [u8]>
65where
66    F: FnMut(Range<usize>, EscapeError),
67{
68    let mut bytes = if needs_unescape(src, kind) {
69        Cow::Owned(parse_literal_unescape(src, kind, f))
70    } else {
71        Cow::Borrowed(src.as_bytes())
72    };
73    if kind == StrKind::Hex {
74        // Currently this should never fail, but it's a good idea to check anyway.
75        if let Ok(decoded) = hex::decode(&bytes) {
76            bytes = Cow::Owned(decoded);
77        }
78    }
79    bytes
80}
81
82#[cold]
83fn parse_literal_unescape<F>(src: &str, kind: StrKind, f: F) -> Vec<u8>
84where
85    F: FnMut(Range<usize>, EscapeError),
86{
87    let mut bytes = Vec::with_capacity(src.len());
88    parse_literal_unescape_into(src, kind, f, &mut bytes);
89    bytes
90}
91
92fn parse_literal_unescape_into<F>(src: &str, kind: StrKind, mut f: F, dst_buf: &mut Vec<u8>)
93where
94    F: FnMut(Range<usize>, EscapeError),
95{
96    // `src.len()` is enough capacity for the unescaped string, so we can just use a slice.
97    // SAFETY: The buffer is never read from.
98    debug_assert!(dst_buf.is_empty());
99    debug_assert!(dst_buf.capacity() >= src.len());
100    let mut dst = unsafe { slice::from_raw_parts_mut(dst_buf.as_mut_ptr(), dst_buf.capacity()) };
101    unescape_literal_unchecked(src, kind, |range, res| match res {
102        Ok(c) => {
103            let written = c.write(dst);
104
105            // SAFETY: Unescaping guarantees that the final unescaped byte array is shorter than
106            // the initial string.
107            debug_assert!(dst.len() >= written);
108            let advanced = unsafe { dst.get_unchecked_mut(written..) };
109
110            // SAFETY: I don't know why this triggers E0521.
111            dst = unsafe { trustme::decouple_lt_mut(advanced) };
112        }
113        Err(e) => f(range, e),
114    });
115    unsafe { dst_buf.set_len(dst_buf.capacity() - dst.len()) };
116}
117
118/// Unescapes the contents of a string literal (without quotes).
119///
120/// The callback is invoked with a range and either a unicode code point or an error.
121#[instrument(level = "debug", skip_all)]
122pub fn unescape_literal<F>(src: &str, kind: StrKind, mut callback: F)
123where
124    F: FnMut(Range<usize>, Result<u32, EscapeError>),
125{
126    if needs_unescape(src, kind) {
127        unescape_literal_unchecked(src, kind, |range, res| {
128            callback(range, res.map(UnescapedUnit::code_point));
129        });
130    } else {
131        for (i, ch) in src.char_indices() {
132            callback(i..i + ch.len_utf8(), Ok(ch as u32));
133        }
134    }
135}
136
137/// Unescapes the contents of a string literal (without quotes).
138///
139/// See [`unescape_literal`] for more details.
140fn unescape_literal_unchecked<F>(src: &str, kind: StrKind, callback: F)
141where
142    F: FnMut(Range<usize>, Result<UnescapedUnit, EscapeError>),
143{
144    match kind {
145        StrKind::Str | StrKind::Unicode => {
146            unescape_str(src, matches!(kind, StrKind::Unicode), callback)
147        }
148        StrKind::Hex => unescape_hex_str(src, callback),
149    }
150}
151
152/// Fast-path check for whether a string literal needs to be unescaped or errors need to be
153/// reported.
154fn needs_unescape(src: &str, kind: StrKind) -> bool {
155    fn needs_unescape_chars(src: &str) -> bool {
156        memchr::memchr3(b'\\', b'\n', b'\r', src.as_bytes()).is_some()
157    }
158
159    match kind {
160        StrKind::Str => needs_unescape_chars(src) || !src.is_ascii(),
161        StrKind::Unicode => needs_unescape_chars(src),
162        StrKind::Hex => !src.len().is_multiple_of(2) || !hex::check_raw(src),
163    }
164}
165
166fn scan_escape(chars: &mut Chars<'_>) -> Result<UnescapedUnit, EscapeError> {
167    // Previous character was '\\', unescape what follows.
168    // https://docs.soliditylang.org/en/latest/grammar.html#a4.SolidityLexer.EscapeSequence
169    // Note that hex and unicode escape codes are not validated since string literals are allowed
170    // to contain invalid UTF-8.
171    let unit = match chars.next().ok_or(EscapeError::LoneSlash)? {
172        // Both quotes are always valid escapes.
173        '\'' => UnescapedUnit::CodePoint('\'' as u32),
174        '"' => UnescapedUnit::CodePoint('"' as u32),
175
176        '\\' => UnescapedUnit::CodePoint('\\' as u32),
177        'n' => UnescapedUnit::CodePoint('\n' as u32),
178        'r' => UnescapedUnit::CodePoint('\r' as u32),
179        't' => UnescapedUnit::CodePoint('\t' as u32),
180
181        'x' => {
182            // Parse hexadecimal character code.
183            let mut value = 0;
184            for _ in 0..2 {
185                let d = chars.next().ok_or(EscapeError::HexEscapeTooShort)?;
186                let d = d.to_digit(16).ok_or(EscapeError::InvalidHexEscape)?;
187                value = value * 16 + d;
188            }
189            UnescapedUnit::Byte(value as u8)
190        }
191
192        'u' => {
193            // Parse hexadecimal unicode character code.
194            let mut value = 0;
195            for _ in 0..4 {
196                let d = chars.next().ok_or(EscapeError::UnicodeEscapeTooShort)?;
197                let d = d.to_digit(16).ok_or(EscapeError::InvalidUnicodeEscape)?;
198                value = value * 16 + d;
199            }
200            UnescapedUnit::CodePoint(value)
201        }
202
203        _ => return Err(EscapeError::InvalidEscape),
204    };
205    Ok(unit)
206}
207
208/// Unescape characters in a string literal.
209///
210/// See [`unescape_literal`] for more details.
211fn unescape_str<F>(src: &str, is_unicode: bool, mut callback: F)
212where
213    F: FnMut(Range<usize>, Result<UnescapedUnit, EscapeError>),
214{
215    let mut chars = src.chars();
216    // The `start` and `end` computation here is complicated because
217    // `skip_ascii_whitespace` makes us to skip over chars without counting
218    // them in the range computation.
219    while let Some(c) = chars.next() {
220        let start = src.len() - chars.as_str().len() - c.len_utf8();
221        let res = match c {
222            '\\' => match chars.clone().next() {
223                Some('\r') if chars.clone().nth(1) == Some('\n') => {
224                    // +2 for the '\\' and '\r' characters.
225                    skip_ascii_whitespace(&mut chars, start + 2, &mut callback);
226                    continue;
227                }
228                Some('\n') => {
229                    // +1 for the '\\' character.
230                    skip_ascii_whitespace(&mut chars, start + 1, &mut callback);
231                    continue;
232                }
233                _ => scan_escape(&mut chars),
234            },
235            '\n' => Err(EscapeError::StrNewline),
236            '\r' => {
237                if chars.clone().next() == Some('\n') {
238                    continue;
239                }
240                Err(EscapeError::BareCarriageReturn)
241            }
242            c if !is_unicode && !c.is_ascii() => Err(EscapeError::StrNonAsciiChar),
243            c => Ok(UnescapedUnit::CodePoint(c as u32)),
244        };
245        let end = src.len() - chars.as_str().len();
246        callback(start..end, res);
247    }
248}
249
250/// Skips over whitespace after a "\\\n" escape sequence.
251///
252/// Reports errors if multiple newlines are encountered.
253fn skip_ascii_whitespace<F>(chars: &mut Chars<'_>, mut start: usize, callback: &mut F)
254where
255    F: FnMut(Range<usize>, Result<UnescapedUnit, EscapeError>),
256{
257    // Skip the first newline.
258    let mut nl = chars.next();
259    if let Some('\r') = nl {
260        nl = chars.next();
261    }
262    debug_assert_eq!(nl, Some('\n'));
263    let mut tail = chars.as_str();
264    start += 1;
265
266    while tail.starts_with(|c: char| c.is_ascii_whitespace()) {
267        let first_non_space =
268            tail.bytes().position(|b| !matches!(b, b' ' | b'\t')).unwrap_or(tail.len());
269        tail = &tail[first_non_space..];
270        start += first_non_space;
271
272        if let Some(tail2) = tail.strip_prefix('\n').or_else(|| tail.strip_prefix("\r\n")) {
273            let skipped = tail.len() - tail2.len();
274            tail = tail2;
275            callback(start..start + skipped, Err(EscapeError::CannotSkipMultipleLines));
276            start += skipped;
277        }
278    }
279    *chars = tail.chars();
280}
281
282/// Unescape characters in a hex string literal.
283///
284/// See [`unescape_literal`] for more details.
285fn unescape_hex_str<F>(src: &str, mut callback: F)
286where
287    F: FnMut(Range<usize>, Result<UnescapedUnit, EscapeError>),
288{
289    let mut chars = src.char_indices();
290    if src.starts_with("0x") || src.starts_with("0X") {
291        chars.next();
292        chars.next();
293        callback(0..2, Err(EscapeError::HexPrefix));
294    }
295
296    let count = chars.clone().filter(|(_, c)| c.is_ascii_hexdigit()).count();
297    if !count.is_multiple_of(2) {
298        callback(0..src.len(), Err(EscapeError::HexOddDigits));
299        return;
300    }
301
302    let mut emit_underscore_errors = true;
303    let mut allow_underscore = false;
304    let mut even = true;
305    for (start, c) in chars {
306        let res = match c {
307            '_' => {
308                if emit_underscore_errors && (!allow_underscore || !even) {
309                    // Don't spam errors for multiple underscores.
310                    emit_underscore_errors = false;
311                    Err(EscapeError::HexBadUnderscore)
312                } else {
313                    allow_underscore = false;
314                    continue;
315                }
316            }
317            c if !c.is_ascii_hexdigit() => Err(EscapeError::HexNotHexDigit),
318            c => Ok(UnescapedUnit::CodePoint(c as u32)),
319        };
320
321        if res.is_ok() {
322            even = !even;
323            allow_underscore = true;
324        }
325
326        let end = start + c.len_utf8();
327        callback(start..end, res);
328    }
329
330    if emit_underscore_errors && src.len() > 1 && src.ends_with('_') {
331        callback(src.len() - 1..src.len(), Err(EscapeError::HexBadUnderscore));
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use EscapeError::*;
339
340    type ExErr = (Range<usize>, EscapeError);
341
342    #[track_caller]
343    fn check_parse(kind: StrKind, src: &str, expected: &[u8]) {
344        let out = try_parse_string_literal(src, kind, |_, e| panic!("{e:?}"));
345        assert_eq!(&*out, expected, "{kind:?}: {src:?}");
346    }
347
348    #[track_caller]
349    fn check_unescape(kind: StrKind, src: &str, expected: &str) {
350        let mut out = String::new();
351        unescape_literal(src, kind, |_, c| out.push(char::try_from(c.unwrap()).unwrap()));
352        assert_eq!(out, expected, "{kind:?}: {src:?}");
353    }
354
355    fn check(kind: StrKind, src: &str, expected_str: &str, expected_errs: &[ExErr]) {
356        let panic_str = format!("{kind:?}: {src:?}");
357
358        let mut ok = String::with_capacity(src.len());
359        let mut errs = Vec::with_capacity(expected_errs.len());
360        unescape_literal(src, kind, |range, c| match c {
361            Ok(c) => ok.push(char::try_from(c).unwrap()),
362            Err(e) => errs.push((range, e)),
363        });
364        assert_eq!(errs, expected_errs, "{panic_str}");
365        assert_eq!(ok, expected_str, "{panic_str}");
366
367        let mut errs2 = Vec::with_capacity(errs.len());
368        let out = try_parse_string_literal(src, kind, |range, e| {
369            errs2.push((range, e));
370        });
371        assert_eq!(errs2, errs, "{panic_str}");
372        if kind == StrKind::Hex {
373            assert_eq!(hex::encode(out), expected_str, "{panic_str}");
374        } else {
375            assert_eq!(hex::encode(out), hex::encode(expected_str), "{panic_str}");
376        }
377    }
378
379    #[test]
380    fn unescape_str() {
381        let cases: &[(&str, &str, &[ExErr])] = &[
382            ("", "", &[]),
383            (" ", " ", &[]),
384            ("\t", "\t", &[]),
385            (" \t ", " \t ", &[]),
386            ("foo", "foo", &[]),
387            ("hello world", "hello world", &[]),
388            (r"\", "", &[(0..1, LoneSlash)]),
389            (r"\\", "\\", &[]),
390            (r"\\\", "\\", &[(2..3, LoneSlash)]),
391            (r"\\\\", "\\\\", &[]),
392            (r"\\ ", "\\ ", &[]),
393            (r"\\ \", "\\ ", &[(3..4, LoneSlash)]),
394            (r"\\ \\", "\\ \\", &[]),
395            (r"\x", "", &[(0..2, HexEscapeTooShort)]),
396            (r"\x1", "", &[(0..3, HexEscapeTooShort)]),
397            (r"\xz", "", &[(0..3, InvalidHexEscape)]),
398            (r"\xzf", "f", &[(0..3, InvalidHexEscape)]),
399            (r"\xzz", "z", &[(0..3, InvalidHexEscape)]),
400            (r"\x69", "\x69", &[]),
401            (r"\u", "", &[(0..2, UnicodeEscapeTooShort)]),
402            (r"\u1", "", &[(0..3, UnicodeEscapeTooShort)]),
403            (r"\uz", "", &[(0..3, InvalidUnicodeEscape)]),
404            (r"\uzf", "f", &[(0..3, InvalidUnicodeEscape)]),
405            (r"\u12", "", &[(0..4, UnicodeEscapeTooShort)]),
406            (r"\u123", "", &[(0..5, UnicodeEscapeTooShort)]),
407            (r"\u1234", "\u{1234}", &[]),
408            (r"\u00e8", "è", &[]),
409            (r"\r", "\r", &[]),
410            (r"\t", "\t", &[]),
411            (r"\n", "\n", &[]),
412            (r"\n\n", "\n\n", &[]),
413            (r"\ ", "", &[(0..2, InvalidEscape)]),
414            (r"\?", "", &[(0..2, InvalidEscape)]),
415            ("\r\n", "", &[(1..2, StrNewline)]),
416            ("\n", "", &[(0..1, StrNewline)]),
417            ("\\\n", "", &[]),
418            ("\\\na", "a", &[]),
419            ("\\\n  a", "a", &[]),
420            ("a \\\n  b", "a b", &[]),
421            ("a\\n\\\n  b", "a\nb", &[]),
422            ("a\\t\\\n  b", "a\tb", &[]),
423            ("a\\n \\\n  b", "a\n b", &[]),
424            ("a\\n \\\n \tb", "a\n b", &[]),
425            ("a\\t \\\n  b", "a\t b", &[]),
426            ("\\\n \t a", "a", &[]),
427            (" \\\n \t a", " a", &[]),
428            ("\\\n \t a\n", "a", &[(6..7, StrNewline)]),
429            ("\\\n   \t   ", "", &[]),
430            (" \\\n   \t   ", " ", &[]),
431            (" he\\\n \\\nllo \\\n wor\\\nld", " hello world", &[]),
432            ("\\\n\na\\\nb", "ab", &[(2..3, CannotSkipMultipleLines)]),
433            ("\\\n \na\\\nb", "ab", &[(3..4, CannotSkipMultipleLines)]),
434            (
435                "\\\n \n\na\\\nb",
436                "ab",
437                &[(3..4, CannotSkipMultipleLines), (4..5, CannotSkipMultipleLines)],
438            ),
439            (
440                "a\\\n \n \t \nb\\\nc",
441                "abc",
442                &[(4..5, CannotSkipMultipleLines), (8..9, CannotSkipMultipleLines)],
443            ),
444        ];
445        for &(src, expected_str, expected_errs) in cases {
446            check(StrKind::Str, src, expected_str, expected_errs);
447            check(StrKind::Unicode, src, expected_str, expected_errs);
448        }
449
450        check_unescape(StrKind::Str, r"\xE8", "è");
451        check_unescape(StrKind::Unicode, r"\xE8", "è");
452        check_parse(StrKind::Str, r"\xE8", b"\xE8");
453        check_parse(StrKind::Unicode, r"\xE8", b"\xE8");
454        check_parse(StrKind::Str, r"\u00e8", "è".as_bytes());
455        check_parse(StrKind::Unicode, r"\u00e8", "è".as_bytes());
456        check_parse(StrKind::Str, r"\xE8\u00e8", b"\xE8\xc3\xa8");
457        check_parse(StrKind::Unicode, r"\xE8\u00e8", b"\xE8\xc3\xa8");
458    }
459
460    #[test]
461    fn unescape_unicode_str() {
462        let cases: &[(&str, &str, &[ExErr], &[ExErr])] = &[
463            ("è", "è", &[], &[(0..2, StrNonAsciiChar)]),
464            ("😀", "😀", &[], &[(0..4, StrNonAsciiChar)]),
465        ];
466        for &(src, expected_str, e1, e2) in cases {
467            check(StrKind::Unicode, src, expected_str, e1);
468            check(StrKind::Str, src, "", e2);
469        }
470    }
471
472    #[test]
473    fn unescape_hex_str() {
474        let cases: &[(&str, &str, &[ExErr])] = &[
475            ("", "", &[]),
476            ("z", "", &[(0..1, HexNotHexDigit)]),
477            ("\n", "", &[(0..1, HexNotHexDigit)]),
478            ("  11", "11", &[(0..1, HexNotHexDigit), (1..2, HexNotHexDigit)]),
479            ("0x", "", &[(0..2, HexPrefix)]),
480            ("0X", "", &[(0..2, HexPrefix)]),
481            ("0x11", "11", &[(0..2, HexPrefix)]),
482            ("0X11", "11", &[(0..2, HexPrefix)]),
483            ("1", "", &[(0..1, HexOddDigits)]),
484            ("12", "12", &[]),
485            ("123", "", &[(0..3, HexOddDigits)]),
486            ("1234", "1234", &[]),
487            ("_", "", &[(0..1, HexBadUnderscore)]),
488            ("_11", "11", &[(0..1, HexBadUnderscore)]),
489            ("_11_", "11", &[(0..1, HexBadUnderscore)]),
490            ("11_", "11", &[(2..3, HexBadUnderscore)]),
491            ("11_22", "1122", &[]),
492            ("11__", "11", &[(3..4, HexBadUnderscore)]),
493            ("11__22", "1122", &[(3..4, HexBadUnderscore)]),
494            ("1_2", "12", &[(1..2, HexBadUnderscore)]),
495        ];
496        for &(src, expected_str, expected_errs) in cases {
497            check(StrKind::Hex, src, expected_str, expected_errs);
498        }
499    }
500}