Skip to main content

base_message/
field_value.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Helpers for parsing field values, which every protocol otherwise implements again.
5
6use crate::FieldValueError;
7use std::iter::FusedIterator;
8
9/// Split one instance of a list field into its elements, as RFC 9110 section 5.6.1 describes: a comma inside a quoted string does not separate elements, spaces and tabs around each element are removed, and empty elements are skipped.
10///
11/// Comments, the parenthesized text some fields such as `Via` allow (RFC 9110 section 5.6.5), are not recognized, so a comma inside one separates elements. A field type whose values can contain comments should use [`Separate`](crate::combination::Separate) and split its values itself.
12///
13/// ```
14/// use base_message::split_list;
15///
16/// let elements: Result<Vec<&[u8]>, _> = split_list(b"text/html, , application/json;q=\"0,9\"").collect();
17/// assert_eq!(elements?, [&b"text/html"[..], &b"application/json;q=\"0,9\""[..]]);
18/// assert!(split_list(b"a, \"b").any(|element| element.is_err()));
19/// # Ok::<(), base_message::FieldValueError>(())
20/// ```
21///
22/// The elements are produced without allocating. A quoted string that does not end is reported as an error after the elements before it, and then the iterator ends.
23pub fn split_list(value: &[u8]) -> ListElements<'_> {
24    ListElements { rest: Some(value) }
25}
26
27/// The elements of one instance of a list field, as [`split_list`] produces them.
28#[derive(Clone, Debug)]
29pub struct ListElements<'a> {
30    /// The value not yet split, or `None` once the elements are exhausted.
31    rest: Option<&'a [u8]>,
32}
33
34impl<'a> Iterator for ListElements<'a> {
35    type Item = Result<&'a [u8], FieldValueError>;
36
37    fn next(&mut self) -> Option<Self::Item> {
38        while let Some(rest) = self.rest {
39            let (mut quoted, mut escaped, mut separator) = (false, false, None);
40            for (index, &byte) in rest.iter().enumerate() {
41                if escaped {
42                    escaped = false;
43                } else if quoted && byte == b'\\' {
44                    escaped = true;
45                } else if byte == b'"' {
46                    quoted = !quoted;
47                } else if byte == b',' && !quoted {
48                    separator = Some(index);
49                    break;
50                }
51            }
52            let element = match separator {
53                Some(index) => {
54                    self.rest = Some(&rest[index + 1..]);
55                    &rest[..index]
56                }
57                None => {
58                    self.rest = None;
59                    if quoted {
60                        return Some(Err(FieldValueError::new("unterminated quoted string")));
61                    }
62                    rest
63                }
64            };
65            let element = trim_whitespace(element);
66            if !element.is_empty() {
67                return Some(Ok(element));
68            }
69        }
70        None
71    }
72}
73
74impl FusedIterator for ListElements<'_> {}
75
76/// Remove spaces and tabs from both ends, the optional whitespace of RFC 9110 section 5.6.3.
77pub fn trim_whitespace(value: &[u8]) -> &[u8] {
78    let start = value
79        .iter()
80        .position(|byte| !matches!(byte, b' ' | b'\t'))
81        .unwrap_or(value.len());
82    let end = value
83        .iter()
84        .rposition(|byte| !matches!(byte, b' ' | b'\t'))
85        .map_or(start, |index| index + 1);
86    &value[start..end]
87}
88
89/// The value as text, when it is visible ASCII, spaces, and tabs, as most field grammars require.
90///
91/// # Errors
92///
93/// A control character or a byte outside ASCII.
94pub fn ascii_text(value: &[u8]) -> Result<&str, FieldValueError> {
95    let invalid = FieldValueError::new("value is not visible ASCII");
96    let text = std::str::from_utf8(value).map_err(|_| invalid)?;
97    if !text
98        .bytes()
99        .all(|byte| byte.is_ascii_graphic() || byte == b' ' || byte == b'\t')
100    {
101        return Err(invalid);
102    }
103    Ok(text)
104}