use crate::error::UrnaError;
pub const DEFAULT_ZSTD_LEVEL: i32 = 19;
pub fn zstd_encode(bytes: &[u8]) -> crate::Result<Vec<u8>> {
zstd::encode_all(bytes, DEFAULT_ZSTD_LEVEL)
.map_err(|e| UrnaError::InvalidInput(format!("zstd compression failed: {}", e)))
}
const MIN_DECOMPRESS_CAP: usize = 64 * 1024 * 1024;
const MAX_DECOMPRESS_RATIO: usize = 128;
fn decompress_cap(compressed_len: usize) -> usize {
compressed_len
.saturating_mul(MAX_DECOMPRESS_RATIO)
.max(MIN_DECOMPRESS_CAP)
}
pub(super) fn zstd_decode(bytes: &[u8]) -> crate::Result<Vec<u8>> {
let cap = decompress_cap(bytes.len());
let bomb = |reason: String| UrnaError::MalformedSectionPayload {
section_id: 0,
reason,
};
let fail = |e: std::io::Error| bomb(format!("zstd decompression failed: {}", e));
match zstd::bulk::Decompressor::upper_bound(bytes) {
Some(declared) if declared > cap => Err(bomb(format!(
"zstd frame declares {} bytes, exceeds cap {}",
declared, cap
))),
Some(declared) => zstd::bulk::decompress(bytes, declared).map_err(fail),
None => zstd::bulk::decompress(bytes, cap).map_err(fail),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg_attr(miri, ignore)] fn roundtrip_within_cap() {
let original = b"the file is the database ".repeat(256);
let compressed = zstd_encode(&original).unwrap();
let decoded = zstd_decode(&compressed).unwrap();
assert_eq!(decoded, original);
}
#[test]
#[cfg_attr(miri, ignore)] fn decompression_bomb_is_rejected_not_ooming() {
let bomb = vec![0u8; MIN_DECOMPRESS_CAP + 1];
let compressed = zstd_encode(&bomb).unwrap();
assert!(compressed.len() < 4096, "bomb should compress tiny");
let err = zstd_decode(&compressed).unwrap_err();
assert!(matches!(err, UrnaError::MalformedSectionPayload { .. }));
}
#[test]
fn cap_has_floor_and_ratio() {
assert_eq!(decompress_cap(0), MIN_DECOMPRESS_CAP);
assert_eq!(decompress_cap(usize::MAX), usize::MAX); assert_eq!(
decompress_cap(MIN_DECOMPRESS_CAP),
MIN_DECOMPRESS_CAP * MAX_DECOMPRESS_RATIO
);
}
}