Skip to main content

rustyhdf5_filters/
lib.rs

1//! Filter and compression pipeline for HDF5.
2//!
3//! Provides deflate (zlib) decompression/compression with multiple backend options:
4//!
5//! - **Default**: `miniz_oxide` (pure Rust, no C dependencies)
6//! - **`fast-deflate` feature**: `zlib-ng` via flate2 (~2-3x faster, matches C HDF5)
7//! - **`apple-compression` feature**: Apple Compression Framework on macOS
8//!   (hardware-accelerated on Apple Silicon)
9//!
10//! Backend priority: apple-compression > zlib-ng > miniz_oxide.
11
12pub mod fast_deflate;
13
14/// Decompress zlib-compressed data.
15///
16/// Uses the fastest available backend. When `max_output_size` > 0,
17/// pre-allocates the output buffer for streaming decompression.
18pub fn deflate_decompress(data: &[u8], max_output_size: usize) -> Result<Vec<u8>, String> {
19    fast_deflate::decompress(data, max_output_size)
20}
21
22/// Compress data with zlib.
23///
24/// Uses the fastest available backend.
25pub fn deflate_compress(data: &[u8], level: u32) -> Result<Vec<u8>, String> {
26    fast_deflate::compress(data, level)
27}
28
29/// Decompress zlib data using the pure-Rust miniz_oxide backend.
30/// Always available regardless of feature flags, for comparison/testing.
31pub fn deflate_decompress_miniz(data: &[u8]) -> Result<Vec<u8>, String> {
32    let result = miniz_oxide::inflate::decompress_to_vec_zlib(data)
33        .map_err(|e| format!("miniz_oxide decompress error: {e:?}"))?;
34    Ok(result)
35}
36
37/// Compress data using the pure-Rust miniz_oxide backend.
38/// Always available regardless of feature flags, for comparison/testing.
39pub fn deflate_compress_miniz(data: &[u8], level: u32) -> Result<Vec<u8>, String> {
40    let level = level.min(10) as u8;
41    let result = miniz_oxide::deflate::compress_to_vec_zlib(data, level);
42    Ok(result)
43}
44
45/// Returns the name of the currently active deflate backend.
46pub fn deflate_backend() -> &'static str {
47    fast_deflate::active_backend()
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn compress_decompress_roundtrip() {
56        let data: Vec<u8> = (0..1000).map(|i| (i % 256) as u8).collect();
57        let compressed = deflate_compress(&data, 6).unwrap();
58        let decompressed = deflate_decompress(&compressed, data.len()).unwrap();
59        assert_eq!(decompressed, data);
60    }
61
62    #[test]
63    fn decompress_python_zlib() {
64        // python3 -c "import zlib; print(list(zlib.compress(bytes(range(10)), 6)))"
65        let compressed: Vec<u8> = vec![
66            120, 156, 99, 96, 100, 98, 102, 97, 101, 99, 231, 224, 4, 0, 0, 175, 0, 46,
67        ];
68        let decompressed = deflate_decompress(&compressed, 10).unwrap();
69        assert_eq!(decompressed, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
70    }
71
72    #[test]
73    fn miniz_always_available() {
74        let data = vec![42u8; 100];
75        let compressed = deflate_compress_miniz(&data, 6).unwrap();
76        let decompressed = deflate_decompress_miniz(&compressed).unwrap();
77        assert_eq!(decompressed, data);
78    }
79
80    #[test]
81    fn cross_backend_compatibility() {
82        // Compress with miniz, decompress with current (possibly zlib-ng) backend
83        let data: Vec<u8> = (0..500).map(|i| (i * 7 % 256) as u8).collect();
84        let compressed = deflate_compress_miniz(&data, 6).unwrap();
85        let decompressed = deflate_decompress(&compressed, data.len()).unwrap();
86        assert_eq!(decompressed, data);
87    }
88
89    #[test]
90    fn cross_backend_reverse() {
91        // Compress with current backend, decompress with miniz
92        let data: Vec<u8> = (0..500).map(|i| (i * 13 % 256) as u8).collect();
93        let compressed = deflate_compress(&data, 6).unwrap();
94        let decompressed = deflate_decompress_miniz(&compressed).unwrap();
95        assert_eq!(decompressed, data);
96    }
97
98    #[test]
99    fn empty_data() {
100        let compressed = deflate_compress(&[], 6).unwrap();
101        let decompressed = deflate_decompress(&compressed, 0).unwrap();
102        assert!(decompressed.is_empty());
103    }
104
105    #[test]
106    fn large_data_roundtrip() {
107        let data: Vec<u8> = (0..100_000).map(|i| (i % 256) as u8).collect();
108        let compressed = deflate_compress(&data, 6).unwrap();
109        assert!(compressed.len() < data.len()); // should actually compress
110        let decompressed = deflate_decompress(&compressed, data.len()).unwrap();
111        assert_eq!(decompressed, data);
112    }
113
114    #[test]
115    fn backend_reports_name() {
116        let name = deflate_backend();
117        assert!(
118            ["miniz_oxide", "zlib-ng", "apple-compression"].contains(&name),
119            "unexpected backend: {name}"
120        );
121    }
122
123    #[test]
124    fn all_backends_produce_identical_output() {
125        let data: Vec<u8> = (0..10_000).map(|i| (i * 31 % 256) as u8).collect();
126
127        // Compress with current backend
128        let compressed_current = deflate_compress(&data, 6).unwrap();
129        // Compress with miniz
130        let compressed_miniz = deflate_compress_miniz(&data, 6).unwrap();
131
132        // Both should decompress to the same data (even if compressed bytes differ)
133        let dec_current = deflate_decompress(&compressed_current, data.len()).unwrap();
134        let dec_miniz = deflate_decompress_miniz(&compressed_miniz).unwrap();
135        let dec_cross = deflate_decompress_miniz(&compressed_current).unwrap();
136        let dec_cross2 = deflate_decompress(&compressed_miniz, data.len()).unwrap();
137
138        assert_eq!(dec_current, data);
139        assert_eq!(dec_miniz, data);
140        assert_eq!(dec_cross, data);
141        assert_eq!(dec_cross2, data);
142    }
143}