nextest_filtering/parsing/
unicode_string.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4// Adapted from https://github.com/Geal/nom/blob/294ffb3d9e0ade2c3b7ddfff52484b6d643dcce1/examples/string.rs
5
6use super::{PResult, Span, SpanLength, expect_n};
7use crate::errors::ParseSingleError;
8use std::fmt;
9use winnow::{
10    Parser,
11    combinator::{alt, delimited, preceded, repeat, trace},
12    token::{take_till, take_while},
13};
14
15fn parse_unicode(input: &mut Span<'_>) -> PResult<char> {
16    trace("parse_unicode", |input: &mut _| {
17        let parse_hex = take_while(1..=6, |c: char| c.is_ascii_hexdigit());
18        let parse_delimited_hex = preceded('u', delimited('{', parse_hex, '}'));
19        let parse_u32 = parse_delimited_hex.try_map(|hex| u32::from_str_radix(hex, 16));
20        parse_u32.verify_map(std::char::from_u32).parse_next(input)
21    })
22    .parse_next(input)
23}
24
25fn parse_escaped_char(input: &mut Span<'_>) -> PResult<Option<char>> {
26    trace("parse_escaped_char", |input: &mut _| {
27        let valid = alt((
28            parse_unicode,
29            'n'.value('\n'),
30            'r'.value('\r'),
31            't'.value('\t'),
32            'b'.value('\u{08}'),
33            'f'.value('\u{0C}'),
34            '\\'.value('\\'),
35            '/'.value('/'),
36            alt((')'.value(')'), ','.value(','))),
37        ));
38        preceded(
39            '\\',
40            // If none of the valid characters are found, this will report an error.
41            expect_n(
42                valid,
43                ParseSingleError::InvalidEscapeCharacter,
44                // -1 to account for the preceding backslash.
45                SpanLength::Offset(-1, 2),
46            ),
47        )
48        .parse_next(input)
49    })
50    .parse_next(input)
51}
52
53// This should match parse_escaped_char above.
54pub(crate) struct DisplayParsedString<'a>(pub(crate) &'a str);
55
56impl fmt::Display for DisplayParsedString<'_> {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        for c in self.0.chars() {
59            match c {
60                // These escapes are custom to nextest.
61                '/' => f.write_str("\\/")?,
62                ')' => f.write_str("\\)")?,
63                ',' => f.write_str("\\,")?,
64                // All the other escapes should be covered by this.
65                c => write!(f, "{}", c.escape_default())?,
66            }
67        }
68        Ok(())
69    }
70}
71fn parse_literal<'i>(input: &mut Span<'i>) -> PResult<&'i str> {
72    trace("parse_literal", |input: &mut _| {
73        let not_quote_slash = take_till(1.., (',', ')', '\\'));
74
75        not_quote_slash
76            .verify(|s: &str| !s.is_empty())
77            .parse_next(input)
78    })
79    .parse_next(input)
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83enum StringFragment<'a> {
84    Literal(&'a str),
85    EscapedChar(char),
86}
87
88fn parse_fragment<'i>(input: &mut Span<'i>) -> PResult<Option<StringFragment<'i>>> {
89    trace(
90        "parse_fragment",
91        alt((
92            parse_literal.map(|span| Some(StringFragment::Literal(span))),
93            parse_escaped_char.map(|res| res.map(StringFragment::EscapedChar)),
94        )),
95    )
96    .parse_next(input)
97}
98
99/// Construct a string by consuming the input until the next unescaped ) or ,.
100///
101/// Returns None if the string isn't valid.
102pub(super) fn parse_string(input: &mut Span<'_>) -> PResult<Option<String>> {
103    trace(
104        "parse_string",
105        repeat(0.., parse_fragment).fold(
106            || Some(String::new()),
107            |string, fragment| {
108                match (string, fragment) {
109                    (Some(mut string), Some(StringFragment::Literal(s))) => {
110                        string.push_str(s);
111                        Some(string)
112                    }
113                    (Some(mut string), Some(StringFragment::EscapedChar(c))) => {
114                        string.push(c);
115                        Some(string)
116                    }
117                    (Some(_), None) => {
118                        // We encountered a parsing error, and at this point we'll stop returning
119                        // values.
120                        None
121                    }
122                    (None, _) => None,
123                }
124            },
125        ),
126    )
127    .parse_next(input)
128}