Skip to main content

compress_lib/
error.rs

1//! The error type every codec returns.
2
3use core::fmt;
4
5/// What went wrong, as a stable, matchable value.
6///
7/// Every kind says what to do in its [`message`](ErrorKind::message) and has a
8/// stable [`code`](ErrorKind::code) for logs and metrics. New kinds may be
9/// added in minor releases, so match with a wildcard arm.
10///
11/// # Examples
12///
13/// ```
14/// use compress_lib::ErrorKind;
15///
16/// assert_eq!(ErrorKind::OutputLimit.code(), "compress.output_limit");
17/// ```
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum ErrorKind {
21    /// A DEFLATE block header names block type 3, which does not exist.
22    InvalidBlockType,
23    /// A stored block's length and its one's complement disagree.
24    InvalidStoredLength,
25    /// A Huffman code description is invalid: over-subscribed, incomplete
26    /// where the format forbids it, a repeat with no previous length, or a
27    /// repeat that runs past the end of the table.
28    InvalidCodeLengths,
29    /// A code decodes to a symbol the format reserves (length symbols 286
30    /// and 287, distance symbols 30 and 31) or to no symbol at all.
31    InvalidSymbol,
32    /// A back-reference points before the start of the data (and any preset
33    /// dictionary) or further back than the window allows.
34    DistanceTooFar,
35    /// The input ended inside the stream: the data is truncated.
36    UnexpectedEnd,
37    /// The output reached the configured `max_output` before the stream
38    /// ended.
39    OutputLimit,
40    /// The output grew past the configured ratio to the input consumed.
41    RatioLimit,
42    /// Bytes follow the end of the stream where none are allowed.
43    TrailingData,
44    /// A window size outside 8 to 15 bits was asked for, or a zlib header
45    /// names a window larger than the decoder allows.
46    InvalidWindowBits,
47    /// A compression level outside 0 to 9 was asked for.
48    InvalidLevel,
49    /// A zlib or gzip header is malformed.
50    InvalidHeader,
51    /// A checksum (Adler-32, CRC-32, or the gzip header CRC) does not match
52    /// the data.
53    ChecksumMismatch,
54    /// The gzip trailer's length does not match the data.
55    LengthMismatch,
56    /// The gzip header's optional fields are larger than the configured cap.
57    HeaderTooLarge,
58    /// A gzip stream has more members than the configured cap.
59    TooManyMembers,
60    /// A zlib stream needs a preset dictionary that was not given.
61    DictionaryRequired,
62    /// The preset dictionary given is not the one the zlib stream names.
63    WrongDictionary,
64    /// The call is not valid in the engine's current state.
65    InvalidState,
66    /// The allocator refused memory for output.
67    AllocationFailed,
68}
69
70impl ErrorKind {
71    /// A stable identifier for logs and metrics, never changed once
72    /// released.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use compress_lib::ErrorKind;
78    ///
79    /// assert_eq!(ErrorKind::DistanceTooFar.code(), "compress.distance_too_far");
80    /// ```
81    #[must_use]
82    pub const fn code(self) -> &'static str {
83        match self {
84            Self::InvalidBlockType => "compress.invalid_block_type",
85            Self::InvalidStoredLength => "compress.invalid_stored_length",
86            Self::InvalidCodeLengths => "compress.invalid_code_lengths",
87            Self::InvalidSymbol => "compress.invalid_symbol",
88            Self::DistanceTooFar => "compress.distance_too_far",
89            Self::UnexpectedEnd => "compress.unexpected_end",
90            Self::OutputLimit => "compress.output_limit",
91            Self::RatioLimit => "compress.ratio_limit",
92            Self::TrailingData => "compress.trailing_data",
93            Self::InvalidWindowBits => "compress.invalid_window_bits",
94            Self::InvalidLevel => "compress.invalid_level",
95            Self::InvalidHeader => "compress.invalid_header",
96            Self::ChecksumMismatch => "compress.checksum_mismatch",
97            Self::LengthMismatch => "compress.length_mismatch",
98            Self::HeaderTooLarge => "compress.header_too_large",
99            Self::TooManyMembers => "compress.too_many_members",
100            Self::DictionaryRequired => "compress.dictionary_required",
101            Self::WrongDictionary => "compress.wrong_dictionary",
102            Self::InvalidState => "compress.invalid_state",
103            Self::AllocationFailed => "compress.allocation_failed",
104        }
105    }
106
107    /// What happened and what to do, in one sentence.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use compress_lib::ErrorKind;
113    ///
114    /// assert!(ErrorKind::OutputLimit.message().contains("max_output"));
115    /// ```
116    #[must_use]
117    pub const fn message(self) -> &'static str {
118        match self {
119            Self::InvalidBlockType => {
120                "invalid DEFLATE block type 3: the data is not DEFLATE or is corrupt"
121            }
122            Self::InvalidStoredLength => {
123                "stored block length check failed: the data is corrupt or truncated"
124            }
125            Self::InvalidCodeLengths => {
126                "invalid Huffman code lengths: the data is corrupt or not DEFLATE"
127            }
128            Self::InvalidSymbol => "invalid or reserved symbol: the data is corrupt",
129            Self::DistanceTooFar => {
130                "back-reference beyond the data or the window: the data is corrupt, \
131                 needs a preset dictionary, or uses a larger window than configured"
132            }
133            Self::UnexpectedEnd => "input ended inside the stream: the data is truncated",
134            Self::OutputLimit => {
135                "output reached max_output: raise the limit only if the source is trusted"
136            }
137            Self::RatioLimit => {
138                "output exceeded the ratio limit: likely a decompression bomb; \
139                 raise max_ratio only if the source is trusted"
140            }
141            Self::TrailingData => {
142                "bytes follow the end of the stream: strip them or allow trailing data"
143            }
144            Self::InvalidWindowBits => "window bits must be 8 to 15 (and within the decoder's)",
145            Self::InvalidLevel => "compression level must be 0 to 9",
146            Self::InvalidHeader => "invalid zlib or gzip header: the data is not in that format",
147            Self::ChecksumMismatch => "checksum mismatch: the data is corrupt",
148            Self::LengthMismatch => "gzip length mismatch: the data is corrupt or truncated",
149            Self::HeaderTooLarge => {
150                "gzip header fields exceed the configured cap: raise max_header if trusted"
151            }
152            Self::TooManyMembers => {
153                "gzip stream has more members than max_members: raise it if trusted"
154            }
155            Self::DictionaryRequired => {
156                "the zlib stream needs a preset dictionary: call set_dictionary first"
157            }
158            Self::WrongDictionary => "wrong preset dictionary: use the one the zlib stream names",
159            Self::InvalidState => "call not valid in the engine's state: reset it or finish first",
160            Self::AllocationFailed => "out of memory for output: lower max_output or free memory",
161        }
162    }
163}
164
165impl fmt::Display for ErrorKind {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        f.write_str(self.message())
168    }
169}
170
171/// A codec error: its [`ErrorKind`] and the input offset where it was found.
172///
173/// # Examples
174///
175/// ```
176/// use compress_lib::{Error, ErrorKind};
177///
178/// fn check(e: Error) -> bool {
179///     matches!(e.kind(), ErrorKind::OutputLimit | ErrorKind::RatioLimit)
180/// }
181/// # let _ = check;
182/// ```
183#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
184pub struct Error {
185    kind: ErrorKind,
186    offset: u64,
187}
188
189impl Error {
190    /// An error of `kind` found at input byte `offset`.
191    // The decoders (0.3) are the callers.
192    #[allow(dead_code)]
193    #[must_use]
194    pub(crate) const fn new(kind: ErrorKind, offset: u64) -> Self {
195        Self { kind, offset }
196    }
197
198    /// An error not tied to a place in the input (configuration, state).
199    #[must_use]
200    pub(crate) const fn config(kind: ErrorKind) -> Self {
201        Self { kind, offset: 0 }
202    }
203
204    /// What went wrong.
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// use compress_lib::{ErrorKind, Level};
210    ///
211    /// assert_eq!(Level::new(10).unwrap_err().kind(), ErrorKind::InvalidLevel);
212    /// ```
213    #[must_use]
214    pub const fn kind(&self) -> ErrorKind {
215        self.kind
216    }
217
218    /// The offset in the compressed input (bytes since the engine started or
219    /// was last reset) where the problem was found; 0 for configuration
220    /// errors.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use compress_lib::Level;
226    ///
227    /// assert_eq!(Level::new(10).unwrap_err().offset(), 0);
228    /// ```
229    #[must_use]
230    pub const fn offset(&self) -> u64 {
231        self.offset
232    }
233
234    /// The stable identifier of the kind, as [`ErrorKind::code`].
235    ///
236    /// # Examples
237    ///
238    /// ```
239    /// use compress_lib::Level;
240    ///
241    /// assert_eq!(Level::new(10).unwrap_err().code(), "compress.invalid_level");
242    /// ```
243    #[must_use]
244    pub const fn code(&self) -> &'static str {
245        self.kind.code()
246    }
247}
248
249impl fmt::Display for Error {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        if self.offset == 0 {
252            f.write_str(self.kind.message())
253        } else {
254            write!(f, "{} (at input byte {})", self.kind.message(), self.offset)
255        }
256    }
257}
258
259impl core::error::Error for Error {}
260
261impl From<Error> for ErrorKind {
262    fn from(e: Error) -> Self {
263        e.kind
264    }
265}
266
267#[cfg(feature = "std")]
268impl From<Error> for std::io::Error {
269    fn from(e: Error) -> Self {
270        let kind = match e.kind {
271            ErrorKind::UnexpectedEnd => std::io::ErrorKind::UnexpectedEof,
272            ErrorKind::InvalidWindowBits | ErrorKind::InvalidLevel | ErrorKind::InvalidState => {
273                std::io::ErrorKind::InvalidInput
274            }
275            ErrorKind::AllocationFailed => std::io::ErrorKind::OutOfMemory,
276            _ => std::io::ErrorKind::InvalidData,
277        };
278        std::io::Error::new(kind, e)
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use alloc::{collections::BTreeSet, string::ToString};
285
286    use super::*;
287
288    const ALL: [ErrorKind; 20] = [
289        ErrorKind::InvalidBlockType,
290        ErrorKind::InvalidStoredLength,
291        ErrorKind::InvalidCodeLengths,
292        ErrorKind::InvalidSymbol,
293        ErrorKind::DistanceTooFar,
294        ErrorKind::UnexpectedEnd,
295        ErrorKind::OutputLimit,
296        ErrorKind::RatioLimit,
297        ErrorKind::TrailingData,
298        ErrorKind::InvalidWindowBits,
299        ErrorKind::InvalidLevel,
300        ErrorKind::InvalidHeader,
301        ErrorKind::ChecksumMismatch,
302        ErrorKind::LengthMismatch,
303        ErrorKind::HeaderTooLarge,
304        ErrorKind::TooManyMembers,
305        ErrorKind::DictionaryRequired,
306        ErrorKind::WrongDictionary,
307        ErrorKind::InvalidState,
308        ErrorKind::AllocationFailed,
309    ];
310
311    #[test]
312    fn test_error_codes_unique_and_prefixed() {
313        let codes: BTreeSet<_> = ALL.iter().map(|k| k.code()).collect();
314        assert_eq!(codes.len(), ALL.len());
315        assert!(codes.iter().all(|c| c.starts_with("compress.")));
316    }
317
318    #[test]
319    fn test_error_messages_say_what_to_do() {
320        for k in ALL {
321            let m = k.message();
322            assert!(m.contains(':') || m.contains("must"), "{k:?}: {m}");
323            assert_eq!(k.to_string(), m);
324        }
325    }
326
327    #[test]
328    fn test_error_display_includes_offset_when_known() {
329        let e = Error::new(ErrorKind::InvalidBlockType, 17);
330        assert!(e.to_string().ends_with("(at input byte 17)"));
331        assert_eq!(e.offset(), 17);
332        assert_eq!(e.kind(), ErrorKind::InvalidBlockType);
333        assert_eq!(ErrorKind::from(e), ErrorKind::InvalidBlockType);
334        let c = Error::config(ErrorKind::InvalidLevel);
335        assert_eq!(c.to_string(), ErrorKind::InvalidLevel.message());
336        assert_eq!(c.code(), "compress.invalid_level");
337    }
338
339    #[cfg(feature = "std")]
340    #[test]
341    fn test_error_into_io_error_maps_kind() {
342        let io: std::io::Error = Error::new(ErrorKind::UnexpectedEnd, 3).into();
343        assert_eq!(io.kind(), std::io::ErrorKind::UnexpectedEof);
344        let io: std::io::Error = Error::config(ErrorKind::InvalidLevel).into();
345        assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput);
346        let io: std::io::Error = Error::new(ErrorKind::ChecksumMismatch, 9).into();
347        assert_eq!(io.kind(), std::io::ErrorKind::InvalidData);
348    }
349}