cookie_store/
cookie_path.rs

1#[cfg(feature = "serde")]
2use serde_derive::{Deserialize, Serialize};
3use std::cmp::max;
4use std::ops::Deref;
5use url::Url;
6
7/// Returns true if `request_url` path-matches `path` per
8/// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4)
9pub fn is_match(path: &str, request_url: &Url) -> bool {
10    CookiePath::parse(path).map_or(false, |cp| cp.matches(request_url))
11}
12
13/// The path of a `Cookie`
14#[derive(PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16pub struct CookiePath(String, bool);
17impl CookiePath {
18    /// Determine if `request_url` path-matches this `CookiePath` per
19    /// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4)
20    pub fn matches(&self, request_url: &Url) -> bool {
21        if request_url.cannot_be_a_base() {
22            false
23        } else {
24            let request_path = request_url.path();
25            let cookie_path = &*self.0;
26            // o  The cookie-path and the request-path are identical.
27            cookie_path == request_path
28                || (request_path.starts_with(cookie_path)
29                    && (cookie_path.ends_with('/')
30                        || &request_path[cookie_path.len()..=cookie_path.len()] == "/"))
31        }
32    }
33
34    /// Returns true if this `CookiePath` was set from a Path attribute; this allows us to
35    /// distinguish from the case where Path was explicitly set to "/"
36    pub fn is_from_path_attr(&self) -> bool {
37        self.1
38    }
39
40    // The user agent MUST use an algorithm equivalent to the following
41    // algorithm to compute the default-path of a cookie:
42    //
43    // 1.  Let uri-path be the path portion of the request-uri if such a
44    //     portion exists (and empty otherwise).  For example, if the
45    //     request-uri contains just a path (and optional query string),
46    //     then the uri-path is that path (without the %x3F ("?") character
47    //     or query string), and if the request-uri contains a full
48    //     absoluteURI, the uri-path is the path component of that URI.
49    //
50    // 2.  If the uri-path is empty or if the first character of the uri-
51    //     path is not a %x2F ("/") character, output %x2F ("/") and skip
52    //     the remaining steps.
53    //
54    // 3.  If the uri-path contains no more than one %x2F ("/") character,
55    //     output %x2F ("/") and skip the remaining step.
56    //
57    // 4.  Output the characters of the uri-path from the first character up
58    //     to, but not including, the right-most %x2F ("/").
59    /// Determine the default-path of `request_url` per
60    /// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4)
61    pub fn default_path(request_url: &Url) -> CookiePath {
62        let cp = if request_url.cannot_be_a_base() {
63            // non-relative path scheme, default to "/" (uri-path "empty", case 2)
64            "/".into()
65        } else {
66            let path = request_url.path();
67            match path.rfind('/') {
68                None => "/".into(),                   // no "/" in string, default to "/" (case 2)
69                Some(i) => path[0..max(i, 1)].into(), // case 4 (subsumes case 3)
70            }
71        };
72        CookiePath(cp, false)
73    }
74
75    /// Attempt to parse `path` as a `CookiePath`; if unsuccessful, the default-path of
76    /// `request_url` will be returned as the `CookiePath`.
77    pub fn new(path: &str, request_url: &Url) -> CookiePath {
78        match CookiePath::parse(path) {
79            Some(cp) => cp,
80            None => CookiePath::default_path(request_url),
81        }
82    }
83
84    /// Attempt to parse `path` as a `CookiePath`. If `path` does not have a leading "/",
85    /// `None` is returned.
86    pub fn parse(path: &str) -> Option<CookiePath> {
87        if path.starts_with('/') {
88            Some(CookiePath(String::from(path), true))
89        } else {
90            None
91        }
92    }
93}
94
95impl AsRef<str> for CookiePath {
96    fn as_ref(&self) -> &str {
97        &self.0
98    }
99}
100
101impl Deref for CookiePath {
102    type Target = str;
103    fn deref(&self) -> &Self::Target {
104        &self.0
105    }
106}
107
108impl<'a> From<&'a CookiePath> for String {
109    fn from(cp: &CookiePath) -> String {
110        cp.0.clone()
111    }
112}
113
114impl From<CookiePath> for String {
115    fn from(cp: CookiePath) -> String {
116        cp.0
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::CookiePath;
123    use url::Url;
124
125    #[test]
126    fn default_path() {
127        fn get_path(url: &str) -> String {
128            CookiePath::default_path(&Url::parse(url).expect("unable to parse url in default_path"))
129                .into()
130        }
131        assert_eq!(get_path("data:foobusbar"), "/");
132        assert_eq!(get_path("http://example.com"), "/");
133        assert_eq!(get_path("http://example.com/"), "/");
134        assert_eq!(get_path("http://example.com/foo"), "/");
135        assert_eq!(get_path("http://example.com/foo/"), "/foo");
136        assert_eq!(get_path("http://example.com//foo/"), "//foo");
137        assert_eq!(get_path("http://example.com/foo//"), "/foo/");
138        assert_eq!(get_path("http://example.com/foo/bus/bar"), "/foo/bus");
139        assert_eq!(get_path("http://example.com/foo//bus/bar"), "/foo//bus");
140        assert_eq!(get_path("http://example.com/foo/bus/bar/"), "/foo/bus/bar");
141    }
142
143    fn do_match(exp: bool, cp: &str, rp: &str) {
144        let url = Url::parse(&format!("http://example.com{}", rp))
145            .expect("unable to parse url in do_match");
146        let cp = CookiePath::parse(cp).expect("unable to parse CookiePath in do_match");
147        assert!(
148            exp == cp.matches(&url),
149            "\n>> {:?}\nshould{}match\n>> {:?}\n>> {:?}\n",
150            cp,
151            if exp { " " } else { " NOT " },
152            url,
153            url.path()
154        );
155    }
156    fn is_match(cp: &str, rp: &str) {
157        do_match(true, cp, rp);
158    }
159    fn is_mismatch(cp: &str, rp: &str) {
160        do_match(false, cp, rp);
161    }
162
163    #[test]
164    fn bad_paths() {
165        assert!(CookiePath::parse("").is_none());
166        assert!(CookiePath::parse("a/foo").is_none());
167    }
168
169    #[test]
170    fn bad_path_defaults() {
171        fn get_path(cp: &str, url: &str) -> String {
172            CookiePath::new(
173                cp,
174                &Url::parse(url).expect("unable to parse url in bad_path_defaults"),
175            )
176            .into()
177        }
178        assert_eq!(get_path("", "http://example.com/"), "/");
179        assert_eq!(get_path("a/foo", "http://example.com/"), "/");
180        assert_eq!(get_path("", "http://example.com/foo/bar"), "/foo");
181        assert_eq!(get_path("a/foo", "http://example.com/foo/bar"), "/foo");
182        assert_eq!(get_path("", "http://example.com/foo/bar/"), "/foo/bar");
183        assert_eq!(get_path("a/foo", "http://example.com/foo/bar/"), "/foo/bar");
184    }
185
186    #[test]
187    fn shortest_path() {
188        is_match("/", "/");
189    }
190
191    // A request-path path-matches a given cookie-path if at least one of
192    // the following conditions holds:
193    #[test]
194    fn identical_paths() {
195        // o  The cookie-path and the request-path are identical.
196        is_match("/foo/bus", "/foo/bus"); // identical
197        is_mismatch("/foo/bus", "/foo/buss"); // trailing character
198        is_mismatch("/foo/bus", "/zoo/bus"); // character mismatch
199        is_mismatch("/foo/bus", "/zfoo/bus"); // leading character
200    }
201
202    #[test]
203    fn cookie_path_prefix1() {
204        // o  The cookie-path is a prefix of the request-path, and the last
205        //    character of the cookie-path is %x2F ("/").
206        is_match("/foo/", "/foo/bus"); // cookie-path a prefix and ends in "/"
207        is_mismatch("/bar", "/foo/bus"); // cookie-path not a prefix of request-path
208        is_mismatch("/foo/bus/bar", "/foo/bus"); // cookie-path not a prefix of request-path
209        is_mismatch("/fo", "/foo/bus"); // cookie-path a prefix, but last char != "/" and first char in request-path ("o") after prefix != "/"
210    }
211
212    #[test]
213    fn cookie_path_prefix2() {
214        // o  The cookie-path is a prefix of the request-path, and the first
215        //    character of the request-path that is not included in the cookie-
216        //    path is a %x2F ("/") character.
217        is_match("/foo", "/foo/bus"); // cookie-path a prefix of request-path, and next char in request-path = "/"
218        is_mismatch("/bar", "/foo/bus"); // cookie-path not a prefix of request-path
219        is_mismatch("/foo/bus/bar", "/foo/bus"); // cookie-path not a prefix of request-path
220        is_mismatch("/fo", "/foo/bus"); // cookie-path a prefix, but next char in request-path ("o") != "/"
221    }
222}