bcp_encoder/
compression.rs1use std::io::Cursor;
2
3use crate::error::CompressionError;
4
5pub const COMPRESSION_THRESHOLD: usize = 256;
14
15const DEFAULT_COMPRESSION_LEVEL: i32 = 3;
21
22pub fn compress(data: &[u8]) -> Option<Vec<u8>> {
43 let compressed = zstd::encode_all(Cursor::new(data), DEFAULT_COMPRESSION_LEVEL).ok()?;
44 if compressed.len() < data.len() {
45 Some(compressed)
46 } else {
47 None
48 }
49}
50
51pub fn decompress(data: &[u8], max_size: usize) -> Result<Vec<u8>, CompressionError> {
65 let decompressed = zstd::decode_all(Cursor::new(data))
66 .map_err(|e| CompressionError::DecompressFailed(e.to_string()))?;
67 if decompressed.len() > max_size {
68 return Err(CompressionError::DecompressionBomb {
69 actual: decompressed.len(),
70 limit: max_size,
71 });
72 }
73 Ok(decompressed)
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn compress_returns_none_for_small_incompressible_data() {
82 let data = b"abc123";
83 assert!(compress(data).is_none());
84 }
85
86 #[test]
87 fn compress_reduces_repetitive_data() {
88 let data = "fn main() { }\n".repeat(100);
89 let result = compress(data.as_bytes());
90 assert!(result.is_some());
91 let compressed = result.unwrap();
92 assert!(compressed.len() < data.len());
93 }
94
95 #[test]
96 fn compress_decompress_roundtrip() {
97 let data = "pub fn hello() -> &'static str { \"world\" }\n".repeat(50);
98 let compressed = compress(data.as_bytes()).expect("should compress");
99 let decompressed = decompress(&compressed, 1024 * 1024).expect("should decompress");
100 assert_eq!(decompressed, data.as_bytes());
101 }
102
103 #[test]
104 fn decompress_rejects_bomb() {
105 let data = "x".repeat(10_000);
106 let compressed = compress(data.as_bytes()).expect("should compress");
107 let result = decompress(&compressed, 100);
108 assert!(matches!(
109 result,
110 Err(CompressionError::DecompressionBomb { .. })
111 ));
112 }
113
114 #[test]
115 fn decompress_rejects_invalid_data() {
116 let garbage = b"this is not zstd data";
117 let result = decompress(garbage, 1024 * 1024);
118 assert!(matches!(result, Err(CompressionError::DecompressFailed(_))));
119 }
120
121 #[test]
122 fn compression_threshold_is_256() {
123 assert_eq!(COMPRESSION_THRESHOLD, 256);
124 }
125}