Skip to main content

ntex_files/header/
entity.rs

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