1use std::fmt;
2use std::iter::FromIterator;
3use std::str::FromStr;
4use std::time::Duration;
5
6use rama_core::error::{BoxError, ErrorContext as _};
7use rama_http_types::{HeaderName, HeaderValue};
8
9use crate::util::{self, Seconds, csv};
10use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
11
12#[derive(PartialEq, Clone, Debug)]
41pub struct CacheControl {
42 flags: Flags,
43 max_age: Option<Seconds>,
44 max_stale: Option<Seconds>,
45 min_fresh: Option<Seconds>,
46 s_max_age: Option<Seconds>,
47}
48
49#[derive(Debug, Clone, PartialEq)]
50struct Flags {
51 bits: u64,
52}
53
54impl Flags {
55 const NO_CACHE: Self = Self { bits: 0b000000001 };
56 const NO_STORE: Self = Self { bits: 0b000000010 };
57 const NO_TRANSFORM: Self = Self { bits: 0b000000100 };
58 const ONLY_IF_CACHED: Self = Self { bits: 0b000001000 };
59 const MUST_REVALIDATE: Self = Self { bits: 0b000010000 };
60 const PUBLIC: Self = Self { bits: 0b000100000 };
61 const PRIVATE: Self = Self { bits: 0b001000000 };
62 const PROXY_REVALIDATE: Self = Self { bits: 0b010000000 };
63 const IMMUTABLE: Self = Self { bits: 0b100000000 };
64 const MUST_UNDERSTAND: Self = Self { bits: 0b1000000000 };
65
66 fn empty() -> Self {
67 Self { bits: 0 }
68 }
69
70 #[expect(clippy::needless_pass_by_value)]
71 fn contains(&self, flag: Self) -> bool {
72 (self.bits & flag.bits) != 0
73 }
74
75 #[expect(clippy::needless_pass_by_value)]
76 fn insert(&mut self, flag: Self) {
77 self.bits |= flag.bits;
78 }
79}
80
81impl Default for CacheControl {
82 #[inline]
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl CacheControl {
89 #[must_use]
91 pub fn new() -> Self {
92 Self {
93 flags: Flags::empty(),
94 max_age: None,
95 max_stale: None,
96 min_fresh: None,
97 s_max_age: None,
98 }
99 }
100
101 #[must_use]
112 pub fn immutable_one_year() -> Self {
113 Self::new()
114 .with_public()
115 .with_immutable()
116 .with_max_age_seconds(31_536_000)
117 }
118
119 #[must_use]
125 pub fn no_cache() -> Self {
126 Self::new().with_no_cache()
127 }
128
129 #[must_use]
136 pub fn short_shared_revalidate(max_age_seconds: u32) -> Self {
137 Self::new()
138 .with_public()
139 .with_max_age_seconds(u64::from(max_age_seconds))
140 .with_must_revalidate()
141 }
142
143 #[must_use]
147 pub fn has_no_cache(self) -> bool {
148 self.flags.contains(Flags::NO_CACHE)
149 }
150
151 #[must_use]
153 pub fn has_no_store(self) -> bool {
154 self.flags.contains(Flags::NO_STORE)
155 }
156
157 #[must_use]
159 pub fn has_no_transform(self) -> bool {
160 self.flags.contains(Flags::NO_TRANSFORM)
161 }
162
163 #[must_use]
165 pub fn has_only_if_cached(self) -> bool {
166 self.flags.contains(Flags::ONLY_IF_CACHED)
167 }
168
169 #[must_use]
171 pub fn has_public(self) -> bool {
172 self.flags.contains(Flags::PUBLIC)
173 }
174
175 #[must_use]
177 pub fn has_private(self) -> bool {
178 self.flags.contains(Flags::PRIVATE)
179 }
180
181 #[must_use]
183 pub fn has_immutable(self) -> bool {
184 self.flags.contains(Flags::IMMUTABLE)
185 }
186
187 #[must_use]
189 pub fn has_must_revalidate(&self) -> bool {
190 self.flags.contains(Flags::MUST_REVALIDATE)
191 }
192
193 #[must_use]
195 pub fn has_must_understand(self) -> bool {
196 self.flags.contains(Flags::MUST_UNDERSTAND)
197 }
198
199 pub fn max_age(&self) -> Option<Duration> {
201 self.max_age.map(Into::into)
202 }
203
204 pub fn max_stale(&self) -> Option<Duration> {
206 self.max_stale.map(Into::into)
207 }
208
209 pub fn min_fresh(&self) -> Option<Duration> {
211 self.min_fresh.map(Into::into)
212 }
213
214 pub fn s_max_age(&self) -> Option<Duration> {
216 self.s_max_age.map(Into::into)
217 }
218
219 rama_utils::macros::generate_set_and_with! {
222 pub fn no_cache(mut self) -> Self {
224 self.flags.insert(Flags::NO_CACHE);
225 self
226 }
227 }
228
229 rama_utils::macros::generate_set_and_with! {
230 pub fn no_store(mut self) -> Self {
232 self.flags.insert(Flags::NO_STORE);
233 self
234 }
235 }
236
237 rama_utils::macros::generate_set_and_with! {
238 pub fn no_transform(mut self) -> Self {
240 self.flags.insert(Flags::NO_TRANSFORM);
241 self
242 }
243 }
244
245 rama_utils::macros::generate_set_and_with! {
246 pub fn only_if_cached(mut self) -> Self {
248 self.flags.insert(Flags::ONLY_IF_CACHED);
249 self
250 }
251 }
252
253 rama_utils::macros::generate_set_and_with! {
254 pub fn private(mut self) -> Self {
256 self.flags.insert(Flags::PRIVATE);
257 self
258 }
259 }
260
261 rama_utils::macros::generate_set_and_with! {
262 pub fn public(mut self) -> Self {
264 self.flags.insert(Flags::PUBLIC);
265 self
266 }
267 }
268
269 rama_utils::macros::generate_set_and_with! {
270 pub fn immutable(mut self) -> Self {
272 self.flags.insert(Flags::IMMUTABLE);
273 self
274 }
275 }
276
277 rama_utils::macros::generate_set_and_with! {
278 pub fn must_revalidate(mut self) -> Self {
280 self.flags.insert(Flags::MUST_REVALIDATE);
281 self
282 }
283 }
284
285 rama_utils::macros::generate_set_and_with! {
286 pub fn must_understand(mut self) -> Self {
288 self.flags.insert(Flags::MUST_UNDERSTAND);
289 self
290 }
291 }
292
293 rama_utils::macros::generate_set_and_with! {
294 pub fn max_age_duration_rounded(mut self, dur: Duration) -> Self {
296 self.max_age = Some(Seconds::from_duration_rounded(dur));
297 self
298 }
299 }
300
301 rama_utils::macros::generate_set_and_with! {
302 pub fn max_age_seconds(mut self, seconds: u64) -> Self {
304 self.max_age = Some(Seconds::new(seconds));
305 self
306 }
307 }
308
309 rama_utils::macros::generate_set_and_with! {
310 pub fn max_age_duration(mut self, dur: Duration) -> Result<Self, BoxError> {
312 self.max_age = Some(Seconds::try_from_duration(dur).context("duration contains sub nano seconds")?);
313 Ok(self)
314 }
315 }
316
317 rama_utils::macros::generate_set_and_with! {
318 pub fn max_stale_duration_rounded(mut self, dur: Duration) -> Self {
320 self.max_stale = Some(Seconds::from_duration_rounded(dur));
321 self
322 }
323 }
324
325 rama_utils::macros::generate_set_and_with! {
326 pub fn max_stale_seconds(mut self, seconds: u64) -> Self {
328 self.max_stale = Some(Seconds::new(seconds));
329 self
330 }
331 }
332
333 rama_utils::macros::generate_set_and_with! {
334 pub fn max_stale_duration(mut self, dur: Duration) -> Result<Self, BoxError> {
336 self.max_stale = Some(Seconds::try_from_duration(dur).context("duration contains sub nano seconds")?);
337 Ok(self)
338 }
339 }
340
341 rama_utils::macros::generate_set_and_with! {
342 pub fn min_fresh_duration_rounded(mut self, dur: Duration) -> Self {
344 self.min_fresh = Some(Seconds::from_duration_rounded(dur));
345 self
346 }
347 }
348
349 rama_utils::macros::generate_set_and_with! {
350 pub fn min_fresh_seconds(mut self, seconds: u64) -> Self {
352 self.min_fresh = Some(Seconds::new(seconds));
353 self
354 }
355 }
356
357 rama_utils::macros::generate_set_and_with! {
358 pub fn min_fresh_duration(mut self, dur: Duration) -> Result<Self, BoxError> {
360 self.min_fresh = Some(Seconds::try_from_duration(dur).context("duration contains sub nano seconds")?);
361 Ok(self)
362 }
363 }
364
365 rama_utils::macros::generate_set_and_with! {
366 pub fn s_max_age_duration_rounded(mut self, dur: Duration) -> Self {
368 self.s_max_age = Some(Seconds::from_duration_rounded(dur));
369 self
370 }
371 }
372
373 rama_utils::macros::generate_set_and_with! {
374 pub fn s_max_age_seconds(mut self, seconds: u64) -> Self {
376 self.s_max_age = Some(Seconds::new(seconds));
377 self
378 }
379 }
380
381 rama_utils::macros::generate_set_and_with! {
382 pub fn s_max_age_duration(mut self, dur: Duration) -> Result<Self, BoxError> {
384 self.s_max_age = Some(Seconds::try_from_duration(dur).context("duration contains sub nano seconds")?);
385 Ok(self)
386 }
387 }
388}
389
390impl TypedHeader for CacheControl {
391 fn name() -> &'static HeaderName {
392 &::rama_http_types::header::CACHE_CONTROL
393 }
394}
395
396impl HeaderDecode for CacheControl {
397 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
398 csv::from_comma_delimited(values).map(|FromIter(cc)| cc)
399 }
400}
401
402impl HeaderEncode for CacheControl {
403 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
404 values.extend(::std::iter::once(util::fmt(Fmt(self))));
405 }
406}
407
408struct FromIter(CacheControl);
410
411impl FromIterator<KnownDirective> for FromIter {
412 fn from_iter<I>(iter: I) -> Self
413 where
414 I: IntoIterator<Item = KnownDirective>,
415 {
416 let mut cc = CacheControl::new();
417
418 let iter = iter.into_iter().filter_map(|dir| match dir {
420 KnownDirective::Known(dir) => Some(dir),
421 KnownDirective::Unknown => None,
422 });
423
424 for directive in iter {
425 match directive {
426 Directive::NoCache => {
427 cc.flags.insert(Flags::NO_CACHE);
428 }
429 Directive::NoStore => {
430 cc.flags.insert(Flags::NO_STORE);
431 }
432 Directive::NoTransform => {
433 cc.flags.insert(Flags::NO_TRANSFORM);
434 }
435 Directive::OnlyIfCached => {
436 cc.flags.insert(Flags::ONLY_IF_CACHED);
437 }
438 Directive::MustRevalidate => {
439 cc.flags.insert(Flags::MUST_REVALIDATE);
440 }
441 Directive::MustUnderstand => {
442 cc.flags.insert(Flags::MUST_UNDERSTAND);
443 }
444 Directive::Public => {
445 cc.flags.insert(Flags::PUBLIC);
446 }
447 Directive::Private => {
448 cc.flags.insert(Flags::PRIVATE);
449 }
450 Directive::Immutable => {
451 cc.flags.insert(Flags::IMMUTABLE);
452 }
453 Directive::ProxyRevalidate => {
454 cc.flags.insert(Flags::PROXY_REVALIDATE);
455 }
456 Directive::MaxAge(secs) => {
457 cc.max_age = Some(Seconds::new(secs));
458 }
459 Directive::MaxStale(secs) => {
460 cc.max_stale = Some(Seconds::new(secs));
461 }
462 Directive::MinFresh(secs) => {
463 cc.min_fresh = Some(Seconds::new(secs));
464 }
465 Directive::SMaxAge(secs) => {
466 cc.s_max_age = Some(Seconds::new(secs));
467 }
468 }
469 }
470
471 Self(cc)
472 }
473}
474
475struct Fmt<'a>(&'a CacheControl);
476
477impl fmt::Display for Fmt<'_> {
478 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
479 let if_flag = |f: Flags, dir: Directive| {
480 if self.0.flags.contains(f) {
481 Some(dir)
482 } else {
483 None
484 }
485 };
486
487 let slice = &[
488 if_flag(Flags::NO_CACHE, Directive::NoCache),
489 if_flag(Flags::NO_STORE, Directive::NoStore),
490 if_flag(Flags::NO_TRANSFORM, Directive::NoTransform),
491 if_flag(Flags::ONLY_IF_CACHED, Directive::OnlyIfCached),
492 if_flag(Flags::MUST_REVALIDATE, Directive::MustRevalidate),
493 if_flag(Flags::PUBLIC, Directive::Public),
494 if_flag(Flags::PRIVATE, Directive::Private),
495 if_flag(Flags::IMMUTABLE, Directive::Immutable),
496 if_flag(Flags::MUST_UNDERSTAND, Directive::MustUnderstand),
497 if_flag(Flags::PROXY_REVALIDATE, Directive::ProxyRevalidate),
498 self.0
499 .max_age
500 .as_ref()
501 .map(|s| Directive::MaxAge(s.as_u64())),
502 self.0
503 .max_stale
504 .as_ref()
505 .map(|s| Directive::MaxStale(s.as_u64())),
506 self.0
507 .min_fresh
508 .as_ref()
509 .map(|s| Directive::MinFresh(s.as_u64())),
510 self.0
511 .s_max_age
512 .as_ref()
513 .map(|s| Directive::SMaxAge(s.as_u64())),
514 ];
515
516 let iter = slice.iter().filter_map(|o| *o);
517
518 csv::fmt_comma_delimited(f, iter)
519 }
520}
521
522#[derive(Clone, Copy)]
523enum KnownDirective {
524 Known(Directive),
525 Unknown,
526}
527
528#[derive(Clone, Copy)]
529enum Directive {
530 NoCache,
531 NoStore,
532 NoTransform,
533 OnlyIfCached,
534
535 MaxAge(u64),
537 MaxStale(u64),
538 MinFresh(u64),
539
540 MustRevalidate,
542 MustUnderstand,
543 Public,
544 Private,
545 Immutable,
546 ProxyRevalidate,
547 SMaxAge(u64),
548}
549
550impl fmt::Display for Directive {
551 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
552 fmt::Display::fmt(
553 match *self {
554 Self::NoCache => "no-cache",
555 Self::NoStore => "no-store",
556 Self::NoTransform => "no-transform",
557 Self::OnlyIfCached => "only-if-cached",
558
559 Self::MaxAge(secs) => return write!(f, "max-age={secs}"),
560 Self::MaxStale(secs) => return write!(f, "max-stale={secs}"),
561 Self::MinFresh(secs) => return write!(f, "min-fresh={secs}"),
562
563 Self::MustRevalidate => "must-revalidate",
564 Self::MustUnderstand => "must-understand",
565 Self::Public => "public",
566 Self::Private => "private",
567 Self::Immutable => "immutable",
568 Self::ProxyRevalidate => "proxy-revalidate",
569 Self::SMaxAge(secs) => return write!(f, "s-maxage={secs}"),
570 },
571 f,
572 )
573 }
574}
575
576impl FromStr for KnownDirective {
577 type Err = ();
578 fn from_str(s: &str) -> Result<Self, Self::Err> {
579 Ok(Self::Known(match s {
580 "no-cache" => Directive::NoCache,
581 "no-store" => Directive::NoStore,
582 "no-transform" => Directive::NoTransform,
583 "only-if-cached" => Directive::OnlyIfCached,
584 "must-revalidate" => Directive::MustRevalidate,
585 "public" => Directive::Public,
586 "private" => Directive::Private,
587 "immutable" => Directive::Immutable,
588 "must-understand" => Directive::MustUnderstand,
589 "proxy-revalidate" => Directive::ProxyRevalidate,
590 "" => return Err(()),
591 _ => match s.find('=') {
592 Some(idx) if idx + 1 < s.len() => {
593 match (&s[..idx], (s[idx + 1..]).trim_matches('"')) {
594 ("max-age", secs) => {
595 secs.parse().map(Directive::MaxAge).map_err(|_e| ())?
596 }
597 ("max-stale", secs) => {
598 secs.parse().map(Directive::MaxStale).map_err(|_e| ())?
599 }
600 ("min-fresh", secs) => {
601 secs.parse().map(Directive::MinFresh).map_err(|_e| ())?
602 }
603 ("s-maxage", secs) => {
604 secs.parse().map(Directive::SMaxAge).map_err(|_e| ())?
605 }
606 _unknown => return Ok(Self::Unknown),
607 }
608 }
609 Some(_) | None => return Ok(Self::Unknown),
610 },
611 }))
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::super::{test_decode, test_encode};
618 use super::*;
619
620 #[test]
621 fn test_parse_multiple_headers() {
622 assert_eq!(
623 test_decode::<CacheControl>(&["no-cache", "private"]).unwrap(),
624 CacheControl::new().with_no_cache().with_private(),
625 );
626 }
627
628 #[test]
629 fn test_parse_argument() {
630 assert_eq!(
631 test_decode::<CacheControl>(&["max-age=100, private"]).unwrap(),
632 CacheControl::new().with_max_age_seconds(100).with_private(),
633 );
634 }
635
636 #[test]
637 fn test_parse_quote_form() {
638 assert_eq!(
639 test_decode::<CacheControl>(&["max-age=\"200\""]).unwrap(),
640 CacheControl::new().with_max_age_seconds(200),
641 );
642 }
643
644 #[test]
645 fn test_parse_quoted_comma() {
646 assert_eq!(
647 test_decode::<CacheControl>(&["foo=\"a, private, immutable, b\", no-cache"]).unwrap(),
648 CacheControl::new().with_no_cache(),
649 "unknown extensions are ignored but shouldn't fail parsing",
650 )
651 }
652
653 #[test]
654 fn test_parse_extension() {
655 assert_eq!(
656 test_decode::<CacheControl>(&["foo, no-cache, bar=baz"]).unwrap(),
657 CacheControl::new().with_no_cache(),
658 "unknown extensions are ignored but shouldn't fail parsing",
659 );
660 }
661
662 #[test]
663 fn test_immutable() {
664 let cc = CacheControl::new().with_immutable();
665 let headers = test_encode(cc.clone());
666 assert_eq!(headers["cache-control"], "immutable");
667 assert_eq!(test_decode::<CacheControl>(&["immutable"]).unwrap(), cc);
668 assert!(cc.has_immutable());
669 }
670
671 #[test]
672 fn test_must_revalidate() {
673 let cc = CacheControl::new().with_must_revalidate();
674 let headers = test_encode(cc.clone());
675 assert_eq!(headers["cache-control"], "must-revalidate");
676 assert_eq!(
677 test_decode::<CacheControl>(&["must-revalidate"]).unwrap(),
678 cc
679 );
680 assert!(cc.has_must_revalidate());
681 }
682
683 #[test]
684 fn test_must_understand() {
685 let cc = CacheControl::new().with_must_understand();
686 let headers = test_encode(cc.clone());
687 assert_eq!(headers["cache-control"], "must-understand");
688 assert_eq!(
689 test_decode::<CacheControl>(&["must-understand"]).unwrap(),
690 cc
691 );
692 assert!(cc.has_must_understand());
693 }
694
695 #[test]
696 fn test_parse_bad_syntax() {
697 assert_eq!(test_decode::<CacheControl>(&["max-age=lolz"]), None);
698 }
699
700 #[test]
701 fn encode_one_flag_directive() {
702 let cc = CacheControl::new().with_no_cache();
703
704 let headers = test_encode(cc);
705 assert_eq!(headers["cache-control"], "no-cache");
706 }
707
708 #[test]
709 fn encode_one_param_directive() {
710 let cc = CacheControl::new().with_max_age_seconds(300);
711
712 let headers = test_encode(cc);
713 assert_eq!(headers["cache-control"], "max-age=300");
714 }
715
716 #[test]
717 fn encode_two_directive() {
718 let headers = test_encode(CacheControl::new().with_no_cache().with_private());
719 assert_eq!(headers["cache-control"], "no-cache, private");
720
721 let headers = test_encode(
722 CacheControl::new()
723 .with_no_cache()
724 .with_max_age_seconds(100),
725 );
726 assert_eq!(headers["cache-control"], "no-cache, max-age=100");
727 }
728
729 #[test]
730 fn preset_immutable_one_year() {
731 let headers = test_encode(CacheControl::immutable_one_year());
732 assert_eq!(
733 headers["cache-control"],
734 "public, immutable, max-age=31536000"
735 );
736 }
737
738 #[test]
739 fn preset_no_cache() {
740 let headers = test_encode(CacheControl::no_cache());
741 assert_eq!(headers["cache-control"], "no-cache");
742 }
743
744 #[test]
745 fn preset_short_shared_revalidate() {
746 let headers = test_encode(CacheControl::short_shared_revalidate(3600));
747 assert_eq!(
748 headers["cache-control"],
749 "must-revalidate, public, max-age=3600"
750 );
751 }
752}