use combine::easy::Errors as ParseError;
use combine::stream::easy::Error;
use combine::stream::position::SourcePosition;
use std::error::Error as StdError;
use std::fmt::{Display, Formatter, Result};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct TomlError {
message: String,
}
impl TomlError {
pub(crate) fn new(error: ParseError<u8, &[u8], usize>, input: &[u8]) -> Self {
Self {
message: format!("{}", FancyError::new(error, input)),
}
}
pub(crate) fn from_unparsed(pos: usize, input: &[u8]) -> Self {
Self::new(
ParseError::new(pos, CustomError::UnparsedLine.into()),
input,
)
}
pub(crate) fn custom(message: String) -> Self {
Self { message }
}
}
impl Display for TomlError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", self.message)
}
}
impl StdError for TomlError {
fn description(&self) -> &'static str {
"TOML parse error"
}
}
#[derive(Debug)]
pub(crate) struct FancyError<'a> {
errors: Vec<Error<char, String>>,
position: SourcePosition,
input: &'a [u8],
}
impl<'a> FancyError<'a> {
pub(crate) fn new(error: ParseError<u8, &'a [u8], usize>, input: &'a [u8]) -> Self {
let position = translate_position(input, error.position);
let errors: Vec<_> = error
.errors
.into_iter()
.map(|e| {
e.map_token(char::from)
.map_range(|s| String::from_utf8_lossy(s).into_owned())
})
.collect();
Self {
errors,
position,
input,
}
}
}
impl<'a> Display for FancyError<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let SourcePosition { line, column } = self.position;
let offset = line.to_string().len();
let content = self
.input
.split(|b| *b == b'\n')
.nth((line - 1) as usize)
.expect("line");
let content = String::from_utf8_lossy(content);
writeln!(f, "TOML parse error at line {}, column {}", line, column)?;
for _ in 0..=offset {
write!(f, " ")?;
}
writeln!(f, "|")?;
write!(f, "{} | ", line)?;
writeln!(f, "{}", content)?;
for _ in 0..=offset {
write!(f, " ")?;
}
write!(f, "|")?;
for _ in 0..column {
write!(f, " ")?;
}
writeln!(f, "^")?;
Error::fmt_errors(self.errors.as_ref(), f)
}
}
fn translate_position(input: &[u8], index: usize) -> SourcePosition {
if input.is_empty() {
return SourcePosition {
line: 1,
column: (index + 1) as i32,
};
}
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() + 1;
let line = line as i32;
let column = std::str::from_utf8(&input[line_start..=index])
.map(|s| s.chars().count())
.unwrap_or_else(|_| index - line_start + 1);
let column = (column + column_offset) as i32;
SourcePosition { 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, SourcePosition { line: 1, column: 1 });
}
#[test]
fn start() {
let input = b"Hello";
let index = 0;
let position = translate_position(&input[..], index);
assert_eq!(position, SourcePosition { line: 1, column: 1 });
}
#[test]
fn end() {
let input = b"Hello";
let index = input.len() - 1;
let position = translate_position(&input[..], index);
assert_eq!(
position,
SourcePosition {
line: 1,
column: input.len() as i32
}
);
}
#[test]
fn after() {
let input = b"Hello";
let index = input.len();
let position = translate_position(&input[..], index);
assert_eq!(
position,
SourcePosition {
line: 1,
column: (input.len() + 1) as i32
}
);
}
#[test]
fn first_line() {
let input = b"Hello\nWorld\n";
let index = 2;
let position = translate_position(&input[..], index);
assert_eq!(position, SourcePosition { line: 1, column: 3 });
}
#[test]
fn end_of_line() {
let input = b"Hello\nWorld\n";
let index = 5;
let position = translate_position(&input[..], index);
assert_eq!(position, SourcePosition { line: 1, column: 6 });
}
#[test]
fn start_of_second_line() {
let input = b"Hello\nWorld\n";
let index = 6;
let position = translate_position(&input[..], index);
assert_eq!(position, SourcePosition { line: 2, column: 1 });
}
#[test]
fn second_line() {
let input = b"Hello\nWorld\n";
let index = 8;
let position = translate_position(&input[..], index);
assert_eq!(position, SourcePosition { line: 2, column: 3 });
}
}
#[derive(Debug, Clone)]
pub(crate) enum CustomError {
DuplicateKey { key: String, table: String },
InvalidHexEscape(u32),
UnparsedLine,
OutOfRange,
}
impl StdError for CustomError {
fn description(&self) -> &'static str {
"TOML parse error"
}
}
impl Display for CustomError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match *self {
CustomError::DuplicateKey { ref key, ref table } => {
writeln!(f, "Duplicate key `{}` in `{}` table", key, table)
}
CustomError::InvalidHexEscape(ref h) => {
writeln!(f, "Invalid hex escape code: {:x} ", h)
}
CustomError::UnparsedLine => writeln!(f, "Could not parse the line"),
CustomError::OutOfRange => writeln!(f, "Value is out of range"),
}
}
}