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;
37#[cfg(feature = "conference-info")]
38pub mod conference_info;
39pub mod contact;
40pub mod geolocation;
41pub mod header;
42pub mod header_addr;
43pub mod history_info;
44#[cfg(feature = "message")]
45pub mod message;
46pub mod replaces;
47pub mod security;
48pub mod target_dialog;
49pub mod uri_info;
50pub mod via;
51pub mod warning;
52
53pub use accept::{SipAccept, SipAcceptEntry, SipAcceptError};
54pub use accept_encoding::{SipAcceptEncoding, SipAcceptEncodingEntry, SipAcceptEncodingError};
55pub use accept_language::{SipAcceptLanguage, SipAcceptLanguageEntry, SipAcceptLanguageError};
56pub use auth::{SipAuthError, SipAuthValue};
57pub use contact::ContactValue;
58pub use geolocation::{SipGeolocation, SipGeolocationRef};
59pub use header::{ParseSipHeaderError, SipHeader, SipHeaderLookup};
60pub use header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
61pub use history_info::{HistoryInfo, HistoryInfoEntry, HistoryInfoError, HistoryInfoReason};
62#[cfg(feature = "message")]
63pub use message::{extract_all_headers, extract_body, extract_header, extract_request_uri};
64pub use replaces::{SipReplaces, SipReplacesError};
65pub use security::{SipSecurity, SipSecurityError, SipSecurityMechanism};
66pub use target_dialog::{SipTargetDialog, SipTargetDialogError};
67pub use uri_info::{UriInfo, UriInfoEntry, UriInfoError};
68pub use via::{SipVia, SipViaEntry, SipViaError};
69pub use warning::{SipWarning, SipWarningEntry, SipWarningError};
70
71/// Format a slice of displayable items as a separated list.
72pub(crate) fn fmt_joined<T: std::fmt::Display>(
73    f: &mut std::fmt::Formatter<'_>,
74    items: &[T],
75    separator: &str,
76) -> std::fmt::Result {
77    for (i, item) in items
78        .iter()
79        .enumerate()
80    {
81        if i > 0 {
82            f.write_str(separator)?;
83        }
84        write!(f, "{item}")?;
85    }
86    Ok(())
87}
88
89/// Unescape RFC 3261 §25.1 `quoted-pair` sequences: `\"` → `"`, `\\` → `\`.
90///
91/// Operates on the content *between* surrounding double-quotes (caller strips
92/// them). Skips allocation when no backslash escapes are present.
93pub(crate) fn unescape_quoted_pair(s: &str) -> String {
94    if !s.contains('\\') {
95        return s.to_string();
96    }
97    let mut result = String::with_capacity(s.len());
98    let mut escaped = false;
99    for ch in s.chars() {
100        if escaped {
101            result.push(ch);
102            escaped = false;
103        } else if ch == '\\' {
104            escaped = true;
105        } else {
106            result.push(ch);
107        }
108    }
109    result
110}
111
112/// Escape a string for use inside a `quoted-string` (RFC 3261 §25.1).
113///
114/// Escapes `"` → `\"` and `\` → `\\`. Does **not** add surrounding quotes.
115pub(crate) fn escape_quoted_pair(s: &str) -> String {
116    if !s.contains(['"', '\\']) {
117        return s.to_string();
118    }
119    let mut result = String::with_capacity(s.len() + 4);
120    for ch in s.chars() {
121        if ch == '"' || ch == '\\' {
122            result.push('\\');
123        }
124        result.push(ch);
125    }
126    result
127}
128
129/// Write a `quoted-string` to a formatter: surrounds with `"` and escapes
130/// embedded quotes/backslashes per RFC 3261 §25.1.
131pub(crate) fn write_quoted_pair(f: &mut std::fmt::Formatter<'_>, value: &str) -> std::fmt::Result {
132    f.write_str("\"")?;
133    for ch in value.chars() {
134        if ch == '"' || ch == '\\' {
135            write!(f, "\\{ch}")?;
136        } else {
137            write!(f, "{ch}")?;
138        }
139    }
140    f.write_str("\"")
141}
142
143/// Split comma-separated header entries respecting angle-bracket nesting
144/// and double-quoted strings.
145///
146/// SIP headers that carry lists (RFC 3261 §7.3.1) use commas as delimiters,
147/// but commas may also appear inside angle-bracketed URIs or quoted strings
148/// (e.g. Warning warn-text per §20.43). This function splits only on commas
149/// at bracket depth zero and outside quoted strings.
150///
151/// Backslash escapes inside quoted strings (RFC 3261 §25.1 `quoted-pair`)
152/// are respected to avoid premature quote-close on `\"`.
153pub fn split_comma_entries(raw: &str) -> Vec<&str> {
154    let mut entries = Vec::new();
155    let mut depth = 0u32;
156    let mut in_quotes = false;
157    let mut prev_backslash = false;
158    let mut start = 0;
159
160    for (i, ch) in raw.char_indices() {
161        if prev_backslash {
162            prev_backslash = false;
163            continue;
164        }
165        match ch {
166            '\\' if in_quotes => prev_backslash = true,
167            '"' => in_quotes = !in_quotes,
168            '<' if !in_quotes => depth += 1,
169            '>' if !in_quotes => depth = depth.saturating_sub(1),
170            ',' if depth == 0 && !in_quotes => {
171                entries.push(&raw[start..i]);
172                start = i + 1;
173            }
174            _ => {}
175        }
176    }
177    if start < raw.len() {
178        entries.push(&raw[start..]);
179    }
180
181    entries
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn split_comma_simple() {
190        assert_eq!(split_comma_entries("a, b, c"), vec!["a", " b", " c"]);
191    }
192
193    #[test]
194    fn split_comma_respects_angle_brackets() {
195        let input = "<sip:a@host,x>, <sip:b@host>";
196        let parts = split_comma_entries(input);
197        assert_eq!(parts.len(), 2);
198        assert!(parts[0].contains("host,x"));
199    }
200
201    #[test]
202    fn split_comma_respects_quoted_strings() {
203        let input = r#"301 example.com "text, comma", 399 example.org "ok""#;
204        let parts = split_comma_entries(input);
205        assert_eq!(parts.len(), 2);
206        assert!(parts[0].contains("text, comma"));
207    }
208
209    #[test]
210    fn split_comma_respects_escaped_quote() {
211        let input = r#"301 example.com "say \"hi, there\"", 399 example.org "ok""#;
212        let parts = split_comma_entries(input);
213        assert_eq!(parts.len(), 2);
214    }
215}