use compcol::deflate::Deflate;
use compcol::gzip::Gzip;
use compcol::zlib::Zlib;
use crate::proto::{Request, Response};
#[derive(Debug, Clone, Copy)]
pub struct Options {
pub enabled: bool,
pub min_size: usize,
}
impl Default for Options {
fn default() -> Options {
Options {
enabled: true,
min_size: 256,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Coding {
Gzip,
Deflate,
#[allow(dead_code)]
Raw,
}
impl Coding {
fn token(self) -> &'static str {
match self {
Coding::Gzip => "gzip",
Coding::Deflate => "deflate",
Coding::Raw => "deflate",
}
}
}
pub fn compress_response(req: &Request, resp: Response, opts: &Options) -> Response {
if !opts.enabled {
return resp;
}
if resp.headers().contains("content-encoding") {
return resp;
}
if resp.status_code().code() == 206 || resp.headers().contains("content-range") {
return resp;
}
if resp.body_ref().is_file() {
return resp;
}
if resp.body_ref().len() < opts.min_size {
return resp;
}
if let Some(ct) = resp.headers().get("content-type")
&& crate::mime::is_precompressed(ct)
{
return resp;
}
let accept = req.headers().get("accept-encoding").unwrap_or("");
let Some(coding) = choose_coding(accept) else {
return resp;
};
let (status, mut headers, body) = resp.into_parts();
let body = body.into_bytes();
let compressed = match coding {
Coding::Gzip => compcol::vec::compress_to_vec::<Gzip>(&body),
Coding::Deflate => compcol::vec::compress_to_vec::<Zlib>(&body),
Coding::Raw => compcol::vec::compress_to_vec::<Deflate>(&body),
};
match compressed {
Ok(out) if out.len() < body.len() => {
headers.set("Content-Encoding", coding.token());
append_vary(&mut headers, "Accept-Encoding");
Response::from_parts(status, headers, out.into())
}
_ => {
append_vary(&mut headers, "Accept-Encoding");
Response::from_parts(status, headers, body.into())
}
}
}
fn choose_coding(accept_encoding: &str) -> Option<Coding> {
let acceptable = |name: &str| accepts(accept_encoding, name);
if acceptable("gzip") {
Some(Coding::Gzip)
} else if acceptable("deflate") {
Some(Coding::Deflate)
} else {
None
}
}
fn accepts(header: &str, name: &str) -> bool {
let mut star: Option<bool> = None;
for part in header.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let (token, q) = match part.split_once(';') {
Some((t, params)) => (t.trim(), parse_q(params)),
None => (part, 1.0),
};
let usable = q > 0.0;
if token.eq_ignore_ascii_case(name) {
return usable;
}
if token == "*" {
star = Some(usable);
}
}
star.unwrap_or(false)
}
fn parse_q(params: &str) -> f32 {
for p in params.split(';') {
let p = p.trim();
if let Some(v) = p.strip_prefix("q=").or_else(|| p.strip_prefix("Q=")) {
return v.trim().parse().unwrap_or(1.0);
}
}
1.0
}
fn append_vary(headers: &mut crate::proto::Headers, value: &str) {
if headers.contains_token("vary", value) || headers.contains_token("vary", "*") {
return;
}
match headers.get("vary").map(|s| s.to_owned()) {
Some(existing) => headers.set("Vary", format!("{existing}, {value}")),
None => headers.set("Vary", value),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accept_encoding_parsing() {
assert!(accepts("gzip, deflate", "gzip"));
assert!(accepts("gzip;q=0.8, deflate", "deflate"));
assert!(!accepts("gzip;q=0", "gzip"));
assert!(accepts("*", "gzip"));
assert!(!accepts("*;q=0", "gzip"));
assert!(!accepts("identity", "gzip"));
}
#[test]
fn chooses_gzip_first() {
assert_eq!(choose_coding("deflate, gzip"), Some(Coding::Gzip));
assert_eq!(choose_coding("deflate"), Some(Coding::Deflate));
assert_eq!(choose_coding("br"), None);
}
fn gzip_request() -> Request {
let mut headers = crate::proto::Headers::new();
headers.set("Accept-Encoding", "gzip");
Request::new(
crate::proto::Method::Get,
"/x".to_owned(),
crate::proto::Version::Http11,
headers,
Vec::new(),
)
}
#[test]
fn partial_content_is_not_compressed() {
let req = gzip_request();
let body = vec![b'a'; 4096];
let resp = Response::new(crate::proto::StatusCode::PARTIAL_CONTENT)
.header("Content-Type", "text/plain")
.header("Content-Range", "bytes 0-4095/8192")
.body(body.clone());
let out = compress_response(&req, resp, &Options::default());
assert!(!out.headers().contains("content-encoding"));
assert_eq!(out.body_ref().len(), body.len());
}
#[test]
fn content_range_is_not_compressed() {
let req = gzip_request();
let body = vec![b'a'; 4096];
let resp = Response::new(crate::proto::StatusCode::OK)
.header("Content-Type", "text/plain")
.header("Content-Range", "bytes 0-4095/8192")
.body(body.clone());
let out = compress_response(&req, resp, &Options::default());
assert!(!out.headers().contains("content-encoding"));
}
#[test]
fn ordinary_body_is_compressed() {
let req = gzip_request();
let body = vec![b'a'; 4096];
let resp = Response::new(crate::proto::StatusCode::OK)
.header("Content-Type", "text/plain")
.body(body);
let out = compress_response(&req, resp, &Options::default());
assert!(out.headers().contains("content-encoding"));
}
}