sipx_sip/headers/
grammar.rs1use std::ops::Range;
9
10use crate::error::HeaderError;
11
12#[must_use]
14pub(crate) fn is_token_char(b: u8) -> bool {
15 b.is_ascii_alphanumeric()
16 || matches!(
17 b,
18 b'-' | b'.' | b'!' | b'%' | b'*' | b'_' | b'+' | b'`' | b'\'' | b'~'
19 )
20}
21
22#[must_use]
24pub(crate) fn skip_ws(input: &[u8], mut at: usize) -> usize {
25 while matches!(input.get(at), Some(b' ' | b'\t')) {
26 at += 1;
27 }
28 at
29}
30
31#[must_use]
33pub(crate) fn trim(mut b: &[u8]) -> &[u8] {
34 while let Some((f, rest)) = b.split_first() {
35 if matches!(f, b' ' | b'\t') {
36 b = rest;
37 } else {
38 break;
39 }
40 }
41 while let Some((l, rest)) = b.split_last() {
42 if matches!(l, b' ' | b'\t') {
43 b = rest;
44 } else {
45 break;
46 }
47 }
48 b
49}
50
51#[must_use]
56pub(crate) fn quoted_string_end(input: &[u8], at: usize) -> Option<usize> {
57 if input.get(at) != Some(&b'"') {
58 return None;
59 }
60 let mut i = at + 1;
61 while let Some(&b) = input.get(i) {
62 match b {
63 b'\\' if i + 1 < input.len() => i += 2,
65 b'"' => return Some(i + 1),
66 _ => i += 1,
67 }
68 }
69 None
70}
71
72pub(crate) fn split_list<'a>(
78 value: &'a [u8],
79 header: &'static str,
80) -> Result<Vec<&'a [u8]>, HeaderError> {
81 split_list_spans(value, header).map(|spans| {
82 spans
83 .into_iter()
84 .map(|span| value.get(span).unwrap_or(&[]))
85 .collect()
86 })
87}
88
89pub(crate) fn split_list_spans(
94 value: &[u8],
95 header: &'static str,
96) -> Result<Vec<Range<usize>>, HeaderError> {
97 let mut parts = Vec::new();
98 let mut start = 0usize;
99 let mut i = 0usize;
100 let mut angle = 0usize;
101 let mut paren = 0usize;
102
103 while i < value.len() {
104 match value.get(i) {
105 Some(b'"') => {
106 i = quoted_string_end(value, i)
107 .ok_or(HeaderError::UnterminatedQuotedString { header })?;
108 }
109 Some(b'<') => {
110 angle += 1;
111 i += 1;
112 }
113 Some(b'>') => {
114 angle = angle.saturating_sub(1);
115 i += 1;
116 }
117 Some(b'(') => {
118 paren += 1;
119 i += 1;
120 }
121 Some(b')') => {
122 paren = paren.saturating_sub(1);
123 i += 1;
124 }
125 Some(b',') if angle == 0 && paren == 0 => {
126 parts.push(start..i);
127 i += 1;
128 start = i;
129 }
130 Some(_) => i += 1,
131 None => break,
132 }
133 }
134 parts.push(start..value.len());
135 Ok(parts)
136}
137
138#[must_use]
141pub(crate) fn find_param_start(value: &[u8]) -> Option<usize> {
142 let mut i = 0usize;
143 let mut angle = 0usize;
144 while i < value.len() {
145 match value.get(i) {
146 Some(b'"') => i = quoted_string_end(value, i)?,
147 Some(b'<') => {
148 angle += 1;
149 i += 1;
150 }
151 Some(b'>') => {
152 angle = angle.saturating_sub(1);
153 i += 1;
154 }
155 Some(b';') if angle == 0 => return Some(i),
156 Some(_) => i += 1,
157 None => break,
158 }
159 }
160 None
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct HeaderParam {
166 pub name: Vec<u8>,
168 pub value: Option<Vec<u8>>,
170}
171
172impl HeaderParam {
173 #[must_use]
175 pub fn is(&self, name: &str) -> bool {
176 self.name == name.as_bytes()
177 }
178}
179
180pub(crate) fn parse_params(
186 tail: &[u8],
187 header: &'static str,
188) -> Result<Vec<HeaderParam>, HeaderError> {
189 let mut params = Vec::new();
190 let mut i = 0usize;
191
192 while i < tail.len() {
193 i = skip_ws(tail, i);
195 if tail.get(i) != Some(&b';') {
196 return Err(HeaderError::Syntax { header });
197 }
198 i = skip_ws(tail, i + 1);
199
200 let name_start = i;
201 while tail.get(i).is_some_and(|&b| is_token_char(b)) {
202 i += 1;
203 }
204 let name = tail.get(name_start..i).unwrap_or(&[]);
205 if name.is_empty() {
206 return Err(HeaderError::Syntax { header });
207 }
208
209 i = skip_ws(tail, i);
210 let value = if tail.get(i) == Some(&b'=') {
211 i = skip_ws(tail, i + 1);
212 if tail.get(i) == Some(&b'"') {
213 let end = quoted_string_end(tail, i)
214 .ok_or(HeaderError::UnterminatedQuotedString { header })?;
215 let raw = tail.get(i + 1..end.saturating_sub(1)).unwrap_or(&[]);
216 let unescaped = unescape_quoted(raw);
217 i = end;
218 Some(unescaped)
219 } else {
220 let start = i;
221 while tail
224 .get(i)
225 .is_some_and(|&b| is_token_char(b) || matches!(b, b'[' | b']' | b':' | b'/'))
226 {
227 i += 1;
228 }
229 if i == start {
230 return Err(HeaderError::Syntax { header });
231 }
232 Some(tail.get(start..i).unwrap_or(&[]).to_vec())
233 }
234 } else {
235 None
236 };
237
238 params.push(HeaderParam {
239 name: name.to_ascii_lowercase(),
240 value,
241 });
242 i = skip_ws(tail, i);
243 }
244
245 Ok(params)
246}
247
248#[must_use]
250fn unescape_quoted(raw: &[u8]) -> Vec<u8> {
251 let mut out = Vec::with_capacity(raw.len());
252 let mut i = 0usize;
253 while let Some(&b) = raw.get(i) {
254 if b == b'\\'
255 && let Some(&next) = raw.get(i + 1)
256 {
257 out.push(next);
258 i += 2;
259 continue;
260 }
261 out.push(b);
262 i += 1;
263 }
264 out
265}
266
267#[must_use]
269pub(crate) fn param<'a>(params: &'a [HeaderParam], name: &str) -> Option<&'a HeaderParam> {
270 params.iter().find(|p| p.is(name))
271}
272
273pub(crate) fn parse_u64(value: &[u8], header: &'static str) -> Result<u64, HeaderError> {
279 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
280 return Err(HeaderError::Syntax { header });
281 }
282 let mut n: u64 = 0;
283 for &b in value {
284 n = n
285 .checked_mul(10)
286 .and_then(|n| n.checked_add(u64::from(b - b'0')))
287 .ok_or(HeaderError::OutOfRange { header })?;
288 }
289 Ok(n)
290}
291
292#[cfg(test)]
293#[allow(
294 clippy::unwrap_used,
295 clippy::expect_used,
296 clippy::panic,
297 clippy::indexing_slicing
298)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn list_splitting_respects_quotes_and_brackets() {
304 let parts = split_list(br#""Bell, Alexander" <sip:a@b>, <sip:c@d>"#, "From").unwrap();
306 assert_eq!(parts.len(), 2);
307 assert_eq!(trim(parts[0]), br#""Bell, Alexander" <sip:a@b>"#);
308 assert_eq!(trim(parts[1]), b"<sip:c@d>");
309 }
310
311 #[test]
312 fn list_splitting_reports_an_unterminated_quote() {
313 let err = split_list(br#""Mr. J. User <sip:j@example.com>"#, "To").unwrap_err();
315 assert!(matches!(
316 err,
317 HeaderError::UnterminatedQuotedString { header: "To" }
318 ));
319 }
320
321 #[test]
322 fn parameters_reject_empty_segments() {
323 assert!(parse_params(b";;", "Via").is_err());
325 assert!(parse_params(b";a=1;;", "Via").is_err());
326 assert!(parse_params(b";", "Via").is_err());
327 }
328
329 #[test]
330 fn parameters_parse_flags_quoted_and_bare_values() {
331 let params = parse_params(br#";lr;tag=abc;text="a;b\"c""#, "To").unwrap();
332 assert_eq!(params.len(), 3);
333 assert!(params[0].is("lr") && params[0].value.is_none());
334 assert_eq!(params[1].value.as_deref(), Some(&b"abc"[..]));
335 assert_eq!(params[2].value.as_deref(), Some(&br#"a;b"c"#[..]));
337 }
338
339 #[test]
340 fn parameter_names_compare_case_insensitively() {
341 let params = parse_params(b";Transport=TCP", "Via").unwrap();
342 assert!(param(¶ms, "transport").is_some());
343 }
344
345 #[test]
346 fn parameter_values_may_be_hosts_and_ipv6_references() {
347 let params = parse_params(b";received=192.0.2.1;maddr=[2001:db8::1]", "Via").unwrap();
348 assert_eq!(
349 param(¶ms, "received").and_then(|p| p.value.as_deref()),
350 Some(&b"192.0.2.1"[..])
351 );
352 assert_eq!(
353 param(¶ms, "maddr").and_then(|p| p.value.as_deref()),
354 Some(&b"[2001:db8::1]"[..])
355 );
356 }
357
358 #[test]
359 fn numbers_keep_leading_zeros_and_reject_signs() {
360 assert_eq!(parse_u64(b"0068", "Max-Forwards").unwrap(), 68);
362 assert!(parse_u64(b"-1", "Max-Forwards").is_err());
363 assert!(parse_u64(b"+1", "Max-Forwards").is_err());
364 assert!(parse_u64(b"", "Max-Forwards").is_err());
365 assert!(parse_u64(b"12x", "Max-Forwards").is_err());
366 assert!(matches!(
368 parse_u64(b"99999999999999999999999", "CSeq"),
369 Err(HeaderError::OutOfRange { .. })
370 ));
371 }
372}