toml_edit/
error.rs

1use std::error::Error as StdError;
2use std::fmt::{Display, Formatter, Result};
3
4/// Type representing a TOML parse error
5#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6pub struct TomlError {
7    message: String,
8    raw: Option<String>,
9    keys: Vec<String>,
10    span: Option<std::ops::Range<usize>>,
11}
12
13impl TomlError {
14    #[cfg(feature = "parse")]
15    pub(crate) fn new(
16        error: winnow::error::ParseError<
17            crate::parser::prelude::Input<'_>,
18            winnow::error::ContextError,
19        >,
20        mut raw: crate::parser::prelude::Input<'_>,
21    ) -> Self {
22        use winnow::stream::Stream;
23
24        let message = error.inner().to_string();
25        let raw = raw.finish();
26        let raw = String::from_utf8(raw.to_owned()).expect("original document was utf8");
27
28        let offset = error.offset();
29        let offset = (0..=offset)
30            .rev()
31            .find(|index| raw.is_char_boundary(*index))
32            .unwrap_or(0);
33
34        let mut indices = raw[offset..].char_indices();
35        indices.next();
36        let len = if let Some((index, _)) = indices.next() {
37            index
38        } else {
39            raw.len() - offset
40        };
41        let span = offset..(offset + len);
42
43        Self {
44            message,
45            raw: Some(raw),
46            keys: Vec::new(),
47            span: Some(span),
48        }
49    }
50
51    #[cfg(any(feature = "serde", feature = "parse"))]
52    pub(crate) fn custom(message: String, span: Option<std::ops::Range<usize>>) -> Self {
53        Self {
54            message,
55            raw: None,
56            keys: Vec::new(),
57            span,
58        }
59    }
60
61    #[cfg(feature = "serde")]
62    pub(crate) fn add_key(&mut self, key: String) {
63        self.keys.insert(0, key);
64    }
65
66    /// What went wrong
67    pub fn message(&self) -> &str {
68        &self.message
69    }
70
71    /// The start/end index into the original document where the error occurred
72    pub fn span(&self) -> Option<std::ops::Range<usize>> {
73        self.span.clone()
74    }
75
76    #[cfg(feature = "serde")]
77    pub(crate) fn set_span(&mut self, span: Option<std::ops::Range<usize>>) {
78        self.span = span;
79    }
80
81    #[cfg(feature = "serde")]
82    pub(crate) fn set_raw(&mut self, raw: Option<String>) {
83        self.raw = raw;
84    }
85}
86
87/// Displays a TOML parse error
88///
89/// # Example
90///
91/// TOML parse error at line 1, column 10
92///   |
93/// 1 | 00:32:00.a999999
94///   |          ^
95/// Unexpected `a`
96/// Expected `digit`
97/// While parsing a Time
98/// While parsing a Date-Time
99impl Display for TomlError {
100    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
101        let mut context = false;
102        if let (Some(raw), Some(span)) = (&self.raw, self.span()) {
103            context = true;
104
105            let (line, column) = translate_position(raw.as_bytes(), span.start);
106            let line_num = line + 1;
107            let col_num = column + 1;
108            let gutter = line_num.to_string().len();
109            let content = raw.split('\n').nth(line).expect("valid line number");
110            let highlight_len = span.end - span.start;
111            // Allow highlight to go one past the line
112            let highlight_len = highlight_len.min(content.len().saturating_sub(column));
113
114            writeln!(
115                f,
116                "TOML parse error at line {}, column {}",
117                line_num, col_num
118            )?;
119            //   |
120            for _ in 0..=gutter {
121                write!(f, " ")?;
122            }
123            writeln!(f, "|")?;
124
125            // 1 | 00:32:00.a999999
126            write!(f, "{} | ", line_num)?;
127            writeln!(f, "{}", content)?;
128
129            //   |          ^
130            for _ in 0..=gutter {
131                write!(f, " ")?;
132            }
133            write!(f, "|")?;
134            for _ in 0..=column {
135                write!(f, " ")?;
136            }
137            // The span will be empty at eof, so we need to make sure we always print at least
138            // one `^`
139            write!(f, "^")?;
140            for _ in 1..highlight_len {
141                write!(f, "^")?;
142            }
143            writeln!(f)?;
144        }
145        writeln!(f, "{}", self.message)?;
146        if !context && !self.keys.is_empty() {
147            writeln!(f, "in `{}`", self.keys.join("."))?;
148        }
149
150        Ok(())
151    }
152}
153
154impl StdError for TomlError {
155    fn description(&self) -> &'static str {
156        "TOML parse error"
157    }
158}
159
160fn translate_position(input: &[u8], index: usize) -> (usize, usize) {
161    if input.is_empty() {
162        return (0, index);
163    }
164
165    let safe_index = index.min(input.len() - 1);
166    let column_offset = index - safe_index;
167    let index = safe_index;
168
169    let nl = input[0..index]
170        .iter()
171        .rev()
172        .enumerate()
173        .find(|(_, b)| **b == b'\n')
174        .map(|(nl, _)| index - nl - 1);
175    let line_start = match nl {
176        Some(nl) => nl + 1,
177        None => 0,
178    };
179    let line = input[0..line_start].iter().filter(|b| **b == b'\n').count();
180
181    let column = std::str::from_utf8(&input[line_start..=index])
182        .map(|s| s.chars().count() - 1)
183        .unwrap_or_else(|_| index - line_start);
184    let column = column + column_offset;
185
186    (line, column)
187}
188
189#[cfg(test)]
190mod test_translate_position {
191    use super::*;
192
193    #[test]
194    fn empty() {
195        let input = b"";
196        let index = 0;
197        let position = translate_position(&input[..], index);
198        assert_eq!(position, (0, 0));
199    }
200
201    #[test]
202    fn start() {
203        let input = b"Hello";
204        let index = 0;
205        let position = translate_position(&input[..], index);
206        assert_eq!(position, (0, 0));
207    }
208
209    #[test]
210    fn end() {
211        let input = b"Hello";
212        let index = input.len() - 1;
213        let position = translate_position(&input[..], index);
214        assert_eq!(position, (0, input.len() - 1));
215    }
216
217    #[test]
218    fn after() {
219        let input = b"Hello";
220        let index = input.len();
221        let position = translate_position(&input[..], index);
222        assert_eq!(position, (0, input.len()));
223    }
224
225    #[test]
226    fn first_line() {
227        let input = b"Hello\nWorld\n";
228        let index = 2;
229        let position = translate_position(&input[..], index);
230        assert_eq!(position, (0, 2));
231    }
232
233    #[test]
234    fn end_of_line() {
235        let input = b"Hello\nWorld\n";
236        let index = 5;
237        let position = translate_position(&input[..], index);
238        assert_eq!(position, (0, 5));
239    }
240
241    #[test]
242    fn start_of_second_line() {
243        let input = b"Hello\nWorld\n";
244        let index = 6;
245        let position = translate_position(&input[..], index);
246        assert_eq!(position, (1, 0));
247    }
248
249    #[test]
250    fn second_line() {
251        let input = b"Hello\nWorld\n";
252        let index = 8;
253        let position = translate_position(&input[..], index);
254        assert_eq!(position, (1, 2));
255    }
256}