use miniz_oxide::deflate::compress_to_vec;
use miniz_oxide::inflate::{TINFLStatus, decompress_to_vec_with_limit};
use crate::{Error, Result};
pub const MAX_DECOMPRESSED_LEN: usize = 64 * 1024 * 1024;
const LEVEL: u8 = 6;
pub fn compress(plaintext: &[u8]) -> Result<Vec<u8>> {
Ok(compress_to_vec(plaintext, LEVEL))
}
pub fn decompress(compressed: &[u8]) -> Result<Vec<u8>> {
if compressed.is_empty() {
return Err(Error::SyncEmptyPayload);
}
decompress_to_vec_with_limit(compressed, MAX_DECOMPRESSED_LEN).map_err(|error| {
match error.status {
TINFLStatus::HasMoreOutput => Error::SyncPayloadTooLarge {
limit: MAX_DECOMPRESSED_LEN,
},
_ => Error::SyncDecompressionFailed,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_preserves_content() {
for size in [0_usize, 1, 15, 4096, 100_000] {
let data: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let packed = compress(&data).expect("compression succeeds");
let back = decompress(&packed).expect("decompression succeeds");
assert_eq!(back, data, "round trip differs at length {size}");
}
}
#[test]
fn repetitive_payload_actually_shrinks() {
let mut data = Vec::new();
for i in 0..500_u32 {
data.extend_from_slice(b"\x01name\x02otpauth://totp/ACME:alice@example.com");
data.extend_from_slice(&i.to_be_bytes());
}
let packed = compress(&data).expect("compression succeeds");
assert!(
packed.len() * 4 < data.len(),
"compressed to {} bytes from {} bytes: gain too small, the compression layer may be inactive",
packed.len(),
data.len()
);
}
#[test]
fn incompressible_payload_still_roundtrips() {
let mut data = vec![0_u8; 8192];
crate::getrandom::fill(&mut data).expect("OS randomness available");
let packed = compress(&data).expect("compression succeeds");
let back = decompress(&packed).expect("decompression succeeds");
assert_eq!(back, data);
}
#[test]
fn empty_input_is_reported_as_empty_not_corrupt() {
let err = decompress(&[]);
assert!(
matches!(err, Err(Error::SyncEmptyPayload)),
"actual {err:?}"
);
}
#[test]
fn garbage_is_rejected() {
for garbage in [
vec![0xFF_u8; 64],
vec![0x78, 0x9C],
b"not deflate at all".to_vec(),
] {
let err = decompress(&garbage);
assert!(
matches!(err, Err(Error::SyncDecompressionFailed)),
"garbage input {garbage:?} was not rejected, actual {err:?}"
);
}
}
#[test]
fn a_decompression_bomb_is_rejected() {
let bomb = vec![0_u8; MAX_DECOMPRESSED_LEN + 1];
let packed = compress(&bomb).expect("compression succeeds");
assert!(
packed.len() < bomb.len() / 100,
"precondition failed: the bomb did not compress small enough ({} bytes after compression)",
packed.len()
);
let err = decompress(&packed);
assert!(
matches!(err, Err(Error::SyncPayloadTooLarge { .. })),
"decompression past the limit was not rejected, actual {err:?}"
);
}
#[test]
fn incompressible_data_over_the_limit_is_rejected() {
let mut data = vec![0_u8; MAX_DECOMPRESSED_LEN + 1];
crate::getrandom::fill(&mut data).expect("OS randomness available");
let packed = compress(&data).expect("compression succeeds");
let err = decompress(&packed);
assert!(
matches!(err, Err(Error::SyncPayloadTooLarge { .. })),
"random data past the limit was not rejected, actual {err:?}"
);
}
#[test]
fn exactly_at_the_limit_is_accepted() {
let mut data = vec![0_u8; MAX_DECOMPRESSED_LEN];
crate::getrandom::fill(&mut data).expect("OS randomness available");
let packed = compress(&data).expect("compression succeeds");
let back = decompress(&packed).expect("exactly at the limit must pass");
assert_eq!(back.len(), MAX_DECOMPRESSED_LEN);
assert_eq!(
back, data,
"content exactly at the limit must be byte-identical"
);
}
#[test]
fn truncated_stream_is_rejected() {
let data = vec![3_u8; 4096];
let packed = compress(&data).expect("compression succeeds");
for cut in 1..packed.len().min(24) {
let truncated = &packed[..packed.len() - cut];
let result = decompress(truncated);
if let Ok(partial) = result {
panic!(
"truncating {cut} bytes still decompressed to {} bytes (originally {} bytes)",
partial.len(),
data.len()
);
}
}
}
}