cookie_store/
cookie_store.rs

1use std::io::{BufRead, Write};
2use std::ops::Deref;
3
4use cookie::Cookie as RawCookie;
5use log::debug;
6use url::Url;
7
8use crate::cookie::Cookie;
9use crate::cookie_domain::is_match as domain_match;
10use crate::cookie_path::is_match as path_match;
11use crate::utils::{is_http_scheme, is_secure};
12use crate::CookieError;
13
14#[cfg(feature = "preserve_order")]
15use indexmap::IndexMap;
16#[cfg(not(feature = "preserve_order"))]
17use std::collections::HashMap;
18#[cfg(feature = "preserve_order")]
19type Map<K, V> = IndexMap<K, V>;
20#[cfg(not(feature = "preserve_order"))]
21type Map<K, V> = HashMap<K, V>;
22
23type NameMap = Map<String, Cookie<'static>>;
24type PathMap = Map<String, NameMap>;
25type DomainMap = Map<String, PathMap>;
26
27#[derive(PartialEq, Clone, Debug, Eq)]
28pub enum StoreAction {
29    /// The `Cookie` was successfully added to the store
30    Inserted,
31    /// The `Cookie` successfully expired a `Cookie` already in the store
32    ExpiredExisting,
33    /// The `Cookie` was added to the store, replacing an existing entry
34    UpdatedExisting,
35}
36
37pub type StoreResult<T> = Result<T, crate::Error>;
38pub type InsertResult = Result<StoreAction, CookieError>;
39
40#[derive(Debug, Default, Clone)]
41/// An implementation for storing and retrieving [`Cookie`]s per the path and domain matching
42/// rules specified in [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265).
43pub struct CookieStore {
44    /// Cookies stored by domain, path, then name
45    cookies: DomainMap,
46    #[cfg(feature = "public_suffix")]
47    /// If set, enables [public suffix](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) rejection based on the provided `publicsuffix::List`
48    public_suffix_list: Option<publicsuffix::List>,
49}
50
51impl CookieStore {
52    #[deprecated(
53        since = "0.14.1",
54        note = "Please use the `get_request_values` function instead"
55    )]
56    /// Return an `Iterator` of the cookies for `url` in the store, suitable for submitting in an
57    /// HTTP request. As the items are intended for use in creating a `Cookie` header in a GET request,
58    /// they may contain only the `name` and `value` of a received cookie, eliding other parameters
59    /// such as `path` or `expires`. For iteration over `Cookie` instances containing all data, please
60    /// refer to [`CookieStore::matches`].
61    pub fn get_request_cookies(&self, url: &Url) -> impl Iterator<Item = &RawCookie<'static>> {
62        self.matches(url).into_iter().map(|c| c.deref())
63    }
64
65    /// Return an `Iterator` of the cookie (`name`, `value`) pairs for `url` in the store, suitable
66    /// for use in the `Cookie` header of an HTTP request. For iteration over `Cookie` instances,
67    /// please refer to [`CookieStore::matches`].
68    pub fn get_request_values(&self, url: &Url) -> impl Iterator<Item = (&str, &str)> {
69        self.matches(url).into_iter().map(|c| c.name_value())
70    }
71
72    /// Store the `cookies` received from `url`
73    pub fn store_response_cookies<I: Iterator<Item = RawCookie<'static>>>(
74        &mut self,
75        cookies: I,
76        url: &Url,
77    ) {
78        for cookie in cookies {
79            if cookie.secure() != Some(true) || cfg!(feature = "log_secure_cookie_values") {
80                debug!("inserting Set-Cookie '{:?}'", cookie);
81            } else {
82                debug!("inserting secure cookie '{}'", cookie.name());
83            }
84
85            if let Err(e) = self.insert_raw(&cookie, url) {
86                debug!("unable to store Set-Cookie: {:?}", e);
87            }
88        }
89    }
90
91    /// Specify a `publicsuffix::List` for the `CookieStore` to allow [public suffix
92    /// matching](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3)
93    #[cfg(feature = "public_suffix")]
94    pub fn with_suffix_list(self, psl: publicsuffix::List) -> CookieStore {
95        CookieStore {
96            cookies: self.cookies,
97            public_suffix_list: Some(psl),
98        }
99    }
100
101    /// Returns true if the `CookieStore` contains an __unexpired__ `Cookie` corresponding to the
102    /// specified `domain`, `path`, and `name`.
103    pub fn contains(&self, domain: &str, path: &str, name: &str) -> bool {
104        self.get(domain, path, name).is_some()
105    }
106
107    /// Returns true if the `CookieStore` contains any (even an __expired__) `Cookie` corresponding
108    /// to the specified `domain`, `path`, and `name`.
109    pub fn contains_any(&self, domain: &str, path: &str, name: &str) -> bool {
110        self.get_any(domain, path, name).is_some()
111    }
112
113    /// Returns a reference to the __unexpired__ `Cookie` corresponding to the specified `domain`,
114    /// `path`, and `name`.
115    pub fn get(&self, domain: &str, path: &str, name: &str) -> Option<&Cookie<'_>> {
116        self.get_any(domain, path, name).and_then(|cookie| {
117            if cookie.is_expired() {
118                None
119            } else {
120                Some(cookie)
121            }
122        })
123    }
124
125    /// Returns a mutable reference to the __unexpired__ `Cookie` corresponding to the specified
126    /// `domain`, `path`, and `name`.
127    fn get_mut(&mut self, domain: &str, path: &str, name: &str) -> Option<&mut Cookie<'static>> {
128        self.get_mut_any(domain, path, name).and_then(|cookie| {
129            if cookie.is_expired() {
130                None
131            } else {
132                Some(cookie)
133            }
134        })
135    }
136
137    /// Returns a reference to the (possibly __expired__) `Cookie` corresponding to the specified
138    /// `domain`, `path`, and `name`.
139    pub fn get_any(&self, domain: &str, path: &str, name: &str) -> Option<&Cookie<'static>> {
140        self.cookies.get(domain).and_then(|domain_cookies| {
141            domain_cookies
142                .get(path)
143                .and_then(|path_cookies| path_cookies.get(name))
144        })
145    }
146
147    /// Returns a mutable reference to the (possibly __expired__) `Cookie` corresponding to the
148    /// specified `domain`, `path`, and `name`.
149    fn get_mut_any(
150        &mut self,
151        domain: &str,
152        path: &str,
153        name: &str,
154    ) -> Option<&mut Cookie<'static>> {
155        self.cookies.get_mut(domain).and_then(|domain_cookies| {
156            domain_cookies
157                .get_mut(path)
158                .and_then(|path_cookies| path_cookies.get_mut(name))
159        })
160    }
161
162    /// Removes a `Cookie` from the store, returning the `Cookie` if it was in the store
163    pub fn remove(&mut self, domain: &str, path: &str, name: &str) -> Option<Cookie<'static>> {
164        #[cfg(not(feature = "preserve_order"))]
165        fn map_remove<K, V, Q>(map: &mut Map<K, V>, key: &Q) -> Option<V>
166        where
167            K: std::borrow::Borrow<Q> + std::cmp::Eq + std::hash::Hash,
168            Q: std::cmp::Eq + std::hash::Hash + ?Sized,
169        {
170            map.remove(key)
171        }
172        #[cfg(feature = "preserve_order")]
173        fn map_remove<K, V, Q>(map: &mut Map<K, V>, key: &Q) -> Option<V>
174        where
175            K: std::borrow::Borrow<Q> + std::cmp::Eq + std::hash::Hash,
176            Q: std::cmp::Eq + std::hash::Hash + ?Sized,
177        {
178            map.shift_remove(key)
179        }
180
181        let (removed, remove_domain) = match self.cookies.get_mut(domain) {
182            None => (None, false),
183            Some(domain_cookies) => {
184                let (removed, remove_path) = match domain_cookies.get_mut(path) {
185                    None => (None, false),
186                    Some(path_cookies) => {
187                        let removed = map_remove(path_cookies, name);
188                        (removed, path_cookies.is_empty())
189                    }
190                };
191
192                if remove_path {
193                    map_remove(domain_cookies, path);
194                    (removed, domain_cookies.is_empty())
195                } else {
196                    (removed, false)
197                }
198            }
199        };
200
201        if remove_domain {
202            map_remove(&mut self.cookies, domain);
203        }
204
205        removed
206    }
207
208    /// Returns a collection of references to __unexpired__ cookies that path- and domain-match
209    /// `request_url`, as well as having HttpOnly and Secure attributes compatible with the
210    /// `request_url`.
211    pub fn matches(&self, request_url: &Url) -> Vec<&Cookie<'static>> {
212        // although we domain_match and path_match as we descend through the tree, we
213        // still need to
214        // do a full Cookie::matches() check in the last filter. Otherwise, we cannot
215        // properly deal
216        // with HostOnly Cookies.
217        let cookies = self
218            .cookies
219            .iter()
220            .filter(|&(d, _)| domain_match(d, request_url))
221            .flat_map(|(_, dcs)| {
222                dcs.iter()
223                    .filter(|&(p, _)| path_match(p, request_url))
224                    .flat_map(|(_, pcs)| {
225                        pcs.values()
226                            .filter(|c| !c.is_expired() && c.matches(request_url))
227                    })
228            });
229        match (!is_http_scheme(request_url), !is_secure(request_url)) {
230            (true, true) => cookies
231                .filter(|c| !c.http_only().unwrap_or(false) && !c.secure().unwrap_or(false))
232                .collect(),
233            (true, false) => cookies
234                .filter(|c| !c.http_only().unwrap_or(false))
235                .collect(),
236            (false, true) => cookies.filter(|c| !c.secure().unwrap_or(false)).collect(),
237            (false, false) => cookies.collect(),
238        }
239    }
240
241    /// Parses a new `Cookie` from `cookie_str` and inserts it into the store.
242    pub fn parse(&mut self, cookie_str: &str, request_url: &Url) -> InsertResult {
243        Cookie::parse(cookie_str, request_url)
244            .and_then(|cookie| self.insert(cookie.into_owned(), request_url))
245    }
246
247    /// Converts a `cookie::Cookie` (from the `cookie` crate) into a `cookie_store::Cookie` and
248    /// inserts it into the store.
249    pub fn insert_raw(&mut self, cookie: &RawCookie<'_>, request_url: &Url) -> InsertResult {
250        Cookie::try_from_raw_cookie(cookie, request_url)
251            .and_then(|cookie| self.insert(cookie.into_owned(), request_url))
252    }
253
254    /// Inserts `cookie`, received from `request_url`, into the store, following the rules of the
255    /// [IETF RFC6265 Storage Model](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3). If the
256    /// `Cookie` is __unexpired__ and is successfully inserted, returns
257    /// `Ok(StoreAction::Inserted)`. If the `Cookie` is __expired__ *and* matches an existing
258    /// `Cookie` in the store, the existing `Cookie` wil be `expired()` and
259    /// `Ok(StoreAction::ExpiredExisting)` will be returned.
260    pub fn insert(&mut self, cookie: Cookie<'static>, request_url: &Url) -> InsertResult {
261        if cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) {
262            // If the cookie was received from a "non-HTTP" API and the
263            // cookie's http-only-flag is set, abort these steps and ignore the
264            // cookie entirely.
265            return Err(CookieError::NonHttpScheme);
266        }
267        #[cfg(feature = "public_suffix")]
268        let mut cookie = cookie;
269        #[cfg(feature = "public_suffix")]
270        if let Some(ref psl) = self.public_suffix_list {
271            // If the user agent is configured to reject "public suffixes"
272            if cookie.domain.is_public_suffix(psl) {
273                // and the domain-attribute is a public suffix:
274                if cookie.domain.host_is_identical(request_url) {
275                    //   If the domain-attribute is identical to the canonicalized
276                    //   request-host:
277                    //     Let the domain-attribute be the empty string.
278                    // (NB: at this point, an empty domain-attribute should be represented
279                    // as the HostOnly variant of CookieDomain)
280                    cookie.domain = crate::cookie_domain::CookieDomain::host_only(request_url)?;
281                } else {
282                    //   Otherwise:
283                    //     Ignore the cookie entirely and abort these steps.
284                    return Err(CookieError::PublicSuffix);
285                }
286            }
287        }
288        if !cookie.domain.matches(request_url) {
289            // If the canonicalized request-host does not domain-match the
290            // domain-attribute:
291            //    Ignore the cookie entirely and abort these steps.
292            return Err(CookieError::DomainMismatch);
293        }
294        // NB: we do not bail out above on is_expired(), as servers can remove a cookie
295        // by sending
296        // an expired one, so we need to do the old_cookie check below before checking
297        // is_expired() on an incoming cookie
298
299        {
300            // At this point in parsing, any non-present Domain attribute should have been
301            // converted into a HostOnly variant
302            let cookie_domain = cookie
303                .domain
304                .as_cow()
305                .ok_or_else(|| CookieError::UnspecifiedDomain)?;
306            if let Some(old_cookie) = self.get_mut(&cookie_domain, &cookie.path, cookie.name()) {
307                if old_cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) {
308                    // 2.  If the newly created cookie was received from a "non-HTTP"
309                    //    API and the old-cookie's http-only-flag is set, abort these
310                    //    steps and ignore the newly created cookie entirely.
311                    return Err(CookieError::NonHttpScheme);
312                } else if cookie.is_expired() {
313                    old_cookie.expire();
314                    return Ok(StoreAction::ExpiredExisting);
315                }
316            }
317        }
318
319        if !cookie.is_expired() {
320            Ok(
321                if self
322                    .cookies
323                    .entry(String::from(&cookie.domain))
324                    .or_insert_with(Map::new)
325                    .entry(String::from(&cookie.path))
326                    .or_insert_with(Map::new)
327                    .insert(cookie.name().to_owned(), cookie)
328                    .is_none()
329                {
330                    StoreAction::Inserted
331                } else {
332                    StoreAction::UpdatedExisting
333                },
334            )
335        } else {
336            Err(CookieError::Expired)
337        }
338    }
339
340    /// Clear the contents of the store
341    pub fn clear(&mut self) {
342        self.cookies.clear()
343    }
344
345    /// An iterator visiting all the __unexpired__ cookies in the store
346    pub fn iter_unexpired<'a>(&'a self) -> impl Iterator<Item = &'a Cookie<'static>> + 'a {
347        self.cookies
348            .values()
349            .flat_map(|dcs| dcs.values())
350            .flat_map(|pcs| pcs.values())
351            .filter(|c| !c.is_expired())
352    }
353
354    /// An iterator visiting all (including __expired__) cookies in the store
355    pub fn iter_any<'a>(&'a self) -> impl Iterator<Item = &'a Cookie<'static>> + 'a {
356        self.cookies
357            .values()
358            .flat_map(|dcs| dcs.values())
359            .flat_map(|pcs| pcs.values())
360    }
361
362    /// Serialize any __unexpired__ and __persistent__ cookies in the store with `cookie_to_string`
363    /// and write them to `writer`
364    pub fn save<W, E, F>(&self, writer: &mut W, cookie_to_string: F) -> StoreResult<()>
365    where
366        W: Write,
367        F: Fn(&Cookie<'static>) -> Result<String, E>,
368        crate::Error: From<E>,
369    {
370        for cookie in self.iter_unexpired().filter_map(|c| {
371            if c.is_persistent() {
372                Some(cookie_to_string(c))
373            } else {
374                None
375            }
376        }) {
377            writeln!(writer, "{}", cookie?)?;
378        }
379        Ok(())
380    }
381
382    /// Serialize all (including __expired__ and __non-persistent__) cookies in the store with `cookie_to_string` and write them to `writer`
383    pub fn save_incl_expired_and_nonpersistent<W, E, F>(
384        &self,
385        writer: &mut W,
386        cookie_to_string: F,
387    ) -> StoreResult<()>
388    where
389        W: Write,
390        F: Fn(&Cookie<'static>) -> Result<String, E>,
391        crate::Error: From<E>,
392    {
393        for cookie in self.iter_any() {
394            writeln!(writer, "{}", cookie_to_string(cookie)?)?;
395        }
396        Ok(())
397    }
398
399    /// Load cookies from `reader`, deserializing with `cookie_from_str`, skipping any __expired__
400    /// cookies
401    pub fn load<R, E, F>(reader: R, cookie_from_str: F) -> StoreResult<CookieStore>
402    where
403        R: BufRead,
404        F: Fn(&str) -> Result<Cookie<'static>, E>,
405        crate::Error: From<E>,
406    {
407        CookieStore::load_from(reader, cookie_from_str, false)
408    }
409
410    /// Load cookies from `reader`, deserializing with `cookie_from_str`, loading both __unexpired__
411    /// and __expired__ cookies
412    pub fn load_all<R, E, F>(reader: R, cookie_from_str: F) -> StoreResult<CookieStore>
413    where
414        R: BufRead,
415        F: Fn(&str) -> Result<Cookie<'static>, E>,
416        crate::Error: From<E>,
417    {
418        CookieStore::load_from(reader, cookie_from_str, true)
419    }
420
421    fn load_from<R, E, F>(
422        reader: R,
423        cookie_from_str: F,
424        include_expired: bool,
425    ) -> StoreResult<CookieStore>
426    where
427        R: BufRead,
428        F: Fn(&str) -> Result<Cookie<'static>, E>,
429        crate::Error: From<E>,
430    {
431        let cookies = reader.lines().map(|line_result| {
432            line_result
433                .map_err(Into::into)
434                .and_then(|line| cookie_from_str(&line).map_err(crate::Error::from))
435        });
436        Self::from_cookies(cookies, include_expired)
437    }
438
439    /// Create a `CookieStore` from an iterator of `Cookie` values. When
440    /// `include_expired` is `true`, both __expired__ and __unexpired__ cookies in the incoming
441    /// iterator will be included in the produced `CookieStore`; otherwise, only
442    /// __unexpired__ cookies will be included, and __expired__ cookies filtered
443    /// out.
444    pub fn from_cookies<I, E>(iter: I, include_expired: bool) -> Result<Self, E>
445    where
446        I: IntoIterator<Item = Result<Cookie<'static>, E>>,
447    {
448        let mut cookies = Map::new();
449        for cookie in iter {
450            let cookie = cookie?;
451            if include_expired || !cookie.is_expired() {
452                cookies
453                    .entry(String::from(&cookie.domain))
454                    .or_insert_with(Map::new)
455                    .entry(String::from(&cookie.path))
456                    .or_insert_with(Map::new)
457                    .insert(cookie.name().to_owned(), cookie);
458            }
459        }
460        Ok(Self {
461            cookies,
462            #[cfg(feature = "public_suffix")]
463            public_suffix_list: None,
464        })
465    }
466
467    pub fn new() -> Self {
468        Self {
469            cookies: DomainMap::new(),
470            #[cfg(feature = "public_suffix")]
471            public_suffix_list: None,
472        }
473    }
474
475    #[cfg(feature = "public_suffix")]
476    pub fn new_with_public_suffix(public_suffix_list: Option<publicsuffix::List>) -> Self {
477        Self {
478            cookies: DomainMap::new(),
479            public_suffix_list,
480        }
481    }
482}
483
484#[cfg(feature = "serde_json")]
485/// Legacy serialization implementations. These methods do **not** produce/consume valid JSON output compatible with
486/// typical JSON libraries/tools.
487impl CookieStore {
488    /// Serialize any __unexpired__ and __persistent__ cookies in the store to JSON format and
489    /// write them to `writer`
490    ///
491    /// __NB__: this method does not produce valid JSON which can be directly loaded; such output
492    /// must be loaded via the corresponding method [CookieStore::load_json]. For a more
493    /// robust/universal
494    /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this
495    /// method.
496    #[deprecated(
497        since = "0.22.0",
498        note = "See `cookie_store::serde` modules for more robust de/serialization options"
499    )]
500    pub fn save_json<W: Write>(&self, writer: &mut W) -> StoreResult<()> {
501        self.save(writer, ::serde_json::to_string)
502    }
503
504    /// Serialize all (including __expired__ and __non-persistent__) cookies in the store to JSON format and write them to `writer`
505    ///
506    /// __NB__: this method does not produce valid JSON which can be directly loaded; such output
507    /// must be loaded via the corresponding method [CookieStore::load_json]. For a more
508    /// robust/universal
509    /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this
510    /// method.
511    #[deprecated(
512        since = "0.22.0",
513        note = "See `cookie_store::serde` modules for more robust de/serialization options"
514    )]
515    pub fn save_incl_expired_and_nonpersistent_json<W: Write>(
516        &self,
517        writer: &mut W,
518    ) -> StoreResult<()> {
519        self.save_incl_expired_and_nonpersistent(writer, ::serde_json::to_string)
520    }
521
522    /// Load JSON-formatted cookies from `reader`, skipping any __expired__ cookies
523    ///
524    /// __NB__: this method does not expect true valid JSON; it is designed to load output
525    /// from the corresponding method [CookieStore::save_json]. For a more robust/universal
526    /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this
527    /// method.
528    #[deprecated(
529        since = "0.22.0",
530        note = "See `cookie_store::serde` modules for more robust de/serialization options"
531    )]
532    pub fn load_json<R: BufRead>(reader: R) -> StoreResult<CookieStore> {
533        CookieStore::load(reader, |cookie| ::serde_json::from_str(cookie))
534    }
535
536    /// Load JSON-formatted cookies from `reader`, loading both __expired__ and __unexpired__ cookies
537    ///
538    /// __NB__: this method does not expect true valid JSON; it is designed to load output
539    /// from the corresponding method [CookieStore::save_json]. For a more robust/universal
540    /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this
541    /// method.
542    #[deprecated(
543        since = "0.22.0",
544        note = "See `cookie_store::serde` modules for more robust de/serialization options"
545    )]
546    pub fn load_json_all<R: BufRead>(reader: R) -> StoreResult<CookieStore> {
547        CookieStore::load_all(reader, |cookie| ::serde_json::from_str(cookie))
548    }
549}
550
551#[cfg(feature = "serde")]
552/// Legacy de/serialization implementation which elides the collection-nature of the contained
553/// cookies. Suitable for line-oriented cookie persistence, but prefer/consider
554/// `cookie_store::serde` modules for more universally consumable serialization formats.
555mod serde_legacy {
556    use serde::de::{SeqAccess, Visitor};
557    use serde::{Deserialize, Deserializer, Serialize, Serializer};
558
559    impl Serialize for super::CookieStore {
560        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
561        where
562            S: Serializer,
563        {
564            serializer.collect_seq(self.iter_unexpired().filter(|c| c.is_persistent()))
565        }
566    }
567
568    struct CookieStoreVisitor;
569
570    impl<'de> Visitor<'de> for CookieStoreVisitor {
571        type Value = super::CookieStore;
572
573        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574            write!(formatter, "a sequence of cookies")
575        }
576
577        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
578        where
579            A: SeqAccess<'de>,
580        {
581            super::CookieStore::from_cookies(
582                std::iter::from_fn(|| seq.next_element().transpose()),
583                false,
584            )
585        }
586    }
587
588    impl<'de> Deserialize<'de> for super::CookieStore {
589        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
590        where
591            D: Deserializer<'de>,
592        {
593            deserializer.deserialize_seq(CookieStoreVisitor)
594        }
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::CookieStore;
601    use super::{InsertResult, StoreAction};
602    use crate::cookie::Cookie;
603    use crate::CookieError;
604    use ::cookie::Cookie as RawCookie;
605    use time::OffsetDateTime;
606
607    use crate::utils::test as test_utils;
608
609    macro_rules! inserted {
610        ($e: expr) => {
611            assert_eq!(Ok(StoreAction::Inserted), $e)
612        };
613    }
614    macro_rules! updated {
615        ($e: expr) => {
616            assert_eq!(Ok(StoreAction::UpdatedExisting), $e)
617        };
618    }
619    macro_rules! expired_existing {
620        ($e: expr) => {
621            assert_eq!(Ok(StoreAction::ExpiredExisting), $e)
622        };
623    }
624    macro_rules! domain_mismatch {
625        ($e: expr) => {
626            assert_eq!(Err(CookieError::DomainMismatch), $e)
627        };
628    }
629    macro_rules! non_http_scheme {
630        ($e: expr) => {
631            assert_eq!(Err(CookieError::NonHttpScheme), $e)
632        };
633    }
634    macro_rules! non_rel_scheme {
635        ($e: expr) => {
636            assert_eq!(Err(CookieError::NonRelativeScheme), $e)
637        };
638    }
639    macro_rules! expired_err {
640        ($e: expr) => {
641            assert_eq!(Err(CookieError::Expired), $e)
642        };
643    }
644    macro_rules! values_are {
645        ($store: expr, $url: expr, $values: expr) => {{
646            let mut matched_values = $store
647                .matches(&test_utils::url($url))
648                .iter()
649                .map(|c| &c.value()[..])
650                .collect::<Vec<_>>();
651            matched_values.sort();
652
653            let mut values: Vec<&str> = $values;
654            values.sort();
655
656            assert!(
657                matched_values == values,
658                "\n{:?}\n!=\n{:?}\n",
659                matched_values,
660                values
661            );
662        }};
663    }
664
665    fn add_cookie(
666        store: &mut CookieStore,
667        cookie: &str,
668        url: &str,
669        expires: Option<OffsetDateTime>,
670        max_age: Option<u64>,
671    ) -> InsertResult {
672        store.insert(
673            test_utils::make_cookie(cookie, url, expires, max_age),
674            &test_utils::url(url),
675        )
676    }
677
678    fn make_match_store() -> CookieStore {
679        let mut store = CookieStore::default();
680        inserted!(add_cookie(
681            &mut store,
682            "cookie1=1",
683            "http://example.com/foo/bar",
684            None,
685            Some(60 * 5),
686        ));
687        inserted!(add_cookie(
688            &mut store,
689            "cookie2=2; Secure",
690            "https://example.com/sec/",
691            None,
692            Some(60 * 5),
693        ));
694        inserted!(add_cookie(
695            &mut store,
696            "cookie3=3; HttpOnly",
697            "https://example.com/sec/",
698            None,
699            Some(60 * 5),
700        ));
701        inserted!(add_cookie(
702            &mut store,
703            "cookie4=4; Secure; HttpOnly",
704            "https://example.com/sec/",
705            None,
706            Some(60 * 5),
707        ));
708        inserted!(add_cookie(
709            &mut store,
710            "cookie5=5",
711            "http://example.com/foo/",
712            None,
713            Some(60 * 5),
714        ));
715        inserted!(add_cookie(
716            &mut store,
717            "cookie6=6",
718            "http://example.com/",
719            None,
720            Some(60 * 5),
721        ));
722        inserted!(add_cookie(
723            &mut store,
724            "cookie7=7",
725            "http://bar.example.com/foo/",
726            None,
727            Some(60 * 5),
728        ));
729
730        inserted!(add_cookie(
731            &mut store,
732            "cookie8=8",
733            "http://example.org/foo/bar",
734            None,
735            Some(60 * 5),
736        ));
737        inserted!(add_cookie(
738            &mut store,
739            "cookie9=9",
740            "http://bar.example.org/foo/bar",
741            None,
742            Some(60 * 5),
743        ));
744        store
745    }
746
747    macro_rules! check_matches {
748        ($store: expr) => {{
749            values_are!($store, "http://unknowndomain.org/foo/bar", vec![]);
750            values_are!($store, "http://example.org/foo/bar", vec!["8"]);
751            values_are!($store, "http://example.org/bus/bar", vec![]);
752            values_are!($store, "http://bar.example.org/foo/bar", vec!["9"]);
753            values_are!($store, "http://bar.example.org/bus/bar", vec![]);
754            values_are!(
755                $store,
756                "https://example.com/sec/foo",
757                vec!["6", "4", "3", "2"]
758            );
759            values_are!($store, "http://example.com/sec/foo", vec!["6", "3"]);
760            values_are!($store, "ftp://example.com/sec/foo", vec!["6"]);
761            values_are!($store, "http://bar.example.com/foo/bar/bus", vec!["7"]);
762            values_are!(
763                $store,
764                "http://example.com/foo/bar/bus",
765                vec!["1", "5", "6"]
766            );
767        }};
768    }
769
770    #[test]
771    fn insert_raw() {
772        let mut store = CookieStore::default();
773        inserted!(store.insert_raw(
774            &RawCookie::parse("cookie1=value1").unwrap(),
775            &test_utils::url("http://example.com/foo/bar"),
776        ));
777        non_rel_scheme!(store.insert_raw(
778            &RawCookie::parse("cookie1=value1").unwrap(),
779            &test_utils::url("data:nonrelativescheme"),
780        ));
781        non_http_scheme!(store.insert_raw(
782            &RawCookie::parse("cookie1=value1; HttpOnly").unwrap(),
783            &test_utils::url("ftp://example.com/"),
784        ));
785        expired_existing!(store.insert_raw(
786            &RawCookie::parse("cookie1=value1; Max-Age=0").unwrap(),
787            &test_utils::url("http://example.com/foo/bar"),
788        ));
789        expired_err!(store.insert_raw(
790            &RawCookie::parse("cookie1=value1; Max-Age=-1").unwrap(),
791            &test_utils::url("http://example.com/foo/bar"),
792        ));
793        updated!(store.insert_raw(
794            &RawCookie::parse("cookie1=value1").unwrap(),
795            &test_utils::url("http://example.com/foo/bar"),
796        ));
797        expired_existing!(store.insert_raw(
798            &RawCookie::parse("cookie1=value1; Max-Age=-1").unwrap(),
799            &test_utils::url("http://example.com/foo/bar"),
800        ));
801        domain_mismatch!(store.insert_raw(
802            &RawCookie::parse("cookie1=value1; Domain=bar.example.com").unwrap(),
803            &test_utils::url("http://example.com/foo/bar"),
804        ));
805    }
806
807    #[test]
808    fn parse() {
809        let mut store = CookieStore::default();
810        inserted!(store.parse(
811            "cookie1=value1",
812            &test_utils::url("http://example.com/foo/bar"),
813        ));
814        non_rel_scheme!(store.parse("cookie1=value1", &test_utils::url("data:nonrelativescheme"),));
815        non_http_scheme!(store.parse(
816            "cookie1=value1; HttpOnly",
817            &test_utils::url("ftp://example.com/"),
818        ));
819        expired_existing!(store.parse(
820            "cookie1=value1; Max-Age=0",
821            &test_utils::url("http://example.com/foo/bar"),
822        ));
823        expired_err!(store.parse(
824            "cookie1=value1; Max-Age=-1",
825            &test_utils::url("http://example.com/foo/bar"),
826        ));
827        updated!(store.parse(
828            "cookie1=value1",
829            &test_utils::url("http://example.com/foo/bar"),
830        ));
831        expired_existing!(store.parse(
832            "cookie1=value1; Max-Age=-1",
833            &test_utils::url("http://example.com/foo/bar"),
834        ));
835        domain_mismatch!(store.parse(
836            "cookie1=value1; Domain=bar.example.com",
837            &test_utils::url("http://example.com/foo/bar"),
838        ));
839    }
840
841    #[test]
842    fn domains() {
843        let mut store = CookieStore::default();
844        //        The user agent will reject cookies unless the Domain attribute
845        // specifies a scope for the cookie that would include the origin
846        // server.  For example, the user agent will accept a cookie with a
847        // Domain attribute of "example.com" or of "foo.example.com" from
848        // foo.example.com, but the user agent will not accept a cookie with a
849        // Domain attribute of "bar.example.com" or of "baz.foo.example.com".
850        fn domain_cookie_from(domain: &str, request_url: &str) -> Cookie<'static> {
851            let cookie_str = format!("cookie1=value1; Domain={}", domain);
852            Cookie::parse(cookie_str, &test_utils::url(request_url)).unwrap()
853        }
854
855        {
856            let request_url = test_utils::url("http://foo.example.com");
857            // foo.example.com can submit cookies for example.com and foo.example.com
858            inserted!(store.insert(
859                domain_cookie_from("example.com", "http://foo.example.com",),
860                &request_url,
861            ));
862            updated!(store.insert(
863                domain_cookie_from(".example.com", "http://foo.example.com",),
864                &request_url,
865            ));
866            inserted!(store.insert(
867                domain_cookie_from("foo.example.com", "http://foo.example.com",),
868                &request_url,
869            ));
870            updated!(store.insert(
871                domain_cookie_from(".foo.example.com", "http://foo.example.com",),
872                &request_url,
873            ));
874            // not for bar.example.com
875            domain_mismatch!(store.insert(
876                domain_cookie_from("bar.example.com", "http://bar.example.com",),
877                &request_url,
878            ));
879            domain_mismatch!(store.insert(
880                domain_cookie_from(".bar.example.com", "http://bar.example.com",),
881                &request_url,
882            ));
883            // not for bar.foo.example.com
884            domain_mismatch!(store.insert(
885                domain_cookie_from("bar.foo.example.com", "http://bar.foo.example.com",),
886                &request_url,
887            ));
888            domain_mismatch!(store.insert(
889                domain_cookie_from(".bar.foo.example.com", "http://bar.foo.example.com",),
890                &request_url,
891            ));
892        }
893
894        {
895            let request_url = test_utils::url("http://bar.example.com");
896            // bar.example.com can submit for example.com and bar.example.com
897            updated!(store.insert(
898                domain_cookie_from("example.com", "http://foo.example.com",),
899                &request_url,
900            ));
901            updated!(store.insert(
902                domain_cookie_from(".example.com", "http://foo.example.com",),
903                &request_url,
904            ));
905            inserted!(store.insert(
906                domain_cookie_from("bar.example.com", "http://bar.example.com",),
907                &request_url,
908            ));
909            updated!(store.insert(
910                domain_cookie_from(".bar.example.com", "http://bar.example.com",),
911                &request_url,
912            ));
913            // bar.example.com cannot submit for foo.example.com
914            domain_mismatch!(store.insert(
915                domain_cookie_from("foo.example.com", "http://foo.example.com",),
916                &request_url,
917            ));
918            domain_mismatch!(store.insert(
919                domain_cookie_from(".foo.example.com", "http://foo.example.com",),
920                &request_url,
921            ));
922        }
923        {
924            let request_url = test_utils::url("http://example.com");
925            // example.com can submit for example.com
926            updated!(store.insert(
927                domain_cookie_from("example.com", "http://foo.example.com",),
928                &request_url,
929            ));
930            updated!(store.insert(
931                domain_cookie_from(".example.com", "http://foo.example.com",),
932                &request_url,
933            ));
934            // example.com cannot submit for foo.example.com or bar.example.com
935            domain_mismatch!(store.insert(
936                domain_cookie_from("foo.example.com", "http://foo.example.com",),
937                &request_url,
938            ));
939            domain_mismatch!(store.insert(
940                domain_cookie_from(".foo.example.com", "http://foo.example.com",),
941                &request_url,
942            ));
943            domain_mismatch!(store.insert(
944                domain_cookie_from("bar.example.com", "http://bar.example.com",),
945                &request_url,
946            ));
947            domain_mismatch!(store.insert(
948                domain_cookie_from(".bar.example.com", "http://bar.example.com",),
949                &request_url,
950            ));
951        }
952    }
953
954    #[test]
955    fn http_only() {
956        let mut store = CookieStore::default();
957        let c = Cookie::parse(
958            "cookie1=value1; HttpOnly",
959            &test_utils::url("http://example.com/foo/bar"),
960        )
961        .unwrap();
962        // cannot add a HttpOnly cookies from a non-http source
963        non_http_scheme!(store.insert(c, &test_utils::url("ftp://example.com/foo/bar"),));
964    }
965
966    #[test]
967    fn clear() {
968        let mut store = CookieStore::default();
969        inserted!(add_cookie(
970            &mut store,
971            "cookie1=value1",
972            "http://example.com/foo/bar",
973            Some(test_utils::in_days(1)),
974            None,
975        ));
976        assert!(
977            store
978                .iter_any()
979                .any(|c| c.name_value() == ("cookie1", "value1")),
980            "did not find expected cookie1=value1 cookie in store"
981        );
982        store.clear();
983        assert!(
984            store.iter_any().count() == 0,
985            "found unexpected cookies in cleared store"
986        );
987    }
988
989    #[test]
990    fn add_and_get() {
991        let mut store = CookieStore::default();
992        assert!(store.get("example.com", "/foo", "cookie1").is_none());
993
994        inserted!(add_cookie(
995            &mut store,
996            "cookie1=value1",
997            "http://example.com/foo/bar",
998            None,
999            None,
1000        ));
1001        assert!(store.get("example.com", "/foo/bar", "cookie1").is_none());
1002        assert!(store.get("example.com", "/foo", "cookie2").is_none());
1003        assert!(store.get("example.org", "/foo", "cookie1").is_none());
1004        assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1");
1005
1006        updated!(add_cookie(
1007            &mut store,
1008            "cookie1=value2",
1009            "http://example.com/foo/bar",
1010            None,
1011            None,
1012        ));
1013        assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2");
1014
1015        inserted!(add_cookie(
1016            &mut store,
1017            "cookie2=value3",
1018            "http://example.com/foo/bar",
1019            None,
1020            None,
1021        ));
1022        assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2");
1023        assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3");
1024
1025        inserted!(add_cookie(
1026            &mut store,
1027            "cookie3=value4; HttpOnly",
1028            "http://example.com/foo/bar",
1029            None,
1030            None,
1031        ));
1032        assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2");
1033        assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3");
1034        assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value4");
1035
1036        non_http_scheme!(add_cookie(
1037            &mut store,
1038            "cookie3=value5",
1039            "ftp://example.com/foo/bar",
1040            None,
1041            None,
1042        ));
1043        assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2");
1044        assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3");
1045        assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value4");
1046    }
1047
1048    #[test]
1049    fn matches() {
1050        let store = make_match_store();
1051        check_matches!(&store);
1052    }
1053
1054    fn matches_are(store: &CookieStore, url: &str, exp: Vec<&str>) {
1055        let matches = store
1056            .matches(&test_utils::url(url))
1057            .iter()
1058            .map(|c| format!("{}={}", c.name(), c.value()))
1059            .collect::<Vec<_>>();
1060        for e in &exp {
1061            assert!(
1062                matches.iter().any(|m| &m[..] == *e),
1063                "{}: matches missing '{}'\nmatches: {:?}\n    exp: {:?}",
1064                url,
1065                e,
1066                matches,
1067                exp
1068            );
1069        }
1070        assert!(
1071            matches.len() == exp.len(),
1072            "{}: matches={:?} != exp={:?}",
1073            url,
1074            matches,
1075            exp
1076        );
1077    }
1078
1079    #[test]
1080    fn some_non_https_uris_are_secure() {
1081        // Matching the list in Firefox's regression test:
1082        // https://hg.mozilla.org/integration/autoland/rev/c4d13b3ca1e2
1083        let secure_uris = vec![
1084            "http://localhost",
1085            "http://localhost:1234",
1086            "http://127.0.0.1",
1087            "http://127.0.0.2",
1088            "http://127.1.0.1",
1089            "http://[::1]",
1090        ];
1091        for secure_uri in secure_uris {
1092            let mut store = CookieStore::default();
1093            inserted!(add_cookie(
1094                &mut store,
1095                "cookie1=1a; Secure",
1096                secure_uri,
1097                None,
1098                None,
1099            ));
1100            matches_are(&store, secure_uri, vec!["cookie1=1a"]);
1101        }
1102    }
1103
1104    #[cfg(feature = "serde_json")]
1105    macro_rules! dump_json {
1106        ($e: expr, $i: ident) => {{
1107            use serde_json;
1108            println!("");
1109            println!(
1110                "==== {}: {} ====",
1111                $e,
1112                time::OffsetDateTime::now_utc()
1113                    .format(crate::rfc3339_fmt::RFC3339_FORMAT)
1114                    .unwrap()
1115            );
1116            for c in $i.iter_any() {
1117                println!(
1118                    "{} {}",
1119                    if c.is_expired() {
1120                        "XXXXX"
1121                    } else if c.is_persistent() {
1122                        "PPPPP"
1123                    } else {
1124                        "     "
1125                    },
1126                    serde_json::to_string(c).unwrap()
1127                );
1128                println!("----------------");
1129            }
1130            println!("================");
1131        }};
1132    }
1133
1134    #[test]
1135    fn domain_collisions() {
1136        let mut store = CookieStore::default();
1137        // - HostOnly, so no collisions here
1138        inserted!(add_cookie(
1139            &mut store,
1140            "cookie1=1a",
1141            "http://foo.bus.example.com/",
1142            None,
1143            None,
1144        ));
1145        inserted!(add_cookie(
1146            &mut store,
1147            "cookie1=1b",
1148            "http://bus.example.com/",
1149            None,
1150            None,
1151        ));
1152        // - Suffix
1153        // both cookie2's domain-match bus.example.com
1154        inserted!(add_cookie(
1155            &mut store,
1156            "cookie2=2a; Domain=bus.example.com",
1157            "http://foo.bus.example.com/",
1158            None,
1159            None,
1160        ));
1161        inserted!(add_cookie(
1162            &mut store,
1163            "cookie2=2b; Domain=example.com",
1164            "http://bus.example.com/",
1165            None,
1166            None,
1167        ));
1168        #[cfg(feature = "serde_json")]
1169        dump_json!("domain_collisions", store);
1170        matches_are(
1171            &store,
1172            "http://foo.bus.example.com/",
1173            vec!["cookie1=1a", "cookie2=2a", "cookie2=2b"],
1174        );
1175        matches_are(
1176            &store,
1177            "http://bus.example.com/",
1178            vec!["cookie1=1b", "cookie2=2a", "cookie2=2b"],
1179        );
1180        matches_are(&store, "http://example.com/", vec!["cookie2=2b"]);
1181        matches_are(&store, "http://foo.example.com/", vec!["cookie2=2b"]);
1182    }
1183
1184    #[test]
1185    fn path_collisions() {
1186        let mut store = CookieStore::default();
1187        // will be default-path: /foo/bar, and /foo, resp.
1188        // both should match /foo/bar/
1189        inserted!(add_cookie(
1190            &mut store,
1191            "cookie3=3a",
1192            "http://bus.example.com/foo/bar/",
1193            None,
1194            None,
1195        ));
1196        inserted!(add_cookie(
1197            &mut store,
1198            "cookie3=3b",
1199            "http://bus.example.com/foo/",
1200            None,
1201            None,
1202        ));
1203        // - Path set explicitly
1204        inserted!(add_cookie(
1205            &mut store,
1206            "cookie4=4a; Path=/foo/bar/",
1207            "http://bus.example.com/",
1208            None,
1209            None,
1210        ));
1211        inserted!(add_cookie(
1212            &mut store,
1213            "cookie4=4b; Path=/foo/",
1214            "http://bus.example.com/",
1215            None,
1216            None,
1217        ));
1218        #[cfg(feature = "serde_json")]
1219        dump_json!("path_collisions", store);
1220        matches_are(
1221            &store,
1222            "http://bus.example.com/foo/bar/",
1223            vec!["cookie3=3a", "cookie3=3b", "cookie4=4a", "cookie4=4b"],
1224        );
1225        // Agrees w/ chrome, but not FF... FF also sends cookie4=4a, but this should be
1226        // a path-match
1227        // fail since request-uri /foo/bar is a *prefix* of the cookie path /foo/bar/
1228        matches_are(
1229            &store,
1230            "http://bus.example.com/foo/bar",
1231            vec!["cookie3=3a", "cookie3=3b", "cookie4=4b"],
1232        );
1233        matches_are(
1234            &store,
1235            "http://bus.example.com/foo/ba",
1236            vec!["cookie3=3b", "cookie4=4b"],
1237        );
1238        matches_are(
1239            &store,
1240            "http://bus.example.com/foo/",
1241            vec!["cookie3=3b", "cookie4=4b"],
1242        );
1243        // Agrees w/ chrome, but not FF... FF also sends cookie4=4b, but this should be
1244        // a path-match
1245        // fail since request-uri /foo is a *prefix* of the cookie path /foo/
1246        matches_are(&store, "http://bus.example.com/foo", vec!["cookie3=3b"]);
1247        matches_are(&store, "http://bus.example.com/fo", vec![]);
1248        matches_are(&store, "http://bus.example.com/", vec![]);
1249        matches_are(&store, "http://bus.example.com", vec![]);
1250    }
1251
1252    #[cfg(feature = "serde_json")]
1253    #[allow(deprecated)]
1254    mod serde_json_tests {
1255        use super::{add_cookie, make_match_store, CookieStore, StoreAction};
1256        use crate::cookie::Cookie;
1257        use crate::CookieError;
1258
1259        use crate::utils::test as test_utils;
1260
1261        macro_rules! has_str {
1262            ($e: expr, $i: ident) => {{
1263                let val = std::str::from_utf8(&$i[..]).unwrap();
1264                assert!(val.contains($e), "exp: {}\nval: {}", $e, val);
1265            }};
1266        }
1267        macro_rules! not_has_str {
1268            ($e: expr, $i: ident) => {{
1269                let val = std::str::from_utf8(&$i[..]).unwrap();
1270                assert!(!val.contains($e), "exp: {}\nval: {}", $e, val);
1271            }};
1272        }
1273
1274        #[test]
1275        fn save_json() {
1276            let mut output = vec![];
1277            let mut store = CookieStore::default();
1278            store.save_json(&mut output).unwrap();
1279            assert_eq!("", std::str::from_utf8(&output[..]).unwrap());
1280            // non-persistent cookie, should not be saved
1281            inserted!(add_cookie(
1282                &mut store,
1283                "cookie0=value0",
1284                "http://example.com/foo/bar",
1285                None,
1286                None,
1287            ));
1288            store.save_json(&mut output).unwrap();
1289            assert_eq!("", std::str::from_utf8(&output[..]).unwrap());
1290
1291            // persistent cookie, Max-Age
1292            inserted!(add_cookie(
1293                &mut store,
1294                "cookie1=value1",
1295                "http://example.com/foo/bar",
1296                None,
1297                Some(10),
1298            ));
1299            store.save_json(&mut output).unwrap();
1300            not_has_str!("cookie0=value0", output);
1301            has_str!("cookie1=value1", output);
1302            output.clear();
1303
1304            // persistent cookie, Expires
1305            inserted!(add_cookie(
1306                &mut store,
1307                "cookie2=value2",
1308                "http://example.com/foo/bar",
1309                Some(test_utils::in_days(1)),
1310                None,
1311            ));
1312            store.save_json(&mut output).unwrap();
1313            not_has_str!("cookie0=value0", output);
1314            has_str!("cookie1=value1", output);
1315            has_str!("cookie2=value2", output);
1316            output.clear();
1317
1318            inserted!(add_cookie(
1319                &mut store,
1320                "cookie3=value3; Domain=example.com",
1321                "http://foo.example.com/foo/bar",
1322                Some(test_utils::in_days(1)),
1323                None,
1324            ));
1325            inserted!(add_cookie(
1326                &mut store,
1327                "cookie4=value4; Path=/foo/",
1328                "http://foo.example.com/foo/bar",
1329                Some(test_utils::in_days(1)),
1330                None,
1331            ));
1332            inserted!(add_cookie(
1333                &mut store,
1334                "cookie5=value5",
1335                "http://127.0.0.1/foo/bar",
1336                Some(test_utils::in_days(1)),
1337                None,
1338            ));
1339            inserted!(add_cookie(
1340                &mut store,
1341                "cookie6=value6",
1342                "http://[::1]/foo/bar",
1343                Some(test_utils::in_days(1)),
1344                None,
1345            ));
1346            inserted!(add_cookie(
1347                &mut store,
1348                "cookie7=value7; Secure",
1349                "https://[::1]/foo/bar",
1350                Some(test_utils::in_days(1)),
1351                None,
1352            ));
1353            inserted!(add_cookie(
1354                &mut store,
1355                "cookie8=value8; HttpOnly",
1356                "http://[::1]/foo/bar",
1357                Some(test_utils::in_days(1)),
1358                None,
1359            ));
1360            store.save_json(&mut output).unwrap();
1361            not_has_str!("cookie0=value0", output);
1362            has_str!("cookie1=value1", output);
1363            has_str!("cookie2=value2", output);
1364            has_str!("cookie3=value3", output);
1365            has_str!("cookie4=value4", output);
1366            has_str!("cookie5=value5", output);
1367            has_str!("cookie6=value6", output);
1368            has_str!("cookie7=value7; Secure", output);
1369            has_str!("cookie8=value8; HttpOnly", output);
1370            output.clear();
1371        }
1372
1373        #[test]
1374        fn serialize_json() {
1375            let mut output = vec![];
1376            let mut store = CookieStore::default();
1377            serde_json::to_writer(&mut output, &store).unwrap();
1378            assert_eq!("[]", std::str::from_utf8(&output[..]).unwrap());
1379            output.clear();
1380
1381            // non-persistent cookie, should not be saved
1382            inserted!(add_cookie(
1383                &mut store,
1384                "cookie0=value0",
1385                "http://example.com/foo/bar",
1386                None,
1387                None,
1388            ));
1389            serde_json::to_writer(&mut output, &store).unwrap();
1390            assert_eq!("[]", std::str::from_utf8(&output[..]).unwrap());
1391            output.clear();
1392
1393            // persistent cookie, Max-Age
1394            inserted!(add_cookie(
1395                &mut store,
1396                "cookie1=value1",
1397                "http://example.com/foo/bar",
1398                None,
1399                Some(10),
1400            ));
1401            serde_json::to_writer(&mut output, &store).unwrap();
1402            not_has_str!("cookie0=value0", output);
1403            has_str!("cookie1=value1", output);
1404            output.clear();
1405
1406            // persistent cookie, Expires
1407            inserted!(add_cookie(
1408                &mut store,
1409                "cookie2=value2",
1410                "http://example.com/foo/bar",
1411                Some(test_utils::in_days(1)),
1412                None,
1413            ));
1414            serde_json::to_writer(&mut output, &store).unwrap();
1415            not_has_str!("cookie0=value0", output);
1416            has_str!("cookie1=value1", output);
1417            has_str!("cookie2=value2", output);
1418            output.clear();
1419
1420            inserted!(add_cookie(
1421                &mut store,
1422                "cookie3=value3; Domain=example.com",
1423                "http://foo.example.com/foo/bar",
1424                Some(test_utils::in_days(1)),
1425                None,
1426            ));
1427            inserted!(add_cookie(
1428                &mut store,
1429                "cookie4=value4; Path=/foo/",
1430                "http://foo.example.com/foo/bar",
1431                Some(test_utils::in_days(1)),
1432                None,
1433            ));
1434            inserted!(add_cookie(
1435                &mut store,
1436                "cookie5=value5",
1437                "http://127.0.0.1/foo/bar",
1438                Some(test_utils::in_days(1)),
1439                None,
1440            ));
1441            inserted!(add_cookie(
1442                &mut store,
1443                "cookie6=value6",
1444                "http://[::1]/foo/bar",
1445                Some(test_utils::in_days(1)),
1446                None,
1447            ));
1448            inserted!(add_cookie(
1449                &mut store,
1450                "cookie7=value7; Secure",
1451                "https://[::1]/foo/bar",
1452                Some(test_utils::in_days(1)),
1453                None,
1454            ));
1455            inserted!(add_cookie(
1456                &mut store,
1457                "cookie8=value8; HttpOnly",
1458                "http://[::1]/foo/bar",
1459                Some(test_utils::in_days(1)),
1460                None,
1461            ));
1462            serde_json::to_writer(&mut output, &store).unwrap();
1463            not_has_str!("cookie0=value0", output);
1464            has_str!("cookie1=value1", output);
1465            has_str!("cookie2=value2", output);
1466            has_str!("cookie3=value3", output);
1467            has_str!("cookie4=value4", output);
1468            has_str!("cookie5=value5", output);
1469            has_str!("cookie6=value6", output);
1470            has_str!("cookie7=value7; Secure", output);
1471            has_str!("cookie8=value8; HttpOnly", output);
1472            output.clear();
1473        }
1474
1475        #[test]
1476        fn load_json() {
1477            let mut store = CookieStore::default();
1478            // non-persistent cookie, should not be saved
1479            inserted!(add_cookie(
1480                &mut store,
1481                "cookie0=value0",
1482                "http://example.com/foo/bar",
1483                None,
1484                None,
1485            ));
1486            // persistent cookie, Max-Age
1487            inserted!(add_cookie(
1488                &mut store,
1489                "cookie1=value1",
1490                "http://example.com/foo/bar",
1491                None,
1492                Some(10),
1493            ));
1494            // persistent cookie, Expires
1495            inserted!(add_cookie(
1496                &mut store,
1497                "cookie2=value2",
1498                "http://example.com/foo/bar",
1499                Some(test_utils::in_days(1)),
1500                None,
1501            ));
1502            inserted!(add_cookie(
1503                &mut store,
1504                "cookie3=value3; Domain=example.com",
1505                "http://foo.example.com/foo/bar",
1506                Some(test_utils::in_days(1)),
1507                None,
1508            ));
1509            inserted!(add_cookie(
1510                &mut store,
1511                "cookie4=value4; Path=/foo/",
1512                "http://foo.example.com/foo/bar",
1513                Some(test_utils::in_days(1)),
1514                None,
1515            ));
1516            inserted!(add_cookie(
1517                &mut store,
1518                "cookie5=value5",
1519                "http://127.0.0.1/foo/bar",
1520                Some(test_utils::in_days(1)),
1521                None,
1522            ));
1523            inserted!(add_cookie(
1524                &mut store,
1525                "cookie6=value6",
1526                "http://[::1]/foo/bar",
1527                Some(test_utils::in_days(1)),
1528                None,
1529            ));
1530            inserted!(add_cookie(
1531                &mut store,
1532                "cookie7=value7; Secure",
1533                "http://example.com/foo/bar",
1534                Some(test_utils::in_days(1)),
1535                None,
1536            ));
1537            inserted!(add_cookie(
1538                &mut store,
1539                "cookie8=value8; HttpOnly",
1540                "http://example.com/foo/bar",
1541                Some(test_utils::in_days(1)),
1542                None,
1543            ));
1544            let mut output = vec![];
1545            store.save_json(&mut output).unwrap();
1546            not_has_str!("cookie0=value0", output);
1547            has_str!("cookie1=value1", output);
1548            has_str!("cookie2=value2", output);
1549            has_str!("cookie3=value3", output);
1550            has_str!("cookie4=value4", output);
1551            has_str!("cookie5=value5", output);
1552            has_str!("cookie6=value6", output);
1553            has_str!("cookie7=value7; Secure", output);
1554            has_str!("cookie8=value8; HttpOnly", output);
1555            let store = CookieStore::load_json(&output[..]).unwrap();
1556            assert!(store.get("example.com", "/foo", "cookie0").is_none());
1557            assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1");
1558            assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value2");
1559            assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value3");
1560            assert!(
1561                store
1562                    .get("foo.example.com", "/foo/", "cookie4")
1563                    .unwrap()
1564                    .value()
1565                    == "value4"
1566            );
1567            assert!(store.get("127.0.0.1", "/foo", "cookie5").unwrap().value() == "value5");
1568            assert!(store.get("[::1]", "/foo", "cookie6").unwrap().value() == "value6");
1569            assert!(store.get("example.com", "/foo", "cookie7").unwrap().value() == "value7");
1570            assert!(store.get("example.com", "/foo", "cookie8").unwrap().value() == "value8");
1571
1572            output.clear();
1573            let store = make_match_store();
1574            store.save_json(&mut output).unwrap();
1575            let store = CookieStore::load_json(&output[..]).unwrap();
1576            check_matches!(&store);
1577        }
1578
1579        #[test]
1580        fn deserialize_json() {
1581            let mut store = CookieStore::default();
1582            // non-persistent cookie, should not be saved
1583            inserted!(add_cookie(
1584                &mut store,
1585                "cookie0=value0",
1586                "http://example.com/foo/bar",
1587                None,
1588                None,
1589            ));
1590            // persistent cookie, Max-Age
1591            inserted!(add_cookie(
1592                &mut store,
1593                "cookie1=value1",
1594                "http://example.com/foo/bar",
1595                None,
1596                Some(10),
1597            ));
1598            // persistent cookie, Expires
1599            inserted!(add_cookie(
1600                &mut store,
1601                "cookie2=value2",
1602                "http://example.com/foo/bar",
1603                Some(test_utils::in_days(1)),
1604                None,
1605            ));
1606            inserted!(add_cookie(
1607                &mut store,
1608                "cookie3=value3; Domain=example.com",
1609                "http://foo.example.com/foo/bar",
1610                Some(test_utils::in_days(1)),
1611                None,
1612            ));
1613            inserted!(add_cookie(
1614                &mut store,
1615                "cookie4=value4; Path=/foo/",
1616                "http://foo.example.com/foo/bar",
1617                Some(test_utils::in_days(1)),
1618                None,
1619            ));
1620            inserted!(add_cookie(
1621                &mut store,
1622                "cookie5=value5",
1623                "http://127.0.0.1/foo/bar",
1624                Some(test_utils::in_days(1)),
1625                None,
1626            ));
1627            inserted!(add_cookie(
1628                &mut store,
1629                "cookie6=value6",
1630                "http://[::1]/foo/bar",
1631                Some(test_utils::in_days(1)),
1632                None,
1633            ));
1634            inserted!(add_cookie(
1635                &mut store,
1636                "cookie7=value7; Secure",
1637                "http://example.com/foo/bar",
1638                Some(test_utils::in_days(1)),
1639                None,
1640            ));
1641            inserted!(add_cookie(
1642                &mut store,
1643                "cookie8=value8; HttpOnly",
1644                "http://example.com/foo/bar",
1645                Some(test_utils::in_days(1)),
1646                None,
1647            ));
1648            let mut output = vec![];
1649            serde_json::to_writer(&mut output, &store).unwrap();
1650            not_has_str!("cookie0=value0", output);
1651            has_str!("cookie1=value1", output);
1652            has_str!("cookie2=value2", output);
1653            has_str!("cookie3=value3", output);
1654            has_str!("cookie4=value4", output);
1655            has_str!("cookie5=value5", output);
1656            has_str!("cookie6=value6", output);
1657            has_str!("cookie7=value7; Secure", output);
1658            has_str!("cookie8=value8; HttpOnly", output);
1659            let store: CookieStore = serde_json::from_reader(&output[..]).unwrap();
1660            assert!(store.get("example.com", "/foo", "cookie0").is_none());
1661            assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1");
1662            assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value2");
1663            assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value3");
1664            assert!(
1665                store
1666                    .get("foo.example.com", "/foo/", "cookie4")
1667                    .unwrap()
1668                    .value()
1669                    == "value4"
1670            );
1671            assert!(store.get("127.0.0.1", "/foo", "cookie5").unwrap().value() == "value5");
1672            assert!(store.get("[::1]", "/foo", "cookie6").unwrap().value() == "value6");
1673            assert!(store.get("example.com", "/foo", "cookie7").unwrap().value() == "value7");
1674            assert!(store.get("example.com", "/foo", "cookie8").unwrap().value() == "value8");
1675
1676            output.clear();
1677            let store = make_match_store();
1678            serde_json::to_writer(&mut output, &store).unwrap();
1679            let store: CookieStore = serde_json::from_reader(&output[..]).unwrap();
1680            check_matches!(&store);
1681        }
1682
1683        #[test]
1684        fn expiry_json() {
1685            let mut store = make_match_store();
1686            let request_url = test_utils::url("http://foo.example.com");
1687            let expired_cookie = Cookie::parse("cookie1=value1; Max-Age=-1", &request_url).unwrap();
1688            expired_err!(store.insert(expired_cookie, &request_url));
1689            check_matches!(&store);
1690            match store.get_mut("example.com", "/", "cookie6") {
1691                Some(cookie) => cookie.expire(),
1692                None => unreachable!(),
1693            }
1694            values_are!(store, "http://unknowndomain.org/foo/bar", vec![]);
1695            values_are!(store, "http://example.org/foo/bar", vec!["8"]);
1696            values_are!(store, "http://example.org/bus/bar", vec![]);
1697            values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]);
1698            values_are!(store, "http://bar.example.org/bus/bar", vec![]);
1699            values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]);
1700            values_are!(store, "http://example.com/sec/foo", vec!["3"]);
1701            values_are!(store, "ftp://example.com/sec/foo", vec![]);
1702            values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]);
1703            values_are!(store, "http://example.com/foo/bar/bus", vec!["1", "5"]);
1704            match store.get_any("example.com", "/", "cookie6") {
1705                Some(cookie) => assert!(cookie.is_expired()),
1706                None => unreachable!(),
1707            }
1708            // inserting an expired cookie that matches an existing cookie should expire
1709            // the existing
1710            let request_url = test_utils::url("http://example.com/foo/");
1711            let expired_cookie = Cookie::parse("cookie5=value5; Max-Age=-1", &request_url).unwrap();
1712            expired_existing!(store.insert(expired_cookie, &request_url));
1713            values_are!(store, "http://unknowndomain.org/foo/bar", vec![]);
1714            values_are!(store, "http://example.org/foo/bar", vec!["8"]);
1715            values_are!(store, "http://example.org/bus/bar", vec![]);
1716            values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]);
1717            values_are!(store, "http://bar.example.org/bus/bar", vec![]);
1718            values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]);
1719            values_are!(store, "http://example.com/sec/foo", vec!["3"]);
1720            values_are!(store, "ftp://example.com/sec/foo", vec![]);
1721            values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]);
1722            values_are!(store, "http://example.com/foo/bar/bus", vec!["1"]);
1723            match store.get_any("example.com", "/foo", "cookie5") {
1724                Some(cookie) => assert!(cookie.is_expired()),
1725                None => unreachable!(),
1726            }
1727            // save and loading the store should drop any expired cookies
1728            let mut output = vec![];
1729            store.save_json(&mut output).unwrap();
1730            store = CookieStore::load_json(&output[..]).unwrap();
1731            values_are!(store, "http://unknowndomain.org/foo/bar", vec![]);
1732            values_are!(store, "http://example.org/foo/bar", vec!["8"]);
1733            values_are!(store, "http://example.org/bus/bar", vec![]);
1734            values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]);
1735            values_are!(store, "http://bar.example.org/bus/bar", vec![]);
1736            values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]);
1737            values_are!(store, "http://example.com/sec/foo", vec!["3"]);
1738            values_are!(store, "ftp://example.com/sec/foo", vec![]);
1739            values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]);
1740            values_are!(store, "http://example.com/foo/bar/bus", vec!["1"]);
1741            assert!(store.get_any("example.com", "/", "cookie6").is_none());
1742            assert!(store.get_any("example.com", "/foo", "cookie5").is_none());
1743        }
1744
1745        #[test]
1746        fn non_persistent_json() {
1747            let mut store = make_match_store();
1748            check_matches!(&store);
1749            let request_url = test_utils::url("http://example.com/tmp/");
1750            let non_persistent = Cookie::parse("cookie10=value10", &request_url).unwrap();
1751            inserted!(store.insert(non_persistent, &request_url));
1752            match store.get("example.com", "/tmp", "cookie10") {
1753                None => unreachable!(),
1754                Some(cookie) => assert_eq!("value10", cookie.value()),
1755            }
1756            // save and loading the store should drop any non-persistent cookies
1757            let mut output = vec![];
1758            store.save_json(&mut output).unwrap();
1759            store = CookieStore::load_json(&output[..]).unwrap();
1760            check_matches!(&store);
1761            assert!(store.get("example.com", "/tmp", "cookie10").is_none());
1762            assert!(store.get_any("example.com", "/tmp", "cookie10").is_none());
1763        }
1764    }
1765}