Skip to main content

rns_net/common/
compressor.rs

1use rns_core::buffer::types::{Compressor, DecompressError};
2
3pub struct Bzip2Compressor;
4
5impl Compressor for Bzip2Compressor {
6    fn compress(&self, data: &[u8]) -> Option<Vec<u8>> {
7        use bzip2::read::BzEncoder;
8        use bzip2::Compression;
9        use std::io::Read;
10        let mut encoder = BzEncoder::new(data, Compression::default());
11        let mut compressed = Vec::new();
12        encoder.read_to_end(&mut compressed).ok()?;
13        Some(compressed)
14    }
15
16    fn decompress(&self, data: &[u8]) -> Option<Vec<u8>> {
17        self.decompress_bounded(data, usize::MAX).ok()
18    }
19
20    fn decompress_bounded(
21        &self,
22        data: &[u8],
23        max_output_size: usize,
24    ) -> Result<Vec<u8>, DecompressError> {
25        use bzip2::read::BzDecoder;
26        use std::io::Read;
27        let mut decoder = BzDecoder::new(data);
28        let mut decompressed = Vec::new();
29        let mut buf = [0u8; 8192];
30
31        loop {
32            let remaining = max_output_size.saturating_sub(decompressed.len());
33            if remaining == 0 {
34                let mut extra = [0u8; 1];
35                return match decoder.read(&mut extra) {
36                    Ok(0) => Ok(decompressed),
37                    Ok(_) => Err(DecompressError::TooLarge),
38                    Err(_) => Err(DecompressError::InvalidData),
39                };
40            }
41
42            let read_len = remaining.min(buf.len());
43            match decoder.read(&mut buf[..read_len]) {
44                Ok(0) => return Ok(decompressed),
45                Ok(n) => decompressed.extend_from_slice(&buf[..n]),
46                Err(_) => return Err(DecompressError::InvalidData),
47            }
48        }
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    const INTEROP_PLAINTEXT: &[u8] =
57        b"Reticulum resource compression interoperability vector\x00\xff";
58    const INTEROP_BZIP2_HEX: &str = concat!(
59        "425a6836314159265359116659ff000002d380c000400010003a27df200000a000",
60        "50a6134d01a62113d413c088d94d9bbcd69421c457d265cbfab3d6a2a2bc0653",
61        "d74711b78f01487710a8f5f8bb9229c284808b32cff8",
62    );
63
64    fn decode_hex(value: &str) -> Vec<u8> {
65        assert_eq!(value.len() % 2, 0);
66        value
67            .as_bytes()
68            .chunks_exact(2)
69            .map(|pair| {
70                let pair = std::str::from_utf8(pair).unwrap();
71                u8::from_str_radix(pair, 16).unwrap()
72            })
73            .collect()
74    }
75
76    #[test]
77    fn bzip2_bounded_roundtrip_within_limit() {
78        let compressor = Bzip2Compressor;
79        let input = b"hello hello hello hello";
80        let compressed = compressor.compress(input).unwrap();
81        let decompressed = compressor
82            .decompress_bounded(&compressed, input.len())
83            .unwrap();
84        assert_eq!(decompressed, input);
85    }
86
87    #[test]
88    fn bzip2_bounded_rejects_oversized_output() {
89        let compressor = Bzip2Compressor;
90        let input = vec![b'A'; 4096];
91        let compressed = compressor.compress(&input).unwrap();
92        assert_eq!(
93            compressor.decompress_bounded(&compressed, 64),
94            Err(DecompressError::TooLarge)
95        );
96    }
97
98    #[test]
99    fn bzip2_matches_reference_libbz2_wire_vector() {
100        let compressor = Bzip2Compressor;
101        let reference = decode_hex(INTEROP_BZIP2_HEX);
102
103        assert_eq!(compressor.compress(INTEROP_PLAINTEXT).unwrap(), reference);
104        assert_eq!(
105            compressor.decompress(&reference).unwrap(),
106            INTEROP_PLAINTEXT
107        );
108    }
109
110    #[test]
111    fn bzip2_empty_payload_respects_zero_limit() {
112        let compressor = Bzip2Compressor;
113        let compressed = compressor.compress(b"").unwrap();
114
115        assert_eq!(compressed, decode_hex("425a683617724538509000000000"));
116        assert_eq!(
117            compressor.decompress_bounded(&compressed, 0),
118            Ok(Vec::new())
119        );
120    }
121
122    #[test]
123    fn bzip2_nonempty_payload_exceeds_zero_limit() {
124        let compressor = Bzip2Compressor;
125        let compressed = compressor.compress(b"x").unwrap();
126
127        assert_eq!(
128            compressor.decompress_bounded(&compressed, 0),
129            Err(DecompressError::TooLarge)
130        );
131    }
132
133    #[test]
134    fn bzip2_rejects_invalid_header_truncation_and_checksum_corruption() {
135        let compressor = Bzip2Compressor;
136        let compressed = compressor.compress(INTEROP_PLAINTEXT).unwrap();
137
138        assert_eq!(
139            compressor.decompress_bounded(b"not a bzip2 stream", 1024),
140            Err(DecompressError::InvalidData)
141        );
142        assert_eq!(
143            compressor.decompress_bounded(&compressed[..compressed.len() - 5], 1024),
144            Err(DecompressError::InvalidData)
145        );
146
147        let mut corrupted = compressed;
148        let last = corrupted.len() - 1;
149        corrupted[last] ^= 0x80;
150        assert_eq!(
151            compressor.decompress_bounded(&corrupted, 1024),
152            Err(DecompressError::InvalidData)
153        );
154    }
155
156    #[test]
157    fn bzip2_roundtrips_across_multiple_compression_blocks() {
158        let compressor = Bzip2Compressor;
159        let input: Vec<u8> = (0..1_100_000usize)
160            .map(|i| ((i.wrapping_mul(31) + i / 251) & 0xff) as u8)
161            .collect();
162        let compressed = compressor.compress(&input).unwrap();
163
164        assert_eq!(
165            compressor.decompress_bounded(&compressed, input.len()),
166            Ok(input)
167        );
168    }
169
170    #[test]
171    fn bzip2_roundtrips_maximum_resource_segment_with_periodic_data() {
172        let compressor = Bzip2Compressor;
173        let input: Vec<u8> = (0..rns_core::constants::RESOURCE_MAX_EFFICIENT_SIZE)
174            .map(|index| (index % 251) as u8)
175            .collect();
176        let compressed = compressor.compress(&input).unwrap();
177
178        assert_eq!(
179            compressor.decompress_bounded(&compressed, input.len()),
180            Ok(input)
181        );
182    }
183}