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}