rover/fetcher/
cache_control.rs1#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct CacheControl {
9 pub max_age: Option<u64>,
10 pub s_maxage: Option<u64>,
11 pub no_store: bool,
12 pub no_cache: bool,
13 pub must_revalidate: bool,
14 pub public: bool,
15 pub private: bool,
16}
17
18impl CacheControl {
19 pub fn parse(header: &str) -> Self {
23 let mut out = Self::default();
24 for token in header.split(',') {
25 let token = token.trim();
26 if token.is_empty() {
27 continue;
28 }
29 let (name, value) = match token.split_once('=') {
30 Some((n, v)) => (n.trim(), Some(strip_quotes(v.trim()))),
31 None => (token, None),
32 };
33 match name.to_ascii_lowercase().as_str() {
34 "max-age" => out.max_age = value.and_then(|v| v.parse().ok()),
35 "s-maxage" => out.s_maxage = value.and_then(|v| v.parse().ok()),
36 "no-store" => out.no_store = true,
37 "no-cache" => out.no_cache = true,
38 "must-revalidate" => out.must_revalidate = true,
39 "public" => out.public = true,
40 "private" => out.private = true,
41 _ => {} }
43 }
44 out
45 }
46}
47
48fn strip_quotes(s: &str) -> &str {
49 let s = s.trim();
50 if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
51 &s[1..s.len() - 1]
52 } else {
53 s
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn empty_is_default() {
63 assert_eq!(CacheControl::parse(""), CacheControl::default());
64 }
65
66 #[test]
67 fn parses_max_age() {
68 let cc = CacheControl::parse("max-age=3600");
69 assert_eq!(cc.max_age, Some(3600));
70 }
71
72 #[test]
73 fn parses_quoted_values() {
74 let cc = CacheControl::parse(r#"max-age="600""#);
75 assert_eq!(cc.max_age, Some(600));
76 }
77
78 #[test]
79 fn case_insensitive_directives() {
80 let cc = CacheControl::parse("MAX-AGE=42, NO-STORE");
81 assert_eq!(cc.max_age, Some(42));
82 assert!(cc.no_store);
83 }
84
85 #[test]
86 fn parses_combined_directives() {
87 let cc = CacheControl::parse("public, max-age=300, must-revalidate");
88 assert!(cc.public);
89 assert_eq!(cc.max_age, Some(300));
90 assert!(cc.must_revalidate);
91 assert!(!cc.no_store);
92 }
93
94 #[test]
95 fn s_maxage_separate_from_max_age() {
96 let cc = CacheControl::parse("max-age=60, s-maxage=600");
97 assert_eq!(cc.max_age, Some(60));
98 assert_eq!(cc.s_maxage, Some(600));
99 }
100
101 #[test]
102 fn ignores_unknown_directives() {
103 let cc = CacheControl::parse("immutable, max-age=100, stale-while-revalidate=30");
104 assert_eq!(cc.max_age, Some(100));
105 assert!(!cc.no_store);
106 }
107
108 #[test]
109 fn no_store_no_cache_are_independent() {
110 let cc = CacheControl::parse("no-store");
111 assert!(cc.no_store && !cc.no_cache);
112 let cc = CacheControl::parse("no-cache");
113 assert!(!cc.no_store && cc.no_cache);
114 }
115
116 #[test]
117 fn malformed_max_age_yields_none() {
118 let cc = CacheControl::parse("max-age=not-a-number");
119 assert_eq!(cc.max_age, None);
120 }
121}