Skip to main content

mbrotli/compressor/
error.rs

1//! What can go wrong while a stream is being encoded.
2//!
3//! Configuration mistakes are reported by
4//! [`ConfigError`](super::ConfigError) when the compressor is built, and
5//! dictionary mistakes by
6//! [`DictionaryError`](super::dictionary::DictionaryError) when the dictionary
7//! is built. What is left — the failures that need an operation in flight to
8//! happen at all — is [`EncodeError`].
9
10use super::config::{Quality, SizeOverflow};
11use super::internal::BrotliCompressError;
12use thiserror::Error;
13
14/// Error returned by an encoding operation.
15///
16/// Every variant describes something about the *operation*: the destination was
17/// too small, the dictionary cannot be used at this quality, an allocation was
18/// refused. A configuration that could never work is rejected earlier, by
19/// [`Compressor::new`](crate::Compressor::new), and never reaches here.
20///
21/// # Examples
22///
23/// ```
24/// use mbrotli::{Compressor, EncodeError, EncoderConfig, Quality};
25///
26/// let mut encoder = Compressor::new(EncoderConfig::default().with_quality(Quality::Q1))?;
27/// let mut cramped = [0u8; 1];
28///
29/// assert!(matches!(
30///     encoder.compress_to_slice(b"a payload that will not fit in one byte", &mut cramped),
31///     Err(EncodeError::OutputTooSmall { provided: 1 })
32/// ));
33/// # Ok::<(), Box<dyn std::error::Error>>(())
34/// ```
35#[derive(Error, Debug)]
36#[non_exhaustive]
37pub enum EncodeError {
38    /// Logical placement plus input exceeds the RFC's 63-bit position range.
39    #[error("stream position {position} plus {input_bytes} input bytes exceeds 63 bits")]
40    StreamPositionOverflow {
41        /// Position before accepting this input.
42        position: u64,
43        /// Number of input bytes offered.
44        input_bytes: u64,
45    },
46    /// The caller's destination cannot hold the whole stream.
47    ///
48    /// Size one with [`Compressor::max_compressed_size`] to make this
49    /// impossible.
50    ///
51    /// [`Compressor::max_compressed_size`]: crate::Compressor::max_compressed_size
52    #[error("a destination of {provided} bytes cannot hold the compressed stream")]
53    OutputTooSmall {
54        /// How many bytes the caller offered.
55        provided: usize,
56    },
57    /// An allocation the operation needed was refused.
58    #[error("an allocation of {requested} bytes failed")]
59    AllocationFailed {
60        /// How many bytes were asked for.
61        requested: usize,
62    },
63    /// The compressed-size bound does not fit in a `usize`.
64    #[error(transparent)]
65    Bound(#[from] SizeOverflow),
66    /// This quality cannot compress against an attached dictionary.
67    ///
68    /// The reference compiles its compound-dictionary search only for the match
69    /// finders qualities five and above select, and silently ignores the
70    /// dictionary elsewhere. This crate refuses instead: a stream compressed
71    /// without the dictionary it was given decodes perfectly well, so the
72    /// mistake would stay invisible until a decoder that does attach it
73    /// produced the wrong bytes.
74    #[error("quality {} cannot compress against a prepared dictionary", quality.get())]
75    DictionaryUnsupportedForQuality {
76        /// The quality that was asked for.
77        quality: Quality,
78    },
79    /// A non-zero stream offset was asked for on an unsupported path.
80    ///
81    /// Continuations require `experimental` and quality two or above.
82    #[error(
83        "stream offset {offset} requires experimental continuation support at quality 2 or above"
84    )]
85    UnsupportedStreamOffset {
86        /// The offset that was asked for.
87        offset: u64,
88    },
89    /// A session was abandoned without being dropped, and its state is unknown.
90    ///
91    /// `::core::mem::forget` can skip a session's destructor, which is the one way
92    /// a compressor can be left holding state no operation has cleaned up.
93    /// Rather than trusting it, the compressor refuses until
94    /// [`Compressor::recover`](crate::Compressor::recover) has put it back into
95    /// a known state.
96    #[error("a previous session was abandoned; call Compressor::recover before encoding again")]
97    AbandonedSession,
98    /// An operation was asked for that the session's state does not allow.
99    #[error("the session cannot {attempted} in its current state")]
100    InvalidState {
101        /// What was attempted.
102        attempted: &'static str,
103    },
104    /// An invariant inside the encoder was violated.
105    ///
106    /// No valid caller input can cause this; it is a defect in this crate.
107    #[error("an internal encoder invariant was violated: {detail}")]
108    InternalInvariant {
109        /// What the encoder reported.
110        detail: &'static str,
111    },
112}
113
114impl EncodeError {
115    /// Lifts a low-level encoder error into the public one.
116    ///
117    /// `provided` is the size of the destination the operation was given, which
118    /// only the caller of the operation knows; the encoders report a short
119    /// buffer without it.
120    pub(crate) fn from_core(error: BrotliCompressError, provided: usize) -> Self {
121        match error {
122            BrotliCompressError::OutputTooSmall => Self::OutputTooSmall { provided },
123            BrotliCompressError::BoundOverflow => Self::Bound(SizeOverflow),
124            // A large window at a quality that cannot carry one is refused when
125            // the compressor is built, a dictionary at a quality that cannot
126            // read one is refused before the encoder is asked, and every
127            // quality has an encoder. None of the three can be reached from a
128            // validated configuration.
129            BrotliCompressError::Shared(_) | BrotliCompressError::UnsupportedQuality(_) => {
130                Self::InternalInvariant {
131                    detail: "a validated configuration reached an encoder that refused it",
132                }
133            }
134            BrotliCompressError::BufferOverflow => Self::InternalInvariant {
135                detail: "the encoder's scratch buffer was too small",
136            },
137            // The encoders perform no I/O; the variant exists for the low-level
138            // error type alone.
139            #[cfg(not(feature = "no_std"))]
140            BrotliCompressError::IOError(_) => Self::InternalInvariant {
141                detail: "an encoder reported an I/O failure it cannot perform",
142            },
143        }
144    }
145}
146
147#[cfg(not(feature = "no_std"))]
148impl From<EncodeError> for std::io::Error {
149    /// Carries an encoding failure through a [`std::io`] adapter.
150    ///
151    /// A short destination becomes [`std::io::ErrorKind::WriteZero`] and an
152    /// allocation failure [`std::io::ErrorKind::OutOfMemory`], so a caller can
153    /// tell the two apart without downcasting; everything else keeps the
154    /// original error as its source.
155    fn from(value: EncodeError) -> Self {
156        let kind = match value {
157            EncodeError::OutputTooSmall { .. } => std::io::ErrorKind::WriteZero,
158            EncodeError::AllocationFailed { .. } => std::io::ErrorKind::OutOfMemory,
159            EncodeError::UnsupportedStreamOffset { .. }
160            | EncodeError::StreamPositionOverflow { .. }
161            | EncodeError::DictionaryUnsupportedForQuality { .. } => {
162                std::io::ErrorKind::InvalidInput
163            }
164            EncodeError::AbandonedSession | EncodeError::InvalidState { .. } => {
165                std::io::ErrorKind::InvalidData
166            }
167            EncodeError::Bound(_) | EncodeError::InternalInvariant { .. } => {
168                std::io::ErrorKind::Other
169            }
170        };
171        Self::new(kind, value)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::compressor::shared::SharedBrotliError;
179    use ::core::error::Error as _;
180    use alloc::string::ToString;
181
182    #[test]
183    fn a_short_destination_reports_what_it_was_given() {
184        let error = EncodeError::from_core(BrotliCompressError::OutputTooSmall, 7);
185        assert!(matches!(error, EncodeError::OutputTooSmall { provided: 7 }));
186        assert!(error.to_string().contains('7'));
187    }
188
189    #[test]
190    fn a_refused_dictionary_names_the_quality() {
191        // The refusal is raised by the compressor, which knows the quality as a
192        // `Quality`, rather than lifted from a low-level error.
193        let error = EncodeError::DictionaryUnsupportedForQuality {
194            quality: Quality::Q3,
195        };
196        assert!(error.to_string().contains('3'));
197    }
198
199    #[test]
200    fn unreachable_low_level_failures_become_internal_invariants() {
201        for error in [
202            BrotliCompressError::BufferOverflow,
203            BrotliCompressError::UnsupportedQuality(5),
204            BrotliCompressError::Shared(SharedBrotliError::UnsupportedLargeWindow { quality: 0 }),
205            #[cfg(not(feature = "no_std"))]
206            BrotliCompressError::IOError(std::io::Error::other("nowhere")),
207        ] {
208            assert!(matches!(
209                EncodeError::from_core(error, 0),
210                EncodeError::InternalInvariant { .. }
211            ));
212        }
213    }
214
215    #[test]
216    fn a_bound_overflow_travels_as_its_own_error() {
217        let error = EncodeError::from_core(BrotliCompressError::BoundOverflow, 0);
218        assert!(matches!(error, EncodeError::Bound(SizeOverflow)));
219        assert_eq!(error.to_string(), SizeOverflow.to_string());
220        assert!(EncodeError::from(SizeOverflow).source().is_none());
221    }
222
223    #[test]
224    #[cfg(not(feature = "no_std"))]
225    fn every_variant_maps_to_a_distinguishable_io_kind() {
226        let cases = [
227            (
228                EncodeError::OutputTooSmall { provided: 1 },
229                std::io::ErrorKind::WriteZero,
230            ),
231            (
232                EncodeError::AllocationFailed { requested: 8 },
233                std::io::ErrorKind::OutOfMemory,
234            ),
235            (
236                EncodeError::UnsupportedStreamOffset { offset: 1 },
237                std::io::ErrorKind::InvalidInput,
238            ),
239            (
240                EncodeError::DictionaryUnsupportedForQuality {
241                    quality: Quality::Q0,
242                },
243                std::io::ErrorKind::InvalidInput,
244            ),
245            (
246                EncodeError::AbandonedSession,
247                std::io::ErrorKind::InvalidData,
248            ),
249            (
250                EncodeError::InvalidState {
251                    attempted: "process",
252                },
253                std::io::ErrorKind::InvalidData,
254            ),
255            (EncodeError::Bound(SizeOverflow), std::io::ErrorKind::Other),
256            (
257                EncodeError::InternalInvariant { detail: "defect" },
258                std::io::ErrorKind::Other,
259            ),
260        ];
261        for (error, expected) in cases {
262            let message = error.to_string();
263            let io = std::io::Error::from(error);
264            assert_eq!(io.kind(), expected);
265            // The original error survives as the source, so nothing is lost.
266            assert_eq!(
267                io.get_ref().map(std::string::ToString::to_string),
268                Some(message)
269            );
270        }
271    }
272}