1use anyhow::{Result, anyhow};
28
29#[cfg(not(feature = "openzl"))]
32const NO_CODEC: &str = "znippy-common was built without the `openzl` feature, so the OpenZL codec \
33 is not linked: compressed blobs cannot be read or written. Stored (uncompressed) \
34 entries, the Arrow index and the metadata sub-index are unaffected. Enable the \
35 `openzl` feature to link the codec (note: it builds the vendored OpenZL C sources \
36 and needs a C compiler at compile time).";
37
38pub struct CompressCtx {
41 #[cfg(feature = "openzl")]
42 cctx: openzl_sys_rs::ZlCCtx,
43}
44
45unsafe impl Send for CompressCtx {}
46
47#[cfg(not(feature = "openzl"))]
48impl CompressCtx {
49 pub fn new(_compression_level: i32) -> Result<Self> {
50 Err(anyhow!(NO_CODEC))
51 }
52
53 pub fn compress(&mut self, _input: &[u8]) -> Result<Vec<u8>> {
54 Err(anyhow!(NO_CODEC))
55 }
56
57 pub fn compress_into(&mut self, _input: &[u8], _out: &mut Vec<u8>) -> Result<usize> {
58 Err(anyhow!(NO_CODEC))
59 }
60}
61
62#[cfg(not(feature = "openzl"))]
63pub fn decompress_frame(_compressed: &[u8]) -> Result<Vec<u8>> {
64 Err(anyhow!(NO_CODEC))
65}
66
67#[cfg(not(feature = "openzl"))]
68pub fn decompress_into(_compressed: &[u8], _out: &mut Vec<u8>) -> Result<usize> {
69 Err(anyhow!(NO_CODEC))
70}
71
72#[cfg(feature = "openzl")]
73impl CompressCtx {
74 pub fn new(compression_level: i32) -> Result<Self> {
75 use openzl_sys_rs::*;
76 let mut cctx = ZlCCtx::new().ok_or_else(|| anyhow!("ZL_CCtx_create failed"))?;
77 let version = unsafe { ZL_getDefaultEncodingVersion() } as i32;
78 cctx.set_parameter(ZL_CParam_ZL_CParam_stickyParameters, 1)
80 .map_err(|e| anyhow!(e))?;
81 cctx.set_parameter(ZL_CParam_ZL_CParam_formatVersion, version)
82 .map_err(|e| anyhow!(e))?;
83 cctx.set_parameter(ZL_CParam_ZL_CParam_compressionLevel, compression_level)
84 .map_err(|e| anyhow!(e))?;
85 Ok(Self { cctx })
86 }
87
88 pub fn compress(&mut self, input: &[u8]) -> Result<Vec<u8>> {
89 use openzl_sys_rs::zl_compress_bound;
90 let bound = zl_compress_bound(input.len());
91 let mut output = vec![0u8; bound];
92 let compressed_size = self.cctx.compress(&mut output, input)
93 .map_err(|e| anyhow!(e))?;
94 output.truncate(compressed_size);
95 Ok(output)
96 }
97
98 pub fn compress_into(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<usize> {
102 use openzl_sys_rs::zl_compress_bound;
103 let bound = zl_compress_bound(input.len());
104 if out.len() < bound {
105 out.resize(bound, 0);
106 }
107 let compressed_size = self
108 .cctx
109 .compress(out.as_mut_slice(), input)
110 .map_err(|e| anyhow!(e))?;
111 out.truncate(compressed_size);
112 Ok(compressed_size)
113 }
114}
115
116#[cfg(feature = "openzl")]
117pub fn decompress_frame(compressed: &[u8]) -> Result<Vec<u8>> {
118 let mut out = Vec::new();
119 decompress_into(compressed, &mut out)?;
120 Ok(out)
121}
122
123#[cfg(feature = "openzl")]
127pub fn decompress_into(compressed: &[u8], out: &mut Vec<u8>) -> Result<usize> {
128 use openzl_sys_rs::*;
129 let decompressed_size = zl_get_decompressed_size(compressed)
130 .map_err(|e| anyhow!("OpenZL getDecompressedSize: {}", e))?;
131 if out.len() < decompressed_size {
132 out.resize(decompressed_size, 0);
133 }
134 let written = zl_decompress(&mut out[..decompressed_size], compressed)
135 .map_err(|e| anyhow!("OpenZL decompress: {}", e))?;
136 out.truncate(written);
137 Ok(written)
138}
139
140#[cfg(all(test, not(feature = "openzl")))]
146mod no_codec_tests {
147 use super::*;
148
149 fn assert_names_the_feature(err: anyhow::Error) {
150 let msg = err.to_string();
151 assert!(
152 msg.contains("`openzl` feature"),
153 "error must name the feature that is missing, got: {msg}"
154 );
155 }
156
157 #[test]
158 fn compress_ctx_refuses_to_exist() {
159 assert_names_the_feature(
160 CompressCtx::new(3).err().expect("CompressCtx::new must fail with no codec linked"),
161 );
162 }
163
164 #[test]
165 fn decompress_refuses_and_does_not_touch_the_buffer() {
166 let frame = [0x5Bu8, 0x2A, 0x4D, 0x18, 0x00, 0x00, 0x00, 0x00];
169 let mut out = vec![0xAAu8; 4];
170 assert_names_the_feature(
171 decompress_into(&frame, &mut out).err().expect("decompress_into must fail"),
172 );
173 assert_eq!(out, vec![0xAAu8; 4], "buffer must be untouched on refusal");
174 assert_names_the_feature(
175 decompress_frame(&frame).err().expect("decompress_frame must fail"),
176 );
177 }
178}
179
180#[cfg(all(test, feature = "openzl"))]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_roundtrip() {
186 let mut ctx = CompressCtx::new(3).unwrap();
187 let input = b"Hello world! This is a test of compression roundtrip. Repeated data helps compression. Repeated data helps compression. Repeated data helps compression.";
188 let compressed = ctx.compress(input).unwrap();
189 println!("Compressed {} -> {} bytes", input.len(), compressed.len());
190 let decompressed = decompress_frame(&compressed).unwrap();
191 assert_eq!(&decompressed[..], &input[..]);
192 }
193
194 #[test]
195 fn test_multi_compress_same_ctx() {
196 let mut ctx = CompressCtx::new(3).unwrap();
197 for i in 0..10 {
198 let input: Vec<u8> = (0..4096).map(|x| ((x + i) % 251) as u8).collect();
199 let compressed = ctx.compress(&input).unwrap();
200 let decompressed = decompress_frame(&compressed).unwrap();
201 assert_eq!(decompressed, input, "Failed at iteration {}", i);
202 }
203 println!("10 sequential compress calls OK");
204 }
205
206 #[test]
207 fn test_parallel_contexts() {
208 let handles: Vec<_> = (0..8).map(|t| {
209 std::thread::spawn(move || {
210 let mut ctx = CompressCtx::new(3).unwrap();
211 for i in 0..5 {
212 let input: Vec<u8> = (0..8192).map(|x| ((x + i + t*100) % 251) as u8).collect();
213 let compressed = ctx.compress(&input).unwrap();
214 let decompressed = decompress_frame(&compressed).unwrap();
215 assert_eq!(decompressed, input);
216 }
217 })
218 }).collect();
219 for h in handles {
220 h.join().unwrap();
221 }
222 println!("8 parallel contexts x 5 calls each OK");
223 }
224}