Coverage Report

Created: 2026-11-03 08:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/headers-0.4.1/src/util/entity.rs
Line
Count
Source
1
use std::fmt;
2
3
use http::HeaderValue;
4
5
use super::{FlatCsv, IterExt};
6
use 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)]
39
pub(crate) struct EntityTag<T = HeaderValue>(T);
40
41
#[derive(Clone, Debug, PartialEq)]
42
pub(crate) enum EntityTagRange {
43
    Any,
44
    Tags(FlatCsv),
45
}
46
47
// ===== impl EntityTag =====
48
49
impl<T: AsRef<[u8]>> EntityTag<T> {
50
    /// Get the tag.
51
0
    pub(crate) fn tag(&self) -> &[u8] {
52
0
        let bytes = self.0.as_ref();
53
0
        let end = bytes.len() - 1;
54
0
        if bytes[0] == b'W' {
55
            // W/"<tag>"
56
0
            &bytes[3..end]
57
        } else {
58
            // "<tag>"
59
0
            &bytes[1..end]
60
        }
61
0
    }
Unexecuted instantiation: <headers::util::entity::EntityTag<&str>>::tag
Unexecuted instantiation: <headers::util::entity::EntityTag>::tag
62
63
    /// Return if this is a "weak" tag.
64
0
    pub(crate) fn is_weak(&self) -> bool {
65
0
        self.0.as_ref()[0] == b'W'
66
0
    }
Unexecuted instantiation: <headers::util::entity::EntityTag<&str>>::is_weak
Unexecuted instantiation: <headers::util::entity::EntityTag>::is_weak
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
0
    pub(crate) fn strong_eq<R>(&self, other: &EntityTag<R>) -> bool
71
0
    where
72
0
        R: AsRef<[u8]>,
73
    {
74
0
        !self.is_weak() && !other.is_weak() && self.tag() == other.tag()
75
0
    }
Unexecuted instantiation: <headers::util::entity::EntityTag<&str>>::strong_eq::<http::header::value::HeaderValue>
Unexecuted instantiation: <headers::util::entity::EntityTag>::strong_eq::<http::header::value::HeaderValue>
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
0
    pub(crate) fn weak_eq<R>(&self, other: &EntityTag<R>) -> bool
81
0
    where
82
0
        R: AsRef<[u8]>,
83
    {
84
0
        self.tag() == other.tag()
85
0
    }
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
0
    pub(crate) fn parse(src: T) -> Option<Self> {
100
0
        let slice = src.as_ref();
101
0
        let length = slice.len();
102
103
        // Early exits if it doesn't terminate in a DQUOTE.
104
0
        if length < 2 || slice[length - 1] != b'"' {
105
0
            return None;
106
0
        }
107
108
0
        let start = match slice[0] {
109
            // "<tag>"
110
0
            b'"' => 1,
111
            // W/"<tag>"
112
            b'W' => {
113
0
                if length >= 4 && slice[1] == b'/' && slice[2] == b'"' {
114
0
                    3
115
                } else {
116
0
                    return None;
117
                }
118
            }
119
0
            _ => return None,
120
        };
121
122
0
        if check_slice_validity(&slice[start..length - 1]) {
123
0
            Some(EntityTag(src))
124
        } else {
125
0
            None
126
        }
127
0
    }
Unexecuted instantiation: <headers::util::entity::EntityTag<&[u8]>>::parse
Unexecuted instantiation: <headers::util::entity::EntityTag<&str>>::parse
128
}
129
130
impl 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
0
    pub(crate) fn from_owned(val: HeaderValue) -> Option<EntityTag> {
167
0
        EntityTag::parse(val.as_bytes())?;
168
0
        Some(EntityTag(val))
169
0
    }
170
171
0
    pub(crate) fn from_val(val: &HeaderValue) -> Option<EntityTag> {
172
0
        EntityTag::parse(val.as_bytes()).map(|_entity| EntityTag(val.clone()))
173
0
    }
