hermes_core/compression/
zstd.rs1use std::io;
9use std::io::Read;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12static NEXT_DICTIONARY_ID: AtomicU64 = AtomicU64::new(1);
13
14#[derive(Debug, Clone, Copy)]
16pub struct CompressionLevel(pub i32);
17
18impl CompressionLevel {
19 pub const FAST: Self = Self(1);
21 pub const DEFAULT: Self = Self(3);
23 pub const BETTER: Self = Self(9);
25 pub const BEST: Self = Self(19);
27 pub const MAX: Self = Self(22);
29}
30
31impl Default for CompressionLevel {
32 fn default() -> Self {
33 Self::FAST }
35}
36
37#[derive(Clone)]
39pub struct CompressionDict {
40 raw_dict: crate::directories::OwnedBytes,
41 cache_id: u64,
45}
46
47impl CompressionDict {
48 pub fn train(samples: &[&[u8]], dict_size: usize) -> io::Result<Self> {
53 let raw_dict = zstd::dict::from_samples(samples, dict_size).map_err(io::Error::other)?;
54 Ok(Self {
55 raw_dict: crate::directories::OwnedBytes::new(raw_dict),
56 cache_id: NEXT_DICTIONARY_ID.fetch_add(1, Ordering::Relaxed),
57 })
58 }
59
60 pub fn from_bytes(bytes: Vec<u8>) -> Self {
62 Self {
63 raw_dict: crate::directories::OwnedBytes::new(bytes),
64 cache_id: NEXT_DICTIONARY_ID.fetch_add(1, Ordering::Relaxed),
65 }
66 }
67
68 pub fn from_owned_bytes(bytes: crate::directories::OwnedBytes) -> Self {
70 Self {
71 raw_dict: bytes,
72 cache_id: NEXT_DICTIONARY_ID.fetch_add(1, Ordering::Relaxed),
73 }
74 }
75
76 pub fn as_bytes(&self) -> &[u8] {
78 self.raw_dict.as_slice()
79 }
80
81 pub fn len(&self) -> usize {
83 self.raw_dict.len()
84 }
85
86 pub fn is_empty(&self) -> bool {
88 self.raw_dict.is_empty()
89 }
90
91 #[inline]
92 fn cache_id(&self) -> u64 {
93 self.cache_id
94 }
95}
96
97pub fn compress(data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>> {
102 thread_local! {
103 static COMPRESSOR: std::cell::RefCell<Option<(i32, zstd::bulk::Compressor<'static>)>> =
104 const { std::cell::RefCell::new(None) };
105 }
106 COMPRESSOR.with(|cell| {
107 let mut slot = cell.borrow_mut();
108 if slot.as_ref().is_none_or(|(l, _)| *l != level.0) {
109 let cmp = zstd::bulk::Compressor::new(level.0).map_err(io::Error::other)?;
110 *slot = Some((level.0, cmp));
111 }
112 slot.as_mut()
113 .unwrap()
114 .1
115 .compress(data)
116 .map_err(io::Error::other)
117 })
118}
119
120pub fn compress_with_dict(
125 data: &[u8],
126 level: CompressionLevel,
127 dict: &CompressionDict,
128) -> io::Result<Vec<u8>> {
129 thread_local! {
130 static DICT_CMP: std::cell::RefCell<Option<(u64, i32, zstd::bulk::Compressor<'static>)>> =
131 const { std::cell::RefCell::new(None) };
132 }
133 let dict_key = dict.cache_id();
134
135 DICT_CMP.with(|cell| {
136 let mut slot = cell.borrow_mut();
137 if slot
138 .as_ref()
139 .is_none_or(|(k, l, _)| *k != dict_key || *l != level.0)
140 {
141 let cmp = zstd::bulk::Compressor::with_dictionary(level.0, dict.as_bytes())
142 .map_err(io::Error::other)?;
143 *slot = Some((dict_key, level.0, cmp));
144 }
145 slot.as_mut()
146 .unwrap()
147 .2
148 .compress(data)
149 .map_err(io::Error::other)
150 })
151}
152
153const DECOMPRESS_CAPACITY: usize = 512 * 1024;
156
157pub fn decompress(data: &[u8]) -> io::Result<Vec<u8>> {
162 thread_local! {
163 static DECOMPRESSOR: std::cell::RefCell<zstd::bulk::Decompressor<'static>> =
164 std::cell::RefCell::new(zstd::bulk::Decompressor::new().unwrap());
165 }
166 DECOMPRESSOR.with(|dc| {
167 dc.borrow_mut()
168 .decompress(data, DECOMPRESS_CAPACITY)
169 .or_else(|_| zstd::decode_all(data))
170 })
171}
172
173pub fn decompress_limited(data: &[u8], max_output: usize) -> io::Result<Vec<u8>> {
179 thread_local! {
180 static DECOMPRESSOR: std::cell::RefCell<zstd::bulk::Decompressor<'static>> =
181 std::cell::RefCell::new(zstd::bulk::Decompressor::new().unwrap());
182 }
183 DECOMPRESSOR.with(|dc| {
184 dc.borrow_mut().decompress(data, max_output).or_else(|_| {
185 let decoder = zstd::Decoder::new(data)?;
186 read_limited(decoder, max_output)
187 })
188 })
189}
190
191pub fn decompress_with_dict(data: &[u8], dict: &CompressionDict) -> io::Result<Vec<u8>> {
199 thread_local! {
200 static DICT_DC: std::cell::RefCell<Option<(u64, zstd::bulk::Decompressor<'static>)>> =
201 const { std::cell::RefCell::new(None) };
202 }
203 let dict_key = dict.cache_id();
205
206 DICT_DC.with(|cell| {
207 let mut slot = cell.borrow_mut();
208 if slot.as_ref().is_none_or(|(k, _)| *k != dict_key) {
210 let dc = zstd::bulk::Decompressor::with_dictionary(dict.as_bytes())
211 .map_err(io::Error::other)?;
212 *slot = Some((dict_key, dc));
213 }
214 slot.as_mut()
215 .unwrap()
216 .1
217 .decompress(data, DECOMPRESS_CAPACITY)
218 .or_else(|_| {
219 let mut decoder = zstd::Decoder::with_dictionary(data, dict.as_bytes())?;
220 let mut output = Vec::new();
221 io::Read::read_to_end(&mut decoder, &mut output)?;
222 Ok(output)
223 })
224 })
225}
226
227pub fn decompress_with_dict_limited(
229 data: &[u8],
230 dict: &CompressionDict,
231 max_output: usize,
232) -> io::Result<Vec<u8>> {
233 thread_local! {
234 static DICT_DC: std::cell::RefCell<Option<(u64, zstd::bulk::Decompressor<'static>)>> =
235 const { std::cell::RefCell::new(None) };
236 }
237 let dict_key = dict.cache_id();
238
239 DICT_DC.with(|cell| {
240 let mut slot = cell.borrow_mut();
241 if slot.as_ref().is_none_or(|(key, _)| *key != dict_key) {
242 let dc = zstd::bulk::Decompressor::with_dictionary(dict.as_bytes())
243 .map_err(io::Error::other)?;
244 *slot = Some((dict_key, dc));
245 }
246 slot.as_mut()
247 .unwrap()
248 .1
249 .decompress(data, max_output)
250 .or_else(|_| {
251 let decoder = zstd::Decoder::with_dictionary(data, dict.as_bytes())?;
252 read_limited(decoder, max_output)
253 })
254 })
255}
256
257fn read_limited(mut reader: impl Read, max_output: usize) -> io::Result<Vec<u8>> {
258 let read_limit = u64::try_from(max_output)
259 .unwrap_or(u64::MAX)
260 .saturating_add(1);
261 let initial_capacity = max_output.min(DECOMPRESS_CAPACITY);
262 let mut output = Vec::with_capacity(initial_capacity);
263 reader.by_ref().take(read_limit).read_to_end(&mut output)?;
264 if output.len() > max_output {
265 return Err(io::Error::new(
266 io::ErrorKind::InvalidData,
267 "decompressed data exceeds configured limit",
268 ));
269 }
270 Ok(output)
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_roundtrip() {
279 let data = b"Hello, World! This is a test of compression.".repeat(100);
280 let compressed = compress(&data, CompressionLevel::default()).unwrap();
281 let decompressed = decompress(&compressed).unwrap();
282 assert_eq!(data, decompressed.as_slice());
283 assert!(compressed.len() < data.len());
284 }
285
286 #[test]
287 fn test_empty_data() {
288 let data: &[u8] = &[];
289 let compressed = compress(data, CompressionLevel::default()).unwrap();
290 let decompressed = decompress(&compressed).unwrap();
291 assert!(decompressed.is_empty());
292 }
293
294 #[test]
295 fn test_compression_levels() {
296 let data = b"Test data for compression levels".repeat(100);
297 for level in [1, 3, 9, 19] {
298 let compressed = compress(&data, CompressionLevel(level)).unwrap();
299 let decompressed = decompress(&compressed).unwrap();
300 assert_eq!(data.as_slice(), decompressed.as_slice());
301 }
302 }
303
304 #[test]
305 fn test_limited_decompression_rejects_oversized_output() {
306 let data = vec![7u8; 4096];
307 let compressed = compress(&data, CompressionLevel::default()).unwrap();
308 assert!(decompress_limited(&compressed, 1024).is_err());
309 assert_eq!(decompress_limited(&compressed, data.len()).unwrap(), data);
310 }
311
312 #[test]
313 fn test_dictionary_cache_identity_is_stable_and_unique() {
314 let first = CompressionDict::from_bytes(b"first dictionary material".repeat(16));
315 let first_clone = first.clone();
316 let second = CompressionDict::from_bytes(b"second dictionary material".repeat(16));
317 assert_eq!(first.cache_id(), first_clone.cache_id());
318 assert_ne!(first.cache_id(), second.cache_id());
319
320 let payload = b"dictionary cache switches must rebuild their codec state".repeat(64);
321 for dict in [&first, &second, &first_clone] {
322 let compressed =
323 compress_with_dict(&payload, CompressionLevel::default(), dict).unwrap();
324 assert_eq!(
325 decompress_with_dict_limited(&compressed, dict, payload.len()).unwrap(),
326 payload
327 );
328 }
329 }
330}