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
use std;

use serde_derive::{Deserialize, Serialize};
use time::{self, OffsetDateTime};

/// When a given `Cookie` expires
#[derive(Eq, Clone, Debug, Serialize, Deserialize)]
pub enum CookieExpiration {
    /// `Cookie` expires at the given UTC time, as set from either the Max-Age
    /// or Expires attribute of a Set-Cookie header
    #[serde(with = "crate::rfc3339_fmt")]
    AtUtc(OffsetDateTime),
    /// `Cookie` expires at the end of the current `Session`; this means the cookie
    /// is not persistent
    SessionEnd,
}

// We directly impl `PartialEq` as the cookie Expires attribute does not include nanosecond precision
impl std::cmp::PartialEq for CookieExpiration {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (CookieExpiration::SessionEnd, CookieExpiration::SessionEnd) => true,
            (CookieExpiration::AtUtc(this_offset), CookieExpiration::AtUtc(other_offset)) => {
                // All instances should already be UTC offset
                this_offset.date() == other_offset.date()
                    && this_offset.time().hour() == other_offset.time().hour()
                    && this_offset.time().minute() == other_offset.time().minute()
                    && this_offset.time().second() == other_offset.time().second()
            }
            _ => false,
        }
    }
}

impl CookieExpiration {
    /// Indicates if the `Cookie` is expired as of *now*.
    pub fn is_expired(&self) -> bool {
        self.expires_by(&time::OffsetDateTime::now_utc())
    }

    /// Indicates if the `Cookie` expires as of `utc_tm`.
    pub fn expires_by(&self, utc_tm: &time::OffsetDateTime) -> bool {
        match *self {
            CookieExpiration::AtUtc(ref expire_tm) => *expire_tm <= *utc_tm,
            CookieExpiration::SessionEnd => false,
        }
    }
}

const MAX_RFC3339: time::OffsetDateTime = time::macros::date!(9999 - 12 - 31)
    .with_time(time::macros::time!(23:59:59))
    .assume_utc();
impl From<u64> for CookieExpiration {
    fn from(max_age: u64) -> CookieExpiration {
        // make sure we don't trigger a panic! in Duration by restricting the seconds
        // to the max
        CookieExpiration::from(time::Duration::seconds(std::cmp::min(
            time::Duration::MAX.whole_seconds() as u64,
            max_age,
        ) as i64))
    }
}

impl From<time::OffsetDateTime> for CookieExpiration {
    fn from(utc_tm: OffsetDateTime) -> CookieExpiration {
        CookieExpiration::AtUtc(utc_tm.min(MAX_RFC3339))
    }
}

impl From<cookie::Expiration> for CookieExpiration {
    fn from(expiration: cookie::Expiration) -> CookieExpiration {
        match expiration {
            cookie::Expiration::DateTime(offset) => CookieExpiration::AtUtc(offset),
            cookie::Expiration::Session => CookieExpiration::SessionEnd,
        }
    }
}

impl From<time::Duration> for CookieExpiration {
    fn from(duration: time::Duration) -> Self {
        // If delta-seconds is less than or equal to zero (0), let expiry-time
        //    be the earliest representable date and time.  Otherwise, let the
        //    expiry-time be the current date and time plus delta-seconds seconds.
        let utc_tm = if duration.is_zero() {
            time::OffsetDateTime::UNIX_EPOCH
        } else {
            let now_utc = time::OffsetDateTime::now_utc();
            let d = (MAX_RFC3339 - now_utc).min(duration);
            now_utc + d
        };
        CookieExpiration::from(utc_tm)
    }
}

#[cfg(test)]
mod tests {
    use super::CookieExpiration;
    use time;

    use crate::utils::test::*;

    #[test]
    fn max_age_bounds() {
        match CookieExpiration::from(time::Duration::MAX.whole_seconds() as u64 + 1) {
            CookieExpiration::AtUtc(_) => assert!(true),
            _ => assert!(false),
        }
    }

    #[test]
    fn expired() {
        let ma = CookieExpiration::from(0u64); // Max-Age<=0 indicates the cookie is expired
        assert!(ma.is_expired());
        assert!(ma.expires_by(&in_days(-1)));
    }

    #[test]
    fn max_age() {
        let ma = CookieExpiration::from(60u64);
        assert!(!ma.is_expired());
        assert!(ma.expires_by(&in_minutes(2)));
    }

    #[test]
    fn session_end() {
        // SessionEnd never "expires"; lives until end of session
        let se = CookieExpiration::SessionEnd;
        assert!(!se.is_expired());
        assert!(!se.expires_by(&in_days(1)));
        assert!(!se.expires_by(&in_days(-1)));
    }

    #[test]
    fn at_utc() {
        {
            let expire_tmrw = CookieExpiration::from(in_days(1));
            assert!(!expire_tmrw.is_expired());
            assert!(expire_tmrw.expires_by(&in_days(2)));
        }
        {
            let expired_yest = CookieExpiration::from(in_days(-1));
            assert!(expired_yest.is_expired());
            assert!(!expired_yest.expires_by(&in_days(-2)));
        }
    }
}