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}