Skip to main content

parse_net/
list.rs

1//! The `#rule` lists of RFC 9110 section 5.6.1, including several field lines
2//! read as one list (section 5.3).
3//!
4//! A comma ends an element only outside quoted strings and comments, so
5//! `a="x,y", b` is two elements. Elements are yielded trimmed of OWS, as raw
6//! bytes; the grammar of each element (a token, `token;params`, a media
7//! range) is the caller's.
8//!
9//! # Examples
10//!
11//! ```
12//! use parse_net::{self as pn, Limits};
13//!
14//! let l = pn::list(b"a, \"b,c\" , , (d, e) f,")?;
15//! let all: Vec<&[u8]> = l.iter().collect();
16//! assert_eq!(all, [&b"a"[..], b"\"b,c\"", b"(d, e) f"]);
17//!
18//! let lines: [&[u8]; 3] = [b"gzip", b"", b"br, zstd"];
19//! let c = pn::list::combined(lines, &Limits::DEFAULT)?;
20//! assert_eq!(c.iter().collect::<Vec<_>>(), [&b"gzip"[..], b"br", b"zstd"]);
21//! # Ok::<(), pn::ParseError>(())
22//! ```
23
24use crate::{
25    class,
26    error::{ErrorKind, Result, err},
27    limits::Limits,
28    scan::control_kind,
29    text::{self, comment_end, parse_quoted, trim_ows},
30};
31
32/// Counters carried across the lines of one field.
33#[derive(Clone, Copy, Default)]
34struct Budget {
35    members: usize,
36    empties: usize,
37}
38
39/// Whether `value` holds no quoted string and no comment, so a plain comma
40/// search finds every element boundary.
41#[inline]
42fn is_simple(value: &[u8]) -> bool {
43    !value.iter().any(|&b| matches!(b, b'"' | b'(' | b')'))
44}
45
46/// Accounts for the segment `value[start..end]` (an element or an empty
47/// one) and calls `f` for a non-empty element.
48#[inline]
49fn segment<'a>(
50    value: &'a [u8],
51    start: usize,
52    end: usize,
53    limits: &Limits,
54    budget: &mut Budget,
55    f: &mut impl FnMut(&'a [u8]),
56) -> Result<()> {
57    let raw = value.get(start..end).unwrap_or(&[]);
58    let element = trim_ows(raw);
59    if element.is_empty() {
60        // An empty value has no elements, not one empty element.
61        if !(end == value.len() && start == 0) {
62            budget.empties += 1;
63            if budget.empties > usize::from(limits.empty_elements) {
64                return err(ErrorKind::TooManyEmptyElements, end);
65            }
66        }
67    } else {
68        budget.members += 1;
69        if budget.members > usize::from(limits.list_members) {
70            return err(ErrorKind::TooManyMembers, start + text::skip_ows(raw));
71        }
72        f(element);
73    }
74    Ok(())
75}
76
77/// Validates `value` as a list and calls `f` for each non-empty element, in
78/// one pass with one table lookup per ordinary byte. Returns whether the
79/// value was simple (no quoted string, no comment).
80fn walk<'a>(
81    value: &'a [u8],
82    limits: &Limits,
83    budget: &mut Budget,
84    mut f: impl FnMut(&'a [u8]),
85) -> Result<bool> {
86    let mut simple = true;
87    let mut start = 0usize;
88    let mut i = 0usize;
89    loop {
90        let rest = value.get(i..).unwrap_or(&[]);
91        i += rest
92            .iter()
93            .position(|&b| !class::is_list_plain(b))
94            .unwrap_or(rest.len());
95        match value.get(i) {
96            None => {
97                segment(value, start, i, limits, budget, &mut f)?;
98                return Ok(simple);
99            }
100            Some(b',') => {
101                segment(value, start, i, limits, budget, &mut f)?;
102                i += 1;
103                start = i;
104            }
105            Some(b'"') => {
106                simple = false;
107                let rest = value.get(i..).unwrap_or(&[]);
108                let (_, used) = parse_quoted(rest).map_err(|e| e.shifted(i))?;
109                i += used;
110            }
111            Some(b'(') => {
112                simple = false;
113                let (end, _) = comment_end(value, i, limits)?;
114                i = end;
115            }
116            Some(b')') => return err(ErrorKind::UnbalancedComment, i),
117            // Every byte that is not plain and not a delimiter is a control.
118            Some(&b) => return err(control_kind(b), i),
119        }
120    }
121}
122
123/// Parses `value` as a list with the default [`Limits`].
124///
125/// # Errors
126///
127/// As [`parse`].
128///
129/// # Examples
130///
131/// ```
132/// let codings: Vec<&[u8]> = parse_net::list(b"gzip, deflate;q=0.5")?.iter().collect();
133/// assert_eq!(codings, [&b"gzip"[..], b"deflate;q=0.5"]);
134/// # Ok::<(), parse_net::ParseError>(())
135/// ```
136pub fn list(value: &[u8]) -> Result<List<'_>> {
137    parse(value, &Limits::DEFAULT)
138}
139
140/// Parses `value` as a list.
141///
142/// The whole value is validated: every byte as a field value (RFC 9110
143/// section 5.5), quoted strings terminated with valid quoted pairs, comments
144/// balanced within the depth limit. Empty elements are skipped, up to
145/// [`Limits::max_empty_elements`] (RFC 9110 section 5.6.1.2).
146///
147/// # Errors
148///
149/// [`ErrorKind::BareCr`], [`ErrorKind::LineFeed`], [`ErrorKind::Nul`],
150/// [`ErrorKind::Control`]; [`ErrorKind::UnterminatedQuote`],
151/// [`ErrorKind::InvalidQuotedPair`]; [`ErrorKind::UnbalancedComment`],
152/// [`ErrorKind::CommentTooDeep`]; [`ErrorKind::TooManyEmptyElements`] at the
153/// separator that exceeded the budget; [`ErrorKind::TooManyMembers`] at the
154/// element beyond [`Limits::max_list_members`].
155///
156/// # Examples
157///
158/// ```
159/// use parse_net::{list, ErrorKind, Limits};
160///
161/// let l = list::parse(b"a, b", &Limits::DEFAULT)?;
162/// assert_eq!(l.len(), 2);
163/// assert_eq!(list::parse(b"a, \"b", &Limits::DEFAULT).unwrap_err().kind(), ErrorKind::UnterminatedQuote);
164/// # Ok::<(), parse_net::ParseError>(())
165/// ```
166pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<List<'a>> {
167    let mut budget = Budget::default();
168    let simple = walk(value, limits, &mut budget, |_| {})?;
169    Ok(List {
170        value,
171        // The member limit is a u16, so the count fits.
172        count: u32::try_from(budget.members).unwrap_or(u32::MAX),
173        simple,
174    })
175}
176
177/// Validates `value` and calls `f` for each element in the same pass, then
178/// returns the element count.
179///
180/// On error, `f` has seen the elements before the failure.
181///
182/// # Errors
183///
184/// As [`parse`].
185///
186/// # Examples
187///
188/// ```
189/// use parse_net::{list, Limits};
190///
191/// let mut longest = 0;
192/// let n = list::for_each(b"gzip, br, zstd", &Limits::DEFAULT, |e| longest = longest.max(e.len()))?;
193/// assert_eq!((n, longest), (3, 4));
194/// # Ok::<(), parse_net::ParseError>(())
195/// ```
196pub fn for_each<'a>(value: &'a [u8], limits: &Limits, f: impl FnMut(&'a [u8])) -> Result<usize> {
197    let mut budget = Budget::default();
198    let _simple = walk(value, limits, &mut budget, f)?;
199    Ok(budget.members)
200}
201
202/// Validates several field lines of one name as one list (RFC 9110 section
203/// 5.3), as if joined by `", "`, without joining them.
204///
205/// An empty line adds nothing. An element never spans two lines, so a quoted
206/// string still open at the end of a line is refused. The empty-element and
207/// member budgets count across all lines.
208///
209/// # Errors
210///
211/// As [`parse`], with [`ParseError::line`](crate::ParseError::line) set to the
212/// failing line.
213///
214/// # Examples
215///
216/// ```
217/// use parse_net::{list, Limits};
218///
219/// let lines: [&[u8]; 2] = [b"a, \"x", b"y\""];
220/// assert!(list::combined(lines, &Limits::DEFAULT).is_err());
221/// ```
222pub fn combined<'a, I>(lines: I, limits: &Limits) -> Result<CombinedList<'a, I::IntoIter>>
223where
224    I: IntoIterator<Item = &'a [u8]>,
225    I::IntoIter: Clone,
226{
227    let lines = lines.into_iter();
228    let mut budget = Budget::default();
229    for (n, line) in lines.clone().enumerate() {
230        let _simple = walk(line, limits, &mut budget, |_| {}).map_err(|e| e.on_line(n))?;
231    }
232    Ok(CombinedList {
233        lines,
234        count: budget.members,
235        _value: core::marker::PhantomData,
236    })
237}
238
239/// Whether any element of any line equals `token`, ignoring ASCII case.
240///
241/// For `Connection: keep-alive, Upgrade` and `Upgrade: websocket`. A line
242/// that fails to parse counts as not containing the token (fail closed).
243///
244/// # Examples
245///
246/// ```
247/// use parse_net::list::contains_token;
248///
249/// assert!(contains_token([&b"keep-alive, Upgrade"[..]], b"upgrade"));
250/// assert!(!contains_token([&b"keep-alive, \"upgrade"[..]], b"upgrade"));
251/// ```
252pub fn contains_token<'a, I>(lines: I, token: &[u8]) -> bool
253where
254    I: IntoIterator<Item = &'a [u8]>,
255{
256    lines.into_iter().any(|line| {
257        parse(line, &Limits::DEFAULT).is_ok_and(|l| l.iter().any(|e| e.eq_ignore_ascii_case(token)))
258    })
259}
260
261/// A validated list. Iterating it cannot fail.
262#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
263pub struct List<'a> {
264    value: &'a [u8],
265    count: u32,
266    simple: bool,
267}
268
269impl<'a> List<'a> {
270    /// The elements, in order, each trimmed of OWS.
271    #[must_use]
272    pub fn iter(&self) -> Elements<'a> {
273        Elements {
274            rest: self.value,
275            remaining: self.count as usize,
276            simple: self.simple,
277        }
278    }
279
280    /// The number of elements.
281    #[must_use]
282    pub const fn len(&self) -> usize {
283        self.count as usize
284    }
285
286    /// Whether the list has no elements.
287    #[must_use]
288    pub const fn is_empty(&self) -> bool {
289        self.count == 0
290    }
291}
292
293impl<'a> IntoIterator for List<'a> {
294    type Item = &'a [u8];
295    type IntoIter = Elements<'a>;
296
297    fn into_iter(self) -> Elements<'a> {
298        self.iter()
299    }
300}
301
302/// The index of the first comma of `s` outside quoted strings and comments.
303/// `s` has been validated, so quotes and comments are well formed.
304fn top_level_comma(s: &[u8]) -> Option<usize> {
305    let mut i = 0usize;
306    let mut depth = 0u32;
307    let mut quoted = false;
308    while let Some(&b) = s.get(i) {
309        match b {
310            b'\\' if quoted || depth > 0 => i += 1,
311            b'"' if depth == 0 => quoted = !quoted,
312            b'(' if !quoted => depth += 1,
313            b')' if !quoted => depth = depth.saturating_sub(1),
314            b',' if !quoted && depth == 0 => return Some(i),
315            _ => {}
316        }
317        i += 1;
318    }
319    None
320}
321
322/// The elements of a validated list.
323#[derive(Clone, Debug)]
324pub struct Elements<'a> {
325    rest: &'a [u8],
326    remaining: usize,
327    simple: bool,
328}
329
330impl<'a> Iterator for Elements<'a> {
331    type Item = &'a [u8];
332
333    fn next(&mut self) -> Option<&'a [u8]> {
334        while self.remaining > 0 && !self.rest.is_empty() {
335            let end = if self.simple {
336                self.rest.iter().position(|&b| b == b',')
337            } else {
338                top_level_comma(self.rest)
339            }
340            .unwrap_or(self.rest.len());
341            let element = trim_ows(self.rest.get(..end).unwrap_or(&[]));
342            self.rest = self.rest.get(end + 1..).unwrap_or(&[]);
343            if !element.is_empty() {
344                self.remaining -= 1;
345                return Some(element);
346            }
347        }
348        None
349    }
350
351    fn size_hint(&self) -> (usize, Option<usize>) {
352        (self.remaining, Some(self.remaining))
353    }
354}
355
356impl ExactSizeIterator for Elements<'_> {}
357
358/// Several validated field lines read as one list.
359#[derive(Clone, Debug)]
360pub struct CombinedList<'a, I> {
361    lines: I,
362    count: usize,
363    _value: core::marker::PhantomData<&'a [u8]>,
364}
365
366impl<'a, I: Iterator<Item = &'a [u8]> + Clone> CombinedList<'a, I> {
367    /// The elements of every line, in order.
368    #[must_use]
369    pub fn iter(&self) -> CombinedElements<'a, I> {
370        CombinedElements {
371            lines: self.lines.clone(),
372            current: Elements {
373                rest: &[],
374                remaining: 0,
375                simple: true,
376            },
377            remaining: self.count,
378        }
379    }
380
381    /// The number of elements across all lines.
382    #[must_use]
383    pub const fn len(&self) -> usize {
384        self.count
385    }
386
387    /// Whether no line has an element.
388    #[must_use]
389    pub const fn is_empty(&self) -> bool {
390        self.count == 0
391    }
392}
393
394/// The elements of a [`CombinedList`].
395#[derive(Clone, Debug)]
396pub struct CombinedElements<'a, I> {
397    lines: I,
398    current: Elements<'a>,
399    remaining: usize,
400}
401
402impl<'a, I: Iterator<Item = &'a [u8]>> Iterator for CombinedElements<'a, I> {
403    type Item = &'a [u8];
404
405    fn next(&mut self) -> Option<&'a [u8]> {
406        while self.remaining > 0 {
407            if let Some(e) = self.current.next() {
408                self.remaining -= 1;
409                return Some(e);
410            }
411            let line = self.lines.next()?;
412            // The line was validated with the same rules; `usize::MAX` lets
413            // the walker run to the line's end.
414            self.current = Elements {
415                rest: line,
416                remaining: usize::MAX,
417                simple: is_simple(line),
418            };
419        }
420        None
421    }
422
423    fn size_hint(&self) -> (usize, Option<usize>) {
424        (self.remaining, Some(self.remaining))
425    }
426}
427
428impl<'a, I: Iterator<Item = &'a [u8]>> ExactSizeIterator for CombinedElements<'a, I> {}
429
430#[cfg(test)]
431mod tests {
432    use std::{vec, vec::Vec};
433
434    use super::*;
435
436    fn all(v: &[u8]) -> Vec<&[u8]> {
437        list(v).unwrap().iter().collect()
438    }
439
440    #[test]
441    fn test_list_edge_cases() {
442        assert!(all(b"").is_empty());
443        assert!(all(b"   ").is_empty());
444        assert!(all(b",").is_empty());
445        assert!(all(b", ,").is_empty());
446        assert_eq!(all(b"a,"), [b"a"]);
447        assert_eq!(all(b",a"), [b"a"]);
448        assert_eq!(all(b"a,,b"), [&b"a"[..], b"b"]);
449        assert_eq!(all(b"a, \"b,c\""), [&b"a"[..], b"\"b,c\""]);
450        assert_eq!(all(b"a, (b,c)"), [&b"a"[..], b"(b,c)"]);
451        assert_eq!(all(b"a;q=1 , b"), [&b"a;q=1"[..], b"b"]);
452        assert_eq!(all(b"a=\"x\\\",y\", b"), [&b"a=\"x\\\",y\""[..], b"b"]);
453        assert_eq!(all(b"(a\\)b,c), d"), [&b"(a\\)b,c)"[..], b"d"]);
454    }
455
456    #[test]
457    fn test_list_comma_in_quotes_is_one_element() {
458        assert_eq!(all(b"filename=\"a,b.txt\""), [&b"filename=\"a,b.txt\""[..]]);
459    }
460
461    #[test]
462    fn test_list_empty_element_budget() {
463        // "a" and "b" around 17 commas leave 16 empty elements; 18 leave 17.
464        let mut sixteen = vec![b'a'];
465        sixteen.extend([b','; 17]);
466        sixteen.push(b'b');
467        assert_eq!(all(&sixteen), [&b"a"[..], b"b"]);
468        let mut seventeen = vec![b'a'];
469        seventeen.extend([b','; 18]);
470        seventeen.push(b'b');
471        let e = list(&seventeen).unwrap_err();
472        assert_eq!(
473            (e.kind(), e.offset()),
474            (ErrorKind::TooManyEmptyElements, 18)
475        );
476    }
477
478    #[test]
479    fn test_list_member_budget() {
480        let mut v = Vec::new();
481        for i in 0..1025 {
482            if i > 0 {
483                v.extend_from_slice(b", ");
484            }
485            v.push(b'a');
486        }
487        assert_eq!(list(&v[..v.len() - 3]).unwrap().len(), 1024);
488        assert_eq!(list(&v).unwrap_err().kind(), ErrorKind::TooManyMembers);
489        let e = parse(b"a, b, c", &Limits::DEFAULT.max_list_members(2)).unwrap_err();
490        assert_eq!(e.offset(), 6);
491    }
492
493    #[test]
494    fn test_list_refuses_bad_bytes_and_structure() {
495        assert_eq!(list(b"a\rb").unwrap_err().kind(), ErrorKind::BareCr);
496        assert_eq!(
497            list(b"a, \"b").unwrap_err().kind(),
498            ErrorKind::UnterminatedQuote
499        );
500        assert_eq!(
501            list(b"a)").unwrap_err().kind(),
502            ErrorKind::UnbalancedComment
503        );
504        assert_eq!(
505            list(b"(a").unwrap_err().kind(),
506            ErrorKind::UnbalancedComment
507        );
508        let e = list(b"ab, \"c\x01\"").unwrap_err();
509        assert_eq!((e.kind(), e.offset()), (ErrorKind::Control, 6));
510    }
511
512    #[test]
513    fn test_combined_lines() {
514        let lines: [&[u8]; 3] = [b"a, b", b"", b"c"];
515        let c = combined(lines, &Limits::DEFAULT).unwrap();
516        assert_eq!(c.len(), 3);
517        assert_eq!(c.iter().collect::<Vec<_>>(), [&b"a"[..], b"b", b"c"]);
518        assert_eq!(c.iter().len(), 3);
519        let bad: [&[u8]; 2] = [b"a", b"\"open"];
520        let e = combined(bad, &Limits::DEFAULT).unwrap_err();
521        assert_eq!((e.kind(), e.line()), (ErrorKind::UnterminatedQuote, 1));
522        let empty: [&[u8]; 0] = [];
523        assert!(combined(empty, &Limits::DEFAULT).unwrap().is_empty());
524    }
525
526    #[test]
527    fn test_combined_budgets_count_across_lines() {
528        let l = Limits::DEFAULT.max_list_members(2);
529        let lines: [&[u8]; 2] = [b"a", b"b, c"];
530        let e = combined(lines, &l).unwrap_err();
531        assert_eq!(
532            (e.kind(), e.line(), e.offset()),
533            (ErrorKind::TooManyMembers, 1, 3)
534        );
535    }
536
537    #[test]
538    fn test_combined_equals_joined_at_every_comma() {
539        let value = b"a, \"b,c\", (d,e), f;g=\"h\", i";
540        let whole = all(value);
541        let mut commas = Vec::new();
542        let mut from = 0;
543        while let Some(i) = top_level_comma(&value[from..]) {
544            commas.push(from + i);
545            from += i + 1;
546        }
547        assert_eq!(commas.len(), 4);
548        for &split in &commas {
549            let lines: [&[u8]; 2] = [&value[..split], &value[split + 1..]];
550            let c = combined(lines, &Limits::DEFAULT).unwrap();
551            assert_eq!(c.iter().collect::<Vec<_>>(), whole, "split at {split}");
552        }
553    }
554
555    #[test]
556    fn test_contains_token_fails_closed() {
557        assert!(contains_token(
558            [&b"close"[..], b"keep-alive, Upgrade"],
559            b"upgrade"
560        ));
561        assert!(!contains_token([&b"upgrade\r"[..]], b"upgrade"));
562        let none: [&[u8]; 0] = [];
563        assert!(!contains_token(none, b"x"));
564    }
565
566    #[test]
567    fn test_for_each_sees_every_element() {
568        let mut seen = vec![];
569        let n = for_each(b"a, b ,c", &Limits::DEFAULT, |e| seen.push(e)).unwrap();
570        assert_eq!((n, seen), (3, vec![&b"a"[..], b"b", b"c"]));
571    }
572}