Skip to main content

scirs2_io/adaptive_compression/
mod.rs

1//! Adaptive compression — automatically selects the best compression algorithm
2//! based on data characteristics profiled at runtime.
3//!
4//! # Algorithm families
5//!
6//! ## Internal pure-Rust codecs
7//!
8//! | Variant | Best for |
9//! |---------|----------|
10//! | `None` | Tiny buffers (< 64 bytes) |
11//! | `Rle` | Buffers with many consecutive repeated bytes |
12//! | `DeltaEncoding` | Nearly-sorted numeric sequences |
13//! | `Lz77Lite` | General-purpose mixed data |
14//! | `DictionaryCoding` | Small alphabets / high-entropy repeated tokens |
15//! | `HuffmanCoding` | Very low-entropy data |
16//!
17//! ## OxiARC-backed codecs (COOLJAPAN policy)
18//!
19//! | Variant | Crate | Best for |
20//! |---------|-------|----------|
21//! | `OxiLz4` | `oxiarc-lz4` | Moderate-entropy data; maximise speed |
22//! | `OxiZstd` | `oxiarc-zstd` | Structured data (JSON, CSV); balance speed/ratio |
23//! | `OxiBrotli` | `oxiarc-brotli` | Low-entropy text; maximise compression ratio |
24//!
25//! ## Auto-compress / auto-decompress
26//!
27//! The `auto_compress` and `auto_decompress` functions select among the three
28//! OxiARC codecs based on Shannon entropy and prepend a 1-byte codec tag so
29//! that `auto_decompress` can round-trip correctly without out-of-band metadata.
30//!
31//! Tag byte layout:
32//! - `0x00` — passthrough (no compression)
33//! - `0x01` — OxiLz4
34//! - `0x02` — OxiZstd
35//! - `0x03` — OxiBrotli
36
37use std::cmp::Reverse;
38use std::collections::BinaryHeap;
39
40use crate::error::IoError;
41
42// OxiARC imports (COOLJAPAN policy: no flate2/zstd/brotli system crates)
43use oxiarc_brotli;
44use oxiarc_lz4;
45use oxiarc_zstd;
46
47// ─── Public types ─────────────────────────────────────────────────────────────
48
49/// Identifies which algorithm was used to produce a `CompressionResult`.
50#[non_exhaustive]
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum CompressionAlgo {
53    /// No compression — data is stored verbatim.
54    None,
55    /// Run-length encoding: `(count, value)` pairs.
56    Rle,
57    /// Delta encoding: first byte + signed deltas.
58    DeltaEncoding,
59    /// Simplified LZ77 sliding-window compression.
60    Lz77Lite,
61    /// Dictionary coding: symbol table + indices.
62    DictionaryCoding,
63    /// Huffman coding: variable-length prefix codes.
64    HuffmanCoding,
65    /// LZ4 via `oxiarc-lz4` (COOLJAPAN policy). Fast, moderate compression.
66    OxiLz4,
67    /// Zstandard via `oxiarc-zstd` (COOLJAPAN policy). Balanced speed/ratio.
68    OxiZstd,
69    /// Brotli via `oxiarc-brotli` (COOLJAPAN policy). High compression for text.
70    OxiBrotli,
71}
72
73/// Statistical profile of a byte buffer used to guide algorithm selection.
74#[derive(Debug, Clone)]
75pub struct DataProfile {
76    /// Shannon entropy (bits per symbol), range [0, 8].
77    pub entropy: f64,
78    /// Fraction of bytes equal to their predecessor (run-length signal).
79    pub repetition_rate: f64,
80    /// Fraction of consecutive pairs that are non-decreasing (sorted-ness signal).
81    pub sorted_fraction: f64,
82    /// Total byte count.
83    pub n_bytes: usize,
84}
85
86/// The output of a compression operation together with metadata.
87#[derive(Debug, Clone)]
88pub struct CompressionResult {
89    /// Algorithm used.
90    pub algorithm: CompressionAlgo,
91    /// Compressed payload bytes.
92    pub compressed: Vec<u8>,
93    /// Original size before compression.
94    pub original_size: usize,
95    /// Size after compression.
96    pub compressed_size: usize,
97    /// `compressed_size / original_size`; lower is better.
98    pub ratio: f64,
99}
100
101// ─── Profile data ─────────────────────────────────────────────────────────────
102
103/// Compute a `DataProfile` for the given byte slice.
104///
105/// Uses Shannon entropy, repetition rate, and sorted-ness fraction.
106pub 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    // Build frequency table.
119    let mut freq = [0u64; 256];
120    for &b in data {
121        freq[b as usize] += 1;
122    }
123
124    // Shannon entropy H = -Σ p log₂ p
125    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
164// ─── Algorithm selection ──────────────────────────────────────────────────────
165
166/// Heuristic algorithm selector based on a `DataProfile`.
167pub 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
183// ─── RLE ──────────────────────────────────────────────────────────────────────
184
185/// Compress using run-length encoding.
186///
187/// Output format: repeated `(count: u8, value: u8)` pairs. Maximum run length is 255.
188pub 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
211/// Decompress RLE-compressed data.
212pub 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
231// ─── Delta encoding ───────────────────────────────────────────────────────────
232
233/// Compress using delta encoding.
234///
235/// Output: `[first_byte, delta_1 as i8 as u8, delta_2 as i8 as u8, ...]`
236/// Deltas that overflow i8 are clamped to ±127.
237pub 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
250/// Decompress delta-encoded data.
251pub 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
269// ─── LZ77 (simplified) ────────────────────────────────────────────────────────
270
271// LZ77 format:
272//   Header: 4 bytes LE u32 = original data length
273//   Body:   4-byte triplets (offset: u16 LE, match_len: u8, next_char: u8)
274//     - offset == 0 && match_len == 0  → literal: emit next_char
275//     - otherwise                      → back-ref of `match_len` bytes at -offset, then emit next_char
276// The decompressor stops once `orig_len` bytes have been emitted (handles final
277// triplet where next_char would overshoot).
278const LZ77_TRIPLET_SIZE: usize = 4; // offset: u16 (2) + length: u8 (1) + next_char: u8 (1)
279const LZ77_HEADER_SIZE: usize = 4; // u32 LE original length
280
281/// Compress using a simplified LZ77 sliding-window algorithm.
282///
283/// Output: 4-byte LE original-length header followed by
284/// `(offset: u16 LE, match_len: u8, next_byte: u8)` triplets.
285pub fn compress_lz77(data: &[u8], window_size: usize) -> Vec<u8> {
286    if data.is_empty() {
287        // Header: 0 length, no triplets.
288        return (0u32).to_le_bytes().to_vec();
289    }
290    let win = window_size.max(1);
291    // Write original-length header.
292    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        // Find the longest match inside the window.
300        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            // Past end — emit a dummy char but the length header will
327            // prevent the decompressor from including it.
328            0
329        };
330
331        // Emit triplet.
332        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
341/// Decompress LZ77-compressed data.
342pub 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            // Literal token.
372            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            // Copy match (may overlap), respecting orig_len cap.
384            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            // Emit trailing char if still under limit.
392            if out.len() < orig_len {
393                out.push(next_char);
394            }
395        }
396    }
397    Ok(out)
398}
399
400// ─── Huffman coding ───────────────────────────────────────────────────────────
401
402/// Huffman tree node used during code construction.
403#[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        // Min-heap by frequency, tie-break by symbol for determinism.
414        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
427/// Assign canonical code lengths by DFS traversal of the Huffman tree.
428fn 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
441/// Build canonical Huffman codes from code lengths.
442///
443/// Returns a table `codes[sym] = (bit_pattern: u32, bit_length: u8)`.
444fn canonical_codes(lengths: &[u8; 256]) -> [(u32, u8); 256] {
445    // Count number of codes of each length.
446    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    // Find starting code for each length (canonical Huffman).
454    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
472/// Compress `data` using canonical Huffman coding.
473///
474/// Header: 256 bytes of code lengths, then bit-packed encoded data, then a
475/// 1-byte trailer giving the number of valid bits in the final byte.
476pub fn compress_huffman(data: &[u8]) -> Result<Vec<u8>, IoError> {
477    if data.is_empty() {
478        // Header + 0 valid bits indicator.
479        let mut out = vec![0u8; 256];
480        out.push(0u8);
481        return Ok(out);
482    }
483
484    // Frequency table.
485    let mut freq = [0u64; 256];
486    for &b in data {
487        freq[b as usize] += 1;
488    }
489
490    // Edge case: only one unique symbol.
491    let unique: Vec<usize> = (0..256).filter(|&i| freq[i] > 0).collect();
492    if unique.len() == 1 {
493        // Assign length 1 to the single symbol.
494        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    // Build min-heap of leaf nodes.
521    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    // Build Huffman tree.
534    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    // Root-only case: fix up length.
556    if let Some(sym) = root.symbol {
557        lengths[sym as usize] = 1;
558    }
559
560    let codes = canonical_codes(&lengths);
561
562    // Bit-pack the encoded data.
563    let mut out = lengths.to_vec(); // 256-byte header
564    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
591/// Decompress Huffman-coded data.
592pub fn decompress_huffman(data: &[u8]) -> Result<Vec<u8>, IoError> {
593    // Minimum: 256-byte header + 1-byte valid_bits trailer.
594    if data.len() < 257 {
595        // Treat as empty.
596        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    // Rebuild canonical codes.
608    let codes = canonical_codes(&lengths);
609
610    // Build a decode table: (bit_pattern, bit_len) → symbol.
611    // For codes with length ≤ 16 bits, build a lookup table.
612    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    // Simple canonical decode: scan bits one at a time.
618    // Build sorted list of (len, code, sym).
619    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    // Decode bit stream.
626    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        // Try to decode symbols.
648        'decode: loop {
649            if bits_in_buf == 0 {
650                break;
651            }
652            // Determine remaining bits this is the last byte.
653            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            // No match found — padding bits at end of stream.
674            break;
675        }
676    }
677
678    Ok(out)
679}
680
681// ─── Adaptive compress / decompress ──────────────────────────────────────────
682
683/// Profile `data`, select the best algorithm, compress, and return a `CompressionResult`.
684pub 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            // Fallback to LZ77 for dictionary; full implementation omitted for size.
696            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
720/// Decompress a `CompressionResult` using the stored algorithm tag.
721pub 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
736// ─── OxiARC-backed codec wrappers ─────────────────────────────────────────────
737
738/// Compress `data` using LZ4 via `oxiarc-lz4`.
739pub 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
744/// Decompress LZ4-frame data via `oxiarc-lz4`.
745///
746/// Uses a generous output size bound: `max(data.len() * 256, 4 MiB)`.
747/// LZ4 frames store the original content size in the frame descriptor when
748/// it is known, but the pure-Rust decompressor still requires an upper bound.
749pub fn decompress_oxi_lz4(data: &[u8]) -> Result<Vec<u8>, IoError> {
750    // Use a large upper bound: compressed data is rarely more than 256× smaller,
751    // and we cap at 4 GiB to avoid absurd allocations on corrupt input.
752    const MAX_CAP: usize = 4 * 1024 * 1024 * 1024; // 4 GiB absolute cap
753    const MIN_OUT: usize = 4 * 1024 * 1024; // 4 MiB floor
754    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
759/// Compress `data` using Zstandard level 3 via `oxiarc-zstd`.
760pub 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
765/// Decompress Zstandard-frame data via `oxiarc-zstd`.
766pub 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
771/// Compress `data` using Brotli quality 6 via `oxiarc-brotli`.
772pub 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
777/// Decompress Brotli data via `oxiarc-brotli`.
778pub 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
783// ─── Entropy-based OxiARC codec selection ─────────────────────────────────────
784
785/// Tag byte values stored as the first byte by `auto_compress`.
786const TAG_NONE: u8 = 0x00;
787const TAG_LZ4: u8 = 0x01;
788const TAG_ZSTD: u8 = 0x02;
789const TAG_BROTLI: u8 = 0x03;
790
791/// Estimate Shannon entropy of a byte slice.
792///
793/// Returns a value in `[0.0, 8.0]` — 0.0 for all-same bytes, 8.0 for
794/// a perfectly uniform distribution over all 256 byte values.
795pub 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
814/// Choose the best OxiARC codec based on entropy and data size.
815///
816/// Returns the `CompressionAlgo` variant and an estimated compression ratio.
817///
818/// Thresholds (COOLJAPAN policy):
819/// - Size < 256 bytes → `None` (overhead not worth it)
820/// - Entropy > 7.5    → `None` (near-random / encrypted data)
821/// - Entropy > 6.0    → `OxiLz4` (fast; moderate entropy)
822/// - Entropy > 4.0    → `OxiZstd` (balanced; structured data like JSON/CSV)
823/// - Otherwise        → `OxiBrotli` (max ratio; low-entropy text / XML / HTML)
824pub 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
841/// Automatically compress `data` using the best OxiARC codec.
842///
843/// The output has a 1-byte algorithm tag prepended:
844/// - `0x00` — no compression
845/// - `0x01` — LZ4 (`oxiarc-lz4`)
846/// - `0x02` — Zstd (`oxiarc-zstd`)
847/// - `0x03` — Brotli (`oxiarc-brotli`)
848///
849/// Use `auto_decompress` to reverse the operation.
850pub 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
865/// Automatically decompress data produced by `auto_compress`.
866///
867/// Reads the 1-byte algorithm tag and dispatches to the correct decompressor.
868pub 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// ─── Tests ───────────────────────────────────────────────────────────────────
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890
891    // ── RLE ──────────────────────────────────────────────────────────────────
892
893    #[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]; // > 255 run
917        let c = compress_rle(&original);
918        let d = decompress_rle(&c).unwrap();
919        assert_eq!(d, original);
920    }
921
922    // ── Delta encoding ────────────────────────────────────────────────────────
923
924    #[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        // Large jumps clamped to ±127 — after decompress values will drift.
941        // Just check no panic and we get the same length.
942        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    // ── LZ77 ─────────────────────────────────────────────────────────────────
949
950    #[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    // ── Huffman ───────────────────────────────────────────────────────────────
974
975    #[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    // ── recommend_algorithm ───────────────────────────────────────────────────
1000
1001    #[test]
1002    fn test_recommend_rle_high_repetition() {
1003        // Use alternating bytes so entropy > 1.0 but repetition is low.
1004        // Instead, use a pattern with high repetition AND entropy > 1.0:
1005        // two alternating distinct bytes — entropy ≈ 1.0, repetition = 0.
1006        // For pure repetition test use many-value high-rep data:
1007        // 70% same value mixed with other values.
1008        let mut data = [0u8; 200];
1009        // 150 zeros + 50 ones → repetition_rate high, entropy > 1.0
1010        for i in 150..200 {
1011            data[i] = 1;
1012        }
1013        // repetition rate: 149 pairs (0,0) out of 199 + 49 pairs (1,1) = 198/199 ≈ 0.995
1014        // Wait, that still has entropy = -(0.75 log2 0.75 + 0.25 log2 0.25) ≈ 0.81 < 1.0
1015        // So we'd still get Huffman. Use nearly equal mix to push entropy > 1.0:
1016        // 120 zeros + 80 ones → H ≈ 0.971 bit
1017        // Better: 105 zeros + 95 ones → H ≈ 0.999 bit
1018        // Even better: 3 bytes with many runs.
1019        // Use explicit profile to test the rule directly:
1020        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        // Nearly-sorted: 0,1,2,...,127 repeated twice → sorted fraction ≈ 1.0
1041        let data: Vec<u8> = (0u8..128).chain(0u8..128).collect();
1042        let profile = profile_data(&data);
1043        // sorted_fraction is high
1044        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        // All same byte → entropy = 0.
1056        let data = vec![42u8; 200];
1057        let profile = profile_data(&data);
1058        assert!(profile.entropy < 1.0);
1059        let algo = recommend_algorithm(&profile);
1060        // entropy < 1.0 → Huffman (before repetition check)
1061        assert_eq!(algo, CompressionAlgo::HuffmanCoding);
1062    }
1063
1064    // ── adaptive ─────────────────────────────────────────────────────────────
1065
1066    #[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        // RLE picks up here; for all-same, Huffman is recommended (entropy=0 < 1.0)
1072        // Either way, roundtrip must hold.
1073        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    // ── profile_data ──────────────────────────────────────────────────────────
1085
1086    #[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    // ── estimate_entropy ─────────────────────────────────────────────────────
1104
1105    #[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        // All 256 byte values equally represented → entropy ≈ 8.0
1115        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    // ── select_oxi_algorithm ──────────────────────────────────────────────────
1129
1130    #[test]
1131    fn test_select_none_high_entropy() {
1132        // Near-random data: uniform distribution → entropy ≈ 8.0 → None
1133        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]; // < 256 bytes
1145        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        // All-same bytes: entropy = 0 → OxiBrotli
1152        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        // JSON-like: 4 symbol alphabet repeated → entropy ~ 2 bits
1164        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        // Entropy of typical repeated JSON is in [3, 5] range
1168        let (algo, _) = select_oxi_algorithm(&data);
1169        // Allow OxiZstd or OxiBrotli depending on actual entropy
1170        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    // ── OxiARC round-trips ────────────────────────────────────────────────────
1177
1178    #[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    // ── auto_compress / auto_decompress ───────────────────────────────────────
1208
1209    #[test]
1210    fn test_auto_compress_round_trip_repeated() {
1211        // Low-entropy repeated data — should pick OxiBrotli or OxiZstd
1212        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        // Empty input: tag byte 0x00 + empty payload
1223        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        // Uniform data → None tag → payload = data verbatim
1238        let data: Vec<u8> = (0u8..=255).cycle().take(2048).collect();
1239        let compressed = auto_compress(&data).expect("auto_compress high entropy");
1240        // Tag should be TAG_NONE
1241        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        // Simulates a repeated JSON payload; expect meaningful compression.
1252        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        // We allow ratio ≥ 1.0 for degenerate cases; just verify round-trip.
1258        let decompressed = auto_decompress(&compressed).expect("auto_decompress json");
1259        assert_eq!(decompressed, data, "JSON round-trip mismatch");
1260        // Optional: log ratio (not a hard assertion since codecs vary)
1261        let _ = ratio;
1262    }
1263}