Skip to main content

sark_core/http/
compress.rs

1//! Built-in gzip/brotli response compression. `with_thread_local` borrows a
2//! per-thread encoder so a handler can compress a body without per-request
3//! allocation; `encode` returns the compressed bytes. This is sark's standard
4//! compression path — response bodies are never hand-rolled.
5//!
6//! ```no_run
7//! use sark_core::http::compress::Gzip;
8//! let body: &[u8] = b"hello";
9//! let zipped = Gzip::with_thread_local(|gz| gz.encode(body).to_vec());
10//! ```
11
12use std::cell::RefCell;
13use std::thread::LocalKey;
14
15use libdeflater::{CompressionLvl, Compressor};
16
17const BUF_CAP: usize = 64 * 1024;
18
19fn with_slot<E, R>(slot: &'static LocalKey<RefCell<E>>, f: impl FnOnce(&mut E) -> R) -> R {
20    slot.with(|cell| f(&mut cell.borrow_mut()))
21}
22
23pub struct Gzip {
24    encoder: Compressor,
25    buf: Vec<u8>,
26}
27
28impl Gzip {
29    const LEVEL: i32 = 3;
30
31    fn new(level: i32) -> Self {
32        let lvl = CompressionLvl::new(level).unwrap_or(CompressionLvl::fastest());
33        Self {
34            encoder: Compressor::new(lvl),
35            buf: Vec::with_capacity(BUF_CAP),
36        }
37    }
38
39    pub fn encode(&mut self, src: &[u8]) -> &[u8] {
40        let cap = self.encoder.gzip_compress_bound(src.len());
41        self.buf.resize(cap, 0);
42        let n = self
43            .encoder
44            .gzip_compress(src, &mut self.buf)
45            .expect("gzip_compress_bound undersized");
46        self.buf.truncate(n);
47        &self.buf
48    }
49
50    pub fn with_thread_local<R>(f: impl FnOnce(&mut Gzip) -> R) -> R {
51        thread_local! {
52            static SLOT: RefCell<Gzip> = RefCell::new(Gzip::new(Gzip::LEVEL));
53        }
54        with_slot(&SLOT, f)
55    }
56}
57
58/// Brotli response compression. `br` typically yields a smaller body than gzip
59/// for text/JSON.
60///
61/// ```no_run
62/// use sark_core::http::compress::Brotli;
63/// let body: &[u8] = b"hello";
64/// let zipped = Brotli::with_thread_local(|br| br.encode(body).to_vec());
65/// ```
66pub struct Brotli {
67    params: brotli::enc::BrotliEncoderParams,
68    buf: Vec<u8>,
69}
70
71impl Brotli {
72    /// `5`/`22`: compression-ratio vs throughput balance for runtime JSON.
73    const QUALITY: i32 = 5;
74    const LGWIN: i32 = 22;
75
76    fn new(quality: i32, lgwin: i32) -> Self {
77        let params = brotli::enc::BrotliEncoderParams {
78            quality,
79            lgwin,
80            ..Default::default()
81        };
82        Self {
83            params,
84            buf: Vec::with_capacity(BUF_CAP),
85        }
86    }
87
88    pub fn encode(&mut self, src: &[u8]) -> &[u8] {
89        self.buf.clear();
90        self.buf
91            .reserve(brotli::enc::BrotliEncoderMaxCompressedSize(src.len()));
92        let mut reader = src;
93        brotli::BrotliCompress(&mut reader, &mut self.buf, &self.params)
94            .expect("brotli compress into Vec is infallible");
95        &self.buf
96    }
97
98    pub fn with_thread_local<R>(f: impl FnOnce(&mut Brotli) -> R) -> R {
99        thread_local! {
100            static SLOT: RefCell<Brotli> = RefCell::new(Brotli::new(Brotli::QUALITY, Brotli::LGWIN));
101        }
102        with_slot(&SLOT, f)
103    }
104}