From f520bba0c44a5afd70bfa7cf01ef38077ce0fed2 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Wed, 26 Apr 2017 13:22:21 -0700 Subject: [PATCH] feat(header): change `Cookie` to be map-like The `Cookie` header now has 'set' and `get` methods, to treat the list of cookies as a map. Closes #1145 BREAKING CHANGE: The `Cookie` header is no longer a wrapper over a `Vec`. It must be accessed via its `get` and `append` methods. --- src/header/common/cookie.rs | 195 ++++++++++++++++++++++++++++---- src/header/internals/vec_map.rs | 18 +++ 2 files changed, 193 insertions(+), 20 deletions(-) diff --git a/src/header/common/cookie.rs b/src/header/common/cookie.rs index c37524541d..16c5349ebb 100644 --- a/src/header/common/cookie.rs +++ b/src/header/common/cookie.rs @@ -1,7 +1,10 @@ -use header::{Header, Raw}; -use std::fmt::{self, Display}; +use std::borrow::Cow; +use std::fmt; use std::str::from_utf8; +use header::{Header, Raw}; +use header::internals::VecMap; + /// `Cookie` header, defined in [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.4) /// /// If the user agent does attach a Cookie header field to an HTTP @@ -20,17 +23,69 @@ use std::str::from_utf8; /// use hyper::header::{Headers, Cookie}; /// /// let mut headers = Headers::new(); +/// let mut cookie = Cookie::new(); +/// cookie.append("foo", "bar"); /// -/// headers.set( -/// Cookie(vec![ -/// String::from("foo=bar") -/// ]) -/// ); +/// assert_eq!(cookie.get("foo"), Some("bar")); +/// +/// headers.set(cookie); /// ``` -#[derive(Clone, PartialEq, Debug)] -pub struct Cookie(pub Vec); +#[derive(Clone)] +pub struct Cookie(VecMap, Cow<'static, str>>); + +impl Cookie { + /// Creates a new `Cookie` header. + pub fn new() -> Cookie { + Cookie(VecMap::with_capacity(0)) + } -__hyper__deref!(Cookie => Vec); + /// Sets a name and value for the `Cookie`. + /// + /// # Note + /// + /// This will remove all other instances with the same name, + /// and insert the new value. + pub fn set(&mut self, key: K, value: V) + where K: Into>, + V: Into>, + { + let key = key.into(); + let value = value.into(); + self.0.remove_all(&key); + self.0.append(key, value); + } + + /// Append a name and value for the `Cookie`. + /// + /// # Note + /// + /// Cookies are allowed to set a name with a + /// a value multiple times. For example: + /// + /// ``` + /// use hyper::header::Cookie; + /// let mut cookie = Cookie::new(); + /// cookie.append("foo", "bar"); + /// cookie.append("foo", "quux"); + /// assert_eq!(cookie.to_string(), "foo=bar; foo=quux"); + pub fn append(&mut self, key: K, value: V) + where K: Into>, + V: Into>, + { + self.0.append(key.into(), value.into()); + } + + /// Get a value for the name, if it exists. + /// + /// # Note + /// + /// Only returns the first instance found. To access + /// any other values associated with the name, parse + /// the `str` representation. + pub fn get(&self, key: &str) -> Option<&str> { + self.0.get(key).map(AsRef::as_ref) + } +} impl Header for Cookie { fn header_name() -> &'static str { @@ -39,16 +94,22 @@ impl Header for Cookie { } fn parse_header(raw: &Raw) -> ::Result { - let mut cookies = Vec::with_capacity(raw.len()); + let mut vec_map = VecMap::with_capacity(raw.len()); for cookies_raw in raw.iter() { let cookies_str = try!(from_utf8(&cookies_raw[..])); for cookie_str in cookies_str.split(';') { - cookies.push(cookie_str.trim().to_owned()) + let mut key_val = cookie_str.splitn(2, '='); + let key_val = (key_val.next(), key_val.next()); + if let (Some(key), Some(val)) = key_val { + vec_map.insert(key.trim().to_owned().into(), val.trim().to_owned().into()); + } else { + return Err(::Error::Header); + } } } - if !cookies.is_empty() { - Ok(Cookie(cookies)) + if vec_map.len() != 0 { + Ok(Cookie(vec_map)) } else { Err(::Error::Header) } @@ -59,16 +120,110 @@ impl Header for Cookie { } } +impl PartialEq for Cookie { + fn eq(&self, other: &Cookie) -> bool { + if self.0.len() == other.0.len() { + for &(ref k, ref v) in self.0.iter() { + if other.get(k) != Some(v) { + return false; + } + } + true + } else { + false + } + } +} + +impl fmt::Debug for Cookie { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_map() + .entries(self.0.iter().map(|&(ref k, ref v)| (k, v))) + .finish() + } +} + impl fmt::Display for Cookie { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let cookies = &self.0; - for (i, cookie) in cookies.iter().enumerate() { - if i != 0 { - try!(f.write_str("; ")); - } - try!(Display::fmt(&cookie, f)); + let mut iter = self.0.iter(); + if let Some(&(ref key, ref val)) = iter.next() { + try!(write!(f, "{}={}", key, val)); + } + for &(ref key, ref val) in iter { + try!(write!(f, "; {}={}", key, val)); } Ok(()) + } +} + +#[cfg(test)] +mod tests { + use ::header::Header; + use super::Cookie; + + #[test] + fn test_set_and_get() { + let mut cookie = Cookie::new(); + cookie.append("foo", "bar"); + cookie.append(String::from("dyn"), String::from("amic")); + + assert_eq!(cookie.get("foo"), Some("bar")); + assert_eq!(cookie.get("dyn"), Some("amic")); + assert!(cookie.get("nope").is_none()); + + cookie.append("foo", "notbar"); + assert_eq!(cookie.get("foo"), Some("bar")); + + cookie.set("foo", "hi"); + assert_eq!(cookie.get("foo"), Some("hi")); + assert_eq!(cookie.get("dyn"), Some("amic")); + } + + #[test] + fn test_eq() { + let mut cookie = Cookie::new(); + let mut cookie2 = Cookie::new(); + + // empty is equal + assert_eq!(cookie, cookie2); + + // left has more params + cookie.append("foo", "bar"); + assert!(cookie != cookie2); + + // same len, different params + cookie2.append("bar", "foo"); + assert!(cookie != cookie2); + + + // right has more params, and matching KV + cookie2.append("foo", "bar"); + assert!(cookie != cookie2); + + // same params, different order + cookie.append("bar", "foo"); + assert_eq!(cookie, cookie2); + } + + #[test] + fn test_parse() { + let mut cookie = Cookie::new(); + + let parsed = Cookie::parse_header(&b"foo=bar".to_vec().into()).unwrap(); + cookie.append("foo", "bar"); + assert_eq!(cookie, parsed); + + let parsed = Cookie::parse_header(&b"foo=bar; baz=quux".to_vec().into()).unwrap(); + cookie.append("baz", "quux"); + assert_eq!(cookie, parsed); + + let parsed = Cookie::parse_header(&b" foo = bar;baz= quux ".to_vec().into()).unwrap(); + assert_eq!(cookie, parsed); + + let parsed = Cookie::parse_header(&vec![b"foo = bar".to_vec(),b"baz= quux ".to_vec()].into()).unwrap(); + assert_eq!(cookie, parsed); + + Cookie::parse_header(&b"foo;bar=baz;quux".to_vec().into()).unwrap_err(); } } diff --git a/src/header/internals/vec_map.rs b/src/header/internals/vec_map.rs index 9ee0c7c9d6..a8e4611f62 100644 --- a/src/header/internals/vec_map.rs +++ b/src/header/internals/vec_map.rs @@ -19,6 +19,11 @@ impl VecMap { } } + #[inline] + pub fn append(&mut self, key: K, value: V) { + self.vec.push((key, value)); + } + #[inline] pub fn entry(&mut self, key: K) -> Entry { match self.find(&key) { @@ -55,10 +60,23 @@ impl VecMap { pub fn iter(&self) -> ::std::slice::Iter<(K, V)> { self.vec.iter() } + #[inline] pub fn remove + ?Sized>(&mut self, key: &K2) -> Option { self.find(key).map(|pos| self.vec.remove(pos)).map(|(_, v)| v) } + + #[inline] + pub fn remove_all + ?Sized>(&mut self, key: &K2) { + let len = self.vec.len(); + for i in (0..len).rev() { + if key == &self.vec[i].0 { + self.vec.remove(i); + } + } + } + + #[inline] pub fn clear(&mut self) { self.vec.clear();