cucumber_expressions/
combinator.rs

1// Copyright (c) 2021-2025  Brendan Molloy <brendan@bbqsrc.net>,
2//                          Ilya Solovyiov <ilya.solovyiov@gmail.com>,
3//                          Kai Ren <tyranron@gmail.com>
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! Helper parser combinators.
12
13use nom::{
14    AsChar, Err, IResult, Input, Offset, Parser,
15    error::{ErrorKind, ParseError},
16};
17
18/// Applies the given `map` function to the `parser`'s [`IResult`] in case it
19/// represents an error.
20///
21/// Can be used to harden an [`Error`] into a [`Failure`].
22///
23/// [`Error`]: nom::Err::Error
24/// [`Failure`]: nom::Err::Failure
25/// [`verify()`]: nom::combinator::verify()
26pub(crate) fn map_err<I, F, G>(
27    mut parser: F,
28    mut map: G,
29) -> impl FnMut(I) -> IResult<I, F::Output, F::Error>
30where
31    F: Parser<I>,
32    G: FnMut(Err<F::Error>) -> Err<F::Error>,
33{
34    move |input: I| parser.parse(input).map_err(&mut map)
35}
36
37/// Matches a byte string with escaped characters.
38///
39/// Differences from [`escaped()`]:
40/// 1. If `normal` matched empty sequence, tries to match escaped;
41/// 2. If `normal` matched empty sequence and then `escapable` didn't match
42///    anything, returns an empty sequence;
43/// 3. Errors with [`ErrorKind::Escaped`] if a `control_char` was followed by a
44///    non-`escapable` `Input` or end of line.
45///
46/// [`escaped()`]: nom::bytes::complete::escaped()
47pub(crate) fn escaped0<I, Error, F, G>(
48    mut normal: F,
49    control_char: char,
50    mut escapable: G,
51) -> impl FnMut(I) -> IResult<I, I, Error>
52where
53    I: Clone + Offset + Input,
54    <I as Input>::Item: AsChar,
55    F: Parser<I, Error = Error>,
56    G: Parser<I, Error = Error>,
57    Error: ParseError<I>,
58{
59    move |input: I| {
60        let mut i = input.clone();
61        let mut consumed_nothing = false;
62
63        while i.input_len() > 0 {
64            let current_len = i.input_len();
65
66            match (normal.parse(i.clone()), consumed_nothing) {
67                (Ok((i2, _)), false) => {
68                    if i2.input_len() == 0 {
69                        return Ok((input.take_from(input.input_len()), input));
70                    }
71                    if i2.input_len() == current_len {
72                        consumed_nothing = true;
73                    }
74                    i = i2;
75                }
76                (Ok(..), true) | (Err(Err::Error(_)), _) => {
77                    let next_char = i
78                        .iter_elements()
79                        .next()
80                        .ok_or_else(|| {
81                            Err::Error(Error::from_error_kind(
82                                i.clone(),
83                                ErrorKind::Escaped,
84                            ))
85                        })?
86                        .as_char();
87                    if next_char == control_char {
88                        let next = control_char.len_utf8();
89                        if next >= i.input_len() {
90                            return Err(Err::Error(Error::from_error_kind(
91                                input,
92                                ErrorKind::Escaped,
93                            )));
94                        }
95                        match escapable.parse(i.take_from(next)) {
96                            Ok((i2, _)) => {
97                                if i2.input_len() == 0 {
98                                    return Ok((
99                                        input.take_from(input.input_len()),
100                                        input,
101                                    ));
102                                }
103                                consumed_nothing = false;
104                                i = i2;
105                            }
106                            Err(_) => {
107                                return Err(Err::Error(
108                                    Error::from_error_kind(
109                                        i,
110                                        ErrorKind::Escaped,
111                                    ),
112                                ));
113                            }
114                        }
115                    } else {
116                        let index = input.offset(&i);
117                        return Ok(input.take_split(index));
118                    }
119                }
120                (Err(e), _) => {
121                    return Err(e);
122                }
123            }
124        }
125
126        Ok((input.take_from(input.input_len()), input))
127    }
128}
129
130#[cfg(test)]
131mod escaped0_spec {
132    use nom::{
133        Err, IResult,
134        bytes::complete::escaped,
135        character::complete::{digit0, digit1, one_of},
136        error::{Error, ErrorKind},
137    };
138
139    use super::escaped0;
140
141    /// Type used to compare behaviour of [`escaped`] and [`escaped0`].
142    ///
143    /// Tuple is constructed from the following parsers results:
144    /// - [`escaped0`]`(`[`digit0`]`, '\\', `[`one_of`]`(r#""n\"#))`
145    /// - [`escaped0`]`(`[`digit1`]`, '\\', `[`one_of`]`(r#""n\"#))`
146    /// - [`escaped`]`(`[`digit0`]`, '\\', `[`one_of`]`(r#""n\"#))`
147    /// - [`escaped`]`(`[`digit1`]`, '\\', `[`one_of`]`(r#""n\"#))`
148    type TestResult<'s> = (
149        IResult<&'s str, &'s str>,
150        IResult<&'s str, &'s str>,
151        IResult<&'s str, &'s str>,
152        IResult<&'s str, &'s str>,
153    );
154
155    /// Produces a [`TestResult`] from the given `input`.
156    fn get_result(input: &str) -> TestResult<'_> {
157        (
158            escaped0(digit0, '\\', one_of(r#""n\"#))(input),
159            escaped0(digit1, '\\', one_of(r#""n\"#))(input),
160            escaped(digit0, '\\', one_of(r#""n\"#))(input),
161            escaped(digit1, '\\', one_of(r#""n\"#))(input),
162        )
163    }
164
165    #[test]
166    fn matches_empty() {
167        assert_eq!(
168            get_result(""),
169            (Ok(("", "")), Ok(("", "")), Ok(("", "")), Ok(("", ""))),
170        );
171    }
172
173    #[test]
174    fn matches_normal() {
175        assert_eq!(
176            get_result("123;"),
177            (
178                Ok((";", "123")),
179                Ok((";", "123")),
180                Ok((";", "123")),
181                Ok((";", "123"))
182            ),
183        );
184    }
185
186    #[test]
187    fn matches_only_escaped() {
188        assert_eq!(
189            get_result(r#"\n\";"#),
190            (
191                Ok((";", r#"\n\""#)),
192                Ok((";", r#"\n\""#)),
193                Ok((r#"\n\";"#, "")),
194                Ok((";", r#"\n\""#)),
195            ),
196        );
197    }
198
199    #[test]
200    fn matches_escaped_followed_by_normal() {
201        assert_eq!(
202            get_result(r#"\n\"123;"#),
203            (
204                Ok((";", r#"\n\"123"#)),
205                Ok((";", r#"\n\"123"#)),
206                Ok((r#"\n\"123;"#, "")),
207                Ok((";", r#"\n\"123"#)),
208            ),
209        );
210    }
211
212    #[test]
213    fn matches_normal_followed_by_escaped() {
214        assert_eq!(
215            get_result(r#"123\n\";"#),
216            (
217                Ok((";", r#"123\n\""#)),
218                Ok((";", r#"123\n\""#)),
219                Ok((r#"\n\";"#, "123")),
220                Ok((";", r#"123\n\""#)),
221            ),
222        );
223    }
224
225    #[test]
226    fn matches_escaped_followed_by_normal_then_escaped() {
227        assert_eq!(
228            get_result(r#"\n\"123\n;"#),
229            (
230                Ok((";", r#"\n\"123\n"#)),
231                Ok((";", r#"\n\"123\n"#)),
232                Ok((r#"\n\"123\n;"#, "")),
233                Ok((";", r#"\n\"123\n"#)),
234            ),
235        );
236    }
237
238    #[test]
239    fn matches_normal_followed_by_escaped_then_normal() {
240        assert_eq!(
241            get_result(r#"123\n\"567;"#),
242            (
243                Ok((";", r#"123\n\"567"#)),
244                Ok((";", r#"123\n\"567"#)),
245                Ok((r#"\n\"567;"#, "123")),
246                Ok((";", r#"123\n\"567"#)),
247            ),
248        );
249    }
250
251    #[test]
252    fn errors_on_escaped_non_reserved() {
253        assert_eq!(
254            get_result(r#"\n\r"#),
255            (
256                Err(Err::Error(Error {
257                    input: r#"\r"#,
258                    code: ErrorKind::Escaped,
259                })),
260                Err(Err::Error(Error {
261                    input: r#"\r"#,
262                    code: ErrorKind::Escaped,
263                })),
264                Ok((r#"\n\r"#, "")),
265                Err(Err::Error(Error {
266                    input: r#"r"#,
267                    code: ErrorKind::OneOf,
268                })),
269            ),
270        );
271    }
272
273    #[test]
274    fn errors_on_ending_with_control_char() {
275        assert_eq!(
276            get_result("\\"),
277            (
278                Err(Err::Error(Error {
279                    input: "\\",
280                    code: ErrorKind::Escaped,
281                })),
282                Err(Err::Error(Error {
283                    input: "\\",
284                    code: ErrorKind::Escaped,
285                })),
286                Ok(("\\", "")),
287                Err(Err::Error(Error {
288                    input: "\\",
289                    code: ErrorKind::Escaped,
290                })),
291            ),
292        );
293    }
294}