prns_runtime/
resource_compression.rs1use std::vec::Vec;
11
12use bzip2::{Compress, Compression, Decompress, Status};
13use prns_core::routing::links::resources::METADATA_PREFIX_LEN;
14
15#[must_use]
16pub fn compress_resource_candidate(data: &[u8], packed_metadata: Option<&[u8]>) -> Option<Vec<u8>> {
17 let Some(packed) = packed_metadata else {
18 return compress_if_smaller(data);
19 };
20 let packed_len = u32::try_from(packed.len()).ok()?;
21 let prefix_start = core::mem::size_of::<u32>().checked_sub(METADATA_PREFIX_LEN)?;
22 let mut composite = Vec::with_capacity(METADATA_PREFIX_LEN + packed.len() + data.len());
23 composite.extend_from_slice(&packed_len.to_be_bytes()[prefix_start..]);
24 composite.extend_from_slice(packed);
25 composite.extend_from_slice(data);
26 compress_if_smaller(&composite)
27}
28
29#[must_use]
48pub fn compress_if_smaller(data: &[u8]) -> Option<Vec<u8>> {
49 if data.len() >= SAMPLE_GATE_LEN && !sample_shrinks(data) {
50 return None;
51 }
52 bz2_if_smaller(data)
53}
54
55pub const SAMPLE_GATE_LEN: usize = 256 * 1024;
58
59const SAMPLE_SLICE_LEN: usize = 16 * 1024;
60
61fn sample_shrinks(data: &[u8]) -> bool {
62 let mut sample = Vec::with_capacity(3 * SAMPLE_SLICE_LEN);
63 sample.extend_from_slice(&data[..SAMPLE_SLICE_LEN]);
64 sample.extend_from_slice(&data[(data.len() - SAMPLE_SLICE_LEN) / 2..][..SAMPLE_SLICE_LEN]);
65 sample.extend_from_slice(&data[data.len() - SAMPLE_SLICE_LEN..]);
66 bz2_if_smaller(&sample).is_some()
67}
68
69fn bz2_if_smaller(data: &[u8]) -> Option<Vec<u8>> {
70 let mut compressor = Compress::new(Compression::best(), 0);
71 let mut compressed = Vec::with_capacity(bz2_worst_case_len(data.len()));
72 match compressor.compress_vec(data, &mut compressed, bzip2::Action::Finish) {
73 Ok(Status::StreamEnd) => (compressed.len() < data.len()).then_some(compressed),
74 _ => None,
75 }
76}
77
78fn bz2_worst_case_len(len: usize) -> usize {
82 len + len / 100 + 600
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum DecompressError {
87 Malformed,
88 Overlong,
89}
90
91const DECOMPRESS_CHUNK_LEN: usize = 64 * 1024;
92
93pub fn decompress_bounded(stream: &[u8], max_len: u64) -> Result<Vec<u8>, DecompressError> {
100 let cap = usize::try_from(max_len).map_err(|_| DecompressError::Overlong)?;
101 let mut out = Vec::with_capacity(cap.min(DECOMPRESS_CHUNK_LEN));
102 let mut chunk = std::vec![0u8; DECOMPRESS_CHUNK_LEN];
103 let mut decoder = Decompress::new(false);
104 let mut input_at = 0usize;
105 loop {
106 let remaining = cap.saturating_sub(out.len());
107 let offered = remaining.saturating_add(1).min(DECOMPRESS_CHUNK_LEN);
108 let before_in = decoder.total_in();
109 let before_out = decoder.total_out();
110 let input = stream.get(input_at..).ok_or(DecompressError::Malformed)?;
111 let output = chunk.get_mut(..offered).ok_or(DecompressError::Overlong)?;
112 let status = decoder
113 .decompress(input, output)
114 .map_err(|_| DecompressError::Malformed)?;
115 let consumed = usize::try_from(decoder.total_in().saturating_sub(before_in))
116 .map_err(|_| DecompressError::Malformed)?;
117 let produced = usize::try_from(decoder.total_out().saturating_sub(before_out))
118 .map_err(|_| DecompressError::Overlong)?;
119 input_at = input_at
120 .checked_add(consumed)
121 .ok_or(DecompressError::Malformed)?;
122 if input_at > stream.len() {
123 return Err(DecompressError::Malformed);
124 }
125 if produced > remaining || produced > offered {
126 return Err(DecompressError::Overlong);
127 }
128 let produced_bytes = chunk.get(..produced).ok_or(DecompressError::Overlong)?;
129 out.extend_from_slice(produced_bytes);
130 if status == Status::StreamEnd {
131 return Ok(out);
132 }
133 if consumed == 0 && produced == 0 {
134 return Err(DecompressError::Malformed);
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn bytes_from_hex(hex: &str) -> Vec<u8> {
144 (0..hex.len())
145 .step_by(2)
146 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
147 .collect()
148 }
149
150 fn reference_input() -> Vec<u8> {
154 b"reticulum resources ride the link "
155 .iter()
156 .copied()
157 .cycle()
158 .take(34 * 40)
159 .collect()
160 }
161
162 const CASE1_BZ2: &str = "425a6839314159265359cf3017f4000207918040000e6f9e002000902980000a54a7a869ea794d3227c13a1382644e09a09a1342684f213f04c09b1382704ec2684d89e04c8ab61302604d09d09d89fc5dc914e142433cc05fd0";
163
164 #[test]
165 fn our_bz2_is_byte_identical_to_the_reference_compressor() {
166 assert_eq!(
167 compress_if_smaller(&reference_input()),
168 Some(bytes_from_hex(CASE1_BZ2)),
169 );
170 }
171
172 #[test]
173 fn the_reference_stream_inflates_back_to_its_input() {
174 let input = reference_input();
175 let inflated = decompress_bounded(&bytes_from_hex(CASE1_BZ2), input.len() as u64);
176 assert_eq!(inflated, Ok(input));
177 }
178
179 fn xorshift_bytes(len: usize) -> Vec<u8> {
184 let mut x = 0x2545_f491_4f6c_dd1du64;
185 let mut out = Vec::with_capacity(len + 8);
186 while out.len() < len {
187 x ^= x << 13;
188 x ^= x >> 7;
189 x ^= x << 17;
190 out.extend_from_slice(&x.wrapping_mul(0x2545_f491_4f6c_dd1d).to_le_bytes());
191 }
192 out.truncate(len);
193 out
194 }
195
196 #[test]
197 fn compression_round_trips_compressible_data() {
198 let data: Vec<u8> = (0..8192u32).map(|i| (i / 32) as u8).collect();
199 let stream = compress_if_smaller(&data).expect("long runs compress");
200 assert_eq!(decompress_bounded(&stream, data.len() as u64), Ok(data));
201 }
202
203 #[test]
204 fn resource_candidate_compresses_metadata_prefix_and_data_together() {
205 let data = std::vec![7u8; 8192];
206 let packed = b"typed resource metadata";
207 let stream =
208 compress_resource_candidate(&data, Some(packed)).expect("composite compresses");
209 let mut composite = Vec::with_capacity(METADATA_PREFIX_LEN + packed.len() + data.len());
210 composite.extend_from_slice(&(packed.len() as u32).to_be_bytes()[1..]);
211 composite.extend_from_slice(packed);
212 composite.extend_from_slice(&data);
213 assert_eq!(
214 decompress_bounded(&stream, composite.len() as u64),
215 Ok(composite),
216 );
217 }
218
219 #[test]
220 fn incompressible_data_declines_compression() {
221 assert_eq!(compress_if_smaller(&xorshift_bytes(1024)), None);
222 }
223
224 #[test]
225 fn a_sampled_dense_payload_declines_compression() {
226 assert_eq!(compress_if_smaller(&xorshift_bytes(SAMPLE_GATE_LEN)), None);
227 }
228
229 #[test]
233 fn a_compressible_run_past_the_coverage_radius_is_detected_at_any_placement() {
234 let gap = (SAMPLE_GATE_LEN - 3 * SAMPLE_SLICE_LEN) / 2;
235 let run_len = gap + 2 * SAMPLE_SLICE_LEN;
236 for start in [
237 0,
238 1,
239 (SAMPLE_GATE_LEN - run_len) / 2,
240 SAMPLE_GATE_LEN - run_len,
241 ] {
242 let mut data = xorshift_bytes(SAMPLE_GATE_LEN);
243 data[start..start + run_len].fill(0);
244 assert!(
245 compress_if_smaller(&data).is_some(),
246 "a {run_len}-byte run starting at {start} always overlaps a whole window",
247 );
248 }
249 }
250
251 #[test]
254 fn a_compressible_run_that_fits_between_the_windows_is_the_traded_corner() {
255 let gap = (SAMPLE_GATE_LEN - 3 * SAMPLE_SLICE_LEN) / 2;
256 let mut data = xorshift_bytes(SAMPLE_GATE_LEN);
257 data[SAMPLE_SLICE_LEN..SAMPLE_SLICE_LEN + gap].fill(0);
258 assert!(
259 bz2_if_smaller(&data).is_some(),
260 "the reference's whole-input attempt would keep this stream",
261 );
262 assert_eq!(
263 compress_if_smaller(&data),
264 None,
265 "no window sees the run, so the screen declines — the documented wire-size trade",
266 );
267 }
268
269 #[test]
270 fn a_sampled_payload_compressible_only_at_its_tail_still_compresses() {
271 let mut data = xorshift_bytes(SAMPLE_GATE_LEN * 2 / 3);
272 data.resize(SAMPLE_GATE_LEN, 0);
273 let stream = compress_if_smaller(&data).expect("the tail sample shrinks");
274 assert_eq!(decompress_bounded(&stream, data.len() as u64), Ok(data));
275 }
276
277 #[test]
278 fn a_stream_shorter_than_the_bound_inflates_to_its_true_length() {
279 let data: Vec<u8> = (0..3000u32).map(|i| (i / 16) as u8).collect();
280 let stream = compress_if_smaller(&data).expect("runs compress");
281 assert_eq!(
282 decompress_bounded(&stream, 1 << 20),
283 Ok(data),
284 "a chunk inflating well under the ceiling is accepted at its own length",
285 );
286 }
287
288 #[test]
289 fn an_empty_payload_declines_compression() {
290 assert_eq!(compress_if_smaller(&[]), None);
291 }
292
293 #[test]
294 fn a_stream_that_inflates_past_its_bound_is_rejected() {
295 let big: Vec<u8> = std::vec![0u8; 64 * 1024];
296 let stream = compress_if_smaller(&big).expect("a run of zeros compresses");
297 assert_eq!(
298 decompress_bounded(&stream, (big.len() - 1) as u64),
299 Err(DecompressError::Overlong),
300 "one byte short of the true length must not silently truncate",
301 );
302 }
303
304 #[test]
305 fn a_truncated_stream_is_malformed() {
306 let data: Vec<u8> = std::vec![7u8; 8192];
307 let stream = compress_if_smaller(&data).expect("a run compresses");
308 let truncated = &stream[..stream.len() - 4];
309 assert!(decompress_bounded(truncated, data.len() as u64).is_err());
310 }
311
312 #[test]
313 fn garbage_is_malformed_not_a_panic() {
314 assert_eq!(
315 decompress_bounded(b"not a bz2 stream at all", 64),
316 Err(DecompressError::Malformed),
317 );
318 }
319
320 #[test]
321 fn an_unbounded_claim_allocates_only_the_inflated_payload() {
322 let input = reference_input();
323 assert_eq!(
324 decompress_bounded(&bytes_from_hex(CASE1_BZ2), u64::MAX),
325 Ok(input),
326 );
327 }
328
329 #[test]
330 fn malformed_input_with_an_unbounded_claim_is_rejected() {
331 assert_eq!(
332 decompress_bounded(b"not a bz2 stream at all", u64::MAX),
333 Err(DecompressError::Malformed),
334 );
335 }
336}