use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::ops::Deref;
use url::Url;
/// Returns true if `request_url` path-matches `path` per
/// [IETF RFC6265 Section 5.1.4](http://tools.ietf.org/html/rfc6265#section-5.1.4)
pub fn is_match(path: &str, request_url: &Url) -> bool {
CookiePath::parse(path).map_or(false, |cp| cp.matches(request_url))
}
/// The path of a `Cookie`
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord)]
pub struct CookiePath(String, bool);
impl CookiePath {
/// Determine if `request_url` path-matches this `CookiePath` per
/// [IETF RFC6265 Section 5.1.4](http://tools.ietf.org/html/rfc6265#section-5.1.4)
pub fn matches(&self, request_url: &Url) -> bool {
if request_url.cannot_be_a_base() {
false
} else {
let request_path = request_url.path();
let cookie_path = &*self.0;
// o The cookie-path and the request-path are identical.
cookie_path == request_path
|| (request_path.starts_with(cookie_path)
&& (cookie_path.ends_with('/')
|| &request_path[cookie_path.len()..=cookie_path.len()] == "/"))
}
}
/// Returns true if this `CookiePath` was set from a Path attribute; this allows us to
/// distinguish from the case where Path was explicitly set to "/"
pub fn is_from_path_attr(&self) -> bool {
self.1
}
// The user agent MUST use an algorithm equivalent to the following
// algorithm to compute the default-path of a cookie:
//
// 1. Let uri-path be the path portion of the request-uri if such a
// portion exists (and empty otherwise). For example, if the
// request-uri contains just a path (and optional query string),
// then the uri-path is that path (without the %x3F ("?") character
// or query string), and if the request-uri contains a full
// absoluteURI, the uri-path is the path component of that URI.
//
// 2. If the uri-path is empty or if the first character of the uri-
// path is not a %x2F ("/") character, output %x2F ("/") and skip
// the remaining steps.
//
// 3. If the uri-path contains no more than one %x2F ("/") character,
// output %x2F ("/") and skip the remaining step.
//
// 4. Output the characters of the uri-path from the first character up
// to, but not including, the right-most %x2F ("/").
/// Determine the default-path of `request_url` per
/// [IETF RFC6265 Section 5.1.4](http://tools.ietf.org/html/rfc6265#section-5.1.4)
pub fn default_path(request_url: &Url) -> CookiePath {
let cp = if request_url.cannot_be_a_base() {
// non-relative path scheme, default to "/" (uri-path "empty", case 2)
"/".into()
} else {
let path = request_url.path();
match path.rfind('/') {
None => "/".into(), // no "/" in string, default to "/" (case 2)
Some(i) => path[0..max(i, 1)].into(), // case 4 (subsumes case 3)
}
};
CookiePath(cp, false)
}
/// Attempt to parse `path` as a `CookiePath`; if unsuccessful, the default-path of
/// `request_url` will be returned as the `CookiePath`.
pub fn new(path: &str, request_url: &Url) -> CookiePath {
match CookiePath::parse(path) {
Some(cp) => cp,
None => CookiePath::default_path(request_url),
}
}
/// Attempt to parse `path` as a `CookiePath`. If `path` does not have a leading "/",
/// `None` is returned.
pub fn parse(path: &str) -> Option<CookiePath> {
if path.starts_with('/') {
Some(CookiePath(String::from(path), true))
} else {
None
}
}
}
impl AsRef<str> for CookiePath {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Deref for CookiePath {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> From<&'a CookiePath> for String {
fn from(cp: &CookiePath) -> String {
cp.0.clone()
}
}
impl From<CookiePath> for String {
fn from(cp: CookiePath) -> String {
cp.0
}
}
#[cfg(test)]
mod tests {
use super::CookiePath;
use url::Url;
#[test]
fn default_path() {
fn get_path(url: &str) -> String {
CookiePath::default_path(&Url::parse(url).expect("unable to parse url in default_path"))
.into()
}
assert_eq!(get_path("data:foobusbar"), "/");
assert_eq!(get_path("http://example.com"), "/");
assert_eq!(get_path("http://example.com/"), "/");
assert_eq!(get_path("http://example.com/foo"), "/");
assert_eq!(get_path("http://example.com/foo/"), "/foo");
assert_eq!(get_path("http://example.com//foo/"), "//foo");
assert_eq!(get_path("http://example.com/foo//"), "/foo/");
assert_eq!(get_path("http://example.com/foo/bus/bar"), "/foo/bus");
assert_eq!(get_path("http://example.com/foo//bus/bar"), "/foo//bus");
assert_eq!(get_path("http://example.com/foo/bus/bar/"), "/foo/bus/bar");
}
fn do_match(exp: bool, cp: &str, rp: &str) {
let url = Url::parse(&format!("http://example.com{}", rp))
.expect("unable to parse url in do_match");
let cp = CookiePath::parse(cp).expect("unable to parse CookiePath in do_match");
assert!(
exp == cp.matches(&url),
"\n>> {:?}\nshould{}match\n>> {:?}\n>> {:?}\n",
cp,
if exp { " " } else { " NOT " },
url,
url.path()
);
}
fn is_match(cp: &str, rp: &str) {
do_match(true, cp, rp);
}
fn is_mismatch(cp: &str, rp: &str) {
do_match(false, cp, rp);
}
#[test]
fn bad_paths() {
assert!(CookiePath::parse("").is_none());
assert!(CookiePath::parse("a/foo").is_none());
}
#[test]
fn bad_path_defaults() {
fn get_path(cp: &str, url: &str) -> String {
CookiePath::new(
cp,
&Url::parse(url).expect("unable to parse url in bad_path_defaults"),
)
.into()
}
assert_eq!(get_path("", "http://example.com/"), "/");
assert_eq!(get_path("a/foo", "http://example.com/"), "/");
assert_eq!(get_path("", "http://example.com/foo/bar"), "/foo");
assert_eq!(get_path("a/foo", "http://example.com/foo/bar"), "/foo");
assert_eq!(get_path("", "http://example.com/foo/bar/"), "/foo/bar");
assert_eq!(get_path("a/foo", "http://example.com/foo/bar/"), "/foo/bar");
}
#[test]
fn shortest_path() {
is_match("/", "/");
}
// A request-path path-matches a given cookie-path if at least one of
// the following conditions holds:
#[test]
fn identical_paths() {
// o The cookie-path and the request-path are identical.
is_match("/foo/bus", "/foo/bus"); // identical
is_mismatch("/foo/bus", "/foo/buss"); // trailing character
is_mismatch("/foo/bus", "/zoo/bus"); // character mismatch
is_mismatch("/foo/bus", "/zfoo/bus"); // leading character
}
#[test]
fn cookie_path_prefix1() {
// o The cookie-path is a prefix of the request-path, and the last
// character of the cookie-path is %x2F ("/").
is_match("/foo/", "/foo/bus"); // cookie-path a prefix and ends in "/"
is_mismatch("/bar", "/foo/bus"); // cookie-path not a prefix of request-path
is_mismatch("/foo/bus/bar", "/foo/bus"); // cookie-path not a prefix of request-path
is_mismatch("/fo", "/foo/bus"); // cookie-path a prefix, but last char != "/" and first char in request-path ("o") after prefix != "/"
}
#[test]
fn cookie_path_prefix2() {
// o The cookie-path is a prefix of the request-path, and the first
// character of the request-path that is not included in the cookie-
// path is a %x2F ("/") character.
is_match("/foo", "/foo/bus"); // cookie-path a prefix of request-path, and next char in request-path = "/"
is_mismatch("/bar", "/foo/bus"); // cookie-path not a prefix of request-path
is_mismatch("/foo/bus/bar", "/foo/bus"); // cookie-path not a prefix of request-path
is_mismatch("/fo", "/foo/bus"); // cookie-path a prefix, but next char in request-path ("o") != "/"
}
}