use crate::validate::trim_ows;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ETagError {
Empty,
InvalidFormat,
MissingQuote,
InvalidCharacter,
}
impl fmt::Display for ETagError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ETagError::Empty => write!(f, "empty etag"),
ETagError::InvalidFormat => write!(f, "invalid etag format"),
ETagError::MissingQuote => write!(f, "missing quote in etag"),
ETagError::InvalidCharacter => write!(f, "invalid character in etag"),
}
}
}
impl core::error::Error for ETagError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntityTag {
weak: bool,
tag: String,
}
impl EntityTag {
pub fn parse(input: &str) -> Result<Self, ETagError> {
let input = trim_ows(input);
if input.is_empty() {
return Err(ETagError::Empty);
}
let (weak, rest) = if let Some(rest) = input.strip_prefix("W/") {
(true, rest)
} else {
(false, input)
};
if !rest.starts_with('"') {
return Err(ETagError::MissingQuote);
}
let end_quote = rest[1..].find('"').ok_or(ETagError::MissingQuote)?;
let tag = &rest[1..1 + end_quote];
for c in tag.chars() {
if !is_etagc_char(c) {
return Err(ETagError::InvalidCharacter);
}
}
let after_quote = &rest[2 + end_quote..];
if !after_quote.is_empty() {
return Err(ETagError::InvalidFormat);
}
Ok(EntityTag {
weak,
tag: tag.to_string(),
})
}
pub fn strong(tag: &str) -> Result<Self, ETagError> {
for c in tag.chars() {
if !is_etagc_char(c) {
return Err(ETagError::InvalidCharacter);
}
}
Ok(EntityTag {
weak: false,
tag: tag.to_string(),
})
}
pub fn weak(tag: &str) -> Result<Self, ETagError> {
for c in tag.chars() {
if !is_etagc_char(c) {
return Err(ETagError::InvalidCharacter);
}
}
Ok(EntityTag {
weak: true,
tag: tag.to_string(),
})
}
pub fn is_weak(&self) -> bool {
self.weak
}
pub fn is_strong(&self) -> bool {
!self.weak
}
pub fn tag(&self) -> &str {
&self.tag
}
pub fn strong_compare(&self, other: &EntityTag) -> bool {
!self.weak && !other.weak && self.tag == other.tag
}
pub fn weak_compare(&self, other: &EntityTag) -> bool {
self.tag == other.tag
}
}
impl fmt::Display for EntityTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.weak {
write!(f, "W/\"{}\"", self.tag)
} else {
write!(f, "\"{}\"", self.tag)
}
}
}
fn is_etagc_char(c: char) -> bool {
c == '\x21' || ('\x23'..='\x7E').contains(&c) || c > '\x7F'
}
fn split_etag_list_raw(input: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut start = 0;
let mut in_quotes = false;
let bytes = input.as_bytes();
for i in 0..bytes.len() {
match bytes[i] {
b'"' => in_quotes = !in_quotes,
b',' if !in_quotes => {
parts.push(&input[start..i]);
start = i + 1;
}
_ => {}
}
}
parts.push(&input[start..]);
parts
}
pub fn parse_etag_list(input: &str) -> Result<ETagList, ETagError> {
let input = trim_ows(input);
if input.is_empty() {
return Err(ETagError::Empty);
}
if input == "*" {
return Ok(ETagList::Any);
}
let mut etags = Vec::new();
for part in split_etag_list_raw(input) {
let part = trim_ows(part);
if !part.is_empty() {
etags.push(EntityTag::parse(part)?);
}
}
if etags.is_empty() {
return Err(ETagError::Empty);
}
Ok(ETagList::Tags(etags))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ETagList {
Any,
Tags(Vec<EntityTag>),
}
impl ETagList {
pub fn is_any(&self) -> bool {
matches!(self, ETagList::Any)
}
pub fn contains_weak(&self, etag: &EntityTag) -> bool {
match self {
ETagList::Any => true,
ETagList::Tags(tags) => tags.iter().any(|t| t.weak_compare(etag)),
}
}
pub fn contains_strong(&self, etag: &EntityTag) -> bool {
match self {
ETagList::Any => true,
ETagList::Tags(tags) => tags.iter().any(|t| t.strong_compare(etag)),
}
}
}
impl fmt::Display for ETagList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ETagList::Any => write!(f, "*"),
ETagList::Tags(tags) => {
let s: Vec<String> = tags.iter().map(|t| t.to_string()).collect();
write!(f, "{}", s.join(", "))
}
}
}
}