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