use core::fmt;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Position {
pub input: u64,
pub output: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
Corrupt(&'static str),
Truncated,
InvalidParameters,
#[non_exhaustive]
AllocationFailed {
bytes: u64,
},
#[non_exhaustive]
MemoryLimit {
required: u64,
limit: u64,
},
Internal(&'static str),
}
impl ErrorKind {
#[inline]
#[must_use]
pub const fn allocation_failed(bytes: u64) -> Self {
Self::AllocationFailed { bytes }
}
#[inline]
#[must_use]
pub const fn memory_limit(required: u64, limit: u64) -> Self {
Self::MemoryLimit { required, limit }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Error {
pub kind: ErrorKind,
pub at: Position,
}
impl Error {
#[inline]
#[must_use]
pub const fn new(kind: ErrorKind) -> Self {
Self {
kind,
at: Position {
input: 0,
output: 0,
},
}
}
#[inline]
pub const fn kind(&self) -> ErrorKind {
self.kind
}
#[inline]
pub const fn position(&self) -> Position {
self.at
}
#[inline]
pub const fn is_data_error(&self) -> bool {
matches!(self.kind, ErrorKind::Corrupt(_) | ErrorKind::Truncated)
}
#[inline]
pub const fn is_resource_error(&self) -> bool {
matches!(
self.kind,
ErrorKind::MemoryLimit { .. } | ErrorKind::AllocationFailed { .. }
)
}
#[cold]
#[inline(never)]
pub(crate) const fn corrupt(detail: &'static str) -> Self {
Self::new(ErrorKind::Corrupt(detail))
}
#[cold]
#[inline(never)]
pub(crate) const fn truncated() -> Self {
Self::new(ErrorKind::Truncated)
}
#[cold]
#[inline(never)]
pub(crate) const fn invalid_parameters() -> Self {
Self::new(ErrorKind::InvalidParameters)
}
#[cold]
#[inline(never)]
pub(crate) const fn internal(detail: &'static str) -> Self {
Self::new(ErrorKind::Internal(detail))
}
#[inline]
pub(crate) const fn at(self, at: Position) -> Self {
Self {
kind: self.kind,
at,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
ErrorKind::Corrupt(detail) => write!(f, "corrupt PPMd stream: {detail}")?,
ErrorKind::Truncated => f.write_str("truncated PPMd stream")?,
ErrorKind::InvalidParameters => f.write_str("invalid PPMd parameters")?,
ErrorKind::AllocationFailed { bytes } => {
write!(f, "could not allocate {bytes} bytes for the PPMd model")?;
}
ErrorKind::MemoryLimit { required, limit } => write!(
f,
"the PPMd model needs {required} bytes, more than the {limit}-byte limit"
)?,
ErrorKind::Internal(detail) => write!(f, "internal PPMd codec failure: {detail}")?,
}
write!(
f,
" (at input byte {}, output byte {})",
self.at.input, self.at.output
)
}
}
impl core::error::Error for Error {}
#[cfg(feature = "std")]
impl From<Error> for std::io::Error {
fn from(e: Error) -> Self {
use std::io::ErrorKind as Io;
let kind = match e.kind {
ErrorKind::Corrupt(_) => Io::InvalidData,
ErrorKind::Truncated => Io::UnexpectedEof,
ErrorKind::InvalidParameters => Io::InvalidInput,
ErrorKind::AllocationFailed { .. } | ErrorKind::MemoryLimit { .. } => Io::OutOfMemory,
ErrorKind::Internal(_) => Io::Other,
};
std::io::Error::new(kind, e)
}
}
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Progress<S> {
pub consumed: usize,
pub produced: usize,
pub status: S,
}
#[cfg(test)]
mod tests {
extern crate alloc;
use super::*;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
fn every_kind() -> [ErrorKind; 7] {
[
ErrorKind::Corrupt("model pointer or frequency out of bounds"),
ErrorKind::Corrupt("7z range coder: first byte is not zero"),
ErrorKind::Truncated,
ErrorKind::InvalidParameters,
ErrorKind::allocation_failed(1 << 30),
ErrorKind::memory_limit(1 << 30, 1 << 20),
ErrorKind::Internal("escape stop in a 7z stream"),
]
}
fn chain(e: &(dyn core::error::Error + 'static)) -> Vec<String> {
let mut out = Vec::new();
let mut next = Some(e);
while let Some(e) = next {
out.push(e.to_string());
next = e.source();
}
out
}
#[test]
fn display_avoids_the_words_downstream_classifiers_match() {
for kind in every_kind() {
let text = Error::new(kind).to_string().to_ascii_lowercase();
for word in ["method", "unsupported", "password", "encrypted"] {
assert!(!text.contains(word), "{text}");
}
}
}
#[test]
fn display_is_lowercase_text_without_trailing_punctuation() {
for kind in every_kind() {
let text = Error::new(kind).to_string();
let first = text.chars().next().unwrap();
assert!(!first.is_uppercase(), "{text}");
assert!(!text.ends_with(['.', '!', '?']), "{text}");
assert_ne!(text, alloc::format!("{:?}", Error::new(kind)));
}
}
#[test]
fn an_error_is_its_own_whole_chain() {
let at = Position {
input: 7,
output: 9,
};
let cases = [
(
ErrorKind::Corrupt("bad symbol"),
"corrupt PPMd stream: bad symbol (at input byte 7, output byte 9)",
),
(
ErrorKind::Truncated,
"truncated PPMd stream (at input byte 7, output byte 9)",
),
(
ErrorKind::InvalidParameters,
"invalid PPMd parameters (at input byte 7, output byte 9)",
),
(
ErrorKind::allocation_failed(4096),
"could not allocate 4096 bytes for the PPMd model (at input byte 7, output byte 9)",
),
(
ErrorKind::memory_limit(4096, 1024),
"the PPMd model needs 4096 bytes, more than the 1024-byte limit (at input byte 7, output byte 9)",
),
(
ErrorKind::Internal("range coder fault"),
"internal PPMd codec failure: range coder fault (at input byte 7, output byte 9)",
),
];
for (kind, text) in cases {
let e = Error::new(kind).at(at);
assert_eq!(chain(&e), [text]);
}
}
#[test]
fn the_error_crosses_threads_and_lives_forever() {
fn assert_bounds<T: Send + Sync + 'static>() {}
assert_bounds::<Error>();
assert_bounds::<ErrorKind>();
}
#[test]
fn the_constructors_build_the_structured_variants() {
assert_eq!(
ErrorKind::allocation_failed(5),
ErrorKind::AllocationFailed { bytes: 5 }
);
assert_eq!(
ErrorKind::memory_limit(8, 2),
ErrorKind::MemoryLimit {
required: 8,
limit: 2
}
);
let e = Error::new(ErrorKind::Truncated);
assert_eq!(e.kind(), ErrorKind::Truncated);
assert_eq!(e.position(), Position::default());
}
#[test]
fn each_kind_falls_in_exactly_one_class() {
for kind in every_kind() {
let e = Error::new(kind);
let data = matches!(kind, ErrorKind::Corrupt(_) | ErrorKind::Truncated);
let resource = matches!(
kind,
ErrorKind::AllocationFailed { .. } | ErrorKind::MemoryLimit { .. }
);
assert_eq!(e.is_data_error(), data, "{kind:?}");
assert_eq!(e.is_resource_error(), resource, "{kind:?}");
}
}
#[cfg(feature = "std")]
#[test]
fn io_conversion_keeps_the_kind_and_the_error() {
use std::io::ErrorKind as Io;
for (kind, io) in [
(ErrorKind::Corrupt("x"), Io::InvalidData),
(ErrorKind::Truncated, Io::UnexpectedEof),
(ErrorKind::InvalidParameters, Io::InvalidInput),
(ErrorKind::allocation_failed(1), Io::OutOfMemory),
(ErrorKind::memory_limit(2, 1), Io::OutOfMemory),
(ErrorKind::Internal("x"), Io::Other),
] {
let e = Error::new(kind).at(Position {
input: 3,
output: 4,
});
let converted = std::io::Error::from(e);
assert_eq!(converted.kind(), io);
assert_eq!(converted.to_string(), e.to_string());
assert_eq!(chain(&converted), [e.to_string()]);
assert_eq!(converted.downcast::<Error>().unwrap(), e);
}
}
}