cogo_http/header/shared/
entity.rs

1use std::str::FromStr;
2use std::fmt::{self, Display};
3
4// check that each char in the slice is either:
5// 1. %x21, or
6// 2. in the range %x23 to %x7E, or
7// 3. in the range %x80 to %xFF
8fn check_slice_validity(slice: &str) -> bool {
9    slice.bytes().all(|c|
10        c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80' && c <= b'\xff'))
11}
12
13/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)
14///
15/// An entity tag consists of a string enclosed by two literal double quotes.
16/// Preceding the first double quote is an optional weakness indicator,
17/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and `W/"xyzzy"`.
18///
19/// # ABNF
20/// ```plain
21/// entity-tag = [ weak ] opaque-tag
22/// weak       = %x57.2F ; "W/", case-sensitive
23/// opaque-tag = DQUOTE *etagc DQUOTE
24/// etagc      = %x21 / %x23-7E / obs-text
25///            ; VCHAR except double quotes, plus obs-text
26/// ```
27///
28/// # Comparison
29/// To check if two entity tags are equivalent in an application always use the `strong_eq` or
30/// `weak_eq` methods based on the context of the Tag. Only use `==` to check if two tags are
31/// identical.
32///
33/// The example below shows the results for a set of entity-tag pairs and
34/// both the weak and strong comparison function results:
35///
36/// | ETag 1  | ETag 2  | Strong Comparison | Weak Comparison |
37/// |---------|---------|-------------------|-----------------|
38/// | `W/"1"` | `W/"1"` | no match          | match           |
39/// | `W/"1"` | `W/"2"` | no match          | no match        |
40/// | `W/"1"` | `"1"`   | no match          | match           |
41/// | `"1"`   | `"1"`   | match             | match           |
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct EntityTag {
44    /// Weakness indicator for the tag
45    pub weak: bool,
46    /// The opaque string in between the DQUOTEs
47    tag: String
48}
49
50impl EntityTag {
51    /// Constructs a new EntityTag.
52    /// # Panics
53    /// If the tag contains invalid characters.
54    pub fn new(weak: bool, tag: String) -> EntityTag {
55        assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
56        EntityTag { weak: weak, tag: tag }
57    }
58
59    /// Constructs a new weak EntityTag.
60    /// # Panics
61    /// If the tag contains invalid characters.
62    pub fn weak(tag: String) -> EntityTag {
63        EntityTag::new(true, tag)
64    }
65
66    /// Constructs a new strong EntityTag.
67    /// # Panics
68    /// If the tag contains invalid characters.
69    pub fn strong(tag: String) -> EntityTag {
70        EntityTag::new(false, tag)
71    }
72
73    /// Get the tag.
74    pub fn tag(&self) -> &str {
75        self.tag.as_ref()
76    }
77
78    /// Set the tag.
79    /// # Panics
80    /// If the tag contains invalid characters.
81    pub fn set_tag(&mut self, tag: String) {
82        assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
83        self.tag = tag
84    }
85
86    /// For strong comparison two entity-tags are equivalent if both are not weak and their
87    /// opaque-tags match character-by-character.
88    pub fn strong_eq(&self, other: &EntityTag) -> bool {
89        !self.weak && !other.weak && self.tag == other.tag
90    }
91
92    /// For weak comparison two entity-tags are equivalent if their
93    /// opaque-tags match character-by-character, regardless of either or
94    /// both being tagged as "weak".
95    pub fn weak_eq(&self, other: &EntityTag) -> bool {
96        self.tag == other.tag
97    }
98
99    /// The inverse of `EntityTag.strong_eq()`.
100    pub fn strong_ne(&self, other: &EntityTag) -> bool {
101        !self.strong_eq(other)
102    }
103
104    /// The inverse of `EntityTag.weak_eq()`.
105    pub fn weak_ne(&self, other: &EntityTag) -> bool {
106        !self.weak_eq(other)
107    }
108}
109
110impl Display for EntityTag {
111    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112        if self.weak {
113            write!(f, "W/\"{}\"", self.tag)
114        } else {
115            write!(f, "\"{}\"", self.tag)
116        }
117    }
118}
119
120impl FromStr for EntityTag {
121    type Err = crate::Error;
122    fn from_str(s: &str) -> crate::Result<EntityTag> {
123        let length: usize = s.len();
124        let slice = &s[..];
125        // Early exits if it doesn't terminate in a DQUOTE.
126        if !slice.ends_with('"') {
127            return Err(crate::Error::Header);
128        }
129        // The etag is weak if its first char is not a DQUOTE.
130        if slice.starts_with('"') && check_slice_validity(&slice[1..length-1]) {
131            // No need to check if the last char is a DQUOTE,
132            // we already did that above.
133            return Ok(EntityTag { weak: false, tag: slice[1..length-1].to_owned() });
134        } else if slice.starts_with("W/\"") && check_slice_validity(&slice[3..length-1]) {
135            return Ok(EntityTag { weak: true, tag: slice[3..length-1].to_owned() });
136        }
137        Err(crate::Error::Header)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::EntityTag;
144
145    #[test]
146    fn test_etag_parse_success() {
147        // Expected success
148        assert_eq!("\"foobar\"".parse::<EntityTag>().unwrap(),
149            EntityTag::strong("foobar".to_owned()));
150        assert_eq!("\"\"".parse::<EntityTag>().unwrap(),
151            EntityTag::strong("".to_owned()));
152        assert_eq!("W/\"weaktag\"".parse::<EntityTag>().unwrap(),
153            EntityTag::weak("weaktag".to_owned()));
154        assert_eq!("W/\"\x65\x62\"".parse::<EntityTag>().unwrap(),
155            EntityTag::weak("\x65\x62".to_owned()));
156        assert_eq!("W/\"\"".parse::<EntityTag>().unwrap(), EntityTag::weak("".to_owned()));
157    }
158
159    #[test]
160    fn test_etag_parse_failures() {
161        // Expected failures
162        assert!("no-dquotes".parse::<EntityTag>().is_err());
163        assert!("w/\"the-first-w-is-case-sensitive\"".parse::<EntityTag>().is_err());
164        assert!("".parse::<EntityTag>().is_err());
165        assert!("\"unmatched-dquotes1".parse::<EntityTag>().is_err());
166        assert!("unmatched-dquotes2\"".parse::<EntityTag>().is_err());
167        assert!("matched-\"dquotes\"".parse::<EntityTag>().is_err());
168    }
169
170    #[test]
171    fn test_etag_fmt() {
172        assert_eq!(format!("{}", EntityTag::strong("foobar".to_owned())), "\"foobar\"");
173        assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\"");
174        assert_eq!(format!("{}", EntityTag::weak("weak-etag".to_owned())), "W/\"weak-etag\"");
175        assert_eq!(format!("{}", EntityTag::weak("\u{0065}".to_owned())), "W/\"\x65\"");
176        assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\"");
177    }
178
179    #[test]
180    fn test_cmp() {
181        // | ETag 1  | ETag 2  | Strong Comparison | Weak Comparison |
182        // |---------|---------|-------------------|-----------------|
183        // | `W/"1"` | `W/"1"` | no match          | match           |
184        // | `W/"1"` | `W/"2"` | no match          | no match        |
185        // | `W/"1"` | `"1"`   | no match          | match           |
186        // | `"1"`   | `"1"`   | match             | match           |
187        let mut etag1 = EntityTag::weak("1".to_owned());
188        let mut etag2 = EntityTag::weak("1".to_owned());
189        assert!(!etag1.strong_eq(&etag2));
190        assert!(etag1.weak_eq(&etag2));
191        assert!(etag1.strong_ne(&etag2));
192        assert!(!etag1.weak_ne(&etag2));
193
194        etag1 = EntityTag::weak("1".to_owned());
195        etag2 = EntityTag::weak("2".to_owned());
196        assert!(!etag1.strong_eq(&etag2));
197        assert!(!etag1.weak_eq(&etag2));
198        assert!(etag1.strong_ne(&etag2));
199        assert!(etag1.weak_ne(&etag2));
200
201        etag1 = EntityTag::weak("1".to_owned());
202        etag2 = EntityTag::strong("1".to_owned());
203        assert!(!etag1.strong_eq(&etag2));
204        assert!(etag1.weak_eq(&etag2));
205        assert!(etag1.strong_ne(&etag2));
206        assert!(!etag1.weak_ne(&etag2));
207
208        etag1 = EntityTag::strong("1".to_owned());
209        etag2 = EntityTag::strong("1".to_owned());
210        assert!(etag1.strong_eq(&etag2));
211        assert!(etag1.weak_eq(&etag2));
212        assert!(!etag1.strong_ne(&etag2));
213        assert!(!etag1.weak_ne(&etag2));
214    }
215}