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 39fd0aa3..8acfa8b5 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" @@ -51,7 +51,7 @@ default = ["rust_backend"] ## Use the zlib-rs backend, a pure Rust rewrite of zlib. ## This is the fastest backend overall, providing excellent performance with some `unsafe` code. ## It does not require a C compiler but uses `unsafe` Rust for optimization. -zlib-rs = ["any_impl", "dep:zlib-rs"] +zlib-rs = ["any_zlib", "dep:zlib-rs"] ## Use the pure Rust `miniz_oxide` backend (default). ## This implementation uses only safe Rust code and doesn't require a C compiler. @@ -63,11 +63,11 @@ rust_backend = ["miniz_oxide", "any_impl"] ## Use the system's installed zlib library. ## This is useful when you need compatibility with other C code that uses zlib, ## or when you want to use the system-provided zlib for consistency. -zlib = ["any_zlib", "libz-sys"] +zlib = ["any_c_zlib", "libz-sys"] ## Use the system's installed zlib library with default features enabled. ## Similar to `zlib` but enables additional features from libz-sys. -zlib-default = ["any_zlib", "libz-sys/default"] +zlib-default = ["any_c_zlib", "libz-sys/default"] ## Use zlib-ng in zlib-compat mode via libz-sys. ## This provides zlib-ng's performance improvements while maintaining compatibility. @@ -83,13 +83,13 @@ zlib-ng-compat = ["zlib", "libz-sys/zlib-ng"] ## Use the high-performance zlib-ng library directly. ## This typically provides better performance than stock zlib and works even when ## other dependencies use zlib. Requires a C compiler. -zlib-ng = ["any_zlib", "libz-ng-sys"] +zlib-ng = ["any_c_zlib", "libz-ng-sys"] ## Use Cloudflare's optimized zlib implementation. ## This provides better performance than stock zlib on x86-64 (with SSE 4.2) and ARM64 (with NEON & CRC). ## * ⚠ Does not support 32-bit CPUs and is incompatible with mingw. ## * ⚠ May cause conflicts if other crates use different zlib versions. -cloudflare_zlib = ["any_zlib", "cloudflare-zlib-sys"] +cloudflare_zlib = ["any_c_zlib", "cloudflare-zlib-sys"] ## Deprecated alias for `rust_backend`, provided for backwards compatibility. ## Use `rust_backend` instead. @@ -100,10 +100,15 @@ miniz-sys = ["rust_backend"] #! They are documented here to aid with maintenance. ## **Internal:** Marker feature indicating that any zlib-based C backend is enabled. -## This is automatically enabled by `zlib`, `zlib-ng`, `zlib-ng-compat`, or `cloudflare_zlib`. +## This is automatically enabled by `zlib-rs`, `zlib`, `zlib-ng`, `zlib-ng-compat`, and `cloudflare_zlib`. ## Do not enable this feature directly; instead, choose a specific backend feature. any_zlib = ["any_impl"] +## **Internal:** Marker feature indicating that any C based fully zlib compatible backend is enabled. +## This is automatically enabled by `zlib`, `zlib-ng`, `zlib-ng-compat`, and `cloudflare_zlib`. +## Do not enable this feature directly; instead, choose a specific backend feature. +any_c_zlib = ["any_zlib"] + ## **Internal:** Marker feature indicating that any compression backend is enabled. ## This is automatically enabled by all backend features to ensure at least one implementation is available. ## Do not enable this feature directly; instead, choose a specific backend feature. diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index 39c1c17a..76e02b44 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -60,37 +60,25 @@ pub trait DeflateBackend: Backend { } // Default to Rust implementation unless explicitly opted in to a different backend. -#[cfg(feature = "any_zlib")] +#[cfg(feature = "any_c_zlib")] mod c; -#[cfg(feature = "any_zlib")] +#[cfg(feature = "any_c_zlib")] pub use self::c::*; -// Prefer zlib-rs when both Rust backends are enabled to avoid duplicate exports. -#[cfg(all(not(feature = "any_zlib"), feature = "zlib-rs"))] +// Only bring in `zlib-rs` if there is no C-based backend. +#[cfg(all(not(feature = "any_c_zlib"), feature = "zlib-rs"))] mod zlib_rs; -#[cfg(all(not(feature = "any_zlib"), feature = "zlib-rs"))] +#[cfg(all(not(feature = "any_c_zlib"), feature = "zlib-rs"))] pub use self::zlib_rs::*; -// Fallback to miniz_oxide when zlib-rs is not selected. -#[cfg(all( - not(feature = "any_zlib"), - not(feature = "zlib-rs"), - feature = "miniz_oxide" -))] +// Use miniz_oxide when no fully compliant zlib is selected. +#[cfg(all(not(feature = "any_zlib"), feature = "miniz_oxide"))] mod miniz_oxide; -#[cfg(all( - not(feature = "any_zlib"), - not(feature = "zlib-rs"), - feature = "miniz_oxide" -))] +#[cfg(all(not(feature = "any_zlib"), feature = "miniz_oxide"))] pub use self::miniz_oxide::*; // If no backend is enabled, fail fast with a clear error message. -#[cfg(all( - not(feature = "any_zlib"), - not(feature = "zlib-rs"), - not(feature = "miniz_oxide") -))] +#[cfg(not(feature = "any_impl"))] compile_error!("No compression backend selected; enable one of `zlib`, `zlib-ng`, `zlib-rs`, or the default `rust_backend` feature."); impl std::fmt::Debug for ErrorMessage { diff --git a/src/ffi/zlib_rs.rs b/src/ffi/zlib_rs.rs index 877f73cb..f5524bbe 100644 --- a/src/ffi/zlib_rs.rs +++ b/src/ffi/zlib_rs.rs @@ -242,12 +242,11 @@ impl Deflate { 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!(), + 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 4f932515..221a75ed 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -212,7 +212,7 @@ impl Compress { /// # Panics /// /// If `window_bits` does not fall into the range 9 ..= 15, - /// `new_with_window_bits` will panic. + /// this function will panic. #[cfg(feature = "any_zlib")] pub fn new_with_window_bits( level: Compression, @@ -239,7 +239,7 @@ impl Compress { /// # Panics /// /// If `window_bits` does not fall into the range 9 ..= 15, - /// `new_with_window_bits` will panic. + /// this function will panic. #[cfg(feature = "any_zlib")] pub fn new_gzip(level: Compression, window_bits: u8) -> Compress { assert!( @@ -266,7 +266,7 @@ impl Compress { /// Specifies the compression dictionary to use. /// /// Returns the Adler-32 checksum of the dictionary. - #[cfg(feature = "any_zlib")] + #[cfg(feature = "any_c_zlib")] pub fn set_dictionary(&mut self, dictionary: &[u8]) -> Result { // 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 @@ -289,7 +289,7 @@ 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"))] + #[cfg(all(not(feature = "any_c_zlib"), feature = "zlib-rs"))] pub fn set_dictionary(&mut self, dictionary: &[u8]) -> Result { self.inner.set_dictionary(dictionary) } @@ -311,14 +311,14 @@ 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(any(feature = "any_zlib", feature = "zlib-rs"))] + #[cfg(feature = "any_zlib")] pub fn set_level(&mut self, level: Compression) -> Result<(), CompressError> { - #[cfg(all(not(feature = "any_zlib"), feature = "zlib-rs"))] + #[cfg(all(not(feature = "any_c_zlib"), feature = "zlib-rs"))] { self.inner.set_level(level) } - #[cfg(feature = "any_zlib")] + #[cfg(feature = "any_c_zlib")] { use std::os::raw::c_int; // SAFETY: The field `inner` must always be accessed as a raw pointer, @@ -415,7 +415,7 @@ impl Decompress { /// # Panics /// /// If `window_bits` does not fall into the range 9 ..= 15, - /// `new_with_window_bits` will panic. + /// this function will panic. #[cfg(feature = "any_zlib")] pub fn new_with_window_bits(zlib_header: bool, window_bits: u8) -> Decompress { assert!( @@ -435,7 +435,7 @@ impl Decompress { /// # Panics /// /// If `window_bits` does not fall into the range 9 ..= 15, - /// `new_with_window_bits` will panic. + /// this function will panic. #[cfg(feature = "any_zlib")] pub fn new_gzip(window_bits: u8) -> Decompress { assert!( @@ -536,7 +536,7 @@ impl Decompress { } /// Specifies the decompression dictionary to use. - #[cfg(feature = "any_zlib")] + #[cfg(feature = "any_c_zlib")] pub fn set_dictionary(&mut self, dictionary: &[u8]) -> Result { // 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 @@ -558,7 +558,7 @@ impl Decompress { } /// Specifies the decompression dictionary to use. - #[cfg(all(not(feature = "any_zlib"), feature = "zlib-rs"))] + #[cfg(all(not(feature = "any_c_zlib"), feature = "zlib-rs"))] pub fn set_dictionary(&mut self, dictionary: &[u8]) -> Result { self.inner.set_dictionary(dictionary) } @@ -723,86 +723,6 @@ 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")] #[test] fn test_gzip_flate() { @@ -829,7 +749,7 @@ mod tests { assert_eq!(&decoded[..decoder.total_out() as usize], string); } - #[cfg(any(feature = "any_zlib", feature = "zlib-rs"))] + #[cfg(feature = "any_zlib")] #[test] fn test_error_message() { let mut decoder = Decompress::new(false); @@ -837,7 +757,7 @@ mod tests { let garbage = b"xbvxzi"; let err = decoder - .decompress(&*garbage, &mut decoded, FlushDecompress::Finish) + .decompress(garbage, &mut decoded, FlushDecompress::Finish) .unwrap_err(); assert_eq!(err.message(), Some("invalid stored block lengths")); diff --git a/tests/capabilities.rs b/tests/capabilities.rs new file mode 100644 index 00000000..0972555a --- /dev/null +++ b/tests/capabilities.rs @@ -0,0 +1,240 @@ +//! Validate that certain feature-gated functionality is still available. +#[cfg(feature = "any_zlib")] +use flate2::{Compress, Compression, Decompress, FlushCompress, FlushDecompress}; + +// Unsupported for `miniz_oxide`. +#[cfg(feature = "any_zlib")] +#[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); +} + +// Unsupported for `miniz_oxide`. +#[cfg(feature = "any_zlib")] +#[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 + ); + } +} + +// Unsupported for `miniz_oxide`. +#[cfg(feature = "any_zlib")] +#[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); +} + +// Unsupported for `miniz_oxide`. +#[cfg(feature = "any_zlib")] +#[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); +} + +// Unsupported for `miniz_oxide`. +#[cfg(feature = "any_zlib")] +#[test] +#[should_panic(expected = "window_bits must be within 9 ..= 15")] +fn compress_new_gzip_invalid_low() { + let _ = Compress::new_gzip(Compression::default(), 8); +} + +// Unsupported for `miniz_oxide`. +#[cfg(feature = "any_zlib")] +#[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(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); +} + +// Unsupported for `miniz_oxide`. +#[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); +} + +// Unsupported for `miniz_oxide`. +#[cfg(feature = "any_zlib")] +#[test] +fn compression_levels_are_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(), + ); +} + +// Unsupported for `miniz_oxide`. +#[cfg(feature = "any_zlib")] +#[test] +fn set_level_is_effective() { + let input = b"hello hello hello hello hello hello hello hello"; + let no_compression = Compression::none(); + let best_compression = Compression::best(); + + // Compress with no compression + let mut encoded_none = Vec::new(); + let mut compress = Compress::new(best_compression, true); + compress.set_level(no_compression).unwrap(); + compress + .compress_vec(input, &mut encoded_none, FlushCompress::Finish) + .unwrap(); + + // Compress with best compression + let mut encoded_best = Vec::new(); + let mut compress = Compress::new(no_compression, true); + compress.set_level(best_compression).unwrap(); + compress + .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(), + ); +}