1#[macro_use]
30mod macros;
31
32pub use sip_uri;
33
34pub mod accept;
35pub mod accept_encoding;
36pub mod accept_language;
37pub mod auth;
38pub mod call_id;
39#[cfg(feature = "conference-info")]
40pub mod conference_info;
41pub mod contact;
42pub mod geolocation;
43pub mod header;
44pub mod header_addr;
45pub mod history_info;
46#[cfg(feature = "message")]
47pub mod message;
48pub mod replaces;
49pub mod security;
50pub mod target_dialog;
51pub mod uri_info;
52pub mod via;
53pub mod warning;
54
55pub use accept::{SipAccept, SipAcceptEntry, SipAcceptError};
56pub use accept_encoding::{SipAcceptEncoding, SipAcceptEncodingEntry, SipAcceptEncodingError};
57pub use accept_language::{SipAcceptLanguage, SipAcceptLanguageEntry, SipAcceptLanguageError};
58pub use auth::{SipAuthError, SipAuthValue};
59pub use call_id::{SipCallId, SipCallIdError};
60pub use contact::ContactValue;
61pub use geolocation::{SipGeolocation, SipGeolocationRef};
62pub use header::{ParseSipHeaderError, SipHeader, SipHeaderLookup};
63pub use header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
64pub use history_info::{HistoryInfo, HistoryInfoEntry, HistoryInfoError, HistoryInfoReason};
65#[cfg(feature = "message")]
66pub use message::{extract_all_headers, extract_body, extract_header, extract_request_uri};
67pub use replaces::{SipReplaces, SipReplacesError};
68pub use security::{SipSecurity, SipSecurityError, SipSecurityMechanism};
69pub use target_dialog::{SipTargetDialog, SipTargetDialogError};
70pub use uri_info::{UriInfo, UriInfoEntry, UriInfoError};
71pub use via::{SipVia, SipViaEntry, SipViaError};
72pub use warning::{SipWarning, SipWarningEntry, SipWarningError};
73
74pub(crate) fn fmt_joined<T: std::fmt::Display>(
76 f: &mut std::fmt::Formatter<'_>,
77 items: &[T],
78 separator: &str,
79) -> std::fmt::Result {
80 for (i, item) in items
81 .iter()
82 .enumerate()
83 {
84 if i > 0 {
85 f.write_str(separator)?;
86 }
87 write!(f, "{item}")?;
88 }
89 Ok(())
90}
91
92pub(crate) fn unescape_quoted_pair(s: &str) -> String {
97 if !s.contains('\\') {
98 return s.to_string();
99 }
100 let mut result = String::with_capacity(s.len());
101 let mut escaped = false;
102 for ch in s.chars() {
103 if escaped {
104 result.push(ch);
105 escaped = false;
106 } else if ch == '\\' {
107 escaped = true;
108 } else {
109 result.push(ch);
110 }
111 }
112 result
113}
114
115pub(crate) fn is_quoted_pair_only(c: char) -> bool {
118 c.is_ascii_control() && !matches!(c, '\t' | '\r' | '\n')
119}
120
121pub(crate) fn write_quoted_pair<W: std::fmt::Write + ?Sized>(
124 f: &mut W,
125 value: &str,
126) -> std::fmt::Result {
127 f.write_char('"')?;
128 for ch in value.chars() {
129 if ch == '"' || ch == '\\' || is_quoted_pair_only(ch) {
130 f.write_char('\\')?;
131 }
132 f.write_char(ch)?;
133 }
134 f.write_char('"')
135}
136
137fn closing_quote(s: &str) -> Option<usize> {
140 let bytes = s.as_bytes();
141 let mut i = 0;
142 while i < bytes.len() {
143 match bytes[i] {
144 b'\\' => i += 2,
145 b'"' => return Some(i),
146 _ => i += 1,
147 }
148 }
149 None
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub(crate) struct RawParam<'a> {
155 pub(crate) key: &'a str,
157 pub(crate) value: Option<&'a str>,
159}
160
161impl RawParam<'_> {
162 pub(crate) fn unquoted(&self) -> Option<(String, bool)> {
165 let v = self.value?;
166 Some(if v.len() >= 2 && v.starts_with('"') && v.ends_with('"') {
167 (unescape_quoted_pair(&v[1..v.len() - 1]), true)
168 } else {
169 (v.to_string(), false)
170 })
171 }
172}
173
174pub(crate) fn parse_params(s: &str) -> Vec<RawParam<'_>> {
179 let mut params = Vec::new();
180 let mut rest = s;
181 loop {
182 rest = rest.trim_start_matches(|c: char| c == ';' || c.is_ascii_whitespace());
183 if rest.is_empty() {
184 return params;
185 }
186 let segment_end = rest
187 .find(';')
188 .unwrap_or(rest.len());
189 let Some(eq) = rest[..segment_end].find('=') else {
190 params.push(RawParam {
191 key: rest[..segment_end].trim_end(),
192 value: None,
193 });
194 rest = &rest[segment_end..];
195 continue;
196 };
197 let key = rest[..eq].trim_end();
198 let value = rest[eq + 1..].trim_start();
199 let value_end = match value
200 .strip_prefix('"')
201 .and_then(closing_quote)
202 {
203 Some(close) => {
204 let after = close + 2;
205 after
206 + value[after..]
207 .find(';')
208 .unwrap_or(value.len() - after)
209 }
210 None => value
211 .find(';')
212 .unwrap_or(value.len()),
213 };
214 params.push(RawParam {
215 key,
216 value: Some(value[..value_end].trim_end()),
217 });
218 rest = &value[value_end..];
219 }
220}
221
222pub(crate) fn write_param<W: std::fmt::Write + ?Sized>(
224 w: &mut W,
225 key: &str,
226 value: Option<&str>,
227 quote: bool,
228) -> std::fmt::Result {
229 w.write_char(';')?;
230 w.write_str(key)?;
231 match value {
232 None => Ok(()),
233 Some(v) => {
234 w.write_char('=')?;
235 if quote {
236 write_quoted_pair(w, v)
237 } else {
238 w.write_str(v)
239 }
240 }
241 }
242}
243
244pub fn split_comma_entries(raw: &str) -> Vec<&str> {
257 let bytes = raw.as_bytes();
258 let mut entries = Vec::new();
259 let mut depth = 0u32;
260 let mut start = 0;
261 let mut i = 0;
262
263 while i < bytes.len() {
264 match bytes[i] {
265 b'"' if depth == 0 => match closing_quote(&raw[i + 1..]) {
266 Some(close) => i += close + 1,
267 None => break,
268 },
269 b'<' => depth += 1,
270 b'>' => depth = depth.saturating_sub(1),
271 b',' if depth == 0 => {
272 entries.push(&raw[start..i]);
273 start = i + 1;
274 }
275 _ => {}
276 }
277 i += 1;
278 }
279 if start < raw.len() {
280 entries.push(&raw[start..]);
281 }
282
283 entries
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn split_comma_simple() {
292 assert_eq!(split_comma_entries("a, b, c"), vec!["a", " b", " c"]);
293 }
294
295 #[test]
296 fn split_comma_respects_angle_brackets() {
297 let input = "<sip:a@host,x>, <sip:b@host>";
298 let parts = split_comma_entries(input);
299 assert_eq!(parts.len(), 2);
300 assert!(parts[0].contains("host,x"));
301 }
302
303 #[test]
304 fn split_comma_respects_quoted_strings() {
305 let input = r#"301 example.com "text, comma", 399 example.org "ok""#;
306 let parts = split_comma_entries(input);
307 assert_eq!(parts.len(), 2);
308 assert!(parts[0].contains("text, comma"));
309 }
310
311 #[test]
312 fn split_comma_respects_escaped_quote() {
313 let input = r#"301 example.com "say \"hi, there\"", 399 example.org "ok""#;
314 let parts = split_comma_entries(input);
315 assert_eq!(parts.len(), 2);
316 }
317
318 #[test]
319 fn split_comma_quote_inside_brackets_confined_to_one_entry() {
320 let input = r#"<https://example.com/a"b>;purpose=icon, <urn:example:call:1>;purpose=info, <https://example.com/c>;purpose=card"#;
321 let parts = split_comma_entries(input);
322 assert_eq!(parts.len(), 3);
323 assert_eq!(parts[2], " <https://example.com/c>;purpose=card");
324 }
325
326 #[test]
327 fn split_comma_quote_inside_brackets_keeps_later_quoted_param() {
328 let input = r#"<https://example.com/a"b>;purpose=icon, <sip:b@example.com>;note="x,y", <sip:c@example.com>"#;
329 let parts = split_comma_entries(input);
330 assert_eq!(parts.len(), 3);
331 assert_eq!(parts[1], r#" <sip:b@example.com>;note="x,y""#);
332 }
333
334 fn params(s: &str) -> Vec<(&str, Option<&str>)> {
335 parse_params(s)
336 .into_iter()
337 .map(|p| (p.key, p.value))
338 .collect()
339 }
340
341 #[test]
342 fn params_quoted_value_keeps_semicolon() {
343 assert_eq!(
344 params(r#";foo="a;b";tag=x"#),
345 vec![("foo", Some(r#""a;b""#)), ("tag", Some("x"))]
346 );
347 }
348
349 #[test]
350 fn params_trim_sws_around_semi_and_equal() {
351 assert_eq!(
352 params(" ; tag = x ; lr "),
353 vec![("tag", Some("x")), ("lr", None)]
354 );
355 }
356
357 #[test]
358 fn params_without_leading_semicolon_and_empty_segments() {
359 assert_eq!(params("tag=x;;lr;"), vec![("tag", Some("x")), ("lr", None)]);
360 }
361
362 #[test]
363 fn params_unterminated_quote_splits_at_next_semicolon() {
364 assert_eq!(
365 params(r#";x="a;to-tag=1;from-tag=2"#),
366 vec![
367 ("x", Some(r#""a"#)),
368 ("to-tag", Some("1")),
369 ("from-tag", Some("2"))
370 ]
371 );
372 }
373
374 #[test]
375 fn params_escaped_quote_inside_value() {
376 let p = parse_params(r#";t="say \"hi;\"";x=1"#);
377 assert_eq!(p.len(), 2);
378 assert_eq!(p[0].value, Some(r#""say \"hi;\"""#));
379 assert_eq!(p[0].unquoted(), Some((r#"say "hi;""#.to_string(), true)));
380 assert_eq!(p[1].value, Some("1"));
381 }
382
383 #[test]
384 fn params_empty_quoted_value() {
385 let p = parse_params(r#";a="""#);
386 assert_eq!(p[0].unquoted(), Some((String::new(), true)));
387 }
388
389 #[test]
390 fn params_unquoted_token_value() {
391 let p = parse_params(";a=b");
392 assert_eq!(p[0].unquoted(), Some(("b".to_string(), false)));
393 assert_eq!(parse_params(";lr")[0].unquoted(), None);
394 }
395
396 #[test]
397 fn write_quoted_pair_escapes_controls_outside_qdtext() {
398 let mut s = String::new();
399 write_quoted_pair(&mut s, "a\u{1}b\tc\u{7f}").unwrap();
400 assert_eq!(s, "\"a\\\u{1}b\tc\\\u{7f}\"");
401 }
402
403 #[test]
404 fn write_param_forms() {
405 let mut s = String::new();
406 write_param(&mut s, "lr", None, false).unwrap();
407 write_param(&mut s, "tag", Some("x"), false).unwrap();
408 write_param(&mut s, "d-ver", Some(r#"a"b"#), true).unwrap();
409 assert_eq!(s, r#";lr;tag=x;d-ver="a\"b""#);
410 }
411
412 #[test]
413 fn split_comma_bracket_inside_quoted_display_name_inert() {
414 let input = r#""a<b" <sip:x@example.com>, <sip:y@example.com>"#;
415 let parts = split_comma_entries(input);
416 assert_eq!(parts.len(), 2);
417 }
418}