use std::fmt; use std::iter::FromIterator; use std::str::FromStr; use std::time::Duration; use util::{self, csv, Seconds}; use HeaderValue; /// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) /// /// The `Cache-Control` header field is used to specify directives for /// caches along the request/response chain. Such cache directives are /// unidirectional in that the presence of a directive in a request does /// not imply that the same directive is to be given in the response. /// /// ## ABNF /// /// ```text /// Cache-Control = 1#cache-directive /// cache-directive = token [ "=" ( token / quoted-string ) ] /// ``` /// /// ## Example values /// /// * `no-cache` /// * `private, community="UCI"` /// * `max-age=30` /// /// # Example /// /// ``` /// # extern crate headers; /// use headers::CacheControl; /// /// let cc = CacheControl::new(); /// ``` #[derive(PartialEq, Clone, Debug)] pub struct CacheControl { flags: Flags, max_age: Option, max_stale: Option, min_fresh: Option, s_max_age: Option, } bitflags! { struct Flags: u32 { const NO_CACHE = 0b00000001; const NO_STORE = 0b00000010; const NO_TRANSFORM = 0b00000100; const ONLY_IF_CACHED = 0b00001000; const MUST_REVALIDATE = 0b00010000; const PUBLIC = 0b00100000; const PRIVATE = 0b01000000; const PROXY_REVALIDATE = 0b10000000; } } impl CacheControl { /// Construct a new empty `CacheControl` header. pub fn new() -> Self { CacheControl { flags: Flags::empty(), max_age: None, max_stale: None, min_fresh: None, s_max_age: None, } } // getters /// Check if the `no-cache` directive is set. pub fn no_cache(&self) -> bool { self.flags.contains(Flags::NO_CACHE) } /// Check if the `no-store` directive is set. pub fn no_store(&self) -> bool { self.flags.contains(Flags::NO_STORE) } /// Check if the `no-transform` directive is set. pub fn no_transform(&self) -> bool { self.flags.contains(Flags::NO_TRANSFORM) } /// Check if the `only-if-cached` directive is set. pub fn only_if_cached(&self) -> bool { self.flags.contains(Flags::ONLY_IF_CACHED) } /// Check if the `public` directive is set. pub fn public(&self) -> bool { self.flags.contains(Flags::PUBLIC) } /// Check if the `private` directive is set. pub fn private(&self) -> bool { self.flags.contains(Flags::PRIVATE) } /// Get the value of the `max-age` directive if set. pub fn max_age(&self) -> Option { self.max_age.map(Into::into) } /// Get the value of the `max-stale` directive if set. pub fn max_stale(&self) -> Option { self.max_stale.map(Into::into) } /// Get the value of the `min-fresh` directive if set. pub fn min_fresh(&self) -> Option { self.min_fresh.map(Into::into) } /// Get the value of the `s-maxage` directive if set. pub fn s_max_age(&self) -> Option { self.s_max_age.map(Into::into) } // setters /// Set the `no-cache` directive. pub fn with_no_cache(mut self) -> Self { self.flags.insert(Flags::NO_CACHE); self } /// Set the `no-store` directive. pub fn with_no_store(mut self) -> Self { self.flags.insert(Flags::NO_STORE); self } /// Set the `no-transform` directive. pub fn with_no_transform(mut self) -> Self { self.flags.insert(Flags::NO_TRANSFORM); self } /// Set the `only-if-cached` directive. pub fn with_only_if_cached(mut self) -> Self { self.flags.insert(Flags::ONLY_IF_CACHED); self } /// Set the `private` directive. pub fn with_private(mut self) -> Self { self.flags.insert(Flags::PRIVATE); self } /// Set the `public` directive. pub fn with_public(mut self) -> Self { self.flags.insert(Flags::PUBLIC); self } /// Set the `max-age` directive. pub fn with_max_age(mut self, seconds: Duration) -> Self { self.max_age = Some(seconds.into()); self } /// Set the `max-stale` directive. pub fn with_max_stale(mut self, seconds: Duration) -> Self { self.max_stale = Some(seconds.into()); self } /// Set the `min-fresh` directive. pub fn with_min_fresh(mut self, seconds: Duration) -> Self { self.min_fresh = Some(seconds.into()); self } /// Set the `s-maxage` directive. pub fn with_s_max_age(mut self, seconds: Duration) -> Self { self.s_max_age = Some(seconds.into()); self } } impl ::Header for CacheControl { fn name() -> &'static ::HeaderName { &::http::header::CACHE_CONTROL } fn decode<'i, I: Iterator>(values: &mut I) -> Result { csv::from_comma_delimited(values).map(|FromIter(cc)| cc) } fn encode>(&self, values: &mut E) { values.extend(::std::iter::once(util::fmt(Fmt(self)))); } } // Adapter to be used in Header::decode struct FromIter(CacheControl); impl FromIterator for FromIter { fn from_iter(iter: I) -> Self where I: IntoIterator, { let mut cc = CacheControl::new(); // ignore all unknown directives let iter = iter.into_iter().filter_map(|dir| match dir { KnownDirective::Known(dir) => Some(dir), KnownDirective::Unknown => None, }); for directive in iter { match directive { Directive::NoCache => { cc.flags.insert(Flags::NO_CACHE); } Directive::NoStore => { cc.flags.insert(Flags::NO_STORE); } Directive::NoTransform => { cc.flags.insert(Flags::NO_TRANSFORM); } Directive::OnlyIfCached => { cc.flags.insert(Flags::ONLY_IF_CACHED); } Directive::MustRevalidate => { cc.flags.insert(Flags::MUST_REVALIDATE); } Directive::Public => { cc.flags.insert(Flags::PUBLIC); } Directive::Private => { cc.flags.insert(Flags::PRIVATE); } Directive::ProxyRevalidate => { cc.flags.insert(Flags::PROXY_REVALIDATE); } Directive::MaxAge(secs) => { cc.max_age = Some(Duration::from_secs(secs.into()).into()); } Directive::MaxStale(secs) => { cc.max_stale = Some(Duration::from_secs(secs.into()).into()); } Directive::MinFresh(secs) => { cc.min_fresh = Some(Duration::from_secs(secs.into()).into()); } Directive::SMaxAge(secs) => { cc.s_max_age = Some(Duration::from_secs(secs.into()).into()); } } } FromIter(cc) } } struct Fmt<'a>(&'a CacheControl); impl<'a> fmt::Display for Fmt<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let if_flag = |f: Flags, dir: Directive| { if self.0.flags.contains(f) { Some(dir) } else { None } }; let slice = &[ if_flag(Flags::NO_CACHE, Directive::NoCache), if_flag(Flags::NO_STORE, Directive::NoStore), if_flag(Flags::NO_TRANSFORM, Directive::NoTransform), if_flag(Flags::ONLY_IF_CACHED, Directive::OnlyIfCached), if_flag(Flags::MUST_REVALIDATE, Directive::MustRevalidate), if_flag(Flags::PUBLIC, Directive::Public), if_flag(Flags::PRIVATE, Directive::Private), if_flag(Flags::PROXY_REVALIDATE, Directive::ProxyRevalidate), self.0 .max_age .as_ref() .map(|s| Directive::MaxAge(s.as_u64())), self.0 .max_stale .as_ref() .map(|s| Directive::MaxStale(s.as_u64())), self.0 .min_fresh .as_ref() .map(|s| Directive::MinFresh(s.as_u64())), self.0 .s_max_age .as_ref() .map(|s| Directive::SMaxAge(s.as_u64())), ]; let iter = slice.iter().filter_map(|o| *o); csv::fmt_comma_delimited(f, iter) } } #[derive(Clone, Copy)] enum KnownDirective { Known(Directive), Unknown, } #[derive(Clone, Copy)] enum Directive { NoCache, NoStore, NoTransform, OnlyIfCached, // request directives MaxAge(u64), MaxStale(u64), MinFresh(u64), // response directives MustRevalidate, Public, Private, ProxyRevalidate, SMaxAge(u64), } impl fmt::Display for Directive { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt( match *self { Directive::NoCache => "no-cache", Directive::NoStore => "no-store", Directive::NoTransform => "no-transform", Directive::OnlyIfCached => "only-if-cached", Directive::MaxAge(secs) => return write!(f, "max-age={}", secs), Directive::MaxStale(secs) => return write!(f, "max-stale={}", secs), Directive::MinFresh(secs) => return write!(f, "min-fresh={}", secs), Directive::MustRevalidate => "must-revalidate", Directive::Public => "public", Directive::Private => "private", Directive::ProxyRevalidate => "proxy-revalidate", Directive::SMaxAge(secs) => return write!(f, "s-maxage={}", secs), }, f, ) } } impl FromStr for KnownDirective { type Err = (); fn from_str(s: &str) -> Result { Ok(KnownDirective::Known(match s { "no-cache" => Directive::NoCache, "no-store" => Directive::NoStore, "no-transform" => Directive::NoTransform, "only-if-cached" => Directive::OnlyIfCached, "must-revalidate" => Directive::MustRevalidate, "public" => Directive::Public, "private" => Directive::Private, "proxy-revalidate" => Directive::ProxyRevalidate, "" => return Err(()), _ => match s.find('=') { Some(idx) if idx + 1 < s.len() => { match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) { ("max-age", secs) => secs.parse().map(Directive::MaxAge).map_err(|_| ())?, ("max-stale", secs) => { secs.parse().map(Directive::MaxStale).map_err(|_| ())? } ("min-fresh", secs) => { secs.parse().map(Directive::MinFresh).map_err(|_| ())? } ("s-maxage", secs) => { secs.parse().map(Directive::SMaxAge).map_err(|_| ())? } _unknown => return Ok(KnownDirective::Unknown), } } Some(_) | None => return Ok(KnownDirective::Unknown), }, })) } } #[cfg(test)] mod tests { use super::super::{test_decode, test_encode}; use super::*; #[test] fn test_parse_multiple_headers() { assert_eq!( test_decode::(&["no-cache", "private"]).unwrap(), CacheControl::new().with_no_cache().with_private(), ); } #[test] fn test_parse_argument() { assert_eq!( test_decode::(&["max-age=100, private"]).unwrap(), CacheControl::new() .with_max_age(Duration::from_secs(100)) .with_private(), ); } #[test] fn test_parse_quote_form() { assert_eq!( test_decode::(&["max-age=\"200\""]).unwrap(), CacheControl::new().with_max_age(Duration::from_secs(200)), ); } #[test] fn test_parse_extension() { assert_eq!( test_decode::(&["foo, no-cache, bar=baz"]).unwrap(), CacheControl::new().with_no_cache(), "unknown extensions are ignored but shouldn't fail parsing", ); } #[test] fn test_parse_bad_syntax() { assert_eq!(test_decode::(&["max-age=lolz"]), None,); } #[test] fn encode_one_flag_directive() { let cc = CacheControl::new().with_no_cache(); let headers = test_encode(cc); assert_eq!(headers["cache-control"], "no-cache"); } #[test] fn encode_one_param_directive() { let cc = CacheControl::new().with_max_age(Duration::from_secs(300)); let headers = test_encode(cc); assert_eq!(headers["cache-control"], "max-age=300"); } #[test] fn encode_two_directive() { let headers = test_encode(CacheControl::new().with_no_cache().with_private()); assert_eq!(headers["cache-control"], "no-cache, private"); let headers = test_encode( CacheControl::new() .with_no_cache() .with_max_age(Duration::from_secs(100)), ); assert_eq!(headers["cache-control"], "no-cache, max-age=100"); } }