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//! - [`message`] — Extract headers, Request-URI and body from raw SIP message text (feature: `message`)
13//! - [`via`] — RFC 3261 Via header parser
14//! - [`warning`] — RFC 3261 Warning header parser
15//! - [`auth`] — SIP authentication value parser (Authorization, WWW-Authenticate, etc.)
16//! - [`contact`] — RFC 3261 Contact header parser
17//! - [`accept`] — RFC 3261 Accept header parser
18//! - [`accept_encoding`] — RFC 3261 Accept-Encoding header parser
19//! - [`accept_language`] — RFC 3261 Accept-Language header parser
20//! - [`security`] — RFC 3329 Security mechanism parser
21//! - [`uri_info`] — `<absoluteURI> *(SEMI generic-param)` parser (Call-Info, Alert-Info, Error-Info)
22//! - [`history_info`] — RFC 7044 History-Info header parser
23//! - [`geolocation`] — RFC 6442 Geolocation header parser
24//! - [`replaces`] — RFC 3891 Replaces / RFC 3911 Join header parser
25//! - [`target_dialog`] — RFC 4538 Target-Dialog header parser
26//! - `conference_info` — RFC 4575 conference event package (feature: `conference-info`)
27
28#[macro_use]
29mod macros;
30
31pub use sip_uri;
32
33pub mod accept;
34pub mod accept_encoding;
35pub mod accept_language;
36pub mod auth;
37pub mod call_id;
38#[cfg(feature = "conference-info")]
39pub mod conference_info;
40pub mod contact;
41pub mod geolocation;
42pub mod header;
43pub mod header_addr;
44pub mod history_info;
45#[cfg(feature = "message")]
46pub mod message;
47pub mod replaces;
48pub mod security;
49pub mod target_dialog;
50pub mod uri_info;
51pub mod via;
52pub mod warning;
53
54pub use accept::{SipAccept, SipAcceptEntry, SipAcceptError};
55pub use accept_encoding::{SipAcceptEncoding, SipAcceptEncodingEntry, SipAcceptEncodingError};
56pub use accept_language::{SipAcceptLanguage, SipAcceptLanguageEntry, SipAcceptLanguageError};
57pub use auth::{SipAuthError, SipAuthValue};
58pub use call_id::{SipCallId, SipCallIdError};
59pub use contact::ContactValue;
60pub use geolocation::{SipGeolocation, SipGeolocationRef};
61pub use header::{ParseSipHeaderError, SipHeader, SipHeaderLookup};
62pub use header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
63pub use history_info::{HistoryInfo, HistoryInfoEntry, HistoryInfoError, HistoryInfoReason};
64#[cfg(feature = "message")]
65pub use message::{extract_all_headers, extract_body, extract_header, extract_request_uri};
66pub use replaces::{SipReplaces, SipReplacesError};
67pub use security::{SipSecurity, SipSecurityError, SipSecurityMechanism};
68pub use target_dialog::{SipTargetDialog, SipTargetDialogError};
69pub use uri_info::{UriInfo, UriInfoEntry, UriInfoError};
70pub use via::{SipVia, SipViaEntry, SipViaError};
71pub use warning::{SipWarning, SipWarningEntry, SipWarningError};
72
73/// Format a slice of displayable items as a separated list.
74pub(crate) fn fmt_joined<T: std::fmt::Display>(
75    f: &mut std::fmt::Formatter<'_>,
76    items: &[T],
77    separator: &str,
78) -> std::fmt::Result {
79    for (i, item) in items
80        .iter()
81        .enumerate()
82    {
83        if i > 0 {
84            f.write_str(separator)?;
85        }
86        write!(f, "{item}")?;
87    }
88    Ok(())
89}
90
91/// Unescape RFC 3261 §25.1 `quoted-pair` sequences: `\"` → `"`, `\\` → `\`.
92///
93/// Operates on the content *between* surrounding double-quotes (caller strips
94/// them). Skips allocation when no backslash escapes are present.
95pub(crate) fn unescape_quoted_pair(s: &str) -> String {
96    if !s.contains('\\') {
97        return s.to_string();
98    }
99    let mut result = String::with_capacity(s.len());
100    let mut escaped = false;
101    for ch in s.chars() {
102        if escaped {
103            result.push(ch);
104            escaped = false;
105        } else if ch == '\\' {
106            escaped = true;
107        } else {
108            result.push(ch);
109        }
110    }
111    result
112}
113
114/// Escape a string for use inside a `quoted-string` (RFC 3261 §25.1).
115///
116/// Escapes `"` → `\"` and `\` → `\\`. Does **not** add surrounding quotes.
117pub(crate) fn escape_quoted_pair(s: &str) -> String {
118    if !s.contains(['"', '\\']) {
119        return s.to_string();
120    }
121    let mut result = String::with_capacity(s.len() + 4);
122    for ch in s.chars() {
123        if ch == '"' || ch == '\\' {
124            result.push('\\');
125        }
126        result.push(ch);
127    }
128    result
129}
130
131/// Write a `quoted-string` to a formatter: surrounds with `"` and escapes
132/// embedded quotes/backslashes per RFC 3261 §25.1.
133pub(crate) fn write_quoted_pair(f: &mut std::fmt::Formatter<'_>, value: &str) -> std::fmt::Result {
134    f.write_str("\"")?;
135    for ch in value.chars() {
136        if ch == '"' || ch == '\\' {
137            write!(f, "\\{ch}")?;
138        } else {
139            write!(f, "{ch}")?;
140        }
141    }
142    f.write_str("\"")
143}
144
145/// Split comma-separated header entries respecting angle-bracket nesting
146/// and double-quoted strings.
147///
148/// SIP headers that carry lists (RFC 3261 §7.3.1) use commas as delimiters,
149/// but commas may also appear inside angle-bracketed URIs or quoted strings
150/// (e.g. Warning warn-text per §20.43). This function splits only on commas
151/// at bracket depth zero and outside quoted strings.
152///
153/// Backslash escapes inside quoted strings (RFC 3261 §25.1 `quoted-pair`)
154/// are respected to avoid premature quote-close on `\"`. Quotes are only
155/// significant at bracket depth zero: a stray `"` inside `<...>` (not legal
156/// in any §25.1 URI character set) affects that entry alone.
157pub fn split_comma_entries(raw: &str) -> Vec<&str> {
158    let mut entries = Vec::new();
159    let mut depth = 0u32;
160    let mut in_quotes = false;
161    let mut prev_backslash = false;
162    let mut start = 0;
163
164    for (i, ch) in raw.char_indices() {
165        if prev_backslash {
166            prev_backslash = false;
167            continue;
168        }
169        match ch {
170            '\\' if in_quotes => prev_backslash = true,
171            '"' if depth == 0 => in_quotes = !in_quotes,
172            '<' if !in_quotes => depth += 1,
173            '>' if !in_quotes => depth = depth.saturating_sub(1),
174            ',' if depth == 0 && !in_quotes => {
175                entries.push(&raw[start..i]);
176                start = i + 1;
177            }
178            _ => {}
179        }
180    }
181    if start < raw.len() {
182        entries.push(&raw[start..]);
183    }
184
185    entries
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn split_comma_simple() {
194        assert_eq!(split_comma_entries("a, b, c"), vec!["a", " b", " c"]);
195    }
196
197    #[test]
198    fn split_comma_respects_angle_brackets() {
199        let input = "<sip:a@host,x>, <sip:b@host>";
200        let parts = split_comma_entries(input);
201        assert_eq!(parts.len(), 2);
202        assert!(parts[0].contains("host,x"));
203    }
204
205    #[test]
206    fn split_comma_respects_quoted_strings() {
207        let input = r#"301 example.com "text, comma", 399 example.org "ok""#;
208        let parts = split_comma_entries(input);
209        assert_eq!(parts.len(), 2);
210        assert!(parts[0].contains("text, comma"));
211    }
212
213    #[test]
214    fn split_comma_respects_escaped_quote() {
215        let input = r#"301 example.com "say \"hi, there\"", 399 example.org "ok""#;
216        let parts = split_comma_entries(input);
217        assert_eq!(parts.len(), 2);
218    }
219
220    #[test]
221    fn split_comma_quote_inside_brackets_confined_to_one_entry() {
222        let input = r#"<https://example.com/a"b>;purpose=icon, <urn:example:call:1>;purpose=info, <https://example.com/c>;purpose=card"#;
223        let parts = split_comma_entries(input);
224        assert_eq!(parts.len(), 3);
225        assert_eq!(parts[2], " <https://example.com/c>;purpose=card");
226    }
227
228    #[test]
229    fn split_comma_quote_inside_brackets_keeps_later_quoted_param() {
230        let input = r#"<https://example.com/a"b>;purpose=icon, <sip:b@example.com>;note="x,y", <sip:c@example.com>"#;
231        let parts = split_comma_entries(input);
232        assert_eq!(parts.len(), 3);
233        assert_eq!(parts[1], r#" <sip:b@example.com>;note="x,y""#);
234    }
235
236    #[test]
237    fn split_comma_bracket_inside_quoted_display_name_inert() {
238        let input = r#""a<b" <sip:x@example.com>, <sip:y@example.com>"#;
239        let parts = split_comma_entries(input);
240        assert_eq!(parts.len(), 2);
241    }
242}