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!(f, "TOML parse error at line {line_num}, column {col_num}")?;
115 for _ in 0..=gutter {
117 write!(f, " ")?;
118 }
119 writeln!(f, "|")?;
120
121 write!(f, "{line_num} | ")?;
123 writeln!(f, "{content}")?;
124
125 for _ in 0..=gutter {
127 write!(f, " ")?;
128 }
129 write!(f, "|")?;
130 for _ in 0..=column {
131 write!(f, " ")?;
132 }
133 write!(f, "^")?;
136 for _ in 1..highlight_len {
137 write!(f, "^")?;
138 }
139 writeln!(f)?;
140 }
141 writeln!(f, "{}", self.message)?;
142 if !context && !self.keys.is_empty() {
143 writeln!(f, "in `{}`", self.keys.join("."))?;
144 }
145
146 Ok(())
147 }
148}
149
150impl StdError for TomlError {
151 fn description(&self) -> &'static str {
152 "TOML parse error"
153 }
154}
155
156fn translate_position(input: &[u8], index: usize) -> (usize, usize) {
157 if input.is_empty() {
158 return (0, index);
159 }
160
161 let safe_index = index.min(input.len() - 1);
162 let column_offset = index - safe_index;
163 let index = safe_index;
164
165 let nl = input[0..index]
166 .iter()
167 .rev()
168 .enumerate()
169 .find(|(_, b)| **b == b'\n')
170 .map(|(nl, _)| index - nl - 1);
171 let line_start = match nl {
172 Some(nl) => nl + 1,
173 None => 0,
174 };
175 let line = input[0..line_start].iter().filter(|b| **b == b'\n').count();
176
177 let column = std::str::from_utf8(&input[line_start..=index])
178 .map(|s| s.chars().count() - 1)
179 .unwrap_or_else(|_| index - line_start);
180 let column = column + column_offset;
181
182 (line, column)
183}
184
185#[cfg(test)]
186mod test_translate_position {
187 use super::*;
188
189 #[test]
190 fn empty() {
191 let input = b"";
192 let index = 0;
193 let position = translate_position(&input[..], index);
194 assert_eq!(position, (0, 0));
195 }
196
197 #[test]
198 fn start() {
199 let input = b"Hello";
200 let index = 0;
201 let position = translate_position(&input[..], index);
202 assert_eq!(position, (0, 0));
203 }
204
205 #[test]
206 fn end() {
207 let input = b"Hello";
208 let index = input.len() - 1;
209 let position = translate_position(&input[..], index);
210 assert_eq!(position, (0, input.len() - 1));
211 }
212
213 #[test]
214 fn after() {
215 let input = b"Hello";
216 let index = input.len();
217 let position = translate_position(&input[..], index);
218 assert_eq!(position, (0, input.len()));
219 }
220
221 #[test]
222 fn first_line() {
223 let input = b"Hello\nWorld\n";
224 let index = 2;
225 let position = translate_position(&input[..], index);
226 assert_eq!(position, (0, 2));
227 }
228
229 #[test]
230 fn end_of_line() {
231 let input = b"Hello\nWorld\n";
232 let index = 5;
233 let position = translate_position(&input[..], index);
234 assert_eq!(position, (0, 5));
235 }
236
237 #[test]
238 fn start_of_second_line() {
239 let input = b"Hello\nWorld\n";
240 let index = 6;
241 let position = translate_position(&input[..], index);
242 assert_eq!(position, (1, 0));
243 }
244
245 #[test]
246 fn second_line() {
247 let input = b"Hello\nWorld\n";
248 let index = 8;
249 let position = translate_position(&input[..], index);
250 assert_eq!(position, (1, 2));
251 }
252}