use super::{DecompressTooLargeSnafu, Error, Lz4DecompressSnafu, ZstdSnafu};
use lz4_flex::block::{compress_prepend_size, decompress_size_prepended};
use snafu::{ResultExt, ensure};
use std::io::Read;
type Result<T> = std::result::Result<T, Error>;
pub const DEFAULT_DECOMPRESS_LIMIT: usize = 64 * 1024 * 1024;
const LZ4_SIZE_PREFIX_LEN: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Algorithm {
Lz4,
Zstd(i32),
}
impl Default for Algorithm {
fn default() -> Self {
Algorithm::Zstd(zstd::DEFAULT_COMPRESSION_LEVEL)
}
}
pub fn compress(data: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
match algorithm {
Algorithm::Lz4 => {
Ok(compress_prepend_size(data))
}
Algorithm::Zstd(level) => {
zstd::encode_all(data, level).context(ZstdSnafu)
}
}
}
pub fn decompress(data: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
decompress_with_limit(data, algorithm, DEFAULT_DECOMPRESS_LIMIT)
}
pub fn decompress_with_limit(data: &[u8], algorithm: Algorithm, limit: usize) -> Result<Vec<u8>> {
match algorithm {
Algorithm::Lz4 => {
if let Some(prefix) = data.get(..LZ4_SIZE_PREFIX_LEN) {
let mut buf = [0u8; LZ4_SIZE_PREFIX_LEN];
buf.copy_from_slice(prefix);
let declared = u32::from_le_bytes(buf) as usize;
ensure!(
declared <= limit,
DecompressTooLargeSnafu {
size: declared,
limit
}
);
}
decompress_size_prepended(data).context(Lz4DecompressSnafu)
}
Algorithm::Zstd(_) => {
let decoder = zstd::Decoder::new(data).context(ZstdSnafu)?;
let mut out = Vec::new();
decoder
.take(limit as u64 + 1)
.read_to_end(&mut out)
.context(ZstdSnafu)?;
ensure!(
out.len() <= limit,
DecompressTooLargeSnafu {
size: out.len(),
limit
}
);
Ok(out)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn round_trip(algo: Algorithm, payload: &[u8]) {
let compressed = compress(payload, algo).expect("compress should succeed");
let decompressed = decompress(&compressed, algo).expect("decompress should succeed");
assert_eq!(decompressed, payload, "round-trip 必须保留所有字节");
}
#[test]
fn lz4_round_trip() {
round_trip(Algorithm::Lz4, b"hello world");
round_trip(Algorithm::Lz4, &vec![0xAB; 1024]); round_trip(Algorithm::Lz4, &[]); }
#[test]
fn zstd_round_trip() {
let algo = Algorithm::Zstd(zstd::DEFAULT_COMPRESSION_LEVEL);
round_trip(algo, b"hello world");
round_trip(algo, &vec![0xAB; 1024]);
round_trip(algo, &[]);
}
#[test]
fn decompress_garbage_returns_error() {
let garbage = b"this is definitely not compressed data";
assert!(decompress(garbage, Algorithm::Lz4).is_err());
assert!(decompress(garbage, Algorithm::Zstd(0)).is_err());
}
#[test]
fn lz4_forged_size_prefix_is_rejected_before_allocating() {
let mut forged = compress(b"tiny payload", Algorithm::Lz4).expect("compress");
forged[..4].copy_from_slice(&u32::MAX.to_le_bytes());
let err = decompress(&forged, Algorithm::Lz4).expect_err("伪造的超大长度必须被拒绝");
assert!(
err.to_string().contains("exceeds limit"),
"应报超限而非其它错误,实际: {err}"
);
}
#[test]
fn limit_is_enforced_for_both_algorithms() {
let payload = vec![b'a'; 4096];
for algo in [Algorithm::Lz4, Algorithm::Zstd(3)] {
let compressed = compress(&payload, algo).expect("compress");
let err = decompress_with_limit(&compressed, algo, 1024)
.expect_err("{algo:?} 超限时必须报错");
assert!(err.to_string().contains("exceeds limit"), "{err}");
let out = decompress_with_limit(&compressed, algo, payload.len())
.expect("恰好等于上限应当放行");
assert_eq!(out, payload);
}
}
#[test]
fn zstd_bomb_is_capped() {
let bomb = compress(&vec![0u8; 16 * 1024 * 1024], Algorithm::Zstd(3)).expect("compress");
assert!(bomb.len() < 4096, "构造前提:压缩后应当很小");
let err = decompress_with_limit(&bomb, Algorithm::Zstd(3), 1024)
.expect_err("高压缩比炸弹必须被上限拦下");
assert!(err.to_string().contains("exceeds limit"), "{err}");
}
}