#![cfg_attr(all(nightly, doc), feature(doc_cfg))]
#![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> {
Indexed(usize, usize),
Concrete(Cow<'c, str>),
}
impl<'c> CookieStr<'c> {
fn indexed(needle: &str, haystack: &str) -> Option<CookieStr<'static>> {
let haystack_start = haystack.as_ptr() as usize;
let needle_start = needle.as_ptr() as usize;
if needle_start < haystack_start {
return None;
}
if (needle_start + needle.len()) > (haystack_start + haystack.len()) {
return None;
}
let start = needle_start - haystack_start;
let end = start + needle.len();
Some(CookieStr::Indexed(start, end))
}
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())),
}
}
}
#[derive(Debug, Clone)]
pub struct Cookie<'c> {
cookie_string: Option<Cow<'c, str>>,
name: CookieStr<'c>,
value: CookieStr<'c>,
expires: Option<Expiration>,
max_age: Option<Duration>,
domain: Option<CookieStr<'c>>,
path: Option<CookieStr<'c>>,
secure: Option<bool>,
http_only: Option<bool>,
same_site: Option<SameSite>,
}
impl<'c> Cookie<'c> {
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,
}
}
#[deprecated(since = "0.18.0", note = "use `Cookie::build(name)` or `Cookie::from(name)`")]
pub fn named<N>(name: N) -> Cookie<'c>
where N: Into<Cow<'c, str>>
{
Cookie::new(name, "")
}
pub fn build<C: Into<Cookie<'c>>>(base: C) -> CookieBuilder<'c> {
CookieBuilder::from(base.into())
}
pub fn parse<S>(s: S) -> Result<Cookie<'c>, ParseError>
where S: Into<Cow<'c, str>>
{
parse_cookie(s.into(), false)
}
#[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.into(), true)
}
#[inline(always)]
pub fn split_parse<S>(string: S) -> SplitCookies<'c>
where S: Into<Cow<'c, str>>
{
SplitCookies {
string: string.into(),
last: 0,
decode: false,
}
}
#[cfg(feature = "percent-encode")]
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))]
#[inline(always)]
pub fn split_parse_encoded<S>(string: S) -> SplitCookies<'c>
where S: Into<Cow<'c, str>>
{
SplitCookies {
string: string.into(),
last: 0,
decode: true,
}
}
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,
}
}
#[inline]
pub fn name(&self) -> &str {
self.name.to_str(self.cookie_string.as_ref())
}
#[inline]
pub fn value(&self) -> &str {
self.value.to_str(self.cookie_string.as_ref())
}
#[inline]
pub fn value_trimmed(&self) -> &str {
#[inline(always)]
fn trim_quotes(s: &str) -> &str {
if s.len() < 2 {
return s;
}
let bytes = s.as_bytes();
match (bytes.first(), bytes.last()) {
(Some(b'"'), Some(b'"')) => &s[1..(s.len() - 1)],
_ => s
}
}
trim_quotes(self.value())
}
#[inline]
pub fn name_value(&self) -> (&str, &str) {
(self.name(), self.value())
}
#[inline]
pub fn name_value_trimmed(&self) -> (&str, &str) {
(self.name(), self.value_trimmed())
}
#[inline]
pub fn http_only(&self) -> Option<bool> {
self.http_only
}
#[inline]
pub fn secure(&self) -> Option<bool> {
self.secure
}
#[inline]
pub fn same_site(&self) -> Option<SameSite> {
self.same_site
}
#[inline]
pub fn max_age(&self) -> Option<Duration> {
self.max_age
}
#[inline]
pub fn path(&self) -> Option<&str> {
match self.path {
Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())),
None => None,
}
}
#[inline]
pub fn domain(&self) -> Option<&str> {
match self.domain {
Some(ref c) => {
let domain = c.to_str(self.cookie_string.as_ref());
domain.strip_prefix(".").or(Some(domain))
},
None => None,
}
}
#[inline]
pub fn expires(&self) -> Option<Expiration> {
self.expires
}
#[inline]
pub fn expires_datetime(&self) -> Option<OffsetDateTime> {
self.expires.and_then(|e| e.datetime())
}
pub fn set_name<N: Into<Cow<'c, str>>>(&mut self, name: N) {
self.name = CookieStr::Concrete(name.into())
}
pub fn set_value<V: Into<Cow<'c, str>>>(&mut self, value: V) {
self.value = CookieStr::Concrete(value.into())
}
#[inline]
pub fn set_http_only<T: Into<Option<bool>>>(&mut self, value: T) {
self.http_only = value.into();
}
#[inline]
pub fn set_secure<T: Into<Option<bool>>>(&mut self, value: T) {
self.secure = value.into();
}
#[inline]
pub fn set_same_site<T: Into<Option<SameSite>>>(&mut self, value: T) {
self.same_site = value.into();
}
#[inline]
pub fn set_max_age<D: Into<Option<Duration>>>(&mut self, value: D) {
self.max_age = value.into();
}
pub fn set_path<P: Into<Cow<'c, str>>>(&mut self, path: P) {
self.path = Some(CookieStr::Concrete(path.into()));
}
pub fn unset_path(&mut self) {
self.path = None;
}
pub fn set_domain<D: Into<Cow<'c, str>>>(&mut self, domain: D) {
self.domain = Some(CookieStr::Concrete(domain.into()));
}
pub fn unset_domain(&mut self) {
self.domain = None;
}
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);
self.expires = Some(time.into()
.map(|time| std::cmp::min(time, MAX_DATETIME)));
}
pub fn unset_expires(&mut self) {
self.expires = None;
}
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);
}
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(())
}
#[inline]
pub fn name_raw(&self) -> Option<&'c str> {
self.cookie_string.as_ref()
.and_then(|s| self.name.to_raw_str(s))
}
#[inline]
pub fn value_raw(&self) -> Option<&'c str> {
self.cookie_string.as_ref()
.and_then(|s| self.value.to_raw_str(s))
}
#[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,
}
}
#[inline]
pub fn domain_raw(&self) -> Option<&'c str> {
match (self.domain.as_ref(), self.cookie_string.as_ref()) {
(Some(domain), Some(string)) => match domain.to_raw_str(string) {
Some(s) => s.strip_prefix(".").or(Some(s)),
None => None,
}
_ => None,
}
}
#[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)
}
#[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)
}
}
pub struct SplitCookies<'c> {
string: Cow<'c, str>,
last: usize,
decode: bool,
}
impl<'c> Iterator for SplitCookies<'c> {
type Item = Result<Cookie<'c>, ParseError>;
fn next(&mut self) -> Option<Self::Item> {
while self.last < self.string.len() {
let i = self.last;
let j = self.string[i..]
.find(';')
.map(|k| i + k)
.unwrap_or(self.string.len());
self.last = j + 1;
if self.string[i..j].chars().all(|c| c.is_whitespace()) {
continue;
}
return Some(match self.string {
Cow::Borrowed(s) => parse_cookie(s[i..j].trim(), self.decode),
Cow::Owned(ref s) => parse_cookie(s[i..j].trim().to_owned(), self.decode),
})
}
None
}
}
#[cfg(feature = "percent-encode")]
mod encoding {
use percent_encoding::{AsciiSet, CONTROLS};
const FRAGMENT: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`');
const PATH: &AsciiSet = &FRAGMENT
.add(b'#')
.add(b'?')
.add(b'{')
.add(b'}');
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'%');
const COOKIE: &AsciiSet = &USERINFO
.add(b'(')
.add(b')')
.add(b',');
pub fn encode(string: &str) -> impl std::fmt::Display + '_ {
percent_encoding::percent_encode(string.as_bytes(), COOKIE)
}
}
#[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 }
}
#[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
}
#[inline]
pub fn stripped(mut self) -> Self {
self.strip = true;
self
}
}
impl<'c> fmt::Display for Cookie<'c> {
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
}
}
impl<'a> From<&'a str> for Cookie<'a> {
fn from(name: &'a str) -> Self {
Cookie::new(name, "")
}
}
impl From<String> for Cookie<'static> {
fn from(name: String) -> Self {
Cookie::new(name, "")
}
}
impl<'a> From<Cow<'a, str>> for Cookie<'a> {
fn from(name: Cow<'a, str>) -> Self {
Cookie::new(name, "")
}
}
impl<'a, N, V> From<(N, V)> for Cookie<'a>
where N: Into<Cow<'a, str>>,
V: Into<Cow<'a, str>>
{
fn from((name, value): (N, V)) -> Self {
Cookie::new(name, value)
}
}
impl<'a> From<CookieBuilder<'a>> for Cookie<'a> {
fn from(builder: CookieBuilder<'a>) -> Self {
builder.build()
}
}
impl<'a> AsRef<Cookie<'a>> for Cookie<'a> {
fn as_ref(&self) -> &Cookie<'a> {
self
}
}
impl<'a> AsMut<Cookie<'a>> for Cookie<'a> {
fn as_mut(&mut self) -> &mut Cookie<'a> {
self
}
}
#[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);
assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly");
let cookie = Cookie::build(("foo", "bar")).max_age(Duration::seconds(10));
assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10");
let cookie = Cookie::build(("foo", "bar")).secure(true);
assert_eq!(&cookie.to_string(), "foo=bar; Secure");
let cookie = Cookie::build(("foo", "bar")).path("/");
assert_eq!(&cookie.to_string(), "foo=bar; Path=/");
let cookie = Cookie::build(("foo", "bar")).domain("www.rust-lang.org");
assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org");
let cookie = Cookie::build(("foo", "bar")).domain(".rust-lang.org");
assert_eq!(&cookie.to_string(), "foo=bar; Domain=rust-lang.org");
let cookie = Cookie::build(("foo", "bar")).domain("rust-lang.org");
assert_eq!(&cookie.to_string(), "foo=bar; Domain=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);
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);
assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict");
let cookie = Cookie::build(("foo", "bar")).same_site(SameSite::Lax);
assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Lax");
let mut cookie = Cookie::build(("foo", "bar")).same_site(SameSite::None).build();
assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None; Secure");
cookie.set_same_site(None);
assert_eq!(&cookie.to_string(), "foo=bar");
let mut c = Cookie::build(("foo", "bar")).same_site(SameSite::None).secure(false).build();
assert_eq!(&c.to_string(), "foo=bar; SameSite=None");
c.set_secure(true);
assert_eq!(&c.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);
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);
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) = {
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) = {
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) = {
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::new("foo !%?=", "bar;;, a");
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"));
}
#[test]
fn split_parse() {
let cases = [
("", vec![]),
(";;", vec![]),
("name=value", vec![("name", "value")]),
("a=%20", vec![("a", "%20")]),
("a=d#$%^&*()_", vec![("a", "d#$%^&*()_")]),
(" name=value ", vec![("name", "value")]),
("name=value ", vec![("name", "value")]),
("name=value;;other=key", vec![("name", "value"), ("other", "key")]),
("name=value; ;other=key", vec![("name", "value"), ("other", "key")]),
("name=value ; ;other=key", vec![("name", "value"), ("other", "key")]),
("name=value ; ; other=key", vec![("name", "value"), ("other", "key")]),
("name=value ; ; other=key ", vec![("name", "value"), ("other", "key")]),
("name=value ; ; other=key;; ", vec![("name", "value"), ("other", "key")]),
(";name=value ; ; other=key ", vec![("name", "value"), ("other", "key")]),
(";a=1 ; ; b=2 ", vec![("a", "1"), ("b", "2")]),
(";a=1 ; ; b= ", vec![("a", "1"), ("b", "")]),
(";a=1 ; ; =v ; c=", vec![("a", "1"), ("c", "")]),
(" ; a=1 ; ; =v ; ;;c=", vec![("a", "1"), ("c", "")]),
(" ; a=1 ; ; =v ; ;;c=== ", vec![("a", "1"), ("c", "==")]),
];
for (string, expected) in cases {
let actual: Vec<_> = Cookie::split_parse(string)
.filter_map(|parse| parse.ok())
.map(|c| (c.name_raw().unwrap(), c.value_raw().unwrap()))
.collect();
assert_eq!(expected, actual);
}
}
#[test]
#[cfg(feature = "percent-encode")]
fn split_parse_encoded() {
let cases = [
("", vec![]),
(";;", vec![]),
("name=val%20ue", vec![("name", "val ue")]),
("foo%20!%25%3F%3D=bar%3B%3B%2C%20a", vec![("foo !%?=", "bar;;, a")]),
(
"name=val%20ue ; ; foo%20!%25%3F%3D=bar%3B%3B%2C%20a",
vec![("name", "val ue"), ("foo !%?=", "bar;;, a")]
),
];
for (string, expected) in cases {
let cookies: Vec<_> = Cookie::split_parse_encoded(string)
.filter_map(|parse| parse.ok())
.collect();
let actual: Vec<_> = cookies.iter()
.map(|c| c.name_value())
.collect();
assert_eq!(expected, actual);
}
}
}