1#[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
73pub(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
91pub(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
114pub(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
131pub(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
145pub fn split_comma_entries(raw: &str) -> Vec<&str> {
156 let mut entries = Vec::new();
157 let mut depth = 0u32;
158 let mut in_quotes = false;
159 let mut prev_backslash = false;
160 let mut start = 0;
161
162 for (i, ch) in raw.char_indices() {
163 if prev_backslash {
164 prev_backslash = false;
165 continue;
166 }
167 match ch {
168 '\\' if in_quotes => prev_backslash = true,
169 '"' => in_quotes = !in_quotes,
170 '<' if !in_quotes => depth += 1,
171 '>' if !in_quotes => depth = depth.saturating_sub(1),
172 ',' if depth == 0 && !in_quotes => {
173 entries.push(&raw[start..i]);
174 start = i + 1;
175 }
176 _ => {}
177 }
178 }
179 if start < raw.len() {
180 entries.push(&raw[start..]);
181 }
182
183 entries
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn split_comma_simple() {
192 assert_eq!(split_comma_entries("a, b, c"), vec!["a", " b", " c"]);
193 }
194
195 #[test]
196 fn split_comma_respects_angle_brackets() {
197 let input = "<sip:a@host,x>, <sip:b@host>";
198 let parts = split_comma_entries(input);
199 assert_eq!(parts.len(), 2);
200 assert!(parts[0].contains("host,x"));
201 }
202
203 #[test]
204 fn split_comma_respects_quoted_strings() {
205 let input = r#"301 example.com "text, comma", 399 example.org "ok""#;
206 let parts = split_comma_entries(input);
207 assert_eq!(parts.len(), 2);
208 assert!(parts[0].contains("text, comma"));
209 }
210
211 #[test]
212 fn split_comma_respects_escaped_quote() {
213 let input = r#"301 example.com "say \"hi, there\"", 399 example.org "ok""#;
214 let parts = split_comma_entries(input);
215 assert_eq!(parts.len(), 2);
216 }
217}