1use 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
173fn limited_bulk_capacity(data: &[u8], max_output: usize) -> io::Result<usize> {
177 match zstd::zstd_safe::get_frame_content_size(data) {
178 Ok(Some(size)) => {
179 let size = usize::try_from(size).map_err(|_| {
180 io::Error::new(
181 io::ErrorKind::InvalidData,
182 "decompressed size exceeds address space",
183 )
184 })?;
185 if size > max_output {
186 return Err(io::Error::new(
187 io::ErrorKind::InvalidData,
188 "decompressed data exceeds configured limit",
189 ));
190 }
191 Ok(size)
192 }
193 Ok(None) | Err(_) => Ok(max_output.min(DECOMPRESS_CAPACITY)),
196 }
197}
198
199pub fn decompress_limited(data: &[u8], max_output: usize) -> io::Result<Vec<u8>> {
205 let capacity = limited_bulk_capacity(data, max_output)?;
206 thread_local! {
207 static DECOMPRESSOR: std::cell::RefCell<zstd::bulk::Decompressor<'static>> =
208 std::cell::RefCell::new(zstd::bulk::Decompressor::new().unwrap());
209 }
210 DECOMPRESSOR.with(|dc| {
211 dc.borrow_mut().decompress(data, capacity).or_else(|_| {
212 let decoder = zstd::Decoder::new(data)?;
213 read_limited(decoder, max_output)
214 })
215 })
216}
217
218pub fn decompress_with_dict(data: &[u8], dict: &CompressionDict) -> io::Result<Vec<u8>> {
226 thread_local! {
227 static DICT_DC: std::cell::RefCell<Option<(u64, zstd::bulk::Decompressor<'static>)>> =
228 const { std::cell::RefCell::new(None) };
229 }
230 let dict_key = dict.cache_id();
232
233 DICT_DC.with(|cell| {
234 let mut slot = cell.borrow_mut();
235 if slot.as_ref().is_none_or(|(k, _)| *k != dict_key) {
237 let dc = zstd::bulk::Decompressor::with_dictionary(dict.as_bytes())
238 .map_err(io::Error::other)?;
239 *slot = Some((dict_key, dc));
240 }
241 slot.as_mut()
242 .unwrap()
243 .1
244 .decompress(data, DECOMPRESS_CAPACITY)
245 .or_else(|_| {
246 let mut decoder = zstd::Decoder::with_dictionary(data, dict.as_bytes())?;
247 let mut output = Vec::new();
248 io::Read::read_to_end(&mut decoder, &mut output)?;
249 Ok(output)
250 })
251 })
252}
253
254pub fn decompress_with_dict_limited(
256 data: &[u8],
257 dict: &CompressionDict,
258 max_output: usize,
259) -> io::Result<Vec<u8>> {
260 let capacity = limited_bulk_capacity(data, max_output)?;
261 thread_local! {
262 static DICT_DC: std::cell::RefCell<Option<(u64, zstd::bulk::Decompressor<'static>)>> =
263 const { std::cell::RefCell::new(None) };
264 }
265 let dict_key = dict.cache_id();
266
267 DICT_DC.with(|cell| {
268 let mut slot = cell.borrow_mut();
269 if slot.as_ref().is_none_or(|(key, _)| *key != dict_key) {
270 let dc = zstd::bulk::Decompressor::with_dictionary(dict.as_bytes())
271 .map_err(io::Error::other)?;
272 *slot = Some((dict_key, dc));
273 }
274 slot.as_mut()
275 .unwrap()
276 .1
277 .decompress(data, capacity)
278 .or_else(|_| {
279 let decoder = zstd::Decoder::with_dictionary(data, dict.as_bytes())?;
280 read_limited(decoder, max_output)
281 })
282 })
283}
284
285fn read_limited(mut reader: impl Read, max_output: usize) -> io::Result<Vec<u8>> {
286 let read_limit = u64::try_from(max_output)
287 .unwrap_or(u64::MAX)
288 .saturating_add(1);
289 let initial_capacity = max_output.min(DECOMPRESS_CAPACITY);
290 let mut output = Vec::with_capacity(initial_capacity);
291 reader.by_ref().take(read_limit).read_to_end(&mut output)?;
292 if output.len() > max_output {
293 return Err(io::Error::new(
294 io::ErrorKind::InvalidData,
295 "decompressed data exceeds configured limit",
296 ));
297 }
298 Ok(output)
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_roundtrip() {
307 let data = b"Hello, World! This is a test of compression.".repeat(100);
308 let compressed = compress(&data, CompressionLevel::default()).unwrap();
309 let decompressed = decompress(&compressed).unwrap();
310 assert_eq!(data, decompressed.as_slice());
311 assert!(compressed.len() < data.len());
312 }
313
314 #[test]
315 fn test_empty_data() {
316 let data: &[u8] = &[];
317 let compressed = compress(data, CompressionLevel::default()).unwrap();
318 let decompressed = decompress(&compressed).unwrap();
319 assert!(decompressed.is_empty());
320 }
321
322 #[test]
323 fn test_compression_levels() {
324 let data = b"Test data for compression levels".repeat(100);
325 for level in [1, 3, 9, 19] {
326 let compressed = compress(&data, CompressionLevel(level)).unwrap();
327 let decompressed = decompress(&compressed).unwrap();
328 assert_eq!(data.as_slice(), decompressed.as_slice());
329 }
330 }
331
332 #[test]
333 fn test_limited_decompression_rejects_oversized_output() {
334 let data = vec![7u8; 4096];
335 let compressed = compress(&data, CompressionLevel::default()).unwrap();
336 assert!(decompress_limited(&compressed, 1024).is_err());
337 assert_eq!(decompress_limited(&compressed, data.len()).unwrap(), data);
338 }
339
340 #[test]
341 fn bounded_small_block_decoding_does_not_reserve_the_safety_limit() {
342 let payload = vec![7u8; 16 * 1024];
343 let limit = 256 * 1024 * 1024;
344 let compressed = compress(&payload, CompressionLevel::default()).unwrap();
345 let output = decompress_limited(&compressed, limit).unwrap();
346 assert_eq!(output, payload);
347 assert!(
348 output.capacity() <= payload.len(),
349 "small block reserved {} bytes",
350 output.capacity()
351 );
352 let dict = CompressionDict::from_bytes(b"dictionary material".repeat(64));
353 let compressed = compress_with_dict(&payload, CompressionLevel::default(), &dict).unwrap();
354 let output = decompress_with_dict_limited(&compressed, &dict, limit).unwrap();
355 assert_eq!(output, payload);
356 assert!(output.capacity() <= payload.len());
357 }
358
359 #[test]
360 fn bounded_unknown_size_frames_preserve_large_output_and_reject_overflow() {
361 let payload = vec![17u8; DECOMPRESS_CAPACITY * 2];
362 let compressed = zstd::stream::encode_all(payload.as_slice(), 3).unwrap();
363 assert_eq!(
364 zstd::zstd_safe::get_frame_content_size(&compressed).unwrap(),
365 None
366 );
367 assert_eq!(
368 decompress_limited(&compressed, payload.len()).unwrap(),
369 payload
370 );
371 assert!(decompress_limited(&compressed, payload.len() - 1).is_err());
372 let dict = CompressionDict::from_bytes(b"dictionary material".repeat(64));
373 let mut encoder =
374 zstd::stream::Encoder::with_dictionary(Vec::new(), 3, dict.as_bytes()).unwrap();
375 std::io::Write::write_all(&mut encoder, &payload).unwrap();
376 let compressed = encoder.finish().unwrap();
377 assert_eq!(
378 decompress_with_dict_limited(&compressed, &dict, payload.len()).unwrap(),
379 payload
380 );
381 assert!(decompress_with_dict_limited(&compressed, &dict, payload.len() - 1).is_err());
382 }
383
384 #[test]
385 fn test_limited_dictionary_decompression_rejects_oversized_output() {
386 let dict = CompressionDict::from_bytes(b"seven seven seven ".repeat(64));
387 let data = b"seven ".repeat(1024);
388 let compressed = compress_with_dict(&data, CompressionLevel::default(), &dict).unwrap();
389 assert!(decompress_with_dict_limited(&compressed, &dict, 1024).is_err());
391 assert!(decompress_with_dict_limited(&compressed, &dict, data.len() - 1).is_err());
392 assert_eq!(
393 decompress_with_dict_limited(&compressed, &dict, data.len()).unwrap(),
394 data
395 );
396 let mut encoder = zstd::Encoder::with_dictionary(Vec::new(), 3, dict.as_bytes()).unwrap();
398 encoder.include_contentsize(false).unwrap();
399 std::io::Write::write_all(&mut encoder, &data).unwrap();
400 let unknown = encoder.finish().unwrap();
401 assert_eq!(
402 zstd::zstd_safe::get_frame_content_size(&unknown).unwrap(),
403 None
404 );
405 assert!(decompress_with_dict_limited(&unknown, &dict, 1024).is_err());
406 assert!(decompress_with_dict_limited(&unknown, &dict, data.len() - 1).is_err());
407 assert_eq!(
408 decompress_with_dict_limited(&unknown, &dict, data.len()).unwrap(),
409 data
410 );
411 }
412
413 #[test]
414 fn test_dictionary_cache_identity_is_stable_and_unique() {
415 let first = CompressionDict::from_bytes(b"first dictionary material".repeat(16));
416 let first_clone = first.clone();
417 let second = CompressionDict::from_bytes(b"second dictionary material".repeat(16));
418 assert_eq!(first.cache_id(), first_clone.cache_id());
419 assert_ne!(first.cache_id(), second.cache_id());
420
421 let payload = b"dictionary cache switches must rebuild their codec state".repeat(64);
422 for dict in [&first, &second, &first_clone] {
423 let compressed =
424 compress_with_dict(&payload, CompressionLevel::default(), dict).unwrap();
425 assert_eq!(
426 decompress_with_dict_limited(&compressed, dict, payload.len()).unwrap(),
427 payload
428 );
429 }
430 }
431}
432
433#[cfg(test)]
434mod bounded_capacity_tests {
435 use super::*;
436
437 #[test]
438 fn small_bounded_zstd_frames_do_not_reserve_the_entire_safety_limit() {
439 let payload = b"small independently compressed dictionary block".repeat(20);
440 let encoded = compress(&payload, CompressionLevel::BETTER).unwrap();
441 let decoded = decompress_limited(&encoded, 64 * 1024 * 1024).unwrap();
442 assert_eq!(decoded, payload);
443 assert!(
444 decoded.capacity() <= 4096,
445 "capacity={}",
446 decoded.capacity()
447 );
448 }
449
450 #[test]
451 fn small_dictionary_frames_do_not_reserve_the_entire_safety_limit() {
452 let dict = CompressionDict::from_bytes(b"dictionary material for small blocks".repeat(20));
453 let payload = b"dictionary material for small blocks and metadata".repeat(20);
454 let encoded = compress_with_dict(&payload, CompressionLevel::BETTER, &dict).unwrap();
455 let decoded = decompress_with_dict_limited(&encoded, &dict, 64 * 1024 * 1024).unwrap();
456 assert_eq!(decoded, payload);
457 assert!(
458 decoded.capacity() <= 4096,
459 "capacity={}",
460 decoded.capacity()
461 );
462 }
463}
464
465#[cfg(test)]
466mod bounded_frame_tests {
467 use super::*;
468 use std::io::Write;
469
470 fn assert_plain_and_dictionary(encoded: &[u8], expected: &[u8], dict: &CompressionDict) {
471 assert_eq!(
472 decompress_limited(encoded, expected.len()).unwrap(),
473 expected
474 );
475 assert_eq!(
476 decompress_with_dict_limited(encoded, dict, expected.len()).unwrap(),
477 expected
478 );
479 if !expected.is_empty() {
480 assert!(decompress_limited(encoded, expected.len() - 1).is_err());
481 assert!(decompress_with_dict_limited(encoded, dict, expected.len() - 1).is_err());
482 }
483 }
484
485 #[test]
486 fn bounded_zstd_decoding_preserves_empty_unknown_size_and_concatenated_frames() {
487 let dict = CompressionDict::from_bytes(Vec::new());
488 let empty = compress(&[], CompressionLevel::FAST).unwrap();
489 assert_plain_and_dictionary(&empty, &[], &dict);
490 let payload = vec![77u8; DECOMPRESS_CAPACITY * 2 + 17];
491 let mut encoder = zstd::Encoder::new(Vec::new(), 1).unwrap();
492 encoder.include_contentsize(false).unwrap();
493 encoder.write_all(&payload).unwrap();
494 let unknown = encoder.finish().unwrap();
495 assert_eq!(
496 zstd::zstd_safe::get_frame_content_size(&unknown).unwrap(),
497 None
498 );
499 assert_eq!(
500 limited_bulk_capacity(&unknown, 64 * 1024 * 1024).unwrap(),
501 DECOMPRESS_CAPACITY
502 );
503 assert_plain_and_dictionary(&unknown, &payload, &dict);
504 let first = compress(b"first", CompressionLevel::FAST).unwrap();
505 let second = compress(b"second", CompressionLevel::FAST).unwrap();
506 let joined = [first.as_slice(), second.as_slice()].concat();
507 assert_eq!(zstd::decode_all(joined.as_slice()).unwrap(), b"firstsecond");
508 assert_plain_and_dictionary(&joined, b"firstsecond", &dict);
509 let skipped = [
511 0x184D2A50u32.to_le_bytes().as_slice(),
512 3u32.to_le_bytes().as_slice(),
513 b"tag",
514 second.as_slice(),
515 ]
516 .concat();
517 assert_eq!(zstd::decode_all(skipped.as_slice()).unwrap(), b"second");
518 assert_plain_and_dictionary(&skipped, b"second", &dict);
519 }
520
521 #[test]
522 fn bounded_zstd_decoding_does_not_hide_truncation_checksum_or_trailing_corruption() {
523 let payload = b"known size does not prove frame integrity".repeat(100);
524 let mut encoder = zstd::Encoder::new(Vec::new(), 1).unwrap();
525 encoder.include_checksum(true).unwrap();
526 encoder.write_all(&payload).unwrap();
527 let good = encoder.finish().unwrap();
528 let mut bad_checksum = good.clone();
529 *bad_checksum.last_mut().unwrap() ^= 1;
530 let mut trailing = good.clone();
531 trailing.extend_from_slice(b"bad trailing frame");
532 for broken in [
533 &good[..good.len() - 1],
534 bad_checksum.as_slice(),
535 trailing.as_slice(),
536 ] {
537 assert!(zstd::decode_all(broken).is_err());
538 assert!(decompress_limited(broken, payload.len()).is_err());
539 let dict = CompressionDict::from_bytes(Vec::new());
540 assert!(decompress_with_dict_limited(broken, &dict, payload.len()).is_err());
541 assert_eq!(decompress_limited(&good, payload.len()).unwrap(), payload);
542 assert_eq!(
543 decompress_with_dict_limited(&good, &dict, payload.len()).unwrap(),
544 payload
545 );
546 }
547 }
548
549 #[test]
550 fn bounded_dictionary_decoding_preserves_concatenation_and_dictionary_switches() {
551 let first = CompressionDict::from_bytes(b"first raw dictionary contents".repeat(20));
552 let second = CompressionDict::from_bytes(b"second raw dictionary contents".repeat(20));
553 for dict in [&first, &second, &first] {
554 let a = compress_with_dict(
555 b"first raw dictionary contents",
556 CompressionLevel::FAST,
557 dict,
558 )
559 .unwrap();
560 let b = compress_with_dict(
561 b"second raw dictionary contents",
562 CompressionLevel::FAST,
563 dict,
564 )
565 .unwrap();
566 let joined = [a, b].concat();
567 let expected = b"first raw dictionary contentssecond raw dictionary contents";
568 assert_eq!(
569 decompress_with_dict_limited(&joined, dict, expected.len()).unwrap(),
570 expected
571 );
572 assert!(decompress_with_dict_limited(&joined, dict, expected.len() - 1).is_err());
573 }
574 }
575}