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