dig_message/
compression.rs1use crate::constants::{MAX_DECOMPRESSED_BYTES, MIN_COMPRESS_BYTES, ZSTD_LEVEL};
9use crate::error::{MessageError, Result};
10
11pub const COMPRESSION_NONE: u8 = 0;
13
14pub const COMPRESSION_ZSTD: u8 = 1;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CompressedPayload {
21 pub compression: u8,
23 pub bytes: Vec<u8>,
25 pub uncompressed_len: u32,
27}
28
29pub fn compress_payload(payload: &[u8]) -> Result<CompressedPayload> {
39 let uncompressed_len =
40 u32::try_from(payload.len()).map_err(|_| MessageError::PayloadTooLarge(payload.len()))?;
41
42 if payload.len() < MIN_COMPRESS_BYTES {
43 return Ok(raw(payload, uncompressed_len));
44 }
45
46 let compressed = zstd::bulk::compress(payload, ZSTD_LEVEL)
47 .map_err(|e| MessageError::Codec(e.to_string()))?;
48
49 if compressed.len() >= payload.len() {
52 return Ok(raw(payload, uncompressed_len));
53 }
54
55 Ok(CompressedPayload {
56 compression: COMPRESSION_ZSTD,
57 bytes: compressed,
58 uncompressed_len,
59 })
60}
61
62pub fn decompress_payload(compression: u8, data: &[u8], uncompressed_len: u32) -> Result<Vec<u8>> {
73 let declared = uncompressed_len as usize;
74 if declared > MAX_DECOMPRESSED_BYTES {
75 return Err(MessageError::DecompressionBomb {
76 declared,
77 max: MAX_DECOMPRESSED_BYTES,
78 });
79 }
80
81 match compression {
82 COMPRESSION_NONE => {
83 if data.len() != declared {
84 return Err(MessageError::DecompressedLengthMismatch {
85 expected: declared,
86 actual: data.len(),
87 });
88 }
89 Ok(data.to_vec())
90 }
91 COMPRESSION_ZSTD => {
92 let decoded = zstd::bulk::decompress(data, declared)
95 .map_err(|e| MessageError::Codec(e.to_string()))?;
96 if decoded.len() != declared {
97 return Err(MessageError::DecompressedLengthMismatch {
98 expected: declared,
99 actual: decoded.len(),
100 });
101 }
102 Ok(decoded)
103 }
104 other => Err(MessageError::UnsupportedCompression(other)),
105 }
106}
107
108fn raw(payload: &[u8], uncompressed_len: u32) -> CompressedPayload {
110 CompressedPayload {
111 compression: COMPRESSION_NONE,
112 bytes: payload.to_vec(),
113 uncompressed_len,
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn compressible(len: usize) -> Vec<u8> {
123 (0..len).map(|i| (i % 7) as u8).collect()
125 }
126
127 #[test]
128 fn small_payload_stays_raw() {
129 let payload = compressible(MIN_COMPRESS_BYTES - 1);
130 let out = compress_payload(&payload).unwrap();
131 assert_eq!(out.compression, COMPRESSION_NONE);
132 assert_eq!(out.bytes, payload);
133 assert_eq!(out.uncompressed_len as usize, payload.len());
134 }
135
136 #[test]
137 fn large_compressible_payload_uses_zstd_and_round_trips() {
138 let payload = compressible(4096);
139 let out = compress_payload(&payload).unwrap();
140 assert_eq!(out.compression, COMPRESSION_ZSTD);
141 assert!(
142 out.bytes.len() < payload.len(),
143 "zstd should shrink a low-entropy payload"
144 );
145 let restored =
146 decompress_payload(out.compression, &out.bytes, out.uncompressed_len).unwrap();
147 assert_eq!(restored, payload);
148 }
149
150 #[test]
151 fn raw_round_trips() {
152 let payload = compressible(16);
153 let out = compress_payload(&payload).unwrap();
154 let restored =
155 decompress_payload(out.compression, &out.bytes, out.uncompressed_len).unwrap();
156 assert_eq!(restored, payload);
157 }
158
159 #[test]
160 fn incompressible_payload_falls_back_to_raw() {
161 use sha2::{Digest, Sha256};
164 let mut payload = Vec::new();
165 let mut ctr = 0u64;
166 while payload.len() < 1024 {
167 let mut h = Sha256::new();
168 h.update(b"incompressible");
169 h.update(ctr.to_le_bytes());
170 payload.extend_from_slice(&h.finalize());
171 ctr += 1;
172 }
173 let out = compress_payload(&payload).unwrap();
174 assert_eq!(out.compression, COMPRESSION_NONE);
175 assert_eq!(out.bytes, payload);
176 }
177
178 #[test]
179 fn compression_is_deterministic() {
180 let payload = compressible(2048);
181 assert_eq!(
182 compress_payload(&payload).unwrap(),
183 compress_payload(&payload).unwrap()
184 );
185 }
186
187 #[test]
188 fn unknown_compression_id_is_rejected() {
189 let err = decompress_payload(2, &[1, 2, 3], 3).unwrap_err();
190 assert_eq!(err, MessageError::UnsupportedCompression(2));
191 }
192
193 #[test]
194 fn declared_length_over_cap_is_a_bomb() {
195 let declared = (MAX_DECOMPRESSED_BYTES + 1) as u32;
196 let err = decompress_payload(COMPRESSION_ZSTD, &[], declared).unwrap_err();
198 assert_eq!(
199 err,
200 MessageError::DecompressionBomb {
201 declared: declared as usize,
202 max: MAX_DECOMPRESSED_BYTES
203 }
204 );
205 }
206
207 #[test]
208 fn zstd_bomb_that_underdeclares_output_is_rejected() {
209 let payload = compressible(8192);
212 let out = compress_payload(&payload).unwrap();
213 assert_eq!(out.compression, COMPRESSION_ZSTD);
214 let err = decompress_payload(out.compression, &out.bytes, 16).unwrap_err();
215 assert!(matches!(
217 err,
218 MessageError::Codec(_) | MessageError::DecompressedLengthMismatch { .. }
219 ));
220 }
221
222 #[test]
223 fn raw_length_mismatch_is_rejected() {
224 let err = decompress_payload(COMPRESSION_NONE, &[1, 2, 3], 4).unwrap_err();
225 assert_eq!(
226 err,
227 MessageError::DecompressedLengthMismatch {
228 expected: 4,
229 actual: 3
230 }
231 );
232 }
233}