password_rules_parser/
error.rs

1//! Errors that can be returned from the parsing process
2
3use std::fmt::{self, Display, Formatter, Write};
4use std::{convert::TryInto, error::Error};
5
6use itertools::{Itertools, Position::*};
7use nom::error::{ErrorKind, FromExternalError, ParseError};
8use pretty_lint::{Position, PrettyLint, Span};
9
10/// Extension trait for nom::error::ParseError that allows for collecting the
11/// failed tag in the event of a mismatch, similar to ParseError::from_char.
12pub(crate) trait WithTagError<I> {
13    /// Construct the error from the given input and tag
14    fn from_tag(input: I, tag: &'static str) -> Self;
15}
16
17impl<I> WithTagError<I> for () {
18    fn from_tag(_: I, _: &'static str) -> Self {}
19}
20
21/// Different kinds of things that can be expected at a given location
22#[derive(Debug, Clone, Copy)]
23pub enum Expected {
24    /// Expected EoF (End of File)
25    Eof,
26    /// Expected a character
27    Char(char),
28    /// Expected a tag
29    ///
30    /// A tag is a particular string token (such as "upper").
31    Tag(&'static str),
32    /// Expected a number
33    Number,
34}
35
36impl Display for Expected {
37    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
38        match self {
39            Expected::Eof => write!(f, "EoF"),
40            Expected::Number => write!(f, "a number"),
41            Expected::Char(c) => write!(f, "{c:?}"),
42            Expected::Tag(tag) => write!(f, "{tag:?}"),
43        }
44    }
45}
46
47/// A single parse failure at a location– reports what was expected at what
48/// location
49#[derive(Debug, Clone, Copy)]
50pub struct ExpectedAt {
51    /// The character index of the error location
52    pub index: usize,
53
54    /// The line number (starting with 1) of the error location
55    pub line: u32,
56
57    /// The column number (starting with 1) of the error location
58    pub column: u32,
59
60    /// The specific element that was expected
61    pub expected: Expected,
62}
63
64/// This error type is used while parsing is running; it knows only the tail
65/// end of the input at the time an error occurred. It can be combined with the
66/// original input to produce an absolute error location via `ExpectedAt`.
67#[derive(Debug, Clone)]
68pub(crate) struct ExpectedContext<'a> {
69    input_tail: &'a str,
70    expected: Expected,
71}
72
73impl<'a> ExpectedContext<'a> {
74    /// Given the original input string, extract the absolute error location of
75    /// an ErrorContext
76    pub fn extract_context(self, input: &'a str) -> ExpectedAt {
77        let offset = input
78            .len()
79            .checked_sub(self.input_tail.len())
80            .expect("input size was smaller than the tail size");
81
82        let prefix = &input[..offset];
83
84        let line_number = prefix.chars().filter(|&c| c == '\n').count() + 1;
85        let last_line_start = prefix
86            .char_indices()
87            .rev()
88            .find(|&(_, c)| c == '\n')
89            .map(|(index, _)| index + 1)
90            .unwrap_or(0);
91        let column_number = (offset - last_line_start) + 1;
92
93        ExpectedAt {
94            line: line_number
95                .try_into()
96                .expect("More than 4 billion lines of input"),
97            column: column_number
98                .try_into()
99                .expect("More than 4 billion columns of input"),
100            index: offset,
101            expected: self.expected,
102        }
103    }
104}
105
106/// This error type is used while parsing is running; it knows only the tail
107/// end of the input at the time an error occurred. It can be combined with the
108/// original input to produce a absolute error locations in `PasswordRulesError`.
109#[derive(Debug, Clone)]
110pub(crate) struct PasswordRulesErrorContext<'a> {
111    expectations: Vec<ExpectedContext<'a>>,
112}
113
114impl<'a> PasswordRulesErrorContext<'a> {
115    /// Given the original input string, extract the absolute error location of
116    /// an all the errors
117    pub fn extract_context(self, input: &'a str) -> PasswordRulesError {
118        let mut expectations: Vec<ExpectedAt> = self
119            .expectations
120            .into_iter()
121            .map(|exp| exp.extract_context(input))
122            .collect();
123
124        expectations.sort_unstable_by_key(|exp| exp.index);
125
126        PasswordRulesError { expectations }
127    }
128}
129
130impl<'a> ParseError<&'a str> for PasswordRulesErrorContext<'a> {
131    fn from_error_kind(input: &'a str, kind: ErrorKind) -> Self {
132        match kind {
133            ErrorKind::Eof => Self {
134                expectations: vec![ExpectedContext {
135                    input_tail: input,
136                    expected: Expected::Eof,
137                }],
138            },
139            ErrorKind::Digit => Self {
140                expectations: vec![ExpectedContext {
141                    input_tail: input,
142                    expected: Expected::Number,
143                }],
144            },
145            _ => Self {
146                expectations: vec![],
147            },
148        }
149    }
150
151    fn append(input: &'a str, kind: nom::error::ErrorKind, other: Self) -> Self {
152        Self::from_error_kind(input, kind).or(other)
153    }
154
155    fn or(mut self, other: Self) -> Self {
156        self.expectations.extend(other.expectations);
157        self
158    }
159
160    fn from_char(input: &'a str, c: char) -> Self {
161        Self {
162            expectations: vec![ExpectedContext {
163                input_tail: input,
164                expected: Expected::Char(c),
165            }],
166        }
167    }
168}
169
170impl<'a> WithTagError<&'a str> for PasswordRulesErrorContext<'a> {
171    fn from_tag(input: &'a str, tag: &'static str) -> Self {
172        Self {
173            expectations: vec![ExpectedContext {
174                input_tail: input,
175                expected: Expected::Tag(tag),
176            }],
177        }
178    }
179}
180
181impl<'a> FromExternalError<&'a str, std::num::ParseIntError> for PasswordRulesErrorContext<'a> {
182    fn from_external_error(input: &'a str, kind: ErrorKind, _: std::num::ParseIntError) -> Self {
183        Self::from_error_kind(input, kind)
184    }
185}
186
187/// Error that can result from parsing password rules
188#[derive(Debug, Clone)]
189pub struct PasswordRulesError {
190    /// Elements (like a character, string tag, or EoF) that the parser was expecting,
191    /// along with the location where the element was expected
192    pub expectations: Vec<ExpectedAt>,
193}
194
195impl PasswordRulesError {
196    pub(crate) fn empty() -> Self {
197        Self {
198            expectations: vec![],
199        }
200    }
201
202    /// Build a pretty version of the error given the original input string.
203    ///
204    /// The default `Display` implementation produces helpful output:
205    ///
206    /// ```text
207    /// Error: expected one of:
208    ///   "required", "allowed", "max-consecutive", "minlength", "maxlength", or EoF at 1:71
209    /// ```
210    ///
211    /// It doesn't have access to the original input string, however, so it's limited
212    /// in what it can do.
213    ///
214    /// This method produces pretty output with colors if you're able to provide that:
215    ///
216    /// ```text
217    /// error: parsing failed
218    ///  --> 1:71
219    ///   |
220    /// 1 | minlength: 8; maxlength: 32; required: lower, upper; required: digit; allow
221    ///   |                                                                       ^ expected one of "required", "allowed", "max-consecutive", "minlength", "maxlength", or EoF
222    /// ```
223    pub fn to_string_pretty(&self, s: &str) -> Result<String, fmt::Error> {
224        let lint_base = PrettyLint::error(s).with_message("parsing failed");
225
226        Ok(match self.expectations.as_slice() {
227            [] => lint_base.with_inline_message("unknown error").to_string(),
228            [exp] => lint_base
229                .with_inline_message(&format!("expected {}", exp.expected))
230                .at(Span {
231                    start: Position {
232                        line: exp.line as usize,
233                        col: exp.column as usize,
234                    },
235                    end: Position {
236                        line: exp.line as usize,
237                        col: exp.column as usize,
238                    },
239                })
240                .to_string(),
241            expectations => {
242                // Group the expectations by location, so that several expectations at the same
243                // location can be shown together
244                let groups = expectations.iter().chunk_by(|exp| (exp.line, exp.column));
245                let mut lint_string = String::new();
246
247                groups.into_iter().try_for_each(|((line, column), group)| {
248                    let mut inline_message = String::from("expected one of ");
249
250                    group
251                        .with_position()
252                        .try_for_each(|positioned_exp| match positioned_exp {
253                            (Only, exp) => write!(inline_message, "{}", exp.expected),
254                            (First, exp) | (Middle, exp) => {
255                                write!(inline_message, "{}, ", exp.expected)
256                            }
257                            (Last, exp) => write!(inline_message, "or {}", exp.expected),
258                        })?;
259
260                    let lint = PrettyLint::error(s)
261                        .with_message("parsing failed")
262                        .with_inline_message(&inline_message)
263                        .at(Span {
264                            start: Position {
265                                line: line as usize,
266                                col: column as usize,
267                            },
268                            end: Position {
269                                line: line as usize,
270                                col: column as usize,
271                            },
272                        });
273
274                    write!(lint_string, "{lint}")
275                })?;
276
277                lint_string
278            }
279        })
280    }
281}
282
283impl Display for PasswordRulesError {
284    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
285        match self.expectations.as_slice() {
286            [] => write!(f, "unknown error"),
287            [exp] => write!(
288                f,
289                "expected {} at {}:{}",
290                exp.expected, exp.line, exp.column
291            ),
292            expectations => {
293                // Group the expectations by location, so that several expectations at the same
294                // location can be shown together
295                writeln!(f, "expected one of:")?;
296
297                let groups = expectations.iter().chunk_by(|exp| (exp.line, exp.column));
298
299                groups.into_iter().try_for_each(|((line, column), group)| {
300                    write!(f, "  ")?;
301
302                    group
303                        .with_position()
304                        .try_for_each(|positioned_exp| match positioned_exp {
305                            (Only, exp) => write!(f, "{}", exp.expected),
306                            (First, exp) | (Middle, exp) => write!(f, "{}, ", exp.expected),
307                            (Last, exp) => write!(f, "or {}", exp.expected),
308                        })?;
309
310                    writeln!(f, " at {line}:{column}")
311                })
312            }
313        }
314    }
315}
316
317impl Error for PasswordRulesError {}