oxihuman_core/
compression_lz.rs1#![allow(dead_code)]
4
5const MAGIC: &[u8; 4] = b"LZ77";
24const WINDOW_SIZE: usize = 65536;
25const MAX_MATCH_LEN: usize = 258; const MIN_MATCH_LEN: usize = 3;
27
28#[derive(Debug, Clone)]
30pub struct LzConfig {
31 pub window_size: usize,
32 pub max_match: usize,
33 pub min_match: usize,
34}
35
36impl Default for LzConfig {
37 fn default() -> Self {
38 Self {
39 window_size: WINDOW_SIZE,
40 max_match: MAX_MATCH_LEN,
41 min_match: MIN_MATCH_LEN,
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct LzCompressor {
49 pub config: LzConfig,
50}
51
52impl LzCompressor {
53 pub fn new(config: LzConfig) -> Self {
54 Self { config }
55 }
56
57 pub fn default_compressor() -> Self {
58 Self::new(LzConfig::default())
59 }
60
61 pub fn with_window_size(window_size: usize) -> Self {
62 Self::new(LzConfig {
63 window_size,
64 ..LzConfig::default()
65 })
66 }
67}
68
69fn find_longest_match(data: &[u8], pos: usize, window_size: usize) -> (usize, usize) {
73 let window_start = pos.saturating_sub(window_size);
74 let max_len = MAX_MATCH_LEN.min(data.len() - pos);
75
76 if max_len < MIN_MATCH_LEN {
77 return (0, 0);
78 }
79
80 let mut best_len = 0usize;
81 let mut best_offset = 0usize;
82
83 let scan_end = window_start;
87 let mut candidate = pos.saturating_sub(1);
88
89 loop {
90 if candidate < scan_end {
91 break;
92 }
93
94 if data[candidate] == data[pos] {
96 let mut ml = 1;
97 while ml < max_len && data[candidate + ml] == data[pos + ml] {
98 ml += 1;
99 }
100 if ml >= MIN_MATCH_LEN && ml > best_len {
101 best_len = ml;
102 best_offset = pos - candidate; if best_len == max_len {
104 break; }
106 }
107 }
108
109 if candidate == 0 {
110 break;
111 }
112 candidate -= 1;
113 }
114
115 if best_len >= MIN_MATCH_LEN {
116 (best_offset, best_len)
117 } else {
118 (0, 0)
119 }
120}
121
122pub fn lz_compress(data: &[u8]) -> Vec<u8> {
124 let mut out = Vec::with_capacity(lz_compress_bound(data.len()));
125
126 out.extend_from_slice(MAGIC);
128 out.extend_from_slice(&(data.len() as u32).to_le_bytes());
129
130 if data.is_empty() {
131 return out;
132 }
133
134 let mut pos = 0;
135 while pos < data.len() {
136 let flag_pos = out.len();
138 out.push(0u8);
139 let mut flags: u8 = 0;
140
141 for bit in 0..8u8 {
142 if pos >= data.len() {
143 break;
144 }
145 let (offset, length) = find_longest_match(data, pos, WINDOW_SIZE);
146 if offset > 0 && length >= MIN_MATCH_LEN {
147 flags |= 1 << bit;
149 let enc_len = (length - MIN_MATCH_LEN) as u8; let enc_off = (offset - 1) as u16; out.push(enc_len);
155 out.push((enc_off & 0xFF) as u8);
156 out.push((enc_off >> 8) as u8);
157 pos += length;
158 } else {
159 out.push(data[pos]);
161 pos += 1;
162 }
163 }
164
165 out[flag_pos] = flags;
166 }
167
168 out
169}
170
171pub fn lz_decompress(data: &[u8]) -> Result<Vec<u8>, String> {
173 if data.len() < 8 {
174 return Err("lz77: input too short for header".to_string());
175 }
176
177 if &data[..4] != MAGIC {
179 return Err("lz77: invalid magic bytes".to_string());
180 }
181
182 let uncompressed_len = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
183
184 let mut out: Vec<u8> = Vec::with_capacity(uncompressed_len);
185 let mut pos = 8usize;
186
187 while pos < data.len() {
188 if out.len() >= uncompressed_len {
189 break;
190 }
191
192 let flags = data[pos];
193 pos += 1;
194
195 for bit in 0..8u8 {
196 if pos >= data.len() || out.len() >= uncompressed_len {
197 break;
198 }
199
200 if (flags >> bit) & 1 == 0 {
201 out.push(data[pos]);
203 pos += 1;
204 } else {
205 if pos + 3 > data.len() {
207 return Err("lz77: truncated back-reference".to_string());
208 }
209 let enc_len = data[pos] as usize;
210 let enc_off_lo = data[pos + 1] as usize;
211 let enc_off_hi = data[pos + 2] as usize;
212 pos += 3;
213
214 let length = enc_len + MIN_MATCH_LEN;
215 let offset = ((enc_off_hi << 8) | enc_off_lo) + 1; if offset > out.len() {
218 return Err(format!(
219 "lz77: back-reference offset {} exceeds output length {}",
220 offset,
221 out.len()
222 ));
223 }
224
225 let match_start = out.len() - offset;
226 for i in 0..length {
227 let b = out[match_start + i];
228 out.push(b);
229 if out.len() >= uncompressed_len {
230 break;
231 }
232 }
233 }
234 }
235 }
236
237 if out.len() != uncompressed_len {
238 return Err(format!(
239 "lz77: expected {} bytes, got {}",
240 uncompressed_len,
241 out.len()
242 ));
243 }
244
245 Ok(out)
246}
247
248pub fn lz_compress_bound(input_len: usize) -> usize {
251 let groups = input_len.div_ceil(8);
252 8 + groups + input_len
253}
254
255pub fn lz_is_compressed(data: &[u8]) -> bool {
257 data.len() >= 4 && &data[..4] == MAGIC
258}
259
260pub fn lz_roundtrip_ok(data: &[u8]) -> bool {
262 match lz_decompress(&lz_compress(data)) {
263 Ok(out) => out == data,
264 Err(_) => false,
265 }
266}
267
268#[allow(dead_code)]
271pub fn compress_bytes(data: &[u8]) -> Vec<u8> {
272 lz_compress(data)
273}
274
275#[allow(dead_code)]
276pub fn decompress_bytes(data: &[u8]) -> Vec<u8> {
277 lz_decompress(data).unwrap_or_default()
278}
279
280#[allow(dead_code)]
281pub fn compressed_size(data: &[u8]) -> usize {
282 lz_compress(data).len()
283}
284
285#[allow(dead_code)]
286pub fn compression_ratio(data: &[u8]) -> f64 {
287 if data.is_empty() {
288 return 1.0;
289 }
290 let c = compressed_size(data);
291 c as f64 / data.len() as f64
292}
293
294#[allow(dead_code)]
295pub fn is_compressed(data: &[u8]) -> bool {
296 lz_is_compressed(data)
297}
298
299#[allow(dead_code)]
300pub fn compress_to_vec(data: &[u8]) -> Vec<u8> {
301 lz_compress(data)
302}
303
304#[allow(dead_code)]
305pub fn decompress_to_vec(data: &[u8]) -> Vec<u8> {
306 lz_decompress(data).unwrap_or_default()
307}
308
309#[allow(dead_code)]
310pub fn compressor_name() -> &'static str {
311 "lz77"
312}
313
314impl LzCompressor {
315 #[allow(dead_code)]
316 pub fn compress(&self, data: &[u8]) -> Vec<u8> {
317 lz_compress(data)
318 }
319
320 #[allow(dead_code)]
321 pub fn decompress(&self, data: &[u8]) -> Vec<u8> {
322 lz_decompress(data).unwrap_or_default()
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn test_roundtrip_simple() {
332 let data = b"aaabbbccc";
333 assert!(lz_roundtrip_ok(data));
334 }
335
336 #[test]
337 fn test_roundtrip_empty() {
338 assert!(lz_roundtrip_ok(&[]));
339 }
340
341 #[test]
342 fn test_single_byte() {
343 assert!(lz_roundtrip_ok(b"x"));
344 }
345
346 #[test]
347 fn test_roundtrip_hello() {
348 assert!(lz_roundtrip_ok(b"Hello, World!"));
349 }
350
351 #[test]
352 fn test_roundtrip_binary() {
353 let data: Vec<u8> = (0u8..=255).collect();
354 assert!(lz_roundtrip_ok(&data));
355 }
356
357 #[test]
358 fn test_is_compressed() {
359 let compressed = lz_compress(b"hello");
360 assert!(lz_is_compressed(&compressed));
361 assert!(!lz_is_compressed(b"hello"));
362 assert!(!lz_is_compressed(&[]));
363 }
364
365 #[test]
366 fn test_compress_bound() {
367 assert!(lz_compress_bound(100) >= 100);
368 }
369
370 #[test]
371 fn test_compress_repetitive_yields_smaller() {
372 let data: Vec<u8> = vec![b'A'; 1000];
373 let compressed = lz_compress(&data);
374 assert!(
377 compressed.len() < 100,
378 "Expected < 100 bytes, got {}",
379 compressed.len()
380 );
381 }
382
383 #[test]
384 fn test_roundtrip_repetitive() {
385 let data: Vec<u8> = vec![b'B'; 500];
386 assert!(lz_roundtrip_ok(&data));
387 }
388
389 #[test]
390 fn test_magic_in_header() {
391 let out = lz_compress(b"test");
392 assert_eq!(&out[..4], b"LZ77");
393 }
394
395 #[test]
396 fn test_decompressed_length_in_header() {
397 let data = b"hello world";
398 let out = lz_compress(data);
399 let stored_len = u32::from_le_bytes([out[4], out[5], out[6], out[7]]) as usize;
400 assert_eq!(stored_len, data.len());
401 }
402
403 #[test]
404 fn test_invalid_magic_errors() {
405 let bad = b"XXXX\x05\x00\x00\x00hello";
406 assert!(lz_decompress(bad).is_err());
407 }
408
409 #[test]
410 fn test_struct_roundtrip() {
411 let lz = LzCompressor::default_compressor();
412 let data = b"hello hello";
413 let c = lz.compress(data);
414 let d = lz.decompress(&c);
415 assert_eq!(d, data);
416 }
417
418 #[test]
419 fn test_compressor_name() {
420 assert_eq!(compressor_name(), "lz77");
421 }
422
423 #[test]
424 fn test_longer_repetitive_text() {
425 let text = b"abcabcabcabcabcabcabcabcabcabc";
426 assert!(lz_roundtrip_ok(text));
427 }
428
429 #[test]
430 fn test_roundtrip_large() {
431 let data: Vec<u8> = (0..1000u32).map(|i| (i % 256) as u8).collect();
432 assert!(lz_roundtrip_ok(&data));
433 }
434}