Skip to main content

rama_http_headers/util/
entity.rs

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