1use crate::request::Request;
45use crate::server::negotiate_precompressed_variant;
46use serde::{Deserialize, Serialize};
47use std::path::{Path, PathBuf};
48use std::sync::atomic::{AtomicUsize, Ordering};
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(default)]
53pub struct CompressionPolicy {
54 pub enabled: bool,
56 pub min_size_bytes: u64,
59 pub dynamic_gzip: bool,
63 pub compressible_types: Vec<String>,
65}
66
67impl Default for CompressionPolicy {
68 fn default() -> Self {
69 Self {
70 enabled: true,
71 min_size_bytes: 1024,
72 dynamic_gzip: true,
73 compressible_types: [
74 "text/",
75 "application/javascript",
76 "application/json",
77 "application/manifest+json",
78 "application/typescript",
79 "application/wasm",
80 "application/x-yaml",
81 "application/toml",
82 "application/xml",
83 "image/svg+xml",
84 ]
85 .map(String::from)
86 .to_vec(),
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub(crate) enum Outcome {
94 Precompressed,
95 Dynamic,
96 Disabled,
97 ClientDeclined,
98 Type,
99 Size,
100 NoVariant,
101}
102
103const OUTCOMES: usize = 7;
104static COUNTERS: [AtomicUsize; OUTCOMES] =
105 [const { AtomicUsize::new(0) }; OUTCOMES];
106
107impl Outcome {
108 const ALL: [Outcome; OUTCOMES] = [
109 Outcome::Precompressed,
110 Outcome::Dynamic,
111 Outcome::Disabled,
112 Outcome::ClientDeclined,
113 Outcome::Type,
114 Outcome::Size,
115 Outcome::NoVariant,
116 ];
117
118 fn label(self) -> &'static str {
119 match self {
120 Outcome::Precompressed => "precompressed",
121 Outcome::Dynamic => "dynamic",
122 Outcome::Disabled => "disabled",
123 Outcome::ClientDeclined => "client",
124 Outcome::Type => "type",
125 Outcome::Size => "size",
126 Outcome::NoVariant => "no_variant",
127 }
128 }
129
130 pub(crate) fn record(self) {
131 let _ = COUNTERS[self as usize].fetch_add(1, Ordering::Relaxed);
132 }
133}
134
135#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
137pub struct CompressionStats {
138 pub precompressed: usize,
140 pub dynamic: usize,
142 pub skipped_disabled: usize,
144 pub skipped_client: usize,
146 pub skipped_type: usize,
148 pub skipped_size: usize,
150 pub skipped_no_variant: usize,
152}
153
154pub fn stats() -> CompressionStats {
156 let get = |o: Outcome| COUNTERS[o as usize].load(Ordering::Relaxed);
157 CompressionStats {
158 precompressed: get(Outcome::Precompressed),
159 dynamic: get(Outcome::Dynamic),
160 skipped_disabled: get(Outcome::Disabled),
161 skipped_client: get(Outcome::ClientDeclined),
162 skipped_type: get(Outcome::Type),
163 skipped_size: get(Outcome::Size),
164 skipped_no_variant: get(Outcome::NoVariant),
165 }
166}
167
168pub(crate) fn metrics_lines() -> String {
170 Outcome::ALL
171 .iter()
172 .map(|o| {
173 format!(
174 "http_handle_compression_total{{outcome=\"{}\"}} {}\n",
175 o.label(),
176 COUNTERS[*o as usize].load(Ordering::Relaxed)
177 )
178 })
179 .collect()
180}
181
182pub(crate) fn accepts(accept_encoding: &str, coding: &str) -> bool {
185 let mut wildcard = None;
186 for entry in accept_encoding.split(',') {
187 let mut fields = entry.split(';');
188 let name = fields.next().unwrap_or_default().trim();
189 let quality = fields
190 .filter_map(|p| p.trim().strip_prefix("q="))
191 .find_map(|q| q.trim().parse::<f32>().ok())
192 .unwrap_or(1.0);
193 if name.eq_ignore_ascii_case(coding) {
194 return quality > 0.0;
195 }
196 if name == "*" {
197 wildcard = Some(quality > 0.0);
198 }
199 }
200 wildcard.unwrap_or(false)
201}
202
203pub(crate) fn precheck(
206 policy: &CompressionPolicy,
207 accept_encoding: Option<&str>,
208 content_type: &str,
209 len: u64,
210) -> Option<Outcome> {
211 let accepted = accept_encoding.is_some_and(|accept| {
212 ["br", "zstd", "zst", "gzip"]
213 .iter()
214 .any(|coding| accepts(accept, coding))
215 });
216 if !policy.enabled {
217 Some(Outcome::Disabled)
218 } else if !accepted {
219 Some(Outcome::ClientDeclined)
220 } else if !policy
221 .compressible_types
222 .iter()
223 .any(|prefix| content_type.starts_with(prefix.as_str()))
224 {
225 Some(Outcome::Type)
226 } else if len < policy.min_size_bytes {
227 Some(Outcome::Size)
228 } else {
229 None
230 }
231}
232
233pub(crate) fn wants_dynamic_gzip(
235 policy: &CompressionPolicy,
236 accept_encoding: Option<&str>,
237 has_range: bool,
238) -> bool {
239 cfg!(feature = "compression")
240 && policy.dynamic_gzip
241 && !has_range
242 && accept_encoding.is_some_and(|accept| accepts(accept, "gzip"))
243}
244
245#[cfg(feature = "compression")]
247pub(crate) fn gzip(body: &[u8]) -> std::io::Result<Vec<u8>> {
248 use flate2::{Compression, write::GzEncoder};
249 use std::io::Write;
250 let mut encoder =
251 GzEncoder::new(Vec::new(), Compression::default());
252 encoder.write_all(body)?;
253 encoder.finish()
254}
255
256#[cfg(not(feature = "compression"))]
258pub(crate) fn gzip(_body: &[u8]) -> std::io::Result<Vec<u8>> {
259 Err(std::io::Error::other(
260 "built without the compression feature",
261 ))
262}
263
264#[derive(Clone, Copy, Debug)]
266pub(crate) enum Mode<'a> {
267 Legacy,
269 Off,
271 Policy(&'a CompressionPolicy),
273}
274
275impl<'a> Mode<'a> {
276 pub(crate) fn new(
278 policy: Option<&'a CompressionPolicy>,
279 route_override: Option<bool>,
280 ) -> Self {
281 match (route_override, policy) {
282 (Some(false), _) => Mode::Off,
283 (_, Some(policy)) => Mode::Policy(policy),
284 (_, None) => Mode::Legacy,
285 }
286 }
287}
288
289#[derive(Debug)]
291pub(crate) enum Encoding {
292 Identity,
294 Precompressed(PathBuf, &'static str),
296 Gzip,
298}
299
300impl Encoding {
301 pub(crate) fn header(&self) -> Option<&'static str> {
303 match self {
304 Encoding::Identity => None,
305 Encoding::Precompressed(_, coding) => Some(coding),
306 Encoding::Gzip => Some("gzip"),
307 }
308 }
309}
310
311pub(crate) fn select(
314 mode: Mode<'_>,
315 request: &Request,
316 path: &Path,
317 canonical_root: &Path,
318 content_type: &str,
319 len: u64,
320) -> Encoding {
321 let accept = request.header("accept-encoding");
322 let policy = match mode {
323 Mode::Off => {
324 Outcome::Disabled.record();
325 return Encoding::Identity;
326 }
327 Mode::Legacy => None,
328 Mode::Policy(policy) => Some(policy),
329 };
330 if let Some(skip) =
331 policy.and_then(|p| precheck(p, accept, content_type, len))
332 {
333 skip.record();
334 return Encoding::Identity;
335 }
336 if let (sibling, Some(coding)) =
337 negotiate_precompressed_variant(path, accept, canonical_root)
338 {
339 if policy.is_some() {
340 Outcome::Precompressed.record();
341 }
342 return Encoding::Precompressed(sibling, coding);
343 }
344 let Some(policy) = policy else {
345 return Encoding::Identity;
346 };
347 if wants_dynamic_gzip(
348 policy,
349 accept,
350 request.header("range").is_some(),
351 ) {
352 Outcome::Dynamic.record();
353 return Encoding::Gzip;
354 }
355 Outcome::NoVariant.record();
356 Encoding::Identity
357}