Skip to main content

cookie_monster/cookie/expires/
mod.rs

1use std::fmt::{Debug, Write};
2
3use super::Cookie;
4
5#[cfg(feature = "time")]
6pub mod dep_time;
7
8#[cfg(feature = "chrono")]
9pub mod dep_chrono;
10
11#[cfg(feature = "jiff")]
12pub mod dep_jiff;
13
14const REMOVE: &str = "Thu, 01 Jan 1970 00:00:00 GMT";
15
16/// The Expires attribute.
17#[derive(Clone, Default)]
18pub enum Expires {
19    // So a user can still remove a cookie without needing any of the datetime features.
20    Remove,
21    // No expiry time.
22    #[default]
23    Session,
24    Exp(ExpVal),
25}
26
27#[derive(Clone, Default)]
28pub struct ExpVal {
29    #[cfg(feature = "time")]
30    time: Option<time::OffsetDateTime>,
31    #[cfg(feature = "chrono")]
32    chrono: Option<chrono::DateTime<chrono::Utc>>,
33    #[cfg(feature = "jiff")]
34    jiff: Option<jiff::Zoned>,
35}
36
37impl Expires {
38    /// If one of the `time`, `chrono` or `jiff` features are enabled, the Expires tag is set to the
39    /// current time minus one year. If none of the those features are enabled, the Expires
40    /// attribute is set to 1 Jan 1970 00:00.
41    pub fn remove() -> Self {
42        #![allow(unreachable_code)]
43
44        #[cfg(feature = "jiff")]
45        return Self::remove_jiff();
46
47        #[cfg(feature = "chrono")]
48        return Self::remove_chrono();
49
50        #[cfg(feature = "time")]
51        return Self::remove_time();
52
53        Self::Remove
54    }
55}
56
57impl Cookie {
58    /// If the Expires attribute is not set, the expiration of the cookie is tied to the session
59    /// with the user-agent.
60    pub fn expires_is_set(&self) -> bool {
61        !matches!(self.expires, Expires::Session)
62    }
63
64    pub(crate) fn serialize_expire(&self, buf: &mut String) -> crate::Result<()> {
65        // Only one can be set at all times, except while parsing but then the first match is used.
66        match &self.expires {
67            #[cfg(feature = "time")]
68            Expires::Exp(ExpVal { time: Some(t), .. }) => dep_time::ser_expires(t, buf),
69            #[cfg(feature = "chrono")]
70            Expires::Exp(ExpVal {
71                chrono: Some(c), ..
72            }) => dep_chrono::ser_expires(c, buf),
73            #[cfg(feature = "jiff")]
74            Expires::Exp(ExpVal { jiff: Some(j), .. }) => dep_jiff::ser_expires(j, buf),
75            Expires::Remove => {
76                let _ = write!(buf, "; Expires={REMOVE}");
77                Ok(())
78            }
79
80            _ => Ok(()),
81        }
82    }
83}
84
85impl PartialEq for Expires {
86    fn eq(&self, other: &Self) -> bool {
87        match (self, other) {
88            (Expires::Remove, Expires::Remove) => true,
89            (Expires::Session, Expires::Session) => true,
90            (Expires::Exp(_s), Expires::Exp(_o)) => {
91                // Each `From` impl only populates the field for the datetime crate it came
92                // from, leaving the others `None`. Compare a field only when at least one
93                // side actually has it set, otherwise a shared `None` would falsely match.
94                #[cfg(feature = "time")]
95                if _s.time.is_some() || _o.time.is_some() {
96                    return _s.time == _o.time;
97                }
98
99                #[cfg(feature = "chrono")]
100                if _s.chrono.is_some() || _o.chrono.is_some() {
101                    return _s.chrono == _o.chrono;
102                }
103
104                #[cfg(feature = "jiff")]
105                if _s.jiff.is_some() || _o.jiff.is_some() {
106                    return _s.jiff == _o.jiff;
107                }
108
109                // Neither side has any datetime set; treat them as equal.
110                true
111            }
112            _ => false,
113        }
114    }
115}
116
117#[cfg(all(test, feature = "time", feature = "jiff"))]
118mod eq_tests {
119    use crate::Cookie;
120    use jiff::{civil::datetime, tz::TimeZone};
121
122    fn cookie_expiring(year: i16) -> Cookie {
123        Cookie::build("foo", "bar")
124            .expires(
125                datetime(year, 1, 1, 0, 0, 0, 0)
126                    .to_zoned(TimeZone::UTC)
127                    .unwrap(),
128            )
129            .build()
130    }
131
132    // With more than one datetime feature enabled, only the populated field
133    // (here `jiff`) carries the expiry. Equality must not treat a shared `None`
134    // in another field (`time`) as a match.
135    #[test]
136    fn different_expiry_is_not_equal() {
137        assert_ne!(cookie_expiring(2020), cookie_expiring(2099));
138    }
139
140    #[test]
141    fn same_expiry_is_equal() {
142        assert_eq!(cookie_expiring(2020), cookie_expiring(2020));
143    }
144}
145
146impl Debug for Expires {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        match self {
149            Self::Remove => write!(f, "{REMOVE}"),
150            Self::Session => write!(f, "Session"),
151            Self::Exp(_exp) => {
152                let mut debug = f.debug_struct("Exp");
153
154                #[cfg(feature = "time")]
155                let debug = debug.field("expires_time", &_exp.time);
156
157                #[cfg(feature = "chrono")]
158                let debug = debug.field("expires_chrono", &_exp.chrono);
159
160                #[cfg(feature = "jiff")]
161                let debug = debug.field("expires_jiff", &_exp.jiff);
162
163                debug.finish()
164            }
165        }
166    }
167}