1use std::borrow::Cow;
4use std::fmt::{self, Write as _};
5use std::str::{FromStr, Utf8Error};
6
7use percent_encoding::percent_decode_str;
8
9use crate::replaces::{SipReplaces, SipReplacesError};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct SipHeaderAddr {
51 display_name: Option<String>,
52 uri: sip_uri::Uri,
53 params: Vec<(String, Option<String>)>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ParseSipHeaderAddrError(pub String);
59
60impl fmt::Display for ParseSipHeaderAddrError {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 write!(f, "invalid SIP header address: {}", self.0)
63 }
64}
65
66impl std::error::Error for ParseSipHeaderAddrError {}
67
68impl From<sip_uri::ParseUriError> for ParseSipHeaderAddrError {
69 fn from(e: sip_uri::ParseUriError) -> Self {
70 Self(e.to_string())
71 }
72}
73
74impl From<sip_uri::ParseSipUriError> for ParseSipHeaderAddrError {
75 fn from(e: sip_uri::ParseSipUriError) -> Self {
76 Self(e.to_string())
77 }
78}
79
80impl SipHeaderAddr {
81 pub fn new(uri: sip_uri::Uri) -> Self {
83 SipHeaderAddr {
84 display_name: None,
85 uri,
86 params: Vec::new(),
87 }
88 }
89
90 pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
93 self.display_name = Some(name.into());
94 self
95 }
96
97 pub fn try_with_display_name(
116 mut self,
117 name: impl Into<String>,
118 ) -> Result<Self, ParseSipHeaderAddrError> {
119 let name = name.into();
120 if name.contains(['\r', '\n']) {
121 return Err(ParseSipHeaderAddrError(
122 "display name contains CR or LF".to_string(),
123 ));
124 }
125 self.display_name = Some(name);
126 Ok(self)
127 }
128
129 pub fn with_param(mut self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
132 self.params
133 .push((
134 key.into()
135 .to_ascii_lowercase(),
136 value.map(Into::into),
137 ));
138 self
139 }
140
141 pub fn try_with_param(
161 mut self,
162 key: impl Into<String>,
163 value: Option<impl Into<String>>,
164 ) -> Result<Self, ParseSipHeaderAddrError> {
165 let key = key.into();
166 if key.is_empty()
167 || !key
168 .chars()
169 .all(is_token_char)
170 {
171 return Err(ParseSipHeaderAddrError(
172 "parameter name is not a token".to_string(),
173 ));
174 }
175 let value = value.map(Into::into);
176 if let Some(v) = &value {
177 validate_param_value(v)?;
178 }
179 self.params
180 .push((key.to_ascii_lowercase(), value));
181 Ok(self)
182 }
183
184 pub fn display_name(&self) -> Option<&str> {
186 self.display_name
187 .as_deref()
188 }
189
190 pub fn uri(&self) -> &sip_uri::Uri {
192 &self.uri
193 }
194
195 pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
197 self.uri
198 .as_sip()
199 }
200
201 pub fn tel_uri(&self) -> Option<&sip_uri::TelUri> {
203 self.uri
204 .as_tel()
205 }
206
207 pub fn urn_uri(&self) -> Option<&sip_uri::UrnUri> {
209 self.uri
210 .as_urn()
211 }
212
213 pub fn params(&self) -> impl Iterator<Item = (&str, Option<&str>)> {
223 self.params
224 .iter()
225 .map(|(k, v)| (k.as_str(), v.as_deref()))
226 }
227
228 pub fn param(&self, name: &str) -> Option<Result<Option<Cow<'_, str>>, Utf8Error>> {
241 let needle = name.to_ascii_lowercase();
242 self.params
243 .iter()
244 .find(|(k, _)| *k == needle)
245 .map(|(_, v)| match v {
246 Some(raw) => percent_decode_str(raw)
247 .decode_utf8()
248 .map(Some),
249 None => Ok(None),
250 })
251 }
252
253 pub fn param_raw(&self, name: &str) -> Option<Option<&str>> {
258 let needle = name.to_ascii_lowercase();
259 self.params
260 .iter()
261 .find(|(k, _)| *k == needle)
262 .map(|(_, v)| v.as_deref())
263 }
264
265 pub fn replaces(&self) -> Option<Result<SipReplaces, SipReplacesError>> {
271 let value = self
272 .sip_uri()?
273 .header("Replaces")?;
274 Some(SipReplaces::parse_uri_header(value))
275 }
276
277 pub fn parse_list(raw: &str) -> Result<Vec<SipHeaderAddr>, ParseSipHeaderAddrError> {
283 if raw
284 .trim()
285 .is_empty()
286 {
287 return Ok(Vec::new());
288 }
289 crate::split_comma_entries(raw)
290 .into_iter()
291 .map(|entry| {
292 entry
293 .trim()
294 .parse()
295 })
296 .collect()
297 }
298
299 pub fn tag(&self) -> Option<&str> {
304 self.param_raw("tag")
305 .flatten()
306 }
307}
308
309fn parse_quoted_string(s: &str) -> Result<(String, &str), String> {
311 if !s.starts_with('"') {
312 return Err("expected opening quote".into());
313 }
314
315 let mut result = String::new();
316 let mut chars = s[1..].char_indices();
317
318 while let Some((i, c)) = chars.next() {
319 match c {
320 '"' => {
321 return Ok((result, &s[i + 2..]));
322 }
323 '\\' => {
324 let (_, escaped) = chars
325 .next()
326 .ok_or("unterminated escape in quoted string")?;
327 result.push(escaped);
328 }
329 _ => {
330 result.push(c);
331 }
332 }
333 }
334
335 Err("unterminated quoted string".into())
336}
337
338fn extract_angle_uri(s: &str) -> Option<(&str, &str)> {
340 let s = s.strip_prefix('<')?;
341 let end = s.find('>')?;
342 Some((&s[..end], &s[end + 1..]))
343}
344
345fn parse_header_params(s: &str) -> Vec<(String, Option<String>)> {
348 crate::parse_params(s)
349 .into_iter()
350 .map(|p| {
351 (
352 p.key
353 .to_ascii_lowercase(),
354 p.value
355 .map(str::to_string),
356 )
357 })
358 .collect()
359}
360
361fn is_token_char(c: char) -> bool {
363 c.is_ascii_alphanumeric()
364 || matches!(
365 c,
366 '-' | '.' | '!' | '%' | '*' | '_' | '+' | '`' | '\'' | '~'
367 )
368}
369
370fn needs_quoting(name: &str) -> bool {
372 !name
373 .chars()
374 .all(is_token_char)
375}
376
377fn is_quoted_string(v: &str) -> bool {
379 let Some(inner) = v.strip_prefix('"') else {
380 return false;
381 };
382 let mut chars = inner.chars();
383 while let Some(c) = chars.next() {
384 match c {
385 '"' => {
386 return chars
387 .as_str()
388 .is_empty()
389 }
390 '\\' => match chars.next() {
391 Some(e) if e.is_ascii() && !matches!(e, '\r' | '\n') => {}
392 _ => return false,
393 },
394 '\r' | '\n' => return false,
395 c if crate::is_quoted_pair_only(c) => return false,
396 _ => {}
397 }
398 }
399 false
400}
401
402fn validate_param_value(v: &str) -> Result<(), ParseSipHeaderAddrError> {
403 if v.is_empty() {
404 return Err(ParseSipHeaderAddrError(
405 "parameter value is empty".to_string(),
406 ));
407 }
408 let host_like = v
409 .chars()
410 .all(|c| is_token_char(c) || matches!(c, ':' | '[' | ']'));
411 if host_like || is_quoted_string(v) {
412 Ok(())
413 } else {
414 Err(ParseSipHeaderAddrError(format!(
415 "parameter value ({} bytes) is not a token, host or quoted-string",
416 v.len()
417 )))
418 }
419}
420
421impl FromStr for SipHeaderAddr {
422 type Err = ParseSipHeaderAddrError;
423
424 fn from_str(input: &str) -> Result<Self, Self::Err> {
425 let err = |msg: &str| ParseSipHeaderAddrError(msg.to_string());
426 let s = input.trim();
427
428 if s.is_empty() {
429 return Err(err("empty input"));
430 }
431
432 if s.starts_with('"') {
434 let (display_name, rest) = parse_quoted_string(s).map_err(|e| err(&e))?;
435 let rest = rest.trim_start();
436 let (uri_str, trailing) = extract_angle_uri(rest)
437 .ok_or_else(|| err("expected '<URI>' after quoted display name"))?;
438 let uri: sip_uri::Uri = uri_str.parse()?;
439 let display_name = if display_name.is_empty() {
440 None
441 } else {
442 Some(display_name)
443 };
444 let params = parse_header_params(trailing);
445 return Ok(SipHeaderAddr {
446 display_name,
447 uri,
448 params,
449 });
450 }
451
452 if s.starts_with('<') {
454 let (uri_str, trailing) = extract_angle_uri(s).ok_or_else(|| err("unclosed '<'"))?;
455 let uri: sip_uri::Uri = uri_str.parse()?;
456 let params = parse_header_params(trailing);
457 return Ok(SipHeaderAddr {
458 display_name: None,
459 uri,
460 params,
461 });
462 }
463
464 if let Some(angle_start) = s.find('<') {
466 let display_name = s[..angle_start].trim();
467 let display_name = if display_name.is_empty() {
468 None
469 } else {
470 Some(display_name.to_string())
471 };
472 let (uri_str, trailing) =
473 extract_angle_uri(&s[angle_start..]).ok_or_else(|| err("unclosed '<'"))?;
474 let uri: sip_uri::Uri = uri_str.parse()?;
475 let params = parse_header_params(trailing);
476 return Ok(SipHeaderAddr {
477 display_name,
478 uri,
479 params,
480 });
481 }
482
483 let uri: sip_uri::Uri = s.parse()?;
487 Ok(SipHeaderAddr {
488 display_name: None,
489 uri,
490 params: Vec::new(),
491 })
492 }
493}
494
495impl fmt::Display for SipHeaderAddr {
496 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 match self
498 .display_name
499 .as_deref()
500 {
501 Some(name) if !name.is_empty() => {
502 if needs_quoting(name) {
503 crate::write_quoted_pair(f, name)?;
504 f.write_char(' ')?;
505 } else {
506 write!(f, "{name} ")?;
507 }
508 write!(f, "<{}>", self.uri)?;
509 }
510 _ => {
511 write!(f, "<{}>", self.uri)?;
512 }
513 }
514 for (key, value) in &self.params {
515 crate::write_param(f, key, value.as_deref(), false)?;
516 }
517 Ok(())
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use std::borrow::Cow;
524
525 use super::*;
526
527 #[test]
528 fn quoted_display_name_with_tag() {
529 let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#
530 .parse()
531 .unwrap();
532 assert_eq!(addr.display_name(), Some("Alice"));
533 assert!(addr
534 .sip_uri()
535 .is_some());
536 assert_eq!(addr.tag(), Some("abc123"));
537 }
538
539 #[test]
540 fn angle_bracket_no_name_multiple_params() {
541 let addr: SipHeaderAddr = "<sip:user@host>;tag=xyz;expires=3600"
542 .parse()
543 .unwrap();
544 assert_eq!(addr.display_name(), None);
545 assert_eq!(addr.tag(), Some("xyz"));
546 assert_eq!(
547 addr.param("expires")
548 .unwrap()
549 .unwrap(),
550 Some(Cow::from("3600")),
551 );
552 }
553
554 #[test]
555 fn bare_addr_spec_no_params() {
556 let addr: SipHeaderAddr = "sip:user@host"
557 .parse()
558 .unwrap();
559 assert_eq!(addr.display_name(), None);
560 assert!(addr
561 .sip_uri()
562 .is_some());
563 assert_eq!(
564 addr.params()
565 .count(),
566 0
567 );
568 }
569
570 #[test]
571 fn unquoted_display_name_with_params() {
572 let addr: SipHeaderAddr = "Alice <sip:alice@example.com>;tag=abc"
573 .parse()
574 .unwrap();
575 assert_eq!(addr.display_name(), Some("Alice"));
576 assert_eq!(addr.tag(), Some("abc"));
577 }
578
579 #[test]
580 fn ng911_refer_to_serviceurn() {
581 let input = "<sip:user@esrp.example.com?Call-Info=x>;serviceurn=urn%3Aservice%3Apolice";
582 let addr: SipHeaderAddr = input
583 .parse()
584 .unwrap();
585 assert_eq!(addr.display_name(), None);
586 assert_eq!(
587 addr.param("serviceurn")
588 .unwrap()
589 .unwrap(),
590 Some(Cow::from("urn:service:police")),
591 );
592 assert_eq!(
593 addr.param_raw("serviceurn"),
594 Some(Some("urn%3Aservice%3Apolice")),
595 );
596 let sip = addr
597 .sip_uri()
598 .unwrap();
599 assert_eq!(
600 sip.host()
601 .to_string(),
602 "esrp.example.com"
603 );
604 }
605
606 #[test]
607 fn p_asserted_identity_uri_params_no_header_params() {
608 let input = r#""EXAMPLE CO" <sip:+15551234567;cpc=emergency@198.51.100.1;user=phone>"#;
609 let addr: SipHeaderAddr = input
610 .parse()
611 .unwrap();
612 assert_eq!(addr.display_name(), Some("EXAMPLE CO"));
613 assert_eq!(
614 addr.params()
615 .count(),
616 0
617 );
618 let sip = addr
619 .sip_uri()
620 .unwrap();
621 assert_eq!(sip.user(), Some("+15551234567"));
622 assert_eq!(sip.param("user"), Some(&Some("phone".to_string())));
623 }
624
625 #[test]
626 fn tel_uri_with_header_params() {
627 let addr: SipHeaderAddr = "<tel:+15551234567>;expires=3600"
628 .parse()
629 .unwrap();
630 assert!(addr
631 .tel_uri()
632 .is_some());
633 assert_eq!(
634 addr.param("expires")
635 .unwrap()
636 .unwrap(),
637 Some(Cow::from("3600")),
638 );
639 }
640
641 #[test]
642 fn flag_param_no_value() {
643 let addr: SipHeaderAddr = "<sip:user@host>;lr;tag=abc"
644 .parse()
645 .unwrap();
646 assert_eq!(
647 addr.param("lr")
648 .unwrap()
649 .unwrap(),
650 None
651 );
652 assert_eq!(addr.tag(), Some("abc"));
653 }
654
655 #[test]
656 fn urn_uri_no_params() {
657 let addr: SipHeaderAddr = "<urn:service:sos>"
658 .parse()
659 .unwrap();
660 assert!(addr
661 .urn_uri()
662 .is_some());
663 assert_eq!(
664 addr.params()
665 .count(),
666 0
667 );
668 }
669
670 #[test]
671 fn empty_input_fails() {
672 assert!(""
673 .parse::<SipHeaderAddr>()
674 .is_err());
675 }
676
677 #[test]
678 fn display_roundtrip_quoted_name_with_params() {
679 let input = r#""Alice" <sip:alice@example.com>;tag=abc123"#;
681 let addr: SipHeaderAddr = input
682 .parse()
683 .unwrap();
684 assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
685 }
686
687 #[test]
688 fn display_roundtrip_name_requiring_quotes() {
689 let input = r#""Alice Smith" <sip:alice@example.com>;tag=abc123"#;
690 let addr: SipHeaderAddr = input
691 .parse()
692 .unwrap();
693 assert_eq!(addr.to_string(), input);
694 }
695
696 #[test]
697 fn display_roundtrip_no_name_with_params() {
698 let input = "<sip:user@host>;tag=xyz;expires=3600";
699 let addr: SipHeaderAddr = input
700 .parse()
701 .unwrap();
702 assert_eq!(addr.to_string(), input);
703 }
704
705 #[test]
706 fn display_roundtrip_bare_uri() {
707 let input = "sip:user@host";
708 let addr: SipHeaderAddr = input
709 .parse()
710 .unwrap();
711 assert_eq!(addr.to_string(), "<sip:user@host>");
713 }
714
715 #[test]
716 fn display_roundtrip_flag_param() {
717 let input = "<sip:user@host>;lr;tag=abc";
718 let addr: SipHeaderAddr = input
719 .parse()
720 .unwrap();
721 assert_eq!(addr.to_string(), input);
722 }
723
724 #[test]
725 fn case_insensitive_param_lookup() {
726 let addr: SipHeaderAddr = "<sip:user@host>;Tag=ABC;Expires=3600"
727 .parse()
728 .unwrap();
729 assert_eq!(
730 addr.param("tag")
731 .unwrap()
732 .unwrap(),
733 Some(Cow::from("ABC")),
734 );
735 assert_eq!(
736 addr.param("TAG")
737 .unwrap()
738 .unwrap(),
739 Some(Cow::from("ABC")),
740 );
741 assert_eq!(
742 addr.param("expires")
743 .unwrap()
744 .unwrap(),
745 Some(Cow::from("3600")),
746 );
747 }
748
749 #[test]
750 fn tag_convenience_accessor() {
751 let with_tag: SipHeaderAddr = "<sip:user@host>;tag=xyz"
752 .parse()
753 .unwrap();
754 assert_eq!(with_tag.tag(), Some("xyz"));
755
756 let without_tag: SipHeaderAddr = "<sip:user@host>"
757 .parse()
758 .unwrap();
759 assert_eq!(without_tag.tag(), None);
760 }
761
762 #[test]
763 fn builder_new() {
764 let uri: sip_uri::Uri = "sip:alice@example.com"
765 .parse()
766 .unwrap();
767 let addr = SipHeaderAddr::new(uri);
768 assert_eq!(addr.display_name(), None);
769 assert_eq!(
770 addr.params()
771 .count(),
772 0
773 );
774 assert_eq!(addr.to_string(), "<sip:alice@example.com>");
775 }
776
777 fn example_addr() -> SipHeaderAddr {
778 SipHeaderAddr::new(
779 "sip:alice@example.com"
780 .parse()
781 .unwrap(),
782 )
783 }
784
785 #[test]
786 fn try_builder_display_name_and_params() {
787 let addr = example_addr()
788 .try_with_display_name("Alice")
789 .unwrap()
790 .try_with_param("Tag", Some("abc123"))
791 .unwrap()
792 .try_with_param("lr", None::<String>)
793 .unwrap();
794 assert_eq!(addr.display_name(), Some("Alice"));
795 assert_eq!(addr.tag(), Some("abc123"));
796 assert_eq!(
797 addr.to_string(),
798 "Alice <sip:alice@example.com>;tag=abc123;lr"
799 );
800 }
801
802 #[test]
803 fn try_with_display_name_accepts_qdtext_and_quoted_pair() {
804 for name in [
805 "José",
806 r#"Say "Hi""#,
807 "a\\b",
808 "a\tb",
809 "a\u{1}b",
810 "a\u{7f}b",
811 "",
812 ] {
813 let addr = example_addr()
814 .try_with_display_name(name)
815 .unwrap();
816 let reparsed: SipHeaderAddr = addr
817 .to_string()
818 .parse()
819 .unwrap();
820 let expected = if name.is_empty() { None } else { Some(name) };
821 assert_eq!(reparsed.display_name(), expected, "{name:?}");
822 }
823 }
824
825 #[test]
826 fn display_escapes_control_chars_as_quoted_pair() {
827 let addr = example_addr()
828 .try_with_display_name("a\u{1}b")
829 .unwrap();
830 assert_eq!(addr.to_string(), "\"a\\\u{1}b\" <sip:alice@example.com>");
831 }
832
833 #[test]
834 fn try_with_display_name_rejects_line_breaks() {
835 for name in ["a\r\nSubject: evil", "a\nSubject: evil", "a\rSubject: evil"] {
836 let e = example_addr()
837 .try_with_display_name(name)
838 .unwrap_err();
839 assert!(
840 !e.to_string()
841 .contains("Subject"),
842 "{e}"
843 );
844 }
845 }
846
847 #[test]
848 fn try_with_param_accepts_token_host_and_quoted() {
849 for (key, value) in [
850 ("tag", Some("abc123")),
851 ("lr", None),
852 ("serviceurn", Some("urn%3Aservice%3Apolice")),
853 ("received", Some("[2001:db8::1]")),
854 ("maddr", Some("198.51.100.1:5060")),
855 ("foo", Some(r#""a;b, c""#)),
856 ("+sip.instance", Some(r#""<urn:uuid:TEST>""#)),
857 ("t", Some(r#""say \"hi\"""#)),
858 ("e", Some(r#""""#)),
859 ] {
860 let addr = example_addr()
861 .try_with_param(key, value)
862 .unwrap();
863 assert_eq!(addr.param_raw(key), Some(value), "{key}");
864 let reparsed: SipHeaderAddr = addr
865 .to_string()
866 .parse()
867 .unwrap();
868 assert_eq!(reparsed, addr, "{key}");
869 }
870 }
871
872 #[test]
873 fn try_with_param_rejects_bad_key() {
874 for key in ["", "a b", "a;b", "a=b", "a\r\nSubject: evil", "a\"b"] {
875 let e = example_addr()
876 .try_with_param(key, Some("x"))
877 .unwrap_err();
878 assert!(
879 !e.to_string()
880 .contains("Subject"),
881 "{e}"
882 );
883 }
884 }
885
886 #[test]
887 fn try_with_param_rejects_bad_value() {
888 for value in [
889 "",
890 "a;b",
891 "a b",
892 "a,b",
893 "a>b",
894 "\"unterminated",
895 "\"a\"b\"",
896 "\"a\\\"",
897 "\"a\r\nb\"",
898 "x\r\nSubject: evil",
899 "\"a\\\nb\"",
900 ] {
901 let e = example_addr()
902 .try_with_param("k", Some(value))
903 .unwrap_err();
904 assert!(
905 !e.to_string()
906 .contains("Subject"),
907 "{e}"
908 );
909 }
910 }
911
912 #[test]
913 fn builder_with_display_name_and_params() {
914 let uri: sip_uri::Uri = "sip:alice@example.com"
915 .parse()
916 .unwrap();
917 let addr = SipHeaderAddr::new(uri)
918 .with_display_name("Alice")
919 .with_param("tag", Some("abc123"));
920 assert_eq!(addr.display_name(), Some("Alice"));
921 assert_eq!(addr.tag(), Some("abc123"));
922 assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
923 }
924
925 #[test]
926 fn builder_flag_param() {
927 let uri: sip_uri::Uri = "sip:proxy@example.com"
928 .parse()
929 .unwrap();
930 let addr = SipHeaderAddr::new(uri).with_param("lr", None::<String>);
931 assert_eq!(
932 addr.param("lr")
933 .unwrap()
934 .unwrap(),
935 None
936 );
937 assert_eq!(addr.to_string(), "<sip:proxy@example.com>;lr");
938 }
939
940 #[test]
941 fn escaped_quotes_in_display_name() {
942 let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
943 let addr: SipHeaderAddr = input
944 .parse()
945 .unwrap();
946 assert_eq!(addr.display_name(), Some(r#"Say "Hello""#));
947 assert_eq!(addr.tag(), Some("t"));
948 }
949
950 #[test]
951 fn display_roundtrip_escaped_quotes() {
952 let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
953 let addr: SipHeaderAddr = input
954 .parse()
955 .unwrap();
956 assert_eq!(addr.to_string(), input);
957 }
958
959 #[test]
960 fn trailing_semicolon_ignored() {
961 let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;"
962 .parse()
963 .unwrap();
964 assert_eq!(
965 addr.params()
966 .count(),
967 1
968 );
969 assert_eq!(addr.tag(), Some("abc"));
970 }
971
972 #[test]
973 fn display_roundtrip_percent_encoded_params() {
974 let input = "<sip:user@esrp.example.com>;serviceurn=urn%3Aservice%3Apolice";
975 let addr: SipHeaderAddr = input
976 .parse()
977 .unwrap();
978 assert_eq!(addr.to_string(), input);
979 }
980
981 #[test]
982 fn param_invalid_utf8_returns_err() {
983 let addr: SipHeaderAddr = "<sip:user@host>;data=%C0%80"
985 .parse()
986 .unwrap();
987 assert!(addr
988 .param("data")
989 .unwrap()
990 .is_err());
991 assert_eq!(addr.param_raw("data"), Some(Some("%C0%80")));
992 }
993
994 #[test]
995 fn param_iso_8859_fallback_to_raw() {
996 let addr: SipHeaderAddr = "<sip:user@host>;name=%E9"
998 .parse()
999 .unwrap();
1000 assert!(addr
1001 .param("name")
1002 .unwrap()
1003 .is_err());
1004 assert_eq!(addr.param_raw("name"), Some(Some("%E9")));
1005 }
1006
1007 #[test]
1008 fn parse_list_multiple_entries() {
1009 let input = r#""Alice" <sip:alice@example.com>;tag=a, <sip:bob@example.com>, sip:carol@example.com"#;
1010 let addrs = SipHeaderAddr::parse_list(input).unwrap();
1011 assert_eq!(addrs.len(), 3);
1012 assert_eq!(addrs[0].display_name(), Some("Alice"));
1013 assert_eq!(addrs[0].tag(), Some("a"));
1014 assert_eq!(addrs[1].display_name(), None);
1015 assert_eq!(
1016 addrs[1]
1017 .sip_uri()
1018 .unwrap()
1019 .user(),
1020 Some("bob"),
1021 );
1022 assert_eq!(
1023 addrs[2]
1024 .sip_uri()
1025 .unwrap()
1026 .user(),
1027 Some("carol"),
1028 );
1029 }
1030
1031 #[test]
1032 fn parse_list_single_entry() {
1033 let addrs = SipHeaderAddr::parse_list("<sip:alice@example.com>").unwrap();
1034 assert_eq!(addrs.len(), 1);
1035 }
1036
1037 #[test]
1038 fn parse_list_empty_returns_empty() {
1039 let addrs = SipHeaderAddr::parse_list("").unwrap();
1040 assert!(addrs.is_empty());
1041 }
1042
1043 #[test]
1044 fn parse_list_propagates_parse_error() {
1045 assert!(SipHeaderAddr::parse_list("not-a-uri, <sip:ok@example.com>").is_err());
1046 }
1047
1048 #[test]
1049 fn replaces_uri_header() {
1050 let addr: SipHeaderAddr =
1051 "<sip:bob@203.0.113.9?Replaces=abc123%40203.0.113.5%3Bto-tag%3Dt1%3Bfrom-tag%3Df1>"
1052 .parse()
1053 .unwrap();
1054 let r = addr
1055 .replaces()
1056 .unwrap()
1057 .unwrap();
1058 assert_eq!(r.call_id(), "abc123@203.0.113.5");
1059 assert_eq!(r.host(), Some("203.0.113.5"));
1060 assert_eq!(r.to_tag(), "t1");
1061 assert_eq!(r.from_tag(), "f1");
1062 }
1063
1064 #[test]
1065 fn replaces_uri_header_absent() {
1066 let addr: SipHeaderAddr = "<sip:bob@203.0.113.9>"
1067 .parse()
1068 .unwrap();
1069 assert!(addr
1070 .replaces()
1071 .is_none());
1072 }
1073
1074 #[test]
1075 fn host_only_ipv4_with_feature_tag_param() {
1076 let input =
1077 "<sip:198.51.100.7:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
1078 let addr: SipHeaderAddr = input
1079 .parse()
1080 .unwrap();
1081 let sip = addr
1082 .sip_uri()
1083 .unwrap();
1084 assert_eq!(sip.user(), None);
1085 assert_eq!(
1086 sip.host()
1087 .to_string(),
1088 "198.51.100.7"
1089 );
1090 assert_eq!(sip.port(), Some(5060));
1091 assert_eq!(sip.param("transport"), Some(&Some("udp".to_string())));
1092 assert_eq!(
1093 addr.param_raw("+urn%3Aemergency%3Amedia-feature.psap-call-control"),
1094 Some(None),
1095 );
1096 }
1097
1098 #[test]
1099 fn host_only_ipv6_with_feature_tag_param() {
1100 let input =
1101 "<sip:[2001:db8::7]:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
1102 let addr: SipHeaderAddr = input
1103 .parse()
1104 .unwrap();
1105 let sip = addr
1106 .sip_uri()
1107 .unwrap();
1108 assert_eq!(sip.user(), None);
1109 assert_eq!(
1110 sip.host()
1111 .to_string(),
1112 "[2001:db8::7]"
1113 );
1114 assert_eq!(sip.port(), Some(5060));
1115 assert_eq!(
1116 addr.param_raw("+urn%3Aemergency%3Amedia-feature.psap-call-control"),
1117 Some(None),
1118 );
1119 }
1120
1121 #[test]
1122 fn parse_list_host_only_ipv4_with_feature_tag_param() {
1123 let input =
1124 "<sip:198.51.100.7:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
1125 let addrs = SipHeaderAddr::parse_list(input).unwrap();
1126 assert_eq!(addrs.len(), 1);
1127 assert_eq!(
1128 addrs[0]
1129 .sip_uri()
1130 .unwrap()
1131 .host()
1132 .to_string(),
1133 "198.51.100.7"
1134 );
1135 }
1136
1137 #[test]
1138 fn display_quotes_non_token_display_name() {
1139 for name in ["José", "A/B", "a=b", "x(y)", "a?b"] {
1140 let input = format!(r#""{name}" <sip:a@example.com>"#);
1141 let addr: SipHeaderAddr = input
1142 .parse()
1143 .unwrap();
1144 assert_eq!(addr.to_string(), input);
1145 }
1146 }
1147
1148 #[test]
1149 fn display_keeps_token_display_name_bare() {
1150 let input = "A-b.c!%*_+`'~ <sip:a@example.com>";
1151 let addr: SipHeaderAddr = input
1152 .parse()
1153 .unwrap();
1154 assert_eq!(addr.to_string(), input);
1155 }
1156
1157 #[test]
1158 fn params_sws_around_semi_and_equal() {
1159 let addr: SipHeaderAddr = "<sip:a@example.com> ; tag = x"
1160 .parse()
1161 .unwrap();
1162 assert_eq!(addr.tag(), Some("x"));
1163 assert_eq!(
1164 addr.params()
1165 .collect::<Vec<_>>(),
1166 vec![("tag", Some("x"))]
1167 );
1168 }
1169
1170 #[test]
1171 fn params_quoted_value_keeps_semicolon() {
1172 let input = r#"<sip:a@example.com>;foo="a;b";tag=x"#;
1173 let addr: SipHeaderAddr = input
1174 .parse()
1175 .unwrap();
1176 assert_eq!(addr.param_raw("foo"), Some(Some(r#""a;b""#)));
1177 assert_eq!(addr.tag(), Some("x"));
1178 assert_eq!(addr.to_string(), input);
1179 }
1180
1181 #[test]
1182 fn params_quoted_instance_kept_raw() {
1183 let input = r#"<sip:a@198.51.100.1>;+sip.instance="<urn:uuid:00000000-0000-0000-0000-000000000001>";expires=60"#;
1184 let addr: SipHeaderAddr = input
1185 .parse()
1186 .unwrap();
1187 assert_eq!(
1188 addr.param_raw("+sip.instance"),
1189 Some(Some(r#""<urn:uuid:00000000-0000-0000-0000-000000000001>""#))
1190 );
1191 assert_eq!(addr.to_string(), input);
1192 }
1193
1194 #[test]
1195 fn params_iterator() {
1196 let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;lr;expires=60"
1197 .parse()
1198 .unwrap();
1199 let params: Vec<_> = addr
1200 .params()
1201 .collect();
1202 assert_eq!(params.len(), 3);
1203 assert_eq!(params[0], ("tag", Some("abc")));
1204 assert_eq!(params[1], ("lr", None));
1205 assert_eq!(params[2], ("expires", Some("60")));
1206 }
1207}