headers/util/
entity.rs

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