From 2da08a975c838ab7e208b4970f862af2a5646b2e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 5 Jan 2017 02:14:44 -0600 Subject: [PATCH] Make Content-Type case-preserving; add 'params' method. --- codegen/src/decorators/route.rs | 10 ++- lib/src/http/ascii.rs | 72 +++++++++++++++-- lib/src/http/content_type.rs | 134 +++++++++++++++++++------------- lib/src/http/method.rs | 20 ++--- lib/src/http/mod.rs | 5 +- 5 files changed, 169 insertions(+), 72 deletions(-) diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index c82967ef..6e2a32e6 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -24,10 +24,14 @@ fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path { fn content_type_to_expr(ecx: &ExtCtxt, ct: Option) -> Option> { ct.map(|ct| { - let (ttype, subtype) = (ct.ttype, ct.subtype); + let (top, sub) = (ct.ttype.as_str(), ct.subtype.as_str()); quote_expr!(ecx, ::rocket::http::ContentType { - ttype: ::std::borrow::Cow::Borrowed($ttype), - subtype: ::std::borrow::Cow::Borrowed($subtype), + ttype: ::rocket::http::ascii::UncasedAscii { + string: ::std::borrow::Cow::Borrowed($top) + }, + subtype: ::rocket::http::ascii::UncasedAscii { + string: ::std::borrow::Cow::Borrowed($sub) + }, params: None }) }) diff --git a/lib/src/http/ascii.rs b/lib/src/http/ascii.rs index 0138e4b9..64509dc4 100644 --- a/lib/src/http/ascii.rs +++ b/lib/src/http/ascii.rs @@ -29,6 +29,34 @@ impl PartialEq for UncasedAsciiRef { } } +impl PartialEq for UncasedAsciiRef { + #[inline(always)] + fn eq(&self, other: &str) -> bool { + self.0.eq_ignore_ascii_case(other) + } +} + +impl PartialEq for str { + #[inline(always)] + fn eq(&self, other: &UncasedAsciiRef) -> bool { + other.0.eq_ignore_ascii_case(self) + } +} + +impl<'a> PartialEq<&'a str> for UncasedAsciiRef { + #[inline(always)] + fn eq(&self, other: & &'a str) -> bool { + self.0.eq_ignore_ascii_case(other) + } +} + +impl<'a> PartialEq for &'a str { + #[inline(always)] + fn eq(&self, other: &UncasedAsciiRef) -> bool { + other.0.eq_ignore_ascii_case(self) + } +} + impl<'a> From<&'a str> for &'a UncasedAsciiRef { #[inline(always)] fn from(string: &'a str) -> &'a UncasedAsciiRef { @@ -65,7 +93,7 @@ impl Ord for UncasedAsciiRef { /// An uncased (case-preserving) ASCII string. #[derive(Clone, Debug)] pub struct UncasedAscii<'s> { - string: Cow<'s, str> + pub string: Cow<'s, str> } impl<'s> UncasedAscii<'s> { @@ -181,6 +209,34 @@ impl<'a, 'b> PartialEq> for UncasedAscii<'a> { } } +impl<'a> PartialEq for UncasedAscii<'a> { + #[inline(always)] + fn eq(&self, other: &str) -> bool { + self.as_ref().eq(other) + } +} + +impl<'b> PartialEq> for str { + #[inline(always)] + fn eq(&self, other: &UncasedAscii<'b>) -> bool { + other.as_ref().eq(self) + } +} + +impl<'a, 'b> PartialEq<&'b str> for UncasedAscii<'a> { + #[inline(always)] + fn eq(&self, other: & &'b str) -> bool { + self.as_ref().eq(other) + } +} + +impl<'a, 'b> PartialEq> for &'a str { + #[inline(always)] + fn eq(&self, other: &UncasedAscii<'b>) -> bool { + other.as_ref().eq(self) + } +} + impl<'s> Eq for UncasedAscii<'s> { } impl<'s> Hash for UncasedAscii<'s> { @@ -215,13 +271,19 @@ mod tests { macro_rules! assert_uncased_eq { ($($string:expr),+) => ({ let mut strings = Vec::new(); - $(strings.push(UncasedAscii::from($string));)+ + $(strings.push($string);)+ for i in 0..strings.len() { for j in i..strings.len() { - let (a, b) = (&strings[i], &strings[j]); - assert_eq!(a, b); - assert_eq!(hash(&a), hash(&b)); + let (str_a, str_b) = (strings[i], strings[j]); + let ascii_a = UncasedAscii::from(str_a); + let ascii_b = UncasedAscii::from(str_b); + assert_eq!(ascii_a, ascii_b); + assert_eq!(hash(&ascii_a), hash(&ascii_b)); + assert_eq!(ascii_a, str_a); + assert_eq!(ascii_b, str_b); + assert_eq!(ascii_a, str_b); + assert_eq!(ascii_b, str_a); } } }) diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index 80339a13..b1dd8c85 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -4,6 +4,7 @@ use std::fmt; use http::Header; use http::hyper::mime::Mime; +use http::ascii::{uncased_eq, UncasedAscii}; use router::Collider; /// Representation of HTTP Content-Types. @@ -39,16 +40,16 @@ use router::Collider; #[derive(Debug, Clone, PartialEq, Hash)] pub struct ContentType { /// The "type" component of the Content-Type. - pub ttype: Cow<'static, str>, + pub ttype: UncasedAscii<'static>, /// The "subtype" component of the Content-Type. - pub subtype: Cow<'static, str>, + pub subtype: UncasedAscii<'static>, /// Semicolon-seperated parameters associated with the Content-Type. - pub params: Option> + pub params: Option> } macro_rules! ctr_params { () => (None); - ($param:expr) => (Some(Cow::Borrowed($param))); + ($param:expr) => (Some(UncasedAscii { string: Cow::Borrowed($param) })); } macro_rules! ctrs { @@ -65,8 +66,8 @@ macro_rules! ctrs { #[doc=""] #[allow(non_upper_case_globals)] pub const $name: ContentType = ContentType { - ttype: Cow::Borrowed($top), - subtype: Cow::Borrowed($sub), + ttype: UncasedAscii { string: Cow::Borrowed($top) }, + subtype: UncasedAscii { string: Cow::Borrowed($sub) }, params: ctr_params!($($param)*) }; )+ @@ -74,12 +75,8 @@ macro_rules! ctrs { /// Returns `true` if this ContentType is known to Rocket, that is, /// there is an associated constant for `self`. pub fn is_known(&self) -> bool { - match (&*self.ttype, &*self.subtype) { - $( - ($top, $sub) => true, - )+ - _ => false - } + $(if self.$check_name() { return true })+ + false } $( @@ -146,24 +143,26 @@ impl ContentType { /// ``` pub fn from_extension(ext: &str) -> ContentType { match ext { - "txt" => ContentType::Plain, - "html" | "htm" => ContentType::HTML, - "xml" => ContentType::XML, - "csv" => ContentType::CSV, - "js" => ContentType::JavaScript, - "css" => ContentType::CSS, - "json" => ContentType::JSON, - "png" => ContentType::PNG, - "gif" => ContentType::GIF, - "bmp" => ContentType::BMP, - "jpeg" | "jpg" => ContentType::JPEG, - "pdf" => ContentType::PDF, + x if uncased_eq(x, "txt") => ContentType::Plain, + x if uncased_eq(x, "html") => ContentType::HTML, + x if uncased_eq(x, "htm") => ContentType::HTML, + x if uncased_eq(x, "xml") => ContentType::XML, + x if uncased_eq(x, "csv") => ContentType::CSV, + x if uncased_eq(x, "js") => ContentType::JavaScript, + x if uncased_eq(x, "css") => ContentType::CSS, + x if uncased_eq(x, "json") => ContentType::JSON, + x if uncased_eq(x, "png") => ContentType::PNG, + x if uncased_eq(x, "gif") => ContentType::GIF, + x if uncased_eq(x, "bmp") => ContentType::BMP, + x if uncased_eq(x, "jpeg") => ContentType::JPEG, + x if uncased_eq(x, "jpg") => ContentType::JPEG, + x if uncased_eq(x, "pdf") => ContentType::PDF, _ => ContentType::Any } } /// Creates a new `ContentType` with type `ttype` and subtype `subtype`. - /// This should be _only_ to construct uncommon Content-Types or custom + /// This should _only_ be used to construct uncommon Content-Types or custom /// Content-Types. Use an associated constant for common Content-Types. /// /// # Example @@ -180,11 +179,7 @@ impl ContentType { pub fn new(ttype: T, subtype: S) -> ContentType where T: Into>, S: Into> { - ContentType { - ttype: ttype.into(), - subtype: subtype.into(), - params: None - } + ContentType::with_params::(ttype, subtype, None) } /// Creates a new `ContentType` with type `ttype`, subtype `subtype`, and @@ -210,11 +205,52 @@ impl ContentType { P: Into> { ContentType { - ttype: ttype.into(), - subtype: subtype.into(), - params: params.map(|p| p.into()) + ttype: UncasedAscii::from(ttype), + subtype: UncasedAscii::from(subtype), + params: params.map(|p| UncasedAscii::from(p)) } } + + /// Returns an iterator over the (key, value) pairs of the Content-Type's + /// parameter list. The iterator will be empty if the Content-Type has no + /// parameters. + /// + /// # Example + /// + /// The `ContentType::Plain` type has one parameter: `charset=utf-8`: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let plain = ContentType::Plain; + /// let plain_params: Vec<_> = plain.params().collect(); + /// assert_eq!(plain_params, vec![("charset", "utf-8")]); + /// ``` + /// + /// The `ContentType::PNG` type has no parameters: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let png = ContentType::PNG; + /// assert_eq!(png.params().count(), 0); + /// ``` + #[inline(always)] + pub fn params<'a>(&'a self) -> impl Iterator + 'a { + let params = match self.params { + Some(ref params) => params.as_str(), + None => "" + }; + + params.split(";") + .filter_map(|param| { + let mut kv = param.split("="); + match (kv.next(), kv.next()) { + (Some(key), Some(val)) => Some((key.trim(), val.trim())), + _ => None + } + }) + } } impl Default for ContentType { @@ -250,30 +286,18 @@ impl From for ContentType { } } -fn is_valid_first_char(c: char) -> bool { - match c { - 'a'...'z' | 'A'...'Z' | '0'...'9' | '*' => true, - _ => false, - } -} - fn is_valid_char(c: char) -> bool { - is_valid_first_char(c) || match c { - '!' | '#' | '$' | '&' | '-' | '^' | '.' | '+' | '_' => true, - _ => false, + match c { + '0'...'9' | 'A'...'Z' | '^'...'~' | '#'...'\'' + | '!' | '*' | '+' | '-' | '.' => true, + _ => false } } fn is_valid_token(string: &str) -> bool { - if string.len() < 1 { - return false; - } - - string.chars().take(1).all(is_valid_first_char) - && string.chars().skip(1).all(is_valid_char) + string.len() >= 1 && string.chars().all(is_valid_char) } - impl FromStr for ContentType { type Err = &'static str; @@ -288,6 +312,7 @@ impl FromStr for ContentType { /// use rocket::http::ContentType; /// /// let json = ContentType::from_str("application/json").unwrap(); + /// assert!(json.is_known()); /// assert_eq!(json, ContentType::JSON); /// ``` /// @@ -355,7 +380,7 @@ impl FromStr for ContentType { trimmed_params.push(param); } - let (ttype, subtype) = (top_s.to_lowercase(), sub_s.to_lowercase()); + let (ttype, subtype) = (top_s.to_string(), sub_s.to_string()); let params = params.map(|_| trimmed_params.join(";")); Ok(ContentType::with_params(ttype, subtype, params)) } @@ -394,8 +419,8 @@ impl Into> for ContentType { impl Collider for ContentType { fn collides_with(&self, other: &ContentType) -> bool { - (self.ttype == "*" || other.ttype == "*" || self.ttype == other.ttype) && - (self.subtype == "*" || other.subtype == "*" || self.subtype == other.subtype) + let collide = |a, b| a == "*" || b == "*" || a == b; + collide(&self.ttype, &other.ttype) && collide(&self.subtype, &other.subtype) } } @@ -460,6 +485,9 @@ mod test { ContentType::with_params("*", "*", Some("charset=utf-8;else=1"))); assert_parse!("*/*; charset=\"utf-8\"; else=1", ContentType::with_params("*", "*", Some("charset=\"utf-8\";else=1"))); + assert_parse!("multipart/form-data; boundary=----WebKitFormBoundarypRshfItmvaC3aEuq", + ContentType::with_params("multipart", "form-data", + Some("boundary=----WebKitFormBoundarypRshfItmvaC3aEuq"))); } #[test] diff --git a/lib/src/http/method.rs b/lib/src/http/method.rs index fa87b714..d0d47a9e 100644 --- a/lib/src/http/method.rs +++ b/lib/src/http/method.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use error::Error; use http::hyper; -use http::ascii; +use http::ascii::uncased_eq; use self::Method::*; @@ -78,15 +78,15 @@ impl FromStr for Method { // clients don't follow this, so we just do a case-insensitive match here. fn from_str(s: &str) -> Result { 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), + x if uncased_eq(x, Get.as_str()) => Ok(Get), + x if uncased_eq(x, Put.as_str()) => Ok(Put), + x if uncased_eq(x, Post.as_str()) => Ok(Post), + x if uncased_eq(x, Delete.as_str()) => Ok(Delete), + x if uncased_eq(x, Options.as_str()) => Ok(Options), + x if uncased_eq(x, Head.as_str()) => Ok(Head), + x if uncased_eq(x, Trace.as_str()) => Ok(Trace), + x if uncased_eq(x, Connect.as_str()) => Ok(Connect), + x if uncased_eq(x, Patch.as_str()) => Ok(Patch), _ => Err(Error::BadMethod), } } diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index 4632beb0..4ccafd0c 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -13,7 +13,10 @@ mod method; mod content_type; mod status; mod header; -mod ascii; + +// We need to export this for codegen, but otherwise it's unnecessary. +// TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) +#[doc(hidden)] pub mod ascii; pub use self::method::Method; pub use self::content_type::ContentType;