Skip to main content

cookie_store/
cookie_expiration.rs

1#[cfg(feature = "serde")]
2use serde_derive::{Deserialize, Serialize};
3use time::{self, OffsetDateTime};
4
5/// When a given `Cookie` expires
6#[derive(Eq, Clone, Debug)]
7#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8pub enum CookieExpiration {
9    /// `Cookie` expires at the given UTC time, as set from either the Max-Age
10    /// or Expires attribute of a Set-Cookie header
11    #[cfg_attr(feature = "serde", serde(with = "crate::rfc3339_fmt"))]
12    AtUtc(OffsetDateTime),
13    /// `Cookie` expires at the end of the current `Session`; this means the cookie
14    /// is not persistent
15    SessionEnd,
16}
17
18// We directly impl `PartialEq` as the cookie Expires attribute does not include nanosecond precision
19impl std::cmp::PartialEq for CookieExpiration {
20    fn eq(&self, other: &Self) -> bool {
21        match (self, other) {
22            (CookieExpiration::SessionEnd, CookieExpiration::SessionEnd) => true,
23            (CookieExpiration::AtUtc(this_offset), CookieExpiration::AtUtc(other_offset)) => {
24                // All instances should already be UTC offset
25                this_offset.date() == other_offset.date()
26                    && this_offset.time().hour() == other_offset.time().hour()
27                    && this_offset.time().minute() == other_offset.time().minute()
28                    && this_offset.time().second() == other_offset.time().second()
29            }
30            _ => false,
31        }
32    }
33}
34
35impl CookieExpiration {
36    /// Indicates if the `Cookie` is expired as of *now*.
37    pub fn is_expired(&self) -> bool {
38        self.expires_by(&time::OffsetDateTime::now_utc())
39    }
40
41    /// Indicates if the `Cookie` expires as of `utc_tm`.
42    pub fn expires_by(&self, utc_tm: &time::OffsetDateTime) -> bool {
43        match *self {
44            CookieExpiration::AtUtc(ref expire_tm) => *expire_tm <= *utc_tm,
45            CookieExpiration::SessionEnd => false,
46        }
47    }
48}
49
50const MAX_RFC3339: time::OffsetDateTime = time::macros::date!(9999 - 12 - 31)
51    .with_time(time::macros::time!(23:59:59))
52    .assume_utc();
53impl From<u64> for CookieExpiration {
54    fn from(max_age: u64) -> CookieExpiration {
55        // make sure we don't trigger a panic! in Duration by restricting the seconds
56        // to the max
57        CookieExpiration::from(time::Duration::seconds(std::cmp::min(
58            time::Duration::MAX.whole_seconds() as u64,
59            max_age,
60        ) as i64))
61    }
62}
63
64impl From<time::OffsetDateTime> for CookieExpiration {
65    fn from(utc_tm: OffsetDateTime) -> CookieExpiration {
66        CookieExpiration::AtUtc(utc_tm.min(MAX_RFC3339))
67    }
68}
69
70impl From<cookie::Expiration> for CookieExpiration {
71    fn from(expiration: cookie::Expiration) -> CookieExpiration {
72        match expiration {
73            cookie::Expiration::DateTime(offset) => CookieExpiration::AtUtc(offset),
74            cookie::Expiration::Session => CookieExpiration::SessionEnd,
75        }
76    }
77}
78
79impl From<time::Duration> for CookieExpiration {
80    fn from(duration: time::Duration) -> Self {
81        // If delta-seconds is less than or equal to zero (0), let expiry-time
82        //    be the earliest representable date and time.  Otherwise, let the
83        //    expiry-time be the current date and time plus delta-seconds seconds.
84        let utc_tm = if duration.is_zero() {
85            time::OffsetDateTime::UNIX_EPOCH
86        } else {
87            let now_utc = time::OffsetDateTime::now_utc();
88            let d = (MAX_RFC3339 - now_utc).min(duration);
89            now_utc + d
90        };
91        CookieExpiration::from(utc_tm)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::CookieExpiration;
98
99    use crate::utils::test::*;
100
101    #[test]
102    fn max_age_bounds() {
103        match CookieExpiration::from(time::Duration::MAX.whole_seconds() as u64 + 1) {
104            CookieExpiration::AtUtc(_) => {}
105            _ => unreachable!(),
106        }
107    }
108
109    #[test]
110    fn expired() {
111        let ma = CookieExpiration::from(0u64); // Max-Age<=0 indicates the cookie is expired
112        assert!(ma.is_expired());
113        assert!(ma.expires_by(&in_days(-1)));
114    }
115
116    #[test]
117    fn max_age() {
118        let ma = CookieExpiration::from(60u64);
119        assert!(!ma.is_expired());
120        assert!(ma.expires_by(&in_minutes(2)));
121    }
122
123    #[test]
124    fn session_end() {
125        // SessionEnd never "expires"; lives until end of session
126        let se = CookieExpiration::SessionEnd;
127        assert!(!se.is_expired());
128        assert!(!se.expires_by(&in_days(1)));
129        assert!(!se.expires_by(&in_days(-1)));
130    }
131
132    #[test]
133    fn at_utc() {
134        {
135            let expire_tmrw = CookieExpiration::from(in_days(1));
136            assert!(!expire_tmrw.is_expired());
137            assert!(expire_tmrw.expires_by(&in_days(2)));
138        }
139        {
140            let expired_yest = CookieExpiration::from(in_days(-1));
141            assert!(expired_yest.is_expired());
142            assert!(!expired_yest.expires_by(&in_days(-2)));
143        }
144    }
145}