#![forbid(unsafe_code)]
#![deny(trivial_casts, trivial_numeric_casts, missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
macro_rules! debug {
( $( $_:expr ),* ) => {};
}
#[cfg(not(feature = "std"))]
macro_rules! trace {
( $( $_:expr ),* ) => {};
}
#[cfg(not(feature = "std"))]
macro_rules! log_enabled {
( $( $_:expr ),* ) => {
false
};
}
#[cfg_attr(not(feature = "std"), macro_use)]
extern crate alloc;
pub use deflate::{BlockType, CompressResult, DeflateEncoder};
pub use enough::{Stop, StopReason, Unstoppable};
#[cfg(feature = "gzip")]
pub use gzip::GzipEncoder;
#[cfg(all(test, feature = "std"))]
use proptest::prelude::*;
#[cfg(feature = "zlib")]
pub use zlib::ZlibEncoder;
mod blocksplitter;
mod cache;
mod deflate;
#[cfg(feature = "gzip")]
mod gzip;
mod hash;
#[cfg(any(doc, not(feature = "std")))]
mod io;
mod iter;
mod katajainen;
mod lz77;
#[cfg(not(feature = "std"))]
mod math;
mod squeeze;
mod symbols;
mod tree;
mod util;
#[cfg(feature = "zlib")]
mod zlib;
use core::num::NonZeroU64;
#[cfg(all(not(doc), feature = "std"))]
use std::io::{Error, Write};
#[cfg(any(doc, not(feature = "std")))]
pub use io::{Error, ErrorKind, Write};
#[cfg(all(not(doc), feature = "std"))]
fn stop_to_error(reason: StopReason) -> Error {
Error::other(match reason {
StopReason::Cancelled => "operation cancelled",
StopReason::TimedOut => "operation timed out",
_ => "operation stopped",
})
}
#[cfg(any(doc, not(feature = "std")))]
fn stop_to_error(_reason: StopReason) -> Error {
io::ErrorKind::Cancelled.into()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(all(test, feature = "std"), derive(proptest_derive::Arbitrary))]
#[non_exhaustive]
pub struct Options {
#[cfg_attr(
all(test, feature = "std"),
proptest(
strategy = "(1..=10u64).prop_map(|iteration_count| NonZeroU64::new(iteration_count).unwrap())"
)
)]
pub iteration_count: NonZeroU64,
pub iterations_without_improvement: NonZeroU64,
pub maximum_block_splits: u16,
pub block_type: BlockType,
pub enhanced: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
iteration_count: NonZeroU64::new(15).unwrap(),
iterations_without_improvement: NonZeroU64::new(u64::MAX).unwrap(),
maximum_block_splits: 15,
block_type: BlockType::Dynamic,
enhanced: false,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[cfg(feature = "std")]
pub enum Format {
#[cfg(feature = "gzip")]
Gzip,
#[cfg(feature = "zlib")]
Zlib,
Deflate,
}
#[cfg(feature = "std")]
pub fn compress<R: std::io::Read, W: Write>(
options: Options,
output_format: Format,
mut in_data: R,
out: W,
) -> Result<(), Error> {
match output_format {
#[cfg(feature = "gzip")]
Format::Gzip => {
let mut encoder = GzipEncoder::new_buffered(options, out)?;
std::io::copy(&mut in_data, &mut encoder)?;
encoder.into_inner()?.finish().map(|_| ())
}
#[cfg(feature = "zlib")]
Format::Zlib => {
let mut encoder = ZlibEncoder::new_buffered(options, out)?;
std::io::copy(&mut in_data, &mut encoder)?;
encoder.into_inner()?.finish().map(|_| ())
}
Format::Deflate => {
let mut encoder = DeflateEncoder::new_buffered(options, out);
std::io::copy(&mut in_data, &mut encoder)?;
encoder.into_inner()?.finish().map(|_| ())
}
}
}
#[cfg(all(test, feature = "std"))]
mod test {
use std::io;
use miniz_oxide::inflate;
use proptest::proptest;
use super::*;
proptest! {
#[test]
fn deflating_is_reversible(
options: Options,
data in prop::collection::vec(any::<u8>(), 0..64 * 1024)
) {
let mut compressed_data = Vec::with_capacity(data.len());
let mut encoder = DeflateEncoder::new(options, &mut compressed_data);
io::copy(&mut &*data, &mut encoder).unwrap();
encoder.finish().unwrap();
let decompressed_data = inflate::decompress_to_vec(&compressed_data).expect("Could not inflate compressed stream");
prop_assert_eq!(data, decompressed_data, "Decompressed data should match input data");
}
}
#[test]
fn enhanced_produces_valid_deflate() {
let data = b"The quick brown fox jumps over the lazy dog. \
The quick brown fox jumps over the lazy dog. \
The quick brown fox jumps over the lazy dog.";
let mut compressed = Vec::new();
let options = Options {
enhanced: true,
iteration_count: core::num::NonZeroU64::new(5).unwrap(),
..Options::default()
};
let mut encoder = DeflateEncoder::new(options, &mut compressed);
io::copy(&mut &data[..], &mut encoder).unwrap();
encoder.finish().unwrap();
let decompressed =
inflate::decompress_to_vec(&compressed).expect("Enhanced DEFLATE output must be valid");
assert_eq!(&data[..], &decompressed[..]);
}
#[test]
fn enhanced_output_no_larger_than_standard() {
let mut data = Vec::with_capacity(16384);
for i in 0..16384u16 {
data.push((i % 256) as u8);
if i % 7 == 0 {
data.extend_from_slice(b"repeat");
}
}
let compress = |enhanced: bool| -> Vec<u8> {
let mut compressed = Vec::new();
let options = Options {
enhanced,
iteration_count: core::num::NonZeroU64::new(10).unwrap(),
..Options::default()
};
let mut encoder = DeflateEncoder::new(options, &mut compressed);
io::copy(&mut &data[..], &mut encoder).unwrap();
encoder.finish().unwrap();
compressed
};
let standard = compress(false);
let enhanced = compress(true);
assert!(
enhanced.len() <= standard.len(),
"Enhanced ({} bytes) should be <= standard ({} bytes)",
enhanced.len(),
standard.len()
);
let decompressed =
inflate::decompress_to_vec(&enhanced).expect("Enhanced output must decompress");
assert_eq!(&data[..], &decompressed[..]);
}
fn extract_png_idat(png_data: &[u8]) -> Vec<u8> {
let mut idat_data = Vec::new();
let mut pos = 8; while pos + 12 <= png_data.len() {
let len = u32::from_be_bytes(png_data[pos..pos + 4].try_into().unwrap()) as usize;
let chunk_type = &png_data[pos + 4..pos + 8];
if chunk_type == b"IDAT" {
idat_data.extend_from_slice(&png_data[pos + 8..pos + 8 + len]);
}
pos += 12 + len; }
inflate::decompress_to_vec_zlib(&idat_data).expect("Failed to decompress IDAT")
}
#[test]
fn enhanced_vs_standard_on_png_idat() {
let test_pngs: &[(&str, &[u8])] = &[
("eeyore.png", include_bytes!("../test/data/eeyore.png")),
(
"heartbleed.png",
include_bytes!("../test/data/heartbleed.png"),
),
("computer.png", include_bytes!("../test/data/computer.png")),
];
for &(name, png_data) in test_pngs {
let filtered = extract_png_idat(png_data);
let compress = |enhanced: bool| -> Vec<u8> {
let mut compressed = Vec::new();
let options = Options {
enhanced,
iteration_count: core::num::NonZeroU64::new(15).unwrap(),
..Options::default()
};
let mut encoder = DeflateEncoder::new(options, &mut compressed);
io::copy(&mut &*filtered, &mut encoder).unwrap();
encoder.finish().unwrap();
compressed
};
let standard = compress(false);
let enhanced = compress(true);
eprintln!(
"{name}: idat={} bytes, standard={} enhanced={} saved={} ({:.3}%)",
filtered.len(),
standard.len(),
enhanced.len(),
standard.len() as i64 - enhanced.len() as i64,
(1.0 - enhanced.len() as f64 / standard.len() as f64) * 100.0,
);
let decompressed =
inflate::decompress_to_vec(&enhanced).expect("Enhanced output must decompress");
assert_eq!(filtered, decompressed);
}
}
#[test]
fn enhanced_vs_standard_size_report() {
let test_files: &[(&str, &[u8])] = &[
(
"calgary-books-32k",
&include_bytes!("../test/data/calgary-books.txt")[..32768],
),
(
"codetriage.js",
include_bytes!("../test/data/codetriage.js"),
),
];
for &(name, data) in test_files {
let compress = |enhanced: bool| -> Vec<u8> {
let mut compressed = Vec::new();
let options = Options {
enhanced,
iteration_count: core::num::NonZeroU64::new(15).unwrap(),
..Options::default()
};
let mut encoder = DeflateEncoder::new(options, &mut compressed);
io::copy(&mut &*data, &mut encoder).unwrap();
encoder.finish().unwrap();
compressed
};
let standard = compress(false);
let enhanced = compress(true);
eprintln!(
"{name} ({} bytes): standard={} enhanced={} saved={} ({:.3}%)",
data.len(),
standard.len(),
enhanced.len(),
standard.len() as i64 - enhanced.len() as i64,
(1.0 - enhanced.len() as f64 / standard.len() as f64) * 100.0,
);
let decompressed =
inflate::decompress_to_vec(&enhanced).expect("Enhanced output must decompress");
assert_eq!(data, &decompressed[..]);
}
}
}