use super::config::{Quality, SizeOverflow};
use super::internal::BrotliCompressError;
use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum EncodeError {
#[error("stream position {position} plus {input_bytes} input bytes exceeds 63 bits")]
StreamPositionOverflow {
position: u64,
input_bytes: u64,
},
#[error("a destination of {provided} bytes cannot hold the compressed stream")]
OutputTooSmall {
provided: usize,
},
#[error("an allocation of {requested} bytes failed")]
AllocationFailed {
requested: usize,
},
#[error(transparent)]
Bound(#[from] SizeOverflow),
#[error("quality {} cannot compress against a prepared dictionary", quality.get())]
DictionaryUnsupportedForQuality {
quality: Quality,
},
#[error(
"stream offset {offset} requires experimental continuation support at quality 2 or above"
)]
UnsupportedStreamOffset {
offset: u64,
},
#[error("a previous session was abandoned; call Compressor::recover before encoding again")]
AbandonedSession,
#[error("the session cannot {attempted} in its current state")]
InvalidState {
attempted: &'static str,
},
#[error("an internal encoder invariant was violated: {detail}")]
InternalInvariant {
detail: &'static str,
},
}
impl EncodeError {
pub(crate) fn from_core(error: BrotliCompressError, provided: usize) -> Self {
match error {
BrotliCompressError::OutputTooSmall => Self::OutputTooSmall { provided },
BrotliCompressError::BoundOverflow => Self::Bound(SizeOverflow),
BrotliCompressError::Shared(_) | BrotliCompressError::UnsupportedQuality(_) => {
Self::InternalInvariant {
detail: "a validated configuration reached an encoder that refused it",
}
}
BrotliCompressError::BufferOverflow => Self::InternalInvariant {
detail: "the encoder's scratch buffer was too small",
},
#[cfg(not(feature = "no_std"))]
BrotliCompressError::IOError(_) => Self::InternalInvariant {
detail: "an encoder reported an I/O failure it cannot perform",
},
}
}
}
#[cfg(not(feature = "no_std"))]
impl From<EncodeError> for std::io::Error {
fn from(value: EncodeError) -> Self {
let kind = match value {
EncodeError::OutputTooSmall { .. } => std::io::ErrorKind::WriteZero,
EncodeError::AllocationFailed { .. } => std::io::ErrorKind::OutOfMemory,
EncodeError::UnsupportedStreamOffset { .. }
| EncodeError::StreamPositionOverflow { .. }
| EncodeError::DictionaryUnsupportedForQuality { .. } => {
std::io::ErrorKind::InvalidInput
}
EncodeError::AbandonedSession | EncodeError::InvalidState { .. } => {
std::io::ErrorKind::InvalidData
}
EncodeError::Bound(_) | EncodeError::InternalInvariant { .. } => {
std::io::ErrorKind::Other
}
};
Self::new(kind, value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compressor::shared::SharedBrotliError;
use ::core::error::Error as _;
use alloc::string::ToString;
#[test]
fn a_short_destination_reports_what_it_was_given() {
let error = EncodeError::from_core(BrotliCompressError::OutputTooSmall, 7);
assert!(matches!(error, EncodeError::OutputTooSmall { provided: 7 }));
assert!(error.to_string().contains('7'));
}
#[test]
fn a_refused_dictionary_names_the_quality() {
let error = EncodeError::DictionaryUnsupportedForQuality {
quality: Quality::Q3,
};
assert!(error.to_string().contains('3'));
}
#[test]
fn unreachable_low_level_failures_become_internal_invariants() {
for error in [
BrotliCompressError::BufferOverflow,
BrotliCompressError::UnsupportedQuality(5),
BrotliCompressError::Shared(SharedBrotliError::UnsupportedLargeWindow { quality: 0 }),
#[cfg(not(feature = "no_std"))]
BrotliCompressError::IOError(std::io::Error::other("nowhere")),
] {
assert!(matches!(
EncodeError::from_core(error, 0),
EncodeError::InternalInvariant { .. }
));
}
}
#[test]
fn a_bound_overflow_travels_as_its_own_error() {
let error = EncodeError::from_core(BrotliCompressError::BoundOverflow, 0);
assert!(matches!(error, EncodeError::Bound(SizeOverflow)));
assert_eq!(error.to_string(), SizeOverflow.to_string());
assert!(EncodeError::from(SizeOverflow).source().is_none());
}
#[test]
#[cfg(not(feature = "no_std"))]
fn every_variant_maps_to_a_distinguishable_io_kind() {
let cases = [
(
EncodeError::OutputTooSmall { provided: 1 },
std::io::ErrorKind::WriteZero,
),
(
EncodeError::AllocationFailed { requested: 8 },
std::io::ErrorKind::OutOfMemory,
),
(
EncodeError::UnsupportedStreamOffset { offset: 1 },
std::io::ErrorKind::InvalidInput,
),
(
EncodeError::DictionaryUnsupportedForQuality {
quality: Quality::Q0,
},
std::io::ErrorKind::InvalidInput,
),
(
EncodeError::AbandonedSession,
std::io::ErrorKind::InvalidData,
),
(
EncodeError::InvalidState {
attempted: "process",
},
std::io::ErrorKind::InvalidData,
),
(EncodeError::Bound(SizeOverflow), std::io::ErrorKind::Other),
(
EncodeError::InternalInvariant { detail: "defect" },
std::io::ErrorKind::Other,
),
];
for (error, expected) in cases {
let message = error.to_string();
let io = std::io::Error::from(error);
assert_eq!(io.kind(), expected);
assert_eq!(
io.get_ref().map(std::string::ToString::to_string),
Some(message)
);
}
}
}