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 text::{skip_ows, trim_ows},
21 write::{Sink, reserve},
22};
23
24/// An entity tag: a weak flag and the opaque bytes between the quotes.
25///
26/// # Examples
27///
28/// ```
29/// use parse_net::EntityTag;
30///
31/// let t = EntityTag::parse(b"W/\"abc\"")?;
32/// assert!(t.is_weak());
33/// assert_eq!(t.opaque(), b"abc");
34/// assert!(t.weak_eq(&EntityTag::strong(b"abc")?));
35/// assert!(!t.strong_eq(&EntityTag::strong(b"abc")?));
36/// # Ok::<(), parse_net::ParseError>(())
37/// ```
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39pub struct EntityTag<'a> {
40 pub(crate) weak: bool,
41 pub(crate) opaque: &'a [u8],
42}
43
44/// The offset of the first byte of `opaque` that is not an `etagc`.
45const fn first_bad(opaque: &[u8]) -> Option<usize> {
46 let mut i = 0;
47 while i < opaque.len() {
48 // `i < opaque.len()` was just checked.
49 #[allow(clippy::indexing_slicing)]
50 let b = opaque[i];
51 if !class::is_etagc(b) {
52 return Some(i);
53 }
54 i += 1;
55 }
56 None
57}
58
59impl<'a> EntityTag<'a> {
60 /// Parses `[ "W/" ] DQUOTE *etagc DQUOTE` (OWS around it stripped).
61 /// `W/` is case-sensitive.
62 ///
63 /// # Errors
64 ///
65 /// [`ErrorKind::InvalidEntityTag`] (a lowercase `w/`, no quotes, a byte
66 /// outside `etagc`, `W/ "a"`); [`ErrorKind::TrailingData`].
67 ///
68 /// # Examples
69 ///
70 /// ```
71 /// use parse_net::EntityTag;
72 ///
73 /// let t = EntityTag::parse(b" \"xyzzy\" ")?;
74 /// assert!(!t.is_weak() && t.opaque() == b"xyzzy");
75 /// assert!(EntityTag::parse(b"w/\"xyzzy\"").is_err());
76 /// # Ok::<(), parse_net::ParseError>(())
77 /// ```
78 pub fn parse(value: &'a [u8]) -> Result<Self> {
79 let lead = crate::text::skip_ows(value);
80 let (tag, used) = parse_prefix(trim_ows(value)).map_err(|e| e.shifted(lead))?;
81 if used != trim_ows(value).len() {
82 return err(ErrorKind::TrailingData, lead + used);
83 }
84 Ok(tag)
85 }
86
87 /// A strong tag with the given opaque bytes.
88 ///
89 /// # Errors
90 ///
91 /// [`ErrorKind::InvalidEntityTag`] at the first byte that is not an
92 /// `etagc`.
93 ///
94 /// # Examples
95 ///
96 /// ```
97 /// use parse_net::EntityTag;
98 ///
99 /// assert!(!EntityTag::strong(b"v1")?.is_weak());
100 /// assert!(EntityTag::strong(b"a\"b").is_err());
101 /// # Ok::<(), parse_net::ParseError>(())
102 /// ```
103 pub const fn strong(opaque: &'a [u8]) -> Result<Self> {
104 match first_bad(opaque) {
105 Some(at) => Err(ParseError::new(ErrorKind::InvalidEntityTag, at)),
106 None => Ok(Self {
107 weak: false,
108 opaque,
109 }),
110 }
111 }
112
113 /// A weak tag with the given opaque bytes.
114 ///
115 /// # Errors
116 ///
117 /// As [`strong`](Self::strong).
118 ///
119 /// # Examples
120 ///
121 /// ```
122 /// let t = parse_net::EntityTag::weak(b"v1")?;
123 /// assert!(t.is_weak() && t.opaque() == b"v1");
124 /// # Ok::<(), parse_net::ParseError>(())
125 /// ```
126 pub const fn weak(opaque: &'a [u8]) -> Result<Self> {
127 match first_bad(opaque) {
128 Some(at) => Err(ParseError::new(ErrorKind::InvalidEntityTag, at)),
129 None => Ok(Self { weak: true, opaque }),
130 }
131 }
132
133 /// Whether the tag is weak (`W/`).
134 ///
135 /// # Examples
136 ///
137 /// ```
138 /// use parse_net::EntityTag;
139 ///
140 /// assert!(EntityTag::parse(b"W/\"1\"")?.is_weak());
141 /// assert!(!EntityTag::parse(b"\"1\"")?.is_weak());
142 /// # Ok::<(), parse_net::ParseError>(())
143 /// ```
144 #[must_use]
145 pub const fn is_weak(&self) -> bool {
146 self.weak
147 }
148
149 /// The opaque bytes, without the quotes.
150 ///
151 /// # Examples
152 ///
153 /// ```
154 /// use parse_net::EntityTag;
155 ///
156 /// assert_eq!(EntityTag::parse(b"W/\"abc\"")?.opaque(), b"abc");
157 /// assert_eq!(EntityTag::parse(b"\"\"")?.opaque(), b"");
158 /// # Ok::<(), parse_net::ParseError>(())
159 /// ```
160 #[must_use]
161 pub const fn opaque(&self) -> &'a [u8] {
162 self.opaque
163 }
164
165 /// Strong comparison (RFC 9110 section 8.8.3.2): both strong and equal.
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// use parse_net::EntityTag;
171 ///
172 /// let a = EntityTag::strong(b"1")?;
173 /// assert!(a.strong_eq(&EntityTag::parse(b"\"1\"")?));
174 /// assert!(!a.strong_eq(&EntityTag::weak(b"1")?));
175 /// # Ok::<(), parse_net::ParseError>(())
176 /// ```
177 #[must_use]
178 pub fn strong_eq(&self, other: &EntityTag<'_>) -> bool {
179 !self.weak && !other.weak && self.opaque == other.opaque
180 }
181
182 /// Weak comparison: equal opaque bytes, either may be weak.
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// use parse_net::EntityTag;
188 ///
189 /// let (w, s) = (EntityTag::weak(b"1")?, EntityTag::strong(b"1")?);
190 /// assert!(w.weak_eq(&s) && !w.weak_eq(&EntityTag::strong(b"2")?));
191 /// # Ok::<(), parse_net::ParseError>(())
192 /// ```
193 #[must_use]
194 pub fn weak_eq(&self, other: &EntityTag<'_>) -> bool {
195 self.opaque == other.opaque
196 }
197
198 /// Writes `W/"..."` or `"..."`.
199 ///
200 /// # Errors
201 ///
202 /// [`WriteError::Full`]; nothing is written when the sink reports its
203 /// room.
204 ///
205 /// # Examples
206 ///
207 /// ```
208 /// use parse_net::{EntityTag, SliceSink};
209 ///
210 /// let mut buf = [0u8; 16];
211 /// let mut out = SliceSink::new(&mut buf);
212 /// EntityTag::weak(b"v1")?.write_to(&mut out).expect("room");
213 /// assert_eq!(out.written(), b"W/\"v1\"");
214 /// # Ok::<(), parse_net::ParseError>(())
215 /// ```
216 pub fn write_to(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
217 let prefix: &[u8] = if self.weak { b"W/\"" } else { b"\"" };
218 reserve(out, prefix.len() + self.opaque.len() + 1)?;
219 out.put(prefix)?;
220 out.put(self.opaque)?;
221 out.put(b"\"")?;
222 Ok(())
223 }
224}
225
226/// Parses one entity tag at the start of `s`; returns it and the bytes used.
227fn parse_prefix(s: &[u8]) -> Result<(EntityTag<'_>, usize)> {
228 let (weak, start) = match s {
229 [b'W', b'/', ..] => (true, 2),
230 _ => (false, 0),
231 };
232 if s.get(start) != Some(&b'"') {
233 return err(ErrorKind::InvalidEntityTag, start);
234 }
235 let body = s.get(start + 1..).unwrap_or(&[]);
236 let close = body.iter().position(|&b| !class::is_etagc(b));
237 match close {
238 Some(i) if body.get(i) == Some(&b'"') => {
239 let opaque = body.get(..i).unwrap_or(&[]);
240 Ok((EntityTag { weak, opaque }, start + i + 2))
241 }
242 Some(i) => {
243 let b = body.get(i).copied().unwrap_or(0);
244 let kind = if crate::scan::is_control(b) {
245 crate::scan::control_kind(b)
246 } else {
247 ErrorKind::InvalidEntityTag
248 };
249 err(kind, start + 1 + i)
250 }
251 None => err(ErrorKind::InvalidEntityTag, s.len()),
252 }
253}
254
255/// An `If-Match` or `If-None-Match` value: `*` or a list of entity tags.
256///
257/// # Examples
258///
259/// ```
260/// use parse_net::{etag::parse_condition, Condition, Limits};
261///
262/// assert_eq!(parse_condition(b"*", &Limits::DEFAULT)?, Condition::Any);
263/// assert!(matches!(parse_condition(b"\"a\"", &Limits::DEFAULT)?, Condition::Tags(t) if t.len() == 1));
264/// # Ok::<(), parse_net::ParseError>(())
265/// ```
266#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
267pub enum Condition<'a> {
268 /// `*`: any current representation.
269 Any,
270 /// The listed tags.
271 Tags(TagList<'a>),
272}
273
274/// The tags of a validated condition.
275///
276/// # Examples
277///
278/// ```
279/// use parse_net::{etag::parse_condition, Condition, Limits};
280///
281/// let Condition::Tags(tags) = parse_condition(b"\"a\", W/\"b\"", &Limits::DEFAULT)? else { unreachable!() };
282/// assert_eq!(tags.iter().filter(|t| t.is_weak()).count(), 1);
283/// # Ok::<(), parse_net::ParseError>(())
284/// ```
285#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
286pub struct TagList<'a> {
287 /// The validated value, OWS trimmed.
288 text: &'a [u8],
289 count: usize,
290}
291
292impl<'a> TagList<'a> {
293 /// The tags, in order.
294 ///
295 /// # Examples
296 ///
297 /// ```
298 /// use parse_net::{etag::parse_condition, Condition, Limits};
299 ///
300 /// let Condition::Tags(tags) = parse_condition(b"\"a\", W/\"b\"", &Limits::DEFAULT)? else { unreachable!() };
301 /// let opaque: Vec<_> = tags.iter().map(|t| t.opaque()).collect();
302 /// assert_eq!(opaque, [&b"a"[..], &b"b"[..]]);
303 /// # Ok::<(), parse_net::ParseError>(())
304 /// ```
305 pub fn iter(&self) -> impl Iterator<Item = EntityTag<'a>> + use<'a> {
306 TagIter { s: self.text, i: 0 }
307 }
308
309 /// The number of tags.
310 ///
311 /// # Examples
312 ///
313 /// ```
314 /// use parse_net::{etag::parse_condition, Condition, Limits};
315 ///
316 /// let Condition::Tags(tags) = parse_condition(b"\"a\", , \"b\"", &Limits::DEFAULT)? else { unreachable!() };
317 /// assert_eq!(tags.len(), 2);
318 /// # Ok::<(), parse_net::ParseError>(())
319 /// ```
320 #[must_use]
321 pub const fn len(&self) -> usize {
322 self.count
323 }
324
325 /// Whether there are no tags.
326 ///
327 /// # Examples
328 ///
329 /// ```
330 /// use parse_net::{etag::parse_condition, Condition, Limits};
331 ///
332 /// let Condition::Tags(tags) = parse_condition(b"\"a\"", &Limits::DEFAULT)? else { unreachable!() };
333 /// assert!(!tags.is_empty());
334 /// # Ok::<(), parse_net::ParseError>(())
335 /// ```
336 #[must_use]
337 pub const fn is_empty(&self) -> bool {
338 self.count == 0
339 }
340}
341
342/// The tags of a validated [`TagList`].
343struct TagIter<'a> {
344 s: &'a [u8],
345 i: usize,
346}
347
348impl<'a> Iterator for TagIter<'a> {
349 type Item = EntityTag<'a>;
350
351 fn next(&mut self) -> Option<EntityTag<'a>> {
352 while matches!(self.s.get(self.i), Some(b' ' | b'\t' | b',')) {
353 self.i += 1;
354 }
355 let (tag, used) = parse_prefix(self.s.get(self.i..)?).ok()?;
356 self.i += used;
357 Some(tag)
358 }
359}
360
361/// What a condition value holds after [`walk`].
362enum Walked {
363 Any,
364 Tags(usize),
365}
366
367/// Validates `"*" / #entity-tag` in one pass over `v` (OWS already trimmed),
368/// calling `f` for every tag. Empty list elements are skipped up to
369/// `max_empty_elements`; `*` must stand alone.
370fn walk<'a>(v: &'a [u8], limits: &Limits, mut f: impl FnMut(EntityTag<'a>)) -> Result<Walked> {
371 let (mut i, mut count, mut empty, mut star) = (0usize, 0usize, 0usize, false);
372 loop {
373 while matches!(v.get(i), Some(b' ' | b'\t')) {
374 i += 1;
375 }
376 match v.get(i) {
377 None => break,
378 Some(b',') => {
379 empty += 1;
380 if empty > usize::from(limits.empty_elements) {
381 return err(ErrorKind::TooManyEmptyElements, i);
382 }
383 i += 1;
384 continue;
385 }
386 Some(b'*') if !star && count == 0 => {
387 star = true;
388 i += 1;
389 }
390 Some(_) if star => return err(ErrorKind::Malformed, i),
391 Some(_) => {
392 let (tag, used) =
393 parse_prefix(v.get(i..).unwrap_or(&[])).map_err(|e| match e.kind() {
394 ErrorKind::InvalidEntityTag
395 if e.offset() + i >= v.len() && v.get(i) == Some(&b'"') =>
396 {
397 ParseError::new(ErrorKind::UnterminatedQuote, v.len())
398 }
399 _ => e.shifted(i),
400 })?;
401 count += 1;
402 if count > usize::from(limits.list_members) {
403 return err(ErrorKind::TooManyMembers, i);
404 }
405 f(tag);
406 i += used;
407 }
408 }
409 while matches!(v.get(i), Some(b' ' | b'\t')) {
410 i += 1;
411 }
412 match v.get(i) {
413 None => break,
414 Some(b',') => {}
415 Some(&b) if crate::scan::is_control(b) => return err(crate::scan::control_kind(b), i),
416 Some(_) if star => return err(ErrorKind::Malformed, i),
417 Some(_) => return err(ErrorKind::InvalidEntityTag, i),
418 }
419 }
420 Ok(if star {
421 Walked::Any
422 } else {
423 Walked::Tags(count)
424 })
425}
426
427/// Parses `"*" / #entity-tag`.
428///
429/// # Errors
430///
431/// [`ErrorKind::Malformed`] for `*` mixed with tags; [`ErrorKind::InvalidEntityTag`];
432/// the list errors.
433///
434/// # Examples
435///
436/// ```
437/// use parse_net::{etag::{parse_condition, Condition}, Limits};
438///
439/// let Condition::Tags(tags) = parse_condition(b"\"a\", , W/\"b\"", &Limits::DEFAULT)? else { unreachable!() };
440/// assert_eq!(tags.len(), 2);
441/// assert!(matches!(parse_condition(b"*", &Limits::DEFAULT)?, Condition::Any));
442/// assert!(parse_condition(b"*, \"a\"", &Limits::DEFAULT).is_err());
443/// # Ok::<(), parse_net::ParseError>(())
444/// ```
445pub fn parse_condition<'a>(value: &'a [u8], limits: &Limits) -> Result<Condition<'a>> {
446 let lead = skip_ows(value);
447 let v = trim_ows(value);
448 Ok(
449 match walk(v, limits, |_| {}).map_err(|e| e.shifted(lead))? {
450 Walked::Any => Condition::Any,
451 Walked::Tags(count) => Condition::Tags(TagList { text: v, count }),
452 },
453 )
454}
455
456/// Evaluates `If-None-Match` against the current representation's tag by
457/// weak comparison (RFC 9110 section 13.1.2).
458///
459/// `true` means the condition is false for the request: answer 304 (GET,
460/// HEAD) or 412 (other methods). `*` matches any current representation.
461///
462/// # Errors
463///
464/// As [`parse_condition`].
465///
466/// # Examples
467///
468/// ```
469/// use parse_net::{if_none_match, EntityTag};
470///
471/// assert!(if_none_match(b"W/\"x\"", &EntityTag::strong(b"x")?)?);
472/// assert!(!if_none_match(b"\"y\"", &EntityTag::strong(b"x")?)?);
473/// # Ok::<(), parse_net::ParseError>(())
474/// ```
475pub fn if_none_match(value: &[u8], current: &EntityTag<'_>) -> Result<bool> {
476 // One pass: the whole field is validated, and the comparison runs as each
477 // tag is read.
478 let lead = skip_ows(value);
479 let mut matched = false;
480 let walked = walk(trim_ows(value), &Limits::DEFAULT, |t| {
481 matched |= t.weak_eq(current)
482 })
483 .map_err(|e| e.shifted(lead))?;
484 Ok(matches!(walked, Walked::Any) || matched)
485}
486
487/// Evaluates `If-Match` by strong comparison (RFC 9110 section 13.1.1).
488///
489/// `true` means the precondition holds; `false` means answer 412. `*` holds
490/// when a current representation exists (`current` is `Some`).
491///
492/// # Errors
493///
494/// As [`parse_condition`].
495///
496/// # Examples
497///
498/// ```
499/// use parse_net::{if_match, EntityTag};
500///
501/// let current = EntityTag::strong(b"x")?;
502/// assert!(if_match(b"\"x\"", Some(¤t))?);
503/// assert!(!if_match(b"W/\"x\"", Some(¤t))?);
504/// assert!(!if_match(b"\"x\"", None)?);
505/// # Ok::<(), parse_net::ParseError>(())
506/// ```
507pub fn if_match(value: &[u8], current: Option<&EntityTag<'_>>) -> Result<bool> {
508 let lead = skip_ows(value);
509 let mut matched = false;
510 let walked = walk(trim_ows(value), &Limits::DEFAULT, |t| {
511 matched |= current.is_some_and(|c| t.strong_eq(c));
512 })
513 .map_err(|e| e.shifted(lead))?;
514 Ok(match walked {
515 Walked::Any => current.is_some(),
516 Walked::Tags(_) => matched,
517 })
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn test_entity_tag_edge_cases() {
526 assert_eq!(EntityTag::parse(b"\"\"").unwrap().opaque(), b"");
527 assert!(EntityTag::parse(b"W/\"\"").unwrap().is_weak());
528 let k = |v: &[u8]| EntityTag::parse(v).unwrap_err().kind();
529 assert_eq!(k(b"w/\"a\""), ErrorKind::InvalidEntityTag);
530 assert_eq!(k(b"\"a\"b"), ErrorKind::TrailingData);
531 assert_eq!(k(b"\"a"), ErrorKind::InvalidEntityTag);
532 assert_eq!(k(b"W/ \"a\""), ErrorKind::InvalidEntityTag);
533 assert_eq!(k(b"\"a\x7f\""), ErrorKind::Control);
534 assert_eq!(k(b"\"a b\""), ErrorKind::InvalidEntityTag);
535 assert_eq!(k(b"abc"), ErrorKind::InvalidEntityTag);
536 assert_eq!(k(b""), ErrorKind::InvalidEntityTag);
537 assert!(EntityTag::parse(b"\"\x80\xff!\"").is_ok());
538 assert!(EntityTag::strong(b"a b").is_err());
539 assert!(EntityTag::weak(b"\"").is_err());
540 }
541
542 #[test]
543 fn test_comparison_table_rfc_9110() {
544 // RFC 9110 section 8.8.3.2.
545 let w1 = EntityTag::weak(b"1").unwrap();
546 let w2 = EntityTag::weak(b"2").unwrap();
547 let s1 = EntityTag::strong(b"1").unwrap();
548 assert!(!w1.strong_eq(&w1) && w1.weak_eq(&w1));
549 assert!(!w1.strong_eq(&w2) && !w1.weak_eq(&w2));
550 assert!(!w1.strong_eq(&s1) && w1.weak_eq(&s1));
551 assert!(s1.strong_eq(&s1) && s1.weak_eq(&s1));
552 }
553
554 #[test]
555 fn test_conditions() {
556 let l = Limits::DEFAULT;
557 assert_eq!(
558 parse_condition(b"*, \"a\"", &l).unwrap_err().kind(),
559 ErrorKind::Malformed
560 );
561 assert_eq!(
562 parse_condition(b"\"a\" \"b\"", &l).unwrap_err().kind(),
563 ErrorKind::InvalidEntityTag
564 );
565 let e = parse_condition(b"\"a\", w/\"b\"", &l).unwrap_err();
566 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidEntityTag, 5));
567 let s = EntityTag::strong(b"b").unwrap();
568 assert!(if_none_match(b"\"a\", , \"b\"", &s).unwrap());
569 assert!(if_none_match(b"*", &s).unwrap());
570 assert!(!if_match(b"*", None).unwrap());
571 assert!(if_match(b"\"a\", \"b\"", Some(&s)).unwrap());
572 assert!(if_none_match(b"\"a", &s).is_err());
573 }
574
575 #[test]
576 fn test_write_round_trip() {
577 let mut buf = [0u8; 8];
578 let mut out = crate::SliceSink::new(&mut buf);
579 let t = EntityTag::strong(b"xyz").unwrap();
580 t.write_to(&mut out).unwrap();
581 assert_eq!(EntityTag::parse(out.written()).unwrap(), t);
582 let mut small = [0u8; 3];
583 let mut out = crate::SliceSink::new(&mut small);
584 assert_eq!(t.write_to(&mut out), Err(WriteError::Full));
585 }
586}