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 [RFC8879][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::enums::CertificateCompressionAlgorithm;
43use crate::msgs::base::{Payload, PayloadU24};
44use crate::msgs::codec::Codec;
45use crate::msgs::handshake::{CertificatePayloadTls13, CompressedCertificatePayload};
46use crate::sync::Arc;
47
48/// Returns the supported `CertDecompressor` implementations enabled
49/// by crate features.
50pub fn default_cert_decompressors() -> &'static [&'static dyn CertDecompressor] {
51    &[
52        #[cfg(feature = "brotli")]
53        BROTLI_DECOMPRESSOR,
54        #[cfg(feature = "zlib")]
55        ZLIB_DECOMPRESSOR,
56    ]
57}
58
59/// An available certificate decompression algorithm.
60pub trait CertDecompressor: Debug + Send + Sync {
61    /// Decompress `input`, writing the result to `output`.
62    ///
63    /// `output` is sized to match the declared length of the decompressed data.
64    ///
65    /// `Err(DecompressionFailed)` should be returned if decompression produces more, or fewer
66    /// bytes than fit in `output`, or if the `input` is in any way malformed.
67    fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed>;
68
69    /// Which algorithm this decompressor handles.
70    fn algorithm(&self) -> CertificateCompressionAlgorithm;
71}
72
73/// Returns the supported `CertCompressor` implementations enabled
74/// by crate features.
75pub fn default_cert_compressors() -> &'static [&'static dyn CertCompressor] {
76    &[
77        #[cfg(feature = "brotli")]
78        BROTLI_COMPRESSOR,
79        #[cfg(feature = "zlib")]
80        ZLIB_COMPRESSOR,
81    ]
82}
83
84/// An available certificate compression algorithm.
85pub trait CertCompressor: Debug + Send + Sync {
86    /// Compress `input`, returning the result.
87    ///
88    /// `input` is consumed by this function so (if the underlying implementation
89    /// supports it) the compression can be performed in-place.
90    ///
91    /// `level` is a hint as to how much effort to expend on the compression.
92    ///
93    /// `Err(CompressionFailed)` may be returned for any reason.
94    fn compress(
95        &self,
96        input: Vec<u8>,
97        level: CompressionLevel,
98    ) -> Result<Vec<u8>, CompressionFailed>;
99
100    /// Which algorithm this compressor handles.
101    fn algorithm(&self) -> CertificateCompressionAlgorithm;
102}
103
104/// A hint for how many resources to dedicate to a compression.
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#[derive(Debug)]
120pub struct DecompressionFailed;
121
122/// A content-less error for when `CertCompressor::compress` fails.
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    /// A certificate decompressor for the Zlib algorithm using the `zlib-rs` crate.
135    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            // `Inflate` rather than `decompress_slice()`, because the latter does not
143            // report how much of `input` the stream used: see `total_in()` below.
144            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    /// A certificate compressor for the Zlib algorithm using the `zlib-rs` crate.
162    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    /// Largest LZ77 window, matching `InflateConfig::default()`.
194    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)] // awaits core::io::Cursor (1.97) in crate MSRV
201#[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    /// A certificate decompressor for the brotli algorithm using the `brotli` crate.
210    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    /// A certificate compressor for the brotli algorithm using the `brotli` crate.
246    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    /// Brotli buffer size.
275    ///
276    /// Chosen based on brotli `examples/compress.rs`.
277    const BUFFER_SIZE: usize = 4096;
278
279    /// This is the default lgwin parameter, see `BrotliEncoderInitParams()`
280    const LGWIN: u32 = 22;
281
282    /// Compression quality we use for interactive compressions.
283    /// See <https://blog.cloudflare.com/results-experimenting-brotli> for data.
284    const QUALITY_FAST: u32 = 4;
285
286    /// Compression quality we use for offline compressions (the maximum).
287    const QUALITY_SLOW: u32 = 11;
288}
289
290#[cfg(feature = "brotli")]
291pub use feat_brotli::{BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR};
292
293/// An LRU cache for compressions.
294///
295/// The prospect of being able to reuse a given compression for many connections
296/// means we can afford to spend more time on that compression (by passing
297/// `CompressionLevel::Amortized` to the compressor).
298#[derive(Debug)]
299pub enum CompressionCache {
300    /// No caching happens, and compression happens each time using
301    /// `CompressionLevel::Interactive`.
302    Disabled,
303
304    /// Compressions are stored in an LRU cache.
305    #[cfg(feature = "std")]
306    Enabled(CompressionCacheInner),
307}
308
309/// Innards of an enabled CompressionCache.
310///
311/// You cannot make one of these directly. Use [`CompressionCache::new`].
312#[cfg(feature = "std")]
313#[derive(Debug)]
314pub struct CompressionCacheInner {
315    /// Maximum size of underlying storage.
316    size: usize,
317
318    /// LRU-order entries.
319    ///
320    /// First is least-used, last is most-used.
321    entries: Mutex<VecDeque<Arc<CompressionCacheEntry>>>,
322}
323
324impl CompressionCache {
325    /// Make a `CompressionCache` that stores up to `size` compressed
326    /// certificate messages.
327    #[cfg(feature = "std")]
328    pub fn new(size: usize) -> Self {
329        if size == 0 {
330            return Self::Disabled;
331        }
332
333        Self::Enabled(CompressionCacheInner {
334            size,
335            entries: Mutex::new(VecDeque::with_capacity(size)),
336        })
337    }
338
339    /// Return a `CompressionCacheEntry`, which is an owning
340    /// wrapper for a `CompressedCertificatePayload`.
341    ///
342    /// `compressor` is the compression function we have negotiated.
343    /// `original` is the uncompressed certificate message.
344    pub(crate) fn compression_for(
345        &self,
346        compressor: &dyn CertCompressor,
347        original: &CertificatePayloadTls13<'_>,
348    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
349        match self {
350            Self::Disabled => Self::uncached_compression(compressor, original),
351
352            #[cfg(feature = "std")]
353            Self::Enabled(_) => self.compression_for_impl(compressor, original),
354        }
355    }
356
357    #[cfg(feature = "std")]
358    fn compression_for_impl(
359        &self,
360        compressor: &dyn CertCompressor,
361        original: &CertificatePayloadTls13<'_>,
362    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
363        let (max_size, entries) = match self {
364            Self::Enabled(CompressionCacheInner { size, entries }) => (*size, entries),
365            _ => unreachable!(),
366        };
367
368        // context is a per-connection quantity, and included in the compressed data.
369        // it is not suitable for inclusion in the cache.
370        if !original.context.0.is_empty() {
371            return Self::uncached_compression(compressor, original);
372        }
373
374        // cache probe:
375        let encoding = original.get_encoding();
376        let algorithm = compressor.algorithm();
377
378        let mut cache = entries
379            .lock()
380            .map_err(|_| CompressionFailed)?;
381        for (i, item) in cache.iter().enumerate() {
382            if item.algorithm == algorithm && item.original == encoding {
383                // this item is now MRU
384                let item = cache.remove(i).unwrap();
385                cache.push_back(item.clone());
386                return Ok(item);
387            }
388        }
389        drop(cache);
390
391        // do compression:
392        let uncompressed_len = encoding.len() as u32;
393        let compressed = compressor.compress(encoding.clone(), CompressionLevel::Amortized)?;
394        let new_entry = Arc::new(CompressionCacheEntry {
395            algorithm,
396            original: encoding,
397            compressed: CompressedCertificatePayload {
398                alg: algorithm,
399                uncompressed_len,
400                compressed: PayloadU24::from(Payload::new(compressed)),
401            },
402        });
403
404        // insert into cache
405        let mut cache = entries
406            .lock()
407            .map_err(|_| CompressionFailed)?;
408        if cache.len() == max_size {
409            cache.pop_front();
410        }
411        cache.push_back(new_entry.clone());
412        Ok(new_entry)
413    }
414
415    /// Compress `original` using `compressor` at `Interactive` level.
416    fn uncached_compression(
417        compressor: &dyn CertCompressor,
418        original: &CertificatePayloadTls13<'_>,
419    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
420        let algorithm = compressor.algorithm();
421        let encoding = original.get_encoding();
422        let uncompressed_len = encoding.len() as u32;
423        let compressed = compressor.compress(encoding, CompressionLevel::Interactive)?;
424
425        // this `CompressionCacheEntry` in fact never makes it into the cache, so
426        // `original` is left empty
427        Ok(Arc::new(CompressionCacheEntry {
428            algorithm,
429            original: Vec::new(),
430            compressed: CompressedCertificatePayload {
431                alg: algorithm,
432                uncompressed_len,
433                compressed: PayloadU24::from(Payload::new(compressed)),
434            },
435        }))
436    }
437}
438
439impl Default for CompressionCache {
440    fn default() -> Self {
441        #[cfg(feature = "std")]
442        {
443            // 4 entries allows 2 certificate chains times 2 compression algorithms
444            Self::new(4)
445        }
446
447        #[cfg(not(feature = "std"))]
448        {
449            Self::Disabled
450        }
451    }
452}
453
454#[cfg_attr(not(feature = "std"), allow(dead_code))]
455#[derive(Debug)]
456pub(crate) struct CompressionCacheEntry {
457    // cache key is algorithm + original:
458    algorithm: CertificateCompressionAlgorithm,
459    original: Vec<u8>,
460
461    // cache value is compression result:
462    compressed: CompressedCertificatePayload<'static>,
463}
464
465impl CompressionCacheEntry {
466    pub(crate) fn compressed_cert_payload(&self) -> CompressedCertificatePayload<'_> {
467        self.compressed.as_borrowed()
468    }
469}
470
471#[cfg(all(test, any(feature = "brotli", feature = "zlib")))]
472mod tests {
473    use std::{println, vec};
474
475    use super::*;
476
477    #[test]
478    #[cfg(feature = "zlib")]
479    fn test_zlib() {
480        test_compressor(ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR);
481    }
482
483    #[test]
484    #[cfg(feature = "brotli")]
485    fn test_brotli() {
486        test_compressor(BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR);
487    }
488
489    #[test]
490    #[cfg(feature = "brotli")]
491    fn test_brotli_rejects_large_window() {
492        let large_window_empty = [0x11, 0xd0];
493        brotli::BrotliDecompress(&mut &large_window_empty[..], &mut Vec::new()).unwrap();
494        BROTLI_DECOMPRESSOR
495            .decompress(&large_window_empty, &mut [])
496            .unwrap_err();
497    }
498
499    fn test_compressor(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
500        assert_eq!(comp.algorithm(), decomp.algorithm());
501        for sz in [16, 64, 512, 2048, 8192, 16384] {
502            test_trivial_pairwise(comp, decomp, sz);
503        }
504        test_decompress_wrong_len(comp, decomp);
505        test_decompress_trailing_data(comp, decomp);
506        test_decompress_garbage(decomp);
507    }
508
509    fn test_trivial_pairwise(
510        comp: &dyn CertCompressor,
511        decomp: &dyn CertDecompressor,
512        plain_len: usize,
513    ) {
514        let original = vec![0u8; plain_len];
515
516        for level in [CompressionLevel::Interactive, CompressionLevel::Amortized] {
517            let compressed = comp
518                .compress(original.clone(), level)
519                .unwrap();
520            println!(
521                "{:?} compressed trivial {} -> {} using {:?} level",
522                comp.algorithm(),
523                original.len(),
524                compressed.len(),
525                level
526            );
527            let mut recovered = vec![0xffu8; plain_len];
528            decomp
529                .decompress(&compressed, &mut recovered)
530                .unwrap();
531            assert_eq!(original, recovered);
532        }
533    }
534
535    fn test_decompress_wrong_len(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
536        let original = vec![0u8; 2048];
537        let compressed = comp
538            .compress(original.clone(), CompressionLevel::Interactive)
539            .unwrap();
540        println!("{compressed:?}");
541
542        // too big
543        let mut recovered = vec![0xffu8; original.len() + 1];
544        decomp
545            .decompress(&compressed, &mut recovered)
546            .unwrap_err();
547
548        // too small
549        let mut recovered = vec![0xffu8; original.len() - 1];
550        decomp
551            .decompress(&compressed, &mut recovered)
552            .unwrap_err();
553    }
554
555    fn test_decompress_trailing_data(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
556        let original = vec![0u8; 2048];
557        let mut compressed = comp
558            .compress(original.clone(), CompressionLevel::Interactive)
559            .unwrap();
560        compressed.extend_from_slice(b"trailing data");
561
562        let mut recovered = vec![0xffu8; original.len()];
563        decomp
564            .decompress(&compressed, &mut recovered)
565            .unwrap_err();
566    }
567
568    fn test_decompress_garbage(decomp: &dyn CertDecompressor) {
569        let junk = [0u8; 1024];
570        let mut recovered = vec![0u8; 512];
571        decomp
572            .decompress(&junk, &mut recovered)
573            .unwrap_err();
574    }
575
576    #[test]
577    #[cfg(all(feature = "brotli", feature = "zlib"))]
578    fn test_cache_evicts_lru() {
579        use core::sync::atomic::{AtomicBool, Ordering};
580
581        use pki_types::CertificateDer;
582
583        let cache = CompressionCache::default();
584
585        let cert = CertificateDer::from(vec![1]);
586
587        let cert1 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"1"));
588        let cert2 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"2"));
589        let cert3 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"3"));
590        let cert4 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"4"));
591
592        // insert zlib (1), (2), (3), (4)
593
594        cache
595            .compression_for(
596                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
597                &cert1,
598            )
599            .unwrap();
600        cache
601            .compression_for(
602                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
603                &cert2,
604            )
605            .unwrap();
606        cache
607            .compression_for(
608                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
609                &cert3,
610            )
611            .unwrap();
612        cache
613            .compression_for(
614                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
615                &cert4,
616            )
617            .unwrap();
618
619        // -- now full
620
621        // insert brotli (1) evicts zlib (1)
622        cache
623            .compression_for(
624                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
625                &cert4,
626            )
627            .unwrap();
628
629        // now zlib (2), (3), (4) and brotli (4) exist
630        cache
631            .compression_for(
632                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
633                &cert2,
634            )
635            .unwrap();
636        cache
637            .compression_for(
638                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
639                &cert3,
640            )
641            .unwrap();
642        cache
643            .compression_for(
644                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
645                &cert4,
646            )
647            .unwrap();
648        cache
649            .compression_for(
650                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), false),
651                &cert4,
652            )
653            .unwrap();
654
655        // insert zlib (1) requires re-compression & evicts zlib (2)
656        cache
657            .compression_for(
658                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
659                &cert1,
660            )
661            .unwrap();
662
663        // now zlib (1), (3), (4) and brotli (4) exist
664        // query zlib (4), (3), (1) to demonstrate LRU tracks usage rather than insertion
665        cache
666            .compression_for(
667                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
668                &cert4,
669            )
670            .unwrap();
671        cache
672            .compression_for(
673                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
674                &cert3,
675            )
676            .unwrap();
677        cache
678            .compression_for(
679                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
680                &cert1,
681            )
682            .unwrap();
683
684        // now brotli (4), zlib (4), (3), (1)
685        // insert brotli (1) evicting brotli (4)
686        cache
687            .compression_for(
688                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
689                &cert1,
690            )
691            .unwrap();
692
693        // verify brotli (4) disappeared
694        cache
695            .compression_for(
696                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
697                &cert4,
698            )
699            .unwrap();
700
701        #[derive(Debug)]
702        struct RequireCompress(&'static dyn CertCompressor, AtomicBool, bool);
703
704        impl CertCompressor for RequireCompress {
705            fn compress(
706                &self,
707                input: Vec<u8>,
708                level: CompressionLevel,
709            ) -> Result<Vec<u8>, CompressionFailed> {
710                self.1.store(true, Ordering::SeqCst);
711                self.0.compress(input, level)
712            }
713
714            fn algorithm(&self) -> CertificateCompressionAlgorithm {
715                self.0.algorithm()
716            }
717        }
718
719        impl Drop for RequireCompress {
720            fn drop(&mut self) {
721                assert_eq!(self.1.load(Ordering::SeqCst), self.2);
722            }
723        }
724    }
725}