Skip to main content

prns_runtime/
resource_compression.rs

1//! RNS 1.4.2 Resource bz2, the half the pure engine leaves to its host. `prns-core`
2//! carries a resource as an opaque stream and flags it compressed or not; the codec is
3//! the host's, because bz2 is unavailable on the embedded targets the core also serves.
4//! The tokio host has no such limit, so it compresses every outgoing resource and inflates
5//! every compressed one, and a std node speaks the reference's wire compression byte-for-byte.
6//!
7//! Pure-Rust bz2 (bzip2's default `libbz2-rs-sys` backend): no C toolchain, and nothing
8//! for the crate's `forbid(unsafe_code)` to trip on.
9
10use 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/// RNS 1.4.2 `Resource.__init__`: `bz2.compress` at level 9 (its default), kept only when
30/// it comes out strictly smaller than the input. `None` is the reference's else-branch: send
31/// the payload as-is with the `c` flag clear. For already-dense bytes bz2 only adds overhead,
32/// so the reference, and we, decline it.
33///
34/// The reference pays the full attempt on every input; on dense data that is where a bulk
35/// sender's whole core goes (~80 ms per 1 MiB segment against ~3 ms of engine work), so past
36/// [`SAMPLE_GATE_LEN`] we first compress a head/middle/tail sample and decline outright when
37/// even the sample refuses to shrink. A kept stream is still the whole-input level-9 attempt,
38/// byte-identical to the reference's; the sample only buys the decline early. The corner this
39/// trades away: a large payload whose only compressible run hides between the sample points
40/// ships uncompressed — wire-legal, just larger than the reference would have sent it.
41///
42/// The corner has a proven radius, because the input is bounded by the segment size and the
43/// windows sit at both ends and the midpoint: the largest unsampled gap is
44/// `(len − 3·SAMPLE_SLICE_LEN) / 2`, so any contiguous compressible region longer than a gap
45/// plus two windows must contain a whole window and is always detected. Only redundancy
46/// entirely below that length — under half the payload — can hide.
47#[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
55/// Sampling pays ~5 ms to sidestep an ~80 ms attempt at 1 MiB; below this the full attempt
56/// is cheap enough to just run, and every input stays on the reference's exact path.
57pub 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
78/// libbz2's guaranteed output ceiling, `len + len/100 + 600`: enough spare capacity that
79/// even incompressible input (which bz2 grows) finishes in one pass, so the keep-smaller
80/// test below is a true size comparison, never an artifact of a filled buffer.
81fn 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
93/// RNS 1.4.2's bounded bz2 inflate, `BZ2Decompressor(...).decompress(data, max_length=…)` with
94/// the `eof` check: inflate to at most `max_len`, refusing a stream that would run past it. Both
95/// callers cap `max_len` at host policy (a resource's advertised length, already gated by the
96/// link's `ResourceStrategy`; a stream chunk's channel MDU), so a bz2 bomb can force neither an
97/// unbounded allocation nor an unbounded inflate. A resource caller passes its exact advertised
98/// length and re-checks it on assembly, so a stream that inflates short is caught there.
99pub 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    /// The exact input behind the resource family's `CASE1_BZ2` reference vector:
151    /// RNS 1.3.5 minted this 90-byte stream from the 1360-byte payload below; RNS 1.4.2
152    /// revalidates it unchanged.
153    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    /// Deterministic high-entropy bytes (xorshift64*, all eight output bytes per step —
180    /// single low bytes of the raw state correlate enough for bz2's BWT to shrink them):
181    /// bz2 cannot shrink these, so they exercise the decline branch and the incompressible
182    /// round trip without a real RNG in the test.
183    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    /// The doc's coverage radius, executable: with windows at `0`, `(L−w)/2`, and `L−w`, the
230    /// largest unsampled gap is `(L−3w)/2`, so a compressible run of `gap + 2·w` bytes contains
231    /// a whole window wherever it sits.
232    #[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    /// The corner the radius bounds: a run no longer than one gap, placed exactly between two
252    /// windows, is invisible to the sample even though the whole input compresses.
253    #[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}