use std::collections::HashSet;
use bitflags::bitflags;
use rkyv::primitive::{ArchivedU16, ArchivedU64};
#[derive(
Clone,
Debug,
Default,
Eq,
PartialEq,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Portable,
rkyv::Serialize,
bytecheck::CheckBytes,
)]
#[rkyv(as = Self)]
#[repr(C)]
pub struct CacheControl {
max_age_seconds: ArchivedU64,
max_stale_seconds: ArchivedU64,
min_fresh_seconds: ArchivedU64,
s_maxage_seconds: ArchivedU64,
flags: ArchivedU16,
}
bitflags! {
#[derive(Debug, Clone, Copy)]
struct CacheControlFlags: u16 {
const NO_CACHE = 1 << 0;
const NO_STORE = 1 << 1;
const NO_TRANSFORM = 1 << 2;
const ONLY_IF_CACHED = 1 << 3;
const MUST_REVALIDATE = 1 << 4;
const MUST_UNDERSTAND = 1 << 5;
const PRIVATE = 1 << 6;
const PROXY_REVALIDATE = 1 << 7;
const PUBLIC = 1 << 8;
const IMMUTABLE = 1 << 9;
const MAX_AGE_SECONDS = 1 << 10;
const MAX_STALE_SECONDS = 1 << 11;
const MIN_FRESH_SECONDS = 1 << 12;
const S_MAXAGE_SECONDS = 1 << 13;
}
}
impl CacheControl {
fn flags(&self) -> CacheControlFlags {
CacheControlFlags::from_bits_retain(self.flags.to_native())
}
fn insert(&mut self, flag: CacheControlFlags) {
self.flags = (self.flags() | flag).bits().into();
}
pub fn no_cache(&self) -> bool {
self.flags().contains(CacheControlFlags::NO_CACHE)
}
pub fn no_store(&self) -> bool {
self.flags().contains(CacheControlFlags::NO_STORE)
}
pub fn must_revalidate(&self) -> bool {
self.flags().contains(CacheControlFlags::MUST_REVALIDATE)
}
pub fn private(&self) -> bool {
self.flags().contains(CacheControlFlags::PRIVATE)
}
pub fn public(&self) -> bool {
self.flags().contains(CacheControlFlags::PUBLIC)
}
pub fn immutable(&self) -> bool {
self.flags().contains(CacheControlFlags::IMMUTABLE)
}
pub fn max_age_seconds(&self) -> Option<u64> {
self.flags()
.contains(CacheControlFlags::MAX_AGE_SECONDS)
.then_some(self.max_age_seconds.to_native())
}
pub fn max_stale_seconds(&self) -> Option<u64> {
self.flags()
.contains(CacheControlFlags::MAX_STALE_SECONDS)
.then_some(self.max_stale_seconds.to_native())
}
pub fn min_fresh_seconds(&self) -> Option<u64> {
self.flags()
.contains(CacheControlFlags::MIN_FRESH_SECONDS)
.then_some(self.min_fresh_seconds.to_native())
}
pub fn s_maxage_seconds(&self) -> Option<u64> {
self.flags()
.contains(CacheControlFlags::S_MAXAGE_SECONDS)
.then_some(self.s_maxage_seconds.to_native())
}
}
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.insert(CacheControlFlags::MUST_REVALIDATE),
Some(seconds) => {
cc.max_age_seconds = seconds.into();
cc.insert(CacheControlFlags::MAX_AGE_SECONDS);
}
},
"no-cache" => cc.insert(CacheControlFlags::NO_CACHE),
"no-store" => cc.insert(CacheControlFlags::NO_STORE),
"no-transform" => cc.insert(CacheControlFlags::NO_TRANSFORM),
"max-stale" => {
let seconds = if ccd.value.is_empty() {
Some(u64::MAX)
} else {
parse_int(&ccd.value)
};
match seconds {
None => cc.insert(CacheControlFlags::MUST_REVALIDATE),
Some(seconds) => {
cc.max_stale_seconds = seconds.into();
cc.insert(CacheControlFlags::MAX_STALE_SECONDS);
}
}
}
"min-fresh" => match parse_int(&ccd.value) {
None => cc.insert(CacheControlFlags::MUST_REVALIDATE),
Some(seconds) => {
cc.min_fresh_seconds = seconds.into();
cc.insert(CacheControlFlags::MIN_FRESH_SECONDS);
}
},
"only-if-cached" => cc.insert(CacheControlFlags::ONLY_IF_CACHED),
"must-revalidate" => cc.insert(CacheControlFlags::MUST_REVALIDATE),
"must-understand" => cc.insert(CacheControlFlags::MUST_UNDERSTAND),
"private" => cc.insert(CacheControlFlags::PRIVATE),
"proxy-revalidate" => cc.insert(CacheControlFlags::PROXY_REVALIDATE),
"public" => cc.insert(CacheControlFlags::PUBLIC),
"s-maxage" => match parse_int(&ccd.value) {
None => cc.insert(CacheControlFlags::MUST_REVALIDATE),
Some(seconds) => {
cc.s_maxage_seconds = seconds.into();
cc.insert(CacheControlFlags::S_MAXAGE_SECONDS);
}
},
"immutable" => cc.insert(CacheControlFlags::IMMUTABLE),
_ => {}
}
}
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 crate::rkyvutil::OwnedArchive;
use super::*;
#[test]
fn archived_cache_control_preserves_optional_values() {
let mut values = [None, Some(0), Some(42), Some(u64::MAX)];
for _ in 0..values.len() {
let directives = ["max-age", "max-stale", "min-fresh", "s-maxage"]
.into_iter()
.zip(values)
.filter_map(|(name, value)| value.map(|value| format!("{name}={value}")))
.collect::<Vec<_>>();
let control = directives.iter().collect::<CacheControl>();
let bytes =
rkyv::to_bytes::<rkyv::rancor::Error>(&control).expect("archive cache control");
let archived =
OwnedArchive::<CacheControl>::new(bytes).expect("validate cache control");
assert_eq!(OwnedArchive::deserialize(&archived), control);
assert_eq!(
[
archived.max_age_seconds(),
archived.max_stale_seconds(),
archived.min_fresh_seconds(),
archived.s_maxage_seconds(),
],
values,
);
values.rotate_left(1);
}
let control = ["max-stale"].into_iter().collect::<CacheControl>();
let archived = OwnedArchive::from_unarchived(&control).expect("archive cache control");
assert_eq!(archived.max_stale_seconds(), Some(u64::MAX));
}
#[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![]
},
]
);
}
}