Merge pull request #409 from pyfisch/entitytag
feat(entitytag): Add EntityTag comparison, make EntityTag safe to use
This commit is contained in:
		| @@ -1,5 +1,5 @@ | |||||||
| use header::{EntityTag, Header, HeaderFormat}; | use header::{EntityTag, Header, HeaderFormat}; | ||||||
| use std::fmt::{self}; | use std::fmt::{self, Display}; | ||||||
| use header::parsing::from_one_raw_str; | use header::parsing::from_one_raw_str; | ||||||
|  |  | ||||||
| /// The `Etag` header. | /// The `Etag` header. | ||||||
| @@ -28,10 +28,7 @@ impl Header for Etag { | |||||||
|  |  | ||||||
| impl HeaderFormat for Etag { | impl HeaderFormat for Etag { | ||||||
|     fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |     fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||||||
|         if self.0.weak { |         self.0.fmt(fmt) | ||||||
|             try!(fmt.write_str("W/")); |  | ||||||
|         } |  | ||||||
|         write!(fmt, "\"{}\"", self.0.tag) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -46,34 +43,19 @@ mod tests { | |||||||
|         let mut etag: Option<Etag>; |         let mut etag: Option<Etag>; | ||||||
|  |  | ||||||
|         etag = Header::parse_header([b"\"foobar\"".to_vec()].as_ref()); |         etag = Header::parse_header([b"\"foobar\"".to_vec()].as_ref()); | ||||||
|         assert_eq!(etag, Some(Etag(EntityTag{ |         assert_eq!(etag, Some(Etag(EntityTag::new(false, "foobar".to_string())))); | ||||||
|             weak: false, |  | ||||||
|             tag: "foobar".to_string() |  | ||||||
|         }))); |  | ||||||
|  |  | ||||||
|         etag = Header::parse_header([b"\"\"".to_vec()].as_ref()); |         etag = Header::parse_header([b"\"\"".to_vec()].as_ref()); | ||||||
|         assert_eq!(etag, Some(Etag(EntityTag{ |         assert_eq!(etag, Some(Etag(EntityTag::new(false, "".to_string())))); | ||||||
|             weak: false, |  | ||||||
|             tag: "".to_string() |  | ||||||
|         }))); |  | ||||||
|  |  | ||||||
|         etag = Header::parse_header([b"W/\"weak-etag\"".to_vec()].as_ref()); |         etag = Header::parse_header([b"W/\"weak-etag\"".to_vec()].as_ref()); | ||||||
|         assert_eq!(etag, Some(Etag(EntityTag{ |         assert_eq!(etag, Some(Etag(EntityTag::new(true, "weak-etag".to_string())))); | ||||||
|             weak: true, |  | ||||||
|             tag: "weak-etag".to_string() |  | ||||||
|         }))); |  | ||||||
|  |  | ||||||
|         etag = Header::parse_header([b"W/\"\x65\x62\"".to_vec()].as_ref()); |         etag = Header::parse_header([b"W/\"\x65\x62\"".to_vec()].as_ref()); | ||||||
|         assert_eq!(etag, Some(Etag(EntityTag{ |         assert_eq!(etag, Some(Etag(EntityTag::new(true, "\u{0065}\u{0062}".to_string())))); | ||||||
|             weak: true, |  | ||||||
|             tag: "\u{0065}\u{0062}".to_string() |  | ||||||
|         }))); |  | ||||||
|  |  | ||||||
|         etag = Header::parse_header([b"W/\"\"".to_vec()].as_ref()); |         etag = Header::parse_header([b"W/\"\"".to_vec()].as_ref()); | ||||||
|         assert_eq!(etag, Some(Etag(EntityTag{ |         assert_eq!(etag, Some(Etag(EntityTag::new(true, "".to_string())))); | ||||||
|             weak: true, |  | ||||||
|             tag: "".to_string() |  | ||||||
|         }))); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
| @@ -102,4 +84,3 @@ mod tests { | |||||||
| } | } | ||||||
|  |  | ||||||
| bench_header!(bench, Etag, { vec![b"W/\"nonemptytag\"".to_vec()] }); | bench_header!(bench, Etag, { vec![b"W/\"nonemptytag\"".to_vec()] }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -55,9 +55,9 @@ fn test_parse_header() { | |||||||
|         let a: IfMatch = Header::parse_header( |         let a: IfMatch = Header::parse_header( | ||||||
|             [b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\"".to_vec()].as_ref()).unwrap(); |             [b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\"".to_vec()].as_ref()).unwrap(); | ||||||
|         let b = IfMatch::EntityTags( |         let b = IfMatch::EntityTags( | ||||||
|             vec![EntityTag{weak:false, tag: "xyzzy".to_string()}, |             vec![EntityTag::new(false, "xyzzy".to_string()), | ||||||
|                  EntityTag{weak:false, tag: "r2d2xxxx".to_string()}, |                  EntityTag::new(false, "r2d2xxxx".to_string()), | ||||||
|                  EntityTag{weak:false, tag: "c3piozzzz".to_string()}]); |                  EntityTag::new(false, "c3piozzzz".to_string())]); | ||||||
|         assert_eq!(a, b); |         assert_eq!(a, b); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -67,14 +67,8 @@ mod tests { | |||||||
|  |  | ||||||
|         if_none_match = Header::parse_header([b"\"foobar\", W/\"weak-etag\"".to_vec()].as_ref()); |         if_none_match = Header::parse_header([b"\"foobar\", W/\"weak-etag\"".to_vec()].as_ref()); | ||||||
|         let mut entities: Vec<EntityTag> = Vec::new(); |         let mut entities: Vec<EntityTag> = Vec::new(); | ||||||
|         let foobar_etag = EntityTag { |         let foobar_etag = EntityTag::new(false, "foobar".to_string()); | ||||||
|             weak: false, |         let weak_etag = EntityTag::new(true, "weak-etag".to_string()); | ||||||
|             tag: "foobar".to_string() |  | ||||||
|         }; |  | ||||||
|         let weak_etag = EntityTag { |  | ||||||
|             weak: true, |  | ||||||
|             tag: "weak-etag".to_string() |  | ||||||
|         }; |  | ||||||
|         entities.push(foobar_etag); |         entities.push(foobar_etag); | ||||||
|         entities.push(weak_etag); |         entities.push(weak_etag); | ||||||
|         assert_eq!(if_none_match, Some(IfNoneMatch::EntityTags(entities))); |         assert_eq!(if_none_match, Some(IfNoneMatch::EntityTags(entities))); | ||||||
|   | |||||||
| @@ -1,29 +1,6 @@ | |||||||
| use std::str::FromStr; | use std::str::FromStr; | ||||||
| use std::fmt::{self, Display}; | 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: | // check that each char in the slice is either: | ||||||
| // 1. %x21, or | // 1. %x21, or | ||||||
| // 2. in the range %x23 to %x7E, or | // 2. in the range %x23 to %x7E, or | ||||||
| @@ -38,108 +15,193 @@ fn check_slice_validity(slice: &str) -> bool { | |||||||
|     true |     true | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) | ||||||
|  | /// | ||||||
|  | /// An entity tag consists of a string enclosed by two literal double quotes. | ||||||
|  | /// Preceding the first double quote is an optional weakness indicator, | ||||||
|  | /// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and `W/"xyzzy"`. | ||||||
|  | /// | ||||||
|  | /// # ABNF | ||||||
|  | /// ```plain | ||||||
|  | /// entity-tag = [ weak ] opaque-tag | ||||||
|  | /// weak       = %x57.2F ; "W/", case-sensitive | ||||||
|  | /// opaque-tag = DQUOTE *etagc DQUOTE | ||||||
|  | /// etagc      = %x21 / %x23-7E / obs-text | ||||||
|  | ///            ; VCHAR except double quotes, plus obs-text | ||||||
|  | /// ``` | ||||||
|  | /// | ||||||
|  | /// # Comparison | ||||||
|  | /// To check if two entity tags are equivalent in an application always use the `strong_eq` or | ||||||
|  | /// `weak_eq` methods based on the context of the Tag. Only use `==` to check if two tags are | ||||||
|  | /// identical. | ||||||
|  | /// | ||||||
|  | /// The example below shows the results for a set of entity-tag pairs and | ||||||
|  | /// both the weak and strong comparison function results: | ||||||
|  | /// | ||||||
|  | /// | ETag 1  | ETag 2  | Strong Comparison | Weak Comparison | | ||||||
|  | /// |---------|---------|-------------------|-----------------| | ||||||
|  | /// | `W/"1"` | `W/"1"` | no match          | match           | | ||||||
|  | /// | `W/"1"` | `W/"2"` | no match          | no match        | | ||||||
|  | /// | `W/"1"` | `"1"`   | no match          | match           | | ||||||
|  | /// | `"1"`   | `"1"`   | match             | match           | | ||||||
|  | #[derive(Clone, Debug, Eq, PartialEq)] | ||||||
|  | pub struct EntityTag { | ||||||
|  |     /// Weakness indicator for the tag | ||||||
|  |     pub weak: bool, | ||||||
|  |     /// The opaque string in between the DQUOTEs | ||||||
|  |     tag: String | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl EntityTag { | ||||||
|  |     /// Constructs a new EntityTag. | ||||||
|  |     /// # Panics | ||||||
|  |     /// If the tag contains invalid characters. | ||||||
|  |     pub fn new(weak: bool, tag: String) -> EntityTag { | ||||||
|  |         match check_slice_validity(&tag) { | ||||||
|  |             true => EntityTag { weak: weak, tag: tag }, | ||||||
|  |             false => panic!("Invalid tag: {:?}", tag), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Get the tag. | ||||||
|  |     pub fn tag(&self) -> &str { | ||||||
|  |         self.tag.as_ref() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Set the tag. | ||||||
|  |     /// # Panics | ||||||
|  |     /// If the tag contains invalid characters. | ||||||
|  |     pub fn set_tag(&mut self, tag: String) { | ||||||
|  |         match check_slice_validity(&tag[..]) { | ||||||
|  |             true => self.tag = tag, | ||||||
|  |             false => panic!("Invalid tag: {:?}", tag), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// For strong comparison two entity-tags are equivalent if both are not weak and their | ||||||
|  |     /// opaque-tags match character-by-character. | ||||||
|  |     pub fn strong_eq(&self, other: &EntityTag) -> bool { | ||||||
|  |         self.weak == false && other.weak == false && self.tag == other.tag | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// For weak comparison two entity-tags are equivalent if their | ||||||
|  |     /// opaque-tags match character-by-character, regardless of either or | ||||||
|  |     /// both being tagged as "weak". | ||||||
|  |     pub fn weak_eq(&self, other: &EntityTag) -> bool { | ||||||
|  |         self.tag == other.tag | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// The inverse of `EntityTag.strong_eq()`. | ||||||
|  |     pub fn strong_ne(&self, other: &EntityTag) -> bool { | ||||||
|  |         !self.strong_eq(other) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// The inverse of `EntityTag.weak_eq()`. | ||||||
|  |     pub fn weak_ne(&self, other: &EntityTag) -> bool { | ||||||
|  |         !self.weak_eq(other) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Display for EntityTag { | ||||||
|  |     fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||||||
|  |         match self.weak { | ||||||
|  |             true => write!(fmt, "W/\"{}\"", self.tag), | ||||||
|  |             false => write!(fmt, "\"{}\"", self.tag), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| impl FromStr for EntityTag { | impl FromStr for EntityTag { | ||||||
|     type Err = (); |     type Err = (); | ||||||
|     fn from_str(s: &str) -> Result<EntityTag, ()> { |     fn from_str(s: &str) -> Result<EntityTag, ()> { | ||||||
|         let length: usize = s.len(); |         let length: usize = s.len(); | ||||||
|         let slice = &s[..]; |         let slice = &s[..]; | ||||||
|  |  | ||||||
|         // Early exits: |         // Early exits: | ||||||
|         // 1. The string is empty, or, |         // 1. The string is empty, or, | ||||||
|         // 2. it doesn't terminate in a DQUOTE. |         // 2. it doesn't terminate in a DQUOTE. | ||||||
|         if slice.is_empty() || !slice.ends_with('"') { |         if slice.is_empty() || !slice.ends_with('"') { | ||||||
|             return Err(()); |             return Err(()); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // The etag is weak if its first char is not a DQUOTE. |         // The etag is weak if its first char is not a DQUOTE. | ||||||
|         if slice.starts_with('"') /* '"' */ { |         if slice.starts_with('"') && check_slice_validity(&slice[1..length-1]) { | ||||||
|             // No need to check if the last char is a DQUOTE, |             // No need to check if the last char is a DQUOTE, | ||||||
|             // we already did that above. |             // we already did that above. | ||||||
|             if check_slice_validity(&slice[1..length-1]) { |             return Ok(EntityTag { weak: false, tag: slice[1..length-1].to_string() }); | ||||||
|                 return Ok(EntityTag { |         } else if slice.starts_with("W/\"") && check_slice_validity(&slice[3..length-1]) { | ||||||
|                     weak: false, |             return Ok(EntityTag { weak: true, tag: slice[3..length-1].to_string() }); | ||||||
|                     tag: slice[1..length-1].to_string() |  | ||||||
|                 }); |  | ||||||
|             } else { |  | ||||||
|                 return Err(()); |  | ||||||
|         } |         } | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if slice.starts_with("W/\"") { |  | ||||||
|             if check_slice_validity(&slice[3..length-1]) { |  | ||||||
|                 return Ok(EntityTag { |  | ||||||
|                     weak: true, |  | ||||||
|                     tag: slice[3..length-1].to_string() |  | ||||||
|                 }); |  | ||||||
|             } else { |  | ||||||
|                 return Err(()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         Err(()) |         Err(()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use super::EntityTag; |     use super::EntityTag; | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|     fn test_etag_successes() { |     fn test_etag_parse_success() { | ||||||
|         // Expected successes |         // Expected success | ||||||
|         let mut etag : EntityTag = "\"foobar\"".parse().unwrap(); |         assert_eq!("\"foobar\"".parse().unwrap(), EntityTag::new(false, "foobar".to_string())); | ||||||
|         assert_eq!(etag, (EntityTag { |         assert_eq!("\"\"".parse().unwrap(), EntityTag::new(false, "".to_string())); | ||||||
|             weak: false, |         assert_eq!("W/\"weaktag\"".parse().unwrap(), EntityTag::new(true, "weaktag".to_string())); | ||||||
|             tag: "foobar".to_string() |         assert_eq!("W/\"\x65\x62\"".parse().unwrap(), EntityTag::new(true, "\x65\x62".to_string())); | ||||||
|         })); |         assert_eq!("W/\"\"".parse().unwrap(), EntityTag::new(true, "".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] |     #[test] | ||||||
|     fn test_etag_failures() { |     fn test_etag_parse_failures() { | ||||||
|         // Expected failures |         // Expected failures | ||||||
|         let mut etag: Result<EntityTag,()>; |         assert_eq!("no-dquotes".parse::<EntityTag>(), Err(())); | ||||||
|  |         assert_eq!("w/\"the-first-w-is-case-sensitive\"".parse::<EntityTag>(), Err(())); | ||||||
|  |         assert_eq!("".parse::<EntityTag>(), Err(())); | ||||||
|  |         assert_eq!("\"unmatched-dquotes1".parse::<EntityTag>(), Err(())); | ||||||
|  |         assert_eq!("unmatched-dquotes2\"".parse::<EntityTag>(), Err(())); | ||||||
|  |         assert_eq!("matched-\"dquotes\"".parse::<EntityTag>(), Err(())); | ||||||
|  |     } | ||||||
|  |  | ||||||
|         etag = "no-dquotes".parse(); |     #[test] | ||||||
|         assert_eq!(etag, Err(())); |     fn test_etag_fmt() { | ||||||
|  |         assert_eq!(format!("{}", EntityTag::new(false, "foobar".to_string())), "\"foobar\""); | ||||||
|  |         assert_eq!(format!("{}", EntityTag::new(false, "".to_string())), "\"\""); | ||||||
|  |         assert_eq!(format!("{}", EntityTag::new(true, "weak-etag".to_string())), "W/\"weak-etag\""); | ||||||
|  |         assert_eq!(format!("{}", EntityTag::new(true, "\u{0065}".to_string())), "W/\"\x65\""); | ||||||
|  |         assert_eq!(format!("{}", EntityTag::new(true, "".to_string())), "W/\"\""); | ||||||
|  |     } | ||||||
|  |  | ||||||
|         etag = "w/\"the-first-w-is-case-sensitive\"".parse(); |     #[test] | ||||||
|         assert_eq!(etag, Err(())); |     fn test_cmp() { | ||||||
|  |         // | ETag 1  | ETag 2  | Strong Comparison | Weak Comparison | | ||||||
|  |         // |---------|---------|-------------------|-----------------| | ||||||
|  |         // | `W/"1"` | `W/"1"` | no match          | match           | | ||||||
|  |         // | `W/"1"` | `W/"2"` | no match          | no match        | | ||||||
|  |         // | `W/"1"` | `"1"`   | no match          | match           | | ||||||
|  |         // | `"1"`   | `"1"`   | match             | match           | | ||||||
|  |         let mut etag1 = EntityTag::new(true, "1".to_string()); | ||||||
|  |         let mut etag2 = EntityTag::new(true, "1".to_string()); | ||||||
|  |         assert_eq!(etag1.strong_eq(&etag2), false); | ||||||
|  |         assert_eq!(etag1.weak_eq(&etag2), true); | ||||||
|  |         assert_eq!(etag1.strong_ne(&etag2), true); | ||||||
|  |         assert_eq!(etag1.weak_ne(&etag2), false); | ||||||
|  |  | ||||||
|         etag = "".parse(); |         etag1 = EntityTag::new(true, "1".to_string()); | ||||||
|         assert_eq!(etag, Err(())); |         etag2 = EntityTag::new(true, "2".to_string()); | ||||||
|  |         assert_eq!(etag1.strong_eq(&etag2), false); | ||||||
|  |         assert_eq!(etag1.weak_eq(&etag2), false); | ||||||
|  |         assert_eq!(etag1.strong_ne(&etag2), true); | ||||||
|  |         assert_eq!(etag1.weak_ne(&etag2), true); | ||||||
|  |  | ||||||
|         etag = "\"unmatched-dquotes1".parse(); |         etag1 = EntityTag::new(true, "1".to_string()); | ||||||
|         assert_eq!(etag, Err(())); |         etag2 = EntityTag::new(false, "1".to_string()); | ||||||
|  |         assert_eq!(etag1.strong_eq(&etag2), false); | ||||||
|  |         assert_eq!(etag1.weak_eq(&etag2), true); | ||||||
|  |         assert_eq!(etag1.strong_ne(&etag2), true); | ||||||
|  |         assert_eq!(etag1.weak_ne(&etag2), false); | ||||||
|  |  | ||||||
|         etag = "unmatched-dquotes2\"".parse(); |         etag1 = EntityTag::new(false, "1".to_string()); | ||||||
|         assert_eq!(etag, Err(())); |         etag2 = EntityTag::new(false, "1".to_string()); | ||||||
|  |         assert_eq!(etag1.strong_eq(&etag2), true); | ||||||
|         etag = "matched-\"dquotes\"".parse(); |         assert_eq!(etag1.weak_eq(&etag2), true); | ||||||
|         assert_eq!(etag, Err(())); |         assert_eq!(etag1.strong_ne(&etag2), false); | ||||||
|  |         assert_eq!(etag1.weak_ne(&etag2), false); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user