actori_http/cookie/
jar.rs

1use std::collections::HashSet;
2use std::mem::replace;
3
4use chrono::Duration;
5
6use super::delta::DeltaCookie;
7use super::Cookie;
8
9#[cfg(feature = "secure-cookies")]
10use super::secure::{Key, PrivateJar, SignedJar};
11
12/// A collection of cookies that tracks its modifications.
13///
14/// A `CookieJar` provides storage for any number of cookies. Any changes made
15/// to the jar are tracked; the changes can be retrieved via the
16/// [delta](#method.delta) method which returns an interator over the changes.
17///
18/// # Usage
19///
20/// A jar's life begins via [new](#method.new) and calls to
21/// [`add_original`](#method.add_original):
22///
23/// ```rust
24/// use actori_http::cookie::{Cookie, CookieJar};
25///
26/// let mut jar = CookieJar::new();
27/// jar.add_original(Cookie::new("name", "value"));
28/// jar.add_original(Cookie::new("second", "another"));
29/// ```
30///
31/// Cookies can be added via [add](#method.add) and removed via
32/// [remove](#method.remove). Finally, cookies can be looked up via
33/// [get](#method.get):
34///
35/// ```rust
36/// # use actori_http::cookie::{Cookie, CookieJar};
37/// let mut jar = CookieJar::new();
38/// jar.add(Cookie::new("a", "one"));
39/// jar.add(Cookie::new("b", "two"));
40///
41/// assert_eq!(jar.get("a").map(|c| c.value()), Some("one"));
42/// assert_eq!(jar.get("b").map(|c| c.value()), Some("two"));
43///
44/// jar.remove(Cookie::named("b"));
45/// assert!(jar.get("b").is_none());
46/// ```
47///
48/// # Deltas
49///
50/// A jar keeps track of any modifications made to it over time. The
51/// modifications are recorded as cookies. The modifications can be retrieved
52/// via [delta](#method.delta). Any new `Cookie` added to a jar via `add`
53/// results in the same `Cookie` appearing in the `delta`; cookies added via
54/// `add_original` do not count towards the delta. Any _original_ cookie that is
55/// removed from a jar results in a "removal" cookie appearing in the delta. A
56/// "removal" cookie is a cookie that a server sends so that the cookie is
57/// removed from the client's machine.
58///
59/// Deltas are typically used to create `Set-Cookie` headers corresponding to
60/// the changes made to a cookie jar over a period of time.
61///
62/// ```rust
63/// # use actori_http::cookie::{Cookie, CookieJar};
64/// let mut jar = CookieJar::new();
65///
66/// // original cookies don't affect the delta
67/// jar.add_original(Cookie::new("original", "value"));
68/// assert_eq!(jar.delta().count(), 0);
69///
70/// // new cookies result in an equivalent `Cookie` in the delta
71/// jar.add(Cookie::new("a", "one"));
72/// jar.add(Cookie::new("b", "two"));
73/// assert_eq!(jar.delta().count(), 2);
74///
75/// // removing an original cookie adds a "removal" cookie to the delta
76/// jar.remove(Cookie::named("original"));
77/// assert_eq!(jar.delta().count(), 3);
78///
79/// // removing a new cookie that was added removes that `Cookie` from the delta
80/// jar.remove(Cookie::named("a"));
81/// assert_eq!(jar.delta().count(), 2);
82/// ```
83#[derive(Default, Debug, Clone)]
84pub struct CookieJar {
85    original_cookies: HashSet<DeltaCookie>,
86    delta_cookies: HashSet<DeltaCookie>,
87}
88
89impl CookieJar {
90    /// Creates an empty cookie jar.
91    ///
92    /// # Example
93    ///
94    /// ```rust
95    /// use actori_http::cookie::CookieJar;
96    ///
97    /// let jar = CookieJar::new();
98    /// assert_eq!(jar.iter().count(), 0);
99    /// ```
100    pub fn new() -> CookieJar {
101        CookieJar::default()
102    }
103
104    /// Returns a reference to the `Cookie` inside this jar with the name
105    /// `name`. If no such cookie exists, returns `None`.
106    ///
107    /// # Example
108    ///
109    /// ```rust
110    /// use actori_http::cookie::{CookieJar, Cookie};
111    ///
112    /// let mut jar = CookieJar::new();
113    /// assert!(jar.get("name").is_none());
114    ///
115    /// jar.add(Cookie::new("name", "value"));
116    /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
117    /// ```
118    pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
119        self.delta_cookies
120            .get(name)
121            .or_else(|| self.original_cookies.get(name))
122            .and_then(|c| if !c.removed { Some(&c.cookie) } else { None })
123    }
124
125    /// Adds an "original" `cookie` to this jar. If an original cookie with the
126    /// same name already exists, it is replaced with `cookie`. Cookies added
127    /// with `add` take precedence and are not replaced by this method.
128    ///
129    /// Adding an original cookie does not affect the [delta](#method.delta)
130    /// computation. This method is intended to be used to seed the cookie jar
131    /// with cookies received from a client's HTTP message.
132    ///
133    /// For accurate `delta` computations, this method should not be called
134    /// after calling `remove`.
135    ///
136    /// # Example
137    ///
138    /// ```rust
139    /// use actori_http::cookie::{CookieJar, Cookie};
140    ///
141    /// let mut jar = CookieJar::new();
142    /// jar.add_original(Cookie::new("name", "value"));
143    /// jar.add_original(Cookie::new("second", "two"));
144    ///
145    /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
146    /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two"));
147    /// assert_eq!(jar.iter().count(), 2);
148    /// assert_eq!(jar.delta().count(), 0);
149    /// ```
150    pub fn add_original(&mut self, cookie: Cookie<'static>) {
151        self.original_cookies.replace(DeltaCookie::added(cookie));
152    }
153
154    /// Adds `cookie` to this jar. If a cookie with the same name already
155    /// exists, it is replaced with `cookie`.
156    ///
157    /// # Example
158    ///
159    /// ```rust
160    /// use actori_http::cookie::{CookieJar, Cookie};
161    ///
162    /// let mut jar = CookieJar::new();
163    /// jar.add(Cookie::new("name", "value"));
164    /// jar.add(Cookie::new("second", "two"));
165    ///
166    /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
167    /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two"));
168    /// assert_eq!(jar.iter().count(), 2);
169    /// assert_eq!(jar.delta().count(), 2);
170    /// ```
171    pub fn add(&mut self, cookie: Cookie<'static>) {
172        self.delta_cookies.replace(DeltaCookie::added(cookie));
173    }
174
175    /// Removes `cookie` from this jar. If an _original_ cookie with the same
176    /// name as `cookie` is present in the jar, a _removal_ cookie will be
177    /// present in the `delta` computation. To properly generate the removal
178    /// cookie, `cookie` must contain the same `path` and `domain` as the cookie
179    /// that was initially set.
180    ///
181    /// A "removal" cookie is a cookie that has the same name as the original
182    /// cookie but has an empty value, a max-age of 0, and an expiration date
183    /// far in the past.
184    ///
185    /// # Example
186    ///
187    /// Removing an _original_ cookie results in a _removal_ cookie:
188    ///
189    /// ```rust
190    /// use actori_http::cookie::{CookieJar, Cookie};
191    /// use chrono::Duration;
192    ///
193    /// let mut jar = CookieJar::new();
194    ///
195    /// // Assume this cookie originally had a path of "/" and domain of "a.b".
196    /// jar.add_original(Cookie::new("name", "value"));
197    ///
198    /// // If the path and domain were set, they must be provided to `remove`.
199    /// jar.remove(Cookie::build("name", "").path("/").domain("a.b").finish());
200    ///
201    /// // The delta will contain the removal cookie.
202    /// let delta: Vec<_> = jar.delta().collect();
203    /// assert_eq!(delta.len(), 1);
204    /// assert_eq!(delta[0].name(), "name");
205    /// assert_eq!(delta[0].max_age(), Some(Duration::seconds(0)));
206    /// ```
207    ///
208    /// Removing a new cookie does not result in a _removal_ cookie:
209    ///
210    /// ```rust
211    /// use actori_http::cookie::{CookieJar, Cookie};
212    ///
213    /// let mut jar = CookieJar::new();
214    /// jar.add(Cookie::new("name", "value"));
215    /// assert_eq!(jar.delta().count(), 1);
216    ///
217    /// jar.remove(Cookie::named("name"));
218    /// assert_eq!(jar.delta().count(), 0);
219    /// ```
220    pub fn remove(&mut self, mut cookie: Cookie<'static>) {
221        if self.original_cookies.contains(cookie.name()) {
222            cookie.set_value("");
223            cookie.set_max_age(Duration::seconds(0));
224            cookie.set_expires(time::now() - Duration::days(365));
225            self.delta_cookies.replace(DeltaCookie::removed(cookie));
226        } else {
227            self.delta_cookies.remove(cookie.name());
228        }
229    }
230
231    /// Removes `cookie` from this jar completely. This method differs from
232    /// `remove` in that no delta cookie is created under any condition. Neither
233    /// the `delta` nor `iter` methods will return a cookie that is removed
234    /// using this method.
235    ///
236    /// # Example
237    ///
238    /// Removing an _original_ cookie; no _removal_ cookie is generated:
239    ///
240    /// ```rust
241    /// use actori_http::cookie::{CookieJar, Cookie};
242    /// use chrono::Duration;
243    ///
244    /// let mut jar = CookieJar::new();
245    ///
246    /// // Add an original cookie and a new cookie.
247    /// jar.add_original(Cookie::new("name", "value"));
248    /// jar.add(Cookie::new("key", "value"));
249    /// assert_eq!(jar.delta().count(), 1);
250    /// assert_eq!(jar.iter().count(), 2);
251    ///
252    /// // Now force remove the original cookie.
253    /// jar.force_remove(Cookie::new("name", "value"));
254    /// assert_eq!(jar.delta().count(), 1);
255    /// assert_eq!(jar.iter().count(), 1);
256    ///
257    /// // Now force remove the new cookie.
258    /// jar.force_remove(Cookie::new("key", "value"));
259    /// assert_eq!(jar.delta().count(), 0);
260    /// assert_eq!(jar.iter().count(), 0);
261    /// ```
262    pub fn force_remove<'a>(&mut self, cookie: Cookie<'a>) {
263        self.original_cookies.remove(cookie.name());
264        self.delta_cookies.remove(cookie.name());
265    }
266
267    /// Removes all cookies from this cookie jar.
268    #[deprecated(
269        since = "0.7.0",
270        note = "calling this method may not remove \
271                all cookies since the path and domain are not specified; use \
272                `remove` instead"
273    )]
274    pub fn clear(&mut self) {
275        self.delta_cookies.clear();
276        for delta in replace(&mut self.original_cookies, HashSet::new()) {
277            self.remove(delta.cookie);
278        }
279    }
280
281    /// Returns an iterator over cookies that represent the changes to this jar
282    /// over time. These cookies can be rendered directly as `Set-Cookie` header
283    /// values to affect the changes made to this jar on the client.
284    ///
285    /// # Example
286    ///
287    /// ```rust
288    /// use actori_http::cookie::{CookieJar, Cookie};
289    ///
290    /// let mut jar = CookieJar::new();
291    /// jar.add_original(Cookie::new("name", "value"));
292    /// jar.add_original(Cookie::new("second", "two"));
293    ///
294    /// // Add new cookies.
295    /// jar.add(Cookie::new("new", "third"));
296    /// jar.add(Cookie::new("another", "fourth"));
297    /// jar.add(Cookie::new("yac", "fifth"));
298    ///
299    /// // Remove some cookies.
300    /// jar.remove(Cookie::named("name"));
301    /// jar.remove(Cookie::named("another"));
302    ///
303    /// // Delta contains two new cookies ("new", "yac") and a removal ("name").
304    /// assert_eq!(jar.delta().count(), 3);
305    /// ```
306    pub fn delta(&self) -> Delta<'_> {
307        Delta {
308            iter: self.delta_cookies.iter(),
309        }
310    }
311
312    /// Returns an iterator over all of the cookies present in this jar.
313    ///
314    /// # Example
315    ///
316    /// ```rust
317    /// use actori_http::cookie::{CookieJar, Cookie};
318    ///
319    /// let mut jar = CookieJar::new();
320    ///
321    /// jar.add_original(Cookie::new("name", "value"));
322    /// jar.add_original(Cookie::new("second", "two"));
323    ///
324    /// jar.add(Cookie::new("new", "third"));
325    /// jar.add(Cookie::new("another", "fourth"));
326    /// jar.add(Cookie::new("yac", "fifth"));
327    ///
328    /// jar.remove(Cookie::named("name"));
329    /// jar.remove(Cookie::named("another"));
330    ///
331    /// // There are three cookies in the jar: "second", "new", and "yac".
332    /// # assert_eq!(jar.iter().count(), 3);
333    /// for cookie in jar.iter() {
334    ///     match cookie.name() {
335    ///         "second" => assert_eq!(cookie.value(), "two"),
336    ///         "new" => assert_eq!(cookie.value(), "third"),
337    ///         "yac" => assert_eq!(cookie.value(), "fifth"),
338    ///         _ => unreachable!("there are only three cookies in the jar")
339    ///     }
340    /// }
341    /// ```
342    pub fn iter(&self) -> Iter<'_> {
343        Iter {
344            delta_cookies: self
345                .delta_cookies
346                .iter()
347                .chain(self.original_cookies.difference(&self.delta_cookies)),
348        }
349    }
350
351    /// Returns a `PrivateJar` with `self` as its parent jar using the key `key`
352    /// to sign/encrypt and verify/decrypt cookies added/retrieved from the
353    /// child jar.
354    ///
355    /// Any modifications to the child jar will be reflected on the parent jar,
356    /// and any retrievals from the child jar will be made from the parent jar.
357    ///
358    /// This method is only available when the `secure` feature is enabled.
359    ///
360    /// # Example
361    ///
362    /// ```rust
363    /// use actori_http::cookie::{Cookie, CookieJar, Key};
364    ///
365    /// // Generate a secure key.
366    /// let key = Key::generate();
367    ///
368    /// // Add a private (signed + encrypted) cookie.
369    /// let mut jar = CookieJar::new();
370    /// jar.private(&key).add(Cookie::new("private", "text"));
371    ///
372    /// // The cookie's contents are encrypted.
373    /// assert_ne!(jar.get("private").unwrap().value(), "text");
374    ///
375    /// // They can be decrypted and verified through the child jar.
376    /// assert_eq!(jar.private(&key).get("private").unwrap().value(), "text");
377    ///
378    /// // A tampered with cookie does not validate but still exists.
379    /// let mut cookie = jar.get("private").unwrap().clone();
380    /// jar.add(Cookie::new("private", cookie.value().to_string() + "!"));
381    /// assert!(jar.private(&key).get("private").is_none());
382    /// assert!(jar.get("private").is_some());
383    /// ```
384    #[cfg(feature = "secure-cookies")]
385    pub fn private(&mut self, key: &Key) -> PrivateJar<'_> {
386        PrivateJar::new(self, key)
387    }
388
389    /// Returns a `SignedJar` with `self` as its parent jar using the key `key`
390    /// to sign/verify cookies added/retrieved from the child jar.
391    ///
392    /// Any modifications to the child jar will be reflected on the parent jar,
393    /// and any retrievals from the child jar will be made from the parent jar.
394    ///
395    /// This method is only available when the `secure` feature is enabled.
396    ///
397    /// # Example
398    ///
399    /// ```rust
400    /// use actori_http::cookie::{Cookie, CookieJar, Key};
401    ///
402    /// // Generate a secure key.
403    /// let key = Key::generate();
404    ///
405    /// // Add a signed cookie.
406    /// let mut jar = CookieJar::new();
407    /// jar.signed(&key).add(Cookie::new("signed", "text"));
408    ///
409    /// // The cookie's contents are signed but still in plaintext.
410    /// assert_ne!(jar.get("signed").unwrap().value(), "text");
411    /// assert!(jar.get("signed").unwrap().value().contains("text"));
412    ///
413    /// // They can be verified through the child jar.
414    /// assert_eq!(jar.signed(&key).get("signed").unwrap().value(), "text");
415    ///
416    /// // A tampered with cookie does not validate but still exists.
417    /// let mut cookie = jar.get("signed").unwrap().clone();
418    /// jar.add(Cookie::new("signed", cookie.value().to_string() + "!"));
419    /// assert!(jar.signed(&key).get("signed").is_none());
420    /// assert!(jar.get("signed").is_some());
421    /// ```
422    #[cfg(feature = "secure-cookies")]
423    pub fn signed(&mut self, key: &Key) -> SignedJar<'_> {
424        SignedJar::new(self, key)
425    }
426}
427
428use std::collections::hash_set::Iter as HashSetIter;
429
430/// Iterator over the changes to a cookie jar.
431pub struct Delta<'a> {
432    iter: HashSetIter<'a, DeltaCookie>,
433}
434
435impl<'a> Iterator for Delta<'a> {
436    type Item = &'a Cookie<'static>;
437
438    fn next(&mut self) -> Option<&'a Cookie<'static>> {
439        self.iter.next().map(|c| &c.cookie)
440    }
441}
442
443use std::collections::hash_map::RandomState;
444use std::collections::hash_set::Difference;
445use std::iter::Chain;
446
447/// Iterator over all of the cookies in a jar.
448pub struct Iter<'a> {
449    delta_cookies:
450        Chain<HashSetIter<'a, DeltaCookie>, Difference<'a, DeltaCookie, RandomState>>,
451}
452
453impl<'a> Iterator for Iter<'a> {
454    type Item = &'a Cookie<'static>;
455
456    fn next(&mut self) -> Option<&'a Cookie<'static>> {
457        for cookie in self.delta_cookies.by_ref() {
458            if !cookie.removed {
459                return Some(&*cookie);
460            }
461        }
462
463        None
464    }
465}
466
467#[cfg(test)]
468mod test {
469    #[cfg(feature = "secure-cookies")]
470    use super::Key;
471    use super::{Cookie, CookieJar};
472
473    #[test]
474    #[allow(deprecated)]
475    fn simple() {
476        let mut c = CookieJar::new();
477
478        c.add(Cookie::new("test", ""));
479        c.add(Cookie::new("test2", ""));
480        c.remove(Cookie::named("test"));
481
482        assert!(c.get("test").is_none());
483        assert!(c.get("test2").is_some());
484
485        c.add(Cookie::new("test3", ""));
486        c.clear();
487
488        assert!(c.get("test").is_none());
489        assert!(c.get("test2").is_none());
490        assert!(c.get("test3").is_none());
491    }
492
493    #[test]
494    fn jar_is_send() {
495        fn is_send<T: Send>(_: T) -> bool {
496            true
497        }
498
499        assert!(is_send(CookieJar::new()))
500    }
501
502    #[test]
503    #[cfg(feature = "secure-cookies")]
504    fn iter() {
505        let key = Key::generate();
506        let mut c = CookieJar::new();
507
508        c.add_original(Cookie::new("original", "original"));
509
510        c.add(Cookie::new("test", "test"));
511        c.add(Cookie::new("test2", "test2"));
512        c.add(Cookie::new("test3", "test3"));
513        assert_eq!(c.iter().count(), 4);
514
515        c.signed(&key).add(Cookie::new("signed", "signed"));
516        c.private(&key).add(Cookie::new("encrypted", "encrypted"));
517        assert_eq!(c.iter().count(), 6);
518
519        c.remove(Cookie::named("test"));
520        assert_eq!(c.iter().count(), 5);
521
522        c.remove(Cookie::named("signed"));
523        c.remove(Cookie::named("test2"));
524        assert_eq!(c.iter().count(), 3);
525
526        c.add(Cookie::new("test2", "test2"));
527        assert_eq!(c.iter().count(), 4);
528
529        c.remove(Cookie::named("test2"));
530        assert_eq!(c.iter().count(), 3);
531    }
532
533    #[test]
534    #[cfg(feature = "secure-cookies")]
535    fn delta() {
536        use chrono::Duration;
537        use std::collections::HashMap;
538
539        let mut c = CookieJar::new();
540
541        c.add_original(Cookie::new("original", "original"));
542        c.add_original(Cookie::new("original1", "original1"));
543
544        c.add(Cookie::new("test", "test"));
545        c.add(Cookie::new("test2", "test2"));
546        c.add(Cookie::new("test3", "test3"));
547        c.add(Cookie::new("test4", "test4"));
548
549        c.remove(Cookie::named("test"));
550        c.remove(Cookie::named("original"));
551
552        assert_eq!(c.delta().count(), 4);
553
554        let names: HashMap<_, _> = c.delta().map(|c| (c.name(), c.max_age())).collect();
555
556        assert!(names.get("test2").unwrap().is_none());
557        assert!(names.get("test3").unwrap().is_none());
558        assert!(names.get("test4").unwrap().is_none());
559        assert_eq!(names.get("original").unwrap(), &Some(Duration::seconds(0)));
560    }
561
562    #[test]
563    fn replace_original() {
564        let mut jar = CookieJar::new();
565        jar.add_original(Cookie::new("original_a", "a"));
566        jar.add_original(Cookie::new("original_b", "b"));
567        assert_eq!(jar.get("original_a").unwrap().value(), "a");
568
569        jar.add(Cookie::new("original_a", "av2"));
570        assert_eq!(jar.get("original_a").unwrap().value(), "av2");
571    }
572
573    #[test]
574    fn empty_delta() {
575        let mut jar = CookieJar::new();
576        jar.add(Cookie::new("name", "val"));
577        assert_eq!(jar.delta().count(), 1);
578
579        jar.remove(Cookie::named("name"));
580        assert_eq!(jar.delta().count(), 0);
581
582        jar.add_original(Cookie::new("name", "val"));
583        assert_eq!(jar.delta().count(), 0);
584
585        jar.remove(Cookie::named("name"));
586        assert_eq!(jar.delta().count(), 1);
587
588        jar.add(Cookie::new("name", "val"));
589        assert_eq!(jar.delta().count(), 1);
590
591        jar.remove(Cookie::named("name"));
592        assert_eq!(jar.delta().count(), 1);
593    }
594
595    #[test]
596    fn add_remove_add() {
597        let mut jar = CookieJar::new();
598        jar.add_original(Cookie::new("name", "val"));
599        assert_eq!(jar.delta().count(), 0);
600
601        jar.remove(Cookie::named("name"));
602        assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
603        assert_eq!(jar.delta().count(), 1);
604
605        // The cookie's been deleted. Another original doesn't change that.
606        jar.add_original(Cookie::new("name", "val"));
607        assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
608        assert_eq!(jar.delta().count(), 1);
609
610        jar.remove(Cookie::named("name"));
611        assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
612        assert_eq!(jar.delta().count(), 1);
613
614        jar.add(Cookie::new("name", "val"));
615        assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1);
616        assert_eq!(jar.delta().count(), 1);
617
618        jar.remove(Cookie::named("name"));
619        assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
620        assert_eq!(jar.delta().count(), 1);
621    }
622
623    #[test]
624    fn replace_remove() {
625        let mut jar = CookieJar::new();
626        jar.add_original(Cookie::new("name", "val"));
627        assert_eq!(jar.delta().count(), 0);
628
629        jar.add(Cookie::new("name", "val"));
630        assert_eq!(jar.delta().count(), 1);
631        assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1);
632
633        jar.remove(Cookie::named("name"));
634        assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
635    }
636
637    #[test]
638    fn remove_with_path() {
639        let mut jar = CookieJar::new();
640        jar.add_original(Cookie::build("name", "val").finish());
641        assert_eq!(jar.iter().count(), 1);
642        assert_eq!(jar.delta().count(), 0);
643        assert_eq!(jar.iter().filter(|c| c.path().is_none()).count(), 1);
644
645        jar.remove(Cookie::build("name", "").path("/").finish());
646        assert_eq!(jar.iter().count(), 0);
647        assert_eq!(jar.delta().count(), 1);
648        assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
649        assert_eq!(jar.delta().filter(|c| c.path() == Some("/")).count(), 1);
650    }
651}