#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Encoding {
Gzip,
Brotli,
Deflate,
Identity,
}
impl Encoding {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"gzip" | "x-gzip" => Some(Encoding::Gzip),
"br" => Some(Encoding::Brotli),
"deflate" => Some(Encoding::Deflate),
"identity" => Some(Encoding::Identity),
"*" => Some(Encoding::Identity),
_ => None,
}
}
pub fn as_str(&self) -> &str {
match self {
Encoding::Gzip => "gzip",
Encoding::Brotli => "br",
Encoding::Deflate => "deflate",
Encoding::Identity => "identity",
}
}
}
impl std::fmt::Display for Encoding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EncodingQuality {
pub encoding: Encoding,
pub quality: f32,
}
impl EncodingQuality {
pub fn new(encoding: Encoding) -> Self {
Self {
encoding,
quality: 1.0,
}
}
pub fn with_quality(encoding: Encoding, quality: f32) -> Self {
Self {
encoding,
quality: quality.clamp(0.0, 1.0),
}
}
pub fn parse(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.split(';').collect();
let encoding = Encoding::parse(parts.first()?.trim())?;
let mut quality = 1.0;
for param in parts.iter().skip(1) {
let param = param.trim();
if let Some((key, value)) = param.split_once('=')
&& key.trim() == "q"
&& let Ok(q) = value.trim().parse::<f32>()
{
if !q.is_finite() {
return None;
}
quality = q.clamp(0.0, 1.0);
}
}
Some(Self { encoding, quality })
}
}
#[derive(Debug, Clone)]
pub struct EncodingNegotiator {
preference_order: Vec<Encoding>,
}
impl EncodingNegotiator {
pub fn new() -> Self {
Self {
preference_order: vec![
Encoding::Brotli,
Encoding::Gzip,
Encoding::Deflate,
Encoding::Identity,
],
}
}
pub fn with_preference(preference_order: Vec<Encoding>) -> Self {
Self { preference_order }
}
pub fn negotiate(&self, accept_encoding: &str, available: &[Encoding]) -> Encoding {
let mut requested = self.parse_accept_encoding(accept_encoding);
requested.sort_by(|a, b| {
b.quality
.partial_cmp(&a.quality)
.unwrap_or(std::cmp::Ordering::Equal)
});
for req in &requested {
if available.contains(&req.encoding) {
return req.encoding.clone();
}
}
for pref in &self.preference_order {
if available.contains(pref) {
return pref.clone();
}
}
Encoding::Identity
}
pub fn parse_accept_encoding(&self, header: &str) -> Vec<EncodingQuality> {
header
.split(',')
.filter_map(|s| EncodingQuality::parse(s.trim()))
.collect()
}
pub fn select_best(&self, accept_encoding: &str, available: &[Encoding]) -> Encoding {
self.negotiate(accept_encoding, available)
}
}
impl Default for EncodingNegotiator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[test]
fn test_encoding_parse() {
assert_eq!(Encoding::parse("gzip"), Some(Encoding::Gzip));
assert_eq!(Encoding::parse("br"), Some(Encoding::Brotli));
assert_eq!(Encoding::parse("deflate"), Some(Encoding::Deflate));
assert_eq!(Encoding::parse("identity"), Some(Encoding::Identity));
}
#[test]
fn test_encoding_quality_parse() {
let enc = EncodingQuality::parse("gzip;q=0.9").unwrap();
assert_eq!(enc.encoding, Encoding::Gzip);
assert_eq!(enc.quality, 0.9);
}
#[test]
fn test_negotiate() {
let negotiator = EncodingNegotiator::new();
let available = vec![Encoding::Gzip, Encoding::Identity];
let result = negotiator.negotiate("gzip, deflate", &available);
assert_eq!(result, Encoding::Gzip);
}
#[test]
fn test_negotiate_quality() {
let negotiator = EncodingNegotiator::new();
let available = vec![Encoding::Gzip, Encoding::Identity];
let result = negotiator.negotiate("gzip;q=0.5, identity;q=1.0", &available);
assert_eq!(result, Encoding::Identity);
}
#[test]
fn test_negotiate_fallback() {
let negotiator = EncodingNegotiator::new();
let available = vec![Encoding::Identity];
let result = negotiator.negotiate("br, gzip", &available);
assert_eq!(result, Encoding::Identity);
}
#[rstest]
#[case("gzip;q=NaN")]
#[case("gzip;q=inf")]
#[case("gzip;q=-inf")]
fn test_parse_rejects_non_finite_quality(#[case] input: &str) {
let result = EncodingQuality::parse(input);
assert_eq!(result, None, "Non-finite quality value should be rejected");
}
#[rstest]
fn test_negotiate_does_not_panic_on_nan_quality() {
let negotiator = EncodingNegotiator::new();
let available = vec![Encoding::Gzip, Encoding::Identity];
let result = negotiator.negotiate("gzip;q=NaN, identity;q=0.9", &available);
assert_eq!(result, Encoding::Identity);
}
}