1use std::cmp::Reverse;
38use std::collections::BinaryHeap;
39
40use crate::error::IoError;
41
42use oxiarc_brotli;
44use oxiarc_lz4;
45use oxiarc_zstd;
46
47#[non_exhaustive]
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum CompressionAlgo {
53 None,
55 Rle,
57 DeltaEncoding,
59 Lz77Lite,
61 DictionaryCoding,
63 HuffmanCoding,
65 OxiLz4,
67 OxiZstd,
69 OxiBrotli,
71}
72
73#[derive(Debug, Clone)]
75pub struct DataProfile {
76 pub entropy: f64,
78 pub repetition_rate: f64,
80 pub sorted_fraction: f64,
82 pub n_bytes: usize,
84}
85
86#[derive(Debug, Clone)]
88pub struct CompressionResult {
89 pub algorithm: CompressionAlgo,
91 pub compressed: Vec<u8>,
93 pub original_size: usize,
95 pub compressed_size: usize,
97 pub ratio: f64,
99}
100
101pub fn profile_data(data: &[u8]) -> DataProfile {
107 let n = data.len();
108
109 if n == 0 {
110 return DataProfile {
111 entropy: 0.0,
112 repetition_rate: 0.0,
113 sorted_fraction: 0.0,
114 n_bytes: 0,
115 };
116 }
117
118 let mut freq = [0u64; 256];
120 for &b in data {
121 freq[b as usize] += 1;
122 }
123
124 let n_f = n as f64;
126 let entropy = freq
127 .iter()
128 .filter(|&&c| c > 0)
129 .map(|&c| {
130 let p = c as f64 / n_f;
131 -p * p.log2()
132 })
133 .sum::<f64>();
134
135 if n < 2 {
136 return DataProfile {
137 entropy,
138 repetition_rate: 0.0,
139 sorted_fraction: 0.0,
140 n_bytes: n,
141 };
142 }
143
144 let pairs = (n - 1) as f64;
145 let mut repeated = 0u64;
146 let mut non_decreasing = 0u64;
147 for i in 1..n {
148 if data[i] == data[i - 1] {
149 repeated += 1;
150 }
151 if data[i] >= data[i - 1] {
152 non_decreasing += 1;
153 }
154 }
155
156 DataProfile {
157 entropy,
158 repetition_rate: repeated as f64 / pairs,
159 sorted_fraction: non_decreasing as f64 / pairs,
160 n_bytes: n,
161 }
162}
163
164pub fn recommend_algorithm(profile: &DataProfile) -> CompressionAlgo {
168 if profile.n_bytes < 64 {
169 return CompressionAlgo::None;
170 }
171 if profile.entropy < 1.0 {
172 return CompressionAlgo::HuffmanCoding;
173 }
174 if profile.repetition_rate > 0.7 {
175 return CompressionAlgo::Rle;
176 }
177 if profile.sorted_fraction > 0.8 {
178 return CompressionAlgo::DeltaEncoding;
179 }
180 CompressionAlgo::Lz77Lite
181}
182
183pub fn compress_rle(data: &[u8]) -> Vec<u8> {
189 if data.is_empty() {
190 return Vec::new();
191 }
192 let mut out = Vec::with_capacity(data.len());
193 let mut i = 0;
194 while i < data.len() {
195 let val = data[i];
196 let mut run: usize = 1;
197 while run < 255 {
198 let next = i + run;
199 if next >= data.len() || data[next] != val {
200 break;
201 }
202 run += 1;
203 }
204 out.push(run as u8);
205 out.push(val);
206 i += run;
207 }
208 out
209}
210
211pub fn decompress_rle(data: &[u8]) -> Result<Vec<u8>, IoError> {
213 if !data.len().is_multiple_of(2) {
214 return Err(IoError::DecompressionError(
215 "RLE data must have an even number of bytes".to_string(),
216 ));
217 }
218 let mut out = Vec::new();
219 let mut i = 0;
220 while i + 1 < data.len() {
221 let count = data[i] as usize;
222 let val = data[i + 1];
223 for _ in 0..count {
224 out.push(val);
225 }
226 i += 2;
227 }
228 Ok(out)
229}
230
231pub fn compress_delta(data: &[u8]) -> Vec<u8> {
238 if data.is_empty() {
239 return Vec::new();
240 }
241 let mut out = Vec::with_capacity(data.len());
242 out.push(data[0]);
243 for i in 1..data.len() {
244 let delta = (data[i] as i16 - data[i - 1] as i16).clamp(-127, 127) as i8;
245 out.push(delta as u8);
246 }
247 out
248}
249
250pub fn decompress_delta(data: &[u8]) -> Result<Vec<u8>, IoError> {
252 if data.is_empty() {
253 return Ok(Vec::new());
254 }
255 let mut out = Vec::with_capacity(data.len());
256 out.push(data[0]);
257 for i in 1..data.len() {
258 let delta = data[i] as i8 as i16;
259 let prev = *out
260 .last()
261 .ok_or_else(|| IoError::DecompressionError("delta decode: empty output".to_string()))?
262 as i16;
263 let next = ((prev + delta).rem_euclid(256)) as u8;
264 out.push(next);
265 }
266 Ok(out)
267}
268
269const LZ77_TRIPLET_SIZE: usize = 4; const LZ77_HEADER_SIZE: usize = 4; pub fn compress_lz77(data: &[u8], window_size: usize) -> Vec<u8> {
286 if data.is_empty() {
287 return (0u32).to_le_bytes().to_vec();
289 }
290 let win = window_size.max(1);
291 let mut out = (data.len() as u32).to_le_bytes().to_vec();
293 let mut pos = 0;
294
295 while pos < data.len() {
296 let window_start = pos.saturating_sub(win);
297 let window = &data[window_start..pos];
298
299 let mut best_offset = 0u16;
301 let mut best_len = 0u8;
302
303 if !window.is_empty() {
304 let look_ahead_end = data.len().min(pos + 255);
305 let look_ahead = &data[pos..look_ahead_end];
306 for start in 0..window.len() {
307 let mut mlen = 0usize;
308 while mlen < look_ahead.len()
309 && mlen < 255
310 && start + mlen < window.len()
311 && window[start + mlen] == look_ahead[mlen]
312 {
313 mlen += 1;
314 }
315 if mlen > best_len as usize {
316 best_len = mlen as u8;
317 best_offset = (window.len() - start) as u16;
318 }
319 }
320 }
321
322 let next_pos = pos + best_len as usize;
323 let next_char = if next_pos < data.len() {
324 data[next_pos]
325 } else {
326 0
329 };
330
331 out.extend_from_slice(&best_offset.to_le_bytes());
333 out.push(best_len);
334 out.push(next_char);
335
336 pos += best_len as usize + 1;
337 }
338 out
339}
340
341pub fn decompress_lz77(data: &[u8]) -> Result<Vec<u8>, IoError> {
343 if data.len() < LZ77_HEADER_SIZE {
344 if data.is_empty() {
345 return Ok(Vec::new());
346 }
347 return Err(IoError::DecompressionError(
348 "LZ77 data too short (missing length header)".to_string(),
349 ));
350 }
351
352 let orig_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
353
354 let body = &data[LZ77_HEADER_SIZE..];
355 if !body.len().is_multiple_of(LZ77_TRIPLET_SIZE) {
356 return Err(IoError::DecompressionError(format!(
357 "LZ77 body length {} is not a multiple of {LZ77_TRIPLET_SIZE}",
358 body.len()
359 )));
360 }
361
362 let mut out: Vec<u8> = Vec::with_capacity(orig_len);
363 let mut i = 0;
364 while i + LZ77_TRIPLET_SIZE <= body.len() && out.len() < orig_len {
365 let offset = u16::from_le_bytes([body[i], body[i + 1]]) as usize;
366 let length = body[i + 2] as usize;
367 let next_char = body[i + 3];
368 i += LZ77_TRIPLET_SIZE;
369
370 if offset == 0 && length == 0 {
371 if out.len() < orig_len {
373 out.push(next_char);
374 }
375 } else {
376 if out.len() < offset {
377 return Err(IoError::DecompressionError(format!(
378 "LZ77 back-reference offset {offset} exceeds output length {}",
379 out.len()
380 )));
381 }
382 let start = out.len() - offset;
383 for k in 0..length {
385 if out.len() >= orig_len {
386 break;
387 }
388 let byte = out[start + k];
389 out.push(byte);
390 }
391 if out.len() < orig_len {
393 out.push(next_char);
394 }
395 }
396 }
397 Ok(out)
398}
399
400#[derive(Debug, Eq, PartialEq)]
404struct HuffNode {
405 freq: u64,
406 symbol: Option<u8>,
407 left: Option<Box<HuffNode>>,
408 right: Option<Box<HuffNode>>,
409}
410
411impl Ord for HuffNode {
412 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
413 other
415 .freq
416 .cmp(&self.freq)
417 .then(other.symbol.cmp(&self.symbol))
418 }
419}
420
421impl PartialOrd for HuffNode {
422 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
423 Some(self.cmp(other))
424 }
425}
426
427fn assign_lengths(node: &HuffNode, depth: u8, lengths: &mut [u8; 256]) {
429 if let Some(sym) = node.symbol {
430 lengths[sym as usize] = depth;
431 } else {
432 if let Some(left) = &node.left {
433 assign_lengths(left, depth + 1, lengths);
434 }
435 if let Some(right) = &node.right {
436 assign_lengths(right, depth + 1, lengths);
437 }
438 }
439}
440
441fn canonical_codes(lengths: &[u8; 256]) -> [(u32, u8); 256] {
445 let mut bl_count = [0u32; 33];
447 for &l in lengths.iter() {
448 if l > 0 {
449 bl_count[l as usize] += 1;
450 }
451 }
452
453 let mut next_code = [0u32; 33];
455 let mut code = 0u32;
456 for bits in 1..=32usize {
457 code = (code + bl_count[bits - 1]) << 1;
458 next_code[bits] = code;
459 }
460
461 let mut codes = [(0u32, 0u8); 256];
462 for sym in 0..256usize {
463 let l = lengths[sym];
464 if l > 0 {
465 codes[sym] = (next_code[l as usize], l);
466 next_code[l as usize] += 1;
467 }
468 }
469 codes
470}
471
472pub fn compress_huffman(data: &[u8]) -> Result<Vec<u8>, IoError> {
477 if data.is_empty() {
478 let mut out = vec![0u8; 256];
480 out.push(0u8);
481 return Ok(out);
482 }
483
484 let mut freq = [0u64; 256];
486 for &b in data {
487 freq[b as usize] += 1;
488 }
489
490 let unique: Vec<usize> = (0..256).filter(|&i| freq[i] > 0).collect();
492 if unique.len() == 1 {
493 let mut lengths = [0u8; 256];
495 lengths[unique[0]] = 1;
496 let codes = canonical_codes(&lengths);
497
498 let mut out = lengths.to_vec();
499 let mut bit_buf = 0u64;
500 let mut bit_len = 0u8;
501 let mut encoded = Vec::new();
502 for &b in data {
503 let (c, cl) = codes[b as usize];
504 bit_buf = (bit_buf << cl) | c as u64;
505 bit_len += cl;
506 while bit_len >= 8 {
507 bit_len -= 8;
508 encoded.push((bit_buf >> bit_len) as u8);
509 }
510 }
511 let valid_bits = if bit_len == 0 { 0u8 } else { bit_len };
512 if bit_len > 0 {
513 encoded.push((bit_buf << (8 - bit_len)) as u8);
514 }
515 out.extend_from_slice(&encoded);
516 out.push(valid_bits);
517 return Ok(out);
518 }
519
520 let mut heap: BinaryHeap<HuffNode> = BinaryHeap::new();
522 for sym in 0..256usize {
523 if freq[sym] > 0 {
524 heap.push(HuffNode {
525 freq: freq[sym],
526 symbol: Some(sym as u8),
527 left: None,
528 right: None,
529 });
530 }
531 }
532
533 while heap.len() > 1 {
535 let left = heap.pop().ok_or_else(|| {
536 IoError::CompressionError("empty heap during Huffman build".to_string())
537 })?;
538 let right = heap.pop().ok_or_else(|| {
539 IoError::CompressionError("single-node heap during Huffman build".to_string())
540 })?;
541 heap.push(HuffNode {
542 freq: left.freq + right.freq,
543 symbol: None,
544 left: Some(Box::new(left)),
545 right: Some(Box::new(right)),
546 });
547 }
548
549 let root = heap
550 .pop()
551 .ok_or_else(|| IoError::CompressionError("empty heap after Huffman build".to_string()))?;
552
553 let mut lengths = [0u8; 256];
554 assign_lengths(&root, 0, &mut lengths);
555 if let Some(sym) = root.symbol {
557 lengths[sym as usize] = 1;
558 }
559
560 let codes = canonical_codes(&lengths);
561
562 let mut out = lengths.to_vec(); let mut bit_buf = 0u64;
565 let mut bit_len = 0u8;
566 let mut encoded = Vec::new();
567
568 for &b in data {
569 let (c, cl) = codes[b as usize];
570 if cl == 0 {
571 return Err(IoError::CompressionError(format!(
572 "symbol {b} has zero code length"
573 )));
574 }
575 bit_buf = (bit_buf << cl) | c as u64;
576 bit_len += cl;
577 while bit_len >= 8 {
578 bit_len -= 8;
579 encoded.push(((bit_buf >> bit_len) & 0xFF) as u8);
580 }
581 }
582 let valid_bits = if bit_len == 0 { 0u8 } else { bit_len };
583 if bit_len > 0 {
584 encoded.push(((bit_buf << (8 - bit_len)) & 0xFF) as u8);
585 }
586 out.extend_from_slice(&encoded);
587 out.push(valid_bits);
588 Ok(out)
589}
590
591pub fn decompress_huffman(data: &[u8]) -> Result<Vec<u8>, IoError> {
593 if data.len() < 257 {
595 return Ok(Vec::new());
597 }
598
599 let mut lengths = [0u8; 256];
600 lengths.copy_from_slice(&data[..256]);
601
602 let valid_bits = *data
603 .last()
604 .ok_or_else(|| IoError::DecompressionError("Huffman data too short".to_string()))?;
605 let encoded = &data[256..data.len() - 1];
606
607 let codes = canonical_codes(&lengths);
609
610 let max_bits = lengths.iter().copied().max().unwrap_or(0) as usize;
613 if max_bits == 0 {
614 return Ok(Vec::new());
615 }
616
617 let mut code_table: Vec<(u8, u32, u8)> = (0..256usize)
620 .filter(|&s| lengths[s] > 0)
621 .map(|s| (lengths[s], codes[s].0, s as u8))
622 .collect();
623 code_table.sort();
624
625 let mut out = Vec::new();
627 let mut bit_buf = 0u64;
628 let mut bits_in_buf = 0u8;
629
630 let total_encoded_bits = if encoded.is_empty() {
631 0usize
632 } else {
633 (encoded.len() - 1) * 8
634 + if valid_bits == 0 {
635 8
636 } else {
637 valid_bits as usize
638 }
639 };
640
641 let mut bits_consumed = 0usize;
642
643 for &byte in encoded {
644 bit_buf = (bit_buf << 8) | byte as u64;
645 bits_in_buf += 8;
646
647 'decode: loop {
649 if bits_in_buf == 0 {
650 break;
651 }
652 let remaining_bits = total_encoded_bits.saturating_sub(bits_consumed);
654 if remaining_bits == 0 {
655 break;
656 }
657
658 for &(cl, c, sym) in &code_table {
659 if cl > bits_in_buf {
660 break 'decode;
661 }
662 let top = (bit_buf >> (bits_in_buf - cl)) & ((1u64 << cl) - 1);
663 if top == c as u64 {
664 out.push(sym);
665 bits_in_buf -= cl;
666 bits_consumed += cl as usize;
667 if bits_consumed >= total_encoded_bits {
668 break 'decode;
669 }
670 continue 'decode;
671 }
672 }
673 break;
675 }
676 }
677
678 Ok(out)
679}
680
681pub fn compress_adaptive(data: &[u8]) -> Result<CompressionResult, IoError> {
685 let profile = profile_data(data);
686 let algo = recommend_algorithm(&profile);
687 let original_size = data.len();
688
689 let compressed = match algo {
690 CompressionAlgo::None => data.to_vec(),
691 CompressionAlgo::Rle => compress_rle(data),
692 CompressionAlgo::DeltaEncoding => compress_delta(data),
693 CompressionAlgo::Lz77Lite => compress_lz77(data, 4096),
694 CompressionAlgo::DictionaryCoding => {
695 compress_lz77(data, 4096)
697 }
698 CompressionAlgo::HuffmanCoding => compress_huffman(data)?,
699 CompressionAlgo::OxiLz4 => compress_oxi_lz4(data)?,
700 CompressionAlgo::OxiZstd => compress_oxi_zstd(data)?,
701 CompressionAlgo::OxiBrotli => compress_oxi_brotli(data)?,
702 };
703
704 let compressed_size = compressed.len();
705 let ratio = if original_size == 0 {
706 1.0
707 } else {
708 compressed_size as f64 / original_size as f64
709 };
710
711 Ok(CompressionResult {
712 algorithm: algo,
713 compressed,
714 original_size,
715 compressed_size,
716 ratio,
717 })
718}
719
720pub fn decompress_adaptive(result: &CompressionResult) -> Result<Vec<u8>, IoError> {
722 match result.algorithm {
723 CompressionAlgo::None => Ok(result.compressed.clone()),
724 CompressionAlgo::Rle => decompress_rle(&result.compressed),
725 CompressionAlgo::DeltaEncoding => decompress_delta(&result.compressed),
726 CompressionAlgo::Lz77Lite | CompressionAlgo::DictionaryCoding => {
727 decompress_lz77(&result.compressed)
728 }
729 CompressionAlgo::HuffmanCoding => decompress_huffman(&result.compressed),
730 CompressionAlgo::OxiLz4 => decompress_oxi_lz4(&result.compressed),
731 CompressionAlgo::OxiZstd => decompress_oxi_zstd(&result.compressed),
732 CompressionAlgo::OxiBrotli => decompress_oxi_brotli(&result.compressed),
733 }
734}
735
736pub fn compress_oxi_lz4(data: &[u8]) -> Result<Vec<u8>, IoError> {
740 oxiarc_lz4::compress(data)
741 .map_err(|e| IoError::CompressionError(format!("oxiarc-lz4 compress: {e}")))
742}
743
744pub fn decompress_oxi_lz4(data: &[u8]) -> Result<Vec<u8>, IoError> {
750 const MAX_CAP: usize = 4 * 1024 * 1024 * 1024; const MIN_OUT: usize = 4 * 1024 * 1024; let max_out = data.len().saturating_mul(256).max(MIN_OUT).min(MAX_CAP);
755 oxiarc_lz4::decompress(data, max_out)
756 .map_err(|e| IoError::DecompressionError(format!("oxiarc-lz4 decompress: {e}")))
757}
758
759pub fn compress_oxi_zstd(data: &[u8]) -> Result<Vec<u8>, IoError> {
761 oxiarc_zstd::compress_with_level(data, 3)
762 .map_err(|e| IoError::CompressionError(format!("oxiarc-zstd compress: {e}")))
763}
764
765pub fn decompress_oxi_zstd(data: &[u8]) -> Result<Vec<u8>, IoError> {
767 oxiarc_zstd::decompress(data)
768 .map_err(|e| IoError::DecompressionError(format!("oxiarc-zstd decompress: {e}")))
769}
770
771pub fn compress_oxi_brotli(data: &[u8]) -> Result<Vec<u8>, IoError> {
773 oxiarc_brotli::compress(data, 6)
774 .map_err(|e| IoError::CompressionError(format!("oxiarc-brotli compress: {e}")))
775}
776
777pub fn decompress_oxi_brotli(data: &[u8]) -> Result<Vec<u8>, IoError> {
779 oxiarc_brotli::decompress(data)
780 .map_err(|e| IoError::DecompressionError(format!("oxiarc-brotli decompress: {e}")))
781}
782
783const TAG_NONE: u8 = 0x00;
787const TAG_LZ4: u8 = 0x01;
788const TAG_ZSTD: u8 = 0x02;
789const TAG_BROTLI: u8 = 0x03;
790
791pub fn estimate_entropy(data: &[u8]) -> f64 {
796 if data.is_empty() {
797 return 0.0;
798 }
799 let mut counts = [0u64; 256];
800 for &b in data {
801 counts[b as usize] += 1;
802 }
803 let n = data.len() as f64;
804 let mut entropy = 0.0_f64;
805 for &c in counts.iter() {
806 if c > 0 {
807 let p = c as f64 / n;
808 entropy -= p * p.log2();
809 }
810 }
811 entropy
812}
813
814pub fn select_oxi_algorithm(data: &[u8]) -> (CompressionAlgo, f64) {
825 let size = data.len();
826 if size < 256 {
827 return (CompressionAlgo::None, 1.0);
828 }
829 let entropy = estimate_entropy(data);
830 if entropy > 7.5 {
831 (CompressionAlgo::None, 1.0)
832 } else if entropy > 6.0 {
833 (CompressionAlgo::OxiLz4, 0.8)
834 } else if entropy > 4.0 {
835 (CompressionAlgo::OxiZstd, 0.5)
836 } else {
837 (CompressionAlgo::OxiBrotli, 0.2)
838 }
839}
840
841pub fn auto_compress(data: &[u8]) -> Result<Vec<u8>, IoError> {
851 let (algo, _estimated_ratio) = select_oxi_algorithm(data);
852 let (tag, compressed) = match algo {
853 CompressionAlgo::None => (TAG_NONE, data.to_vec()),
854 CompressionAlgo::OxiLz4 => (TAG_LZ4, compress_oxi_lz4(data)?),
855 CompressionAlgo::OxiZstd => (TAG_ZSTD, compress_oxi_zstd(data)?),
856 CompressionAlgo::OxiBrotli => (TAG_BROTLI, compress_oxi_brotli(data)?),
857 _ => (TAG_NONE, data.to_vec()),
858 };
859 let mut out = Vec::with_capacity(1 + compressed.len());
860 out.push(tag);
861 out.extend_from_slice(&compressed);
862 Ok(out)
863}
864
865pub fn auto_decompress(data: &[u8]) -> Result<Vec<u8>, IoError> {
869 if data.is_empty() {
870 return Ok(Vec::new());
871 }
872 let tag = data[0];
873 let payload = &data[1..];
874 match tag {
875 TAG_NONE => Ok(payload.to_vec()),
876 TAG_LZ4 => decompress_oxi_lz4(payload),
877 TAG_ZSTD => decompress_oxi_zstd(payload),
878 TAG_BROTLI => decompress_oxi_brotli(payload),
879 other => Err(IoError::DecompressionError(format!(
880 "auto_decompress: unknown algorithm tag 0x{other:02x}"
881 ))),
882 }
883}
884
885#[cfg(test)]
888mod tests {
889 use super::*;
890
891 #[test]
894 fn test_rle_roundtrip_simple() {
895 let original = vec![0u8, 0, 0, 1, 1, 2];
896 let compressed = compress_rle(&original);
897 let decompressed = decompress_rle(&compressed).expect("decompress rle");
898 assert_eq!(decompressed, original);
899 }
900
901 #[test]
902 fn test_rle_roundtrip_single() {
903 let original = vec![42u8];
904 let c = compress_rle(&original);
905 assert_eq!(decompress_rle(&c).unwrap(), original);
906 }
907
908 #[test]
909 fn test_rle_roundtrip_empty() {
910 let c = compress_rle(&[]);
911 assert_eq!(decompress_rle(&c).unwrap(), Vec::<u8>::new());
912 }
913
914 #[test]
915 fn test_rle_max_run() {
916 let original: Vec<u8> = vec![7u8; 300]; let c = compress_rle(&original);
918 let d = decompress_rle(&c).unwrap();
919 assert_eq!(d, original);
920 }
921
922 #[test]
925 fn test_delta_roundtrip() {
926 let original = vec![1u8, 2, 3, 4];
927 let c = compress_delta(&original);
928 let d = decompress_delta(&c).unwrap();
929 assert_eq!(d, original);
930 }
931
932 #[test]
933 fn test_delta_empty() {
934 assert_eq!(compress_delta(&[]), Vec::<u8>::new());
935 assert_eq!(decompress_delta(&[]).unwrap(), Vec::<u8>::new());
936 }
937
938 #[test]
939 fn test_delta_roundtrip_with_overflow() {
940 let original = vec![0u8, 200, 100, 50];
943 let c = compress_delta(&original);
944 let d = decompress_delta(&c).unwrap();
945 assert_eq!(d.len(), original.len());
946 }
947
948 #[test]
951 fn test_lz77_roundtrip_short() {
952 let original = b"abcabc".to_vec();
953 let c = compress_lz77(&original, 64);
954 let d = decompress_lz77(&c).unwrap();
955 assert_eq!(d, original);
956 }
957
958 #[test]
959 fn test_lz77_roundtrip_empty() {
960 let c = compress_lz77(&[], 64);
961 let d = decompress_lz77(&c).unwrap();
962 assert_eq!(d, Vec::<u8>::new());
963 }
964
965 #[test]
966 fn test_lz77_roundtrip_repeated() {
967 let original: Vec<u8> = b"aaaa".repeat(10);
968 let c = compress_lz77(&original, 64);
969 let d = decompress_lz77(&c).unwrap();
970 assert_eq!(d, original);
971 }
972
973 #[test]
976 fn test_huffman_roundtrip_ascii() {
977 let original: Vec<u8> = b"hello world this is a huffman test string".to_vec();
978 let original: Vec<u8> = original.iter().copied().cycle().take(100).collect();
979 let c = compress_huffman(&original).unwrap();
980 let d = decompress_huffman(&c).unwrap();
981 assert_eq!(d, original, "Huffman roundtrip failed");
982 }
983
984 #[test]
985 fn test_huffman_empty() {
986 let c = compress_huffman(&[]).unwrap();
987 let d = decompress_huffman(&c).unwrap();
988 assert_eq!(d, Vec::<u8>::new());
989 }
990
991 #[test]
992 fn test_huffman_single_symbol() {
993 let original = vec![0xABu8; 50];
994 let c = compress_huffman(&original).unwrap();
995 let d = decompress_huffman(&c).unwrap();
996 assert_eq!(d, original);
997 }
998
999 #[test]
1002 fn test_recommend_rle_high_repetition() {
1003 let mut data = [0u8; 200];
1009 for i in 150..200 {
1011 data[i] = 1;
1012 }
1013 let profile = DataProfile {
1021 entropy: 2.5,
1022 repetition_rate: 0.85,
1023 sorted_fraction: 0.4,
1024 n_bytes: 200,
1025 };
1026 let algo = recommend_algorithm(&profile);
1027 assert_eq!(algo, CompressionAlgo::Rle);
1028 }
1029
1030 #[test]
1031 fn test_recommend_none_small() {
1032 let data = vec![1u8, 2, 3];
1033 let profile = profile_data(&data);
1034 let algo = recommend_algorithm(&profile);
1035 assert_eq!(algo, CompressionAlgo::None);
1036 }
1037
1038 #[test]
1039 fn test_recommend_delta_sorted() {
1040 let data: Vec<u8> = (0u8..128).chain(0u8..128).collect();
1042 let profile = profile_data(&data);
1043 assert!(
1045 profile.sorted_fraction > 0.8,
1046 "sorted_fraction={}",
1047 profile.sorted_fraction
1048 );
1049 let algo = recommend_algorithm(&profile);
1050 assert_eq!(algo, CompressionAlgo::DeltaEncoding);
1051 }
1052
1053 #[test]
1054 fn test_recommend_huffman_low_entropy() {
1055 let data = vec![42u8; 200];
1057 let profile = profile_data(&data);
1058 assert!(profile.entropy < 1.0);
1059 let algo = recommend_algorithm(&profile);
1060 assert_eq!(algo, CompressionAlgo::HuffmanCoding);
1062 }
1063
1064 #[test]
1067 fn test_adaptive_roundtrip_rle() {
1068 let data: Vec<u8> = vec![99u8; 200];
1069 let result = compress_adaptive(&data).unwrap();
1070 let recovered = decompress_adaptive(&result).unwrap();
1071 assert_eq!(recovered, data);
1074 }
1075
1076 #[test]
1077 fn test_adaptive_roundtrip_mixed() {
1078 let data: Vec<u8> = (0u8..200).collect();
1079 let result = compress_adaptive(&data).unwrap();
1080 let recovered = decompress_adaptive(&result).unwrap();
1081 assert_eq!(recovered, data);
1082 }
1083
1084 #[test]
1087 fn test_profile_uniform() {
1088 let data: Vec<u8> = (0u8..=255).cycle().take(256).collect();
1089 let profile = profile_data(&data);
1090 assert!(
1091 (profile.entropy - 8.0).abs() < 0.01,
1092 "uniform entropy should be ~8 bits"
1093 );
1094 }
1095
1096 #[test]
1097 fn test_profile_empty() {
1098 let profile = profile_data(&[]);
1099 assert_eq!(profile.n_bytes, 0);
1100 assert_eq!(profile.entropy, 0.0);
1101 }
1102
1103 #[test]
1106 fn test_entropy_all_zeros() {
1107 let data = vec![0u8; 1024];
1108 let e = estimate_entropy(&data);
1109 assert!(e < 1e-9, "all-zero entropy should be ~0, got {e}");
1110 }
1111
1112 #[test]
1113 fn test_entropy_uniform() {
1114 let data: Vec<u8> = (0u8..=255).cycle().take(2048).collect();
1116 let e = estimate_entropy(&data);
1117 assert!(
1118 (e - 8.0).abs() < 0.01,
1119 "uniform entropy should be ~8.0, got {e}"
1120 );
1121 }
1122
1123 #[test]
1124 fn test_entropy_empty() {
1125 assert_eq!(estimate_entropy(&[]), 0.0);
1126 }
1127
1128 #[test]
1131 fn test_select_none_high_entropy() {
1132 let data: Vec<u8> = (0u8..=255).cycle().take(2048).collect();
1134 let (algo, _ratio) = select_oxi_algorithm(&data);
1135 assert_eq!(
1136 algo,
1137 CompressionAlgo::None,
1138 "high entropy should select None"
1139 );
1140 }
1141
1142 #[test]
1143 fn test_select_none_small_data() {
1144 let data = vec![42u8; 100]; let (algo, _) = select_oxi_algorithm(&data);
1146 assert_eq!(algo, CompressionAlgo::None, "small data should select None");
1147 }
1148
1149 #[test]
1150 fn test_select_brotli_low_entropy() {
1151 let data = vec![b'A'; 512];
1153 let (algo, _) = select_oxi_algorithm(&data);
1154 assert_eq!(
1155 algo,
1156 CompressionAlgo::OxiBrotli,
1157 "very low entropy should select OxiBrotli"
1158 );
1159 }
1160
1161 #[test]
1162 fn test_select_zstd_structured_data() {
1163 let json = r#"{"key":"value","n":1}"#;
1165 let data: Vec<u8> = json.bytes().cycle().take(1024).collect();
1166 let entropy = estimate_entropy(&data);
1167 let (algo, _) = select_oxi_algorithm(&data);
1169 assert!(
1171 matches!(algo, CompressionAlgo::OxiZstd | CompressionAlgo::OxiBrotli),
1172 "structured repeated data (entropy={entropy:.2}) should select Zstd or Brotli, got {algo:?}"
1173 );
1174 }
1175
1176 #[test]
1179 fn test_oxi_lz4_roundtrip() {
1180 let original: Vec<u8> = b"Hello LZ4! ".iter().copied().cycle().take(1024).collect();
1181 let compressed = compress_oxi_lz4(&original).expect("lz4 compress");
1182 let decompressed = decompress_oxi_lz4(&compressed).expect("lz4 decompress");
1183 assert_eq!(decompressed, original, "LZ4 round-trip mismatch");
1184 }
1185
1186 #[test]
1187 fn test_oxi_zstd_roundtrip() {
1188 let original: Vec<u8> = b"Zstd data! ".iter().copied().cycle().take(1024).collect();
1189 let compressed = compress_oxi_zstd(&original).expect("zstd compress");
1190 let decompressed = decompress_oxi_zstd(&compressed).expect("zstd decompress");
1191 assert_eq!(decompressed, original, "Zstd round-trip mismatch");
1192 }
1193
1194 #[test]
1195 fn test_oxi_brotli_roundtrip() {
1196 let original: Vec<u8> = b"Brotli text! "
1197 .iter()
1198 .copied()
1199 .cycle()
1200 .take(1024)
1201 .collect();
1202 let compressed = compress_oxi_brotli(&original).expect("brotli compress");
1203 let decompressed = decompress_oxi_brotli(&compressed).expect("brotli decompress");
1204 assert_eq!(decompressed, original, "Brotli round-trip mismatch");
1205 }
1206
1207 #[test]
1210 fn test_auto_compress_round_trip_repeated() {
1211 let data: Vec<u8> = b"abcabcabc".iter().copied().cycle().take(2048).collect();
1213 let compressed = auto_compress(&data).expect("auto_compress failed");
1214 assert!(!compressed.is_empty());
1215 let decompressed = auto_decompress(&compressed).expect("auto_decompress failed");
1216 assert_eq!(decompressed, data, "auto round-trip mismatch");
1217 }
1218
1219 #[test]
1220 fn test_auto_compress_empty() {
1221 let result = auto_compress(&[]).expect("auto_compress empty");
1222 assert_eq!(result.len(), 1);
1224 assert_eq!(result[0], TAG_NONE);
1225 let decompressed = auto_decompress(&result).expect("auto_decompress empty");
1226 assert!(decompressed.is_empty());
1227 }
1228
1229 #[test]
1230 fn test_auto_decompress_empty_input() {
1231 let result = auto_decompress(&[]).expect("auto_decompress of empty slice");
1232 assert!(result.is_empty());
1233 }
1234
1235 #[test]
1236 fn test_auto_compress_high_entropy_passthrough() {
1237 let data: Vec<u8> = (0u8..=255).cycle().take(2048).collect();
1239 let compressed = auto_compress(&data).expect("auto_compress high entropy");
1240 assert_eq!(
1242 compressed[0], TAG_NONE,
1243 "high-entropy data should use passthrough tag"
1244 );
1245 let decompressed = auto_decompress(&compressed).expect("auto_decompress");
1246 assert_eq!(decompressed, data);
1247 }
1248
1249 #[test]
1250 fn test_adaptive_compression_json() {
1251 let json = r#"{"id":1,"name":"Alice","score":42.0,"active":true}"#;
1253 let data: Vec<u8> = json.bytes().cycle().take(8192).collect();
1254 let original_size = data.len();
1255 let compressed = auto_compress(&data).expect("auto_compress json");
1256 let ratio = compressed.len() as f64 / original_size as f64;
1257 let decompressed = auto_decompress(&compressed).expect("auto_decompress json");
1259 assert_eq!(decompressed, data, "JSON round-trip mismatch");
1260 let _ = ratio;
1262 }
1263}