1use alloc::collections::VecDeque;
36use alloc::vec::Vec;
37use core::fmt::Debug;
38use std::sync::Mutex;
39
40use crate::crypto::cipher::Payload;
41use crate::enums::CertificateCompressionAlgorithm;
42use crate::msgs::{CertificatePayloadTls13, Codec, CompressedCertificatePayload, SizedPayload};
43use crate::sync::Arc;
44
45pub fn default_cert_decompressors() -> &'static [&'static dyn CertDecompressor] {
48 &[
49 #[cfg(feature = "brotli")]
50 BROTLI_DECOMPRESSOR,
51 #[cfg(feature = "zlib")]
52 ZLIB_DECOMPRESSOR,
53 ]
54}
55
56pub trait CertDecompressor: Debug + Send + Sync {
58 fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed>;
65
66 fn algorithm(&self) -> CertificateCompressionAlgorithm;
68}
69
70pub fn default_cert_compressors() -> &'static [&'static dyn CertCompressor] {
73 &[
74 #[cfg(feature = "brotli")]
75 BROTLI_COMPRESSOR,
76 #[cfg(feature = "zlib")]
77 ZLIB_COMPRESSOR,
78 ]
79}
80
81pub trait CertCompressor: Debug + Send + Sync {
83 fn compress(
92 &self,
93 input: Vec<u8>,
94 level: CompressionLevel,
95 ) -> Result<Vec<u8>, CompressionFailed>;
96
97 fn algorithm(&self) -> CertificateCompressionAlgorithm;
99}
100
101#[non_exhaustive]
103#[derive(Debug, Copy, Clone, Eq, PartialEq)]
104pub enum CompressionLevel {
105 Interactive,
109
110 Amortized,
114}
115
116#[expect(clippy::exhaustive_structs)]
118#[derive(Debug)]
119pub struct DecompressionFailed;
120
121#[expect(clippy::exhaustive_structs)]
123#[derive(Debug)]
124pub struct CompressionFailed;
125
126#[cfg(feature = "zlib")]
127mod feat_zlib_rs {
128 use zlib_rs::{
129 DeflateConfig, Inflate, InflateFlush, ReturnCode, Status, compress_bound, compress_slice,
130 };
131
132 use super::*;
133
134 pub const ZLIB_DECOMPRESSOR: &dyn CertDecompressor = &ZlibRsDecompressor;
136
137 #[derive(Debug)]
138 struct ZlibRsDecompressor;
139
140 impl CertDecompressor for ZlibRsDecompressor {
141 fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed> {
142 let mut state = Inflate::new(true, ZLIB_WINDOW_BITS);
145 match state.decompress(input, output, InflateFlush::Finish) {
146 Ok(Status::StreamEnd)
147 if state.total_in() == input.len() as u64
148 && state.total_out() == output.len() as u64 =>
149 {
150 Ok(())
151 }
152 _ => Err(DecompressionFailed),
153 }
154 }
155
156 fn algorithm(&self) -> CertificateCompressionAlgorithm {
157 CertificateCompressionAlgorithm::Zlib
158 }
159 }
160
161 pub const ZLIB_COMPRESSOR: &dyn CertCompressor = &ZlibRsCompressor;
163
164 #[derive(Debug)]
165 struct ZlibRsCompressor;
166
167 impl CertCompressor for ZlibRsCompressor {
168 fn compress(
169 &self,
170 input: Vec<u8>,
171 level: CompressionLevel,
172 ) -> Result<Vec<u8>, CompressionFailed> {
173 let mut output = alloc::vec![0u8; compress_bound(input.len())];
174 let config = match level {
175 CompressionLevel::Interactive => DeflateConfig::default(),
176 CompressionLevel::Amortized => DeflateConfig::best_compression(),
177 };
178 let (output_filled, rc) = compress_slice(&mut output, &input, config);
179 if rc != ReturnCode::Ok {
180 return Err(CompressionFailed);
181 }
182
183 let used = output_filled.len();
184 output.truncate(used);
185 Ok(output)
186 }
187
188 fn algorithm(&self) -> CertificateCompressionAlgorithm {
189 CertificateCompressionAlgorithm::Zlib
190 }
191 }
192
193 const ZLIB_WINDOW_BITS: u8 = 15;
195}
196
197#[cfg(feature = "zlib")]
198pub use feat_zlib_rs::{ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR};
199
200#[allow(clippy::std_instead_of_core)] #[cfg(feature = "brotli")]
202mod feat_brotli {
203 use std::io::{Cursor, Write};
204
205 use brotli_decompressor::{BrotliDecompressStream, BrotliResult, BrotliState, StandardAlloc};
206
207 use super::*;
208
209 pub const BROTLI_DECOMPRESSOR: &dyn CertDecompressor = &BrotliDecompressor;
211
212 #[derive(Debug)]
213 struct BrotliDecompressor;
214
215 impl CertDecompressor for BrotliDecompressor {
216 fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed> {
217 let mut state = BrotliState::new_strict(
218 StandardAlloc::default(),
219 StandardAlloc::default(),
220 StandardAlloc::default(),
221 );
222 let (mut available_in, mut input_offset) = (input.len(), 0);
223 let (mut available_out, mut output_offset, mut total_out) = (output.len(), 0, 0);
224
225 match BrotliDecompressStream(
226 &mut available_in,
227 &mut input_offset,
228 input,
229 &mut available_out,
230 &mut output_offset,
231 output,
232 &mut total_out,
233 &mut state,
234 ) {
235 BrotliResult::ResultSuccess if available_out == 0 && available_in == 0 => Ok(()),
236 _ => Err(DecompressionFailed),
237 }
238 }
239
240 fn algorithm(&self) -> CertificateCompressionAlgorithm {
241 CertificateCompressionAlgorithm::Brotli
242 }
243 }
244
245 pub const BROTLI_COMPRESSOR: &dyn CertCompressor = &BrotliCompressor;
247
248 #[derive(Debug)]
249 struct BrotliCompressor;
250
251 impl CertCompressor for BrotliCompressor {
252 fn compress(
253 &self,
254 input: Vec<u8>,
255 level: CompressionLevel,
256 ) -> Result<Vec<u8>, CompressionFailed> {
257 let quality = match level {
258 CompressionLevel::Interactive => QUALITY_FAST,
259 CompressionLevel::Amortized => QUALITY_SLOW,
260 };
261 let output = Cursor::new(Vec::with_capacity(input.len() / 2));
262 let mut compressor = brotli::CompressorWriter::new(output, BUFFER_SIZE, quality, LGWIN);
263 compressor
264 .write_all(&input)
265 .map_err(|_| CompressionFailed)?;
266 Ok(compressor.into_inner().into_inner())
267 }
268
269 fn algorithm(&self) -> CertificateCompressionAlgorithm {
270 CertificateCompressionAlgorithm::Brotli
271 }
272 }
273
274 const BUFFER_SIZE: usize = 4096;
278
279 const LGWIN: u32 = 22;
281
282 const QUALITY_FAST: u32 = 4;
285
286 const QUALITY_SLOW: u32 = 11;
288}
289
290#[cfg(feature = "brotli")]
291pub use feat_brotli::{BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR};
292
293#[expect(clippy::exhaustive_enums)]
299#[derive(Debug)]
300pub enum CompressionCache {
301 Disabled,
304
305 Enabled(CompressionCacheInner),
307}
308
309#[derive(Debug)]
313pub struct CompressionCacheInner {
314 size: usize,
316
317 entries: Mutex<VecDeque<Arc<CompressionCacheEntry>>>,
321}
322
323impl CompressionCache {
324 pub fn new(size: usize) -> Self {
327 if size == 0 {
328 return Self::Disabled;
329 }
330
331 Self::Enabled(CompressionCacheInner {
332 size,
333 entries: Mutex::new(VecDeque::with_capacity(size)),
334 })
335 }
336
337 pub(crate) fn compression_for(
343 &self,
344 compressor: &dyn CertCompressor,
345 original: &CertificatePayloadTls13<'_>,
346 ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
347 match self {
348 Self::Disabled => Self::uncached_compression(compressor, original),
349 Self::Enabled(_) => self.compression_for_impl(compressor, original),
350 }
351 }
352
353 fn compression_for_impl(
354 &self,
355 compressor: &dyn CertCompressor,
356 original: &CertificatePayloadTls13<'_>,
357 ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
358 let (max_size, entries) = match self {
359 Self::Enabled(CompressionCacheInner { size, entries }) => (*size, entries),
360 _ => unreachable!(),
361 };
362
363 if !original.context.is_empty() {
366 return Self::uncached_compression(compressor, original);
367 }
368
369 let encoding = original.get_encoding();
371 let algorithm = compressor.algorithm();
372
373 let mut cache = entries
374 .lock()
375 .map_err(|_| CompressionFailed)?;
376 for (i, item) in cache.iter().enumerate() {
377 if item.algorithm == algorithm && item.original == encoding {
378 let item = cache.remove(i).unwrap();
380 cache.push_back(item.clone());
381 return Ok(item);
382 }
383 }
384 drop(cache);
385
386 let uncompressed_len = encoding.len() as u32;
388 let compressed = compressor.compress(encoding.clone(), CompressionLevel::Amortized)?;
389 let new_entry = Arc::new(CompressionCacheEntry {
390 algorithm,
391 original: encoding,
392 compressed: CompressedCertificatePayload {
393 alg: algorithm,
394 uncompressed_len,
395 compressed: SizedPayload::from(Payload::new(compressed)),
396 },
397 });
398
399 let mut cache = entries
401 .lock()
402 .map_err(|_| CompressionFailed)?;
403 if cache.len() == max_size {
404 cache.pop_front();
405 }
406 cache.push_back(new_entry.clone());
407 Ok(new_entry)
408 }
409
410 fn uncached_compression(
412 compressor: &dyn CertCompressor,
413 original: &CertificatePayloadTls13<'_>,
414 ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
415 let algorithm = compressor.algorithm();
416 let encoding = original.get_encoding();
417 let uncompressed_len = encoding.len() as u32;
418 let compressed = compressor.compress(encoding, CompressionLevel::Interactive)?;
419
420 Ok(Arc::new(CompressionCacheEntry {
423 algorithm,
424 original: Vec::new(),
425 compressed: CompressedCertificatePayload {
426 alg: algorithm,
427 uncompressed_len,
428 compressed: SizedPayload::from(Payload::new(compressed)),
429 },
430 }))
431 }
432}
433
434impl Default for CompressionCache {
435 fn default() -> Self {
436 Self::new(4)
438 }
439}
440
441#[derive(Debug)]
442pub(crate) struct CompressionCacheEntry {
443 algorithm: CertificateCompressionAlgorithm,
445 original: Vec<u8>,
446
447 compressed: CompressedCertificatePayload<'static>,
449}
450
451impl CompressionCacheEntry {
452 pub(crate) fn compressed_cert_payload(&self) -> CompressedCertificatePayload<'_> {
453 self.compressed.as_borrowed()
454 }
455}
456
457#[cfg(all(test, any(feature = "brotli", feature = "zlib")))]
458mod tests {
459 use std::{println, vec};
460
461 use super::*;
462
463 #[test]
464 #[cfg(feature = "zlib")]
465 fn test_zlib() {
466 test_compressor(ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR);
467 }
468
469 #[test]
470 #[cfg(feature = "brotli")]
471 fn test_brotli() {
472 test_compressor(BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR);
473 }
474
475 #[test]
476 #[cfg(feature = "brotli")]
477 fn test_brotli_rejects_large_window() {
478 let large_window_empty = [0x11, 0xd0];
479 brotli::BrotliDecompress(&mut &large_window_empty[..], &mut Vec::new()).unwrap();
480 BROTLI_DECOMPRESSOR
481 .decompress(&large_window_empty, &mut [])
482 .unwrap_err();
483 }
484
485 fn test_compressor(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
486 assert_eq!(comp.algorithm(), decomp.algorithm());
487 for sz in [16, 64, 512, 2048, 8192, 16384] {
488 test_trivial_pairwise(comp, decomp, sz);
489 }
490 test_decompress_wrong_len(comp, decomp);
491 test_decompress_trailing_data(comp, decomp);
492 test_decompress_garbage(decomp);
493 }
494
495 fn test_trivial_pairwise(
496 comp: &dyn CertCompressor,
497 decomp: &dyn CertDecompressor,
498 plain_len: usize,
499 ) {
500 let original = vec![0u8; plain_len];
501
502 for level in [CompressionLevel::Interactive, CompressionLevel::Amortized] {
503 let compressed = comp
504 .compress(original.clone(), level)
505 .unwrap();
506 println!(
507 "{:?} compressed trivial {} -> {} using {:?} level",
508 comp.algorithm(),
509 original.len(),
510 compressed.len(),
511 level
512 );
513 let mut recovered = vec![0xffu8; plain_len];
514 decomp
515 .decompress(&compressed, &mut recovered)
516 .unwrap();
517 assert_eq!(original, recovered);
518 }
519 }
520
521 fn test_decompress_wrong_len(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
522 let original = vec![0u8; 2048];
523 let compressed = comp
524 .compress(original.clone(), CompressionLevel::Interactive)
525 .unwrap();
526 println!("{compressed:?}");
527
528 let mut recovered = vec![0xffu8; original.len() + 1];
530 decomp
531 .decompress(&compressed, &mut recovered)
532 .unwrap_err();
533
534 let mut recovered = vec![0xffu8; original.len() - 1];
536 decomp
537 .decompress(&compressed, &mut recovered)
538 .unwrap_err();
539 }
540
541 fn test_decompress_trailing_data(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
542 let original = vec![0u8; 2048];
543 let mut compressed = comp
544 .compress(original.clone(), CompressionLevel::Interactive)
545 .unwrap();
546 compressed.extend_from_slice(b"trailing data");
547
548 let mut recovered = vec![0xffu8; original.len()];
549 decomp
550 .decompress(&compressed, &mut recovered)
551 .unwrap_err();
552 }
553
554 fn test_decompress_garbage(decomp: &dyn CertDecompressor) {
555 let junk = [0u8; 1024];
556 let mut recovered = vec![0u8; 512];
557 decomp
558 .decompress(&junk, &mut recovered)
559 .unwrap_err();
560 }
561
562 #[test]
563 #[cfg(all(feature = "brotli", feature = "zlib"))]
564 fn test_cache_evicts_lru() {
565 use core::sync::atomic::{AtomicBool, Ordering};
566
567 use pki_types::CertificateDer;
568
569 let cache = CompressionCache::default();
570
571 let certs = [CertificateDer::from(vec![1])].into_iter();
572
573 let cert1 = CertificatePayloadTls13::new(certs.clone(), Some(b"1"));
574 let cert2 = CertificatePayloadTls13::new(certs.clone(), Some(b"2"));
575 let cert3 = CertificatePayloadTls13::new(certs.clone(), Some(b"3"));
576 let cert4 = CertificatePayloadTls13::new(certs.clone(), Some(b"4"));
577
578 cache
581 .compression_for(
582 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
583 &cert1,
584 )
585 .unwrap();
586 cache
587 .compression_for(
588 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
589 &cert2,
590 )
591 .unwrap();
592 cache
593 .compression_for(
594 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
595 &cert3,
596 )
597 .unwrap();
598 cache
599 .compression_for(
600 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
601 &cert4,
602 )
603 .unwrap();
604
605 cache
609 .compression_for(
610 &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
611 &cert4,
612 )
613 .unwrap();
614
615 cache
617 .compression_for(
618 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
619 &cert2,
620 )
621 .unwrap();
622 cache
623 .compression_for(
624 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
625 &cert3,
626 )
627 .unwrap();
628 cache
629 .compression_for(
630 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
631 &cert4,
632 )
633 .unwrap();
634 cache
635 .compression_for(
636 &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), false),
637 &cert4,
638 )
639 .unwrap();
640
641 cache
643 .compression_for(
644 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
645 &cert1,
646 )
647 .unwrap();
648
649 cache
652 .compression_for(
653 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
654 &cert4,
655 )
656 .unwrap();
657 cache
658 .compression_for(
659 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
660 &cert3,
661 )
662 .unwrap();
663 cache
664 .compression_for(
665 &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
666 &cert1,
667 )
668 .unwrap();
669
670 cache
673 .compression_for(
674 &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
675 &cert1,
676 )
677 .unwrap();
678
679 cache
681 .compression_for(
682 &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
683 &cert4,
684 )
685 .unwrap();
686
687 #[derive(Debug)]
688 struct RequireCompress(&'static dyn CertCompressor, AtomicBool, bool);
689
690 impl CertCompressor for RequireCompress {
691 fn compress(
692 &self,
693 input: Vec<u8>,
694 level: CompressionLevel,
695 ) -> Result<Vec<u8>, CompressionFailed> {
696 self.1.store(true, Ordering::SeqCst);
697 self.0.compress(input, level)
698 }
699
700 fn algorithm(&self) -> CertificateCompressionAlgorithm {
701 self.0.algorithm()
702 }
703 }
704
705 impl Drop for RequireCompress {
706 fn drop(&mut self) {
707 assert_eq!(self.1.load(Ordering::SeqCst), self.2);
708 }
709 }
710 }
711}