use crate::date::HttpDate;
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheError {
Empty,
InvalidFormat,
InvalidNumber,
InvalidDate,
}
impl fmt::Display for CacheError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CacheError::Empty => write!(f, "empty cache header"),
CacheError::InvalidFormat => write!(f, "invalid cache header format"),
CacheError::InvalidNumber => write!(f, "invalid number in cache header"),
CacheError::InvalidDate => write!(f, "invalid date in cache header"),
}
}
}
impl std::error::Error for CacheError {}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CacheControl {
max_age: Option<u64>,
s_maxage: Option<u64>,
max_stale: Option<u64>,
min_fresh: Option<u64>,
stale_while_revalidate: Option<u64>,
stale_if_error: Option<u64>,
no_cache: bool,
no_store: bool,
no_transform: bool,
only_if_cached: bool,
must_revalidate: bool,
proxy_revalidate: bool,
must_understand: bool,
public: bool,
private: bool,
immutable: bool,
}
impl CacheControl {
pub fn new() -> Self {
Self::default()
}
pub fn parse(input: &str) -> Result<Self, CacheError> {
let input = input.trim();
if input.is_empty() {
return Ok(CacheControl::new());
}
let mut cc = CacheControl::new();
for directive in input.split(',') {
let directive = directive.trim();
if directive.is_empty() {
continue;
}
if let Some((name, value)) = directive.split_once('=') {
let name = name.trim().to_lowercase();
let value = value.trim().trim_matches('"');
match name.as_str() {
"max-age" => {
cc.max_age = Some(parse_seconds(value)?);
}
"s-maxage" => {
cc.s_maxage = Some(parse_seconds(value)?);
}
"max-stale" => {
cc.max_stale = Some(parse_seconds(value)?);
}
"min-fresh" => {
cc.min_fresh = Some(parse_seconds(value)?);
}
"stale-while-revalidate" => {
cc.stale_while_revalidate = Some(parse_seconds(value)?);
}
"stale-if-error" => {
cc.stale_if_error = Some(parse_seconds(value)?);
}
"no-cache" => cc.no_cache = true,
"private" => cc.private = true,
_ => {} }
} else {
let name = directive.to_lowercase();
match name.as_str() {
"no-cache" => cc.no_cache = true,
"no-store" => cc.no_store = true,
"no-transform" => cc.no_transform = true,
"only-if-cached" => cc.only_if_cached = true,
"must-revalidate" => cc.must_revalidate = true,
"proxy-revalidate" => cc.proxy_revalidate = true,
"must-understand" => cc.must_understand = true,
"public" => cc.public = true,
"private" => cc.private = true,
"immutable" => cc.immutable = true,
"max-stale" => cc.max_stale = Some(u64::MAX), _ => {} }
}
}
Ok(cc)
}
pub fn with_max_age(mut self, seconds: u64) -> Self {
self.max_age = Some(seconds);
self
}
pub fn with_s_maxage(mut self, seconds: u64) -> Self {
self.s_maxage = Some(seconds);
self
}
pub fn with_no_cache(mut self) -> Self {
self.no_cache = true;
self
}
pub fn with_no_store(mut self) -> Self {
self.no_store = true;
self
}
pub fn with_no_transform(mut self) -> Self {
self.no_transform = true;
self
}
pub fn with_must_revalidate(mut self) -> Self {
self.must_revalidate = true;
self
}
pub fn with_proxy_revalidate(mut self) -> Self {
self.proxy_revalidate = true;
self
}
pub fn with_public(mut self) -> Self {
self.public = true;
self
}
pub fn with_private(mut self) -> Self {
self.private = true;
self
}
pub fn with_immutable(mut self) -> Self {
self.immutable = true;
self
}
pub fn max_age(&self) -> Option<u64> {
self.max_age
}
pub fn s_maxage(&self) -> Option<u64> {
self.s_maxage
}
pub fn max_stale(&self) -> Option<u64> {
self.max_stale
}
pub fn min_fresh(&self) -> Option<u64> {
self.min_fresh
}
pub fn stale_while_revalidate(&self) -> Option<u64> {
self.stale_while_revalidate
}
pub fn stale_if_error(&self) -> Option<u64> {
self.stale_if_error
}
pub fn is_no_cache(&self) -> bool {
self.no_cache
}
pub fn is_no_store(&self) -> bool {
self.no_store
}
pub fn is_no_transform(&self) -> bool {
self.no_transform
}
pub fn is_only_if_cached(&self) -> bool {
self.only_if_cached
}
pub fn is_must_revalidate(&self) -> bool {
self.must_revalidate
}
pub fn is_proxy_revalidate(&self) -> bool {
self.proxy_revalidate
}
pub fn is_must_understand(&self) -> bool {
self.must_understand
}
pub fn is_public(&self) -> bool {
self.public
}
pub fn is_private(&self) -> bool {
self.private
}
pub fn is_immutable(&self) -> bool {
self.immutable
}
pub fn is_cacheable(&self) -> bool {
!self.no_store && (self.public || self.max_age.is_some() || self.s_maxage.is_some())
}
pub fn to_header_value(&self) -> String {
self.to_string()
}
}
impl fmt::Display for CacheControl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
if let Some(max_age) = self.max_age {
parts.push(format!("max-age={}", max_age));
}
if let Some(s_maxage) = self.s_maxage {
parts.push(format!("s-maxage={}", s_maxage));
}
if let Some(max_stale) = self.max_stale {
if max_stale == u64::MAX {
parts.push("max-stale".to_string());
} else {
parts.push(format!("max-stale={}", max_stale));
}
}
if let Some(min_fresh) = self.min_fresh {
parts.push(format!("min-fresh={}", min_fresh));
}
if let Some(swr) = self.stale_while_revalidate {
parts.push(format!("stale-while-revalidate={}", swr));
}
if let Some(sie) = self.stale_if_error {
parts.push(format!("stale-if-error={}", sie));
}
if self.no_cache {
parts.push("no-cache".to_string());
}
if self.no_store {
parts.push("no-store".to_string());
}
if self.no_transform {
parts.push("no-transform".to_string());
}
if self.only_if_cached {
parts.push("only-if-cached".to_string());
}
if self.must_revalidate {
parts.push("must-revalidate".to_string());
}
if self.proxy_revalidate {
parts.push("proxy-revalidate".to_string());
}
if self.must_understand {
parts.push("must-understand".to_string());
}
if self.public {
parts.push("public".to_string());
}
if self.private {
parts.push("private".to_string());
}
if self.immutable {
parts.push("immutable".to_string());
}
write!(f, "{}", parts.join(", "))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Age {
seconds: u64,
}
impl Age {
pub fn new(seconds: u64) -> Self {
Age { seconds }
}
pub fn parse(input: &str) -> Result<Self, CacheError> {
let input = input.trim();
if input.is_empty() {
return Err(CacheError::Empty);
}
let seconds = parse_seconds(input)?;
Ok(Age { seconds })
}
pub fn seconds(&self) -> u64 {
self.seconds
}
pub fn to_header_value(&self) -> String {
self.to_string()
}
}
impl fmt::Display for Age {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.seconds)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Expires {
date: HttpDate,
}
impl Expires {
pub fn new(date: HttpDate) -> Self {
Expires { date }
}
pub fn parse(input: &str) -> Result<Self, CacheError> {
let date = HttpDate::parse(input).map_err(|_| CacheError::InvalidDate)?;
Ok(Expires { date })
}
pub fn date(&self) -> &HttpDate {
&self.date
}
pub fn to_header_value(&self) -> String {
self.to_string()
}
}
impl fmt::Display for Expires {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.date)
}
}
const DELTA_SECONDS_OVERFLOW: u64 = 2_147_483_648;
fn parse_seconds(s: &str) -> Result<u64, CacheError> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(CacheError::InvalidNumber);
}
Ok(s.parse::<u64>().unwrap_or(DELTA_SECONDS_OVERFLOW))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_control_parse_max_age() {
let cc = CacheControl::parse("max-age=3600").unwrap();
assert_eq!(cc.max_age(), Some(3600));
}
#[test]
fn test_cache_control_parse_multiple() {
let cc = CacheControl::parse("max-age=3600, public, no-transform").unwrap();
assert_eq!(cc.max_age(), Some(3600));
assert!(cc.is_public());
assert!(cc.is_no_transform());
}
#[test]
fn test_cache_control_parse_no_store() {
let cc = CacheControl::parse("no-store").unwrap();
assert!(cc.is_no_store());
assert!(!cc.is_cacheable());
}
#[test]
fn test_cache_control_parse_private() {
let cc = CacheControl::parse("private, max-age=600").unwrap();
assert!(cc.is_private());
assert_eq!(cc.max_age(), Some(600));
}
#[test]
fn test_cache_control_parse_no_cache_qualified() {
let cc = CacheControl::parse("no-cache=\"Set-Cookie\"").unwrap();
assert!(cc.is_no_cache());
}
#[test]
fn test_cache_control_parse_private_qualified() {
let cc = CacheControl::parse("private=\"Content-Type\"").unwrap();
assert!(cc.is_private());
}
#[test]
fn test_cache_control_parse_s_maxage() {
let cc = CacheControl::parse("public, s-maxage=86400").unwrap();
assert!(cc.is_public());
assert_eq!(cc.s_maxage(), Some(86400));
}
#[test]
fn test_cache_control_parse_must_revalidate() {
let cc = CacheControl::parse("max-age=0, must-revalidate").unwrap();
assert_eq!(cc.max_age(), Some(0));
assert!(cc.is_must_revalidate());
}
#[test]
fn test_cache_control_parse_immutable() {
let cc = CacheControl::parse("max-age=31536000, immutable").unwrap();
assert_eq!(cc.max_age(), Some(31536000));
assert!(cc.is_immutable());
}
#[test]
fn test_cache_control_parse_empty() {
let cc = CacheControl::parse("").unwrap();
assert_eq!(cc, CacheControl::default());
assert_eq!(cc.max_age(), None);
assert!(!cc.is_public());
}
#[test]
fn test_cache_control_builder() {
let cc = CacheControl::new()
.with_max_age(3600)
.with_public()
.with_no_transform();
assert_eq!(cc.max_age(), Some(3600));
assert!(cc.is_public());
assert!(cc.is_no_transform());
}
#[test]
fn test_cache_control_display() {
let cc = CacheControl::new().with_max_age(3600).with_public();
let s = cc.to_string();
assert!(s.contains("max-age=3600"));
assert!(s.contains("public"));
}
#[test]
fn test_cache_control_roundtrip() {
let original = CacheControl::new()
.with_max_age(3600)
.with_public()
.with_must_revalidate();
let header = original.to_string();
let reparsed = CacheControl::parse(&header).unwrap();
assert_eq!(original.max_age(), reparsed.max_age());
assert_eq!(original.is_public(), reparsed.is_public());
assert_eq!(original.is_must_revalidate(), reparsed.is_must_revalidate());
}
#[test]
fn test_cache_control_is_cacheable() {
let cc1 = CacheControl::new().with_max_age(3600);
assert!(cc1.is_cacheable());
let cc2 = CacheControl::new().with_no_store();
assert!(!cc2.is_cacheable());
let cc3 = CacheControl::new().with_public();
assert!(cc3.is_cacheable());
}
#[test]
fn test_age_parse() {
let age = Age::parse("120").unwrap();
assert_eq!(age.seconds(), 120);
}
#[test]
fn test_age_parse_zero() {
let age = Age::parse("0").unwrap();
assert_eq!(age.seconds(), 0);
}
#[test]
fn test_age_parse_empty() {
assert!(Age::parse("").is_err());
}
#[test]
fn test_age_parse_invalid() {
assert!(Age::parse("abc").is_err());
assert!(Age::parse("-1").is_err());
assert!(Age::parse("+1").is_err());
}
#[test]
fn test_age_parse_overflow_clamp() {
let age = Age::parse("99999999999999999999999").unwrap();
assert_eq!(age.seconds(), 2_147_483_648);
}
#[test]
fn test_cache_control_delta_seconds_overflow() {
let cc = CacheControl::parse("max-age=99999999999999999999999").unwrap();
assert_eq!(cc.max_age(), Some(2_147_483_648));
}
#[test]
fn test_cache_control_delta_seconds_plus_sign() {
assert!(CacheControl::parse("max-age=+3600").is_err());
}
#[test]
fn test_age_display() {
let age = Age::new(120);
assert_eq!(age.to_string(), "120");
}
#[test]
fn test_age_roundtrip() {
let original = Age::new(3600);
let header = original.to_string();
let reparsed = Age::parse(&header).unwrap();
assert_eq!(original.seconds(), reparsed.seconds());
}
#[test]
fn test_expires_parse() {
let expires = Expires::parse("Sun, 06 Nov 1994 08:49:37 GMT").unwrap();
assert_eq!(expires.date().year(), 1994);
assert_eq!(expires.date().month(), 11);
assert_eq!(expires.date().day(), 6);
}
#[test]
fn test_expires_parse_invalid() {
assert!(Expires::parse("invalid date").is_err());
assert!(Expires::parse("").is_err());
}
#[test]
fn test_expires_display() {
let expires = Expires::parse("Sun, 06 Nov 1994 08:49:37 GMT").unwrap();
let s = expires.to_string();
assert!(s.contains("1994"));
assert!(s.contains("Nov"));
}
#[test]
fn test_expires_roundtrip() {
let original = Expires::parse("Sun, 06 Nov 1994 08:49:37 GMT").unwrap();
let header = original.to_string();
let reparsed = Expires::parse(&header).unwrap();
assert_eq!(original.date().year(), reparsed.date().year());
assert_eq!(original.date().month(), reparsed.date().month());
assert_eq!(original.date().day(), reparsed.date().day());
}
}