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;
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
71pub(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
89pub(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
112pub(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
129pub(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
143pub 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}