limnifs_core/codec/
fsst_brotli.rs1use crate::codec::brotli::DEFAULT_QUALITY;
28use crate::codec::CODEC_FSST_BROTLI;
29use crate::codec::{brotli, Codec};
30use crate::error::CoreError;
31
32pub 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 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 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
85fn 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
100pub fn compress_with_baseline(
115 plaintext: &[u8],
116 baseline: Option<&[u8]>,
117) -> Result<Vec<u8>, CoreError> {
118 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 if plaintext.len() < 1024 {
132 return Ok(pack_no_fsst(plain_brotli));
133 }
134
135 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 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 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 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}