Skip to main content

sark_core/http/
compress.rs

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