use http::header::{ACCEPT_ENCODING, CONTENT_ENCODING, VARY};
use crate::{Request, Response};
use super::StaticOptions;
pub(super) enum Compression {
Brotli,
Gzip,
}
pub(super) fn negotiate(
options: &StaticOptions,
req: &Request,
content_type: Option<&mime::Mime>,
) -> Option<Compression> {
if !options.enable_compression {
return None;
}
let content_type = content_type?;
if !is_mime_compressible(content_type) {
return None;
}
let header = req.headers().get(ACCEPT_ENCODING)?;
let value = header.to_str().ok()?;
parse_accept_encoding(value)
}
pub(super) fn apply_headers(res: &mut Response, compression: &Compression) {
let (encoding, vary) = match compression {
Compression::Brotli => ("br", "Accept-Encoding"),
Compression::Gzip => ("gzip", "Accept-Encoding"),
};
res.headers_mut()
.insert(CONTENT_ENCODING, encoding.parse().unwrap());
res.headers_mut().insert(VARY, vary.parse().unwrap());
}
fn parse_accept_encoding(header: &str) -> Option<Compression> {
let mut brotli = None;
let mut gzip = None;
for (index, item) in header.split(',').enumerate() {
let item = item.trim();
let mut parts = item.split(';');
let encoding = parts.next()?.trim();
let mut quality = 1.0_f32;
for param in parts {
let mut kv = param.splitn(2, '=');
if kv.next().map(|p| p.trim()) == Some("q")
&& let Some(v) = kv.next()
&& let Ok(parsed) = v.trim().parse::<f32>()
{
quality = parsed;
}
}
if quality == 0.0 {
continue;
}
match encoding {
"br" => {
brotli.get_or_insert(index);
}
"gzip" | "x-gzip" => {
gzip.get_or_insert(index);
}
"*" => {
gzip.get_or_insert(index + 1000);
}
_ => {}
}
}
if brotli.is_some() {
Some(Compression::Brotli)
} else if gzip.is_some() {
Some(Compression::Gzip)
} else {
None
}
}
fn is_mime_compressible(mime: &mime::Mime) -> bool {
matches!(
(mime.type_(), mime.subtype().as_str()),
(mime::TEXT, _)
| (mime::APPLICATION, "json")
| (mime::APPLICATION, "xml")
| (mime::APPLICATION, "javascript")
| (mime::APPLICATION, "ecmascript")
| (mime::APPLICATION, "x-javascript")
| (mime::APPLICATION, "xhtml+xml")
| (mime::APPLICATION, "rss+xml")
| (mime::APPLICATION, "svg+xml")
| (mime::IMAGE, "svg+xml")
)
}
#[cfg(test)]
mod tests {
use super::*;
use http::header::{ACCEPT_ENCODING, CONTENT_ENCODING, HeaderValue, VARY};
#[test]
fn test_is_mime_compressible_text_types() {
assert!(is_mime_compressible(&mime::TEXT_PLAIN));
assert!(is_mime_compressible(&mime::TEXT_HTML));
assert!(is_mime_compressible(&mime::TEXT_CSS));
assert!(is_mime_compressible(&mime::TEXT_JAVASCRIPT));
}
#[test]
fn test_is_mime_compressible_application_json() {
assert!(is_mime_compressible(&mime::APPLICATION_JSON));
assert!(is_mime_compressible(&"application/xml".parse().unwrap()));
assert!(is_mime_compressible(&mime::APPLICATION_JAVASCRIPT));
assert!(is_mime_compressible(
&"application/ecmascript".parse().unwrap()
));
assert!(is_mime_compressible(
&"application/x-javascript".parse().unwrap()
));
}
#[test]
fn test_is_mime_compressible_xml_types() {
let xhtml: mime::Mime = "application/xhtml+xml".parse().unwrap();
assert_eq!(xhtml.type_(), mime::APPLICATION);
assert_eq!(xhtml.subtype().as_str(), "xhtml"); assert!(
!is_mime_compressible(&xhtml),
"当前实现不匹配 application/xhtml+xml"
);
let rss: mime::Mime = "application/rss+xml".parse().unwrap();
assert!(
!is_mime_compressible(&rss),
"当前实现不匹配 application/rss+xml"
);
let svg_app: mime::Mime = "application/svg+xml".parse().unwrap();
assert!(
!is_mime_compressible(&svg_app),
"当前实现不匹配 application/svg+xml"
);
}
#[test]
fn test_is_mime_compressible_image_svg() {
let svg: mime::Mime = "image/svg+xml".parse().unwrap();
assert_eq!(svg.type_(), mime::IMAGE);
assert_eq!(svg.subtype().as_str(), "svg");
assert!(!is_mime_compressible(&svg), "当前实现不匹配 image/svg+xml");
assert_eq!(mime::IMAGE_SVG.subtype().as_str(), "svg");
assert!(
!is_mime_compressible(&mime::IMAGE_SVG),
"当前实现不匹配 image/svg"
);
}
#[test]
fn test_is_mime_compressible_non_compressible() {
assert!(!is_mime_compressible(&mime::IMAGE_PNG));
assert!(!is_mime_compressible(&mime::IMAGE_JPEG));
assert!(!is_mime_compressible(&"video/mp4".parse().unwrap()));
assert!(!is_mime_compressible(&"audio/mp3".parse().unwrap()));
assert!(!is_mime_compressible(&mime::APPLICATION_OCTET_STREAM));
}
#[test]
fn test_parse_accept_encoding_brotli() {
assert!(parse_accept_encoding("br").is_some());
assert!(matches!(
parse_accept_encoding("br"),
Some(Compression::Brotli)
));
}
#[test]
fn test_parse_accept_encoding_gzip() {
assert!(parse_accept_encoding("gzip").is_some());
assert!(matches!(
parse_accept_encoding("gzip"),
Some(Compression::Gzip)
));
assert!(matches!(
parse_accept_encoding("x-gzip"),
Some(Compression::Gzip)
));
}
#[test]
fn test_parse_accept_encoding_wildcard() {
assert!(parse_accept_encoding("*").is_some());
assert!(matches!(
parse_accept_encoding("*"),
Some(Compression::Gzip)
));
}
#[test]
fn test_parse_accept_encoding_multiple() {
assert!(matches!(
parse_accept_encoding("br, gzip"),
Some(Compression::Brotli)
));
assert!(matches!(
parse_accept_encoding("gzip, br"),
Some(Compression::Brotli)
));
}
#[test]
fn test_parse_accept_encoding_with_quality() {
assert!(parse_accept_encoding("br;q=0, gzip").is_some());
assert!(matches!(
parse_accept_encoding("br;q=0, gzip"),
Some(Compression::Gzip)
));
assert!(matches!(
parse_accept_encoding("br;q=0.5, gzip;q=0.8"),
Some(Compression::Brotli)
));
assert!(parse_accept_encoding("br;q=0, gzip;q=0").is_none());
}
#[test]
fn test_parse_accept_encoding_invalid() {
assert!(parse_accept_encoding("").is_none());
assert!(parse_accept_encoding("identity").is_none());
assert!(parse_accept_encoding("deflate").is_none());
}
#[test]
fn test_parse_accept_encoding_whitespace() {
assert!(parse_accept_encoding("br, gzip").is_some());
assert!(parse_accept_encoding(" br , gzip ").is_some());
}
#[test]
fn test_parse_accept_encoding_priority() {
assert!(matches!(
parse_accept_encoding("gzip, br"),
Some(Compression::Brotli)
));
}
#[test]
fn test_negotiate_compression_disabled() {
let req = Request::default();
let options = StaticOptions {
enable_compression: false,
..Default::default()
};
assert!(negotiate(&options, &req, Some(&mime::TEXT_PLAIN)).is_none());
}
#[test]
fn test_negotiate_no_content_type() {
let req = Request::default();
let options = StaticOptions {
enable_compression: true,
..Default::default()
};
assert!(negotiate(&options, &req, None).is_none());
}
#[test]
fn test_negotiate_uncompressible_mime() {
let mut req = Request::default();
req.headers_mut()
.insert(ACCEPT_ENCODING, HeaderValue::from_static("br"));
let options = StaticOptions {
enable_compression: true,
..Default::default()
};
assert!(negotiate(&options, &req, Some(&mime::IMAGE_PNG)).is_none());
}
#[test]
fn test_negotiate_no_accept_encoding() {
let req = Request::default();
let options = StaticOptions {
enable_compression: true,
..Default::default()
};
assert!(negotiate(&options, &req, Some(&mime::TEXT_PLAIN)).is_none());
}
#[test]
fn test_negotiate_brotli() {
let mut req = Request::default();
req.headers_mut()
.insert(ACCEPT_ENCODING, HeaderValue::from_static("br"));
let options = StaticOptions {
enable_compression: true,
..Default::default()
};
assert!(matches!(
negotiate(&options, &req, Some(&mime::TEXT_PLAIN)),
Some(Compression::Brotli)
));
}
#[test]
fn test_negotiate_gzip() {
let mut req = Request::default();
req.headers_mut()
.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip"));
let options = StaticOptions {
enable_compression: true,
..Default::default()
};
assert!(matches!(
negotiate(&options, &req, Some(&mime::TEXT_PLAIN)),
Some(Compression::Gzip)
));
}
#[test]
fn test_negotiate_invalid_accept_encoding() {
let mut req = Request::default();
req.headers_mut()
.insert(ACCEPT_ENCODING, HeaderValue::from_static("invalid"));
let options = StaticOptions {
enable_compression: true,
..Default::default()
};
assert!(negotiate(&options, &req, Some(&mime::TEXT_PLAIN)).is_none());
}
#[test]
fn test_negotiate_multiple_text_types() {
let mut req = Request::default();
req.headers_mut()
.insert(ACCEPT_ENCODING, HeaderValue::from_static("br, gzip"));
let options = StaticOptions {
enable_compression: true,
..Default::default()
};
assert!(negotiate(&options, &req, Some(&mime::TEXT_HTML)).is_some());
assert!(negotiate(&options, &req, Some(&mime::APPLICATION_JSON)).is_some());
assert!(negotiate(&options, &req, Some(&mime::TEXT_CSS)).is_some());
}
#[test]
fn test_apply_headers_brotli() {
let mut res = Response::empty();
apply_headers(&mut res, &Compression::Brotli);
assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "br");
assert_eq!(res.headers().get(VARY).unwrap(), "Accept-Encoding");
}
#[test]
fn test_apply_headers_gzip() {
let mut res = Response::empty();
apply_headers(&mut res, &Compression::Gzip);
assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "gzip");
assert_eq!(res.headers().get(VARY).unwrap(), "Accept-Encoding");
}
#[test]
fn test_apply_headers_preserves_other_headers() {
let mut res = Response::empty();
res.headers_mut()
.insert("custom-header", "value".parse().unwrap());
apply_headers(&mut res, &Compression::Brotli);
assert_eq!(res.headers().get("custom-header").unwrap(), "value");
}
}