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(¤t))?);
511/// assert!(!if_match(b"W/\"x\"", Some(¤t))?);
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}