Treat header names as case-preserving in HeaderMap.

Fixes #92.
This commit is contained in:
Sergio Benitez 2017-01-02 21:33:36 -06:00
parent 82f6f78189
commit 24805bbf16
6 changed files with 88 additions and 43 deletions

View File

@ -3,12 +3,13 @@ use std::borrow::{Borrow, Cow};
use std::fmt;
use http::hyper::header as hyper;
use http::ascii::{UncasedAscii, UncasedAsciiRef};
/// Simple representation of an HTTP header.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Header<'h> {
/// The name of the header.
pub name: Cow<'h, str>,
pub name: UncasedAscii<'h>,
/// The value of the header.
pub value: Cow<'h, str>,
}
@ -46,7 +47,7 @@ impl<'h> Header<'h> {
where N: Into<Cow<'a, str>>, V: Into<Cow<'b, str>>
{
Header {
name: name.into(),
name: UncasedAscii::new(name),
value: value.into()
}
}
@ -69,7 +70,7 @@ impl<T> From<T> for Header<'static> where T: hyper::Header + hyper::HeaderFormat
/// A collection of headers, mapping a header name to its many ordered values.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct HeaderMap<'h> {
headers: HashMap<Cow<'h, str>, Vec<Cow<'h, str>>>
headers: HashMap<UncasedAscii<'h>, Vec<Cow<'h, str>>>
}
impl<'h> HeaderMap<'h> {
@ -94,7 +95,7 @@ impl<'h> HeaderMap<'h> {
/// ```
#[inline]
pub fn contains(&self, name: &str) -> bool {
self.headers.get(name).is_some()
self.headers.get(name.into() : &UncasedAsciiRef).is_some()
}
/// Returns the number of _values_ stored in the map.
@ -158,9 +159,10 @@ impl<'h> HeaderMap<'h> {
/// ```
#[inline]
pub fn get<'a>(&'a self, name: &str) -> impl Iterator<Item=&'a str> {
self.headers.get(name).into_iter().flat_map(|values| {
values.iter().map(|val| val.borrow())
})
self.headers
.get(name.into() : &UncasedAsciiRef)
.into_iter()
.flat_map(|values| values.iter().map(|val| val.borrow()))
}
/// Returns the _first_ value stored for the header with name `name` if
@ -196,10 +198,11 @@ impl<'h> HeaderMap<'h> {
/// ```
#[inline]
pub fn get_one<'a>(&'a self, name: &str) -> Option<&'a str> {
self.headers.get(name).and_then(|values| {
if values.len() >= 1 { Some(values[0].borrow()) }
else { None }
})
self.headers.get(name.into() : &UncasedAsciiRef)
.and_then(|values| {
if values.len() >= 1 { Some(values[0].borrow()) }
else { None }
})
}
/// Replace any header that matches the name of `header.name` with `header`.
@ -287,7 +290,7 @@ impl<'h> HeaderMap<'h> {
pub fn replace_all<'n, 'v: 'h, H>(&mut self, name: H, values: Vec<Cow<'v, str>>)
where 'n: 'h, H: Into<Cow<'n, str>>
{
self.headers.insert(name.into(), values);
self.headers.insert(UncasedAscii::new(name), values);
}
/// Adds `header` into the map. If a header with `header.name` was
@ -365,7 +368,9 @@ impl<'h> HeaderMap<'h> {
pub fn add_all<'n, H>(&mut self, name: H, values: &mut Vec<Cow<'h, str>>)
where 'n:'h, H: Into<Cow<'n, str>>
{
self.headers.entry(name.into()).or_insert(vec![]).append(values)
self.headers.entry(UncasedAscii::new(name))
.or_insert(vec![])
.append(values)
}
/// Remove all of the values for header with name `name`.
@ -386,7 +391,7 @@ impl<'h> HeaderMap<'h> {
/// assert_eq!(map.len(), 1);
#[inline(always)]
pub fn remove(&mut self, name: &str) {
self.headers.remove(name);
self.headers.remove(name.into() : &UncasedAsciiRef);
}
/// Removes all of the headers stored in this map and returns a vector
@ -436,7 +441,7 @@ impl<'h> HeaderMap<'h> {
pub fn iter<'s>(&'s self) -> impl Iterator<Item=Header<'s>> {
self.headers.iter().flat_map(|(key, values)| {
values.iter().map(move |val| {
Header::new(key.borrow(), val.borrow())
Header::new(key.as_str(), val.borrow())
})
})
}
@ -464,7 +469,35 @@ impl<'h> HeaderMap<'h> {
#[doc(hidden)]
#[inline(always)]
pub fn into_iter_raw(self)
-> impl Iterator<Item=(Cow<'h, str>, Vec<Cow<'h, str>>)> {
-> impl Iterator<Item=(UncasedAscii<'h>, Vec<Cow<'h, str>>)> {
self.headers.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::HeaderMap;
#[test]
fn case_insensitive_add_get() {
let mut map = HeaderMap::new();
map.add_raw("content-type", "application/json");
let ct = map.get_one("Content-Type");
assert_eq!(ct, Some("application/json"));
let ct2 = map.get_one("CONTENT-TYPE");
assert_eq!(ct2, Some("application/json"))
}
#[test]
fn case_insensitive_multiadd() {
let mut map = HeaderMap::new();
map.add_raw("x-custom", "a");
map.add_raw("X-Custom", "b");
map.add_raw("x-CUSTOM", "c");
let vals: Vec<_> = map.get("x-CuStOm").collect();
assert_eq!(vals, vec!["a", "b", "c"]);
}
}

View File

@ -1,8 +1,11 @@
use std::fmt;
use std::str::FromStr;
use error::Error;
use http::hyper;
use http::ascii;
use self::Method::*;
// TODO: Support non-standard methods, here and in codegen.
@ -51,30 +54,10 @@ impl Method {
Get | Head | Connect | Trace | Options => false,
}
}
}
impl FromStr for Method {
type Err = Error;
fn from_str(s: &str) -> Result<Method, Error> {
match s {
"GET" | "get" => Ok(Get),
"PUT" | "put" => Ok(Put),
"POST" | "post" => Ok(Post),
"DELETE" | "delete" => Ok(Delete),
"OPTIONS" | "options" => Ok(Options),
"HEAD" | "head" => Ok(Head),
"TRACE" | "trace" => Ok(Trace),
"CONNECT" | "connect" => Ok(Connect),
"PATCH" | "patch" => Ok(Patch),
_ => Err(Error::BadMethod),
}
}
}
impl fmt::Display for Method {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match *self {
#[inline(always)]
pub fn as_str(&self) -> &'static str {
match *self {
Get => "GET",
Put => "PUT",
Post => "POST",
@ -84,6 +67,33 @@ impl fmt::Display for Method {
Trace => "TRACE",
Connect => "CONNECT",
Patch => "PATCH",
})
}
}
}
impl FromStr for Method {
type Err = Error;
// According to the RFC, method names are case-sensitive. But some old
// clients don't follow this, so we just do a case-insensitive match here.
fn from_str(s: &str) -> Result<Method, Error> {
match s {
x if ascii::uncased_eq(x, Get.as_str()) => Ok(Get),
x if ascii::uncased_eq(x, Put.as_str()) => Ok(Put),
x if ascii::uncased_eq(x, Post.as_str()) => Ok(Post),
x if ascii::uncased_eq(x, Delete.as_str()) => Ok(Delete),
x if ascii::uncased_eq(x, Options.as_str()) => Ok(Options),
x if ascii::uncased_eq(x, Head.as_str()) => Ok(Head),
x if ascii::uncased_eq(x, Trace.as_str()) => Ok(Trace),
x if ascii::uncased_eq(x, Connect.as_str()) => Ok(Connect),
x if ascii::uncased_eq(x, Patch.as_str()) => Ok(Patch),
_ => Err(Error::BadMethod),
}
}
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.as_str().fmt(f)
}
}

View File

@ -13,6 +13,7 @@ mod method;
mod content_type;
mod status;
mod header;
mod ascii;
pub use self::method::Method;
pub use self::content_type::ContentType;

View File

@ -3,6 +3,7 @@
#![feature(drop_types_in_const)]
#![feature(associated_consts)]
#![feature(const_fn)]
#![feature(type_ascription)]
//! # Rocket - Core API Documentation
//!

View File

@ -1023,7 +1023,7 @@ impl<'r> Response<'r> {
}
for (name, values) in other.headers.into_iter_raw() {
self.headers.replace_all(name, values);
self.headers.replace_all(name.into_cow(), values);
}
}
@ -1072,7 +1072,7 @@ impl<'r> Response<'r> {
}
for (name, mut values) in other.headers.into_iter_raw() {
self.headers.add_all(name, &mut values);
self.headers.add_all(name.into_cow(), &mut values);
}
}
}

View File

@ -90,7 +90,7 @@ impl Rocket {
*hyp_res.status_mut() = hyper::StatusCode::from_u16(response.status().code);
for header in response.headers() {
let name = header.name.into_owned();
let name = header.name.into_string();
let value = vec![header.value.into_owned().into()];
hyp_res.headers_mut().set_raw(name, value);
}