use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContentEncodingError {
Empty,
InvalidFormat,
InvalidEncoding,
}
impl fmt::Display for ContentEncodingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContentEncodingError::Empty => write!(f, "empty Content-Encoding"),
ContentEncodingError::InvalidFormat => {
write!(f, "invalid Content-Encoding format")
}
ContentEncodingError::InvalidEncoding => {
write!(f, "invalid Content-Encoding token")
}
}
}
}
impl std::error::Error for ContentEncodingError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContentCoding {
Gzip,
Deflate,
Compress,
Identity,
Other(String),
}
impl ContentCoding {
pub fn as_str(&self) -> &str {
match self {
ContentCoding::Gzip => "gzip",
ContentCoding::Deflate => "deflate",
ContentCoding::Compress => "compress",
ContentCoding::Identity => "identity",
ContentCoding::Other(value) => value.as_str(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentEncoding {
encodings: Vec<ContentCoding>,
}
impl ContentEncoding {
pub fn parse(input: &str) -> Result<Self, ContentEncodingError> {
let input = input.trim();
let mut encodings = Vec::new();
if !input.is_empty() {
for part in input.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let coding = parse_coding(part)?;
encodings.push(coding);
}
}
Ok(ContentEncoding { encodings })
}
pub fn encodings(&self) -> &[ContentCoding] {
&self.encodings
}
pub fn has_gzip(&self) -> bool {
self.encodings
.iter()
.any(|coding| matches!(coding, ContentCoding::Gzip))
}
pub fn has_deflate(&self) -> bool {
self.encodings
.iter()
.any(|coding| matches!(coding, ContentCoding::Deflate))
}
pub fn has_compress(&self) -> bool {
self.encodings
.iter()
.any(|coding| matches!(coding, ContentCoding::Compress))
}
pub fn has_identity(&self) -> bool {
self.encodings
.iter()
.any(|coding| matches!(coding, ContentCoding::Identity))
}
}
impl fmt::Display for ContentEncoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let values: Vec<&str> = self.encodings.iter().map(ContentCoding::as_str).collect();
write!(f, "{}", values.join(", "))
}
}
fn parse_coding(token: &str) -> Result<ContentCoding, ContentEncodingError> {
if token.is_empty() {
return Err(ContentEncodingError::InvalidFormat);
}
if !is_valid_token(token) {
return Err(ContentEncodingError::InvalidEncoding);
}
let normalized = token.to_ascii_lowercase();
let coding = match normalized.as_str() {
"gzip" => ContentCoding::Gzip,
"deflate" => ContentCoding::Deflate,
"compress" => ContentCoding::Compress,
"identity" => ContentCoding::Identity,
_ => ContentCoding::Other(normalized),
};
Ok(coding)
}
fn is_valid_token(s: &str) -> bool {
!s.is_empty() && s.bytes().all(is_token_char)
}
fn is_token_char(b: u8) -> bool {
matches!(
b,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
b'0'..=b'9' | b'A'..=b'Z' | b'^' | b'_' | b'`' | b'a'..=b'z' | b'|' | b'~'
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single() {
let ce = ContentEncoding::parse("gzip").unwrap();
assert_eq!(ce.encodings().len(), 1);
assert!(ce.has_gzip());
}
#[test]
fn parse_multiple() {
let ce = ContentEncoding::parse("gzip, deflate, identity").unwrap();
assert_eq!(ce.encodings().len(), 3);
assert!(ce.has_deflate());
assert!(ce.has_identity());
}
#[test]
fn parse_unknown() {
let ce = ContentEncoding::parse("br").unwrap();
assert_eq!(ce.encodings().len(), 1);
assert_eq!(ce.encodings()[0], ContentCoding::Other("br".to_string()));
}
#[test]
fn parse_empty() {
let ce = ContentEncoding::parse("").unwrap();
assert!(ce.encodings().is_empty());
}
#[test]
fn parse_invalid() {
assert!(ContentEncoding::parse("gzip,").is_ok());
assert!(ContentEncoding::parse("g zip").is_err());
}
#[test]
fn display() {
let ce = ContentEncoding::parse("GZIP, Deflate").unwrap();
assert_eq!(ce.to_string(), "gzip, deflate");
}
}