1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use crate::headers::HeaderValue;
use crate::Status;

use std::time::Duration;

/// An HTTP `Cache-Control` directive.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheDirective {
    /// The response body will not change over time.
    Immutable,
    /// The maximum amount of time a resource is considered fresh.
    MaxAge(Duration),
    /// Indicates the client will accept a stale response.
    MaxStale(Option<Duration>),
    /// A response that will still be fresh for at least the specified duration.
    MinFresh(Duration),
    /// Once a response is stale, a fresh response must be retrieved.
    MustRevalidate,
    /// The response may be cached, but must always be revalidated before being used.
    NoCache,
    /// The response may not be cached.
    NoStore,
    /// An intermediate cache or proxy should not edit the response body,
    /// Content-Encoding, Content-Range, or Content-Type.
    NoTransform,
    /// Do not use the network for a response.
    OnlyIfCached,
    /// The response may be stored only by a browser's cache, even if the
    /// response is normally non-cacheable.
    Private,
    /// Like must-revalidate, but only for shared caches (e.g., proxies).
    ProxyRevalidate,
    /// The response may be stored by any cache, even if the response is normally
    /// non-cacheable.
    Public,
    /// Overrides max-age or the Expires header, but only for shared caches.
    SMaxAge(Duration),
    /// The client will accept a stale response if retrieving a fresh one fails.
    StaleIfError(Duration),
    /// Indicates the client will accept a stale response, while asynchronously
    /// checking in the background for a fresh one.
    StaleWhileRevalidate(Duration),
}

impl CacheDirective {
    /// Check whether this directive is valid in an HTTP request.
    pub fn valid_in_req(&self) -> bool {
        use CacheDirective::*;
        matches!(
            self,
            MaxAge(_) | MaxStale(_) | MinFresh(_) | NoCache | NoStore | NoTransform | OnlyIfCached
        )
    }

    /// Check whether this directive is valid in an HTTP response.
    pub fn valid_in_res(&self) -> bool {
        use CacheDirective::*;
        matches!(
            self,
            MustRevalidate
                | NoCache
                | NoStore
                | NoTransform
                | Public
                | Private
                | ProxyRevalidate
                | MaxAge(_)
                | SMaxAge(_)
                | StaleIfError(_)
                | StaleWhileRevalidate(_)
        )
    }

    /// Create an instance from a string slice.
    //
    // This is a private method rather than a trait because we assume the
    // input string is a single-value only. This is upheld by the calling
    // function, but we cannot guarantee this to be true in the general
    // sense.
    pub(crate) fn from_str(s: &str) -> crate::Result<Option<Self>> {
        use CacheDirective::*;

        let s = s.trim();

        // We're dealing with an empty string.
        if s.is_empty() {
            return Ok(None);
        }

        s.to_lowercase();
        let mut parts = s.split('=');
        let next = parts.next().unwrap();

        let mut get_dur = || -> crate::Result<Duration> {
            let dur = parts.next().status(400)?;
            let dur: u64 = dur.parse().status(400)?;
            Ok(Duration::new(dur, 0))
        };

        // This won't panic because each input string has at least one part.
        let res = match next {
            "immutable" => Some(Immutable),
            "no-cache" => Some(NoCache),
            "no-store" => Some(NoStore),
            "no-transform" => Some(NoTransform),
            "only-if-cached" => Some(OnlyIfCached),
            "must-revalidate" => Some(MustRevalidate),
            "public" => Some(Public),
            "private" => Some(Private),
            "proxy-revalidate" => Some(ProxyRevalidate),
            "max-age" => Some(MaxAge(get_dur()?)),
            "max-stale" => match parts.next() {
                Some(secs) => {
                    let dur: u64 = secs.parse().status(400)?;
                    Some(MaxStale(Some(Duration::new(dur, 0))))
                }
                None => Some(MaxStale(None)),
            },
            "min-fresh" => Some(MinFresh(get_dur()?)),
            "s-maxage" => Some(SMaxAge(get_dur()?)),
            "stale-if-error" => Some(StaleIfError(get_dur()?)),
            "stale-while-revalidate" => Some(StaleWhileRevalidate(get_dur()?)),
            _ => None,
        };
        Ok(res)
    }
}

impl From<CacheDirective> for HeaderValue {
    fn from(directive: CacheDirective) -> Self {
        use CacheDirective::*;
        let h = |s: String| unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) };

        match directive {
            Immutable => h("immutable".to_string()),
            MaxAge(dur) => h(format!("max-age={}", dur.as_secs())),
            MaxStale(dur) => match dur {
                Some(dur) => h(format!("max-stale={}", dur.as_secs())),
                None => h("max-stale".to_string()),
            },
            MinFresh(dur) => h(format!("min-fresh={}", dur.as_secs())),
            MustRevalidate => h("must-revalidate".to_string()),
            NoCache => h("no-cache".to_string()),
            NoStore => h("no-store".to_string()),
            NoTransform => h("no-transform".to_string()),
            OnlyIfCached => h("only-if-cached".to_string()),
            Private => h("private".to_string()),
            ProxyRevalidate => h("proxy-revalidate".to_string()),
            Public => h("public".to_string()),
            SMaxAge(dur) => h(format!("s-max-age={}", dur.as_secs())),
            StaleIfError(dur) => h(format!("stale-if-error={}", dur.as_secs())),
            StaleWhileRevalidate(dur) => h(format!("stale-while-revalidate={}", dur.as_secs())),
        }
    }
}