Skip to main content

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}