1use std::borrow::Cow;
4use std::fmt;
5use std::str::{FromStr, Utf8Error};
6
7use percent_encoding::percent_decode_str;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
47#[non_exhaustive]
48pub struct SipHeaderAddr {
49 display_name: Option<String>,
50 uri: sip_uri::Uri,
51 params: Vec<(String, Option<String>)>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ParseSipHeaderAddrError(pub String);
57
58impl fmt::Display for ParseSipHeaderAddrError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "invalid SIP header address: {}", self.0)
61 }
62}
63
64impl std::error::Error for ParseSipHeaderAddrError {}
65
66impl From<sip_uri::ParseUriError> for ParseSipHeaderAddrError {
67 fn from(e: sip_uri::ParseUriError) -> Self {
68 Self(e.to_string())
69 }
70}
71
72impl From<sip_uri::ParseSipUriError> for ParseSipHeaderAddrError {
73 fn from(e: sip_uri::ParseSipUriError) -> Self {
74 Self(e.to_string())
75 }
76}
77
78impl SipHeaderAddr {
79 pub fn new(uri: sip_uri::Uri) -> Self {
81 SipHeaderAddr {
82 display_name: None,
83 uri,
84 params: Vec::new(),
85 }
86 }
87
88 pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
90 self.display_name = Some(name.into());
91 self
92 }
93
94 pub fn with_param(mut self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
96 self.params
97 .push((
98 key.into()
99 .to_ascii_lowercase(),
100 value.map(Into::into),
101 ));
102 self
103 }
104
105 pub fn display_name(&self) -> Option<&str> {
107 self.display_name
108 .as_deref()
109 }
110
111 pub fn uri(&self) -> &sip_uri::Uri {
113 &self.uri
114 }
115
116 pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
118 self.uri
119 .as_sip()
120 }
121
122 pub fn tel_uri(&self) -> Option<&sip_uri::TelUri> {
124 self.uri
125 .as_tel()
126 }
127
128 pub fn urn_uri(&self) -> Option<&sip_uri::UrnUri> {
130 self.uri
131 .as_urn()
132 }
133
134 pub fn params(&self) -> impl Iterator<Item = (&str, Option<&str>)> {
137 self.params
138 .iter()
139 .map(|(k, v)| (k.as_str(), v.as_deref()))
140 }
141
142 pub fn param(&self, name: &str) -> Option<Result<Option<Cow<'_, str>>, Utf8Error>> {
151 let needle = name.to_ascii_lowercase();
152 self.params
153 .iter()
154 .find(|(k, _)| *k == needle)
155 .map(|(_, v)| match v {
156 Some(raw) => percent_decode_str(raw)
157 .decode_utf8()
158 .map(Some),
159 None => Ok(None),
160 })
161 }
162
163 pub fn param_raw(&self, name: &str) -> Option<Option<&str>> {
168 let needle = name.to_ascii_lowercase();
169 self.params
170 .iter()
171 .find(|(k, _)| *k == needle)
172 .map(|(_, v)| v.as_deref())
173 }
174
175 pub fn parse_list(raw: &str) -> Result<Vec<SipHeaderAddr>, ParseSipHeaderAddrError> {
181 if raw
182 .trim()
183 .is_empty()
184 {
185 return Ok(Vec::new());
186 }
187 crate::split_comma_entries(raw)
188 .into_iter()
189 .map(|entry| {
190 entry
191 .trim()
192 .parse()
193 })
194 .collect()
195 }
196
197 pub fn tag(&self) -> Option<&str> {
202 self.param_raw("tag")
203 .flatten()
204 }
205}
206
207fn parse_quoted_string(s: &str) -> Result<(String, &str), String> {
209 if !s.starts_with('"') {
210 return Err("expected opening quote".into());
211 }
212
213 let mut result = String::new();
214 let mut chars = s[1..].char_indices();
215
216 while let Some((i, c)) = chars.next() {
217 match c {
218 '"' => {
219 return Ok((result, &s[i + 2..]));
220 }
221 '\\' => {
222 let (_, escaped) = chars
223 .next()
224 .ok_or("unterminated escape in quoted string")?;
225 result.push(escaped);
226 }
227 _ => {
228 result.push(c);
229 }
230 }
231 }
232
233 Err("unterminated quoted string".into())
234}
235
236fn extract_angle_uri(s: &str) -> Option<(&str, &str)> {
238 let s = s.strip_prefix('<')?;
239 let end = s.find('>')?;
240 Some((&s[..end], &s[end + 1..]))
241}
242
243fn parse_header_params(s: &str) -> Vec<(String, Option<String>)> {
246 let mut params = Vec::new();
247 for segment in s.split(';') {
248 if segment.is_empty() {
249 continue;
250 }
251 if let Some((key, value)) = segment.split_once('=') {
252 params.push((key.to_ascii_lowercase(), Some(value.to_string())));
253 } else {
254 params.push((segment.to_ascii_lowercase(), None));
255 }
256 }
257 params
258}
259
260fn needs_quoting(name: &str) -> bool {
262 name.bytes()
263 .any(|b| {
264 matches!(
265 b,
266 b'"' | b'\\' | b'<' | b'>' | b',' | b';' | b':' | b'@' | b' ' | b'\t'
267 )
268 })
269}
270
271fn escape_display_name(name: &str) -> String {
273 let mut out = String::with_capacity(name.len());
274 for c in name.chars() {
275 if matches!(c, '"' | '\\') {
276 out.push('\\');
277 }
278 out.push(c);
279 }
280 out
281}
282
283impl FromStr for SipHeaderAddr {
284 type Err = ParseSipHeaderAddrError;
285
286 fn from_str(input: &str) -> Result<Self, Self::Err> {
287 let err = |msg: &str| ParseSipHeaderAddrError(msg.to_string());
288 let s = input.trim();
289
290 if s.is_empty() {
291 return Err(err("empty input"));
292 }
293
294 if s.starts_with('"') {
296 let (display_name, rest) = parse_quoted_string(s).map_err(|e| err(&e))?;
297 let rest = rest.trim_start();
298 let (uri_str, trailing) = extract_angle_uri(rest)
299 .ok_or_else(|| err("expected '<URI>' after quoted display name"))?;
300 let uri: sip_uri::Uri = uri_str.parse()?;
301 let display_name = if display_name.is_empty() {
302 None
303 } else {
304 Some(display_name)
305 };
306 let params = parse_header_params(trailing);
307 return Ok(SipHeaderAddr {
308 display_name,
309 uri,
310 params,
311 });
312 }
313
314 if s.starts_with('<') {
316 let (uri_str, trailing) = extract_angle_uri(s).ok_or_else(|| err("unclosed '<'"))?;
317 let uri: sip_uri::Uri = uri_str.parse()?;
318 let params = parse_header_params(trailing);
319 return Ok(SipHeaderAddr {
320 display_name: None,
321 uri,
322 params,
323 });
324 }
325
326 if let Some(angle_start) = s.find('<') {
328 let display_name = s[..angle_start].trim();
329 let display_name = if display_name.is_empty() {
330 None
331 } else {
332 Some(display_name.to_string())
333 };
334 let (uri_str, trailing) =
335 extract_angle_uri(&s[angle_start..]).ok_or_else(|| err("unclosed '<'"))?;
336 let uri: sip_uri::Uri = uri_str.parse()?;
337 let params = parse_header_params(trailing);
338 return Ok(SipHeaderAddr {
339 display_name,
340 uri,
341 params,
342 });
343 }
344
345 let uri: sip_uri::Uri = s.parse()?;
349 Ok(SipHeaderAddr {
350 display_name: None,
351 uri,
352 params: Vec::new(),
353 })
354 }
355}
356
357impl fmt::Display for SipHeaderAddr {
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 match self
360 .display_name
361 .as_deref()
362 {
363 Some(name) if !name.is_empty() => {
364 if needs_quoting(name) {
365 write!(f, "\"{}\" ", escape_display_name(name))?;
366 } else {
367 write!(f, "{name} ")?;
368 }
369 write!(f, "<{}>", self.uri)?;
370 }
371 _ => {
372 write!(f, "<{}>", self.uri)?;
373 }
374 }
375 for (key, value) in &self.params {
376 match value {
377 Some(v) => write!(f, ";{key}={v}")?,
378 None => write!(f, ";{key}")?,
379 }
380 }
381 Ok(())
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use std::borrow::Cow;
388
389 use super::*;
390
391 #[test]
392 fn quoted_display_name_with_tag() {
393 let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#
394 .parse()
395 .unwrap();
396 assert_eq!(addr.display_name(), Some("Alice"));
397 assert!(addr
398 .sip_uri()
399 .is_some());
400 assert_eq!(addr.tag(), Some("abc123"));
401 }
402
403 #[test]
404 fn angle_bracket_no_name_multiple_params() {
405 let addr: SipHeaderAddr = "<sip:user@host>;tag=xyz;expires=3600"
406 .parse()
407 .unwrap();
408 assert_eq!(addr.display_name(), None);
409 assert_eq!(addr.tag(), Some("xyz"));
410 assert_eq!(
411 addr.param("expires")
412 .unwrap()
413 .unwrap(),
414 Some(Cow::from("3600")),
415 );
416 }
417
418 #[test]
419 fn bare_addr_spec_no_params() {
420 let addr: SipHeaderAddr = "sip:user@host"
421 .parse()
422 .unwrap();
423 assert_eq!(addr.display_name(), None);
424 assert!(addr
425 .sip_uri()
426 .is_some());
427 assert_eq!(
428 addr.params()
429 .count(),
430 0
431 );
432 }
433
434 #[test]
435 fn unquoted_display_name_with_params() {
436 let addr: SipHeaderAddr = "Alice <sip:alice@example.com>;tag=abc"
437 .parse()
438 .unwrap();
439 assert_eq!(addr.display_name(), Some("Alice"));
440 assert_eq!(addr.tag(), Some("abc"));
441 }
442
443 #[test]
444 fn ng911_refer_to_serviceurn() {
445 let input = "<sip:user@esrp.example.com?Call-Info=x>;serviceurn=urn%3Aservice%3Apolice";
446 let addr: SipHeaderAddr = input
447 .parse()
448 .unwrap();
449 assert_eq!(addr.display_name(), None);
450 assert_eq!(
451 addr.param("serviceurn")
452 .unwrap()
453 .unwrap(),
454 Some(Cow::from("urn:service:police")),
455 );
456 assert_eq!(
457 addr.param_raw("serviceurn"),
458 Some(Some("urn%3Aservice%3Apolice")),
459 );
460 let sip = addr
461 .sip_uri()
462 .unwrap();
463 assert_eq!(
464 sip.host()
465 .to_string(),
466 "esrp.example.com"
467 );
468 }
469
470 #[test]
471 fn p_asserted_identity_uri_params_no_header_params() {
472 let input = r#""EXAMPLE CO" <sip:+15551234567;cpc=emergency@198.51.100.1;user=phone>"#;
473 let addr: SipHeaderAddr = input
474 .parse()
475 .unwrap();
476 assert_eq!(addr.display_name(), Some("EXAMPLE CO"));
477 assert_eq!(
478 addr.params()
479 .count(),
480 0
481 );
482 let sip = addr
483 .sip_uri()
484 .unwrap();
485 assert_eq!(sip.user(), Some("+15551234567"));
486 assert_eq!(sip.param("user"), Some(&Some("phone".to_string())));
487 }
488
489 #[test]
490 fn tel_uri_with_header_params() {
491 let addr: SipHeaderAddr = "<tel:+15551234567>;expires=3600"
492 .parse()
493 .unwrap();
494 assert!(addr
495 .tel_uri()
496 .is_some());
497 assert_eq!(
498 addr.param("expires")
499 .unwrap()
500 .unwrap(),
501 Some(Cow::from("3600")),
502 );
503 }
504
505 #[test]
506 fn flag_param_no_value() {
507 let addr: SipHeaderAddr = "<sip:user@host>;lr;tag=abc"
508 .parse()
509 .unwrap();
510 assert_eq!(
511 addr.param("lr")
512 .unwrap()
513 .unwrap(),
514 None
515 );
516 assert_eq!(addr.tag(), Some("abc"));
517 }
518
519 #[test]
520 fn urn_uri_no_params() {
521 let addr: SipHeaderAddr = "<urn:service:sos>"
522 .parse()
523 .unwrap();
524 assert!(addr
525 .urn_uri()
526 .is_some());
527 assert_eq!(
528 addr.params()
529 .count(),
530 0
531 );
532 }
533
534 #[test]
535 fn empty_input_fails() {
536 assert!(""
537 .parse::<SipHeaderAddr>()
538 .is_err());
539 }
540
541 #[test]
542 fn display_roundtrip_quoted_name_with_params() {
543 let input = r#""Alice" <sip:alice@example.com>;tag=abc123"#;
545 let addr: SipHeaderAddr = input
546 .parse()
547 .unwrap();
548 assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
549 }
550
551 #[test]
552 fn display_roundtrip_name_requiring_quotes() {
553 let input = r#""Alice Smith" <sip:alice@example.com>;tag=abc123"#;
554 let addr: SipHeaderAddr = input
555 .parse()
556 .unwrap();
557 assert_eq!(addr.to_string(), input);
558 }
559
560 #[test]
561 fn display_roundtrip_no_name_with_params() {
562 let input = "<sip:user@host>;tag=xyz;expires=3600";
563 let addr: SipHeaderAddr = input
564 .parse()
565 .unwrap();
566 assert_eq!(addr.to_string(), input);
567 }
568
569 #[test]
570 fn display_roundtrip_bare_uri() {
571 let input = "sip:user@host";
572 let addr: SipHeaderAddr = input
573 .parse()
574 .unwrap();
575 assert_eq!(addr.to_string(), "<sip:user@host>");
577 }
578
579 #[test]
580 fn display_roundtrip_flag_param() {
581 let input = "<sip:user@host>;lr;tag=abc";
582 let addr: SipHeaderAddr = input
583 .parse()
584 .unwrap();
585 assert_eq!(addr.to_string(), input);
586 }
587
588 #[test]
589 fn case_insensitive_param_lookup() {
590 let addr: SipHeaderAddr = "<sip:user@host>;Tag=ABC;Expires=3600"
591 .parse()
592 .unwrap();
593 assert_eq!(
594 addr.param("tag")
595 .unwrap()
596 .unwrap(),
597 Some(Cow::from("ABC")),
598 );
599 assert_eq!(
600 addr.param("TAG")
601 .unwrap()
602 .unwrap(),
603 Some(Cow::from("ABC")),
604 );
605 assert_eq!(
606 addr.param("expires")
607 .unwrap()
608 .unwrap(),
609 Some(Cow::from("3600")),
610 );
611 }
612
613 #[test]
614 fn tag_convenience_accessor() {
615 let with_tag: SipHeaderAddr = "<sip:user@host>;tag=xyz"
616 .parse()
617 .unwrap();
618 assert_eq!(with_tag.tag(), Some("xyz"));
619
620 let without_tag: SipHeaderAddr = "<sip:user@host>"
621 .parse()
622 .unwrap();
623 assert_eq!(without_tag.tag(), None);
624 }
625
626 #[test]
627 fn builder_new() {
628 let uri: sip_uri::Uri = "sip:alice@example.com"
629 .parse()
630 .unwrap();
631 let addr = SipHeaderAddr::new(uri);
632 assert_eq!(addr.display_name(), None);
633 assert_eq!(
634 addr.params()
635 .count(),
636 0
637 );
638 assert_eq!(addr.to_string(), "<sip:alice@example.com>");
639 }
640
641 #[test]
642 fn builder_with_display_name_and_params() {
643 let uri: sip_uri::Uri = "sip:alice@example.com"
644 .parse()
645 .unwrap();
646 let addr = SipHeaderAddr::new(uri)
647 .with_display_name("Alice")
648 .with_param("tag", Some("abc123"));
649 assert_eq!(addr.display_name(), Some("Alice"));
650 assert_eq!(addr.tag(), Some("abc123"));
651 assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
652 }
653
654 #[test]
655 fn builder_flag_param() {
656 let uri: sip_uri::Uri = "sip:proxy@example.com"
657 .parse()
658 .unwrap();
659 let addr = SipHeaderAddr::new(uri).with_param("lr", None::<String>);
660 assert_eq!(
661 addr.param("lr")
662 .unwrap()
663 .unwrap(),
664 None
665 );
666 assert_eq!(addr.to_string(), "<sip:proxy@example.com>;lr");
667 }
668
669 #[test]
670 fn escaped_quotes_in_display_name() {
671 let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
672 let addr: SipHeaderAddr = input
673 .parse()
674 .unwrap();
675 assert_eq!(addr.display_name(), Some(r#"Say "Hello""#));
676 assert_eq!(addr.tag(), Some("t"));
677 }
678
679 #[test]
680 fn display_roundtrip_escaped_quotes() {
681 let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
682 let addr: SipHeaderAddr = input
683 .parse()
684 .unwrap();
685 assert_eq!(addr.to_string(), input);
686 }
687
688 #[test]
689 fn trailing_semicolon_ignored() {
690 let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;"
691 .parse()
692 .unwrap();
693 assert_eq!(
694 addr.params()
695 .count(),
696 1
697 );
698 assert_eq!(addr.tag(), Some("abc"));
699 }
700
701 #[test]
702 fn display_roundtrip_percent_encoded_params() {
703 let input = "<sip:user@esrp.example.com>;serviceurn=urn%3Aservice%3Apolice";
704 let addr: SipHeaderAddr = input
705 .parse()
706 .unwrap();
707 assert_eq!(addr.to_string(), input);
708 }
709
710 #[test]
711 fn param_invalid_utf8_returns_err() {
712 let addr: SipHeaderAddr = "<sip:user@host>;data=%C0%80"
714 .parse()
715 .unwrap();
716 assert!(addr
717 .param("data")
718 .unwrap()
719 .is_err());
720 assert_eq!(addr.param_raw("data"), Some(Some("%C0%80")));
721 }
722
723 #[test]
724 fn param_iso_8859_fallback_to_raw() {
725 let addr: SipHeaderAddr = "<sip:user@host>;name=%E9"
727 .parse()
728 .unwrap();
729 assert!(addr
730 .param("name")
731 .unwrap()
732 .is_err());
733 assert_eq!(addr.param_raw("name"), Some(Some("%E9")));
734 }
735
736 #[test]
737 fn parse_list_multiple_entries() {
738 let input = r#""Alice" <sip:alice@example.com>;tag=a, <sip:bob@example.com>, sip:carol@example.com"#;
739 let addrs = SipHeaderAddr::parse_list(input).unwrap();
740 assert_eq!(addrs.len(), 3);
741 assert_eq!(addrs[0].display_name(), Some("Alice"));
742 assert_eq!(addrs[0].tag(), Some("a"));
743 assert_eq!(addrs[1].display_name(), None);
744 assert_eq!(
745 addrs[1]
746 .sip_uri()
747 .unwrap()
748 .user(),
749 Some("bob"),
750 );
751 assert_eq!(
752 addrs[2]
753 .sip_uri()
754 .unwrap()
755 .user(),
756 Some("carol"),
757 );
758 }
759
760 #[test]
761 fn parse_list_single_entry() {
762 let addrs = SipHeaderAddr::parse_list("<sip:alice@example.com>").unwrap();
763 assert_eq!(addrs.len(), 1);
764 }
765
766 #[test]
767 fn parse_list_empty_returns_empty() {
768 let addrs = SipHeaderAddr::parse_list("").unwrap();
769 assert!(addrs.is_empty());
770 }
771
772 #[test]
773 fn parse_list_propagates_parse_error() {
774 assert!(SipHeaderAddr::parse_list("not-a-uri, <sip:ok@example.com>").is_err());
775 }
776
777 #[test]
778 fn params_iterator() {
779 let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;lr;expires=60"
780 .parse()
781 .unwrap();
782 let params: Vec<_> = addr
783 .params()
784 .collect();
785 assert_eq!(params.len(), 3);
786 assert_eq!(params[0], ("tag", Some("abc")));
787 assert_eq!(params[1], ("lr", None));
788 assert_eq!(params[2], ("expires", Some("60")));
789 }
790}