diff --git a/src/header/common/etag.rs b/src/header/common/etag.rs index 4bf93abef5..8060eb421b 100644 --- a/src/header/common/etag.rs +++ b/src/header/common/etag.rs @@ -1,4 +1,4 @@ -use header::{Header, HeaderFormat}; +use header::{EntityTag, Header, HeaderFormat}; use std::fmt::{self}; use header::parsing::from_one_raw_str; @@ -9,12 +9,9 @@ use header::parsing::from_one_raw_str; /// which always looks like this: W/ /// See also: https://tools.ietf.org/html/rfc7232#section-2.3 #[derive(Clone, PartialEq, Debug)] -pub struct Etag { - /// Weakness indicator for the tag - pub weak: bool, - /// The opaque string in between the DQUOTEs - pub tag: String -} +pub struct Etag(pub EntityTag); + +deref!(Etag => EntityTag); impl Header for Etag { fn header_name() -> &'static str { @@ -22,75 +19,26 @@ impl Header for Etag { } fn parse_header(raw: &[Vec]) -> Option { - // check that each char in the slice is either: - // 1. %x21, or - // 2. in the range %x23 to %x7E, or - // 3. in the range %x80 to %xFF - fn check_slice_validity(slice: &str) -> bool { - for c in slice.bytes() { - match c { - b'\x21' | b'\x23' ... b'\x7e' | b'\x80' ... b'\xff' => (), - _ => { return false; } - } - } - true - } - from_one_raw_str(raw).and_then(|s: String| { - let length: usize = s.len(); - let slice = &s[]; - - // Early exits: - // 1. The string is empty, or, - // 2. it doesn't terminate in a DQUOTE. - if slice.is_empty() || !slice.ends_with("\"") { - return None; - } - - // The etag is weak if its first char is not a DQUOTE. - if slice.char_at(0) == '"' { - // No need to check if the last char is a DQUOTE, - // we already did that above. - if check_slice_validity(slice.slice_chars(1, length-1)) { - return Some(Etag { - weak: false, - tag: slice.slice_chars(1, length-1).to_string() - }); - } else { - return None; - } - } - - if slice.slice_chars(0, 3) == "W/\"" { - if check_slice_validity(slice.slice_chars(3, length-1)) { - return Some(Etag { - weak: true, - tag: slice.slice_chars(3, length-1).to_string() - }); - } else { - return None; - } - } - - None + s.parse::().and_then(|x| Ok(Etag(x))).ok() }) } } impl HeaderFormat for Etag { fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - if self.weak { + if self.0.weak { try!(fmt.write_str("W/")); } - write!(fmt, "\"{}\"", self.tag) + write!(fmt, "\"{}\"", self.0.tag) } } #[cfg(test)] mod tests { use super::Etag; - use header::Header; + use header::{Header,EntityTag}; #[test] fn test_etag_successes() { @@ -98,34 +46,34 @@ mod tests { let mut etag: Option; etag = Header::parse_header([b"\"foobar\"".to_vec()].as_slice()); - assert_eq!(etag, Some(Etag { + assert_eq!(etag, Some(Etag(EntityTag{ weak: false, tag: "foobar".to_string() - })); + }))); etag = Header::parse_header([b"\"\"".to_vec()].as_slice()); - assert_eq!(etag, Some(Etag { + assert_eq!(etag, Some(Etag(EntityTag{ weak: false, tag: "".to_string() - })); + }))); etag = Header::parse_header([b"W/\"weak-etag\"".to_vec()].as_slice()); - assert_eq!(etag, Some(Etag { + assert_eq!(etag, Some(Etag(EntityTag{ weak: true, tag: "weak-etag".to_string() - })); + }))); etag = Header::parse_header([b"W/\"\x65\x62\"".to_vec()].as_slice()); - assert_eq!(etag, Some(Etag { + assert_eq!(etag, Some(Etag(EntityTag{ weak: true, tag: "\u{0065}\u{0062}".to_string() - })); + }))); etag = Header::parse_header([b"W/\"\"".to_vec()].as_slice()); - assert_eq!(etag, Some(Etag { + assert_eq!(etag, Some(Etag(EntityTag{ weak: true, tag: "".to_string() - })); + }))); } #[test] diff --git a/src/header/mod.rs b/src/header/mod.rs index 03ecdbe797..ca22450de7 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -22,7 +22,7 @@ use unicase::UniCase; use {http, HttpResult, HttpError}; -pub use self::shared::{Encoding, QualityItem, qitem}; +pub use self::shared::{Encoding, EntityTag, QualityItem, qitem}; pub use self::common::*; mod common; diff --git a/src/header/shared/entity.rs b/src/header/shared/entity.rs new file mode 100644 index 0000000000..740ba2abab --- /dev/null +++ b/src/header/shared/entity.rs @@ -0,0 +1,145 @@ +use std::str::FromStr; +use std::fmt::{self, Display}; + +/// An entity tag +/// +/// An Etag consists of a string enclosed by two literal double quotes. +/// Preceding the first double quote is an optional weakness indicator, +/// which always looks like this: W/ +/// See also: https://tools.ietf.org/html/rfc7232#section-2.3 +#[derive(Clone, PartialEq, Debug)] +pub struct EntityTag { + /// Weakness indicator for the tag + pub weak: bool, + /// The opaque string in between the DQUOTEs + pub tag: String +} + +impl Display for EntityTag { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + if self.weak { + try!(write!(fmt, "{}", "W/")); + } + write!(fmt, "{}", self.tag) + } +} + +// check that each char in the slice is either: +// 1. %x21, or +// 2. in the range %x23 to %x7E, or +// 3. in the range %x80 to %xFF +fn check_slice_validity(slice: &str) -> bool { + for c in slice.bytes() { + match c { + b'\x21' | b'\x23' ... b'\x7e' | b'\x80' ... b'\xff' => (), + _ => { return false; } + } + } + true +} + +impl FromStr for EntityTag { + type Err = (); + fn from_str(s: &str) -> Result { + let length: usize = s.len(); + let slice = &s[]; + + // Early exits: + // 1. The string is empty, or, + // 2. it doesn't terminate in a DQUOTE. + if slice.is_empty() || !slice.ends_with("\"") { + return Err(()); + } + + // The etag is weak if its first char is not a DQUOTE. + if slice.char_at(0) == '"' /* '"' */ { + // No need to check if the last char is a DQUOTE, + // we already did that above. + if check_slice_validity(slice.slice_chars(1, length-1)) { + return Ok(EntityTag { + weak: false, + tag: slice.slice_chars(1, length-1).to_string() + }); + } else { + return Err(()); + } + } + + if slice.slice_chars(0, 3) == "W/\"" { + if check_slice_validity(slice.slice_chars(3, length-1)) { + return Ok(EntityTag { + weak: true, + tag: slice.slice_chars(3, length-1).to_string() + }); + } else { + return Err(()); + } + } + + Err(()) + } +} + + +#[cfg(test)] +mod tests { + use super::EntityTag; + + #[test] + fn test_etag_successes() { + // Expected successes + let mut etag : EntityTag = "\"foobar\"".parse().unwrap(); + assert_eq!(etag, (EntityTag { + weak: false, + tag: "foobar".to_string() + })); + + etag = "\"\"".parse().unwrap(); + assert_eq!(etag, EntityTag { + weak: false, + tag: "".to_string() + }); + + etag = "W/\"weak-etag\"".parse().unwrap(); + assert_eq!(etag, EntityTag { + weak: true, + tag: "weak-etag".to_string() + }); + + etag = "W/\"\x65\x62\"".parse().unwrap(); + assert_eq!(etag, EntityTag { + weak: true, + tag: "\u{0065}\u{0062}".to_string() + }); + + etag = "W/\"\"".parse().unwrap(); + assert_eq!(etag, EntityTag { + weak: true, + tag: "".to_string() + }); + } + + #[test] + fn test_etag_failures() { + // Expected failures + let mut etag: Result; + + etag = "no-dquotes".parse(); + assert_eq!(etag, Err(())); + + etag = "w/\"the-first-w-is-case-sensitive\"".parse(); + assert_eq!(etag, Err(())); + + etag = "".parse(); + assert_eq!(etag, Err(())); + + etag = "\"unmatched-dquotes1".parse(); + assert_eq!(etag, Err(())); + + etag = "unmatched-dquotes2\"".parse(); + assert_eq!(etag, Err(())); + + etag = "matched-\"dquotes\"".parse(); + assert_eq!(etag, Err(())); + } +} diff --git a/src/header/shared/mod.rs b/src/header/shared/mod.rs index 85b62f0d8c..7c12bd487f 100644 --- a/src/header/shared/mod.rs +++ b/src/header/shared/mod.rs @@ -1,5 +1,7 @@ pub use self::encoding::Encoding; +pub use self::entity::EntityTag; pub use self::quality_item::{QualityItem, qitem}; mod encoding; +mod entity; mod quality_item;