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            ')'.value(')'),
37            ','.value(','),
38        ));
39        preceded(
40            '\\',
41            // If none of the valid characters are found, this will report an error.
42            expect_n(
43                valid,
44                ParseSingleError::InvalidEscapeCharacter,
45                // -1 to account for the preceding backslash.
46                SpanLength::Offset(-1, 2),
47            ),
48        )
49        .parse_next(input)
50    })
51    .parse_next(input)
52}
53
54// This should match parse_escaped_char above.
55pub(crate) struct DisplayParsedString<'a>(pub(crate) &'a str);
56
57impl fmt::Display for DisplayParsedString<'_> {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        for c in self.0.chars() {
60            match c {
61                // These escapes are custom to nextest.
62                '/' => f.write_str("\\/")?,
63                ')' => f.write_str("\\)")?,
64                ',' => f.write_str("\\,")?,
65                // All the other escapes should be covered by this.
66                c => write!(f, "{}", c.escape_default())?,
67            }
68        }
69        Ok(())
70    }
71}
72fn parse_literal<'i>(input: &mut Span<'i>) -> PResult<&'i str> {
73    trace("parse_literal", |input: &mut _| {
74        let not_quote_slash = take_till(1.., (',', ')', '\\'));
75
76        not_quote_slash
77            .verify(|s: &str| !s.is_empty())
78            .parse_next(input)
79    })
80    .parse_next(input)
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84enum StringFragment<'a> {
85    Literal(&'a str),
86    EscapedChar(char),
87}
88
89fn parse_fragment<'i>(input: &mut Span<'i>) -> PResult<Option<StringFragment<'i>>> {
90    trace(
91        "parse_fragment",
92        alt((
93            parse_literal.map(|span| Some(StringFragment::Literal(span))),
94            parse_escaped_char.map(|res| res.map(StringFragment::EscapedChar)),
95        )),
96    )
97    .parse_next(input)
98}
99
100/// Construct a string by consuming the input until the next unescaped ) or ,.
101///
102/// Returns None if the string isn't valid.
103pub(super) fn parse_string(input: &mut Span<'_>) -> PResult<Option<String>> {
104    trace(
105        "parse_string",
106        repeat(0.., parse_fragment).fold(
107            || Some(String::new()),
108            |string, fragment| {
109                match (string, fragment) {
110                    (Some(mut string), Some(StringFragment::Literal(s))) => {
111                        string.push_str(s);
112                        Some(string)
113                    }
114                    (Some(mut string), Some(StringFragment::EscapedChar(c))) => {
115                        string.push(c);
116                        Some(string)
117                    }
118                    (Some(_), None) => {
119                        // We encountered a parsing error, and at this point we'll stop returning
120                        // values.
121                        None
122                    }
123                    (None, _) => None,
124                }
125            },
126        ),
127    )
128    .parse_next(input)
129}