1#![forbid(unsafe_code)]
16#![warn(clippy::pedantic)]
17
18pub mod block;
19pub mod frame;
20mod hc;
21pub mod streaming;
22
23use omnizip_codecs::{Codec, CodecId, CompressionLevel, OmnizipError};
24
25pub struct Lz4FastCodec;
27
28pub struct Lz4HcCodec;
33
34impl Codec for Lz4FastCodec {
35 fn id(&self) -> CodecId {
36 CodecId::LZ4
37 }
38 fn name(&self) -> &'static str {
39 "lz4"
40 }
41 fn compress(
42 &self,
43 plaintext: &[u8],
44 _level: CompressionLevel,
45 ) -> Result<Vec<u8>, OmnizipError> {
46 let compressed = block::compress_block(plaintext);
47 let mut out = Vec::with_capacity(4 + compressed.len());
48 out.extend_from_slice(&(plaintext.len() as u32).to_le_bytes());
49 out.extend_from_slice(&compressed);
50 Ok(out)
51 }
52 fn decompress(&self, compressed: &[u8], expected_len: u32) -> Result<Vec<u8>, OmnizipError> {
53 decompress_lz4(compressed, expected_len, CodecId::LZ4)
54 }
55
56 fn default_fast_level(&self) -> u8 {
57 1
58 }
59 fn default_balanced_level(&self) -> u8 {
60 1
61 }
62 fn default_max_ratio_level(&self) -> u8 {
63 1
64 }
65
66 fn capabilities(&self) -> omnizip_codecs::Capabilities {
67 omnizip_codecs::Capabilities {
68 min_level: 1,
69 max_level: 1,
70 streaming: true, parallel_batch: true,
72 has_static_dictionary: false,
73 content_type_aware: false,
74 approx_throughput_mbps: 500,
75 }
76 }
77}
78
79impl omnizip_codecs::MemoryBudget for Lz4FastCodec {
81 fn estimated_compress_memory(&self, input_len: usize, _level: CompressionLevel) -> usize {
82 let hash_table = 16 * 1024; input_len + input_len / 2 + hash_table
84 }
85}
86
87impl Codec for Lz4HcCodec {
88 fn id(&self) -> CodecId {
89 CodecId::LZ4_HC
90 }
91 fn name(&self) -> &'static str {
92 "lz4-hc"
93 }
94 fn compress(
95 &self,
96 plaintext: &[u8],
97 _level: CompressionLevel,
98 ) -> Result<Vec<u8>, OmnizipError> {
99 let compressed = hc::compress(plaintext);
100 let mut out = Vec::with_capacity(4 + compressed.len());
101 out.extend_from_slice(&(plaintext.len() as u32).to_le_bytes());
102 out.extend_from_slice(&compressed);
103 Ok(out)
104 }
105 fn decompress(&self, compressed: &[u8], expected_len: u32) -> Result<Vec<u8>, OmnizipError> {
106 decompress_lz4(compressed, expected_len, CodecId::LZ4_HC)
107 }
108
109 fn default_fast_level(&self) -> u8 {
110 4
111 }
112 fn default_balanced_level(&self) -> u8 {
113 9
114 }
115 fn default_max_ratio_level(&self) -> u8 {
116 12
117 }
118
119 fn capabilities(&self) -> omnizip_codecs::Capabilities {
120 omnizip_codecs::Capabilities {
121 min_level: 1,
122 max_level: 12,
123 streaming: false, parallel_batch: true,
125 has_static_dictionary: false,
126 content_type_aware: false,
127 approx_throughput_mbps: 200,
128 }
129 }
130}
131
132impl omnizip_codecs::MemoryBudget for Lz4HcCodec {
134 fn estimated_compress_memory(&self, input_len: usize, level: CompressionLevel) -> usize {
135 let table_size = if level.as_u8() >= 9 {
136 64 * 1024 } else {
138 16 * 1024
139 };
140 input_len + input_len / 2 + table_size
141 }
142}
143
144fn decompress_lz4(
146 compressed: &[u8],
147 expected_len: u32,
148 codec: CodecId,
149) -> Result<Vec<u8>, OmnizipError> {
150 if compressed.len() < 4 {
151 return Err(OmnizipError::Corrupt {
152 codec,
153 reason: "input too short for size prefix".into(),
154 });
155 }
156 let stored_len =
157 u32::from_le_bytes([compressed[0], compressed[1], compressed[2], compressed[3]]) as usize;
158 let block_data = &compressed[4..];
159 let decoded = block::decompress_block(block_data, stored_len).map_err(|reason| {
160 OmnizipError::DecodeFailed {
161 codec,
162 reason: format!("lz4 block decode failed: {reason}"),
163 }
164 })?;
165
166 let expected_us = usize::try_from(expected_len).map_err(|_| OmnizipError::Corrupt {
167 codec,
168 reason: format!("expected_len {expected_len} exceeds usize"),
169 })?;
170 if decoded.len() != expected_us {
171 return Err(OmnizipError::LengthMismatch {
172 codec,
173 expected: expected_len,
174 actual: decoded.len(),
175 });
176 }
177 Ok(decoded)
178}
179
180pub fn compress_frame(plaintext: &[u8]) -> Result<Vec<u8>, OmnizipError> {
188 Ok(frame::compress_frame(plaintext))
189}
190
191pub fn decompress_frame(compressed: &[u8]) -> Result<Vec<u8>, OmnizipError> {
197 frame::decompress_frame(compressed).map_err(|reason| OmnizipError::DecodeFailed {
198 codec: CodecId::LZ4,
199 reason: format!("lz4 frame decode failed: {reason}"),
200 })
201}
202
203#[cfg(test)]
204#[allow(clippy::cast_possible_truncation)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn fast_round_trips_text() {
210 let data = b"Lorem ipsum dolor sit amet. ".repeat(100);
211 let compressed = Lz4FastCodec
212 .compress(&data, CompressionLevel::default())
213 .expect("compress");
214 let decompressed = Lz4FastCodec
215 .decompress(&compressed, data.len() as u32)
216 .expect("decompress");
217 assert_eq!(decompressed, data);
218 }
219
220 #[test]
221 fn hc_round_trips_text() {
222 let data = b"Lorem ipsum dolor sit amet. ".repeat(100);
223 let compressed = Lz4HcCodec
224 .compress(&data, CompressionLevel::default())
225 .expect("compress");
226 let decompressed = Lz4HcCodec
227 .decompress(&compressed, data.len() as u32)
228 .expect("decompress");
229 assert_eq!(decompressed, data);
230 }
231
232 #[test]
233 fn fast_and_hc_share_decoder_format() {
234 let data = b"The quick brown fox. ".repeat(1000);
235 let fast_compressed = Lz4FastCodec
236 .compress(&data, CompressionLevel::default())
237 .expect("fast compress");
238 let hc_compressed = Lz4HcCodec
239 .compress(&data, CompressionLevel::default())
240 .expect("hc compress");
241 let from_fast = Lz4FastCodec
242 .decompress(&hc_compressed, data.len() as u32)
243 .expect("cross-decode fast from hc");
244 let from_hc = Lz4HcCodec
245 .decompress(&fast_compressed, data.len() as u32)
246 .expect("cross-decode hc from fast");
247 assert_eq!(from_fast, data);
248 assert_eq!(from_hc, data);
249 }
250
251 #[test]
252 fn compresses_repetitive_data() {
253 let data = vec![0x41u8; 10_000];
254 let fast = Lz4FastCodec
255 .compress(&data, CompressionLevel::default())
256 .expect("fast");
257 assert!(fast.len() < data.len());
258 }
259
260 #[test]
261 fn hc_produces_different_output_than_fast() {
262 let data: Vec<u8> = (0..10_000)
263 .map(|i| {
264 if i % 100 < 50 {
265 (i % 26 + b'a' as i32) as u8
266 } else {
267 (i % 256) as u8
268 }
269 })
270 .collect();
271 let fast = Lz4FastCodec
272 .compress(&data, CompressionLevel::default())
273 .expect("fast");
274 let hc = Lz4HcCodec
275 .compress(&data, CompressionLevel::default())
276 .expect("hc");
277 assert_ne!(fast, hc, "HC must produce different output");
278 }
279
280 #[test]
281 fn frame_round_trip() {
282 let data = b"the quick brown fox jumps over the lazy dog. ".repeat(10);
283 let compressed = compress_frame(&data).expect("frame compress");
284 let decompressed = decompress_frame(&compressed).expect("frame decompress");
285 assert_eq!(decompressed.as_slice(), data.as_slice());
286 }
287}