use std::collections::HashSet;
use crate::rkyvutil::OwnedArchive;
#[derive(
Clone,
Debug,
Default,
Eq,
PartialEq,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
)]
#[rkyv(derive(Debug))]
pub struct CacheControl {
pub max_age_seconds: Option<u64>,
pub no_cache: bool,
pub no_store: bool,
pub no_transform: bool,
pub max_stale_seconds: Option<u64>,
pub min_fresh_seconds: Option<u64>,
pub only_if_cached: bool,
pub must_revalidate: bool,
pub must_understand: bool,
pub private: bool,
pub proxy_revalidate: bool,
pub public: bool,
pub s_maxage_seconds: Option<u64>,
pub immutable: bool,
}
impl CacheControl {
pub fn to_archived(&self) -> OwnedArchive<Self> {
OwnedArchive::from_unarchived(self).expect("all possible values can be archived")
}
}
impl<'b, B: 'b + ?Sized + AsRef<[u8]>> FromIterator<&'b B> for CacheControl {
fn from_iter<T: IntoIterator<Item = &'b B>>(it: T) -> Self {
CacheControlParser::new(it).collect()
}
}
impl FromIterator<CacheControlDirective> for CacheControl {
fn from_iter<T: IntoIterator<Item = CacheControlDirective>>(it: T) -> Self {
fn parse_int(value: &[u8]) -> Option<u64> {
if !value.iter().all(u8::is_ascii_digit) {
return None;
}
std::str::from_utf8(value).ok()?.parse().ok()
}
let mut cc = Self::default();
for ccd in it {
match &*ccd.name {
"max-age" => match parse_int(&ccd.value) {
None => cc.must_revalidate = true,
Some(seconds) => cc.max_age_seconds = Some(seconds),
},
"no-cache" => cc.no_cache = true,
"no-store" => cc.no_store = true,
"no-transform" => cc.no_transform = true,
"max-stale" => {
if ccd.value.is_empty() {
cc.max_stale_seconds = Some(u64::MAX);
} else {
match parse_int(&ccd.value) {
None => cc.must_revalidate = true,
Some(seconds) => cc.max_stale_seconds = Some(seconds),
}
}
}
"min-fresh" => match parse_int(&ccd.value) {
None => cc.must_revalidate = true,
Some(seconds) => cc.min_fresh_seconds = Some(seconds),
},
"only-if-cached" => cc.only_if_cached = true,
"must-revalidate" => cc.must_revalidate = true,
"must-understand" => cc.must_understand = true,
"private" => cc.private = true,
"proxy-revalidate" => cc.proxy_revalidate = true,
"public" => cc.public = true,
"s-maxage" => match parse_int(&ccd.value) {
None => cc.must_revalidate = true,
Some(seconds) => cc.s_maxage_seconds = Some(seconds),
},
"immutable" => cc.immutable = true,
_ => {}
}
}
cc
}
}
struct CacheControlParser<'b, I> {
cur: &'b [u8],
directives: I,
seen: HashSet<String>,
}
impl<'b, B: 'b + ?Sized + AsRef<[u8]>, I: Iterator<Item = &'b B>> CacheControlParser<'b, I> {
fn new<II: IntoIterator<IntoIter = I>>(headers: II) -> Self {
let mut directives = headers.into_iter();
let cur = directives.next().map(AsRef::as_ref).unwrap_or(b"");
CacheControlParser {
cur,
directives,
seen: HashSet::new(),
}
}
fn parse_token(&mut self) -> Option<String> {
fn is_token_byte(byte: u8) -> bool {
matches!(
byte,
| b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+'
| b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~'
| b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
)
}
let mut end = 0;
while self.cur.get(end).copied().is_some_and(is_token_byte) {
end += 1;
}
if end == 0 {
None
} else {
let (token, rest) = self.cur.split_at(end);
self.cur = rest;
Some(String::from_utf8(token.to_vec()).expect("all valid token bytes are valid UTF-8"))
}
}
fn maybe_parse_equals(&mut self) -> bool {
if self.cur.first().is_some_and(|&byte| byte == b'=') {
self.cur = &self.cur[1..];
true
} else {
false
}
}
fn parse_value(&mut self) -> Option<Vec<u8>> {
if *self.cur.first()? == b'"' {
self.cur = &self.cur[1..];
self.parse_quoted_string()
} else {
self.parse_token().map(String::into_bytes)
}
}
fn parse_quoted_string(&mut self) -> Option<Vec<u8>> {
fn is_qdtext_byte(byte: u8) -> bool {
matches!(byte, b'\t' | b' ' | 0x21 | 0x23..=0x5B | 0x5D..=0x7E | 0x80..=0xFF)
}
fn is_quoted_pair_byte(byte: u8) -> bool {
matches!(byte, b'\t' | b' ' | 0x21..=0x7E | 0x80..=0xFF)
}
let mut value = vec![];
while !self.cur.is_empty() {
let byte = self.cur[0];
self.cur = &self.cur[1..];
if byte == b'"' {
return Some(value);
} else if byte == b'\\' {
let byte = *self.cur.first()?;
self.cur = &self.cur[1..];
if !is_quoted_pair_byte(byte) {
return None;
}
value.push(byte);
} else if is_qdtext_byte(byte) {
value.push(byte);
} else {
break;
}
}
None
}
fn maybe_parse_directive_delimiter(&mut self) -> bool {
if self.cur.first().is_some_and(|&byte| byte == b',') {
self.cur = &self.cur[1..];
true
} else {
false
}
}
fn skip_whitespace(&mut self) {
while self.cur.first().is_some_and(u8::is_ascii_whitespace) {
self.cur = &self.cur[1..];
}
}
fn emit_directive(
&mut self,
directive: CacheControlDirective,
) -> Option<CacheControlDirective> {
let duplicate = !self.seen.insert(directive.name.clone());
if duplicate {
self.emit_revalidation()
} else {
Some(directive)
}
}
fn emit_revalidation(&mut self) -> Option<CacheControlDirective> {
if self.seen.insert("must-revalidate".to_string()) {
Some(CacheControlDirective::must_revalidate())
} else {
None
}
}
}
impl<'b, B: 'b + ?Sized + AsRef<[u8]>, I: Iterator<Item = &'b B>> Iterator
for CacheControlParser<'b, I>
{
type Item = CacheControlDirective;
fn next(&mut self) -> Option<CacheControlDirective> {
loop {
if self.cur.is_empty() {
self.cur = self.directives.next().map(AsRef::as_ref)?;
}
while !self.cur.is_empty() {
self.skip_whitespace();
let Some(mut name) = self.parse_token() else {
let invalid = !self.cur.is_empty();
self.cur = b"";
if invalid {
if let Some(d) = self.emit_revalidation() {
return Some(d);
}
}
break;
};
name.make_ascii_lowercase();
if !self.maybe_parse_equals() {
self.skip_whitespace();
self.maybe_parse_directive_delimiter();
let directive = CacheControlDirective {
name,
value: vec![],
};
match self.emit_directive(directive) {
None => continue,
Some(d) => return Some(d),
}
}
let Some(value) = self.parse_value() else {
self.cur = b"";
match self.emit_revalidation() {
None => break,
Some(d) => return Some(d),
}
};
self.skip_whitespace();
self.maybe_parse_directive_delimiter();
let directive = CacheControlDirective { name, value };
if let Some(d) = self.emit_directive(directive) {
return Some(d);
}
}
}
}
}
#[derive(Debug, Eq, PartialEq)]
struct CacheControlDirective {
name: String,
value: Vec<u8>,
}
impl CacheControlDirective {
fn must_revalidate() -> Self {
Self {
name: "must-revalidate".to_string(),
value: vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_control_token() {
let cc: CacheControl = CacheControlParser::new(["no-cache"]).collect();
assert!(cc.no_cache);
assert!(!cc.must_revalidate);
}
#[test]
fn cache_control_max_age() {
let cc: CacheControl = CacheControlParser::new(["max-age=60"]).collect();
assert_eq!(Some(60), cc.max_age_seconds);
assert!(!cc.must_revalidate);
}
#[test]
fn cache_control_max_age_quoted() {
let cc: CacheControl = CacheControlParser::new([r#"max-age="60""#]).collect();
assert_eq!(Some(60), cc.max_age_seconds);
assert!(!cc.must_revalidate);
}
#[test]
fn cache_control_max_age_invalid() {
let cc: CacheControl = CacheControlParser::new(["max-age=6a0"]).collect();
assert_eq!(None, cc.max_age_seconds);
assert!(cc.must_revalidate);
}
#[test]
fn cache_control_immutable() {
let cc: CacheControl = CacheControlParser::new(["max-age=31536000, immutable"]).collect();
assert_eq!(Some(31_536_000), cc.max_age_seconds);
assert!(cc.immutable);
assert!(!cc.must_revalidate);
}
#[test]
fn cache_control_unrecognized() {
let cc: CacheControl = CacheControlParser::new(["lion,max-age=60,zebra"]).collect();
assert_eq!(Some(60), cc.max_age_seconds);
}
#[test]
fn cache_control_invalid_squashes_remainder() {
let cc: CacheControl = CacheControlParser::new(["no-cache,\x00,max-age=60"]).collect();
assert!(cc.no_cache);
assert_eq!(None, cc.max_age_seconds);
assert!(cc.must_revalidate);
}
#[test]
fn cache_control_invalid_squashes_remainder_but_not_other_header_values() {
let cc: CacheControl =
CacheControlParser::new(["no-cache,\x00,max-age=60", "max-stale=30"]).collect();
assert!(cc.no_cache);
assert_eq!(Some(30), cc.max_stale_seconds);
assert!(cc.must_revalidate);
}
#[test]
fn cache_control_parse_token() {
let directives = CacheControlParser::new(["no-cache"]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![CacheControlDirective {
name: "no-cache".to_string(),
value: vec![]
}]
);
}
#[test]
fn cache_control_parse_token_to_token_value() {
let directives = CacheControlParser::new(["max-age=60"]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
}]
);
}
#[test]
fn cache_control_parse_token_to_quoted_string() {
let directives =
CacheControlParser::new([r#"private="cookie,x-something-else""#]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![CacheControlDirective {
name: "private".to_string(),
value: b"cookie,x-something-else".to_vec(),
}]
);
}
#[test]
fn cache_control_parse_token_to_quoted_string_with_escape() {
let directives =
CacheControlParser::new([r#"private="something\"crazy""#]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![CacheControlDirective {
name: "private".to_string(),
value: br#"something"crazy"#.to_vec(),
}]
);
}
#[test]
fn cache_control_parse_multiple_directives() {
let header = r#"max-age=60, no-cache, private="cookie", no-transform"#;
let directives = CacheControlParser::new([header]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![
CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
},
CacheControlDirective {
name: "no-cache".to_string(),
value: vec![]
},
CacheControlDirective {
name: "private".to_string(),
value: b"cookie".to_vec(),
},
CacheControlDirective {
name: "no-transform".to_string(),
value: vec![]
},
]
);
}
#[test]
fn cache_control_parse_multiple_directives_across_multiple_header_values() {
let headers = [
r"max-age=60, no-cache",
r#"private="cookie""#,
r"no-transform",
];
let directives = CacheControlParser::new(headers).collect::<Vec<_>>();
assert_eq!(
directives,
vec![
CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
},
CacheControlDirective {
name: "no-cache".to_string(),
value: vec![]
},
CacheControlDirective {
name: "private".to_string(),
value: b"cookie".to_vec(),
},
CacheControlDirective {
name: "no-transform".to_string(),
value: vec![]
},
]
);
}
#[test]
fn cache_control_parse_one_header_invalid() {
let headers = [
r"max-age=60, no-cache",
r#", private="cookie""#,
r"no-transform",
];
let directives = CacheControlParser::new(headers).collect::<Vec<_>>();
assert_eq!(
directives,
vec![
CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
},
CacheControlDirective {
name: "no-cache".to_string(),
value: vec![]
},
CacheControlDirective {
name: "must-revalidate".to_string(),
value: vec![]
},
CacheControlDirective {
name: "no-transform".to_string(),
value: vec![]
},
]
);
}
#[test]
fn cache_control_parse_invalid_directive_drops_remainder() {
let header = r#"max-age=60, no-cache, ="cookie", no-transform"#;
let directives = CacheControlParser::new([header]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![
CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
},
CacheControlDirective {
name: "no-cache".to_string(),
value: vec![]
},
CacheControlDirective {
name: "must-revalidate".to_string(),
value: vec![]
},
]
);
}
#[test]
fn cache_control_parse_name_normalized() {
let header = r"MAX-AGE=60";
let directives = CacheControlParser::new([header]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
}]
);
}
#[test]
fn cache_control_parse_duplicate_directives() {
let header = r"max-age=60, no-cache, max-age=30";
let directives = CacheControlParser::new([header]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![
CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
},
CacheControlDirective {
name: "no-cache".to_string(),
value: vec![]
},
CacheControlDirective {
name: "must-revalidate".to_string(),
value: vec![]
},
]
);
}
#[test]
fn cache_control_parse_duplicate_directives_across_headers() {
let headers = [r"max-age=60, no-cache", r"max-age=30"];
let directives = CacheControlParser::new(headers).collect::<Vec<_>>();
assert_eq!(
directives,
vec![
CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
},
CacheControlDirective {
name: "no-cache".to_string(),
value: vec![]
},
CacheControlDirective {
name: "must-revalidate".to_string(),
value: vec![]
},
]
);
}
#[test]
fn cache_control_parse_duplicate_redux() {
let header = r"max-age=60, no-cache, no-cache, max-age=30";
let directives = CacheControlParser::new([header]).collect::<Vec<_>>();
assert_eq!(
directives,
vec![
CacheControlDirective {
name: "max-age".to_string(),
value: b"60".to_vec(),
},
CacheControlDirective {
name: "no-cache".to_string(),
value: vec![]
},
CacheControlDirective {
name: "must-revalidate".to_string(),
value: vec![]
},
]
);
}
}