cookie_monster/
jar.rs

1use std::{borrow::Borrow, collections::HashSet, fmt::Debug, hash::Hash};
2
3use crate::Cookie;
4
5/// A generic `CookieJar` for cookie management. Can be used to read update or delete cookies from
6/// a user session.
7///
8/// ## `axum` feature
9///
10/// Note that to set the cookies, the jar _must_ be returned from the handler. Otherwise the
11/// cookies are not updated.
12///
13///
14/// ## Example
15/// ```rust
16/// use cookie_monster::{CookieJar, Cookie};
17///
18/// static COOKIE_NAME: &str = "session";
19///
20/// async fn handler(mut jar: CookieJar) -> CookieJar {
21///
22///     if let Some(cookie) = jar.get(COOKIE_NAME) {
23///         println!("Removing cookie {cookie:?}");
24///         jar.remove(Cookie::named(COOKIE_NAME));
25///     } else {
26///         let cookie = Cookie::new(COOKIE_NAME, "hello, world");
27///         println!("Setting cookie {cookie:?}");
28///         jar.add(cookie);
29///     }
30///
31///     // Important, return the jar to update the cookies!
32///     jar
33/// }
34/// ```
35#[derive(Default, Debug)]
36pub struct CookieJar {
37    cookies: HashSet<HashCookie>,
38}
39
40pub(crate) enum HashCookie {
41    // An original cookie. These should never be sent back to the user-agent.
42    Original(Cookie),
43    // A new cookie, the should always be sent back to the user-agent.
44    New(Cookie),
45    // A removed cookie, the should always be sent back to the user-agent but should never be
46    // visible by the user.
47    Removal(Cookie),
48}
49
50impl HashCookie {
51    fn name(&self) -> &str {
52        match self {
53            HashCookie::Original(c) => c.name(),
54            HashCookie::New(c) => c.name(),
55            HashCookie::Removal(c) => c.name(),
56        }
57    }
58}
59
60impl Hash for HashCookie {
61    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
62        self.name().hash(state);
63    }
64}
65
66impl PartialEq for HashCookie {
67    fn eq(&self, other: &Self) -> bool {
68        self.name() == other.name()
69    }
70}
71
72impl Borrow<str> for HashCookie {
73    fn borrow(&self) -> &str {
74        self.name()
75    }
76}
77
78impl Eq for HashCookie {}
79
80impl Debug for HashCookie {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            HashCookie::Original(cookie) => cookie.fmt(f),
84            HashCookie::New(cookie) => cookie.fmt(f),
85            HashCookie::Removal(cookie) => cookie.fmt(f),
86        }
87    }
88}
89
90impl CookieJar {
91    /// Creates an empty `CookieJar`.
92    pub fn empty() -> Self {
93        Self::default()
94    }
95
96    /// Parses the given `cookie` header value and return a `CookieJar`. This function ignores
97    /// cookies that were not able to be parsed.
98    pub fn from_cookie(header: &str) -> Self {
99        Self::from_original(header.split(';').flat_map(Cookie::parse_cookie))
100    }
101
102    /// Parses the given `cookie` header value and return a `CookieJar`. The cookie name and values
103    /// are percent-decoded. Cookies that were not able to be parsed are ignored.
104    #[cfg(feature = "percent-encode")]
105    pub fn from_encoded_cookie(header: &str) -> Self {
106        Self::from_original(header.split(';').flat_map(Cookie::parse_cookie_encoded))
107    }
108
109    /// Adds an __original__ cookie to the jar. These are never sent back to the
110    /// user-agent, but are visible in the cookie jar.
111    pub fn add_original(&mut self, cookie: Cookie) {
112        self.cookies.insert(HashCookie::Original(cookie));
113    }
114
115    // Creates a `CookieJar` from an iterator of cookies. It is assumed that the cookies are
116    // __original__. E.g. from a `Cookie` header value.
117    pub fn from_original<T: IntoIterator<Item = Cookie>>(cookies: T) -> Self {
118        let mut jar = Self::empty();
119
120        for cookie in cookies {
121            jar.add_original(cookie);
122        }
123
124        jar
125    }
126
127    /// Get a cookie by name. Gives back either an __original__ or newly added cookie.
128    pub fn get(&self, name: &str) -> Option<&Cookie> {
129        self.cookies.get(name).and_then(|c| match c {
130            HashCookie::New(c) | HashCookie::Original(c) => Some(c),
131            HashCookie::Removal(_) => None,
132        })
133    }
134
135    /// Iterate over all changes. This returns all removed and newly created cookies.
136    pub fn set_cookie_headers(&self) -> impl Iterator<Item = crate::Result<String>> {
137        self.cookies
138            .iter()
139            .filter_map(|c| match c {
140                HashCookie::Original(_) => None,
141                HashCookie::New(c) | HashCookie::Removal(c) => Some(c),
142            })
143            .map(Cookie::serialize)
144    }
145
146    /// Removes the cookie from the local cookie store and issues a cookie with an Expires
147    /// attribute in the past and Max-Age of 0 seconds.
148    ///
149    /// If one of the `time`, `chrono` or `jiff` features are enabled, the Expires tag is set to the
150    /// current time minus one year. If none of the those features are enabled, the Expires
151    /// attribute is set to 1 Jan 1970 00:00.
152    ///
153    /// **To ensure a cookie is removed from the user-agent, set the `Path` and `Domain` attributes
154    /// with the same values that were used to create the cookie.**
155    pub fn remove(&mut self, cookie: impl Into<Cookie>) {
156        let cookie = HashCookie::Removal(cookie.into().into_remove());
157        self.cookies.replace(cookie);
158    }
159
160    /// Adds a cookie to the jar. If a cookie with the same name is already in the jar, it is
161    /// replaced with the given cookie.
162    pub fn add(&mut self, cookie: impl Into<Cookie>) {
163        self.cookies.replace(HashCookie::New(cookie.into()));
164    }
165
166    #[allow(unused)]
167    pub(crate) fn iter_non_original(&self) -> impl Iterator<Item = &Cookie> {
168        self.cookies.iter().flat_map(|cookie| match cookie {
169            HashCookie::Original(_) => None,
170            HashCookie::New(cookie) | HashCookie::Removal(cookie) => Some(cookie),
171        })
172    }
173}