use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[repr(u8)]
pub enum ErrorKind {
UnknownProperty,
InvalidLinkType,
InvalidHeader,
InvalidMetadata,
InputTooLarge,
InvalidUtf8,
}
impl ErrorKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::UnknownProperty => "unknown package property",
Self::InvalidLinkType => "invalid linkType (expected `hard` or `soft`)",
Self::InvalidHeader => "missing or invalid yarn lockfile header",
Self::InvalidMetadata => "missing or invalid __metadata block",
Self::InputTooLarge => "input too large (u32::MAX bytes or more)",
Self::InvalidUtf8 => "invalid UTF-8",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Error {
offset: u32,
kind: ErrorKind,
}
impl Error {
#[cold]
#[inline(never)]
#[must_use]
pub fn new(kind: ErrorKind, offset: usize) -> Self {
Self {
offset: u32::try_from(offset).unwrap_or(u32::MAX),
kind,
}
}
#[must_use]
pub const fn kind(&self) -> ErrorKind {
self.kind
}
#[must_use]
pub const fn offset(&self) -> usize {
self.offset as usize
}
#[must_use]
pub fn line_col(&self, src: &str) -> (usize, usize) {
let mut off = self.offset().min(src.len());
while !src.is_char_boundary(off) {
off -= 1;
}
let before = &src[..off];
let line = before.bytes().filter(|&b| b == b'\n').count() + 1;
let line_start = before.rfind('\n').map_or(0, |i| i + 1);
let col = before[line_start..].chars().count() + 1;
(line, col)
}
#[must_use]
pub fn describe(&self, src: &str) -> String {
let (line, col) = self.line_col(src);
format!("{} at line {line}, column {col}", self.detail(src))
}
#[cold]
#[must_use]
pub fn detail(&self, src: &str) -> String {
let mut off = self.offset().min(src.len());
while !src.is_char_boundary(off) {
off -= 1;
}
let rest = &src[off..];
let token_end = rest.find([':', '\n', '\r']).unwrap_or(rest.len());
let token = &rest[..token_end];
match self.kind {
ErrorKind::InvalidLinkType => {
let value = rest[token_end..].strip_prefix(':').map_or("", |v| {
v.split(['\n', '\r']).next().unwrap_or("").trim_start()
});
format!("{} (found {value:?})", self.kind.as_str())
}
ErrorKind::UnknownProperty => format!("{} `{token}`", self.kind.as_str()),
_ => self.kind.as_str().to_owned(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} at byte offset {}", self.kind.as_str(), self.offset)
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_is_small_and_copy() {
assert_eq!(std::mem::size_of::<Error>(), 8);
let e = Error::new(ErrorKind::UnknownProperty, 3);
let f = e;
assert_eq!(e, f);
}
#[test]
fn offset_saturates() {
let e = Error::new(ErrorKind::InputTooLarge, usize::MAX);
assert_eq!(e.offset(), u32::MAX as usize);
}
#[test]
fn line_col_and_describe() {
let src = "a\nbb\n someKey: v\n";
let off = src.find("someKey").unwrap();
let e = Error::new(ErrorKind::UnknownProperty, off);
assert_eq!(e.line_col(src), (3, 3));
assert_eq!(
e.describe(src),
"unknown package property `someKey` at line 3, column 3"
);
assert_eq!(
e.to_string(),
format!("unknown package property at byte offset {off}")
);
let src = "x\n linkType: HARD\n";
let e = Error::new(ErrorKind::InvalidLinkType, src.find("linkType").unwrap());
assert_eq!(
e.describe(src),
"invalid linkType (expected `hard` or `soft`) (found \"HARD\") at line 2, column 3"
);
assert_eq!(
e.detail(src),
"invalid linkType (expected `hard` or `soft`) (found \"HARD\")"
);
assert_eq!(
Error::new(ErrorKind::InvalidHeader, 0).detail(src),
ErrorKind::InvalidHeader.as_str()
);
}
#[test]
fn line_col_handles_unicode_and_out_of_range() {
let src = "\u{e9}\u{e9}x";
assert_eq!(
Error::new(ErrorKind::InvalidHeader, 1).line_col(src),
(1, 1)
);
assert_eq!(
Error::new(ErrorKind::InvalidHeader, 4).line_col(src),
(1, 3)
);
assert_eq!(
Error::new(ErrorKind::InvalidHeader, 99).line_col(src),
(1, 4)
);
assert_eq!(Error::new(ErrorKind::InvalidHeader, 0).line_col(""), (1, 1));
assert!(
Error::new(ErrorKind::InvalidHeader, 99)
.describe("")
.contains("line 1")
);
}
}