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