Skip to main content

sip_header/
lib.rs

1//! SIP header field parsers for standard RFC types.
2//!
3//! This crate provides parsers for SIP header values as defined in RFC 3261
4//! and extensions. It sits between URI parsing ([`sip_uri`]) and full SIP
5//! stacks, handling the header-level grammar: display names, header parameters,
6//! and structured header values.
7//!
8//! # Modules
9//!
10//! - [`header_addr`] — RFC 3261 `name-addr` with header-level parameters
11//! - [`header`] — SIP header name catalog and [`SipHeaderLookup`] trait
12//! - [`call_id`] — RFC 3261 Call-ID value
13//! - [`message`] — Extract headers, Request-URI and body from raw SIP message text (feature: `message`)
14//! - [`via`] — RFC 3261 Via header parser
15//! - [`warning`] — RFC 3261 Warning header parser
16//! - [`auth`] — SIP authentication value parser (Authorization, WWW-Authenticate, etc.)
17//! - [`contact`] — RFC 3261 Contact header parser
18//! - [`accept`] — RFC 3261 Accept header parser
19//! - [`accept_encoding`] — RFC 3261 Accept-Encoding header parser
20//! - [`accept_language`] — RFC 3261 Accept-Language header parser
21//! - [`security`] — RFC 3329 Security mechanism parser
22//! - [`uri_info`] — `<absoluteURI> *(SEMI generic-param)` parser (Call-Info, Alert-Info, Error-Info)
23//! - [`history_info`] — RFC 7044 History-Info header parser
24//! - [`geolocation`] — RFC 6442 Geolocation header parser
25//! - [`replaces`] — RFC 3891 Replaces / RFC 3911 Join header parser
26//! - [`target_dialog`] — RFC 4538 Target-Dialog header parser
27//! - `conference_info` — RFC 4575 conference event package (feature: `conference-info`)
28
29#[macro_use]
30mod macros;
31
32pub use sip_uri;
33
34pub mod accept;
35pub mod accept_encoding;
36pub mod accept_language;
37pub mod auth;
38pub mod call_id;
39#[cfg(feature = "conference-info")]
40pub mod conference_info;
41pub mod contact;
42pub mod geolocation;
43pub mod header;
44pub mod header_addr;
45pub mod history_info;
46#[cfg(feature = "message")]
47pub mod message;
48pub mod replaces;
49pub mod security;
50pub mod target_dialog;
51pub mod uri_info;
52pub mod via;
53pub mod warning;
54
55pub use accept::{SipAccept, SipAcceptEntry, SipAcceptError};
56pub use accept_encoding::{SipAcceptEncoding, SipAcceptEncodingEntry, SipAcceptEncodingError};
57pub use accept_language::{SipAcceptLanguage, SipAcceptLanguageEntry, SipAcceptLanguageError};
58pub use auth::{SipAuthError, SipAuthValue};
59pub use call_id::{SipCallId, SipCallIdError};
60pub use contact::ContactValue;
61pub use geolocation::{SipGeolocation, SipGeolocationRef};
62pub use header::{ParseSipHeaderError, SipHeader, SipHeaderLookup};
63pub use header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
64pub use history_info::{HistoryInfo, HistoryInfoEntry, HistoryInfoError, HistoryInfoReason};
65#[cfg(feature = "message")]
66pub use message::{extract_all_headers, extract_body, extract_header, extract_request_uri};
67pub use replaces::{SipReplaces, SipReplacesError};
68pub use security::{SipSecurity, SipSecurityError, SipSecurityMechanism};
69pub use target_dialog::{SipTargetDialog, SipTargetDialogError};
70pub use uri_info::{UriInfo, UriInfoEntry, UriInfoError};
71pub use via::{SipVia, SipViaEntry, SipViaError};
72pub use warning::{SipWarning, SipWarningEntry, SipWarningError};
73
74/// Format a slice of displayable items as a separated list.
75pub(crate) fn fmt_joined<T: std::fmt::Display>(
76    f: &mut std::fmt::Formatter<'_>,
77    items: &[T],
78    separator: &str,
79) -> std::fmt::Result {
80    for (i, item) in items
81        .iter()
82        .enumerate()
83    {
84        if i > 0 {
85            f.write_str(separator)?;
86        }
87        write!(f, "{item}")?;
88    }
89    Ok(())
90}
91
92/// Unescape RFC 3261 §25.1 `quoted-pair` sequences: `\"` → `"`, `\\` → `\`.
93///
94/// Operates on the content *between* surrounding double-quotes (caller strips
95/// them). Skips allocation when no backslash escapes are present.
96pub(crate) fn unescape_quoted_pair(s: &str) -> String {
97    if !s.contains('\\') {
98        return s.to_string();
99    }
100    let mut result = String::with_capacity(s.len());
101    let mut escaped = false;
102    for ch in s.chars() {
103        if escaped {
104            result.push(ch);
105            escaped = false;
106        } else if ch == '\\' {
107            escaped = true;
108        } else {
109            result.push(ch);
110        }
111    }
112    result
113}
114
115/// A control character `qdtext` excludes but `quoted-pair` carries
116/// (RFC 3261 §25.1); CR and LF fit neither.
117pub(crate) fn is_quoted_pair_only(c: char) -> bool {
118    c.is_ascii_control() && !matches!(c, '\t' | '\r' | '\n')
119}
120
121/// Write a `quoted-string`: surrounds with `"` and emits `"`, `\` and
122/// [`is_quoted_pair_only`] characters as `quoted-pair` (RFC 3261 §25.1).
123pub(crate) fn write_quoted_pair<W: std::fmt::Write + ?Sized>(
124    f: &mut W,
125    value: &str,
126) -> std::fmt::Result {
127    f.write_char('"')?;
128    for ch in value.chars() {
129        if ch == '"' || ch == '\\' || is_quoted_pair_only(ch) {
130            f.write_char('\\')?;
131        }
132        f.write_char(ch)?;
133    }
134    f.write_char('"')
135}
136
137/// Byte index of the first `"` not escaped by `quoted-pair`, scanning text
138/// that follows an opening quote.
139fn closing_quote(s: &str) -> Option<usize> {
140    let bytes = s.as_bytes();
141    let mut i = 0;
142    while i < bytes.len() {
143        match bytes[i] {
144            b'\\' => i += 2,
145            b'"' => return Some(i),
146            _ => i += 1,
147        }
148    }
149    None
150}
151
152/// One `generic-param` (RFC 3261 §25.1) as it appeared on the wire.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub(crate) struct RawParam<'a> {
155    /// Name, SWS-trimmed, case preserved.
156    pub(crate) key: &'a str,
157    /// Value, SWS-trimmed, quotes and escapes intact; `None` for a flag.
158    pub(crate) value: Option<&'a str>,
159}
160
161impl RawParam<'_> {
162    /// The value without its surrounding quotes and with `quoted-pair`
163    /// unescaped, plus whether it was quoted.
164    pub(crate) fn unquoted(&self) -> Option<(String, bool)> {
165        let v = self.value?;
166        Some(if v.len() >= 2 && v.starts_with('"') && v.ends_with('"') {
167            (unescape_quoted_pair(&v[1..v.len() - 1]), true)
168        } else {
169            (v.to_string(), false)
170        })
171    }
172}
173
174/// Read `*(SEMI generic-param)`, with or without the leading `;`.
175///
176/// A value opening with `"` runs to its closing quote, so a `;` inside it does
177/// not split; a quote that never closes ends at the next `;` like any value.
178pub(crate) fn parse_params(s: &str) -> Vec<RawParam<'_>> {
179    let mut params = Vec::new();
180    let mut rest = s;
181    loop {
182        rest = rest.trim_start_matches(|c: char| c == ';' || c.is_ascii_whitespace());
183        if rest.is_empty() {
184            return params;
185        }
186        let segment_end = rest
187            .find(';')
188            .unwrap_or(rest.len());
189        let Some(eq) = rest[..segment_end].find('=') else {
190            params.push(RawParam {
191                key: rest[..segment_end].trim_end(),
192                value: None,
193            });
194            rest = &rest[segment_end..];
195            continue;
196        };
197        let key = rest[..eq].trim_end();
198        let value = rest[eq + 1..].trim_start();
199        let value_end = match value
200            .strip_prefix('"')
201            .and_then(closing_quote)
202        {
203            Some(close) => {
204                let after = close + 2;
205                after
206                    + value[after..]
207                        .find(';')
208                        .unwrap_or(value.len() - after)
209            }
210            None => value
211                .find(';')
212                .unwrap_or(value.len()),
213        };
214        params.push(RawParam {
215            key,
216            value: Some(value[..value_end].trim_end()),
217        });
218        rest = &value[value_end..];
219    }
220}
221
222/// Write `;key`, `;key=value`, or `;key="value"` when `quote` is set.
223pub(crate) fn write_param<W: std::fmt::Write + ?Sized>(
224    w: &mut W,
225    key: &str,
226    value: Option<&str>,
227    quote: bool,
228) -> std::fmt::Result {
229    w.write_char(';')?;
230    w.write_str(key)?;
231    match value {
232        None => Ok(()),
233        Some(v) => {
234            w.write_char('=')?;
235            if quote {
236                write_quoted_pair(w, v)
237            } else {
238                w.write_str(v)
239            }
240        }
241    }
242}
243
244/// Split comma-separated header entries respecting angle-bracket nesting
245/// and double-quoted strings.
246///
247/// SIP headers that carry lists (RFC 3261 §7.3.1) use commas as delimiters,
248/// but commas may also appear inside angle-bracketed URIs or quoted strings
249/// (e.g. Warning warn-text per §20.43). This function splits only on commas
250/// at bracket depth zero and outside quoted strings.
251///
252/// Backslash escapes inside quoted strings (RFC 3261 §25.1 `quoted-pair`)
253/// are respected to avoid premature quote-close on `\"`. Quotes are only
254/// significant at bracket depth zero: a stray `"` inside `<...>` (not legal
255/// in any §25.1 URI character set) affects that entry alone.
256pub fn split_comma_entries(raw: &str) -> Vec<&str> {
257    let bytes = raw.as_bytes();
258    let mut entries = Vec::new();
259    let mut depth = 0u32;
260    let mut start = 0;
261    let mut i = 0;
262
263    while i < bytes.len() {
264        match bytes[i] {
265            b'"' if depth == 0 => match closing_quote(&raw[i + 1..]) {
266                Some(close) => i += close + 1,
267                None => break,
268            },
269            b'<' => depth += 1,
270            b'>' => depth = depth.saturating_sub(1),
271            b',' if depth == 0 => {
272                entries.push(&raw[start..i]);
273                start = i + 1;
274            }
275            _ => {}
276        }
277        i += 1;
278    }
279    if start < raw.len() {
280        entries.push(&raw[start..]);
281    }
282
283    entries
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn split_comma_simple() {
292        assert_eq!(split_comma_entries("a, b, c"), vec!["a", " b", " c"]);
293    }
294
295    #[test]
296    fn split_comma_respects_angle_brackets() {
297        let input = "<sip:a@host,x>, <sip:b@host>";
298        let parts = split_comma_entries(input);
299        assert_eq!(parts.len(), 2);
300        assert!(parts[0].contains("host,x"));
301    }
302
303    #[test]
304    fn split_comma_respects_quoted_strings() {
305        let input = r#"301 example.com "text, comma", 399 example.org "ok""#;
306        let parts = split_comma_entries(input);
307        assert_eq!(parts.len(), 2);
308        assert!(parts[0].contains("text, comma"));
309    }
310
311    #[test]
312    fn split_comma_respects_escaped_quote() {
313        let input = r#"301 example.com "say \"hi, there\"", 399 example.org "ok""#;
314        let parts = split_comma_entries(input);
315        assert_eq!(parts.len(), 2);
316    }
317
318    #[test]
319    fn split_comma_quote_inside_brackets_confined_to_one_entry() {
320        let input = r#"<https://example.com/a"b>;purpose=icon, <urn:example:call:1>;purpose=info, <https://example.com/c>;purpose=card"#;
321        let parts = split_comma_entries(input);
322        assert_eq!(parts.len(), 3);
323        assert_eq!(parts[2], " <https://example.com/c>;purpose=card");
324    }
325
326    #[test]
327    fn split_comma_quote_inside_brackets_keeps_later_quoted_param() {
328        let input = r#"<https://example.com/a"b>;purpose=icon, <sip:b@example.com>;note="x,y", <sip:c@example.com>"#;
329        let parts = split_comma_entries(input);
330        assert_eq!(parts.len(), 3);
331        assert_eq!(parts[1], r#" <sip:b@example.com>;note="x,y""#);
332    }
333
334    fn params(s: &str) -> Vec<(&str, Option<&str>)> {
335        parse_params(s)
336            .into_iter()
337            .map(|p| (p.key, p.value))
338            .collect()
339    }
340
341    #[test]
342    fn params_quoted_value_keeps_semicolon() {
343        assert_eq!(
344            params(r#";foo="a;b";tag=x"#),
345            vec![("foo", Some(r#""a;b""#)), ("tag", Some("x"))]
346        );
347    }
348
349    #[test]
350    fn params_trim_sws_around_semi_and_equal() {
351        assert_eq!(
352            params(" ; tag = x ; lr "),
353            vec![("tag", Some("x")), ("lr", None)]
354        );
355    }
356
357    #[test]
358    fn params_without_leading_semicolon_and_empty_segments() {
359        assert_eq!(params("tag=x;;lr;"), vec![("tag", Some("x")), ("lr", None)]);
360    }
361
362    #[test]
363    fn params_unterminated_quote_splits_at_next_semicolon() {
364        assert_eq!(
365            params(r#";x="a;to-tag=1;from-tag=2"#),
366            vec![
367                ("x", Some(r#""a"#)),
368                ("to-tag", Some("1")),
369                ("from-tag", Some("2"))
370            ]
371        );
372    }
373
374    #[test]
375    fn params_escaped_quote_inside_value() {
376        let p = parse_params(r#";t="say \"hi;\"";x=1"#);
377        assert_eq!(p.len(), 2);
378        assert_eq!(p[0].value, Some(r#""say \"hi;\"""#));
379        assert_eq!(p[0].unquoted(), Some((r#"say "hi;""#.to_string(), true)));
380        assert_eq!(p[1].value, Some("1"));
381    }
382
383    #[test]
384    fn params_empty_quoted_value() {
385        let p = parse_params(r#";a="""#);
386        assert_eq!(p[0].unquoted(), Some((String::new(), true)));
387    }
388
389    #[test]
390    fn params_unquoted_token_value() {
391        let p = parse_params(";a=b");
392        assert_eq!(p[0].unquoted(), Some(("b".to_string(), false)));
393        assert_eq!(parse_params(";lr")[0].unquoted(), None);
394    }
395
396    #[test]
397    fn write_quoted_pair_escapes_controls_outside_qdtext() {
398        let mut s = String::new();
399        write_quoted_pair(&mut s, "a\u{1}b\tc\u{7f}").unwrap();
400        assert_eq!(s, "\"a\\\u{1}b\tc\\\u{7f}\"");
401    }
402
403    #[test]
404    fn write_param_forms() {
405        let mut s = String::new();
406        write_param(&mut s, "lr", None, false).unwrap();
407        write_param(&mut s, "tag", Some("x"), false).unwrap();
408        write_param(&mut s, "d-ver", Some(r#"a"b"#), true).unwrap();
409        assert_eq!(s, r#";lr;tag=x;d-ver="a\"b""#);
410    }
411
412    #[test]
413    fn split_comma_bracket_inside_quoted_display_name_inert() {
414        let input = r#""a<b" <sip:x@example.com>, <sip:y@example.com>"#;
415        let parts = split_comma_entries(input);
416        assert_eq!(parts.len(), 2);
417    }
418}