requiem_http/header/shared/
entity.rs

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