174
}
175
176
impl<T: fmt::Debug> fmt::Debug for EntityTag<T> {
177
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178
0
        self.0.fmt(f)
179
0
    }
180
}
181
182
impl super::TryFromValues for EntityTag {
183
0
    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
184
0
    where
185
0
        I: Iterator<Item = &'i HeaderValue>,
186
    {
187
0
        values
188
0
            .just_one()
189
0
            .and_then(EntityTag::from_val)
190
0
            .ok_or_else(Error::invalid)
191
0
    }
192
}
193
194
impl From<EntityTag> for HeaderValue {
195
0
    fn from(tag: EntityTag) -> HeaderValue {
196
0
        tag.0
197
0
    }
198
}
199
200
impl<'a> From<&'a EntityTag> for HeaderValue {
201
0
    fn from(tag: &'a EntityTag) -> HeaderValue {
202
0
        tag.0.clone()
203
0
    }
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`
210
0
fn check_slice_validity(slice: &[u8]) -> bool {
211
0
    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
0
        debug_assert!(
218
0
            (b'\x21'..=b'\x7e').contains(&c) | (c >= b'\x80'),
219
            "EntityTag expects HeaderValue to have check for control characters"
220
        );
221
0
        c != b'"'
222
0
    })
223
0
}
224
225
// ===== impl EntityTagRange =====
226
227
impl EntityTagRange {
228
0
    pub(crate) fn matches_strong(&self, entity: &EntityTag) -> bool {
229
0
        self.matches_if(entity, |a, b| a.strong_eq(b))
230
0
    }
231
232
0
    pub(crate) fn matches_weak(&self, entity: &EntityTag) -> bool {
233
0
        self.matches_if(entity, |a, b| a.weak_eq(b))
234
0
    }
235
236
0
    fn matches_if<F>(&self, entity: &EntityTag, func: F) -> bool
237
0
    where
238
0
        F: Fn(&EntityTag<&str>, &EntityTag) -> bool,
239
    {
240
0
        match *self {
241
0
            EntityTagRange::Any => true,
242
0
            EntityTagRange::Tags(ref tags) => tags
243
0
                .iter()
244
0
                .flat_map(EntityTag::<&str>::parse)
245
0
                .any(|tag| func(&tag, entity)),
Unexecuted instantiation: <headers::util::entity::EntityTagRange>::matches_if::<<headers::util::entity::EntityTagRange>::matches_weak::{closure#0}>::{closure#0}
Unexecuted instantiation: <headers::util::entity::EntityTagRange>::matches_if::<<headers::util::entity::EntityTagRange>::matches_strong::{closure#0}>::{closure#0}
246
        }
247
0
    }
Unexecuted instantiation: <headers::util::entity::EntityTagRange>::matches_if::<<headers::util::entity::EntityTagRange>::matches_weak::{closure#0}>
Unexecuted instantiation: <headers::util::entity::EntityTagRange>::matches_if::<<headers::util::entity::EntityTagRange>::matches_strong::{closure#0}>
248
}
249
250
impl super::TryFromValues for EntityTagRange {
251
0
    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
252
0
    where
253
0
        I: Iterator<Item = &'i HeaderValue>,
254
    {
255
0
        let flat = FlatCsv::try_from_values(values)?;
256
0
        if flat.value == "*" {
257
0
            Ok(EntityTagRange::Any)
258
        } else {
259
0
            Ok(EntityTagRange::Tags(flat))
260
        }
261
0
    }
262
}
263
264
impl<'a> From<&'a EntityTagRange> for HeaderValue {
265
0
    fn from(tag: &'a EntityTagRange) -> HeaderValue {
266
0
        match *tag {
267
0
            EntityTagRange::Any => HeaderValue::from_static("*"),
268
0
            EntityTagRange::Tags(ref tags) => tags.into(),
269
        }
270
0
    }
271
}
272
273
#[cfg(test)]
274
mod 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
}