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