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
97#[deny(clippy::arithmetic_side_effects)]
98fn check_expected_size(compressed_len: usize, expected: usize) -> Result<()> {
99 let ceiling = compressed_len.saturating_mul(MAX_EXPANSION_RATIO).max(1 << 20);
100 if expected > ceiling {
101 return Err(err!(
102 CompressionFailed,
103 "block claims to decompress {expected} bytes from {compressed_len}, \
104 beyond the {MAX_EXPANSION_RATIO}x sanity limit"
105 ));
106 }
107 Ok(())
108}
109
110fn read_bounded(mut reader: impl std::io::Read, expected: usize, what: &str) -> Result<Vec<u8>> {
124 use std::io::Read as _;
125
126 let mut out = Vec::new();
127 let read = (&mut reader)
130 .take(expected as u64 + 1)
131 .read_to_end(&mut out)
132 .map_err(|e| err!(CompressionFailed, "{what} decompression failed: {e}"))?;
133
134 if read > expected {
135 return Err(err!(
136 CompressionFailed,
137 "{what} stream expands past the {expected} bytes the block header declares"
138 ));
139 }
140 Ok(out)
141}
142
143mod zlib {
144 use super::*;
145
146 #[cfg(feature = "zlib")]
147 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
148 check_expected_size(data.len(), expected)?;
149 read_bounded(flate2::read::ZlibDecoder::new(data), expected, "zlib")
150 }
151
152 #[cfg(feature = "zlib")]
153 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
154 use std::io::Write;
155 let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
156 enc.write_all(data)
157 .and_then(|()| enc.finish())
158 .map_err(|e| err!(CompressionFailed, "zlib compression failed: {e}"))
159 }
160
161 #[cfg(not(feature = "zlib"))]
162 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
163 Err(err!(UnknownCompression, "zlib support was not compiled in"))
164 }
165
166 #[cfg(not(feature = "zlib"))]
167 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
168 Err(err!(UnknownCompression, "zlib support was not compiled in"))
169 }
170}
171
172mod bzp2 {
173 use super::*;
174
175 #[cfg(feature = "bzp2")]
176 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
177 check_expected_size(data.len(), expected)?;
178 read_bounded(bzip2::read::BzDecoder::new(data), expected, "bzip2")
179 }
180
181 #[cfg(feature = "bzp2")]
182 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
183 use std::io::Write;
184 let mut enc = bzip2::write::BzEncoder::new(Vec::new(), bzip2::Compression::best());
186 enc.write_all(data)
187 .and_then(|()| enc.finish())
188 .map_err(|e| err!(CompressionFailed, "bzip2 compression failed: {e}"))
189 }
190
191 #[cfg(not(feature = "bzp2"))]
192 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
193 Err(err!(UnknownCompression, "bzip2 support was not compiled in"))
194 }
195
196 #[cfg(not(feature = "bzp2"))]
197 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
198 Err(err!(UnknownCompression, "bzip2 support was not compiled in"))
199 }
200}
201
202pub mod lz4 {
218 use super::*;
219
220 pub const CHUNK_SIZE: usize = 1 << 22;
222
223 pub const CHUNK_HEADER_SIZE: usize = 8;
226
227 #[cfg(feature = "lz4")]
228 pub fn decompress(data: &[u8], expected: usize) -> Result<Vec<u8>> {
229 check_expected_size(data.len(), expected)?;
230 let mut out = Vec::new();
231 let mut pos = 0usize;
232
233 while pos < data.len() {
234 if pos + 4 > data.len() {
235 return Err(err!(
236 CompressionFailed,
237 "lz4 stream truncated in a chunk length at offset {pos}"
238 ));
239 }
240 let framed_len =
241 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
242 as usize;
243 pos += 4;
244
245 if framed_len < 4 || pos + framed_len > data.len() {
246 return Err(err!(
247 CompressionFailed,
248 "lz4 chunk at offset {pos} claims {framed_len} bytes, \
249 past the end of the {} byte stream",
250 data.len()
251 ));
252 }
253
254 let chunk = &data[pos..pos + framed_len];
258
259 let declared = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) as usize;
265 let remaining = expected.saturating_sub(out.len());
266 if declared > remaining {
267 return Err(err!(
268 CompressionFailed,
269 "lz4 chunk declares {declared} bytes with {remaining} left of the \
270 {expected} the block header allows"
271 ));
272 }
273
274 let decoded = lz4_flex::block::decompress_size_prepended(chunk)
275 .map_err(|e| err!(CompressionFailed, "lz4 decompression failed: {e}"))?;
276 if decoded.len() > remaining {
279 return Err(err!(
280 CompressionFailed,
281 "lz4 stream expands past the {expected} bytes the block header declares"
282 ));
283 }
284 out.extend_from_slice(&decoded);
285 pos += framed_len;
286 }
287 Ok(out)
288 }
289
290 #[cfg(feature = "lz4")]
291 pub fn compress(data: &[u8]) -> Result<Vec<u8>> {
292 let mut out = Vec::new();
293 for chunk in data.chunks(CHUNK_SIZE) {
295 let framed = lz4_flex::block::compress_prepend_size(chunk);
296 let len = u32::try_from(framed.len())
297 .map_err(|_| err!(CompressionFailed, "lz4 chunk too large to frame"))?;
298 out.extend_from_slice(&len.to_be_bytes());
299 out.extend_from_slice(&framed);
300 }
301 Ok(out)
302 }
303
304 #[cfg(not(feature = "lz4"))]
305 pub fn decompress(_data: &[u8], _expected: usize) -> Result<Vec<u8>> {
306 Err(err!(UnknownCompression, "lz4 support was not compiled in"))
307 }
308
309 #[cfg(not(feature = "lz4"))]
310 pub fn compress(_data: &[u8]) -> Result<Vec<u8>> {
311 Err(err!(UnknownCompression, "lz4 support was not compiled in"))
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::error::ErrorCode;
319
320 fn counter_payload() -> Vec<u8> {
323 let mut v = Vec::new();
324 for i in 0..10_000u32 {
325 v.extend_from_slice(&i.to_le_bytes());
326 }
327 v
328 }
329
330 fn compressible_payload() -> Vec<u8> {
332 let mut v = Vec::new();
333 for i in 0..10_000u32 {
334 v.extend_from_slice(&(i % 16).to_le_bytes());
335 }
336 v
337 }
338
339 #[test]
340 fn names_round_trip() {
341 for c in [Compression::None, Compression::Zlib, Compression::Bzp2, Compression::Lz4] {
342 assert_eq!(Compression::from_name(c.name()).unwrap(), c);
343 assert!(c.name().len() <= 4, "{:?} name too long", c);
345 }
346 }
347
348 #[test]
349 fn unknown_names_are_rejected() {
350 let e = Compression::from_name("zstd").unwrap_err();
351 assert_eq!(e.code(), ErrorCode::UnknownCompression);
352 }
353
354 #[test]
355 fn round_trips_through_every_method() {
356 for data in [counter_payload(), compressible_payload()] {
357 for c in available() {
358 let packed = c.compress(&data).unwrap_or_else(|e| panic!("{:?}: {e}", c));
359 let unpacked =
360 c.decompress(&packed, data.len()).unwrap_or_else(|e| panic!("{:?}: {e}", c));
361 assert_eq!(unpacked, data, "{:?} did not round trip", c);
362 }
363 }
364 }
365
366 #[test]
367 fn every_method_shrinks_redundant_data() {
368 let data = compressible_payload();
372 for c in available() {
373 let packed = c.compress(&data).unwrap();
374 assert!(
375 packed.len() < data.len(),
376 "{:?} grew {} bytes to {}",
377 c,
378 data.len(),
379 packed.len()
380 );
381 }
382 }
383
384 #[test]
385 fn round_trips_empty_and_tiny_inputs() {
386 for c in available() {
387 for data in [vec![], vec![0u8], vec![7u8; 3]] {
388 let packed = c.compress(&data).unwrap();
389 let unpacked = c.decompress(&packed, data.len()).unwrap();
390 assert_eq!(unpacked, data, "{:?} failed on {} bytes", c, data.len());
391 }
392 }
393 }
394
395 #[test]
396 fn none_is_a_passthrough() {
397 let data = b"unchanged".to_vec();
398 assert_eq!(Compression::None.compress(&data).unwrap(), data);
399 assert_eq!(Compression::None.decompress(&data, data.len()).unwrap(), data);
400 }
401
402 #[cfg(feature = "lz4")]
403 #[test]
404 fn lz4_uses_the_asdf_chunk_framing() {
405 let data = vec![0xABu8; 1000];
408 let packed = lz4::compress(&data).unwrap();
409
410 assert!(packed.len() > lz4::CHUNK_HEADER_SIZE);
411 let framed_len = u32::from_be_bytes([packed[0], packed[1], packed[2], packed[3]]) as usize;
412 assert_eq!(
413 framed_len,
414 packed.len() - 4,
415 "the big-endian length must cover the rest of the chunk"
416 );
417
418 let decompressed_size =
419 u32::from_le_bytes([packed[4], packed[5], packed[6], packed[7]]) as usize;
420 assert_eq!(
421 decompressed_size,
422 data.len(),
423 "the little-endian header must carry the decompressed size"
424 );
425 }
426
427 #[cfg(feature = "lz4")]
428 #[test]
429 fn lz4_splits_large_inputs_into_chunks() {
430 let data = vec![0x5Au8; lz4::CHUNK_SIZE + 1024];
432 let packed = lz4::compress(&data).unwrap();
433 let unpacked = lz4::decompress(&packed, data.len()).unwrap();
434 assert_eq!(unpacked.len(), data.len());
435 assert_eq!(unpacked, data);
436
437 let mut pos = 0;
439 let mut frames = 0;
440 while pos < packed.len() {
441 let len = u32::from_be_bytes([
442 packed[pos],
443 packed[pos + 1],
444 packed[pos + 2],
445 packed[pos + 3],
446 ]) as usize;
447 pos += 4 + len;
448 frames += 1;
449 }
450 assert_eq!(frames, 2, "a 4 MiB + 1 KiB input should make two chunks");
451 }
452
453 #[cfg(feature = "lz4")]
454 #[cfg(feature = "lz4")]
462 #[test]
463 fn an_lz4_chunk_may_not_allocate_from_its_own_header() {
464 let mut stream = Vec::new();
465 let body = {
468 let mut b = Vec::new();
469 b.extend_from_slice(&0xFFFF_FF00u32.to_le_bytes());
470 b.push(0);
471 b
472 };
473 stream.extend_from_slice(&(body.len() as u32).to_be_bytes());
474 stream.extend_from_slice(&body);
475
476 let err = Compression::Lz4.decompress(&stream, 64).expect_err("must refuse");
478 let text = format!("{err}");
479 assert!(
480 text.contains("declares") && text.contains("left of the"),
481 "the refusal should name the chunk's own claim, got: {text}"
482 );
483 }
484
485 #[test]
486 fn truncated_lz4_streams_are_rejected() {
487 let data = vec![0x11u8; 5000];
488 let packed = lz4::compress(&data).unwrap();
489
490 let e = lz4::decompress(&packed[..packed.len() - 10], data.len()).unwrap_err();
492 assert_eq!(e.code(), ErrorCode::CompressionFailed);
493
494 let e = lz4::decompress(&packed[..2], data.len()).unwrap_err();
496 assert_eq!(e.code(), ErrorCode::CompressionFailed);
497 }
498
499 #[test]
500 fn corrupt_input_is_an_error_not_a_panic() {
501 let garbage = vec![0xFFu8; 64];
502 for c in available() {
503 let r = c.decompress(&garbage, 1024);
504 if let Ok(v) = r {
507 assert!(v.len() <= 1 << 20);
508 }
509 }
510 }
511
512 #[test]
513 fn absurd_expected_sizes_are_refused() {
514 let small = vec![0u8; 16];
517 for c in available() {
518 let e = c.decompress(&small, usize::MAX / 2);
519 assert!(e.is_err(), "{:?} accepted an absurd size", c);
520 }
521 }
522
523 #[test]
524 fn available_reports_compiled_features() {
525 let names: Vec<_> = available().iter().map(|c| c.name()).collect();
526 #[cfg(feature = "zlib")]
528 assert!(names.contains(&"zlib"));
529 #[cfg(feature = "bzp2")]
530 assert!(names.contains(&"bzp2"));
531 let _ = names;
532 }
533}