use std::{
ffi::c_int,
fmt::{
self,
Display,
Formatter,
},
slice,
};
use tidy_sys::TidyBuffer;
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub enum Error {
ContainsNullByte(usize),
ParseUtf8,
MaxSizeReached,
Doc(Vec<Diagnostic>),
Errno(i32),
}
impl std::error::Error for Error {}
#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct Diagnostic {
pub level: DiagnosticLevel,
pub line: usize,
pub column: usize,
pub message: String,
}
#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
pub enum DiagnosticLevel {
Warning,
Error,
}
fn errno_msg(no: c_int) -> String {
#[cfg(windows)]
{
extern "C" {
fn strerror_s(buf: *mut u8, bufsz: usize, errnum: c_int) -> c_int;
}
let mut buf = vec![0; 128];
let res = unsafe { strerror_s(buf.as_mut_ptr(), buf.capacity(), no) };
debug_assert_eq!(res, 0);
let nil = memchr::memchr(0, &buf)
.expect("buffer was supposed to contain a null byte but it doesn't");
buf.truncate(nil);
String::from_utf8(buf).expect("strerror returned a non-utf8 string")
}
#[cfg(not(windows))]
errno::Errno(no).to_string()
}
impl Diagnostic {
pub(crate) fn parse(s: &str) -> Option<Self> {
let s = s.trim();
let s = s.strip_prefix("line ")?;
let (ln, s) = s.split_once(' ')?;
let s = s.strip_prefix("column ")?;
let (col, s) = s.split_once(' ')?;
let s = s.strip_prefix("- ")?;
let (level, msg) = s.split_once(": ")?;
let level = match level {
"Error" => DiagnosticLevel::Error,
"Warning" => DiagnosticLevel::Warning,
_ => return None,
};
let line = ln.parse::<usize>().ok()?;
let column = col.parse::<usize>().ok()?;
Some(Diagnostic {
line,
column,
level,
message: msg.to_string(),
})
}
pub(crate) fn parse_all(err_buf: *const TidyBuffer) -> Vec<Self> {
let sink = unsafe {
let buf = *err_buf;
if buf.bp.is_null() {
return Vec::new();
}
slice::from_raw_parts::<u8>(buf.bp, buf.size as _)
};
let mut list = Vec::with_capacity(memchr::memchr_iter(b'\n', sink).count() + 1);
let mut last = 0;
for i in memchr::memchr_iter(b'\n', sink) {
let s = &sink[last..i];
last = i + 1;
if !s.is_empty() {
list.extend(Self::parse(&String::from_utf8_lossy(s)));
}
}
if last < sink.len() {
list.extend(Self::parse(&String::from_utf8_lossy(&sink[last..])));
}
list
}
pub fn is_error(&self) -> bool {
self.level == DiagnosticLevel::Error
}
pub fn is_warning(&self) -> bool {
self.level == DiagnosticLevel::Warning
}
}
impl Display for Diagnostic {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{} at line {} column {}: {}",
self.level, self.line, self.column, self.message
)
}
}
impl Display for DiagnosticLevel {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let s = match self {
Self::Error => "error",
Self::Warning => "warning",
};
f.write_str(s)
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::ContainsNullByte(index) => {
write!(f, "the input contains a null byte at index {index}")
}
Self::ParseUtf8 => f.write_str("failed to parse the formatted output as UTF8"),
Self::MaxSizeReached => {
f.write_str("formatted document exceeded the maximum size possible with tidy")
}
&Self::Errno(no) => {
write!(f, "Tidy error errno {}: {}", no, errno_msg(no))
}
Self::Doc(errs) => {
let n_err = errs.iter().filter(|d| d.is_error()).count();
if n_err == 0 {
return f.write_str("unknown document error");
} else if n_err > 1 {
writeln!(f, "encountered {n_err} errors while parsing/formatting:")?;
}
for (i, e) in errs.iter().filter(|d| d.is_error()).enumerate() {
if i > 0 {
f.write_str("\n")?;
}
write!(f, "{e}")?;
}
Ok(())
}
}
}
}