rns_net/common/
compressor.rs1use 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}