1use std::error::Error as StdError;
2use std::fmt::{Display, Formatter, Result};
3
4#[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 pub fn message(&self) -> &str {
68 &self.message
69 }
70
71 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
87impl 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 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 for _ in 0..=gutter {
121 write!(f, " ")?;
122 }
123 writeln!(f, "|")?;
124
125 write!(f, "{} | ", line_num)?;
127 writeln!(f, "{}", content)?;
128
129 for _ in 0..=gutter {
131 write!(f, " ")?;
132 }
133 write!(f, "|")?;
134 for _ in 0..=column {
135 write!(f, " ")?;
136 }
137 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}