1use crate::error::{Result, err};
12
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
15pub enum Compression {
16 None,
18 Zlib,
20 Bzp2,
22 Lz4,
24}
25
26impl Compression {
27 pub fn from_name(name: &str) -> Result<Self> {
31 match name {
32 "" => Ok(Compression::None),
33 "zlib" => Ok(Compression::Zlib),
34 "bzp2" => Ok(Compression::Bzp2),
35 "lz4" => Ok(Compression::Lz4),
36 other => Err(err!(UnknownCompression, "unknown compression type: {other}")),
37 }
38 }
39
40 pub fn name(self) -> &'static str {
42 match self {
43 Compression::None => "",
44 Compression::Zlib => "zlib",
45 Compression::Bzp2 => "bzp2",
46 Compression::Lz4 => "lz4",
47 }
48 }
49
50 pub fn is_available(self) -> bool {
52 match self {
53 Compression::None => true,
54 Compression::Zlib => cfg!(feature = "zlib"),
55 Compression::Bzp2 => cfg!(feature = "bzp2"),
56 Compression::Lz4 => cfg!(feature = "lz4"),
57 }
58 }
59
60 pub fn decompress(self, data: &[u8], expected_size: usize) -> Result<Vec<u8>> {
62 match self {
63 Compression::None => Ok(data.to_vec()),
64 Compression::Zlib => zlib::decompress(data, expected_size),
65 Compression::Bzp2 => bzp2::decompress(data, expected_size),
66 Compression::Lz4 => lz4::decompress(data, expected_size),
67 }
68 }
69
70 pub fn compress(self, data: &[u8]) -> Result<Vec<u8>> {
72 match self {
73 Compression::None => Ok(data.to_vec()),
74 Compression::Zlib => zlib::compress(data),
75 Compression::Bzp2 => bzp2::compress(data),
76 Compression::Lz4 => lz4::compress(data),
77 }
78 }
79}
80
81pub fn available() -> Vec<Compression> {
83 [Compression::Zlib, Compression::Bzp2, Compression::Lz4]
84 .into_iter()
85 .filter(|c| c.is_available())
86 .collect()
87}
88
89const MAX_EXPANSION_RATIO: usize = 4096;
96
97fn check_expected_size(compressed_len: usize, expected: usize) -> Result<()> {
98 let ceiling = compressed_len.saturating_mul(MAX_EXPANSION_RATIO).max(1 << 20);
99 if expected > ceiling {
100 return Err(err!(
101 CompressionFailed,
102 "block claims to decompress {expected} bytes from {compressed_len}, \
103 beyond the {MAX_EXPANSION_RATIO}x sanity limit"
104 ));
105 }
106 Ok(())
107}
108
109mod zlib {
110 use super::*;
111
112 #[cfg(feature = "zlib")]
113 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
114 use std::io::Read;
115 check_expected_size(data.len(), expected)?;
116 let mut out = Vec::with_capacity(expected);
117 flate2::read::ZlibDecoder::new(data)
118 .read_to_end(&mut out)
119 .map_err(|e| err!(CompressionFailed, "zlib decompression failed: {e}"))?;
120 Ok(out)
121 }
122
123 #[cfg(feature = "zlib")]
124 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
125 use std::io::Write;
126 let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
127 enc.write_all(data)
128 .and_then(|()| enc.finish())
129 .map_err(|e| err!(CompressionFailed, "zlib compression failed: {e}"))
130 }
131
132 #[cfg(not(feature = "zlib"))]
133 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
134 Err(err!(UnknownCompression, "zlib support was not compiled in"))
135 }
136
137 #[cfg(not(feature = "zlib"))]
138 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
139 Err(err!(UnknownCompression, "zlib support was not compiled in"))
140 }
141}
142
143mod bzp2 {
144 use super::*;
145
146 #[cfg(feature = "bzp2")]
147 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
148 use std::io::Read;
149 check_expected_size(data.len(), expected)?;
150 let mut out = Vec::with_capacity(expected);
151 bzip2::read::BzDecoder::new(data)
152 .read_to_end(&mut out)
153 .map_err(|e| err!(CompressionFailed, "bzip2 decompression failed: {e}"))?;
154 Ok(out)
155 }
156
157 #[cfg(feature = "bzp2")]
158 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
159 use std::io::Write;
160 let mut enc = bzip2::write::BzEncoder::new(Vec::new(), bzip2::Compression::best());
162 enc.write_all(data)
163 .and_then(|()| enc.finish())
164 .map_err(|e| err!(CompressionFailed, "bzip2 compression failed: {e}"))
165 }
166
167 #[cfg(not(feature = "bzp2"))]
168 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
169 Err(err!(UnknownCompression, "bzip2 support was not compiled in"))
170 }
171
172 #[cfg(not(feature = "bzp2"))]
173 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
174 Err(err!(UnknownCompression, "bzip2 support was not compiled in"))
175 }
176}
177
178pub mod lz4 {
194 use super::*;
195
196 pub const CHUNK_SIZE: usize = 1 << 22;
198
199 pub const CHUNK_HEADER_SIZE: usize = 8;
202
203 #[cfg(feature = "lz4")]
204 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
205 check_expected_size(data.len(), expected)?;
206 let mut out = Vec::with_capacity(expected);
207 let mut pos = 0usize;
208
209 while pos < data.len() {
210 if pos + 4 > data.len() {
211 return Err(err!(
212 CompressionFailed,
213 "lz4 stream truncated in a chunk length at offset {pos}"
214 ));
215 }
216 let framed_len =
217 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
218 as usize;
219 pos += 4;
220
221 if framed_len < 4 || pos + framed_len > data.len() {
222 return Err(err!(
223 CompressionFailed,
224 "lz4 chunk at offset {pos} claims {framed_len} bytes, \
225 past the end of the {} byte stream",
226 data.len()
227 ));
228 }
229
230 let chunk = &data[pos..pos + framed_len];
234 let decoded = lz4_flex::block::decompress_size_prepended(chunk)
235 .map_err(|e| err!(CompressionFailed, "lz4 decompression failed: {e}"))?;
236 out.extend_from_slice(&decoded);
237 pos += framed_len;
238 }
239 Ok(out)
240 }
241
242 #[cfg(feature = "lz4")]
243 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
244 let mut out = Vec::new();
245 for chunk in data.chunks(CHUNK_SIZE) {
247 let framed = lz4_flex::block::compress_prepend_size(chunk);
248 let len = u32::try_from(framed.len())
249 .map_err(|_| err!(CompressionFailed, "lz4 chunk too large to frame"))?;
250 out.extend_from_slice(&len.to_be_bytes());
251 out.extend_from_slice(&framed);
252 }
253 Ok(out)
254 }
255
256 #[cfg(not(feature = "lz4"))]
257 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
258 Err(err!(UnknownCompression, "lz4 support was not compiled in"))
259 }
260
261 #[cfg(not(feature = "lz4"))]
262 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
263 Err(err!(UnknownCompression, "lz4 support was not compiled in"))
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::error::ErrorCode;
271
272 fn counter_payload() -> Vec<u8> {
275 let mut v = Vec::new();
276 for i in 0..10_000u32 {
277 v.extend_from_slice(&i.to_le_bytes());
278 }
279 v
280 }
281
282 fn compressible_payload() -> Vec<u8> {
284 let mut v = Vec::new();
285 for i in 0..10_000u32 {
286 v.extend_from_slice(&(i % 16).to_le_bytes());
287 }
288 v
289 }
290
291 #[test]
292 fn names_round_trip() {
293 for c in [Compression::None, Compression::Zlib, Compression::Bzp2, Compression::Lz4] {
294 assert_eq!(Compression::from_name(c.name()).unwrap(), c);
295 assert!(c.name().len() <= 4, "{:?} name too long", c);
297 }
298 }
299
300 #[test]
301 fn unknown_names_are_rejected() {
302 let e = Compression::from_name("zstd").unwrap_err();
303 assert_eq!(e.code(), ErrorCode::UnknownCompression);
304 }
305
306 #[test]
307 fn round_trips_through_every_method() {
308 for data in [counter_payload(), compressible_payload()] {
309 for c in available() {
310 let packed = c.compress(&data).unwrap_or_else(|e| panic!("{:?}: {e}", c));
311 let unpacked =
312 c.decompress(&packed, data.len()).unwrap_or_else(|e| panic!("{:?}: {e}", c));
313 assert_eq!(unpacked, data, "{:?} did not round trip", c);
314 }
315 }
316 }
317
318 #[test]
319 fn every_method_shrinks_redundant_data() {
320 let data = compressible_payload();
324 for c in available() {
325 let packed = c.compress(&data).unwrap();
326 assert!(
327 packed.len() < data.len(),
328 "{:?} grew {} bytes to {}",
329 c,
330 data.len(),
331 packed.len()
332 );
333 }
334 }
335
336 #[test]
337 fn round_trips_empty_and_tiny_inputs() {
338 for c in available() {
339 for data in [vec![], vec![0u8], vec![7u8; 3]] {
340 let packed = c.compress(&data).unwrap();
341 let unpacked = c.decompress(&packed, data.len()).unwrap();
342 assert_eq!(unpacked, data, "{:?} failed on {} bytes", c, data.len());
343 }
344 }
345 }
346
347 #[test]
348 fn none_is_a_passthrough() {
349 let data = b"unchanged".to_vec();
350 assert_eq!(Compression::None.compress(&data).unwrap(), data);
351 assert_eq!(Compression::None.decompress(&data, data.len()).unwrap(), data);
352 }
353
354 #[cfg(feature = "lz4")]
355 #[test]
356 fn lz4_uses_the_asdf_chunk_framing() {
357 let data = vec![0xABu8; 1000];
360 let packed = lz4::compress(&data).unwrap();
361
362 assert!(packed.len() > lz4::CHUNK_HEADER_SIZE);
363 let framed_len = u32::from_be_bytes([packed[0], packed[1], packed[2], packed[3]]) as usize;
364 assert_eq!(
365 framed_len,
366 packed.len() - 4,
367 "the big-endian length must cover the rest of the chunk"
368 );
369
370 let decompressed_size =
371 u32::from_le_bytes([packed[4], packed[5], packed[6], packed[7]]) as usize;
372 assert_eq!(
373 decompressed_size,
374 data.len(),
375 "the little-endian header must carry the decompressed size"
376 );
377 }
378
379 #[cfg(feature = "lz4")]
380 #[test]
381 fn lz4_splits_large_inputs_into_chunks() {
382 let data = vec![0x5Au8; lz4::CHUNK_SIZE + 1024];
384 let packed = lz4::compress(&data).unwrap();
385 let unpacked = lz4::decompress(&packed, data.len()).unwrap();
386 assert_eq!(unpacked.len(), data.len());
387 assert_eq!(unpacked, data);
388
389 let mut pos = 0;
391 let mut frames = 0;
392 while pos < packed.len() {
393 let len = u32::from_be_bytes([
394 packed[pos],
395 packed[pos + 1],
396 packed[pos + 2],
397 packed[pos + 3],
398 ]) as usize;
399 pos += 4 + len;
400 frames += 1;
401 }
402 assert_eq!(frames, 2, "a 4 MiB + 1 KiB input should make two chunks");
403 }
404
405 #[cfg(feature = "lz4")]
406 #[test]
407 fn truncated_lz4_streams_are_rejected() {
408 let data = vec![0x11u8; 5000];
409 let packed = lz4::compress(&data).unwrap();
410
411 let e = lz4::decompress(&packed[..packed.len() - 10], data.len()).unwrap_err();
413 assert_eq!(e.code(), ErrorCode::CompressionFailed);
414
415 let e = lz4::decompress(&packed[..2], data.len()).unwrap_err();
417 assert_eq!(e.code(), ErrorCode::CompressionFailed);
418 }
419
420 #[test]
421 fn corrupt_input_is_an_error_not_a_panic() {
422 let garbage = vec![0xFFu8; 64];
423 for c in available() {
424 let r = c.decompress(&garbage, 1024);
425 if let Ok(v) = r {
428 assert!(v.len() <= 1 << 20);
429 }
430 }
431 }
432
433 #[test]
434 fn absurd_expected_sizes_are_refused() {
435 let small = vec![0u8; 16];
438 for c in available() {
439 let e = c.decompress(&small, usize::MAX / 2);
440 assert!(e.is_err(), "{:?} accepted an absurd size", c);
441 }
442 }
443
444 #[test]
445 fn available_reports_compiled_features() {
446 let names: Vec<_> = available().iter().map(|c| c.name()).collect();
447 #[cfg(feature = "zlib")]
449 assert!(names.contains(&"zlib"));
450 #[cfg(feature = "bzp2")]
451 assert!(names.contains(&"bzp2"));
452 let _ = names;
453 }
454}