headers/util/
entity.rs

1use std::fmt;
2
3use super::{FlatCsv, IterExt};
4use HeaderValue;
5
6/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)
7///
8/// An entity tag consists of a string enclosed by two literal double quotes.
9/// Preceding the first double quote is an optional weakness indicator,
10/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and `W/"xyzzy"`.
11///
12/// # ABNF
13///
14/// ```text
15/// entity-tag = [ weak ] opaque-tag
16/// weak       = %x57.2F ; "W/", case-sensitive
17/// opaque-tag = DQUOTE *etagc DQUOTE
18/// etagc      = %x21 / %x23-7E / obs-text
19///            ; VCHAR except double quotes, plus obs-text
20/// ```
21///
22/// # Comparison
23/// To check if two entity tags are equivalent in an application always use the `strong_eq` or
24/// `weak_eq` methods based on the context of the Tag. Only use `==` to check if two tags are
25/// identical.
26///
27/// The example below shows the results for a set of entity-tag pairs and
28/// both the weak and strong comparison function results:
29///
30/// | ETag 1  | ETag 2  | Strong Comparison | Weak Comparison |
31/// |---------|---------|-------------------|-----------------|
32/// | `W/"1"` | `W/"1"` | no match          | match           |
33/// | `W/"1"` | `W/"2"` | no match          | no match        |
34/// | `W/"1"` | `"1"`   | no match          | match           |
35/// | `"1"`   | `"1"`   | match             | match           |
36#[derive(Clone, Eq, PartialEq)]
37pub(crate) struct EntityTag<T = HeaderValue>(T);
38
39#[derive(Clone, Debug, PartialEq)]
40pub(crate) enum EntityTagRange {
41    Any,
42    Tags(FlatCsv),
43}
44
45// ===== impl EntityTag =====
46
47impl<T: AsRef<[u8]>> EntityTag<T> {
48    /// Get the tag.
49    pub(crate) fn tag(&self) -> &[u8] {
50        let bytes = self.0.as_ref();
51        let end = bytes.len() - 1;
52        if bytes[0] == b'W' {
53            // W/"<tag>"
54            &bytes[3..end]
55        } else {
56            // "<tag>"
57            &bytes[1..end]
58        }
59    }
60
61    /// Return if this is a "weak" tag.
62    pub(crate) fn is_weak(&self) -> bool {
63        self.0.as_ref()[0] == b'W'
64    }
65
66    /// For strong comparison two entity-tags are equivalent if both are not weak and their
67    /// opaque-tags match character-by-character.
68    pub(crate) fn strong_eq<R>(&self, other: &EntityTag<R>) -> bool
69    where
70        R: AsRef<[u8]>,
71    {
72        !self.is_weak() && !other.is_weak() && self.tag() == other.tag()
73    }
74
75    /// For weak comparison two entity-tags are equivalent if their
76    /// opaque-tags match character-by-character, regardless of either or
77    /// both being tagged as "weak".
78    pub(crate) fn weak_eq<R>(&self, other: &EntityTag<R>) -> bool
79    where
80        R: AsRef<[u8]>,
81    {
82        self.tag() == other.tag()
83    }
84
85    /// The inverse of `EntityTag.strong_eq()`.
86    #[cfg(test)]
87    pub(crate) fn strong_ne(&self, other: &EntityTag) -> bool {
88        !self.strong_eq(other)
89    }
90
91    /// The inverse of `EntityTag.weak_eq()`.
92    #[cfg(test)]
93    pub(crate) fn weak_ne(&self, other: &EntityTag) -> bool {
94        !self.weak_eq(other)
95    }
96
97    pub(crate) fn parse(src: T) -> Option<Self> {
98        let slice = src.as_ref();
99        let length = slice.len();
100
101        // Early exits if it doesn't terminate in a DQUOTE.
102        if length < 2 || slice[length - 1] != b'"' {
103            return None;
104        }
105
106        let start = match slice[0] {
107            // "<tag>"
108            b'"' => 1,
109            // W/"<tag>"
110            b'W' => {
111                if length >= 4 && slice[1] == b'/' && slice[2] == b'"' {
112                    3
113                } else {
114                    return None;
115                }
116            }
117            _ => return None,
118        };
119
120        if check_slice_validity(&slice[start..length - 1]) {
121            Some(EntityTag(src))
122        } else {
123            None
124        }
125    }
126}
127
128impl EntityTag {
129    /*
130    /// Constructs a new EntityTag.
131    /// # Panics
132    /// If the tag contains invalid characters.
133    pub fn new(weak: bool, tag: String) -> EntityTag {
134        assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
135        EntityTag { weak: weak, tag: tag }
136    }
137
138    /// Constructs a new weak EntityTag.
139    /// # Panics
140    /// If the tag contains invalid characters.
141    pub fn weak(tag: String) -> EntityTag {
142        EntityTag::new(true, tag)
143    }
144
145    /// Constructs a new strong EntityTag.
146    /// # Panics
147    /// If the tag contains invalid characters.
148    pub fn strong(tag: String) -> EntityTag {
149        EntityTag::new(false, tag)
150    }
151    */
152
153    #[cfg(test)]
154    pub fn from_static(bytes: &'static str) -> EntityTag {
155        let val = HeaderValue::from_static(bytes);
156        match EntityTag::from_val(&val) {
157            Some(tag) => tag,
158            None => {
159                panic!("invalid static string for EntityTag: {:?}", bytes);
160            }
161        }
162    }
163
164    pub(crate) fn from_owned(val: HeaderValue) -> Option<EntityTag> {
165        EntityTag::parse(val.as_bytes())?;
166        Some(EntityTag(val))
167    }
168
169    pub(crate) fn from_val(val: &HeaderValue) -> Option<EntityTag> {
170        EntityTag::parse(val.as_bytes()).map(|_entity| EntityTag(val.clone()))
171    }
172}
173
174impl<T: fmt::Debug> fmt::Debug for EntityTag<T> {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        self.0.fmt(f)
177    }
178}
179
180impl super::TryFromValues for EntityTag {
181    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error>
182    where
183        I: Iterator<Item = &'i HeaderValue>,
184    {
185        values
186            .just_one()
187            .and_then(EntityTag::from_val)
188            .ok_or_else(::Error::invalid)
189    }
190}
191
192impl From<EntityTag> for HeaderValue {
193    fn from(tag: EntityTag) -> HeaderValue {
194        tag.0
195    }
196}
197
198impl<'a> From<&'a EntityTag> for HeaderValue {
199    fn from(tag: &'a EntityTag) -> HeaderValue {
200        tag.0.clone()
201    }
202}
203
204/// check that each char in the slice is either:
205/// 1. `%x21`, or
206/// 2. in the range `%x23` to `%x7E`, or
207/// 3. above `%x80`
208fn check_slice_validity(slice: &[u8]) -> bool {
209    slice.iter().all(|&c| {
210        // HeaderValue already validates that this doesnt contain control
211        // characters, so we only need to look for DQUOTE (`"`).
212        //
213        // The debug_assert is just in case we use check_slice_validity in
214        // some new context that didnt come from a HeaderValue.
215        debug_assert!(
216            (c >= b'\x21' && c <= b'\x7e') | (c >= b'\x80'),
217            "EntityTag expects HeaderValue to have check for control characters"
218        );
219        c != b'"'
220    })
221}
222
223// ===== impl EntityTagRange =====
224
225impl EntityTagRange {
226    pub(crate) fn matches_strong(&self, entity: &EntityTag) -> bool {
227        self.matches_if(entity, |a, b| a.strong_eq(b))
228    }
229
230    pub(crate) fn matches_weak(&self, entity: &EntityTag) -> bool {
231        self.matches_if(entity, |a, b| a.weak_eq(b))
232    }
233
234    fn matches_if<F>(&self, entity: &EntityTag, func: F) -> bool
235    where
236        F: Fn(&EntityTag<&str>, &EntityTag) -> bool,
237    {
238        match *self {
239            EntityTagRange::Any => true,
240            EntityTagRange::Tags(ref tags) => tags
241                .iter()
242                .flat_map(EntityTag::<&str>::parse)
243                .any(|tag| func(&tag, entity)),
244        }
245    }
246}
247
248impl super::TryFromValues for EntityTagRange {
249    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error>
250    where
251        I: Iterator<Item = &'i HeaderValue>,
252    {
253        let flat = FlatCsv::try_from_values(values)?;
254        if flat.value == "*" {
255            Ok(EntityTagRange::Any)
256        } else {
257            Ok(EntityTagRange::Tags(flat))
258        }
259    }
260}
261
262impl<'a> From<&'a EntityTagRange> for HeaderValue {
263    fn from(tag: &'a EntityTagRange) -> HeaderValue {
264        match *tag {
265            EntityTagRange::Any => HeaderValue::from_static("*"),
266            EntityTagRange::Tags(ref tags) => tags.into(),
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    fn parse(slice: &[u8]) -> Option<EntityTag> {
276        let val = HeaderValue::from_bytes(slice).ok()?;
277        EntityTag::from_val(&val)
278    }
279
280    #[test]
281    fn test_etag_parse_success() {
282        // Expected success
283        let tag = parse(b"\"foobar\"").unwrap();
284        assert!(!tag.is_weak());
285        assert_eq!(tag.tag(), b"foobar");
286
287        let weak = parse(b"W/\"weaktag\"").unwrap();
288        assert!(weak.is_weak());
289        assert_eq!(weak.tag(), b"weaktag");
290    }
291
292    #[test]
293    fn test_etag_parse_failures() {
294        // Expected failures
295        macro_rules! fails {
296            ($slice:expr) => {
297                assert_eq!(parse($slice), None);
298            };
299        }
300
301        fails!(b"no-dquote");
302        fails!(b"w/\"the-first-w-is-case sensitive\"");
303        fails!(b"W/\"");
304        fails!(b"");
305        fails!(b"\"unmatched-dquotes1");
306        fails!(b"unmatched-dquotes2\"");
307        fails!(b"\"inner\"quotes\"");
308    }
309
310    /*
311    #[test]
312    fn test_etag_fmt() {
313        assert_eq!(format!("{}", EntityTag::strong("foobar".to_owned())), "\"foobar\"");
314        assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\"");
315        assert_eq!(format!("{}", EntityTag::weak("weak-etag".to_owned())), "W/\"weak-etag\"");
316        assert_eq!(format!("{}", EntityTag::weak("\u{0065}".to_owned())), "W/\"\x65\"");
317        assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\"");
318    }
319    */
320
321    #[test]
322    fn test_cmp() {
323        // | ETag 1  | ETag 2  | Strong Comparison | Weak Comparison |
324        // |---------|---------|-------------------|-----------------|
325        // | `W/"1"` | `W/"1"` | no match          | match           |
326        // | `W/"1"` | `W/"2"` | no match          | no match        |
327        // | `W/"1"` | `"1"`   | no match          | match           |
328        // | `"1"`   | `"1"`   | match             | match           |
329        let mut etag1 = EntityTag::from_static("W/\"1\"");
330        let mut etag2 = etag1.clone();
331        assert!(!etag1.strong_eq(&etag2));
332        assert!(etag1.weak_eq(&etag2));
333        assert!(etag1.strong_ne(&etag2));
334        assert!(!etag1.weak_ne(&etag2));
335
336        etag2 = EntityTag::from_static("W/\"2\"");
337        assert!(!etag1.strong_eq(&etag2));
338        assert!(!etag1.weak_eq(&etag2));
339        assert!(etag1.strong_ne(&etag2));
340        assert!(etag1.weak_ne(&etag2));
341
342        etag2 = EntityTag::from_static("\"1\"");
343        assert!(!etag1.strong_eq(&etag2));
344        assert!(etag1.weak_eq(&etag2));
345        assert!(etag1.strong_ne(&etag2));
346        assert!(!etag1.weak_ne(&etag2));
347
348        etag1 = EntityTag::from_static("\"1\"");
349        assert!(etag1.strong_eq(&etag2));
350        assert!(etag1.weak_eq(&etag2));
351        assert!(!etag1.strong_ne(&etag2));
352        assert!(!etag1.weak_ne(&etag2));
353    }
354}