Skip to main content

limnifs_core/codec/
fsst_brotli.rs

1//! FSST + Brotli composite codec (id 0x09).
2//!
3//! FSST (Fast Static Symbol Table) is a preprocessor that finds the
4//! most common substrings in a block and replaces each with a single
5//! byte. Brotli then compresses the FSST output. The composition
6//! exploits substring redundancy that Brotli alone misses at high
7//! compression levels (Brotli's window is bounded; FSST's dictionary
8//! is built per-block).
9//!
10//! ## Wire format
11//!
12//! ```text
13//! [u32 LE fsst_compressed_len][fsst_compressed_bytes][brotli_compressed_bytes]
14//! ```
15//!
16//! The FSST section carries its own symbol table; the Brotli section
17//! is a standard Brotli stream of the FSST-escaped text. Reader
18//! reverses: Brotli decompress → FSST expand.
19//!
20//! ## When to use
21//!
22//! CSV/JSON/TSV with strong column-header and value-pattern
23//! redundancy. Plain text and source code do not benefit — Brotli
24//! alone is already optimal there. The `csv_text` categorizer gates
25//! this codec behind a content-sniffing heuristic.
26
27use crate::codec::brotli::DEFAULT_QUALITY;
28use crate::codec::CODEC_FSST_BROTLI;
29use crate::codec::{brotli, Codec};
30use crate::error::CoreError;
31
32/// Codec 0x09 — FSST preprocessor + Brotli.
33pub struct FsstBrotliCodec;
34
35impl Codec for FsstBrotliCodec {
36    fn id(&self) -> u8 {
37        CODEC_FSST_BROTLI
38    }
39    fn name(&self) -> &'static str {
40        "fsst+brotli"
41    }
42
43    fn min_compress_size(&self) -> usize {
44        256
45    }
46
47    fn compress(&self, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
48        compress_with_baseline(plaintext, None)
49    }
50
51    fn decompress(&self, compressed: &[u8], _expected_len: u32) -> Result<Vec<u8>, CoreError> {
52        if compressed.len() < 4 {
53            return Err(CoreError::Corrupt {
54                reason: "fsst+brotli: truncated header".into(),
55            });
56        }
57        let mut len_bytes = [0u8; 4];
58        len_bytes.copy_from_slice(&compressed[..4]);
59        let fsst_len = u32::from_le_bytes(len_bytes) as usize;
60        if fsst_len == 0 {
61            // No-FSST form: the rest is plain Brotli.
62            let brotli_bytes = &compressed[4..];
63            return brotli::decompress_at_quality(brotli_bytes, _expected_len);
64        }
65        if 4 + fsst_len > compressed.len() {
66            return Err(CoreError::Corrupt {
67                reason: format!(
68                    "fsst+brotli: fsst_len {fsst_len} overruns buffer {}",
69                    compressed.len()
70                ),
71            });
72        }
73        let _fsst_bytes = &compressed[4..4 + fsst_len];
74        let brotli_bytes = &compressed[4 + fsst_len..];
75
76        // Decompress Brotli first, then FSST-expand.
77        // We don't know the FSST-escaped length ahead of time; pass
78        // u32::MAX to skip the length check (Brotli stops at stream end).
79        let fsst_escaped = brotli::decompress_at_quality(brotli_bytes, u32::MAX)?;
80        let plaintext = omnizip_fsst::decompress(&fsst_escaped).map_err(fsst_err)?;
81        Ok(plaintext)
82    }
83}
84
85/// Pack a plain-Brotli result with a zero-length FSST prefix so the
86/// reader knows to skip the FSST stage.
87fn pack_no_fsst(brotli_bytes: &[u8]) -> Vec<u8> {
88    let mut out = Vec::with_capacity(4 + brotli_bytes.len());
89    out.extend_from_slice(&0u32.to_le_bytes());
90    out.extend_from_slice(brotli_bytes);
91    out
92}
93
94fn fsst_err(e: omnizip_codecs::OmnizipError) -> CoreError {
95    CoreError::Corrupt {
96        reason: format!("fsst: {e}"),
97    }
98}
99
100/// Compress with an optional pre-computed Brotli baseline.
101///
102/// Callers that already ran Brotli on `plaintext` (e.g.
103/// `process_whole_file_drop`, which compresses every categorizer-routed
104/// file with Brotli first) can pass `Some(brotli_c)` to skip the
105/// redundant Brotli pass that FSST+Brotli would otherwise run for
106/// comparison. The baseline is used as-is — no recompression.
107///
108/// When `baseline` is `None`, the function runs Brotli on `plaintext`
109/// internally (matching the v0.1 behaviour).
110///
111/// # Errors
112///
113/// Returns [`CoreError::Corrupt`] if FSST or Brotli encoding fails.
114pub fn compress_with_baseline(
115    plaintext: &[u8],
116    baseline: Option<&[u8]>,
117) -> Result<Vec<u8>, CoreError> {
118    // Resolve the plain-Brotli baseline: use the caller-provided bytes
119    // if present, else compute it inline.
120    let owned_baseline: Option<Vec<u8>>;
121    let plain_brotli: &[u8] = match baseline {
122        Some(b) => b,
123        None => {
124            let c = crate::codec::codec_call(|| brotli::compress(plaintext, DEFAULT_QUALITY))?;
125            owned_baseline = Some(c);
126            owned_baseline.as_deref().unwrap_or_default()
127        }
128    };
129
130    // Skip FSST for tiny inputs — dictionary overhead exceeds gain.
131    if plaintext.len() < 1024 {
132        return Ok(pack_no_fsst(plain_brotli));
133    }
134
135    // Try FSST + Brotli. If it doesn't beat the plain baseline, fall back.
136    let fsst_compressed =
137        crate::codec::codec_call(|| omnizip_fsst::compress(plaintext).map_err(fsst_err))?;
138    let brotli_input = &fsst_compressed[..];
139    let brotli_compressed =
140        crate::codec::codec_call(|| brotli::compress(brotli_input, DEFAULT_QUALITY))?;
141
142    let composite_len = 4 + brotli_compressed.len() + fsst_compressed.len();
143    if composite_len >= plain_brotli.len() {
144        // Plain Brotli wins; emit the no-FSST form. Clone the baseline
145        // bytes so we own the output regardless of caller lifetime.
146        return Ok(pack_no_fsst(plain_brotli));
147    }
148
149    let mut out = Vec::with_capacity(composite_len);
150    let fsst_len = u32::try_from(fsst_compressed.len()).map_err(|_| CoreError::Corrupt {
151        reason: format!(
152            "fsst+brotli: fsst_compressed length {} exceeds u32",
153            fsst_compressed.len()
154        ),
155    })?;
156    out.extend_from_slice(&fsst_len.to_le_bytes());
157    out.extend_from_slice(&fsst_compressed);
158    out.extend_from_slice(&brotli_compressed);
159    Ok(out)
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn round_trips_csv_like_input() {
168        let input = b"id,name,city\n1,alice,paris\n2,bob,london\n3,carol,paris\n".repeat(200);
169        let c = FsstBrotliCodec;
170        let compressed = c.compress(&input).expect("compress");
171        // Composite must beat plain Brotli on this highly-redundant input
172        // or at least match it (heuristic falls back to plain Brotli).
173        let plain = brotli::compress(&input, DEFAULT_QUALITY).expect("plain brotli");
174        assert!(
175            compressed.len() <= plain.len() + 8,
176            "composite ({}) should not be much worse than plain Brotli ({})",
177            compressed.len(),
178            plain.len()
179        );
180        let recovered = c
181            .decompress(&compressed, input.len() as u32)
182            .expect("decompress");
183        assert_eq!(recovered, input);
184    }
185
186    #[test]
187    fn round_trips_small_input_uses_no_fsst_form() {
188        let input = b"hello world hello world";
189        let c = FsstBrotliCodec;
190        let compressed = c.compress(input).expect("compress");
191        // First 4 bytes are fsst_len; should be 0 for small input.
192        let mut len_bytes = [0u8; 4];
193        len_bytes.copy_from_slice(&compressed[..4]);
194        assert_eq!(u32::from_le_bytes(len_bytes), 0);
195        let recovered = c
196            .decompress(&compressed, input.len() as u32)
197            .expect("decompress");
198        assert_eq!(recovered.as_slice(), input);
199    }
200}