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
109fn read_bounded(mut reader: impl std::io::Read, expected: usize, what: &str) -> Result<Vec<u8>> {
123 use std::io::Read as _;
124
125 let mut out = Vec::new();
126 let read = (&mut reader)
129 .take(expected as u64 + 1)
130 .read_to_end(&mut out)
131 .map_err(|e| err!(CompressionFailed, "{what} decompression failed: {e}"))?;
132
133 if read > expected {
134 return Err(err!(
135 CompressionFailed,
136 "{what} stream expands past the {expected} bytes the block header declares"
137 ));
138 }
139 Ok(out)
140}
141
142mod zlib {
143 use super::*;
144
145 #[cfg(feature = "zlib")]
146 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
147 check_expected_size(data.len(), expected)?;
148 read_bounded(flate2::read::ZlibDecoder::new(data), expected, "zlib")
149 }
150
151 #[cfg(feature = "zlib")]
152 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
153 use std::io::Write;
154 let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
155 enc.write_all(data)
156 .and_then(|()| enc.finish())
157 .map_err(|e| err!(CompressionFailed, "zlib compression failed: {e}"))
158 }
159
160 #[cfg(not(feature = "zlib"))]
161 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
162 Err(err!(UnknownCompression, "zlib support was not compiled in"))
163 }
164
165 #[cfg(not(feature = "zlib"))]
166 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
167 Err(err!(UnknownCompression, "zlib support was not compiled in"))
168 }
169}
170
171mod bzp2 {
172 use super::*;
173
174 #[cfg(feature = "bzp2")]
175 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
176 check_expected_size(data.len(), expected)?;
177 read_bounded(bzip2::read::BzDecoder::new(data), expected, "bzip2")
178 }
179
180 #[cfg(feature = "bzp2")]
181 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
182 use std::io::Write;
183 let mut enc = bzip2::write::BzEncoder::new(Vec::new(), bzip2::Compression::best());
185 enc.write_all(data)
186 .and_then(|()| enc.finish())
187 .map_err(|e| err!(CompressionFailed, "bzip2 compression failed: {e}"))
188 }
189
190 #[cfg(not(feature = "bzp2"))]
191 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
192 Err(err!(UnknownCompression, "bzip2 support was not compiled in"))
193 }
194
195 #[cfg(not(feature = "bzp2"))]
196 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
197 Err(err!(UnknownCompression, "bzip2 support was not compiled in"))
198 }
199}
200
201pub mod lz4 {
217 use super::*;
218
219 pub const CHUNK_SIZE: usize = 1 << 22;
221
222 pub const CHUNK_HEADER_SIZE: usize = 8;
225
226 #[cfg(feature = "lz4")]
227 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
228 check_expected_size(data.len(), expected)?;
229 let mut out = Vec::new();
230 let mut pos = 0usize;
231
232 while pos < data.len() {
233 if pos + 4 > data.len() {
234 return Err(err!(
235 CompressionFailed,
236 "lz4 stream truncated in a chunk length at offset {pos}"
237 ));
238 }
239 let framed_len =
240 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
241 as usize;
242 pos += 4;
243
244 if framed_len < 4 || pos + framed_len > data.len() {
245 return Err(err!(
246 CompressionFailed,
247 "lz4 chunk at offset {pos} claims {framed_len} bytes, \
248 past the end of the {} byte stream",
249 data.len()
250 ));
251 }
252
253 let chunk = &data[pos..pos + framed_len];
257 let decoded = lz4_flex::block::decompress_size_prepended(chunk)
258 .map_err(|e| err!(CompressionFailed, "lz4 decompression failed: {e}"))?;
259 if out.len() + decoded.len() > expected {
262 return Err(err!(
263 CompressionFailed,
264 "lz4 stream expands past the {expected} bytes the block header declares"
265 ));
266 }
267 out.extend_from_slice(&decoded);
268 pos += framed_len;
269 }
270 Ok(out)
271 }
272
273 #[cfg(feature = "lz4")]
274 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
275 let mut out = Vec::new();
276 for chunk in data.chunks(CHUNK_SIZE) {
278 let framed = lz4_flex::block::compress_prepend_size(chunk);
279 let len = u32::try_from(framed.len())
280 .map_err(|_| err!(CompressionFailed, "lz4 chunk too large to frame"))?;
281 out.extend_from_slice(&len.to_be_bytes());
282 out.extend_from_slice(&framed);
283 }
284 Ok(out)
285 }
286
287 #[cfg(not(feature = "lz4"))]
288 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
289 Err(err!(UnknownCompression, "lz4 support was not compiled in"))
290 }
291
292 #[cfg(not(feature = "lz4"))]
293 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
294 Err(err!(UnknownCompression, "lz4 support was not compiled in"))
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::error::ErrorCode;
302
303 fn counter_payload() -> Vec<u8> {
306 let mut v = Vec::new();
307 for i in 0..10_000u32 {
308 v.extend_from_slice(&i.to_le_bytes());
309 }
310 v
311 }
312
313 fn compressible_payload() -> Vec<u8> {
315 let mut v = Vec::new();
316 for i in 0..10_000u32 {
317 v.extend_from_slice(&(i % 16).to_le_bytes());
318 }
319 v
320 }
321
322 #[test]
323 fn names_round_trip() {
324 for c in [Compression::None, Compression::Zlib, Compression::Bzp2, Compression::Lz4] {
325 assert_eq!(Compression::from_name(c.name()).unwrap(), c);
326 assert!(c.name().len() <= 4, "{:?} name too long", c);
328 }
329 }
330
331 #[test]
332 fn unknown_names_are_rejected() {
333 let e = Compression::from_name("zstd").unwrap_err();
334 assert_eq!(e.code(), ErrorCode::UnknownCompression);
335 }
336
337 #[test]
338 fn round_trips_through_every_method() {
339 for data in [counter_payload(), compressible_payload()] {
340 for c in available() {
341 let packed = c.compress(&data).unwrap_or_else(|e| panic!("{:?}: {e}", c));
342 let unpacked =
343 c.decompress(&packed, data.len()).unwrap_or_else(|e| panic!("{:?}: {e}", c));
344 assert_eq!(unpacked, data, "{:?} did not round trip", c);
345 }
346 }
347 }
348
349 #[test]
350 fn every_method_shrinks_redundant_data() {
351 let data = compressible_payload();
355 for c in available() {
356 let packed = c.compress(&data).unwrap();
357 assert!(
358 packed.len() < data.len(),
359 "{:?} grew {} bytes to {}",
360 c,
361 data.len(),
362 packed.len()
363 );
364 }
365 }
366
367 #[test]
368 fn round_trips_empty_and_tiny_inputs() {
369 for c in available() {
370 for data in [vec![], vec![0u8], vec![7u8; 3]] {
371 let packed = c.compress(&data).unwrap();
372 let unpacked = c.decompress(&packed, data.len()).unwrap();
373 assert_eq!(unpacked, data, "{:?} failed on {} bytes", c, data.len());
374 }
375 }
376 }
377
378 #[test]
379 fn none_is_a_passthrough() {
380 let data = b"unchanged".to_vec();
381 assert_eq!(Compression::None.compress(&data).unwrap(), data);
382 assert_eq!(Compression::None.decompress(&data, data.len()).unwrap(), data);
383 }
384
385 #[cfg(feature = "lz4")]
386 #[test]
387 fn lz4_uses_the_asdf_chunk_framing() {
388 let data = vec![0xABu8; 1000];
391 let packed = lz4::compress(&data).unwrap();
392
393 assert!(packed.len() > lz4::CHUNK_HEADER_SIZE);
394 let framed_len = u32::from_be_bytes([packed[0], packed[1], packed[2], packed[3]]) as usize;
395 assert_eq!(
396 framed_len,
397 packed.len() - 4,
398 "the big-endian length must cover the rest of the chunk"
399 );
400
401 let decompressed_size =
402 u32::from_le_bytes([packed[4], packed[5], packed[6], packed[7]]) as usize;
403 assert_eq!(
404 decompressed_size,
405 data.len(),
406 "the little-endian header must carry the decompressed size"
407 );
408 }
409
410 #[cfg(feature = "lz4")]
411 #[test]
412 fn lz4_splits_large_inputs_into_chunks() {
413 let data = vec![0x5Au8; lz4::CHUNK_SIZE + 1024];
415 let packed = lz4::compress(&data).unwrap();
416 let unpacked = lz4::decompress(&packed, data.len()).unwrap();
417 assert_eq!(unpacked.len(), data.len());
418 assert_eq!(unpacked, data);
419
420 let mut pos = 0;
422 let mut frames = 0;
423 while pos < packed.len() {
424 let len = u32::from_be_bytes([
425 packed[pos],
426 packed[pos + 1],
427 packed[pos + 2],
428 packed[pos + 3],
429 ]) as usize;
430 pos += 4 + len;
431 frames += 1;
432 }
433 assert_eq!(frames, 2, "a 4 MiB + 1 KiB input should make two chunks");
434 }
435
436 #[cfg(feature = "lz4")]
437 #[test]
438 fn truncated_lz4_streams_are_rejected() {
439 let data = vec![0x11u8; 5000];
440 let packed = lz4::compress(&data).unwrap();
441
442 let e = lz4::decompress(&packed[..packed.len() - 10], data.len()).unwrap_err();
444 assert_eq!(e.code(), ErrorCode::CompressionFailed);
445
446 let e = lz4::decompress(&packed[..2], data.len()).unwrap_err();
448 assert_eq!(e.code(), ErrorCode::CompressionFailed);
449 }
450
451 #[test]
452 fn corrupt_input_is_an_error_not_a_panic() {
453 let garbage = vec![0xFFu8; 64];
454 for c in available() {
455 let r = c.decompress(&garbage, 1024);
456 if let Ok(v) = r {
459 assert!(v.len() <= 1 << 20);
460 }
461 }
462 }
463
464 #[test]
465 fn absurd_expected_sizes_are_refused() {
466 let small = vec![0u8; 16];
469 for c in available() {
470 let e = c.decompress(&small, usize::MAX / 2);
471 assert!(e.is_err(), "{:?} accepted an absurd size", c);
472 }
473 }
474
475 #[test]
476 fn available_reports_compiled_features() {
477 let names: Vec<_> = available().iter().map(|c| c.name()).collect();
478 #[cfg(feature = "zlib")]
480 assert!(names.contains(&"zlib"));
481 #[cfg(feature = "bzp2")]
482 assert!(names.contains(&"bzp2"));
483 let _ = names;
484 }
485}