ppmd-turbo 0.2.0

PPMd variant H (Ppmd7) compression and decompression, bit-exact with 7-Zip and RAR, performance-first
Documentation
//! Errors, stream positions and the progress every step call reports.
//!
//! The crate performs no I/O, so nothing here wraps an I/O error: a call
//! fails because the stream is corrupt or truncated, because the parameters
//! or the order of calls are wrong, because the model's memory could not be
//! had, or because one of the codec's own invariants failed. Each is its own
//! [`ErrorKind`], and an [`Error`] has no `source()`: it is the whole chain.
//!
//! **Progress invariant.** Every step call returns a [`Progress`] that says
//! how many input bytes it took and how many output bytes it wrote. A call
//! that took nothing and wrote nothing always says why in its status: the
//! input left is too short to decode a symbol from and is not the last
//! (`NeedInput`), the output slice is empty (`OutputFull`), or the stream
//! has stopped (a size reached, an end marker, a RAR escape or model end).
//! A caller loop that feeds more input on `NeedInput`, drains output on
//! `OutputFull` and stops on the rest therefore always makes progress; it
//! never spins and never needs a timer to notice a stall. The fuzz target
//! `chunking_invariance` asserts this on every call.

use core::fmt;

/// Where in a stream an error was found.
///
/// Both counts run from the start of the stream: the codec's construction or
/// last `reset` for 7z and the raw carry-less streams, the last
/// `start_block` for RAR. The container adds its own context (block index,
/// packed offset).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Position {
    /// Input bytes consumed before the error.
    pub input: u64,
    /// Output bytes produced before the error.
    pub output: u64,
}

/// What went wrong.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
    /// The stream is not a valid PPMd stream for its parameters. The text
    /// says what the decoder found, for diagnostics only.
    Corrupt(&'static str),
    /// The input ended before the stream did: the last input was given and a
    /// symbol needed a byte past it (7z), or more padding than the caller
    /// allows (RAR).
    Truncated,
    /// The model order or memory size is outside what variant H accepts, or
    /// a call came in an order the codec does not allow.
    InvalidParameters,
    /// The model's memory could not be allocated.
    #[non_exhaustive]
    AllocationFailed {
        /// Bytes asked of the allocator.
        bytes: u64,
    },
    /// The model would need more memory than the caller allowed.
    #[non_exhaustive]
    MemoryLimit {
        /// Bytes the stream's parameters need.
        required: u64,
        /// The caller's limit.
        limit: u64,
    },
    /// One of the codec's own invariants failed: an encoder's range coder
    /// faulted, or a step stopped in a way its framing cannot. Input alone
    /// never causes it. The text says which, for diagnostics only.
    Internal(&'static str),
}

impl ErrorKind {
    /// [`ErrorKind::AllocationFailed`] for `bytes` asked of the allocator.
    #[inline]
    #[must_use]
    pub const fn allocation_failed(bytes: u64) -> Self {
        Self::AllocationFailed { bytes }
    }

    /// [`ErrorKind::MemoryLimit`]: `required` bytes needed, `limit` allowed.
    #[inline]
    #[must_use]
    pub const fn memory_limit(required: u64, limit: u64) -> Self {
        Self::MemoryLimit { required, limit }
    }
}

/// An error from a decoder or encoder, with the stream position it was found
/// at.
///
/// Errors are `Copy`: after one, the codec repeats it on every later call
/// until it is reset, so a caller that retries sees exactly the same error.
/// Build one with [`Error::new`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Error {
    /// What went wrong.
    pub kind: ErrorKind,
    /// Where it was found.
    pub at: Position,
}

impl Error {
    /// An error of `kind` at the start of the stream.
    #[inline]
    #[must_use]
    pub const fn new(kind: ErrorKind) -> Self {
        Self {
            kind,
            at: Position {
                input: 0,
                output: 0,
            },
        }
    }

    /// What went wrong.
    #[inline]
    pub const fn kind(&self) -> ErrorKind {
        self.kind
    }

    /// Where it was found.
    #[inline]
    pub const fn position(&self) -> Position {
        self.at
    }

    /// Whether the bytes of the stream are at fault ([`ErrorKind::Corrupt`]
    /// or [`ErrorKind::Truncated`]), as opposed to the parameters or the
    /// memory available. A container that can repair or refetch data treats
    /// these as repairable.
    #[inline]
    pub const fn is_data_error(&self) -> bool {
        matches!(self.kind, ErrorKind::Corrupt(_) | ErrorKind::Truncated)
    }

    /// Whether memory refused the model ([`ErrorKind::MemoryLimit`] or
    /// [`ErrorKind::AllocationFailed`]), as opposed to the data, the
    /// parameters or the codec. A container reports these as a resource
    /// limit rather than as damage.
    #[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))
    }

    /// The same error at `at`.
    #[inline]
    pub(crate) const fn at(self, at: Position) -> Self {
        Self {
            kind: self.kind,
            at,
        }
    }
}

/// The text deliberately avoids the words "method", "unsupported",
/// "password" and "encrypted": archive tooling downstream classifies errors
/// by matching them, and a PPMd error is none of those.
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 {}

/// Errors as [`std::io::Error`]s, for code that drives a codec through
/// `Read` or `Write`: a corrupt stream is `InvalidData`, a truncated one
/// `UnexpectedEof`, bad parameters `InvalidInput`, a memory refusal
/// `OutOfMemory`, and an internal failure `Other`. The [`Error`] stays inside
/// and can be recovered with `downcast::<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)
    }
}

/// The result type used throughout the crate.
pub type Result<T> = core::result::Result<T, Error>;

/// What one step call did.
///
/// `consumed` and `produced` are exact: the codec never keeps caller bytes
/// between calls, so the bytes after `consumed` belong to the caller and
/// must be presented again (followed by more) on the next call. See the
/// module documentation for the progress invariant.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Progress<S> {
    /// Input bytes taken.
    pub consumed: usize,
    /// Output bytes written.
    pub produced: usize,
    /// Why the call returned.
    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);
        }
    }
}