diff options
Diffstat (limited to '')
-rw-r--r-- | third_party/rust/cookie/src/lib.rs | 1422 |
1 files changed, 1422 insertions, 0 deletions
diff --git a/third_party/rust/cookie/src/lib.rs b/third_party/rust/cookie/src/lib.rs new file mode 100644 index 0000000000..392728efbc --- /dev/null +++ b/third_party/rust/cookie/src/lib.rs @@ -0,0 +1,1422 @@ +//! HTTP cookie parsing and cookie jar management. +//! +//! This crates provides the [`Cookie`] type, representing an HTTP cookie, and +//! the [`CookieJar`] type, which manages a collection of cookies for session +//! management, recording changes as they are made, and optional automatic +//! cookie encryption and signing. +//! +//! # Usage +//! +//! Add the following to the `[dependencies]` section of your `Cargo.toml`: +//! +//! ```toml +//! cookie = "0.16" +//! ``` +//! +//! # Features +//! +//! This crate exposes several features, all of which are disabled by default: +//! +//! * **`percent-encode`** +//! +//! Enables _percent encoding and decoding_ of names and values in cookies. +//! +//! When this feature is enabled, the [`Cookie::encoded()`] and +//! [`Cookie::parse_encoded()`] methods are available. The `encoded` method +//! returns a wrapper around a `Cookie` whose `Display` implementation +//! percent-encodes the name and value of the cookie. The `parse_encoded` +//! method percent-decodes the name and value of a `Cookie` during parsing. +//! +//! * **`signed`** +//! +//! Enables _signed_ cookies via [`CookieJar::signed()`]. +//! +//! When this feature is enabled, the [`CookieJar::signed()`] method, +//! [`SignedJar`] type, and [`Key`] type are available. The jar acts as "child +//! jar"; operations on the jar automatically sign and verify cookies as they +//! are added and retrieved from the parent jar. +//! +//! * **`private`** +//! +//! Enables _private_ (authenticated, encrypted) cookies via +//! [`CookieJar::private()`]. +//! +//! When this feature is enabled, the [`CookieJar::private()`] method, +//! [`PrivateJar`] type, and [`Key`] type are available. The jar acts as "child +//! jar"; operations on the jar automatically encrypt and decrypt/authenticate +//! cookies as they are added and retrieved from the parent jar. +//! +//! * **`key-expansion`** +//! +//! Enables _key expansion_ or _key derivation_ via [`Key::derive_from()`]. +//! +//! When this feature is enabled, and either `signed` or `private` are _also_ +//! enabled, the [`Key::derive_from()`] method is available. The method can be +//! used to derive a `Key` structure appropriate for use with signed and +//! private jars from cryptographically valid key material that is shorter in +//! length than the full key. +//! +//! * **`secure`** +//! +//! A meta-feature that simultaneously enables `signed`, `private`, and +//! `key-expansion`. +//! +//! You can enable features via `Cargo.toml`: +//! +//! ```toml +//! [dependencies.cookie] +//! features = ["secure", "percent-encode"] +//! ``` + +#![cfg_attr(all(nightly, doc), feature(doc_cfg))] + +#![doc(html_root_url = "https://docs.rs/cookie/0.16")] +#![deny(missing_docs)] + +pub use time; + +mod builder; +mod parse; +mod jar; +mod delta; +mod draft; +mod expiration; + +#[cfg(any(feature = "private", feature = "signed"))] #[macro_use] mod secure; +#[cfg(any(feature = "private", feature = "signed"))] pub use secure::*; + +use std::borrow::Cow; +use std::fmt; +use std::str::FromStr; + +#[allow(unused_imports, deprecated)] +use std::ascii::AsciiExt; + +use time::{Duration, OffsetDateTime, UtcOffset, macros::datetime}; + +use crate::parse::parse_cookie; +pub use crate::parse::ParseError; +pub use crate::builder::CookieBuilder; +pub use crate::jar::{CookieJar, Delta, Iter}; +pub use crate::draft::*; +pub use crate::expiration::*; + +#[derive(Debug, Clone)] +enum CookieStr<'c> { + /// An string derived from indexes (start, end). + Indexed(usize, usize), + /// A string derived from a concrete string. + Concrete(Cow<'c, str>), +} + +impl<'c> CookieStr<'c> { + /// Retrieves the string `self` corresponds to. If `self` is derived from + /// indexes, the corresponding subslice of `string` is returned. Otherwise, + /// the concrete string is returned. + /// + /// # Panics + /// + /// Panics if `self` is an indexed string and `string` is None. + fn to_str<'s>(&'s self, string: Option<&'s Cow<str>>) -> &'s str { + match *self { + CookieStr::Indexed(i, j) => { + let s = string.expect("`Some` base string must exist when \ + converting indexed str to str! (This is a module invariant.)"); + &s[i..j] + }, + CookieStr::Concrete(ref cstr) => &*cstr, + } + } + + #[allow(clippy::ptr_arg)] + fn to_raw_str<'s, 'b: 's>(&'s self, string: &'s Cow<'b, str>) -> Option<&'b str> { + match *self { + CookieStr::Indexed(i, j) => { + match *string { + Cow::Borrowed(s) => Some(&s[i..j]), + Cow::Owned(_) => None, + } + }, + CookieStr::Concrete(_) => None, + } + } + + fn into_owned(self) -> CookieStr<'static> { + use crate::CookieStr::*; + + match self { + Indexed(a, b) => Indexed(a, b), + Concrete(Cow::Owned(c)) => Concrete(Cow::Owned(c)), + Concrete(Cow::Borrowed(c)) => Concrete(Cow::Owned(c.into())), + } + } +} + +/// Representation of an HTTP cookie. +/// +/// # Constructing a `Cookie` +/// +/// To construct a cookie with only a name/value, use [`Cookie::new()`]: +/// +/// ```rust +/// use cookie::Cookie; +/// +/// let cookie = Cookie::new("name", "value"); +/// assert_eq!(&cookie.to_string(), "name=value"); +/// ``` +/// +/// To construct more elaborate cookies, use [`Cookie::build()`] and +/// [`CookieBuilder`] methods: +/// +/// ```rust +/// use cookie::Cookie; +/// +/// let cookie = Cookie::build("name", "value") +/// .domain("www.rust-lang.org") +/// .path("/") +/// .secure(true) +/// .http_only(true) +/// .finish(); +/// ``` +#[derive(Debug, Clone)] +pub struct Cookie<'c> { + /// Storage for the cookie string. Only used if this structure was derived + /// from a string that was subsequently parsed. + cookie_string: Option<Cow<'c, str>>, + /// The cookie's name. + name: CookieStr<'c>, + /// The cookie's value. + value: CookieStr<'c>, + /// The cookie's expiration, if any. + expires: Option<Expiration>, + /// The cookie's maximum age, if any. + max_age: Option<Duration>, + /// The cookie's domain, if any. + domain: Option<CookieStr<'c>>, + /// The cookie's path domain, if any. + path: Option<CookieStr<'c>>, + /// Whether this cookie was marked Secure. + secure: Option<bool>, + /// Whether this cookie was marked HttpOnly. + http_only: Option<bool>, + /// The draft `SameSite` attribute. + same_site: Option<SameSite>, +} + +impl<'c> Cookie<'c> { + /// Creates a new `Cookie` with the given name and value. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let cookie = Cookie::new("name", "value"); + /// assert_eq!(cookie.name_value(), ("name", "value")); + /// ``` + pub fn new<N, V>(name: N, value: V) -> Self + where N: Into<Cow<'c, str>>, + V: Into<Cow<'c, str>> + { + Cookie { + cookie_string: None, + name: CookieStr::Concrete(name.into()), + value: CookieStr::Concrete(value.into()), + expires: None, + max_age: None, + domain: None, + path: None, + secure: None, + http_only: None, + same_site: None, + } + } + + /// Creates a new `Cookie` with the given name and an empty value. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let cookie = Cookie::named("name"); + /// assert_eq!(cookie.name(), "name"); + /// assert!(cookie.value().is_empty()); + /// ``` + pub fn named<N>(name: N) -> Cookie<'c> + where N: Into<Cow<'c, str>> + { + Cookie::new(name, "") + } + + /// Creates a new `CookieBuilder` instance from the given key and value + /// strings. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar").finish(); + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// ``` + pub fn build<N, V>(name: N, value: V) -> CookieBuilder<'c> + where N: Into<Cow<'c, str>>, + V: Into<Cow<'c, str>> + { + CookieBuilder::new(name, value) + } + + /// Parses a `Cookie` from the given HTTP cookie header value string. Does + /// not perform any percent-decoding. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("foo=bar%20baz; HttpOnly").unwrap(); + /// assert_eq!(c.name_value(), ("foo", "bar%20baz")); + /// assert_eq!(c.http_only(), Some(true)); + /// ``` + pub fn parse<S>(s: S) -> Result<Cookie<'c>, ParseError> + where S: Into<Cow<'c, str>> + { + parse_cookie(s, false) + } + + /// Parses a `Cookie` from the given HTTP cookie header value string where + /// the name and value fields are percent-encoded. Percent-decodes the + /// name/value fields. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse_encoded("foo=bar%20baz; HttpOnly").unwrap(); + /// assert_eq!(c.name_value(), ("foo", "bar baz")); + /// assert_eq!(c.http_only(), Some(true)); + /// ``` + #[cfg(feature = "percent-encode")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] + pub fn parse_encoded<S>(s: S) -> Result<Cookie<'c>, ParseError> + where S: Into<Cow<'c, str>> + { + parse_cookie(s, true) + } + + /// Converts `self` into a `Cookie` with a static lifetime with as few + /// allocations as possible. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::new("a", "b"); + /// let owned_cookie = c.into_owned(); + /// assert_eq!(owned_cookie.name_value(), ("a", "b")); + /// ``` + pub fn into_owned(self) -> Cookie<'static> { + Cookie { + cookie_string: self.cookie_string.map(|s| s.into_owned().into()), + name: self.name.into_owned(), + value: self.value.into_owned(), + expires: self.expires, + max_age: self.max_age, + domain: self.domain.map(|s| s.into_owned()), + path: self.path.map(|s| s.into_owned()), + secure: self.secure, + http_only: self.http_only, + same_site: self.same_site, + } + } + + /// Returns the name of `self`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::new("name", "value"); + /// assert_eq!(c.name(), "name"); + /// ``` + #[inline] + pub fn name(&self) -> &str { + self.name.to_str(self.cookie_string.as_ref()) + } + + /// Returns the value of `self`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::new("name", "value"); + /// assert_eq!(c.value(), "value"); + /// ``` + #[inline] + pub fn value(&self) -> &str { + self.value.to_str(self.cookie_string.as_ref()) + } + + /// Returns the name and value of `self` as a tuple of `(name, value)`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::new("name", "value"); + /// assert_eq!(c.name_value(), ("name", "value")); + /// ``` + #[inline] + pub fn name_value(&self) -> (&str, &str) { + (self.name(), self.value()) + } + + /// Returns whether this cookie was marked `HttpOnly` or not. Returns + /// `Some(true)` when the cookie was explicitly set (manually or parsed) as + /// `HttpOnly`, `Some(false)` when `http_only` was manually set to `false`, + /// and `None` otherwise. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("name=value; httponly").unwrap(); + /// assert_eq!(c.http_only(), Some(true)); + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.http_only(), None); + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.http_only(), None); + /// + /// // An explicitly set "false" value. + /// c.set_http_only(false); + /// assert_eq!(c.http_only(), Some(false)); + /// + /// // An explicitly set "true" value. + /// c.set_http_only(true); + /// assert_eq!(c.http_only(), Some(true)); + /// ``` + #[inline] + pub fn http_only(&self) -> Option<bool> { + self.http_only + } + + /// Returns whether this cookie was marked `Secure` or not. Returns + /// `Some(true)` when the cookie was explicitly set (manually or parsed) as + /// `Secure`, `Some(false)` when `secure` was manually set to `false`, and + /// `None` otherwise. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("name=value; Secure").unwrap(); + /// assert_eq!(c.secure(), Some(true)); + /// + /// let mut c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.secure(), None); + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.secure(), None); + /// + /// // An explicitly set "false" value. + /// c.set_secure(false); + /// assert_eq!(c.secure(), Some(false)); + /// + /// // An explicitly set "true" value. + /// c.set_secure(true); + /// assert_eq!(c.secure(), Some(true)); + /// ``` + #[inline] + pub fn secure(&self) -> Option<bool> { + self.secure + } + + /// Returns the `SameSite` attribute of this cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::{Cookie, SameSite}; + /// + /// let c = Cookie::parse("name=value; SameSite=Lax").unwrap(); + /// assert_eq!(c.same_site(), Some(SameSite::Lax)); + /// ``` + #[inline] + pub fn same_site(&self) -> Option<SameSite> { + self.same_site + } + + /// Returns the specified max-age of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.max_age(), None); + /// + /// let c = Cookie::parse("name=value; Max-Age=3600").unwrap(); + /// assert_eq!(c.max_age().map(|age| age.whole_hours()), Some(1)); + /// ``` + #[inline] + pub fn max_age(&self) -> Option<Duration> { + self.max_age + } + + /// Returns the `Path` of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.path(), None); + /// + /// let c = Cookie::parse("name=value; Path=/").unwrap(); + /// assert_eq!(c.path(), Some("/")); + /// + /// let c = Cookie::parse("name=value; path=/sub").unwrap(); + /// assert_eq!(c.path(), Some("/sub")); + /// ``` + #[inline] + pub fn path(&self) -> Option<&str> { + match self.path { + Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), + None => None, + } + } + + /// Returns the `Domain` of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.domain(), None); + /// + /// let c = Cookie::parse("name=value; Domain=crates.io").unwrap(); + /// assert_eq!(c.domain(), Some("crates.io")); + /// ``` + #[inline] + pub fn domain(&self) -> Option<&str> { + match self.domain { + Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), + None => None, + } + } + + /// Returns the [`Expiration`] of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::{Cookie, Expiration}; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.expires(), None); + /// + /// // Here, `cookie.expires_datetime()` returns `None`. + /// let c = Cookie::build("name", "value").expires(None).finish(); + /// assert_eq!(c.expires(), Some(Expiration::Session)); + /// + /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; + /// let cookie_str = format!("name=value; Expires={}", expire_time); + /// let c = Cookie::parse(cookie_str).unwrap(); + /// assert_eq!(c.expires().and_then(|e| e.datetime()).map(|t| t.year()), Some(2017)); + /// ``` + #[inline] + pub fn expires(&self) -> Option<Expiration> { + self.expires + } + + /// Returns the expiration date-time of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.expires_datetime(), None); + /// + /// // Here, `cookie.expires()` returns `Some`. + /// let c = Cookie::build("name", "value").expires(None).finish(); + /// assert_eq!(c.expires_datetime(), None); + /// + /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; + /// let cookie_str = format!("name=value; Expires={}", expire_time); + /// let c = Cookie::parse(cookie_str).unwrap(); + /// assert_eq!(c.expires_datetime().map(|t| t.year()), Some(2017)); + /// ``` + #[inline] + pub fn expires_datetime(&self) -> Option<OffsetDateTime> { + self.expires.and_then(|e| e.datetime()) + } + + /// Sets the name of `self` to `name`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.name(), "name"); + /// + /// c.set_name("foo"); + /// assert_eq!(c.name(), "foo"); + /// ``` + pub fn set_name<N: Into<Cow<'c, str>>>(&mut self, name: N) { + self.name = CookieStr::Concrete(name.into()) + } + + /// Sets the value of `self` to `value`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.value(), "value"); + /// + /// c.set_value("bar"); + /// assert_eq!(c.value(), "bar"); + /// ``` + pub fn set_value<V: Into<Cow<'c, str>>>(&mut self, value: V) { + self.value = CookieStr::Concrete(value.into()) + } + + /// Sets the value of `http_only` in `self` to `value`. If `value` is + /// `None`, the field is unset. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.http_only(), None); + /// + /// c.set_http_only(true); + /// assert_eq!(c.http_only(), Some(true)); + /// + /// c.set_http_only(false); + /// assert_eq!(c.http_only(), Some(false)); + /// + /// c.set_http_only(None); + /// assert_eq!(c.http_only(), None); + /// ``` + #[inline] + pub fn set_http_only<T: Into<Option<bool>>>(&mut self, value: T) { + self.http_only = value.into(); + } + + /// Sets the value of `secure` in `self` to `value`. If `value` is `None`, + /// the field is unset. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.secure(), None); + /// + /// c.set_secure(true); + /// assert_eq!(c.secure(), Some(true)); + /// + /// c.set_secure(false); + /// assert_eq!(c.secure(), Some(false)); + /// + /// c.set_secure(None); + /// assert_eq!(c.secure(), None); + /// ``` + #[inline] + pub fn set_secure<T: Into<Option<bool>>>(&mut self, value: T) { + self.secure = value.into(); + } + + /// Sets the value of `same_site` in `self` to `value`. If `value` is + /// `None`, the field is unset. If `value` is `SameSite::None`, the "Secure" + /// flag will be set when the cookie is written out unless `secure` is + /// explicitly set to `false` via [`Cookie::set_secure()`] or the equivalent + /// builder method. + /// + /// [HTTP draft]: https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 + /// + /// # Example + /// + /// ``` + /// use cookie::{Cookie, SameSite}; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.same_site(), None); + /// + /// c.set_same_site(SameSite::None); + /// assert_eq!(c.same_site(), Some(SameSite::None)); + /// assert_eq!(c.to_string(), "name=value; SameSite=None; Secure"); + /// + /// c.set_secure(false); + /// assert_eq!(c.to_string(), "name=value; SameSite=None"); + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.same_site(), None); + /// + /// c.set_same_site(SameSite::Strict); + /// assert_eq!(c.same_site(), Some(SameSite::Strict)); + /// assert_eq!(c.to_string(), "name=value; SameSite=Strict"); + /// + /// c.set_same_site(None); + /// assert_eq!(c.same_site(), None); + /// assert_eq!(c.to_string(), "name=value"); + /// ``` + #[inline] + pub fn set_same_site<T: Into<Option<SameSite>>>(&mut self, value: T) { + self.same_site = value.into(); + } + + /// Sets the value of `max_age` in `self` to `value`. If `value` is `None`, + /// the field is unset. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::time::Duration; + /// + /// # fn main() { + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.max_age(), None); + /// + /// c.set_max_age(Duration::hours(10)); + /// assert_eq!(c.max_age(), Some(Duration::hours(10))); + /// + /// c.set_max_age(None); + /// assert!(c.max_age().is_none()); + /// # } + /// ``` + #[inline] + pub fn set_max_age<D: Into<Option<Duration>>>(&mut self, value: D) { + self.max_age = value.into(); + } + + /// Sets the `path` of `self` to `path`. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.path(), None); + /// + /// c.set_path("/"); + /// assert_eq!(c.path(), Some("/")); + /// ``` + pub fn set_path<P: Into<Cow<'c, str>>>(&mut self, path: P) { + self.path = Some(CookieStr::Concrete(path.into())); + } + + /// Unsets the `path` of `self`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.path(), None); + /// + /// c.set_path("/"); + /// assert_eq!(c.path(), Some("/")); + /// + /// c.unset_path(); + /// assert_eq!(c.path(), None); + /// ``` + pub fn unset_path(&mut self) { + self.path = None; + } + + /// Sets the `domain` of `self` to `domain`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.domain(), None); + /// + /// c.set_domain("rust-lang.org"); + /// assert_eq!(c.domain(), Some("rust-lang.org")); + /// ``` + pub fn set_domain<D: Into<Cow<'c, str>>>(&mut self, domain: D) { + self.domain = Some(CookieStr::Concrete(domain.into())); + } + + /// Unsets the `domain` of `self`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.domain(), None); + /// + /// c.set_domain("rust-lang.org"); + /// assert_eq!(c.domain(), Some("rust-lang.org")); + /// + /// c.unset_domain(); + /// assert_eq!(c.domain(), None); + /// ``` + pub fn unset_domain(&mut self) { + self.domain = None; + } + + /// Sets the expires field of `self` to `time`. If `time` is `None`, an + /// expiration of [`Session`](Expiration::Session) is set. + /// + /// # Example + /// + /// ``` + /// # extern crate cookie; + /// use cookie::{Cookie, Expiration}; + /// use cookie::time::{Duration, OffsetDateTime}; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.expires(), None); + /// + /// let mut now = OffsetDateTime::now_utc(); + /// now += Duration::weeks(52); + /// + /// c.set_expires(now); + /// assert!(c.expires().is_some()); + /// + /// c.set_expires(None); + /// assert_eq!(c.expires(), Some(Expiration::Session)); + /// ``` + pub fn set_expires<T: Into<Expiration>>(&mut self, time: T) { + static MAX_DATETIME: OffsetDateTime = datetime!(9999-12-31 23:59:59.999_999 UTC); + + // RFC 6265 requires dates not to exceed 9999 years. + self.expires = Some(time.into() + .map(|time| std::cmp::min(time, MAX_DATETIME))); + } + + /// Unsets the `expires` of `self`. + /// + /// # Example + /// + /// ``` + /// use cookie::{Cookie, Expiration}; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.expires(), None); + /// + /// c.set_expires(None); + /// assert_eq!(c.expires(), Some(Expiration::Session)); + /// + /// c.unset_expires(); + /// assert_eq!(c.expires(), None); + /// ``` + pub fn unset_expires(&mut self) { + self.expires = None; + } + + /// Makes `self` a "permanent" cookie by extending its expiration and max + /// age 20 years into the future. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::time::Duration; + /// + /// # fn main() { + /// let mut c = Cookie::new("foo", "bar"); + /// assert!(c.expires().is_none()); + /// assert!(c.max_age().is_none()); + /// + /// c.make_permanent(); + /// assert!(c.expires().is_some()); + /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); + /// # } + /// ``` + pub fn make_permanent(&mut self) { + let twenty_years = Duration::days(365 * 20); + self.set_max_age(twenty_years); + self.set_expires(OffsetDateTime::now_utc() + twenty_years); + } + + /// Make `self` a "removal" cookie by clearing its value, setting a max-age + /// of `0`, and setting an expiration date far in the past. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::time::Duration; + /// + /// # fn main() { + /// let mut c = Cookie::new("foo", "bar"); + /// c.make_permanent(); + /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); + /// assert_eq!(c.value(), "bar"); + /// + /// c.make_removal(); + /// assert_eq!(c.value(), ""); + /// assert_eq!(c.max_age(), Some(Duration::ZERO)); + /// # } + /// ``` + pub fn make_removal(&mut self) { + self.set_value(""); + self.set_max_age(Duration::seconds(0)); + self.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); + } + + fn fmt_parameters(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(true) = self.http_only() { + write!(f, "; HttpOnly")?; + } + + if let Some(same_site) = self.same_site() { + write!(f, "; SameSite={}", same_site)?; + + if same_site.is_none() && self.secure().is_none() { + write!(f, "; Secure")?; + } + } + + if let Some(true) = self.secure() { + write!(f, "; Secure")?; + } + + if let Some(path) = self.path() { + write!(f, "; Path={}", path)?; + } + + if let Some(domain) = self.domain() { + write!(f, "; Domain={}", domain)?; + } + + if let Some(max_age) = self.max_age() { + write!(f, "; Max-Age={}", max_age.whole_seconds())?; + } + + if let Some(time) = self.expires_datetime() { + let time = time.to_offset(UtcOffset::UTC); + write!(f, "; Expires={}", time.format(&crate::parse::FMT1).map_err(|_| fmt::Error)?)?; + } + + Ok(()) + } + + /// Returns the name of `self` as a string slice of the raw string `self` + /// was originally parsed from. If `self` was not originally parsed from a + /// raw string, returns `None`. + /// + /// This method differs from [`Cookie::name()`] in that it returns a string + /// with the same lifetime as the originally parsed string. This lifetime + /// may outlive `self`. If a longer lifetime is not required, or you're + /// unsure if you need a longer lifetime, use [`Cookie::name()`]. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let cookie_string = format!("{}={}", "foo", "bar"); + /// + /// // `c` will be dropped at the end of the scope, but `name` will live on + /// let name = { + /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); + /// c.name_raw() + /// }; + /// + /// assert_eq!(name, Some("foo")); + /// ``` + #[inline] + pub fn name_raw(&self) -> Option<&'c str> { + self.cookie_string.as_ref() + .and_then(|s| self.name.to_raw_str(s)) + } + + /// Returns the value of `self` as a string slice of the raw string `self` + /// was originally parsed from. If `self` was not originally parsed from a + /// raw string, returns `None`. + /// + /// This method differs from [`Cookie::value()`] in that it returns a + /// string with the same lifetime as the originally parsed string. This + /// lifetime may outlive `self`. If a longer lifetime is not required, or + /// you're unsure if you need a longer lifetime, use [`Cookie::value()`]. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let cookie_string = format!("{}={}", "foo", "bar"); + /// + /// // `c` will be dropped at the end of the scope, but `value` will live on + /// let value = { + /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); + /// c.value_raw() + /// }; + /// + /// assert_eq!(value, Some("bar")); + /// ``` + #[inline] + pub fn value_raw(&self) -> Option<&'c str> { + self.cookie_string.as_ref() + .and_then(|s| self.value.to_raw_str(s)) + } + + /// Returns the `Path` of `self` as a string slice of the raw string `self` + /// was originally parsed from. If `self` was not originally parsed from a + /// raw string, or if `self` doesn't contain a `Path`, or if the `Path` has + /// changed since parsing, returns `None`. + /// + /// This method differs from [`Cookie::path()`] in that it returns a + /// string with the same lifetime as the originally parsed string. This + /// lifetime may outlive `self`. If a longer lifetime is not required, or + /// you're unsure if you need a longer lifetime, use [`Cookie::path()`]. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let cookie_string = format!("{}={}; Path=/", "foo", "bar"); + /// + /// // `c` will be dropped at the end of the scope, but `path` will live on + /// let path = { + /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); + /// c.path_raw() + /// }; + /// + /// assert_eq!(path, Some("/")); + /// ``` + #[inline] + pub fn path_raw(&self) -> Option<&'c str> { + match (self.path.as_ref(), self.cookie_string.as_ref()) { + (Some(path), Some(string)) => path.to_raw_str(string), + _ => None, + } + } + + /// Returns the `Domain` of `self` as a string slice of the raw string + /// `self` was originally parsed from. If `self` was not originally parsed + /// from a raw string, or if `self` doesn't contain a `Domain`, or if the + /// `Domain` has changed since parsing, returns `None`. + /// + /// This method differs from [`Cookie::domain()`] in that it returns a + /// string with the same lifetime as the originally parsed string. This + /// lifetime may outlive `self` struct. If a longer lifetime is not + /// required, or you're unsure if you need a longer lifetime, use + /// [`Cookie::domain()`]. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let cookie_string = format!("{}={}; Domain=crates.io", "foo", "bar"); + /// + /// //`c` will be dropped at the end of the scope, but `domain` will live on + /// let domain = { + /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); + /// c.domain_raw() + /// }; + /// + /// assert_eq!(domain, Some("crates.io")); + /// ``` + #[inline] + pub fn domain_raw(&self) -> Option<&'c str> { + match (self.domain.as_ref(), self.cookie_string.as_ref()) { + (Some(domain), Some(string)) => domain.to_raw_str(string), + _ => None, + } + } + + /// Wraps `self` in an encoded [`Display`]: a cost-free wrapper around + /// `Cookie` whose [`fmt::Display`] implementation percent-encodes the name + /// and value of the wrapped `Cookie`. + /// + /// The returned structure can be chained with [`Display::stripped()`] to + /// display only the name and value. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let mut c = Cookie::build("my name", "this; value?").secure(true).finish(); + /// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F; Secure"); + /// assert_eq!(&c.encoded().stripped().to_string(), "my%20name=this%3B%20value%3F"); + /// ``` + #[cfg(feature = "percent-encode")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] + #[inline(always)] + pub fn encoded<'a>(&'a self) -> Display<'a, 'c> { + Display::new_encoded(self) + } + + /// Wraps `self` in a stripped `Display`]: a cost-free wrapper around + /// `Cookie` whose [`fmt::Display`] implementation prints only the `name` + /// and `value` of the wrapped `Cookie`. + /// + /// The returned structure can be chained with [`Display::encoded()`] to + /// encode the name and value. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let mut c = Cookie::build("key?", "value").secure(true).path("/").finish(); + /// assert_eq!(&c.stripped().to_string(), "key?=value"); + #[cfg_attr(feature = "percent-encode", doc = r##" +// Note: `encoded()` is only available when `percent-encode` is enabled. +assert_eq!(&c.stripped().encoded().to_string(), "key%3F=value"); + #"##)] + /// ``` + #[inline(always)] + pub fn stripped<'a>(&'a self) -> Display<'a, 'c> { + Display::new_stripped(self) + } +} + +#[cfg(feature = "percent-encode")] +mod encoding { + use percent_encoding::{AsciiSet, CONTROLS}; + + /// https://url.spec.whatwg.org/#fragment-percent-encode-set + const FRAGMENT: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'<') + .add(b'>') + .add(b'`'); + + /// https://url.spec.whatwg.org/#path-percent-encode-set + const PATH: &AsciiSet = &FRAGMENT + .add(b'#') + .add(b'?') + .add(b'{') + .add(b'}'); + + /// https://url.spec.whatwg.org/#userinfo-percent-encode-set + const USERINFO: &AsciiSet = &PATH + .add(b'/') + .add(b':') + .add(b';') + .add(b'=') + .add(b'@') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'^') + .add(b'|') + .add(b'%'); + + /// https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + '(', ')' + const COOKIE: &AsciiSet = &USERINFO + .add(b'(') + .add(b')') + .add(b','); + + /// Percent-encode a cookie name or value with the proper encoding set. + pub fn encode(string: &str) -> impl std::fmt::Display + '_ { + percent_encoding::percent_encode(string.as_bytes(), COOKIE) + } +} + +/// Wrapper around `Cookie` whose `Display` implementation either +/// percent-encodes the cookie's name and value, skips displaying the cookie's +/// parameters (only displaying it's name and value), or both. +/// +/// A value of this type can be obtained via [`Cookie::encoded()`] and +/// [`Cookie::stripped()`], or an arbitrary chaining of the two methods. This +/// type should only be used for its `Display` implementation. +/// +/// # Example +/// +/// ```rust +/// use cookie::Cookie; +/// +/// let c = Cookie::build("my name", "this; value%?").secure(true).finish(); +/// assert_eq!(&c.stripped().to_string(), "my name=this; value%?"); +#[cfg_attr(feature = "percent-encode", doc = r##" +// Note: `encoded()` is only available when `percent-encode` is enabled. +assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%25%3F; Secure"); +assert_eq!(&c.stripped().encoded().to_string(), "my%20name=this%3B%20value%25%3F"); +assert_eq!(&c.encoded().stripped().to_string(), "my%20name=this%3B%20value%25%3F"); +"##)] +/// ``` +pub struct Display<'a, 'c: 'a> { + cookie: &'a Cookie<'c>, + #[cfg(feature = "percent-encode")] + encode: bool, + strip: bool, +} + +impl<'a, 'c: 'a> fmt::Display for Display<'a, 'c> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + #[cfg(feature = "percent-encode")] { + if self.encode { + let name = encoding::encode(self.cookie.name()); + let value = encoding::encode(self.cookie.value()); + write!(f, "{}={}", name, value)?; + } else { + write!(f, "{}={}", self.cookie.name(), self.cookie.value())?; + } + } + + #[cfg(not(feature = "percent-encode"))] { + write!(f, "{}={}", self.cookie.name(), self.cookie.value())?; + } + + match self.strip { + true => Ok(()), + false => self.cookie.fmt_parameters(f) + } + } +} + +impl<'a, 'c> Display<'a, 'c> { + #[cfg(feature = "percent-encode")] + fn new_encoded(cookie: &'a Cookie<'c>) -> Self { + Display { cookie, strip: false, encode: true } + } + + fn new_stripped(cookie: &'a Cookie<'c>) -> Self { + Display { cookie, strip: true, #[cfg(feature = "percent-encode")] encode: false } + } + + /// Percent-encode the name and value pair. + #[inline] + #[cfg(feature = "percent-encode")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] + pub fn encoded(mut self) -> Self { + self.encode = true; + self + } + + /// Only display the name and value. + #[inline] + pub fn stripped(mut self) -> Self { + self.strip = true; + self + } +} + +impl<'c> fmt::Display for Cookie<'c> { + /// Formats the cookie `self` as a `Set-Cookie` header value. + /// + /// Does _not_ percent-encode any values. To percent-encode, use + /// [`Cookie::encoded()`]. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let mut cookie = Cookie::build("foo", "bar") + /// .path("/") + /// .finish(); + /// + /// assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); + /// ``` + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}={}", self.name(), self.value())?; + self.fmt_parameters(f) + } +} + +impl FromStr for Cookie<'static> { + type Err = ParseError; + + fn from_str(s: &str) -> Result<Cookie<'static>, ParseError> { + Cookie::parse(s).map(|c| c.into_owned()) + } +} + +impl<'a, 'b> PartialEq<Cookie<'b>> for Cookie<'a> { + fn eq(&self, other: &Cookie<'b>) -> bool { + let so_far_so_good = self.name() == other.name() + && self.value() == other.value() + && self.http_only() == other.http_only() + && self.secure() == other.secure() + && self.max_age() == other.max_age() + && self.expires() == other.expires(); + + if !so_far_so_good { + return false; + } + + match (self.path(), other.path()) { + (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} + (None, None) => {} + _ => return false, + }; + + match (self.domain(), other.domain()) { + (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} + (None, None) => {} + _ => return false, + }; + + true + } +} + +#[cfg(test)] +mod tests { + use crate::{Cookie, SameSite, parse::parse_date}; + use time::{Duration, OffsetDateTime}; + + #[test] + fn format() { + let cookie = Cookie::new("foo", "bar"); + assert_eq!(&cookie.to_string(), "foo=bar"); + + let cookie = Cookie::build("foo", "bar") + .http_only(true).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly"); + + let cookie = Cookie::build("foo", "bar") + .max_age(Duration::seconds(10)).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10"); + + let cookie = Cookie::build("foo", "bar") + .secure(true).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Secure"); + + let cookie = Cookie::build("foo", "bar") + .path("/").finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); + + let cookie = Cookie::build("foo", "bar") + .domain("www.rust-lang.org").finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org"); + + let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; + let expires = parse_date(time_str, &crate::parse::FMT1).unwrap(); + let cookie = Cookie::build("foo", "bar") + .expires(expires).finish(); + assert_eq!(&cookie.to_string(), + "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT"); + + let cookie = Cookie::build("foo", "bar") + .same_site(SameSite::Strict).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict"); + + let cookie = Cookie::build("foo", "bar") + .same_site(SameSite::Lax).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Lax"); + + let mut cookie = Cookie::build("foo", "bar") + .same_site(SameSite::None).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None; Secure"); + + cookie.set_same_site(None); + assert_eq!(&cookie.to_string(), "foo=bar"); + + let mut cookie = Cookie::build("foo", "bar") + .same_site(SameSite::None) + .secure(false) + .finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None"); + cookie.set_secure(true); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None; Secure"); + } + + #[test] + #[ignore] + fn format_date_wraps() { + let expires = OffsetDateTime::UNIX_EPOCH + Duration::MAX; + let cookie = Cookie::build("foo", "bar").expires(expires).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Expires=Fri, 31 Dec 9999 23:59:59 GMT"); + + let expires = time::macros::datetime!(9999-01-01 0:00 UTC) + Duration::days(1000); + let cookie = Cookie::build("foo", "bar").expires(expires).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Expires=Fri, 31 Dec 9999 23:59:59 GMT"); + } + + #[test] + fn cookie_string_long_lifetimes() { + let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); + let (name, value, path, domain) = { + // Create a cookie passing a slice + let c = Cookie::parse(cookie_string.as_str()).unwrap(); + (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) + }; + + assert_eq!(name, Some("bar")); + assert_eq!(value, Some("baz")); + assert_eq!(path, Some("/subdir")); + assert_eq!(domain, Some("crates.io")); + } + + #[test] + fn owned_cookie_string() { + let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); + let (name, value, path, domain) = { + // Create a cookie passing an owned string + let c = Cookie::parse(cookie_string).unwrap(); + (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) + }; + + assert_eq!(name, None); + assert_eq!(value, None); + assert_eq!(path, None); + assert_eq!(domain, None); + } + + #[test] + fn owned_cookie_struct() { + let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io"; + let (name, value, path, domain) = { + // Create an owned cookie + let c = Cookie::parse(cookie_string).unwrap().into_owned(); + + (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) + }; + + assert_eq!(name, None); + assert_eq!(value, None); + assert_eq!(path, None); + assert_eq!(domain, None); + } + + #[test] + #[cfg(feature = "percent-encode")] + fn format_encoded() { + let cookie = Cookie::build("foo !%?=", "bar;;, a").finish(); + let cookie_str = cookie.encoded().to_string(); + assert_eq!(&cookie_str, "foo%20!%25%3F%3D=bar%3B%3B%2C%20a"); + + let cookie = Cookie::parse_encoded(cookie_str).unwrap(); + assert_eq!(cookie.name_value(), ("foo !%?=", "bar;;, a")); + } +} |