#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod request;
pub mod response;
use std::fmt;
use http::header::{CONTENT_ENCODING, CONTENT_LENGTH, HeaderMap, HeaderValue};
use crate::response::AcceptEncoding;
#[derive(Clone, Debug, Default)]
pub struct ContentEncoding {
codings: Vec<Coding>,
unreadable: bool,
}
impl From<&str> for ContentEncoding {
fn from(value: &str) -> Self {
let mut this = Self::default();
this.merge(value);
this
}
}
impl From<&HeaderMap> for ContentEncoding {
fn from(headers: &HeaderMap) -> Self {
let mut this = Self::default();
for value in headers.get_all(CONTENT_ENCODING) {
let Ok(value) = value.to_str() else {
this.unreadable = true;
continue;
};
this.merge(value);
}
this
}
}
impl ContentEncoding {
fn merge(&mut self, value: &str) {
self.codings.extend(
value
.split(',')
.map(str::trim)
.filter(|token| !token.is_empty())
.map(Coding::from_token),
);
}
pub fn codings(&self) -> &[Coding] {
&self.codings
}
pub fn layer(&self, coding: Coding) -> Self {
let mut layered = self.clone();
layered.codings.push(coding);
layered
}
pub fn to_header_value(&self) -> Option<HeaderValue> {
if self.codings.is_empty() {
return None;
}
HeaderValue::from_str(&self.to_string()).ok()
}
pub fn can_decode_as(&self, accept: &AcceptEncoding) -> Option<Coding> {
if self.unreadable {
return None;
}
let outermost = self.codings.last()?;
(outermost.is_supported() && accept.accepts(outermost)).then(|| outermost.clone())
}
pub fn peeled(&self) -> Self {
let mut peeled = self.clone();
peeled.codings.pop();
peeled
}
pub fn peel_one_header(headers: &mut HeaderMap, accept: &AcceptEncoding) -> Option<Coding> {
let encoding = Self::from(&*headers);
let coding = encoding.can_decode_as(accept)?;
match encoding.peeled().to_header_value() {
Some(value) => headers.insert(CONTENT_ENCODING, value),
None => headers.remove(CONTENT_ENCODING),
};
headers.remove(CONTENT_LENGTH);
Some(coding)
}
}
impl fmt::Display for ContentEncoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (n, coding) in self.codings.iter().enumerate() {
if n > 0 {
f.write_str(", ")?;
}
f.write_str(coding.token())?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Coding {
Gzip,
Deflate,
Brotli,
Zstd,
Other(String),
}
impl Coding {
pub fn from_option(value: &str) -> Option<Self> {
match value {
"gzip" => Some(Self::Gzip),
"deflate" => Some(Self::Deflate),
"br" => Some(Self::Brotli),
"zstd" => Some(Self::Zstd),
_ => None,
}
}
pub fn token(&self) -> &str {
match self {
Self::Gzip => "gzip",
Self::Deflate => "deflate",
Self::Brotli => "br",
Self::Zstd => "zstd",
Self::Other(token) => token,
}
}
pub fn from_token(token: &str) -> Self {
let token = token.trim();
if token.eq_ignore_ascii_case("gzip") || token.eq_ignore_ascii_case("x-gzip") {
Self::Gzip
} else if token.eq_ignore_ascii_case("deflate") {
Self::Deflate
} else if token.eq_ignore_ascii_case("br") {
Self::Brotli
} else if token.eq_ignore_ascii_case("zstd") {
Self::Zstd
} else {
Self::Other(token.to_ascii_lowercase())
}
}
pub fn is_supported(&self) -> bool {
!matches!(self, Self::Other(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::header::HeaderValue;
fn headers(value: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_ENCODING, HeaderValue::from_str(value).unwrap());
headers
}
#[test]
fn a_layered_coding_is_added_last() {
let existing = ContentEncoding::from(&headers("gzip"));
let layered = existing.layer(Coding::Zstd);
assert_eq!(layered.to_string(), "gzip, zstd");
assert_eq!(
ContentEncoding::from(&headers("gzip, br"))
.layer(Coding::Deflate)
.to_string(),
"gzip, br, deflate"
);
assert_eq!(existing.to_string(), "gzip");
}
#[test]
fn a_request_declaring_nothing_carries_only_the_layered_coding() {
assert_eq!(
ContentEncoding::default().layer(Coding::Brotli).to_string(),
"br"
);
for value in ["", " "] {
assert_eq!(
ContentEncoding::from(&headers(value))
.layer(Coding::Gzip)
.to_string(),
"gzip"
);
}
}
#[test]
fn a_coding_this_crate_cannot_apply_is_still_declarable() {
let layered = ContentEncoding::from(&headers("custom-thing")).layer(Coding::Gzip);
assert_eq!(layered.to_string(), "custom-thing, gzip");
assert_eq!(
layered.codings(),
[Coding::Other("custom-thing".into()), Coding::Gzip]
);
assert!(
futures::executor::block_on(crate::request::compress_buffer(
b"x",
Coding::Other("custom-thing".into())
))
.is_err()
);
}
#[test]
fn nothing_to_declare_makes_no_header() {
assert!(ContentEncoding::default().to_header_value().is_none());
assert_eq!(
ContentEncoding::default()
.layer(Coding::Gzip)
.to_header_value()
.unwrap(),
"gzip"
);
}
#[test]
fn the_compress_option_names_a_coding_by_its_wire_token() {
assert_eq!(Coding::from_option("gzip"), Some(Coding::Gzip));
assert_eq!(Coding::from_option("deflate"), Some(Coding::Deflate));
assert_eq!(Coding::from_option("br"), Some(Coding::Brotli));
assert_eq!(Coding::from_option("zstd"), Some(Coding::Zstd));
}
#[test]
fn the_compress_option_matches_its_tokens_exactly() {
assert_eq!(Coding::from_token("x-gzip"), Coding::Gzip);
assert_eq!(Coding::from_option("x-gzip"), None);
assert_eq!(Coding::from_token("GZIP"), Coding::Gzip);
assert_eq!(Coding::from_option("GZIP"), None);
assert_eq!(Coding::from_option(" gzip"), None);
assert_eq!(Coding::from_option("brotli"), None);
assert_eq!(Coding::from_option("identity"), None);
assert_eq!(Coding::from_option(""), None);
}
#[test]
fn a_coding_this_crate_cannot_decode_is_still_named() {
let identity = Coding::from_token("identity");
assert_eq!(identity, Coding::Other("identity".into()));
assert!(!identity.is_supported());
assert_eq!(identity.token(), "identity");
assert_eq!(Coding::from_token("LZMA"), Coding::from_token("lzma"));
assert!(Coding::Gzip.is_supported());
}
}