actix_web/http/header/
entity.rs

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