use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result}; /// Type representing a TOML parse error #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct TomlError { message: String, raw: Option, keys: Vec, span: Option>, } impl TomlError { #[cfg(feature = "parse")] pub(crate) fn new( error: winnow::error::ParseError< crate::parser::prelude::Input<'_>, winnow::error::ContextError, >, mut raw: crate::parser::prelude::Input<'_>, ) -> Self { use winnow::stream::Stream; let message = error.inner().to_string(); let raw = raw.finish(); let raw = String::from_utf8(raw.to_owned()).expect("original document was utf8"); let offset = error.offset(); let offset = (0..=offset) .rev() .find(|index| raw.is_char_boundary(*index)) .unwrap_or(0); let mut indices = raw[offset..].char_indices(); indices.next(); let len = if let Some((index, _)) = indices.next() { index } else { raw.len() - offset }; let span = offset..(offset + len); Self { message, raw: Some(raw), keys: Vec::new(), span: Some(span), } } #[cfg(any(feature = "serde", feature = "parse"))] pub(crate) fn custom(message: String, span: Option>) -> Self { Self { message, raw: None, keys: Vec::new(), span, } } #[cfg(feature = "serde")] pub(crate) fn add_key(&mut self, key: String) { self.keys.insert(0, key); } /// What went wrong pub fn message(&self) -> &str { &self.message } /// The start/end index into the original document where the error occurred pub fn span(&self) -> Option> { self.span.clone() } #[cfg(feature = "serde")] pub(crate) fn set_span(&mut self, span: Option>) { self.span = span; } #[cfg(feature = "serde")] pub(crate) fn set_raw(&mut self, raw: Option) { self.raw = raw; } } /// Displays a TOML parse error /// /// # Example /// /// TOML parse error at line 1, column 10 /// | /// 1 | 00:32:00.a999999 /// | ^ /// Unexpected `a` /// Expected `digit` /// While parsing a Time /// While parsing a Date-Time impl Display for TomlError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let mut context = false; if let (Some(raw), Some(span)) = (&self.raw, self.span()) { context = true; let (line, column) = translate_position(raw.as_bytes(), span.start); let line_num = line + 1; let col_num = column + 1; let gutter = line_num.to_string().len(); let content = raw.split('\n').nth(line).expect("valid line number"); let highlight_len = span.end - span.start; // Allow highlight to go one past the line let highlight_len = highlight_len.min(content.len().saturating_sub(column)); writeln!( f, "TOML parse error at line {}, column {}", line_num, col_num )?; // | for _ in 0..=gutter { write!(f, " ")?; } writeln!(f, "|")?; // 1 | 00:32:00.a999999 write!(f, "{} | ", line_num)?; writeln!(f, "{}", content)?; // | ^ for _ in 0..=gutter { write!(f, " ")?; } write!(f, "|")?; for _ in 0..=column { write!(f, " ")?; } // The span will be empty at eof, so we need to make sure we always print at least // one `^` write!(f, "^")?; for _ in 1..highlight_len { write!(f, "^")?; } writeln!(f)?; } writeln!(f, "{}", self.message)?; if !context && !self.keys.is_empty() { writeln!(f, "in `{}`", self.keys.join("."))?; } Ok(()) } } impl StdError for TomlError { fn description(&self) -> &'static str { "TOML parse error" } } fn translate_position(input: &[u8], index: usize) -> (usize, usize) { if input.is_empty() { return (0, index); } let safe_index = index.min(input.len() - 1); let column_offset = index - safe_index; let index = safe_index; let nl = input[0..index] .iter() .rev() .enumerate() .find(|(_, b)| **b == b'\n') .map(|(nl, _)| index - nl - 1); let line_start = match nl { Some(nl) => nl + 1, None => 0, }; let line = input[0..line_start].iter().filter(|b| **b == b'\n').count(); let column = std::str::from_utf8(&input[line_start..=index]) .map(|s| s.chars().count() - 1) .unwrap_or_else(|_| index - line_start); let column = column + column_offset; (line, column) } #[cfg(test)] mod test_translate_position { use super::*; #[test] fn empty() { let input = b""; let index = 0; let position = translate_position(&input[..], index); assert_eq!(position, (0, 0)); } #[test] fn start() { let input = b"Hello"; let index = 0; let position = translate_position(&input[..], index); assert_eq!(position, (0, 0)); } #[test] fn end() { let input = b"Hello"; let index = input.len() - 1; let position = translate_position(&input[..], index); assert_eq!(position, (0, input.len() - 1)); } #[test] fn after() { let input = b"Hello"; let index = input.len(); let position = translate_position(&input[..], index); assert_eq!(position, (0, input.len())); } #[test] fn first_line() { let input = b"Hello\nWorld\n"; let index = 2; let position = translate_position(&input[..], index); assert_eq!(position, (0, 2)); } #[test] fn end_of_line() { let input = b"Hello\nWorld\n"; let index = 5; let position = translate_position(&input[..], index); assert_eq!(position, (0, 5)); } #[test] fn start_of_second_line() { let input = b"Hello\nWorld\n"; let index = 6; let position = translate_position(&input[..], index); assert_eq!(position, (1, 0)); } #[test] fn second_line() { let input = b"Hello\nWorld\n"; let index = 8; let position = translate_position(&input[..], index); assert_eq!(position, (1, 2)); } }