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" )] |
36 | use alloc::collections::VecDeque; |
37 | use alloc::vec::Vec; |
38 | use core::fmt::Debug; |
39 | #[cfg (feature = "std" )] |
40 | use std::sync::Mutex; |
41 | |
42 | use crate::enums::CertificateCompressionAlgorithm; |
43 | use crate::msgs::base::{Payload, PayloadU24}; |
44 | use crate::msgs::codec::Codec; |
45 | use crate::msgs::handshake::{CertificatePayloadTls13, CompressedCertificatePayload}; |
46 | use crate::sync::Arc; |
47 | |
48 | /// Returns the supported `CertDecompressor` implementations enabled |
49 | /// by crate features. |
50 | pub 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. |
60 | pub 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. |
75 | pub 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. |
85 | pub 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)] |
106 | pub 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)] |
120 | pub struct DecompressionFailed; |
121 | |
122 | /// A content-less error for when `CertCompressor::compress` fails. |
123 | #[derive (Debug)] |
124 | pub struct CompressionFailed; |
125 | |
126 | #[cfg (feature = "zlib" )] |
127 | mod feat_zlib_rs { |
128 | use zlib_rs::c_api::Z_BEST_COMPRESSION; |
129 | use zlib_rs::{ReturnCode, deflate, inflate}; |
130 | |
131 | use super::*; |
132 | |
133 | /// A certificate decompressor for the Zlib algorithm using the `zlib-rs` crate. |
134 | pub const ZLIB_DECOMPRESSOR: &dyn CertDecompressor = &ZlibRsDecompressor; |
135 | |
136 | #[derive (Debug)] |
137 | struct ZlibRsDecompressor; |
138 | |
139 | impl CertDecompressor for ZlibRsDecompressor { |
140 | fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed> { |
141 | let output_len = output.len(); |
142 | match inflate::uncompress_slice(output, input, inflate::InflateConfig::default()) { |
143 | (output_filled, ReturnCode::Ok) if output_filled.len() == output_len => Ok(()), |
144 | (_, _) => Err(DecompressionFailed), |
145 | } |
146 | } |
147 | |
148 | fn algorithm(&self) -> CertificateCompressionAlgorithm { |
149 | CertificateCompressionAlgorithm::Zlib |
150 | } |
151 | } |
152 | |
153 | /// A certificate compressor for the Zlib algorithm using the `zlib-rs` crate. |
154 | pub const ZLIB_COMPRESSOR: &dyn CertCompressor = &ZlibRsCompressor; |
155 | |
156 | #[derive (Debug)] |
157 | struct ZlibRsCompressor; |
158 | |
159 | impl CertCompressor for ZlibRsCompressor { |
160 | fn compress( |
161 | &self, |
162 | input: Vec<u8>, |
163 | level: CompressionLevel, |
164 | ) -> Result<Vec<u8>, CompressionFailed> { |
165 | let mut output = alloc::vec![0u8; deflate::compress_bound(input.len())]; |
166 | let config = match level { |
167 | CompressionLevel::Interactive => deflate::DeflateConfig::default(), |
168 | CompressionLevel::Amortized => deflate::DeflateConfig::new(Z_BEST_COMPRESSION), |
169 | }; |
170 | let (output_filled, rc) = deflate::compress_slice(&mut output, &input, config); |
171 | if rc != ReturnCode::Ok { |
172 | return Err(CompressionFailed); |
173 | } |
174 | |
175 | let used = output_filled.len(); |
176 | output.truncate(used); |
177 | Ok(output) |
178 | } |
179 | |
180 | fn algorithm(&self) -> CertificateCompressionAlgorithm { |
181 | CertificateCompressionAlgorithm::Zlib |
182 | } |
183 | } |
184 | } |
185 | |
186 | #[cfg (feature = "zlib" )] |
187 | pub use feat_zlib_rs::{ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR}; |
188 | |
189 | #[cfg (feature = "brotli" )] |
190 | mod feat_brotli { |
191 | use std::io::{Cursor, Write}; |
192 | |
193 | use super::*; |
194 | |
195 | /// A certificate decompressor for the brotli algorithm using the `brotli` crate. |
196 | pub const BROTLI_DECOMPRESSOR: &dyn CertDecompressor = &BrotliDecompressor; |
197 | |
198 | #[derive (Debug)] |
199 | struct BrotliDecompressor; |
200 | |
201 | impl CertDecompressor for BrotliDecompressor { |
202 | fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed> { |
203 | let mut in_cursor = Cursor::new(input); |
204 | let mut out_cursor = Cursor::new(output); |
205 | |
206 | brotli::BrotliDecompress(&mut in_cursor, &mut out_cursor) |
207 | .map_err(|_| DecompressionFailed)?; |
208 | |
209 | if out_cursor.position() as usize != out_cursor.into_inner().len() { |
210 | return Err(DecompressionFailed); |
211 | } |
212 | |
213 | Ok(()) |
214 | } |
215 | |
216 | fn algorithm(&self) -> CertificateCompressionAlgorithm { |
217 | CertificateCompressionAlgorithm::Brotli |
218 | } |
219 | } |
220 | |
221 | /// A certificate compressor for the brotli algorithm using the `brotli` crate. |
222 | pub const BROTLI_COMPRESSOR: &dyn CertCompressor = &BrotliCompressor; |
223 | |
224 | #[derive (Debug)] |
225 | struct BrotliCompressor; |
226 | |
227 | impl CertCompressor for BrotliCompressor { |
228 | fn compress( |
229 | &self, |
230 | input: Vec<u8>, |
231 | level: CompressionLevel, |
232 | ) -> Result<Vec<u8>, CompressionFailed> { |
233 | let quality = match level { |
234 | CompressionLevel::Interactive => QUALITY_FAST, |
235 | CompressionLevel::Amortized => QUALITY_SLOW, |
236 | }; |
237 | let output = Cursor::new(Vec::with_capacity(input.len() / 2)); |
238 | let mut compressor = brotli::CompressorWriter::new(output, BUFFER_SIZE, quality, LGWIN); |
239 | compressor |
240 | .write_all(&input) |
241 | .map_err(|_| CompressionFailed)?; |
242 | Ok(compressor.into_inner().into_inner()) |
243 | } |
244 | |
245 | fn algorithm(&self) -> CertificateCompressionAlgorithm { |
246 | CertificateCompressionAlgorithm::Brotli |
247 | } |
248 | } |
249 | |
250 | /// Brotli buffer size. |
251 | /// |
252 | /// Chosen based on brotli `examples/compress.rs`. |
253 | const BUFFER_SIZE: usize = 4096; |
254 | |
255 | /// This is the default lgwin parameter, see `BrotliEncoderInitParams()` |
256 | const LGWIN: u32 = 22; |
257 | |
258 | /// Compression quality we use for interactive compressions. |
259 | /// See <https://blog.cloudflare.com/results-experimenting-brotli> for data. |
260 | const QUALITY_FAST: u32 = 4; |
261 | |
262 | /// Compression quality we use for offline compressions (the maximum). |
263 | const QUALITY_SLOW: u32 = 11; |
264 | } |
265 | |
266 | #[cfg (feature = "brotli" )] |
267 | pub use feat_brotli::{BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR}; |
268 | |
269 | /// An LRU cache for compressions. |
270 | /// |
271 | /// The prospect of being able to reuse a given compression for many connections |
272 | /// means we can afford to spend more time on that compression (by passing |
273 | /// `CompressionLevel::Amortized` to the compressor). |
274 | #[derive (Debug)] |
275 | pub enum CompressionCache { |
276 | /// No caching happens, and compression happens each time using |
277 | /// `CompressionLevel::Interactive`. |
278 | Disabled, |
279 | |
280 | /// Compressions are stored in an LRU cache. |
281 | #[cfg (feature = "std" )] |
282 | Enabled(CompressionCacheInner), |
283 | } |
284 | |
285 | /// Innards of an enabled CompressionCache. |
286 | /// |
287 | /// You cannot make one of these directly. Use [`CompressionCache::new`]. |
288 | #[cfg (feature = "std" )] |
289 | #[derive (Debug)] |
290 | pub struct CompressionCacheInner { |
291 | /// Maximum size of underlying storage. |
292 | size: usize, |
293 | |
294 | /// LRU-order entries. |
295 | /// |
296 | /// First is least-used, last is most-used. |
297 | entries: Mutex<VecDeque<Arc<CompressionCacheEntry>>>, |
298 | } |
299 | |
300 | impl CompressionCache { |
301 | /// Make a `CompressionCache` that stores up to `size` compressed |
302 | /// certificate messages. |
303 | #[cfg (feature = "std" )] |
304 | pub fn new(size: usize) -> Self { |
305 | if size == 0 { |
306 | return Self::Disabled; |
307 | } |
308 | |
309 | Self::Enabled(CompressionCacheInner { |
310 | size, |
311 | entries: Mutex::new(VecDeque::with_capacity(size)), |
312 | }) |
313 | } |
314 | |
315 | /// Return a `CompressionCacheEntry`, which is an owning |
316 | /// wrapper for a `CompressedCertificatePayload`. |
317 | /// |
318 | /// `compressor` is the compression function we have negotiated. |
319 | /// `original` is the uncompressed certificate message. |
320 | pub(crate) fn compression_for( |
321 | &self, |
322 | compressor: &dyn CertCompressor, |
323 | original: &CertificatePayloadTls13<'_>, |
324 | ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> { |
325 | match self { |
326 | Self::Disabled => Self::uncached_compression(compressor, original), |
327 | |
328 | #[cfg (feature = "std" )] |
329 | Self::Enabled(_) => self.compression_for_impl(compressor, original), |
330 | } |
331 | } |
332 | |
333 | #[cfg (feature = "std" )] |
334 | fn compression_for_impl( |
335 | &self, |
336 | compressor: &dyn CertCompressor, |
337 | original: &CertificatePayloadTls13<'_>, |
338 | ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> { |
339 | let (max_size, entries) = match self { |
340 | Self::Enabled(CompressionCacheInner { size, entries }) => (*size, entries), |
341 | _ => unreachable!(), |
342 | }; |
343 | |
344 | // context is a per-connection quantity, and included in the compressed data. |
345 | // it is not suitable for inclusion in the cache. |
346 | if !original.context.0.is_empty() { |
347 | return Self::uncached_compression(compressor, original); |
348 | } |
349 | |
350 | // cache probe: |
351 | let encoding = original.get_encoding(); |
352 | let algorithm = compressor.algorithm(); |
353 | |
354 | let mut cache = entries |
355 | .lock() |
356 | .map_err(|_| CompressionFailed)?; |
357 | for (i, item) in cache.iter().enumerate() { |
358 | if item.algorithm == algorithm && item.original == encoding { |
359 | // this item is now MRU |
360 | let item = cache.remove(i).unwrap(); |
361 | cache.push_back(Arc::clone(&item)); |
362 | return Ok(item); |
363 | } |
364 | } |
365 | drop(cache); |
366 | |
367 | // do compression: |
368 | let uncompressed_len = encoding.len() as u32; |
369 | let compressed = compressor.compress(encoding.clone(), CompressionLevel::Amortized)?; |
370 | let new_entry = Arc::new(CompressionCacheEntry { |
371 | algorithm, |
372 | original: encoding, |
373 | compressed: CompressedCertificatePayload { |
374 | alg: algorithm, |
375 | uncompressed_len, |
376 | compressed: PayloadU24(Payload::new(compressed)), |
377 | }, |
378 | }); |
379 | |
380 | // insert into cache |
381 | let mut cache = entries |
382 | .lock() |
383 | .map_err(|_| CompressionFailed)?; |
384 | if cache.len() == max_size { |
385 | cache.pop_front(); |
386 | } |
387 | cache.push_back(Arc::clone(&new_entry)); |
388 | Ok(new_entry) |
389 | } |
390 | |
391 | /// Compress `original` using `compressor` at `Interactive` level. |
392 | fn uncached_compression( |
393 | compressor: &dyn CertCompressor, |
394 | original: &CertificatePayloadTls13<'_>, |
395 | ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> { |
396 | let algorithm = compressor.algorithm(); |
397 | let encoding = original.get_encoding(); |
398 | let uncompressed_len = encoding.len() as u32; |
399 | let compressed = compressor.compress(encoding, CompressionLevel::Interactive)?; |
400 | |
401 | // this `CompressionCacheEntry` in fact never makes it into the cache, so |
402 | // `original` is left empty |
403 | Ok(Arc::new(CompressionCacheEntry { |
404 | algorithm, |
405 | original: Vec::new(), |
406 | compressed: CompressedCertificatePayload { |
407 | alg: algorithm, |
408 | uncompressed_len, |
409 | compressed: PayloadU24(Payload::new(compressed)), |
410 | }, |
411 | })) |
412 | } |
413 | } |
414 | |
415 | impl Default for CompressionCache { |
416 | fn default() -> Self { |
417 | #[cfg (feature = "std" )] |
418 | { |
419 | // 4 entries allows 2 certificate chains times 2 compression algorithms |
420 | Self::new(size:4) |
421 | } |
422 | |
423 | #[cfg (not(feature = "std" ))] |
424 | { |
425 | Self::Disabled |
426 | } |
427 | } |
428 | } |
429 | |
430 | #[cfg_attr (not(feature = "std" ), allow(dead_code))] |
431 | #[derive (Debug)] |
432 | pub(crate) struct CompressionCacheEntry { |
433 | // cache key is algorithm + original: |
434 | algorithm: CertificateCompressionAlgorithm, |
435 | original: Vec<u8>, |
436 | |
437 | // cache value is compression result: |
438 | compressed: CompressedCertificatePayload<'static>, |
439 | } |
440 | |
441 | impl CompressionCacheEntry { |
442 | pub(crate) fn compressed_cert_payload(&self) -> CompressedCertificatePayload<'_> { |
443 | self.compressed.as_borrowed() |
444 | } |
445 | } |
446 | |
447 | #[cfg (all(test, any(feature = "brotli" , feature = "zlib" )))] |
448 | mod tests { |
449 | use std::{println, vec}; |
450 | |
451 | use super::*; |
452 | |
453 | #[test ] |
454 | #[cfg (feature = "zlib" )] |
455 | fn test_zlib() { |
456 | test_compressor(ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR); |
457 | } |
458 | |
459 | #[test ] |
460 | #[cfg (feature = "brotli" )] |
461 | fn test_brotli() { |
462 | test_compressor(BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR); |
463 | } |
464 | |
465 | fn test_compressor(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) { |
466 | assert_eq!(comp.algorithm(), decomp.algorithm()); |
467 | for sz in [16, 64, 512, 2048, 8192, 16384] { |
468 | test_trivial_pairwise(comp, decomp, sz); |
469 | } |
470 | test_decompress_wrong_len(comp, decomp); |
471 | test_decompress_garbage(decomp); |
472 | } |
473 | |
474 | fn test_trivial_pairwise( |
475 | comp: &dyn CertCompressor, |
476 | decomp: &dyn CertDecompressor, |
477 | plain_len: usize, |
478 | ) { |
479 | let original = vec![0u8; plain_len]; |
480 | |
481 | for level in [CompressionLevel::Interactive, CompressionLevel::Amortized] { |
482 | let compressed = comp |
483 | .compress(original.clone(), level) |
484 | .unwrap(); |
485 | println!( |
486 | "{:?} compressed trivial {} -> {} using {:?} level" , |
487 | comp.algorithm(), |
488 | original.len(), |
489 | compressed.len(), |
490 | level |
491 | ); |
492 | let mut recovered = vec![0xffu8; plain_len]; |
493 | decomp |
494 | .decompress(&compressed, &mut recovered) |
495 | .unwrap(); |
496 | assert_eq!(original, recovered); |
497 | } |
498 | } |
499 | |
500 | fn test_decompress_wrong_len(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) { |
501 | let original = vec![0u8; 2048]; |
502 | let compressed = comp |
503 | .compress(original.clone(), CompressionLevel::Interactive) |
504 | .unwrap(); |
505 | println!("{compressed:?}" ); |
506 | |
507 | // too big |
508 | let mut recovered = vec![0xffu8; original.len() + 1]; |
509 | decomp |
510 | .decompress(&compressed, &mut recovered) |
511 | .unwrap_err(); |
512 | |
513 | // too small |
514 | let mut recovered = vec![0xffu8; original.len() - 1]; |
515 | decomp |
516 | .decompress(&compressed, &mut recovered) |
517 | .unwrap_err(); |
518 | } |
519 | |
520 | fn test_decompress_garbage(decomp: &dyn CertDecompressor) { |
521 | let junk = [0u8; 1024]; |
522 | let mut recovered = vec![0u8; 512]; |
523 | decomp |
524 | .decompress(&junk, &mut recovered) |
525 | .unwrap_err(); |
526 | } |
527 | |
528 | #[test ] |
529 | #[cfg (all(feature = "brotli" , feature = "zlib" ))] |
530 | fn test_cache_evicts_lru() { |
531 | use core::sync::atomic::{AtomicBool, Ordering}; |
532 | |
533 | use pki_types::CertificateDer; |
534 | |
535 | let cache = CompressionCache::default(); |
536 | |
537 | let cert = CertificateDer::from(vec![1]); |
538 | |
539 | let cert1 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"1" )); |
540 | let cert2 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"2" )); |
541 | let cert3 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"3" )); |
542 | let cert4 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"4" )); |
543 | |
544 | // insert zlib (1), (2), (3), (4) |
545 | |
546 | cache |
547 | .compression_for( |
548 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true), |
549 | &cert1, |
550 | ) |
551 | .unwrap(); |
552 | cache |
553 | .compression_for( |
554 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true), |
555 | &cert2, |
556 | ) |
557 | .unwrap(); |
558 | cache |
559 | .compression_for( |
560 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true), |
561 | &cert3, |
562 | ) |
563 | .unwrap(); |
564 | cache |
565 | .compression_for( |
566 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true), |
567 | &cert4, |
568 | ) |
569 | .unwrap(); |
570 | |
571 | // -- now full |
572 | |
573 | // insert brotli (1) evicts zlib (1) |
574 | cache |
575 | .compression_for( |
576 | &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true), |
577 | &cert4, |
578 | ) |
579 | .unwrap(); |
580 | |
581 | // now zlib (2), (3), (4) and brotli (4) exist |
582 | cache |
583 | .compression_for( |
584 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false), |
585 | &cert2, |
586 | ) |
587 | .unwrap(); |
588 | cache |
589 | .compression_for( |
590 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false), |
591 | &cert3, |
592 | ) |
593 | .unwrap(); |
594 | cache |
595 | .compression_for( |
596 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false), |
597 | &cert4, |
598 | ) |
599 | .unwrap(); |
600 | cache |
601 | .compression_for( |
602 | &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), false), |
603 | &cert4, |
604 | ) |
605 | .unwrap(); |
606 | |
607 | // insert zlib (1) requires re-compression & evicts zlib (2) |
608 | cache |
609 | .compression_for( |
610 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true), |
611 | &cert1, |
612 | ) |
613 | .unwrap(); |
614 | |
615 | // now zlib (1), (3), (4) and brotli (4) exist |
616 | // query zlib (4), (3), (1) to demonstrate LRU tracks usage rather than insertion |
617 | cache |
618 | .compression_for( |
619 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false), |
620 | &cert4, |
621 | ) |
622 | .unwrap(); |
623 | cache |
624 | .compression_for( |
625 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false), |
626 | &cert3, |
627 | ) |
628 | .unwrap(); |
629 | cache |
630 | .compression_for( |
631 | &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false), |
632 | &cert1, |
633 | ) |
634 | .unwrap(); |
635 | |
636 | // now brotli (4), zlib (4), (3), (1) |
637 | // insert brotli (1) evicting brotli (4) |
638 | cache |
639 | .compression_for( |
640 | &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true), |
641 | &cert1, |
642 | ) |
643 | .unwrap(); |
644 | |
645 | // verify brotli (4) disappeared |
646 | cache |
647 | .compression_for( |
648 | &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true), |
649 | &cert4, |
650 | ) |
651 | .unwrap(); |
652 | |
653 | #[derive (Debug)] |
654 | struct RequireCompress(&'static dyn CertCompressor, AtomicBool, bool); |
655 | |
656 | impl CertCompressor for RequireCompress { |
657 | fn compress( |
658 | &self, |
659 | input: Vec<u8>, |
660 | level: CompressionLevel, |
661 | ) -> Result<Vec<u8>, CompressionFailed> { |
662 | self.1.store(true, Ordering::SeqCst); |
663 | self.0.compress(input, level) |
664 | } |
665 | |
666 | fn algorithm(&self) -> CertificateCompressionAlgorithm { |
667 | self.0.algorithm() |
668 | } |
669 | } |
670 | |
671 | impl Drop for RequireCompress { |
672 | fn drop(&mut self) { |
673 | assert_eq!(self.1.load(Ordering::SeqCst), self.2); |
674 | } |
675 | } |
676 | } |
677 | } |
678 | |