Skip to main content

ppmd_turbo/
error.rs

1//! Errors, stream positions and the progress every step call reports.
2//!
3//! The crate performs no I/O, so nothing here wraps an I/O error: a call
4//! fails because the stream is corrupt or truncated, because the parameters
5//! or the order of calls are wrong, because the model's memory could not be
6//! had, or because one of the codec's own invariants failed. Each is its own
7//! [`ErrorKind`], and an [`Error`] has no `source()`: it is the whole chain.
8//!
9//! **Progress invariant.** Every step call returns a [`Progress`] that says
10//! how many input bytes it took and how many output bytes it wrote. A call
11//! that took nothing and wrote nothing always says why in its status: the
12//! input left is too short to decode a symbol from and is not the last
13//! (`NeedInput`), the output slice is empty (`OutputFull`), or the stream
14//! has stopped (a size reached, an end marker, a RAR escape or model end).
15//! A caller loop that feeds more input on `NeedInput`, drains output on
16//! `OutputFull` and stops on the rest therefore always makes progress; it
17//! never spins and never needs a timer to notice a stall. The fuzz target
18//! `chunking_invariance` asserts this on every call.
19
20use core::fmt;
21
22/// Where in a stream an error was found.
23///
24/// Both counts run from the start of the stream: the codec's construction or
25/// last `reset` for 7z and the raw carry-less streams, the last
26/// `start_block` for RAR. The container adds its own context (block index,
27/// packed offset).
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
29pub struct Position {
30    /// Input bytes consumed before the error.
31    pub input: u64,
32    /// Output bytes produced before the error.
33    pub output: u64,
34}
35
36/// What went wrong.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum ErrorKind {
40    /// The stream is not a valid PPMd stream for its parameters. The text
41    /// says what the decoder found, for diagnostics only.
42    Corrupt(&'static str),
43    /// The input ended before the stream did: the last input was given and a
44    /// symbol needed a byte past it (7z), or more padding than the caller
45    /// allows (RAR).
46    Truncated,
47    /// The model order or memory size is outside what variant H accepts, or
48    /// a call came in an order the codec does not allow.
49    InvalidParameters,
50    /// The model's memory could not be allocated.
51    #[non_exhaustive]
52    AllocationFailed {
53        /// Bytes asked of the allocator.
54        bytes: u64,
55    },
56    /// The model would need more memory than the caller allowed.
57    #[non_exhaustive]
58    MemoryLimit {
59        /// Bytes the stream's parameters need.
60        required: u64,
61        /// The caller's limit.
62        limit: u64,
63    },
64    /// One of the codec's own invariants failed: an encoder's range coder
65    /// faulted, or a step stopped in a way its framing cannot. Input alone
66    /// never causes it. The text says which, for diagnostics only.
67    Internal(&'static str),
68}
69
70impl ErrorKind {
71    /// [`ErrorKind::AllocationFailed`] for `bytes` asked of the allocator.
72    #[inline]
73    #[must_use]
74    pub const fn allocation_failed(bytes: u64) -> Self {
75        Self::AllocationFailed { bytes }
76    }
77
78    /// [`ErrorKind::MemoryLimit`]: `required` bytes needed, `limit` allowed.
79    #[inline]
80    #[must_use]
81    pub const fn memory_limit(required: u64, limit: u64) -> Self {
82        Self::MemoryLimit { required, limit }
83    }
84}
85
86/// An error from a decoder or encoder, with the stream position it was found
87/// at.
88///
89/// Errors are `Copy`: after one, the codec repeats it on every later call
90/// until it is reset, so a caller that retries sees exactly the same error.
91/// Build one with [`Error::new`].
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93#[non_exhaustive]
94pub struct Error {
95    /// What went wrong.
96    pub kind: ErrorKind,
97    /// Where it was found.
98    pub at: Position,
99}
100
101impl Error {
102    /// An error of `kind` at the start of the stream.
103    #[inline]
104    #[must_use]
105    pub const fn new(kind: ErrorKind) -> Self {
106        Self {
107            kind,
108            at: Position {
109                input: 0,
110                output: 0,
111            },
112        }
113    }
114
115    /// What went wrong.
116    #[inline]
117    pub const fn kind(&self) -> ErrorKind {
118        self.kind
119    }
120
121    /// Where it was found.
122    #[inline]
123    pub const fn position(&self) -> Position {
124        self.at
125    }
126
127    /// Whether the bytes of the stream are at fault ([`ErrorKind::Corrupt`]
128    /// or [`ErrorKind::Truncated`]), as opposed to the parameters or the
129    /// memory available. A container that can repair or refetch data treats
130    /// these as repairable.
131    #[inline]
132    pub const fn is_data_error(&self) -> bool {
133        matches!(self.kind, ErrorKind::Corrupt(_) | ErrorKind::Truncated)
134    }
135
136    /// Whether memory refused the model ([`ErrorKind::MemoryLimit`] or
137    /// [`ErrorKind::AllocationFailed`]), as opposed to the data, the
138    /// parameters or the codec. A container reports these as a resource
139    /// limit rather than as damage.
140    #[inline]
141    pub const fn is_resource_error(&self) -> bool {
142        matches!(
143            self.kind,
144            ErrorKind::MemoryLimit { .. } | ErrorKind::AllocationFailed { .. }
145        )
146    }
147
148    #[cold]
149    #[inline(never)]
150    pub(crate) const fn corrupt(detail: &'static str) -> Self {
151        Self::new(ErrorKind::Corrupt(detail))
152    }
153
154    #[cold]
155    #[inline(never)]
156    pub(crate) const fn truncated() -> Self {
157        Self::new(ErrorKind::Truncated)
158    }
159
160    #[cold]
161    #[inline(never)]
162    pub(crate) const fn invalid_parameters() -> Self {
163        Self::new(ErrorKind::InvalidParameters)
164    }
165
166    #[cold]
167    #[inline(never)]
168    pub(crate) const fn internal(detail: &'static str) -> Self {
169        Self::new(ErrorKind::Internal(detail))
170    }
171
172    /// The same error at `at`.
173    #[inline]
174    pub(crate) const fn at(self, at: Position) -> Self {
175        Self {
176            kind: self.kind,
177            at,
178        }
179    }
180}
181
182/// The text deliberately avoids the words "method", "unsupported",
183/// "password" and "encrypted": archive tooling downstream classifies errors
184/// by matching them, and a PPMd error is none of those.
185impl fmt::Display for Error {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        match self.kind {
188            ErrorKind::Corrupt(detail) => write!(f, "corrupt PPMd stream: {detail}")?,
189            ErrorKind::Truncated => f.write_str("truncated PPMd stream")?,
190            ErrorKind::InvalidParameters => f.write_str("invalid PPMd parameters")?,
191            ErrorKind::AllocationFailed { bytes } => {
192                write!(f, "could not allocate {bytes} bytes for the PPMd model")?;
193            }
194            ErrorKind::MemoryLimit { required, limit } => write!(
195                f,
196                "the PPMd model needs {required} bytes, more than the {limit}-byte limit"
197            )?,
198            ErrorKind::Internal(detail) => write!(f, "internal PPMd codec failure: {detail}")?,
199        }
200        write!(
201            f,
202            " (at input byte {}, output byte {})",
203            self.at.input, self.at.output
204        )
205    }
206}
207
208impl core::error::Error for Error {}
209
210/// Errors as [`std::io::Error`]s, for code that drives a codec through
211/// `Read` or `Write`: a corrupt stream is `InvalidData`, a truncated one
212/// `UnexpectedEof`, bad parameters `InvalidInput`, a memory refusal
213/// `OutOfMemory`, and an internal failure `Other`. The [`Error`] stays inside
214/// and can be recovered with `downcast::<Error>()`.
215#[cfg(feature = "std")]
216impl From<Error> for std::io::Error {
217    fn from(e: Error) -> Self {
218        use std::io::ErrorKind as Io;
219        let kind = match e.kind {
220            ErrorKind::Corrupt(_) => Io::InvalidData,
221            ErrorKind::Truncated => Io::UnexpectedEof,
222            ErrorKind::InvalidParameters => Io::InvalidInput,
223            ErrorKind::AllocationFailed { .. } | ErrorKind::MemoryLimit { .. } => Io::OutOfMemory,
224            ErrorKind::Internal(_) => Io::Other,
225        };
226        std::io::Error::new(kind, e)
227    }
228}
229
230/// The result type used throughout the crate.
231pub type Result<T> = core::result::Result<T, Error>;
232
233/// What one step call did.
234///
235/// `consumed` and `produced` are exact: the codec never keeps caller bytes
236/// between calls, so the bytes after `consumed` belong to the caller and
237/// must be presented again (followed by more) on the next call. See the
238/// module documentation for the progress invariant.
239#[derive(Clone, Copy, Debug, PartialEq, Eq)]
240pub struct Progress<S> {
241    /// Input bytes taken.
242    pub consumed: usize,
243    /// Output bytes written.
244    pub produced: usize,
245    /// Why the call returned.
246    pub status: S,
247}
248
249#[cfg(test)]
250mod tests {
251    extern crate alloc;
252    use super::*;
253    use alloc::string::{String, ToString};
254    use alloc::vec::Vec;
255
256    fn every_kind() -> [ErrorKind; 7] {
257        [
258            ErrorKind::Corrupt("model pointer or frequency out of bounds"),
259            ErrorKind::Corrupt("7z range coder: first byte is not zero"),
260            ErrorKind::Truncated,
261            ErrorKind::InvalidParameters,
262            ErrorKind::allocation_failed(1 << 30),
263            ErrorKind::memory_limit(1 << 30, 1 << 20),
264            ErrorKind::Internal("escape stop in a 7z stream"),
265        ]
266    }
267
268    fn chain(e: &(dyn core::error::Error + 'static)) -> Vec<String> {
269        let mut out = Vec::new();
270        let mut next = Some(e);
271        while let Some(e) = next {
272            out.push(e.to_string());
273            next = e.source();
274        }
275        out
276    }
277
278    #[test]
279    fn display_avoids_the_words_downstream_classifiers_match() {
280        for kind in every_kind() {
281            let text = Error::new(kind).to_string().to_ascii_lowercase();
282            for word in ["method", "unsupported", "password", "encrypted"] {
283                assert!(!text.contains(word), "{text}");
284            }
285        }
286    }
287
288    #[test]
289    fn display_is_lowercase_text_without_trailing_punctuation() {
290        for kind in every_kind() {
291            let text = Error::new(kind).to_string();
292            let first = text.chars().next().unwrap();
293            assert!(!first.is_uppercase(), "{text}");
294            assert!(!text.ends_with(['.', '!', '?']), "{text}");
295            assert_ne!(text, alloc::format!("{:?}", Error::new(kind)));
296        }
297    }
298
299    #[test]
300    fn an_error_is_its_own_whole_chain() {
301        let at = Position {
302            input: 7,
303            output: 9,
304        };
305        let cases = [
306            (
307                ErrorKind::Corrupt("bad symbol"),
308                "corrupt PPMd stream: bad symbol (at input byte 7, output byte 9)",
309            ),
310            (
311                ErrorKind::Truncated,
312                "truncated PPMd stream (at input byte 7, output byte 9)",
313            ),
314            (
315                ErrorKind::InvalidParameters,
316                "invalid PPMd parameters (at input byte 7, output byte 9)",
317            ),
318            (
319                ErrorKind::allocation_failed(4096),
320                "could not allocate 4096 bytes for the PPMd model (at input byte 7, output byte 9)",
321            ),
322            (
323                ErrorKind::memory_limit(4096, 1024),
324                "the PPMd model needs 4096 bytes, more than the 1024-byte limit (at input byte 7, output byte 9)",
325            ),
326            (
327                ErrorKind::Internal("range coder fault"),
328                "internal PPMd codec failure: range coder fault (at input byte 7, output byte 9)",
329            ),
330        ];
331        for (kind, text) in cases {
332            let e = Error::new(kind).at(at);
333            assert_eq!(chain(&e), [text]);
334        }
335    }
336
337    #[test]
338    fn the_error_crosses_threads_and_lives_forever() {
339        fn assert_bounds<T: Send + Sync + 'static>() {}
340        assert_bounds::<Error>();
341        assert_bounds::<ErrorKind>();
342    }
343
344    #[test]
345    fn the_constructors_build_the_structured_variants() {
346        assert_eq!(
347            ErrorKind::allocation_failed(5),
348            ErrorKind::AllocationFailed { bytes: 5 }
349        );
350        assert_eq!(
351            ErrorKind::memory_limit(8, 2),
352            ErrorKind::MemoryLimit {
353                required: 8,
354                limit: 2
355            }
356        );
357        let e = Error::new(ErrorKind::Truncated);
358        assert_eq!(e.kind(), ErrorKind::Truncated);
359        assert_eq!(e.position(), Position::default());
360    }
361
362    #[test]
363    fn each_kind_falls_in_exactly_one_class() {
364        for kind in every_kind() {
365            let e = Error::new(kind);
366            let data = matches!(kind, ErrorKind::Corrupt(_) | ErrorKind::Truncated);
367            let resource = matches!(
368                kind,
369                ErrorKind::AllocationFailed { .. } | ErrorKind::MemoryLimit { .. }
370            );
371            assert_eq!(e.is_data_error(), data, "{kind:?}");
372            assert_eq!(e.is_resource_error(), resource, "{kind:?}");
373        }
374    }
375
376    #[cfg(feature = "std")]
377    #[test]
378    fn io_conversion_keeps_the_kind_and_the_error() {
379        use std::io::ErrorKind as Io;
380        for (kind, io) in [
381            (ErrorKind::Corrupt("x"), Io::InvalidData),
382            (ErrorKind::Truncated, Io::UnexpectedEof),
383            (ErrorKind::InvalidParameters, Io::InvalidInput),
384            (ErrorKind::allocation_failed(1), Io::OutOfMemory),
385            (ErrorKind::memory_limit(2, 1), Io::OutOfMemory),
386            (ErrorKind::Internal("x"), Io::Other),
387        ] {
388            let e = Error::new(kind).at(Position {
389                input: 3,
390                output: 4,
391            });
392            let converted = std::io::Error::from(e);
393            assert_eq!(converted.kind(), io);
394            assert_eq!(converted.to_string(), e.to_string());
395            assert_eq!(chain(&converted), [e.to_string()]);
396            assert_eq!(converted.downcast::<Error>().unwrap(), e);
397        }
398    }
399}