Skip to main content

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