use std::fmt;
use std::io::Read;
use std::io::Write;
use std::str::FromStr;
use bytes::Bytes;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compression {
Auto,
Zstd,
Brotli,
Gzip,
Deflate,
}
impl Compression {
pub const CODINGS: &[Self] = &[Self::Zstd, Self::Brotli, Self::Gzip, Self::Deflate];
pub const COUNT: usize = Self::CODINGS.len() + 1;
pub const ACCEPTED: &str = "zstd, br, gzip, deflate";
pub const IDENTITY: &str = "identity";
pub const GZIP_ALIAS: &str = "x-gzip";
pub const ZSTD_LEVEL: i32 = 3;
pub const BROTLI_QUALITY: i32 = 5;
pub const BROTLI_WINDOW: i32 = 22;
pub const BUFFER: usize = 8192;
pub fn as_str(&self) -> &'static str {
match self {
Self::Auto => "",
Self::Zstd => "zstd",
Self::Brotli => "br",
Self::Gzip => "gzip",
Self::Deflate => "deflate",
}
}
pub fn parse(token: &str) -> Option<Self> {
let token = token.trim_ascii();
match (token.len(), token.as_bytes().first()?.to_ascii_lowercase()) {
(2, b'b') => token.eq_ignore_ascii_case(Self::Brotli.as_str()).then_some(Self::Brotli),
(4, b'g') => token.eq_ignore_ascii_case(Self::Gzip.as_str()).then_some(Self::Gzip),
(4, b'z') => token.eq_ignore_ascii_case(Self::Zstd.as_str()).then_some(Self::Zstd),
(6, b'x') => token.eq_ignore_ascii_case(Self::GZIP_ALIAS).then_some(Self::Gzip),
(7, b'd') => token.eq_ignore_ascii_case(Self::Deflate.as_str()).then_some(Self::Deflate),
_ => None,
}
}
pub fn accepted<'a>(values: impl Iterator<Item = &'a str>) -> Option<Self> {
let mut quality = [None; Self::COUNT];
let mut wildcard = None;
for value in values {
for coding in Coding::list(value) {
match coding.compression() {
Some(compression) => quality[compression as usize] = Some(coding.quality),
None if coding.wildcard() => wildcard = Some(coding.quality),
None => {}
}
}
}
let permitted = |coding: &Self| quality[*coding as usize].or(wildcard).unwrap_or(Coding::NONE) > Coding::NONE;
Self::CODINGS.iter().copied().find(permitted)
}
pub fn applied<'a>(values: impl Iterator<Item = &'a str>) -> Option<Self> {
let mut applied = None;
for value in values {
for coding in Coding::list(value) {
if coding.token.eq_ignore_ascii_case(Self::IDENTITY) {
continue;
}
if applied.is_some() {
return None;
}
applied = Some(coding.compression()?);
}
}
applied
}
pub fn encoded<'a>(values: impl Iterator<Item = &'a str>) -> bool {
for value in values {
for coding in Coding::list(value) {
if !coding.token.eq_ignore_ascii_case(Self::IDENTITY) {
return true;
}
}
}
false
}
pub fn drain(reader: impl Read, max: u64, out: &mut Vec<u8>) -> Result<(), Error> {
let start = out.len();
let mut bounded = reader.take(max.saturating_add(1));
match bounded.read_to_end(out) {
Ok(produced) if produced as u64 <= max => Ok(()),
Ok(_) => {
out.truncate(start);
Err(Error::TooLarge(max))
}
Err(err) => {
out.truncate(start);
Err(Error::coding(err))
}
}
}
pub fn encode(&self, input: &[u8]) -> Result<Bytes, Error> {
let mut out = Vec::with_capacity(input.len() / 2 + Self::BUFFER.min(input.len() + 64));
self.encode_into(input, &mut out)?;
Ok(Bytes::from(out))
}
pub fn encode_into(&self, input: &[u8], out: &mut Vec<u8>) -> Result<(), Error> {
match self {
Self::Auto => Err(Error::Settled),
Self::Zstd => zstd::stream::copy_encode(input, out, Self::ZSTD_LEVEL).map_err(Error::coding),
Self::Brotli => {
let params = brotli::enc::BrotliEncoderParams { quality: Self::BROTLI_QUALITY, lgwin: Self::BROTLI_WINDOW, ..Default::default() };
let mut source = input;
brotli::BrotliCompress(&mut source, out, ¶ms).map(drop).map_err(Error::coding)
}
Self::Gzip => {
let mut encoder = flate2::write::GzEncoder::new(out, flate2::Compression::default());
encoder.write_all(input).map_err(Error::coding)?;
encoder.finish().map(drop).map_err(Error::coding)
}
Self::Deflate => {
let mut encoder = flate2::write::ZlibEncoder::new(out, flate2::Compression::default());
encoder.write_all(input).map_err(Error::coding)?;
encoder.finish().map(drop).map_err(Error::coding)
}
}
}
pub fn decode(&self, input: &[u8], max: u64) -> Result<Bytes, Error> {
let mut out = Vec::new();
self.decode_into(input, max, &mut out)?;
Ok(Bytes::from(out))
}
pub fn decode_into(&self, input: &[u8], max: u64, out: &mut Vec<u8>) -> Result<(), Error> {
match self {
Self::Auto => Err(Error::Settled),
Self::Zstd => Self::drain(zstd::stream::read::Decoder::new(input).map_err(Error::coding)?, max, out),
Self::Brotli => Self::drain(brotli::Decompressor::new(input, Self::BUFFER), max, out),
Self::Gzip => Self::drain(flate2::read::GzDecoder::new(input), max, out),
Self::Deflate => match Self::drain(flate2::read::ZlibDecoder::new(input), max, out) {
Err(Error::Coding(_)) => Self::drain(flate2::read::DeflateDecoder::new(input), max, out),
settled => settled,
},
}
}
}
impl fmt::Display for Compression {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Compression {
type Err = ();
fn from_str(text: &str) -> Result<Self, Self::Err> {
Self::parse(text).ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Coding<'a> {
pub token: &'a str,
pub quality: f32,
}
impl<'a> Coding<'a> {
pub const WILDCARD: &'static str = "*";
pub const FULL: f32 = 1.0;
pub const NONE: f32 = 0.0;
pub const QUALITY: &'static str = "q";
pub const PLACES: [f32; 4] = [1.0, 10.0, 100.0, 1000.0];
#[inline]
pub fn qvalue(text: &str) -> Option<f32> {
let (whole, fraction) = text.as_bytes().split_first()?;
if !whole.is_ascii_digit() {
return None;
}
let mut value = (whole - b'0') as u32;
let mut places = 0;
if let Some((point, digits)) = fraction.split_first() {
if *point != b'.' || digits.len() >= Self::PLACES.len() {
return None;
}
for digit in digits {
if !digit.is_ascii_digit() {
return None;
}
value = value * 10 + (digit - b'0') as u32;
places += 1;
}
}
Some(value as f32 / Self::PLACES[places])
}
pub fn parse(entry: &'a str) -> Self {
let (token, parameters) = match Coding::split(entry, Self::PARAMETER) {
Some((token, parameters)) => (token.trim_ascii(), Some(parameters)),
None => (entry.trim_ascii(), None),
};
let Some(parameters) = parameters else {
return Self { token, quality: Self::FULL };
};
let written = parameters
.split(Self::PARAMETER as char)
.filter_map(|parameter| parameter.split_once('='))
.find(|(name, _)| name.trim_ascii().eq_ignore_ascii_case(Self::QUALITY));
let quality = match written {
Some((_, value)) => {
let value = value.trim_ascii();
Self::qvalue(value).or_else(|| value.parse::<f32>().ok()).filter(|quality| (Self::NONE..=Self::FULL).contains(quality)).unwrap_or(Self::NONE)
}
None => Self::FULL,
};
Self { token, quality }
}
pub const SEPARATOR: u8 = b',';
pub const PARAMETER: u8 = b';';
pub fn split(text: &str, octet: u8) -> Option<(&str, &str)> {
debug_assert!(octet.is_ascii(), "a delimiter outside ASCII can fall inside a character");
let at = crate::helpers::scan::find(text.as_bytes(), octet)?;
Some((&text[..at], &text[at + 1..]))
}
pub fn list(value: &'a str) -> Codings<'a> {
Codings { rest: value }
}
pub fn compression(&self) -> Option<Compression> {
Compression::parse(self.token)
}
pub fn wildcard(&self) -> bool {
self.token == Self::WILDCARD
}
pub fn accepts(&self) -> bool {
self.quality > Self::NONE
}
}
pub struct Codings<'a> {
pub rest: &'a str,
}
impl<'a> Iterator for Codings<'a> {
type Item = Coding<'a>;
fn next(&mut self) -> Option<Coding<'a>> {
loop {
if self.rest.is_empty() {
return None;
}
let entry = match Coding::split(self.rest, Coding::SEPARATOR) {
Some((entry, rest)) => {
self.rest = rest;
entry
}
None => std::mem::take(&mut self.rest),
};
let coding = Coding::parse(entry);
if !coding.token.is_empty() {
return Some(coding);
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
Settled,
TooLarge(u64),
Coding(String),
}
impl Error {
pub fn coding(error: impl fmt::Display) -> Self {
Self::Coding(error.to_string())
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Settled => write!(f, "the content coding was never settled"),
Self::TooLarge(max) => write!(f, "the decoded body exceeds {max} octets"),
Self::Coding(reason) => write!(f, "the content coding failed: {reason}"),
}
}
}
impl std::error::Error for Error {}