Skip to main content

rustls/
compress.rs

1//! Certificate compression and decompression support
2//!
3//! This crate supports compression and decompression everywhere
4//! certificates are used, in accordance with [RFC 8879][rfc8879].
5//!
6//! Note that this is only supported for TLS1.3 connections.
7//!
8//! # Getting started
9//!
10//! Build this crate with the `brotli` and/or `zlib` crate features.  This
11//! adds dependencies on these crates.  They are used by default if enabled.
12//!
13//! We especially recommend `brotli` as it has the widest deployment so far.
14//!
15//! # Custom compression/decompression implementations
16//!
17//! 1. Implement the [`CertCompressor`] and/or [`CertDecompressor`] traits
18//! 2. Provide those to:
19//!   - [`ClientConfig::cert_compressors`][cc_cc] or [`ServerConfig::cert_compressors`][sc_cc].
20//!   - [`ClientConfig::cert_decompressors`][cc_cd] or [`ServerConfig::cert_decompressors`][sc_cd].
21//!
22//! These are used in these circumstances:
23//!
24//! | Peer | Client authentication | Server authentication |
25//! | ---- | --------------------- | --------------------- |
26//! | *Client* | [`ClientConfig::cert_compressors`][cc_cc] | [`ClientConfig::cert_decompressors`][cc_cd] |
27//! | *Server* | [`ServerConfig::cert_decompressors`][sc_cd] | [`ServerConfig::cert_compressors`][sc_cc] |
28//!
29//! [rfc8879]: https://datatracker.ietf.org/doc/html/rfc8879
30//! [cc_cc]: crate::ClientConfig::cert_compressors
31//! [sc_cc]: crate::ServerConfig::cert_compressors
32//! [cc_cd]: crate::ClientConfig::cert_decompressors
33//! [sc_cd]: crate::ServerConfig::cert_decompressors
34
35#[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
47/// Returns the supported `CertDecompressor` implementations enabled
48/// by crate features.
49pub 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
58/// An available certificate decompression algorithm.
59pub trait CertDecompressor: Debug + Send + Sync {
60    /// Decompress `input`, writing the result to `output`.
61    ///
62    /// `output` is sized to match the declared length of the decompressed data.
63    ///
64    /// `Err(DecompressionFailed)` should be returned if decompression produces more, or fewer
65    /// bytes than fit in `output`, or if the `input` is in any way malformed.
66    fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed>;
67
68    /// Which algorithm this decompressor handles.
69    fn algorithm(&self) -> CertificateCompressionAlgorithm;
70}
71
72/// Returns the supported `CertCompressor` implementations enabled
73/// by crate features.
74pub 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
83/// An available certificate compression algorithm.
84pub trait CertCompressor: Debug + Send + Sync {
85    /// Compress `input`, returning the result.
86    ///
87    /// `input` is consumed by this function so (if the underlying implementation
88    /// supports it) the compression can be performed in-place.
89    ///
90    /// `level` is a hint as to how much effort to expend on the compression.
91    ///
92    /// `Err(CompressionFailed)` may be returned for any reason.
93    fn compress(
94        &self,
95        input: Vec<u8>,
96        level: CompressionLevel,
97    ) -> Result<Vec<u8>, CompressionFailed>;
98
99    /// Which algorithm this compressor handles.
100    fn algorithm(&self) -> CertificateCompressionAlgorithm;
101}
102
103/// A hint for how many resources to dedicate to a compression.
104#[non_exhaustive]
105#[derive(Debug, Copy, Clone, Eq, PartialEq)]
106pub enum CompressionLevel {
107    /// This compression is happening interactively during a handshake.
108    ///
109    /// Implementations may wish to choose a conservative compression level.
110    Interactive,
111
112    /// The compression may be amortized over many connections.
113    ///
114    /// Implementations may wish to choose an aggressive compression level.
115    Amortized,
116}
117
118/// A content-less error for when `CertDecompressor::decompress` fails.
119#[expect(clippy::exhaustive_structs)]
120#[derive(Debug)]
121pub struct DecompressionFailed;
122
123/// A content-less error for when `CertCompressor::compress` fails.
124#[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    /// A certificate decompressor for the Zlib algorithm using the `zlib-rs` crate.
137    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            // `Inflate` rather than `decompress_slice()`, because the latter does not
145            // report how much of `input` the stream used: see `total_in()` below.
146            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    /// A certificate compressor for the Zlib algorithm using the `zlib-rs` crate.
164    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    /// Largest LZ77 window, matching `InflateConfig::default()`.
196    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)] // awaits core::io::Cursor (1.97) in crate MSRV
203#[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    /// A certificate decompressor for the brotli algorithm using the `brotli` crate.
212    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    /// A certificate compressor for the brotli algorithm using the `brotli` crate.
248    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    /// Brotli buffer size.
277    ///
278    /// Chosen based on brotli `examples/compress.rs`.
279    const BUFFER_SIZE: usize = 4096;
280
281    /// This is the default lgwin parameter, see `BrotliEncoderInitParams()`
282    const LGWIN: u32 = 22;
283
284    /// Compression quality we use for interactive compressions.
285    /// See <https://blog.cloudflare.com/results-experimenting-brotli> for data.
286    const QUALITY_FAST: u32 = 4;
287
288    /// Compression quality we use for offline compressions (the maximum).
289    const QUALITY_SLOW: u32 = 11;
290}
291
292#[cfg(all(feature = "brotli", feature = "std"))]
293pub use feat_brotli::{BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR};
294
295/// An LRU cache for compressions.
296///
297/// The prospect of being able to reuse a given compression for many connections
298/// means we can afford to spend more time on that compression (by passing
299/// [`CompressionLevel::Amortized`] to the compressor).
300#[expect(clippy::exhaustive_enums)]
301#[derive(Debug)]
302pub enum CompressionCache {
303    /// No caching happens, and compression happens each time using
304    /// [`CompressionLevel::Interactive`].
305    Disabled,
306
307    /// Compressions are stored in an LRU cache.
308    #[cfg(feature = "std")]
309    Enabled(CompressionCacheInner),
310}
311
312/// Innards of an enabled CompressionCache.
313///
314/// You cannot make one of these directly. Use [`CompressionCache::new`].
315#[cfg(feature = "std")]
316#[derive(Debug)]
317pub struct CompressionCacheInner {
318    /// Maximum size of underlying storage.
319    size: usize,
320
321    /// LRU-order entries.
322    ///
323    /// First is least-used, last is most-used.
324    entries: Mutex<VecDeque<Arc<CompressionCacheEntry>>>,
325}
326
327impl CompressionCache {
328    /// Make a `CompressionCache` that stores up to `size` compressed certificate messages.
329    #[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    /// Return a `CompressionCacheEntry`, which is an owning
342    /// wrapper for a `CompressedCertificatePayload`.
343    ///
344    /// `compressor` is the compression function we have negotiated.
345    /// `original` is the uncompressed certificate message.
346    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        // context is a per-connection quantity, and included in the compressed data.
370        // it is not suitable for inclusion in the cache.
371        if !original.context.is_empty() {
372            return Self::uncached_compression(compressor, original);
373        }
374
375        // cache probe:
376        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                // this item is now MRU
385                let item = cache.remove(i).unwrap();
386                cache.push_back(item.clone());
387                return Ok(item);
388            }
389        }
390        drop(cache);
391
392        // do compression:
393        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        // insert into cache
406        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    /// Compress `original` using `compressor` at `Interactive` level.
417    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        // this `CompressionCacheEntry` in fact never makes it into the cache, so
427        // `original` is left empty
428        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        // 4 entries allows 2 certificate chains times 2 compression algorithms
446        #[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    // cache key is algorithm + original:
458    #[cfg(feature = "std")]
459    algorithm: CertificateCompressionAlgorithm,
460    #[cfg(feature = "std")]
461    original: Vec<u8>,
462
463    // cache value is compression result:
464    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        // too big
545        let mut recovered = vec![0xffu8; original.len() + 1];
546        decomp
547            .decompress(&compressed, &mut recovered)
548            .unwrap_err();
549
550        // too small
551        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        // insert zlib (1), (2), (3), (4)
595
596        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        // -- now full
622
623        // insert brotli (1) evicts zlib (1)
624        cache
625            .compression_for(
626                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
627                &cert4,
628            )
629            .unwrap();
630
631        // now zlib (2), (3), (4) and brotli (4) exist
632        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        // insert zlib (1) requires re-compression & evicts zlib (2)
658        cache
659            .compression_for(
660                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
661                &cert1,
662            )
663            .unwrap();
664
665        // now zlib (1), (3), (4) and brotli (4) exist
666        // query zlib (4), (3), (1) to demonstrate LRU tracks usage rather than insertion
667        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        // now brotli (4), zlib (4), (3), (1)
687        // insert brotli (1) evicting brotli (4)
688        cache
689            .compression_for(
690                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
691                &cert1,
692            )
693            .unwrap();
694
695        // verify brotli (4) disappeared
696        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}