mod inflate;
#[cfg(feature = "alloc")]
mod stream;
mod tables;
mod wrapper;
#[cfg(feature = "std")]
pub use stream::DecompressReader;
#[cfg(feature = "alloc")]
pub use stream::{
MeasureProgress, StreamDecompressor, StreamMeasurer, StreamProgress, StreamStatus,
};
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::fmt;
use crate::Format;
use inflate::Status;
use tables::Tables;
use wrapper::{body_bounds, Check};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DecompressError {
BadData,
ChecksumMismatch,
InsufficientSpace,
TrailingData,
Truncated,
}
impl fmt::Display for DecompressError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
DecompressError::BadData => "invalid compressed data",
DecompressError::ChecksumMismatch => "checksum mismatch in compressed data",
DecompressError::InsufficientSpace => "output buffer too small",
DecompressError::TrailingData => "unexpected data after the compressed stream",
DecompressError::Truncated => "compressed data ended early",
})
}
}
impl core::error::Error for DecompressError {}
#[cfg(feature = "std")]
impl From<DecompressError> for std::io::Error {
fn from(e: DecompressError) -> Self {
let kind = match e {
DecompressError::Truncated => std::io::ErrorKind::UnexpectedEof,
_ => std::io::ErrorKind::InvalidData,
};
std::io::Error::new(kind, e)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Decompressed {
pub consumed: usize,
pub written: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Measured {
pub consumed: usize,
pub len: usize,
}
#[derive(Clone)]
pub struct Decompressor {
format: Format,
tables: Tables,
}
impl fmt::Debug for Decompressor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Decompressor")
.field("format", &self.format)
.finish_non_exhaustive()
}
}
impl Decompressor {
#[must_use]
pub const fn new(format: Format) -> Self {
Decompressor {
format,
tables: Tables::new(),
}
}
#[must_use]
pub const fn format(&self) -> Format {
self.format
}
pub fn set_format(&mut self, format: Format) {
self.format = format;
}
pub fn decompress_into(
&mut self,
input: &[u8],
output: &mut [u8],
) -> Result<Decompressed, DecompressError> {
let raw = self.format == Format::Deflate;
let (start, body_end) = if raw {
(0, input.len())
} else {
body_bounds(self.format, input)?
};
let len = output.len();
let (consumed, written) = whole(inflate::run::<false, true>(
&mut self.tables,
&input[start..body_end],
output,
len,
None,
false,
))?;
if raw {
return Ok(Decompressed { consumed, written });
}
let mut check = Check::new(self.format);
check.update(&output[..written]);
let consumed = check.verify_at(input, start + consumed)?;
Ok(Decompressed { consumed, written })
}
pub fn measure(&mut self, input: &[u8]) -> Result<Measured, DecompressError> {
let (start, body_end) = body_bounds(self.format, input)?;
let body = &input[start..body_end];
let (consumed, len) = whole(inflate::run::<false, false>(
&mut self.tables,
body,
&mut [],
usize::MAX,
None,
false,
))?;
let mut check = Check::len_only(self.format);
check.count(len);
let consumed = check.verify_at(input, start + consumed)?;
Ok(Measured { consumed, len })
}
#[cfg(feature = "alloc")]
pub fn decompress(&mut self, input: &[u8], max_len: usize) -> Result<Vec<u8>, DecompressError> {
let mut out = Vec::new();
let mut rest = input;
loop {
let consumed = self.decompress_append(rest, &mut out, max_len)?;
rest = &rest[consumed..];
if rest.is_empty() {
return Ok(out);
}
if self.format != Format::Gzip {
return Err(DecompressError::TrailingData);
}
}
}
#[cfg(feature = "alloc")]
fn decompress_append(
&mut self,
input: &[u8],
out: &mut Vec<u8>,
max_len: usize,
) -> Result<usize, DecompressError> {
let guess = match (self.format, input.len().checked_sub(4)) {
(Format::Gzip, Some(i)) => u32::from_le_bytes(input[i..].try_into().unwrap()) as usize,
_ => input.len().saturating_mul(4).max(1024),
};
self.decompress_append_from(input, out, max_len, guess)
}
#[cfg(feature = "alloc")]
fn decompress_append_from(
&mut self,
input: &[u8],
out: &mut Vec<u8>,
max_len: usize,
guess: usize,
) -> Result<usize, DecompressError> {
let out_start = out.len();
let r = body_bounds(self.format, input).and_then(|(start, body_end)| {
let consumed =
self.inflate_growing(&input[start..body_end], out, out_start, max_len, guess)?;
let mut check = Check::new(self.format);
check.update(&out[out_start..]);
check.verify_at(input, start + consumed)
});
if r.is_err() {
out.truncate(out_start);
}
r
}
#[cfg(feature = "alloc")]
fn inflate_growing(
&mut self,
input: &[u8],
out: &mut Vec<u8>,
start: usize,
max_len: usize,
guess: usize,
) -> Result<usize, DecompressError> {
let room = max_len - start;
let mut len = guess.min(room);
let mut resume = None;
loop {
if out.capacity() == 0 {
*out = alloc::vec![0; len];
} else {
out.resize(start + len, 0);
}
match inflate::run::<true, true>(
&mut self.tables,
input,
&mut out[start..],
len,
resume.as_ref(),
false,
)? {
Status::Done(consumed, written) => {
out.truncate(start + written);
return Ok(consumed);
}
Status::Full(_) if len == room => {
return Err(DecompressError::InsufficientSpace);
}
Status::Full(r) => {
len = next_len(len, r.in_pos(), input.len()).min(room);
resume = Some(r);
}
}
}
}
}
fn whole(status: Result<Status, DecompressError>) -> Result<(usize, usize), DecompressError> {
match status? {
Status::Done(consumed, written) => Ok((consumed, written)),
Status::Full(_) => Err(DecompressError::InsufficientSpace),
}
}
#[cfg(feature = "alloc")]
fn next_len(len: usize, in_pos: usize, in_len: usize) -> usize {
let rest = (in_len - in_pos) as u128;
let estimate = if in_pos == 0 {
len
} else {
usize::try_from(len as u128 * rest / in_pos as u128).unwrap_or(usize::MAX)
};
let extra = estimate.saturating_add(estimate / 8).max(len / 4).max(1024);
len.saturating_add(extra)
}
#[cfg(all(test, feature = "alloc"))]
fn reference_compress(format: Format, level: i32, data: &[u8]) -> Vec<u8> {
use miniz_oxide::deflate::{compress_to_vec, compress_to_vec_zlib};
let level = level.min(10) as u8;
match format {
Format::Deflate => compress_to_vec(data, level),
Format::Zlib => compress_to_vec_zlib(data, level),
Format::Gzip => {
let mut out = alloc::vec![0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 0xff];
out.extend(compress_to_vec(data, level));
out.extend(crate::checksum::crc32(0, data).to_le_bytes());
out.extend((data.len() as u32).to_le_bytes());
out
}
}
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
use alloc::vec;
const FORMATS: [Format; 3] = [Format::Deflate, Format::Zlib, Format::Gzip];
#[test]
fn vec_grows_past_the_first_guess() {
let data = vec![b'a'; 1 << 20];
for format in FORMATS {
let c = reference_compress(format, 2, &data);
let mut d = Decompressor::new(format);
assert_eq!(d.decompress(&c, data.len()).as_deref(), Ok(&data[..]));
assert_eq!(
d.decompress(&c, data.len() - 1),
Err(DecompressError::InsufficientSpace),
"{format:?}"
);
}
}
#[test]
fn vec_rejects_trailing_data() {
for format in FORMATS {
let mut c = reference_compress(format, 2, b"hello hello hello");
c.push(0);
let want = match format {
Format::Gzip => DecompressError::BadData,
_ => DecompressError::TrailingData,
};
assert_eq!(crate::decompress(format, &c, 1000), Err(want), "{format:?}");
}
}
#[test]
fn gzip_vec_reads_every_member() {
let mut c = reference_compress(Format::Gzip, 2, b"first member, ");
c.extend(reference_compress(Format::Gzip, 10, &[b'x'; 5000]));
let mut want = b"first member, ".to_vec();
want.extend([b'x'; 5000]);
assert_eq!(crate::decompress(Format::Gzip, &c, 1 << 20), Ok(want));
let mut out = [0; 100];
let r = Decompressor::new(Format::Gzip)
.decompress_into(&c, &mut out)
.unwrap();
assert_eq!(&out[..r.written], b"first member, ");
assert!(r.consumed < c.len());
}
#[test]
fn gzip_vec_survives_a_wrong_size_footer() {
let data = vec![b'z'; 100_000];
for isize in [0u32, 1, 99_999, 100_001, u32::MAX] {
let mut c = reference_compress(Format::Gzip, 2, &data);
let n = c.len();
c[n - 4..].copy_from_slice(&isize.to_le_bytes());
assert_eq!(
crate::decompress(Format::Gzip, &c, 200_000),
Err(DecompressError::ChecksumMismatch),
"ISIZE {isize}"
);
}
}
#[test]
fn checksum_mismatch_is_reported() {
for format in [Format::Zlib, Format::Gzip] {
let mut c = reference_compress(format, 2, b"checksummed data");
let i = match format {
Format::Zlib => c.len() - 1,
_ => c.len() - 5,
};
c[i] ^= 1;
assert_eq!(
crate::decompress(format, &c, 1000),
Err(DecompressError::ChecksumMismatch),
"{format:?}"
);
}
}
#[test]
fn empty_input_round_trips() {
for format in FORMATS {
let c = reference_compress(format, 6, b"");
assert_eq!(crate::decompress(format, &c, 0), Ok(vec![]), "{format:?}");
}
}
}
#[cfg(all(test, feature = "alloc"))]
mod resume_tests {
use super::*;
use alloc::vec;
use proptest::prelude::*;
fn decompress_from_guess(
format: Format,
c: &[u8],
max_len: usize,
guess: usize,
) -> Result<Vec<u8>, DecompressError> {
let mut out = b"prefix".to_vec();
let consumed =
Decompressor::new(format).decompress_append_from(c, &mut out, 6 + max_len, guess)?;
assert_eq!(consumed, c.len());
assert_eq!(&out[..6], b"prefix");
Ok(out.split_off(6))
}
fn data() -> impl Strategy<Value = Vec<u8>> {
prop::collection::vec(
prop_oneof![
prop::collection::vec(any::<u8>(), 0..2000),
(any::<u8>(), 1usize..3000).prop_map(|(b, n)| vec![b; n]),
(prop::collection::vec(any::<u8>(), 1..9), 1usize..300)
.prop_map(|(p, n)| p.repeat(n)),
],
0..20,
)
.prop_map(|chunks| chunks.concat())
}
fn assert_resumed_errors_match(format: Format, c: &[u8], max_len: usize) {
let mut whole = vec![0; max_len];
let want = Decompressor::new(format)
.decompress_into(c, &mut whole)
.map(|d| {
whole.truncate(d.written);
(d.consumed, whole)
});
let mut out = Vec::new();
let got = Decompressor::new(format)
.decompress_append_from(c, &mut out, max_len, 1)
.map(|consumed| (consumed, out));
assert_eq!(got, want);
}
#[test]
fn resume_after_reading_past_the_end() {
let c = [
0xed, 0xc0, 0x31, 0x0d, 0x00, 0x00, 0x0c, 0x02, 0xb0, 0x63, 0x16, 0xf0, 0xaf, 0x75,
0x36, 0x20, 0x69, 0x13, 0x00, 0x0a, 0x18, 0x2c, 0x78,
];
assert_resumed_errors_match(Format::Deflate, &c, 2104 + 5000);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn resumed_decode_matches(data in data(), level in 0i32..=12, f in 0usize..3, guess in 0usize..64) {
let format = [Format::Deflate, Format::Zlib, Format::Gzip][f];
let c = reference_compress(format, level, &data);
prop_assert_eq!(decompress_from_guess(format, &c, data.len(), guess), Ok(data.clone()));
if !data.is_empty() {
prop_assert_eq!(
decompress_from_guess(format, &c, data.len() - 1, guess),
Err(DecompressError::InsufficientSpace)
);
}
}
#[test]
fn resumed_decode_errors_match(data in data(), level in 0i32..=12, f in 0usize..3, flip in any::<prop::sample::Index>(), bit in 0u8..8) {
let format = [Format::Deflate, Format::Zlib, Format::Gzip][f];
let mut c = reference_compress(format, level, &data);
let i = flip.index(c.len());
c[i] ^= 1 << bit;
assert_resumed_errors_match(format, &c, data.len() + 5000);
}
#[test]
fn measure_matches_decode(data in data(), level in 0i32..=12, f in 0usize..3, tail in prop::collection::vec(any::<u8>(), 0..16)) {
let format = [Format::Deflate, Format::Zlib, Format::Gzip][f];
let mut c = reference_compress(format, level, &data);
let n = c.len();
c.extend(tail);
let m = Decompressor::new(format).measure(&c);
prop_assert_eq!(m, Ok(Measured { consumed: n, len: data.len() }));
}
#[test]
fn measure_errors_match_decode(data in data(), level in 0i32..=12, f in 0usize..3, flip in any::<prop::sample::Index>(), bit in 0u8..8) {
let format = [Format::Deflate, Format::Zlib, Format::Gzip][f];
let mut c = reference_compress(format, level, &data);
let i = flip.index(c.len());
c[i] ^= 1 << bit;
let mut out = vec![0; c.len() * 1032];
let want = Decompressor::new(format).decompress_into(&c, &mut out);
let got = Decompressor::new(format).measure(&c);
match want {
Ok(d) => prop_assert_eq!(got, Ok(Measured { consumed: d.consumed, len: d.written })),
Err(DecompressError::ChecksumMismatch) => prop_assert!(
matches!(got, Ok(_) | Err(DecompressError::ChecksumMismatch)),
"{:?}", got
),
Err(e) => prop_assert_eq!(got, Err(e)),
}
}
}
}