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}