diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89e9414b..28a63499 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,7 +43,6 @@ jobs: - run: cargo test --features zlib-ng --no-default-features if: matrix.build != 'mingw' - run: cargo test --features zlib-rs --no-default-features - if: matrix.build != 'mingw' - run: cargo test --features cloudflare_zlib --no-default-features if: matrix.build != 'mingw' - run: | diff --git a/Cargo.toml b/Cargo.toml index 4b83ffb6..39185347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "flate2" authors = ["Alex Crichton ", "Josh Triplett "] -version = "1.1.7" +version = "1.1.8" edition = "2018" license = "MIT OR Apache-2.0" readme = "README.md" @@ -22,7 +22,7 @@ exclude = [".*"] libz-sys = { version = "1.1.20", optional = true, default-features = false } libz-ng-sys = { version = "1.1.16", optional = true } # this matches the default features, but we don't want to depend on the default features staying the same -zlib-rs = { version = "0.5.3", optional = true, default-features = false, features = ["std", "rust-allocator"] } +zlib-rs = { version = "0.5.4", optional = true, default-features = false, features = ["std", "rust-allocator"] } cloudflare-zlib-sys = { version = "0.3.6", optional = true } miniz_oxide = { version = "0.8.5", optional = true, default-features = false, features = ["with-alloc", "simd"] } crc32fast = "1.2.0" diff --git a/src/ffi/zlib_rs.rs b/src/ffi/zlib_rs.rs index ddb9339d..f5524bbe 100644 --- a/src/ffi/zlib_rs.rs +++ b/src/ffi/zlib_rs.rs @@ -30,6 +30,7 @@ pub const MZ_FINISH: isize = DeflateFlush::Finish as isize; pub const MZ_DEFAULT_WINDOW_BITS: core::ffi::c_int = 15; use super::*; +use crate::mem::{compress_failed, decompress_failed}; impl From<::zlib_rs::Status> for crate::mem::Status { fn from(value: ::zlib_rs::Status) -> Self { @@ -52,6 +53,9 @@ impl ErrorMessage { pub struct Inflate { pub(crate) inner: ::zlib_rs::Inflate, + // NOTE: these counts do not count the dictionary. + total_in: u64, + total_out: u64, } impl fmt::Debug for Inflate { @@ -59,8 +63,8 @@ impl fmt::Debug for Inflate { write!( f, "zlib_rs inflate internal state. total_in: {}, total_out: {}", - self.inner.total_in(), - self.inner.total_out(), + self.total_in(), + self.total_out(), ) } } @@ -79,6 +83,8 @@ impl InflateBackend for Inflate { fn make(zlib_header: bool, window_bits: u8) -> Self { Inflate { inner: ::zlib_rs::Inflate::new(zlib_header, window_bits), + total_in: 0, + total_out: 0, } } @@ -94,14 +100,24 @@ impl InflateBackend for Inflate { FlushDecompress::Finish => InflateFlush::Finish, }; - match self.inner.decompress(input, output, flush) { + let total_in_start = self.inner.total_in(); + let total_out_start = self.inner.total_out(); + + let result = self.inner.decompress(input, output, flush); + + self.total_in += self.inner.total_in() - total_in_start; + self.total_out += self.inner.total_out() - total_out_start; + + match result { Ok(status) => Ok(status.into()), Err(InflateError::NeedDict { dict_id }) => crate::mem::decompress_need_dict(dict_id), - Err(e) => crate::mem::decompress_failed(ErrorMessage(Some(e.as_str()))), + Err(_) => self.decompress_error(), } } fn reset(&mut self, zlib_header: bool) { + self.total_in = 0; + self.total_out = 0; self.inner.reset(zlib_header); } } @@ -109,17 +125,33 @@ impl InflateBackend for Inflate { impl Backend for Inflate { #[inline] fn total_in(&self) -> u64 { - self.inner.total_in() + self.total_in } #[inline] fn total_out(&self) -> u64 { - self.inner.total_out() + self.total_out + } +} + +impl Inflate { + fn decompress_error(&self) -> Result { + decompress_failed(ErrorMessage(self.inner.error_message())) + } + + pub fn set_dictionary(&mut self, dictionary: &[u8]) -> Result { + match self.inner.set_dictionary(dictionary) { + Ok(v) => Ok(v), + Err(_) => self.decompress_error(), + } } } pub struct Deflate { pub(crate) inner: ::zlib_rs::Deflate, + // NOTE: these counts do not count the dictionary. + total_in: u64, + total_out: u64, } impl fmt::Debug for Deflate { @@ -127,8 +159,8 @@ impl fmt::Debug for Deflate { write!( f, "zlib_rs deflate internal state. total_in: {}, total_out: {}", - self.inner.total_in(), - self.inner.total_out(), + self.total_in(), + self.total_out(), ) } } @@ -140,6 +172,8 @@ impl DeflateBackend for Deflate { Deflate { inner: ::zlib_rs::Deflate::new(level.level() as i32, zlib_header, window_bits), + total_in: 0, + total_out: 0, } } @@ -157,13 +191,23 @@ impl DeflateBackend for Deflate { FlushCompress::Finish => DeflateFlush::Finish, }; - match self.inner.compress(input, output, flush) { + let total_in_start = self.inner.total_in(); + let total_out_start = self.inner.total_out(); + + let result = self.inner.compress(input, output, flush); + + self.total_in += self.inner.total_in() - total_in_start; + self.total_out += self.inner.total_out() - total_out_start; + + match result { Ok(status) => Ok(status.into()), - Err(e) => crate::mem::compress_failed(ErrorMessage(Some(e.as_str()))), + Err(_) => self.compress_error(), } } fn reset(&mut self) { + self.total_in = 0; + self.total_out = 0; self.inner.reset(); } } @@ -171,11 +215,39 @@ impl DeflateBackend for Deflate { impl Backend for Deflate { #[inline] fn total_in(&self) -> u64 { - self.inner.total_in() + self.total_in } #[inline] fn total_out(&self) -> u64 { - self.inner.total_out() + self.total_out + } +} + +impl Deflate { + fn compress_error(&self) -> Result { + compress_failed(ErrorMessage(self.inner.error_message())) + } + + pub fn set_dictionary(&mut self, dictionary: &[u8]) -> Result { + match self.inner.set_dictionary(dictionary) { + Ok(v) => Ok(v), + Err(_) => self.compress_error(), + } + } + + pub fn set_level(&mut self, level: Compression) -> Result<(), CompressError> { + use ::zlib_rs::Status; + + match self.inner.set_level(level.level() as i32) { + Ok(status) => match status { + Status::Ok => Ok(()), + Status::BufError => compress_failed(ErrorMessage(Some("insufficient space"))), + Status::StreamEnd => { + unreachable!("zlib-rs is known to never return the StreamEnd status") + } + }, + Err(_) => self.compress_error(), + } } } diff --git a/src/mem.rs b/src/mem.rs index 903e46b5..2a8815d2 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -212,8 +212,8 @@ impl Compress { /// # Panics /// /// If `window_bits` does not fall into the range 9 ..= 15, - /// `new_with_window_bits` will panic. - #[cfg(feature = "any_zlib")] + /// this function will panic. + #[cfg(feature = "any_impl")] pub fn new_with_window_bits( level: Compression, zlib_header: bool, @@ -239,8 +239,8 @@ impl Compress { /// # Panics /// /// If `window_bits` does not fall into the range 9 ..= 15, - /// `new_with_window_bits` will panic. - #[cfg(feature = "any_zlib")] + /// this function will panic. + #[cfg(feature = "any_impl")] pub fn new_gzip(level: Compression, window_bits: u8) -> Compress { assert!( window_bits > 8 && window_bits < 16, @@ -286,6 +286,14 @@ impl Compress { } } + /// Specifies the compression dictionary to use. + /// + /// Returns the Adler-32 checksum of the dictionary. + #[cfg(all(not(feature = "any_zlib"), feature = "zlib-rs"))] + pub fn set_dictionary(&mut self, dictionary: &[u8]) -> Result { + self.inner.set_dictionary(dictionary) + } + /// Quickly resets this compressor without having to reallocate anything. /// /// This is equivalent to dropping this object and then creating a new one. @@ -303,22 +311,31 @@ impl Compress { /// the compression of the available input data before changing the /// compression level. Flushing the stream before calling this method /// ensures that the function will succeed on the first call. - #[cfg(feature = "any_zlib")] + #[cfg(any(feature = "any_zlib", feature = "zlib-rs"))] pub fn set_level(&mut self, level: Compression) -> Result<(), CompressError> { - use std::os::raw::c_int; - // SAFETY: The field `inner` must always be accessed as a raw pointer, - // since it points to a cyclic structure. No copies of `inner` can be - // retained for longer than the lifetime of `self.inner.inner.stream_wrapper`. - let stream = self.inner.inner.stream_wrapper.inner; - unsafe { - (*stream).msg = std::ptr::null_mut(); + #[cfg(all(not(feature = "any_zlib"), feature = "zlib-rs"))] + { + self.inner.set_level(level) } - let rc = unsafe { ffi::deflateParams(stream, level.0 as c_int, ffi::MZ_DEFAULT_STRATEGY) }; - match rc { - ffi::MZ_OK => Ok(()), - ffi::MZ_BUF_ERROR => compress_failed(self.inner.inner.msg()), - c => panic!("unknown return code: {}", c), + #[cfg(feature = "any_zlib")] + { + use std::os::raw::c_int; + // SAFETY: The field `inner` must always be accessed as a raw pointer, + // since it points to a cyclic structure. No copies of `inner` can be + // retained for longer than the lifetime of `self.inner.inner.stream_wrapper`. + let stream = self.inner.inner.stream_wrapper.inner; + unsafe { + (*stream).msg = std::ptr::null_mut(); + } + let rc = + unsafe { ffi::deflateParams(stream, level.0 as c_int, ffi::MZ_DEFAULT_STRATEGY) }; + + match rc { + ffi::MZ_OK => Ok(()), + ffi::MZ_BUF_ERROR => compress_failed(self.inner.inner.msg()), + c => panic!("unknown return code: {}", c), + } } } @@ -398,8 +415,8 @@ impl Decompress { /// # Panics /// /// If `window_bits` does not fall into the range 9 ..= 15, - /// `new_with_window_bits` will panic. - #[cfg(feature = "any_zlib")] + /// this function will panic. + #[cfg(feature = "any_impl")] pub fn new_with_window_bits(zlib_header: bool, window_bits: u8) -> Decompress { assert!( window_bits > 8 && window_bits < 16, @@ -418,8 +435,8 @@ impl Decompress { /// # Panics /// /// If `window_bits` does not fall into the range 9 ..= 15, - /// `new_with_window_bits` will panic. - #[cfg(feature = "any_zlib")] + /// this function will panic. + #[cfg(feature = "any_impl")] pub fn new_gzip(window_bits: u8) -> Decompress { assert!( window_bits > 8 && window_bits < 16, @@ -540,6 +557,12 @@ impl Decompress { } } + /// Specifies the decompression dictionary to use. + #[cfg(all(not(feature = "any_zlib"), feature = "zlib-rs"))] + pub fn set_dictionary(&mut self, dictionary: &[u8]) -> Result { + self.inner.set_dictionary(dictionary) + } + /// Performs the equivalent of replacing this decompression state with a /// freshly allocated copy. /// @@ -640,7 +663,7 @@ mod tests { use crate::write; use crate::{Compression, Decompress, FlushDecompress}; - #[cfg(feature = "any_zlib")] + #[cfg(feature = "any_impl")] use crate::{Compress, FlushCompress}; #[test] @@ -700,87 +723,7 @@ mod tests { assert!(dst.starts_with(string)); } - #[cfg(feature = "any_zlib")] - #[test] - fn set_dictionary_with_zlib_header() { - let string = "hello, hello!".as_bytes(); - let dictionary = "hello".as_bytes(); - - let mut encoded = Vec::with_capacity(1024); - - let mut encoder = Compress::new(Compression::default(), true); - - let dictionary_adler = encoder.set_dictionary(&dictionary).unwrap(); - - encoder - .compress_vec(string, &mut encoded, FlushCompress::Finish) - .unwrap(); - - assert_eq!(encoder.total_in(), string.len() as u64); - assert_eq!(encoder.total_out(), encoded.len() as u64); - - let mut decoder = Decompress::new(true); - let mut decoded = [0; 1024]; - let decompress_error = decoder - .decompress(&encoded, &mut decoded, FlushDecompress::Finish) - .expect_err("decompression should fail due to requiring a dictionary"); - - let required_adler = decompress_error.needs_dictionary() - .expect("the first call to decompress should indicate a dictionary is required along with the required Adler-32 checksum"); - - assert_eq!(required_adler, dictionary_adler, - "the Adler-32 checksum should match the value when the dictionary was set on the compressor"); - - let actual_adler = decoder.set_dictionary(&dictionary).unwrap(); - - assert_eq!(required_adler, actual_adler); - - // Decompress the rest of the input to the remainder of the output buffer - let total_in = decoder.total_in(); - let total_out = decoder.total_out(); - - let decompress_result = decoder.decompress( - &encoded[total_in as usize..], - &mut decoded[total_out as usize..], - FlushDecompress::Finish, - ); - assert!(decompress_result.is_ok()); - - assert_eq!(&decoded[..decoder.total_out() as usize], string); - } - - #[cfg(feature = "any_zlib")] - #[test] - fn set_dictionary_raw() { - let string = "hello, hello!".as_bytes(); - let dictionary = "hello".as_bytes(); - - let mut encoded = Vec::with_capacity(1024); - - let mut encoder = Compress::new(Compression::default(), false); - - encoder.set_dictionary(&dictionary).unwrap(); - - encoder - .compress_vec(string, &mut encoded, FlushCompress::Finish) - .unwrap(); - - assert_eq!(encoder.total_in(), string.len() as u64); - assert_eq!(encoder.total_out(), encoded.len() as u64); - - let mut decoder = Decompress::new(false); - - decoder.set_dictionary(&dictionary).unwrap(); - - let mut decoded = [0; 1024]; - let decompress_result = decoder.decompress(&encoded, &mut decoded, FlushDecompress::Finish); - - assert!(decompress_result.is_ok()); - - assert_eq!(&decoded[..decoder.total_out() as usize], string); - } - - #[cfg(feature = "any_zlib")] + #[cfg(feature = "any_impl")] #[test] fn test_gzip_flate() { let string = "hello, hello!".as_bytes(); @@ -806,7 +749,7 @@ mod tests { assert_eq!(&decoded[..decoder.total_out() as usize], string); } - #[cfg(feature = "any_zlib")] + #[cfg(any(feature = "any_zlib", feature = "zlib-rs"))] #[test] fn test_error_message() { let mut decoder = Decompress::new(false); diff --git a/tests/capabilities.rs b/tests/capabilities.rs new file mode 100644 index 00000000..7cd18fd8 --- /dev/null +++ b/tests/capabilities.rs @@ -0,0 +1,195 @@ +//! Validate that certain feature-gated functionality is still available. +use flate2::{Compress, Compression, Decompress, FlushCompress, FlushDecompress}; + +#[test] +fn compress_new_with_window_bits_is_present_and_works() { + let string = "hello world".as_bytes(); + + // Test with window_bits = 9 (minimum) + let mut encoded = Vec::with_capacity(1024); + let mut encoder = Compress::new_with_window_bits(Compression::default(), true, 9); + encoder + .compress_vec(string, &mut encoded, FlushCompress::Finish) + .unwrap(); + assert_ne!(encoded.len(), 0); + + let mut decoder = Decompress::new_with_window_bits(true, 9); + let mut decoded = [0; 1024]; + decoder + .decompress(&encoded, &mut decoded, FlushDecompress::Finish) + .unwrap(); + assert_eq!(&decoded[..string.len()], string); + + // Test with window_bits = 15 (maximum) + let mut encoded = Vec::with_capacity(1024); + let mut encoder = Compress::new_with_window_bits(Compression::default(), false, 15); + encoder + .compress_vec(string, &mut encoded, FlushCompress::Finish) + .unwrap(); + assert_ne!(encoded.len(), 0); + + let mut decoder = Decompress::new_with_window_bits(false, 15); + let mut decoded = [0; 1024]; + decoder + .decompress(&encoded, &mut decoded, FlushDecompress::Finish) + .unwrap(); + assert_eq!(&decoded[..string.len()], string); +} + +#[test] +fn decompress_new_gzip_window_bits_is_present_and_works() { + let string = "hello world".as_bytes(); + + // Test with different window_bits values + for window_bits in [9, 12, 15] { + let mut encoded = Vec::with_capacity(1024); + let mut encoder = Compress::new_gzip(Compression::default(), window_bits); + encoder + .compress_vec(string, &mut encoded, FlushCompress::Finish) + .unwrap(); + + let mut decoder = Decompress::new_gzip(window_bits); + let mut decoded = [0; 1024]; + decoder + .decompress(&encoded, &mut decoded, FlushDecompress::Finish) + .unwrap(); + assert_eq!( + &decoded[..string.len()], + string, + "Failed with window_bits={}", + window_bits + ); + } +} + +#[test] +#[should_panic(expected = "window_bits must be within 9 ..= 15")] +fn compress_new_with_window_bits_invalid_low() { + let _ = Compress::new_with_window_bits(Compression::default(), true, 8); +} + +#[test] +#[should_panic(expected = "window_bits must be within 9 ..= 15")] +fn compress_new_with_window_bits_invalid_high() { + let _ = Compress::new_with_window_bits(Compression::default(), true, 16); +} + +#[test] +#[should_panic(expected = "window_bits must be within 9 ..= 15")] +fn compress_new_gzip_invalid_low() { + let _ = Compress::new_gzip(Compression::default(), 8); +} + +#[test] +#[should_panic(expected = "window_bits must be within 9 ..= 15")] +fn compress_new_gzip_invalid_high() { + let _ = Compress::new_gzip(Compression::default(), 16); +} + +// Unsupported for `miniz_oxide`. +#[cfg(not(feature = "miniz_oxide"))] +#[test] +fn set_dictionary_with_zlib_header() { + let string = "hello, hello!".as_bytes(); + let dictionary = "hello".as_bytes(); + + let mut encoded = Vec::with_capacity(1024); + + let mut encoder = Compress::new(Compression::default(), true); + + let dictionary_adler = encoder.set_dictionary(&dictionary).unwrap(); + + encoder + .compress_vec(string, &mut encoded, FlushCompress::Finish) + .unwrap(); + + assert_eq!(encoder.total_in(), string.len() as u64); + assert_eq!(encoder.total_out(), encoded.len() as u64); + + let mut decoder = Decompress::new(true); + let mut decoded = [0; 1024]; + let decompress_error = decoder + .decompress(&encoded, &mut decoded, FlushDecompress::Finish) + .expect_err("decompression should fail due to requiring a dictionary"); + + let required_adler = decompress_error.needs_dictionary() + .expect("the first call to decompress should indicate a dictionary is required along with the required Adler-32 checksum"); + + assert_eq!(required_adler, dictionary_adler, + "the Adler-32 checksum should match the value when the dictionary was set on the compressor"); + + let actual_adler = decoder.set_dictionary(&dictionary).unwrap(); + + assert_eq!(required_adler, actual_adler); + + // Decompress the rest of the input to the remainder of the output buffer + let total_in = decoder.total_in(); + let total_out = decoder.total_out(); + + let decompress_result = decoder.decompress( + &encoded[total_in as usize..], + &mut decoded[total_out as usize..], + FlushDecompress::Finish, + ); + assert!(decompress_result.is_ok()); + + assert_eq!(&decoded[..decoder.total_out() as usize], string); +} + +// Unsupported for `miniz_oxide`. +#[cfg(not(feature = "miniz_oxide"))] +#[test] +fn set_dictionary_raw() { + let string = "hello, hello!".as_bytes(); + let dictionary = "hello".as_bytes(); + + let mut encoded = Vec::with_capacity(1024); + + let mut encoder = Compress::new(Compression::default(), false); + + encoder.set_dictionary(&dictionary).unwrap(); + + encoder + .compress_vec(string, &mut encoded, FlushCompress::Finish) + .unwrap(); + + assert_eq!(encoder.total_in(), string.len() as u64); + assert_eq!(encoder.total_out(), encoded.len() as u64); + + let mut decoder = Decompress::new(false); + + decoder.set_dictionary(&dictionary).unwrap(); + + let mut decoded = [0; 1024]; + let decompress_result = decoder.decompress(&encoded, &mut decoded, FlushDecompress::Finish); + + assert!(decompress_result.is_ok()); + + assert_eq!(&decoded[..decoder.total_out() as usize], string); +} + +#[test] +// Unsupported for `miniz_oxide`. +#[cfg(not(feature = "miniz_oxide"))] +fn set_level_is_effective() { + let input = b"hello hello hello hello hello hello hello hello"; + + // Compress with no compression + let mut encoded_none = Vec::new(); + Compress::new(Compression::none(), true) + .compress_vec(input, &mut encoded_none, FlushCompress::Finish) + .unwrap(); + + // Compress with best compression + let mut encoded_best = Vec::new(); + Compress::new(Compression::best(), true) + .compress_vec(input, &mut encoded_best, FlushCompress::Finish) + .unwrap(); + + assert!( + encoded_best.len() <= encoded_none.len(), + "best compression produced larger output than no compression: best={}, none={}", + encoded_best.len(), + encoded_none.len(), + ); +}