Skip to main content

parse_net/
etag.rs

1//! Entity tags and the `If-Match` / `If-None-Match` conditions (RFC 9110
2//! sections 8.8.3 and 13.1).
3//!
4//! # Examples
5//!
6//! ```
7//! use parse_net::{self as pn, EntityTag};
8//!
9//! let v2 = EntityTag::strong(b"v2")?;
10//! assert!(pn::if_none_match(b"\"v1\", W/\"v2\"", &v2)?);         // weak comparison: 304
11//! assert!(!pn::if_match(b"W/\"v2\"", Some(&v2))?);               // strong comparison: 412
12//! assert!(pn::if_match(b"*", Some(&v2))? && !pn::if_match(b"*", None)?);
13//! # Ok::<(), pn::ParseError>(())
14//! ```
15
16use crate::{
17    class,
18    error::{ErrorKind, ParseError, Result, WriteError, err},
19    limits::Limits,
20    list,
21    text::trim_ows,
22    write::{Sink, reserve},
23};
24
25/// An entity tag: a weak flag and the opaque bytes between the quotes.
26///
27/// # Examples
28///
29/// ```
30/// use parse_net::EntityTag;
31///
32/// let t = EntityTag::parse(b"W/\"abc\"")?;
33/// assert!(t.is_weak());
34/// assert_eq!(t.opaque(), b"abc");
35/// assert!(t.weak_eq(&EntityTag::strong(b"abc")?));
36/// assert!(!t.strong_eq(&EntityTag::strong(b"abc")?));
37/// # Ok::<(), parse_net::ParseError>(())
38/// ```
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub struct EntityTag<'a> {
41    weak: bool,
42    opaque: &'a [u8],
43}
44
45/// The offset of the first byte of `opaque` that is not an `etagc`.
46const fn first_bad(opaque: &[u8]) -> Option<usize> {
47    let mut i = 0;
48    while i < opaque.len() {
49        // `i < opaque.len()` was just checked.
50        #[allow(clippy::indexing_slicing)]
51        let b = opaque[i];
52        if !class::is_etagc(b) {
53            return Some(i);
54        }
55        i += 1;
56    }
57    None
58}
59
60impl<'a> EntityTag<'a> {
61    /// Parses `[ "W/" ] DQUOTE *etagc DQUOTE` (OWS around it stripped).
62    /// `W/` is case-sensitive.
63    ///
64    /// # Errors
65    ///
66    /// [`ErrorKind::InvalidEntityTag`] (a lowercase `w/`, no quotes, a byte
67    /// outside `etagc`, `W/ "a"`); [`ErrorKind::TrailingData`].
68    pub fn parse(value: &'a [u8]) -> Result<Self> {
69        let lead = crate::text::skip_ows(value);
70        let (tag, used) = parse_prefix(trim_ows(value)).map_err(|e| e.shifted(lead))?;
71        if used != trim_ows(value).len() {
72            return err(ErrorKind::TrailingData, lead + used);
73        }
74        Ok(tag)
75    }
76
77    /// A strong tag with the given opaque bytes.
78    ///
79    /// # Errors
80    ///
81    /// [`ErrorKind::InvalidEntityTag`] at the first byte that is not an
82    /// `etagc`.
83    pub const fn strong(opaque: &'a [u8]) -> Result<Self> {
84        match first_bad(opaque) {
85            Some(at) => Err(ParseError::new(ErrorKind::InvalidEntityTag, at)),
86            None => Ok(Self {
87                weak: false,
88                opaque,
89            }),
90        }
91    }
92
93    /// A weak tag with the given opaque bytes.
94    ///
95    /// # Errors
96    ///
97    /// As [`strong`](Self::strong).
98    pub const fn weak(opaque: &'a [u8]) -> Result<Self> {
99        match first_bad(opaque) {
100            Some(at) => Err(ParseError::new(ErrorKind::InvalidEntityTag, at)),
101            None => Ok(Self { weak: true, opaque }),
102        }
103    }
104
105    /// Whether the tag is weak (`W/`).
106    #[must_use]
107    pub const fn is_weak(&self) -> bool {
108        self.weak
109    }
110
111    /// The opaque bytes, without the quotes.
112    #[must_use]
113    pub const fn opaque(&self) -> &'a [u8] {
114        self.opaque
115    }
116
117    /// Strong comparison (RFC 9110 section 8.8.3.2): both strong and equal.
118    #[must_use]
119    pub fn strong_eq(&self, other: &EntityTag<'_>) -> bool {
120        !self.weak && !other.weak && self.opaque == other.opaque
121    }
122
123    /// Weak comparison: equal opaque bytes, either may be weak.
124    #[must_use]
125    pub fn weak_eq(&self, other: &EntityTag<'_>) -> bool {
126        self.opaque == other.opaque
127    }
128
129    /// Writes `W/"..."` or `"..."`.
130    ///
131    /// # Errors
132    ///
133    /// [`WriteError::Full`]; nothing is written when the sink reports its
134    /// room.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use parse_net::{EntityTag, SliceSink};
140    ///
141    /// let mut buf = [0u8; 16];
142    /// let mut out = SliceSink::new(&mut buf);
143    /// EntityTag::weak(b"v1")?.write_to(&mut out).expect("room");
144    /// assert_eq!(out.written(), b"W/\"v1\"");
145    /// # Ok::<(), parse_net::ParseError>(())
146    /// ```
147    pub fn write_to(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
148        let prefix: &[u8] = if self.weak { b"W/\"" } else { b"\"" };
149        reserve(out, prefix.len() + self.opaque.len() + 1)?;
150        out.put(prefix)?;
151        out.put(self.opaque)?;
152        out.put(b"\"")?;
153        Ok(())
154    }
155}
156
157/// Parses one entity tag at the start of `s`; returns it and the bytes used.
158fn parse_prefix(s: &[u8]) -> Result<(EntityTag<'_>, usize)> {
159    let (weak, start) = match s {
160        [b'W', b'/', ..] => (true, 2),
161        _ => (false, 0),
162    };
163    if s.get(start) != Some(&b'"') {
164        return err(ErrorKind::InvalidEntityTag, start);
165    }
166    let body = s.get(start + 1..).unwrap_or(&[]);
167    let close = body.iter().position(|&b| !class::is_etagc(b));
168    match close {
169        Some(i) if body.get(i) == Some(&b'"') => {
170            let opaque = body.get(..i).unwrap_or(&[]);
171            Ok((EntityTag { weak, opaque }, start + i + 2))
172        }
173        Some(i) => {
174            let b = body.get(i).copied().unwrap_or(0);
175            let kind = if crate::scan::is_control(b) {
176                crate::scan::control_kind(b)
177            } else {
178                ErrorKind::InvalidEntityTag
179            };
180            err(kind, start + 1 + i)
181        }
182        None => err(ErrorKind::InvalidEntityTag, s.len()),
183    }
184}
185
186/// An `If-Match` or `If-None-Match` value: `*` or a list of entity tags.
187#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
188pub enum Condition<'a> {
189    /// `*`: any current representation.
190    Any,
191    /// The listed tags.
192    Tags(TagList<'a>),
193}
194
195/// The tags of a validated condition.
196#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
197pub struct TagList<'a> {
198    list: list::List<'a>,
199}
200
201impl<'a> TagList<'a> {
202    /// The tags, in order.
203    pub fn iter(&self) -> impl Iterator<Item = EntityTag<'a>> + use<'a> {
204        self.list
205            .iter()
206            .filter_map(|e| parse_prefix(e).ok().map(|(t, _)| t))
207    }
208
209    /// The number of tags.
210    #[must_use]
211    pub const fn len(&self) -> usize {
212        self.list.len()
213    }
214
215    /// Whether there are no tags.
216    #[must_use]
217    pub const fn is_empty(&self) -> bool {
218        self.list.is_empty()
219    }
220}
221
222/// Parses `"*" / #entity-tag`.
223///
224/// # Errors
225///
226/// [`ErrorKind::Malformed`] for `*` mixed with tags; [`ErrorKind::InvalidEntityTag`];
227/// the list errors.
228///
229/// # Examples
230///
231/// ```
232/// use parse_net::{etag::{parse_condition, Condition}, Limits};
233///
234/// let Condition::Tags(tags) = parse_condition(b"\"a\", , W/\"b\"", &Limits::DEFAULT)? else { unreachable!() };
235/// assert_eq!(tags.len(), 2);
236/// assert!(matches!(parse_condition(b"*", &Limits::DEFAULT)?, Condition::Any));
237/// assert!(parse_condition(b"*, \"a\"", &Limits::DEFAULT).is_err());
238/// # Ok::<(), parse_net::ParseError>(())
239/// ```
240pub fn parse_condition<'a>(value: &'a [u8], limits: &Limits) -> Result<Condition<'a>> {
241    let l = list::parse(value, limits)?;
242    let mut star = false;
243    for e in l.iter() {
244        let at = (e.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
245        if e == b"*" {
246            star = true;
247            continue;
248        }
249        let (_, used) = parse_prefix(e).map_err(|x| x.shifted(at))?;
250        if used != e.len() {
251            return err(ErrorKind::InvalidEntityTag, at + used);
252        }
253    }
254    if star {
255        if l.len() != 1 {
256            return err(ErrorKind::Malformed, 0);
257        }
258        return Ok(Condition::Any);
259    }
260    Ok(Condition::Tags(TagList { list: l }))
261}
262
263/// Evaluates `If-None-Match` against the current representation's tag by
264/// weak comparison (RFC 9110 section 13.1.2).
265///
266/// `true` means the condition is false for the request: answer 304 (GET,
267/// HEAD) or 412 (other methods). `*` matches any current representation.
268///
269/// # Errors
270///
271/// As [`parse_condition`].
272///
273/// # Examples
274///
275/// ```
276/// use parse_net::{if_none_match, EntityTag};
277///
278/// assert!(if_none_match(b"W/\"x\"", &EntityTag::strong(b"x")?)?);
279/// assert!(!if_none_match(b"\"y\"", &EntityTag::strong(b"x")?)?);
280/// # Ok::<(), parse_net::ParseError>(())
281/// ```
282pub fn if_none_match(value: &[u8], current: &EntityTag<'_>) -> Result<bool> {
283    Ok(match parse_condition(value, &Limits::DEFAULT)? {
284        Condition::Any => true,
285        Condition::Tags(tags) => tags.iter().any(|t| t.weak_eq(current)),
286    })
287}
288
289/// Evaluates `If-Match` by strong comparison (RFC 9110 section 13.1.1).
290///
291/// `true` means the precondition holds; `false` means answer 412. `*` holds
292/// when a current representation exists (`current` is `Some`).
293///
294/// # Errors
295///
296/// As [`parse_condition`].
297///
298/// # Examples
299///
300/// ```
301/// use parse_net::{if_match, EntityTag};
302///
303/// let current = EntityTag::strong(b"x")?;
304/// assert!(if_match(b"\"x\"", Some(&current))?);
305/// assert!(!if_match(b"W/\"x\"", Some(&current))?);
306/// assert!(!if_match(b"\"x\"", None)?);
307/// # Ok::<(), parse_net::ParseError>(())
308/// ```
309pub fn if_match(value: &[u8], current: Option<&EntityTag<'_>>) -> Result<bool> {
310    Ok(match parse_condition(value, &Limits::DEFAULT)? {
311        Condition::Any => current.is_some(),
312        Condition::Tags(tags) => current.is_some_and(|c| tags.iter().any(|t| t.strong_eq(c))),
313    })
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn test_entity_tag_edge_cases() {
322        assert_eq!(EntityTag::parse(b"\"\"").unwrap().opaque(), b"");
323        assert!(EntityTag::parse(b"W/\"\"").unwrap().is_weak());
324        let k = |v: &[u8]| EntityTag::parse(v).unwrap_err().kind();
325        assert_eq!(k(b"w/\"a\""), ErrorKind::InvalidEntityTag);
326        assert_eq!(k(b"\"a\"b"), ErrorKind::TrailingData);
327        assert_eq!(k(b"\"a"), ErrorKind::InvalidEntityTag);
328        assert_eq!(k(b"W/ \"a\""), ErrorKind::InvalidEntityTag);
329        assert_eq!(k(b"\"a\x7f\""), ErrorKind::Control);
330        assert_eq!(k(b"\"a b\""), ErrorKind::InvalidEntityTag);
331        assert_eq!(k(b"abc"), ErrorKind::InvalidEntityTag);
332        assert_eq!(k(b""), ErrorKind::InvalidEntityTag);
333        assert!(EntityTag::parse(b"\"\x80\xff!\"").is_ok());
334        assert!(EntityTag::strong(b"a b").is_err());
335        assert!(EntityTag::weak(b"\"").is_err());
336    }
337
338    #[test]
339    fn test_comparison_table_rfc_9110() {
340        // RFC 9110 section 8.8.3.2.
341        let w1 = EntityTag::weak(b"1").unwrap();
342        let w2 = EntityTag::weak(b"2").unwrap();
343        let s1 = EntityTag::strong(b"1").unwrap();
344        assert!(!w1.strong_eq(&w1) && w1.weak_eq(&w1));
345        assert!(!w1.strong_eq(&w2) && !w1.weak_eq(&w2));
346        assert!(!w1.strong_eq(&s1) && w1.weak_eq(&s1));
347        assert!(s1.strong_eq(&s1) && s1.weak_eq(&s1));
348    }
349
350    #[test]
351    fn test_conditions() {
352        let l = Limits::DEFAULT;
353        assert_eq!(
354            parse_condition(b"*, \"a\"", &l).unwrap_err().kind(),
355            ErrorKind::Malformed
356        );
357        assert_eq!(
358            parse_condition(b"\"a\" \"b\"", &l).unwrap_err().kind(),
359            ErrorKind::InvalidEntityTag
360        );
361        let e = parse_condition(b"\"a\", w/\"b\"", &l).unwrap_err();
362        assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidEntityTag, 5));
363        let s = EntityTag::strong(b"b").unwrap();
364        assert!(if_none_match(b"\"a\", , \"b\"", &s).unwrap());
365        assert!(if_none_match(b"*", &s).unwrap());
366        assert!(!if_match(b"*", None).unwrap());
367        assert!(if_match(b"\"a\", \"b\"", Some(&s)).unwrap());
368        assert!(if_none_match(b"\"a", &s).is_err());
369    }
370
371    #[test]
372    fn test_write_round_trip() {
373        let mut buf = [0u8; 8];
374        let mut out = crate::SliceSink::new(&mut buf);
375        let t = EntityTag::strong(b"xyz").unwrap();
376        t.write_to(&mut out).unwrap();
377        assert_eq!(EntityTag::parse(out.written()).unwrap(), t);
378        let mut small = [0u8; 3];
379        let mut out = crate::SliceSink::new(&mut small);
380        assert_eq!(t.write_to(&mut out), Err(WriteError::Full));
381    }
382}