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