parse_net/text.rs
1//! The lexical forms of RFC 9110 section 5.6: tokens, optional whitespace,
2//! quoted strings, words (`token / quoted-string`), and comments.
3//!
4//! # Examples
5//!
6//! ```
7//! use parse_net::text;
8//!
9//! assert!(text::is_token(b"no-cache") && !text::is_token(b"no cache"));
10//! assert_eq!(text::trim_ows(b" \t gzip \t"), b"gzip");
11//! let (q, used) = text::parse_quoted(b"\"a \\\"b\\\" c\"; x")?;
12//! assert_eq!(used, 11);
13//! assert!(q.eq_bytes(b"a \"b\" c"));
14//! # Ok::<(), parse_net::ParseError>(())
15//! ```
16
17use core::fmt;
18
19use crate::{
20 class,
21 error::{ErrorKind, Result, err},
22 limits::Limits,
23 scan::control_kind,
24};
25
26/// Whether `s` is a token: non-empty, every byte `tchar` (RFC 9110 section
27/// 5.6.2).
28///
29/// # Examples
30///
31/// ```
32/// use parse_net::text::is_token;
33///
34/// assert!(is_token(b"GET") && !is_token(b"GE T") && !is_token(b""));
35/// ```
36#[inline]
37#[must_use]
38pub fn is_token(s: &[u8]) -> bool {
39 !s.is_empty() && class::all(s, class::Class::TCHAR)
40}
41
42/// Whether `b` is optional whitespace: SP or HTAB only.
43///
44/// # Examples
45///
46/// ```
47/// use parse_net::text::is_ows;
48///
49/// assert!(is_ows(b' ') && is_ows(b'\t') && !is_ows(0x0b));
50/// ```
51#[inline]
52#[must_use]
53pub const fn is_ows(b: u8) -> bool {
54 b == b' ' || b == b'\t'
55}
56
57/// `s` without leading and trailing SP and HTAB.
58///
59/// `<[u8]>::trim_ascii` is deliberately not used: it also strips vertical tab
60/// and form feed, which are invalid in a field value and must be refused, not
61/// silently removed. NBSP and every other byte stay too.
62///
63/// # Examples
64///
65/// ```
66/// use parse_net::text::trim_ows;
67///
68/// assert_eq!(trim_ows(b" \t a b \t"), b"a b");
69/// assert_eq!(trim_ows(b"\x0ba\x0c"), b"\x0ba\x0c");
70/// assert_eq!(trim_ows(b" "), b"");
71/// ```
72#[inline]
73#[must_use]
74pub fn trim_ows(mut s: &[u8]) -> &[u8] {
75 while let [first, rest @ ..] = s {
76 if is_ows(*first) {
77 s = rest;
78 } else {
79 break;
80 }
81 }
82 while let [rest @ .., last] = s {
83 if is_ows(*last) {
84 s = rest;
85 } else {
86 break;
87 }
88 }
89 s
90}
91
92/// The number of leading SP and HTAB bytes in `s`.
93///
94/// # Examples
95///
96/// ```
97/// assert_eq!(parse_net::text::skip_ows(b" \t x"), 3);
98/// ```
99#[inline]
100#[must_use]
101pub fn skip_ows(s: &[u8]) -> usize {
102 s.iter().take_while(|&&b| is_ows(b)).count()
103}
104
105/// The length of the longest token prefix of `s` (0 when `s` does not start
106/// with a `tchar`).
107#[inline]
108pub(crate) fn token_len(s: &[u8]) -> usize {
109 s.iter()
110 .position(|&b| !class::is_tchar(b))
111 .unwrap_or(s.len())
112}
113
114/// The word at the front of text a parser has already validated: the view
115/// and the bytes used. Nothing is checked again; quoted strings are only
116/// scanned for their closing quote.
117#[inline]
118pub(crate) fn validated_word(s: &[u8]) -> (Word<'_>, usize) {
119 let Some((&b'"', body)) = s.split_first() else {
120 let n = token_len(s);
121 return (Word::Token(s.get(..n).unwrap_or(&[])), n);
122 };
123 let mut has_escapes = false;
124 let mut rest = body;
125 loop {
126 match rest {
127 [b'"', ..] | [] => break,
128 [b'\\', _, tail @ ..] => {
129 has_escapes = true;
130 rest = tail;
131 }
132 [_, tail @ ..] => rest = tail,
133 }
134 }
135 let len = body.len() - rest.len();
136 let raw = body.get(..len).unwrap_or(&[]);
137 (Word::Quoted(Quoted { raw, has_escapes }), len + 2)
138}
139
140/// The longest token prefix of `s`, and its length.
141///
142/// The rest of `s` is the caller's to check.
143///
144/// # Errors
145///
146/// [`ErrorKind::Empty`] when `s` does not start with a `tchar`.
147///
148/// # Examples
149///
150/// ```
151/// use parse_net::text::parse_token;
152///
153/// assert_eq!(parse_token(b"charset=utf-8")?, (&b"charset"[..], 7));
154/// assert!(parse_token(b"=x").is_err());
155/// # Ok::<(), parse_net::ParseError>(())
156/// ```
157pub fn parse_token(s: &[u8]) -> Result<(&[u8], usize)> {
158 let n = token_len(s);
159 if n == 0 {
160 return err(ErrorKind::Empty, 0);
161 }
162 Ok((s.get(..n).unwrap_or(&[]), n))
163}
164
165/// The error for a byte refused inside a quoted string or comment.
166#[inline]
167const fn refused(b: u8) -> ErrorKind {
168 if crate::scan::is_control(b) {
169 control_kind(b)
170 } else {
171 ErrorKind::InvalidByte
172 }
173}
174
175/// Parses the quoted string at the start of `s` (RFC 9110 section 5.6.4).
176///
177/// Returns the view and the length used, both quotes included.
178///
179/// # Errors
180///
181/// [`ErrorKind::InvalidByte`] at offset 0 when `s` does not start with `"`;
182/// [`ErrorKind::BareCr`], [`ErrorKind::LineFeed`], [`ErrorKind::Nul`], or
183/// [`ErrorKind::Control`] for a control byte inside; [`ErrorKind::InvalidQuotedPair`]
184/// when the byte after `\` is not `HTAB / SP / VCHAR / obs-text`;
185/// [`ErrorKind::UnterminatedQuote`] at the end of input.
186///
187/// # Examples
188///
189/// ```
190/// use parse_net::text::parse_quoted;
191///
192/// let (q, used) = parse_quoted(b"\"say \\\"hi\\\"\" rest")?;
193/// assert_eq!(used, 12);
194/// assert_eq!(q.raw(), b"say \\\"hi\\\"");
195/// # Ok::<(), parse_net::ParseError>(())
196/// ```
197pub fn parse_quoted(s: &[u8]) -> Result<(Quoted<'_>, usize)> {
198 let Some((&b'"', body)) = s.split_first() else {
199 return err(ErrorKind::InvalidByte, 0);
200 };
201 let mut has_escapes = false;
202 let mut i = 0usize;
203 let mut bytes = body.iter();
204 while let Some(&b) = bytes.next() {
205 match b {
206 b'"' => {
207 let raw = body.get(..i).unwrap_or(&[]);
208 return Ok((Quoted { raw, has_escapes }, i + 2));
209 }
210 b'\\' => {
211 has_escapes = true;
212 match bytes.next() {
213 None => return err(ErrorKind::UnterminatedQuote, s.len()),
214 Some(&e) if class::is_quoted_pair_char(e) => i += 2,
215 Some(_) => return err(ErrorKind::InvalidQuotedPair, i + 2),
216 }
217 }
218 _ if class::is_qdtext(b) => i += 1,
219 _ => return err(refused(b), i + 1),
220 }
221 }
222 err(ErrorKind::UnterminatedQuote, s.len())
223}
224
225/// Parses a word at the start of `s`: a quoted string when `s` starts with
226/// `"`, a token otherwise.
227///
228/// # Errors
229///
230/// As [`parse_token`] and [`parse_quoted`].
231///
232/// # Examples
233///
234/// ```
235/// use parse_net::text::parse_word;
236///
237/// let (a, _) = parse_word(b"utf-8")?;
238/// let (b, _) = parse_word(b"\"UTF-8\"")?;
239/// assert!(a.eq_ignore_ascii_case(b"UTF-8") && b.eq_ignore_ascii_case(b"utf-8"));
240/// # Ok::<(), parse_net::ParseError>(())
241/// ```
242pub fn parse_word(s: &[u8]) -> Result<(Word<'_>, usize)> {
243 if s.first() == Some(&b'"') {
244 let (q, n) = parse_quoted(s)?;
245 Ok((Word::Quoted(q), n))
246 } else {
247 let (t, n) = parse_token(s)?;
248 Ok((Word::Token(t), n))
249 }
250}
251
252/// Parses the comment at the start of `s` (RFC 9110 section 5.6.5), nested
253/// comments included, counted and never recursed.
254///
255/// Returns the view and the length used, both outer parentheses included.
256///
257/// # Errors
258///
259/// [`ErrorKind::InvalidByte`] at offset 0 when `s` does not start with `(`;
260/// a control kind for a control byte; [`ErrorKind::InvalidQuotedPair`];
261/// [`ErrorKind::UnbalancedComment`] when the comment never closes;
262/// [`ErrorKind::CommentTooDeep`] at the `(` that exceeded
263/// [`Limits::max_comment_depth`].
264///
265/// # Examples
266///
267/// ```
268/// use parse_net::{text::parse_comment, Limits};
269///
270/// let (c, used) = parse_comment(b"(compatible; (nested)) rest", &Limits::DEFAULT)?;
271/// assert_eq!((c.raw(), c.max_depth(), used), (&b"compatible; (nested)"[..], 2, 22));
272/// # Ok::<(), parse_net::ParseError>(())
273/// ```
274pub fn parse_comment<'a>(s: &'a [u8], limits: &Limits) -> Result<(Comment<'a>, usize)> {
275 if s.first() != Some(&b'(') {
276 return err(ErrorKind::InvalidByte, 0);
277 }
278 let end = comment_end(s, 0, limits)?;
279 let (end, max_depth) = end;
280 let raw = s.get(1..end - 1).unwrap_or(&[]);
281 Ok((Comment { raw, max_depth }, end))
282}
283
284/// Scans the comment that starts at `s[start]` (an opening `(`). Returns the
285/// offset just past its closing `)` and the deepest nesting seen.
286pub(crate) fn comment_end(s: &[u8], start: usize, limits: &Limits) -> Result<(usize, u8)> {
287 let limit = u16::from(limits.comment_depth);
288 let mut depth: u16 = 0;
289 let mut max: u16 = 0;
290 let mut i = start;
291 let mut bytes = s.get(start..).unwrap_or(&[]).iter();
292 while let Some(&b) = bytes.next() {
293 match b {
294 b'(' => {
295 depth += 1;
296 if depth > limit {
297 return err(ErrorKind::CommentTooDeep, i);
298 }
299 max = max.max(depth);
300 i += 1;
301 }
302 b')' => {
303 depth -= 1;
304 i += 1;
305 if depth == 0 {
306 // The limit is a u8, so `max` fits.
307 return Ok((i, u8::try_from(max).unwrap_or(u8::MAX)));
308 }
309 }
310 b'\\' => match bytes.next() {
311 None => return err(ErrorKind::UnbalancedComment, s.len()),
312 Some(&e) if class::is_quoted_pair_char(e) => i += 2,
313 Some(_) => return err(ErrorKind::InvalidQuotedPair, i + 1),
314 },
315 _ if class::is_ctext(b) => i += 1,
316 _ => return err(refused(b), i),
317 }
318 }
319 err(ErrorKind::UnbalancedComment, s.len())
320}
321
322/// A validated quoted string: the bytes between the quotes, and whether any
323/// quoted pair occurs.
324///
325/// `Debug` prints the raw bytes as an escaped byte string.
326///
327/// # Examples
328///
329/// ```
330/// use parse_net::text;
331///
332/// let (q, _) = text::parse_quoted(b"\"say \\\"hi\\\"\"")?;
333/// assert!(q.has_escapes() && q.as_plain().is_none());
334/// assert_eq!(q.unescaped_len(), 8);
335/// let mut buf = [0u8; 8];
336/// assert_eq!(q.unescape_into(&mut buf)?, b"say \"hi\"");
337/// assert!(q.eq_bytes(b"say \"hi\""));
338/// let mut short = [0u8; 7];
339/// assert_eq!(q.unescape_into(&mut short).unwrap_err().kind(), parse_net::ErrorKind::BufferTooSmall);
340/// # Ok::<(), parse_net::ParseError>(())
341/// ```
342#[derive(Clone, Copy, PartialEq, Eq, Hash)]
343pub struct Quoted<'a> {
344 raw: &'a [u8],
345 has_escapes: bool,
346}
347
348impl<'a> Quoted<'a> {
349 /// A view over content already validated by another grammar whose
350 /// escapes are `\x` pairs (RFC 9651 strings).
351 pub(crate) const fn from_raw(raw: &'a [u8], has_escapes: bool) -> Self {
352 Self { raw, has_escapes }
353 }
354
355 /// The content between the quotes exactly as sent, quoted pairs included.
356 ///
357 /// # Examples
358 ///
359 /// ```
360 /// use parse_net::text::parse_quoted;
361 ///
362 /// let (q, _) = parse_quoted(b"\"a\\\"b\"")?;
363 /// assert_eq!(q.raw(), b"a\\\"b");
364 /// # Ok::<(), parse_net::ParseError>(())
365 /// ```
366 #[must_use]
367 pub const fn raw(&self) -> &'a [u8] {
368 self.raw
369 }
370
371 /// Whether the content contains a quoted pair (`\x`).
372 ///
373 /// # Examples
374 ///
375 /// ```
376 /// use parse_net::text::parse_quoted;
377 ///
378 /// assert!(parse_quoted(b"\"a\\\"b\"")?.0.has_escapes());
379 /// assert!(!parse_quoted(b"\"ab\"")?.0.has_escapes());
380 /// # Ok::<(), parse_net::ParseError>(())
381 /// ```
382 #[must_use]
383 pub const fn has_escapes(&self) -> bool {
384 self.has_escapes
385 }
386
387 /// The content when it contains no quoted pair: the value, with no copy.
388 ///
389 /// # Examples
390 ///
391 /// ```
392 /// use parse_net::text::parse_quoted;
393 ///
394 /// assert_eq!(parse_quoted(b"\"gzip\"")?.0.as_plain(), Some(&b"gzip"[..]));
395 /// assert_eq!(parse_quoted(b"\"a\\\"b\"")?.0.as_plain(), None);
396 /// # Ok::<(), parse_net::ParseError>(())
397 /// ```
398 #[must_use]
399 pub const fn as_plain(&self) -> Option<&'a [u8]> {
400 if self.has_escapes {
401 None
402 } else {
403 Some(self.raw)
404 }
405 }
406
407 /// The length of the value with quoted pairs replaced.
408 ///
409 /// # Examples
410 ///
411 /// ```
412 /// use parse_net::text::parse_quoted;
413 ///
414 /// let (q, _) = parse_quoted(b"\"a\\\"b\"")?;
415 /// assert_eq!((q.raw().len(), q.unescaped_len()), (4, 3));
416 /// # Ok::<(), parse_net::ParseError>(())
417 /// ```
418 #[must_use]
419 pub fn unescaped_len(&self) -> usize {
420 if self.has_escapes {
421 self.bytes().count()
422 } else {
423 self.raw.len()
424 }
425 }
426
427 /// The bytes of the value with quoted pairs replaced.
428 ///
429 /// # Examples
430 ///
431 /// ```
432 /// use parse_net::text::parse_quoted;
433 ///
434 /// let (q, _) = parse_quoted(b"\"a\\\"b\"")?;
435 /// assert_eq!(q.bytes().collect::<Vec<u8>>(), b"a\"b");
436 /// # Ok::<(), parse_net::ParseError>(())
437 /// ```
438 #[must_use]
439 pub fn bytes(&self) -> Unescape<'a> {
440 Unescape {
441 rest: self.raw.iter(),
442 }
443 }
444
445 /// Writes the value into `out`, every quoted pair replaced by the byte
446 /// after the backslash, and returns the written part.
447 ///
448 /// # Errors
449 ///
450 /// [`ErrorKind::BufferTooSmall`] when `out` is shorter than
451 /// [`unescaped_len`](Self::unescaped_len); nothing is written.
452 ///
453 /// # Examples
454 ///
455 /// ```
456 /// use parse_net::text::parse_quoted;
457 ///
458 /// let (q, _) = parse_quoted(b"\"a\\\"b\"")?;
459 /// let mut buf = [0u8; 8];
460 /// assert_eq!(q.unescape_into(&mut buf)?, b"a\"b");
461 /// # Ok::<(), parse_net::ParseError>(())
462 /// ```
463 pub fn unescape_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b [u8]> {
464 let len = self.unescaped_len();
465 let Some(dst) = out.get_mut(..len) else {
466 return err(ErrorKind::BufferTooSmall, 0);
467 };
468 for (d, b) in dst.iter_mut().zip(self.bytes()) {
469 *d = b;
470 }
471 Ok(dst)
472 }
473
474 /// Whether the value equals `other`, compared without a buffer.
475 ///
476 /// # Examples
477 ///
478 /// ```
479 /// use parse_net::text::parse_quoted;
480 ///
481 /// let (q, _) = parse_quoted(b"\"a\\\"b\"")?;
482 /// assert!(q.eq_bytes(b"a\"b") && !q.eq_bytes(b"a\\\"b"));
483 /// # Ok::<(), parse_net::ParseError>(())
484 /// ```
485 #[must_use]
486 pub fn eq_bytes(&self, other: &[u8]) -> bool {
487 match self.as_plain() {
488 Some(p) => p == other,
489 None => self.bytes().eq(other.iter().copied()),
490 }
491 }
492
493 /// Whether the value equals `other` ignoring ASCII case, compared without
494 /// a buffer.
495 ///
496 /// # Examples
497 ///
498 /// ```
499 /// use parse_net::text::parse_quoted;
500 ///
501 /// let (q, _) = parse_quoted(b"\"UTF\\-8\"")?;
502 /// assert!(q.eq_ignore_ascii_case(b"utf-8") && !q.eq_ignore_ascii_case(b"utf8"));
503 /// # Ok::<(), parse_net::ParseError>(())
504 /// ```
505 #[must_use]
506 pub fn eq_ignore_ascii_case(&self, other: &[u8]) -> bool {
507 match self.as_plain() {
508 Some(p) => p.eq_ignore_ascii_case(other),
509 None => {
510 let mut theirs = other.iter();
511 self.bytes()
512 .all(|b| theirs.next().is_some_and(|o| o.eq_ignore_ascii_case(&b)))
513 && theirs.next().is_none()
514 }
515 }
516 }
517}
518
519impl fmt::Debug for Quoted<'_> {
520 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
521 f.write_str("Quoted(\"")?;
522 for &b in self.raw {
523 for c in core::ascii::escape_default(b) {
524 f.write_str(core::str::from_utf8(&[c]).unwrap_or("?"))?;
525 }
526 }
527 f.write_str("\")")
528 }
529}
530
531/// The unescaped bytes of a [`Quoted`] value.
532///
533/// # Examples
534///
535/// ```
536/// use parse_net::text::parse_quoted;
537///
538/// let (q, _) = parse_quoted(b"\"a\\\"b\"")?;
539/// let mut it = q.bytes();
540/// assert_eq!((it.next(), it.next(), it.next(), it.next()), (Some(b'a'), Some(b'"'), Some(b'b'), None));
541/// # Ok::<(), parse_net::ParseError>(())
542/// ```
543#[derive(Clone, Debug)]
544pub struct Unescape<'a> {
545 rest: core::slice::Iter<'a, u8>,
546}
547
548impl Iterator for Unescape<'_> {
549 type Item = u8;
550
551 #[inline]
552 fn next(&mut self) -> Option<u8> {
553 match self.rest.next() {
554 Some(&b'\\') => self.rest.next().copied(),
555 other => other.copied(),
556 }
557 }
558
559 fn size_hint(&self) -> (usize, Option<usize>) {
560 let n = self.rest.len();
561 (n.div_ceil(2), Some(n))
562 }
563}
564
565/// `token / quoted-string`: the value form of parameters and directives.
566///
567/// A token and a quoted string with the same content compare equal, as
568/// RFC 9110 section 5.6.6 requires: [`eq_bytes`](Self::eq_bytes) and
569/// [`eq_ignore_ascii_case`](Self::eq_ignore_ascii_case) compare unescaped
570/// content. The derived `PartialEq` compares the forms structurally.
571///
572/// # Examples
573///
574/// ```
575/// use parse_net::text;
576///
577/// let (a, _) = text::parse_word(b"utf-8")?;
578/// let (b, _) = text::parse_word(b"\"UTF-8\"")?;
579/// assert!(a.eq_ignore_ascii_case(b"UTF-8") && b.eq_ignore_ascii_case(b"utf-8"));
580/// assert_eq!(b.as_str(), Some("UTF-8"));
581/// # Ok::<(), parse_net::ParseError>(())
582/// ```
583#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
584pub enum Word<'a> {
585 /// A token.
586 Token(&'a [u8]),
587 /// A quoted string.
588 Quoted(Quoted<'a>),
589}
590
591impl<'a> Word<'a> {
592 /// The token, or the quoted content when it has no quoted pair.
593 ///
594 /// # Examples
595 ///
596 /// ```
597 /// use parse_net::text::parse_word;
598 ///
599 /// assert_eq!(parse_word(b"gzip")?.0.as_plain(), Some(&b"gzip"[..]));
600 /// assert_eq!(parse_word(b"\"a\\\"b\"")?.0.as_plain(), None);
601 /// # Ok::<(), parse_net::ParseError>(())
602 /// ```
603 #[must_use]
604 pub const fn as_plain(&self) -> Option<&'a [u8]> {
605 match self {
606 Self::Token(t) => Some(t),
607 Self::Quoted(q) => q.as_plain(),
608 }
609 }
610
611 /// [`as_plain`](Self::as_plain) when it is valid UTF-8.
612 ///
613 /// # Examples
614 ///
615 /// ```
616 /// use parse_net::text::parse_word;
617 ///
618 /// assert_eq!(parse_word(b"\"caf\xc3\xa9\"")?.0.as_str(), Some("caf\u{e9}"));
619 /// assert_eq!(parse_word(b"\"\xff\"")?.0.as_str(), None);
620 /// # Ok::<(), parse_net::ParseError>(())
621 /// ```
622 #[must_use]
623 pub fn as_str(&self) -> Option<&'a str> {
624 self.as_plain().and_then(|p| core::str::from_utf8(p).ok())
625 }
626
627 /// Whether the unescaped value equals `other`.
628 ///
629 /// # Examples
630 ///
631 /// ```
632 /// use parse_net::text::parse_word;
633 ///
634 /// assert!(parse_word(b"\"a b\"")?.0.eq_bytes(b"a b"));
635 /// assert!(parse_word(b"gzip")?.0.eq_bytes(b"gzip") && !parse_word(b"gzip")?.0.eq_bytes(b"GZIP"));
636 /// # Ok::<(), parse_net::ParseError>(())
637 /// ```
638 #[must_use]
639 pub fn eq_bytes(&self, other: &[u8]) -> bool {
640 match self {
641 Self::Token(t) => *t == other,
642 Self::Quoted(q) => q.eq_bytes(other),
643 }
644 }
645
646 /// Whether the unescaped value equals `other` ignoring ASCII case.
647 ///
648 /// # Examples
649 ///
650 /// ```
651 /// use parse_net::text::parse_word;
652 ///
653 /// assert!(parse_word(b"GZIP")?.0.eq_ignore_ascii_case(b"gzip"));
654 /// assert!(parse_word(b"\"Br\"")?.0.eq_ignore_ascii_case(b"br"));
655 /// # Ok::<(), parse_net::ParseError>(())
656 /// ```
657 #[must_use]
658 pub fn eq_ignore_ascii_case(&self, other: &[u8]) -> bool {
659 match self {
660 Self::Token(t) => t.eq_ignore_ascii_case(other),
661 Self::Quoted(q) => q.eq_ignore_ascii_case(other),
662 }
663 }
664
665 /// Writes the unescaped value into `out` and returns the written part.
666 ///
667 /// # Errors
668 ///
669 /// [`ErrorKind::BufferTooSmall`]; nothing is written.
670 ///
671 /// # Examples
672 ///
673 /// ```
674 /// use parse_net::{text::parse_word, ErrorKind};
675 ///
676 /// let (w, _) = parse_word(b"\"a\\\"b\"")?;
677 /// let mut buf = [0u8; 8];
678 /// assert_eq!(w.unescape_into(&mut buf)?, b"a\"b");
679 /// assert_eq!(w.unescape_into(&mut [0u8; 2]).unwrap_err().kind(), ErrorKind::BufferTooSmall);
680 /// # Ok::<(), parse_net::ParseError>(())
681 /// ```
682 pub fn unescape_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b [u8]> {
683 match self {
684 Self::Token(t) => {
685 let Some(dst) = out.get_mut(..t.len()) else {
686 return err(ErrorKind::BufferTooSmall, 0);
687 };
688 dst.copy_from_slice(t);
689 Ok(dst)
690 }
691 Self::Quoted(q) => q.unescape_into(out),
692 }
693 }
694
695 /// The length of the unescaped value.
696 ///
697 /// # Examples
698 ///
699 /// ```
700 /// use parse_net::text::parse_word;
701 ///
702 /// assert_eq!(parse_word(b"\"a\\\"b\"")?.0.len(), 3);
703 /// assert_eq!(parse_word(b"gzip")?.0.len(), 4);
704 /// # Ok::<(), parse_net::ParseError>(())
705 /// ```
706 #[must_use]
707 pub fn len(&self) -> usize {
708 match self {
709 Self::Token(t) => t.len(),
710 Self::Quoted(q) => q.unescaped_len(),
711 }
712 }
713
714 /// Whether two words have equal unescaped values, optionally ignoring
715 /// ASCII case (RFC 9110 section 5.6.6: the token and quoted forms of the
716 /// same value are equal).
717 pub(crate) fn same_value(&self, other: &Word<'_>, ignore_case: bool) -> bool {
718 match other {
719 Word::Token(t) if ignore_case => self.eq_ignore_ascii_case(t),
720 Word::Token(t) => self.eq_bytes(t),
721 Word::Quoted(q) => {
722 let mut theirs = q.bytes();
723 let eq = |a: u8, b: u8| {
724 if ignore_case {
725 a.eq_ignore_ascii_case(&b)
726 } else {
727 a == b
728 }
729 };
730 let all = match self {
731 Word::Token(t) => t.iter().all(|&a| theirs.next().is_some_and(|b| eq(a, b))),
732 Word::Quoted(mine) => mine
733 .bytes()
734 .all(|a| theirs.next().is_some_and(|b| eq(a, b))),
735 };
736 all && theirs.next().is_none()
737 }
738 }
739 }
740
741 /// Whether the unescaped value is empty (only a quoted string can be).
742 ///
743 /// # Examples
744 ///
745 /// ```
746 /// use parse_net::text::parse_word;
747 ///
748 /// assert!(parse_word(b"\"\"")?.0.is_empty());
749 /// assert!(!parse_word(b"x")?.0.is_empty());
750 /// # Ok::<(), parse_net::ParseError>(())
751 /// ```
752 #[must_use]
753 pub fn is_empty(&self) -> bool {
754 match self {
755 Self::Token(t) => t.is_empty(),
756 Self::Quoted(q) => q.raw.is_empty(),
757 }
758 }
759}
760
761/// A validated comment: the text without the outer parentheses, and the
762/// deepest nesting reached.
763///
764/// # Examples
765///
766/// ```
767/// use parse_net::{text::parse_comment, Limits};
768///
769/// let (c, _) = parse_comment(b"(a(b))", &Limits::DEFAULT)?;
770/// assert_eq!((c.raw(), c.max_depth()), (&b"a(b)"[..], 2));
771/// # Ok::<(), parse_net::ParseError>(())
772/// ```
773#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
774pub struct Comment<'a> {
775 raw: &'a [u8],
776 max_depth: u8,
777}
778
779impl<'a> Comment<'a> {
780 /// The text between the outer parentheses, as sent.
781 ///
782 /// # Examples
783 ///
784 /// ```
785 /// use parse_net::{text::parse_comment, Limits};
786 ///
787 /// let (c, _) = parse_comment(b"(gzip; level 9) x", &Limits::DEFAULT)?;
788 /// assert_eq!(c.raw(), b"gzip; level 9");
789 /// # Ok::<(), parse_net::ParseError>(())
790 /// ```
791 #[must_use]
792 pub const fn raw(&self) -> &'a [u8] {
793 self.raw
794 }
795
796 /// The deepest nesting reached: 1 for `(a)`, 2 for `(a(b))`.
797 ///
798 /// # Examples
799 ///
800 /// ```
801 /// use parse_net::{text::parse_comment, Limits};
802 ///
803 /// assert_eq!(parse_comment(b"(a)", &Limits::DEFAULT)?.0.max_depth(), 1);
804 /// assert_eq!(parse_comment(b"(a(b(c)) d)", &Limits::DEFAULT)?.0.max_depth(), 3);
805 /// # Ok::<(), parse_net::ParseError>(())
806 /// ```
807 #[must_use]
808 pub const fn max_depth(&self) -> u8 {
809 self.max_depth
810 }
811}
812
813#[cfg(test)]
814mod tests {
815 use std::{format, vec, vec::Vec};
816
817 use super::*;
818
819 fn q(s: &[u8]) -> Result<(Quoted<'_>, usize)> {
820 parse_quoted(s)
821 }
822
823 #[test]
824 fn test_quoted_edge_cases() {
825 assert_eq!(q(b"\"\"").unwrap().0.raw(), b"");
826 assert!(q(b"\"\\\"\"").unwrap().0.eq_bytes(b"\""));
827 assert!(q(b"\"\\\\\"").unwrap().0.eq_bytes(b"\\"));
828 assert_eq!(q(b"\"\\").unwrap_err().kind(), ErrorKind::UnterminatedQuote);
829 assert_eq!(q(b"\"a").unwrap_err().kind(), ErrorKind::UnterminatedQuote);
830 let e = q(b"\"a\x00\"").unwrap_err();
831 assert_eq!((e.kind(), e.offset()), (ErrorKind::Nul, 2));
832 let e = q(b"\"\\\x7f\"").unwrap_err();
833 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidQuotedPair, 2));
834 assert!(q(b"\"\x80\xff\"").is_ok());
835 assert!(q(b"\"\\\t\"").unwrap().0.eq_bytes(b"\t"));
836 assert_eq!(q(b"x").unwrap_err().kind(), ErrorKind::InvalidByte);
837 assert_eq!(q(b"").unwrap_err().kind(), ErrorKind::InvalidByte);
838 assert_eq!(q(b"\"a\rb\"").unwrap_err().kind(), ErrorKind::BareCr);
839 assert_eq!(q(b"\"a\nb\"").unwrap_err().kind(), ErrorKind::LineFeed);
840 }
841
842 #[test]
843 fn test_quoted_unescape_buffer_one_short_writes_nothing() {
844 let (v, _) = q(b"\"a\\\"b\"").unwrap();
845 let mut buf = [0xaau8; 2];
846 assert_eq!(
847 v.unescape_into(&mut buf).unwrap_err().kind(),
848 ErrorKind::BufferTooSmall
849 );
850 assert_eq!(buf, [0xaa, 0xaa]);
851 let mut buf = [0u8; 3];
852 assert_eq!(v.unescape_into(&mut buf).unwrap(), b"a\"b");
853 }
854
855 #[test]
856 fn test_quoted_many_backslashes_linear() {
857 let mut v = vec![b'"'];
858 v.extend(core::iter::repeat_n(b'\\', 64 * 1024));
859 v.push(b'"');
860 let (quoted, used) = q(&v).unwrap();
861 assert_eq!(used, v.len());
862 assert_eq!(quoted.unescaped_len(), 32 * 1024);
863 }
864
865 #[test]
866 fn test_quoted_case_insensitive_compare() {
867 let (v, _) = q(b"\"A\\Bc\"").unwrap();
868 assert!(v.eq_ignore_ascii_case(b"abc"));
869 assert!(!v.eq_ignore_ascii_case(b"ab"));
870 assert!(!v.eq_ignore_ascii_case(b"abcd"));
871 assert!(!v.eq_bytes(b"abc"));
872 assert_eq!(v.bytes().collect::<Vec<u8>>(), b"ABc");
873 }
874
875 #[test]
876 fn test_word_token_and_quoted_compare_equal() {
877 let (a, _) = parse_word(b"abc").unwrap();
878 let (b, _) = parse_word(b"\"a\\bc\"").unwrap();
879 assert!(a.eq_bytes(b"abc") && b.eq_bytes(b"abc"));
880 assert_ne!(a, b);
881 assert_eq!((a.len(), b.len()), (3, 3));
882 assert_eq!(b.as_str(), None);
883 let (e, _) = parse_word(b"\"\"").unwrap();
884 assert!(e.is_empty() && !a.is_empty());
885 let mut buf = [0u8; 3];
886 assert_eq!(a.unescape_into(&mut buf).unwrap(), b"abc");
887 assert_eq!(
888 a.unescape_into(&mut buf[..2]).unwrap_err().kind(),
889 ErrorKind::BufferTooSmall
890 );
891 assert_eq!(parse_word(b";").unwrap_err().kind(), ErrorKind::Empty);
892 }
893
894 #[test]
895 fn test_trim_ows_never_strips_vt_ff_nbsp() {
896 assert_eq!(trim_ows(b"\x0b a"), b"\x0b a");
897 assert_eq!(trim_ows(b"a\x0c"), b"a\x0c");
898 assert_eq!(trim_ows(b"\xc2\xa0a"), b"\xc2\xa0a");
899 assert_eq!(trim_ows(b""), b"");
900 assert_eq!(skip_ows(b""), 0);
901 }
902
903 #[test]
904 fn test_comment_edge_cases() {
905 let l = Limits::DEFAULT;
906 assert_eq!(parse_comment(b"()", &l).unwrap().0.raw(), b"");
907 assert_eq!(parse_comment(b"(a(b)c)", &l).unwrap().0.max_depth(), 2);
908 assert_eq!(parse_comment(b"(\\))", &l).unwrap().1, 4);
909 assert_eq!(
910 parse_comment(b"((a)", &l).unwrap_err().kind(),
911 ErrorKind::UnbalancedComment
912 );
913 assert_eq!(parse_comment(b"(a))", &l).unwrap().1, 3);
914 assert_eq!(
915 parse_comment(b"x", &l).unwrap_err().kind(),
916 ErrorKind::InvalidByte
917 );
918 assert_eq!(
919 parse_comment(b"(\\", &l).unwrap_err().kind(),
920 ErrorKind::UnbalancedComment
921 );
922 assert_eq!(
923 parse_comment(b"(\\\x00)", &l).unwrap_err().kind(),
924 ErrorKind::InvalidQuotedPair
925 );
926 assert_eq!(
927 parse_comment(b"(a\rb)", &l).unwrap_err().kind(),
928 ErrorKind::BareCr
929 );
930 }
931
932 #[test]
933 fn test_comment_depth_limit_plus_one_refused() {
934 let l = Limits::DEFAULT.max_comment_depth(3);
935 assert!(parse_comment(b"(((x)))", &l).is_ok());
936 let e = parse_comment(b"((((x))))", &l).unwrap_err();
937 assert_eq!((e.kind(), e.offset()), (ErrorKind::CommentTooDeep, 3));
938 let deep: Vec<u8> = core::iter::repeat_n(b'(', 10_000)
939 .chain(core::iter::repeat_n(b')', 10_000))
940 .collect();
941 assert_eq!(
942 parse_comment(&deep, &Limits::DEFAULT).unwrap_err().kind(),
943 ErrorKind::CommentTooDeep
944 );
945 }
946
947 #[test]
948 fn test_quoted_debug_escapes() {
949 let (v, _) = q(b"\"a\\\"\x80\"").unwrap();
950 assert_eq!(format!("{v:?}"), "Quoted(\"a\\\\\\\"\\x80\")");
951 }
952}