cookie_monster/cookie/
mod.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Debug},
4    time::Duration,
5};
6
7mod builder;
8mod domain;
9pub(crate) mod expires;
10mod parse;
11mod path;
12pub(crate) mod same_site;
13mod serialize;
14
15#[cfg(feature = "percent-encode")]
16mod encoding;
17
18pub use builder::CookieBuilder;
19use expires::Expires;
20
21use crate::{SameSite, util::TinyStr};
22
23/// An HTTP Cookie.
24#[derive(Default, Clone)]
25pub struct Cookie {
26    // A read only buffer to the raw cookie value. We're using a Box<str> here since it makes the
27    // Cookie a bit smaller.
28    raw_value: Option<Box<str>>,
29    name: TinyStr,
30    value: TinyStr,
31    expires: Expires,
32    max_age: Option<u64>,
33    domain: Option<TinyStr>,
34    path: Option<TinyStr>,
35    secure: bool,
36    http_only: bool,
37    partitioned: bool,
38    same_site: Option<SameSite>,
39}
40
41impl Cookie {
42    /// Creates a new cookie with the given name and value.
43    ///
44    /// # Example
45    /// ```rust
46    /// use cookie_monster::Cookie;
47    ///
48    /// let cookie = Cookie::new("hello", "world");
49    ///
50    /// assert_eq!(cookie.name(), "hello");
51    /// assert_eq!(cookie.value(), "world");
52    /// ```
53    ///
54    /// For more options, see [`Cookie::build`].
55    pub fn new<N, V>(name: N, value: V) -> Cookie
56    where
57        N: Into<Cow<'static, str>>,
58        V: Into<Cow<'static, str>>,
59    {
60        Self::new_inner(TinyStr::from(name), TinyStr::from(value))
61    }
62
63    /// Creates a cookie that can be used to remove the cookie from the user-agent. This sets the
64    /// Expires attribute in the past and Max-Age to 0 seconds.
65    ///
66    /// If one of the `time`, `chrono` or `jiff` features are enabled, the Expires tag is set to the
67    /// current time minus one year. If none of the those features are enabled, the Expires
68    /// attribute is set to 1 Jan 1970 00:00.
69    ///
70    /// **To ensure a cookie is removed from the user-agent, set the `Path` and `Domain` attributes
71    /// with the same values that were used to create the cookie.**
72    ///
73    /// # Note
74    /// You don't have to use this method in combination with
75    /// [`CookieJar::remove`](crate::CookieJar), the jar
76    /// automatically set's the Expires and Max-Age attributes.
77    ///
78    /// # Example
79    /// ```rust
80    /// use cookie_monster::Cookie;
81    ///
82    /// let cookie = Cookie::remove("session");
83    ///
84    /// assert_eq!(cookie.max_age_secs(), Some(0));
85    /// assert!(cookie.expires_is_set());
86    /// ```
87    pub fn remove<N>(name: N) -> Cookie
88    where
89        N: Into<Cow<'static, str>>,
90    {
91        Cookie::new(name, "").into_remove()
92    }
93
94    pub(crate) fn into_remove(mut self) -> Self {
95        self.set_expires(Expires::remove());
96        self.set_max_age_secs(0);
97        self.set_value("");
98        self
99    }
100
101    fn new_inner(name: TinyStr, value: TinyStr) -> Cookie {
102        Cookie {
103            name,
104            value,
105            ..Default::default()
106        }
107    }
108
109    /// Build a new cookie. This returns a [`CookieBuilder`](crate::CookieBuilder) that can be used
110    /// to  set other attribute values.
111    ///
112    /// # Example
113    /// ```rust
114    /// use cookie_monster::Cookie;
115    ///
116    /// let cookie = Cookie::build("foo", "bar")
117    ///     .secure()
118    ///     .http_only()
119    ///     .build();
120    ///
121    /// assert!(cookie.secure());
122    /// assert!(cookie.http_only());
123    /// ```
124    pub fn build<N, V>(name: N, value: V) -> CookieBuilder
125    where
126        N: Into<Cow<'static, str>>,
127        V: Into<Cow<'static, str>>,
128    {
129        CookieBuilder::new(name, value)
130    }
131
132    /// Creates a [`CookieBuilder`] with the given name and an empty value. This can be used when
133    /// removing a cookie from a [`CookieJar`](crate::CookieJar).
134    ///
135    /// # Example
136    /// ```rust
137    /// use cookie_monster::{Cookie, CookieJar};
138    ///
139    /// let mut jar = CookieJar::empty();
140    /// jar.remove(Cookie::named("session").path("/login"));
141    ///
142    /// assert!(jar.get("session").is_none());
143    /// ```
144    pub fn named<N>(name: N) -> CookieBuilder
145    where
146        N: Into<Cow<'static, str>>,
147    {
148        Self::build(name, "")
149    }
150
151    /// Returns the cookie name.
152    #[inline]
153    pub fn name(&self) -> &str {
154        self.name.as_str(self.raw_value.as_deref())
155    }
156
157    /// Set the cookie name.
158    #[inline]
159    pub fn set_name<N: Into<Cow<'static, str>>>(&mut self, name: N) {
160        self.name = TinyStr::from(name)
161    }
162
163    /// Get the cookie value. This does not trim `"` characters.
164    #[inline]
165    pub fn value(&self) -> &str {
166        self.value.as_str(self.raw_value.as_deref())
167    }
168
169    /// Set the cookie value.
170    #[inline]
171    pub fn set_value<V: Into<Cow<'static, str>>>(&mut self, value: V) {
172        self.value = TinyStr::from(value)
173    }
174
175    /// Set the Expired attribute.
176    #[inline]
177    pub fn set_expires<E: Into<Expires>>(&mut self, expires: E) {
178        self.expires = expires.into();
179    }
180
181    /// Get the Max-Age duration. This returns a [`std::time::Duration`].
182    ///
183    /// If you'd like a `time`, `chrono` or `jiff` specific duration use the
184    /// `max_age_{time,chrono,jiff}` methods.
185    #[inline]
186    pub fn max_age(&self) -> Option<Duration> {
187        self.max_age.map(Duration::from_secs)
188    }
189
190    /// Get the Max-Age as seconds.
191    #[inline]
192    pub fn max_age_secs(&self) -> Option<u64> {
193        self.max_age
194    }
195
196    /// Set the Max-Age attribute.
197    #[inline]
198    pub fn set_max_age(&mut self, max_age: Duration) {
199        self.set_max_age_secs(max_age.as_secs());
200    }
201
202    /// Set the Max-Age value in seconds.
203    #[inline]
204    pub fn set_max_age_secs(&mut self, max_age_secs: u64) {
205        self.max_age = Some(max_age_secs);
206    }
207
208    /// Removes the Max-Age attribute.
209    #[inline]
210    pub fn unset_max_age(&mut self) {
211        self.max_age = None;
212    }
213
214    /// Returns the Domain attribute if it's set.
215    #[inline]
216    pub fn domain(&self) -> Option<&str> {
217        self.domain
218            .as_ref()
219            .map(|s| s.as_str(self.raw_value.as_deref()))
220    }
221
222    pub(crate) fn domain_sanitized(&self) -> Option<&str> {
223        self.domain().map(|d| d.strip_prefix('.').unwrap_or(d))
224    }
225
226    /// Set the Domain attribute.
227    #[inline]
228    pub fn set_domain<D: Into<Cow<'static, str>>>(&mut self, domain: D) {
229        self.domain = Some(TinyStr::from(domain))
230    }
231
232    /// Removes the Domain attribute.
233    #[inline]
234    pub fn unset_domain(&mut self) {
235        self.domain = None
236    }
237
238    /// Returns the Path attribute if it's set.
239    #[inline]
240    pub fn path(&self) -> Option<&str> {
241        self.path
242            .as_ref()
243            .map(|val| val.as_str(self.raw_value.as_deref()))
244    }
245
246    /// Set the Path attribute.
247    #[inline]
248    pub fn set_path<D: Into<Cow<'static, str>>>(&mut self, path: D) {
249        self.path = Some(TinyStr::from(path))
250    }
251
252    /// Removes the path attribute.
253    #[inline]
254    pub fn unset_path(&mut self) {
255        self.path = None
256    }
257
258    /// Returns if the Secure attribute is set.
259    #[inline]
260    pub fn secure(&self) -> bool {
261        self.secure
262    }
263
264    /// Sets the Secure attribute of the cookie.
265    #[inline]
266    pub fn set_secure(&mut self, secure: bool) {
267        self.secure = secure
268    }
269
270    /// Returns if the HttpOnly attribute is set.
271    #[inline]
272    pub fn http_only(&self) -> bool {
273        self.http_only
274    }
275
276    /// Sets the HttpOnly attribute of the cookie.
277    #[inline]
278    pub fn set_http_only(&mut self, http_only: bool) {
279        self.http_only = http_only
280    }
281
282    /// Returns if the Partitioned attribute is set.
283    #[inline]
284    pub fn partitioned(&self) -> bool {
285        self.partitioned
286    }
287
288    /// Set the Partitioned flag, enabling the Partitioned attribute also enables the Secure Attribute.
289    #[inline]
290    pub fn set_partitioned(&mut self, partitioned: bool) {
291        self.partitioned = partitioned;
292    }
293
294    /// Returns the SameSite attribute if it is set.
295    #[inline]
296    pub fn same_site(&self) -> Option<SameSite> {
297        self.same_site
298    }
299
300    /// Set the SameSite attribute.
301    #[inline]
302    pub fn set_same_site<S: Into<Option<SameSite>>>(&mut self, same_site: S) {
303        self.same_site = same_site.into();
304    }
305}
306
307impl fmt::Display for Cookie {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        write!(f, "{:?}", self)
310    }
311}
312
313impl Debug for Cookie {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        let mut debug = f.debug_struct("Cookie");
316
317        debug
318            .field("name", &self.name())
319            .field("value", &self.value())
320            .field("max_age", &self.max_age())
321            .field("domain", &self.domain())
322            .field("path", &self.path())
323            .field("secure", &self.secure())
324            .field("http_only", &self.http_only())
325            .field("partitioned", &self.partitioned())
326            .field("expires", &self.expires)
327            .finish()
328    }
329}
330
331impl PartialEq<Cookie> for Cookie {
332    fn eq(&self, other: &Cookie) -> bool {
333        if self.name() != other.name()
334            || self.value() != other.value()
335            || self.secure() != other.secure()
336            || self.http_only() != other.http_only()
337            || self.partitioned() != other.partitioned()
338            || self.max_age() != other.max_age()
339            || self.same_site() != other.same_site()
340            || self.expires != other.expires
341        {
342            return false;
343        }
344
345        if !opt_str_eq(self.domain_sanitized(), other.domain_sanitized()) {
346            return false;
347        }
348
349        if !opt_str_eq(self.path(), other.path()) {
350            return false;
351        }
352
353        true
354    }
355}
356
357fn opt_str_eq(left: Option<&str>, right: Option<&str>) -> bool {
358    match (left, right) {
359        (None, None) => true,
360        (Some(l), Some(r)) => l.eq_ignore_ascii_case(r),
361        _ => false,
362    }
363}