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 tag(&self) -> Option<&str> {
180 self.param_raw("tag")
181 .flatten()
182 }
183}
184
185fn parse_quoted_string(s: &str) -> Result<(String, &str), String> {
187 if !s.starts_with('"') {
188 return Err("expected opening quote".into());
189 }
190
191 let mut result = String::new();
192 let mut chars = s[1..].char_indices();
193
194 while let Some((i, c)) = chars.next() {
195 match c {
196 '"' => {
197 return Ok((result, &s[i + 2..]));
198 }
199 '\\' => {
200 let (_, escaped) = chars
201 .next()
202 .ok_or("unterminated escape in quoted string")?;
203 result.push(escaped);
204 }
205 _ => {
206 result.push(c);
207 }
208 }
209 }
210
211 Err("unterminated quoted string".into())
212}
213
214fn extract_angle_uri(s: &str) -> Option<(&str, &str)> {
216 let s = s.strip_prefix('<')?;
217 let end = s.find('>')?;
218 Some((&s[..end], &s[end + 1..]))
219}
220
221fn parse_header_params(s: &str) -> Vec<(String, Option<String>)> {
224 let mut params = Vec::new();
225 for segment in s.split(';') {
226 if segment.is_empty() {
227 continue;
228 }
229 if let Some((key, value)) = segment.split_once('=') {
230 params.push((key.to_ascii_lowercase(), Some(value.to_string())));
231 } else {
232 params.push((segment.to_ascii_lowercase(), None));
233 }
234 }
235 params
236}
237
238fn needs_quoting(name: &str) -> bool {
240 name.bytes()
241 .any(|b| {
242 matches!(
243 b,
244 b'"' | b'\\' | b'<' | b'>' | b',' | b';' | b':' | b'@' | b' ' | b'\t'
245 )
246 })
247}
248
249fn escape_display_name(name: &str) -> String {
251 let mut out = String::with_capacity(name.len());
252 for c in name.chars() {
253 if matches!(c, '"' | '\\') {
254 out.push('\\');
255 }
256 out.push(c);
257 }
258 out
259}
260
261impl FromStr for SipHeaderAddr {
262 type Err = ParseSipHeaderAddrError;
263
264 fn from_str(input: &str) -> Result<Self, Self::Err> {
265 let err = |msg: &str| ParseSipHeaderAddrError(msg.to_string());
266 let s = input.trim();
267
268 if s.is_empty() {
269 return Err(err("empty input"));
270 }
271
272 if s.starts_with('"') {
274 let (display_name, rest) = parse_quoted_string(s).map_err(|e| err(&e))?;
275 let rest = rest.trim_start();
276 let (uri_str, trailing) = extract_angle_uri(rest)
277 .ok_or_else(|| err("expected '<URI>' after quoted display name"))?;
278 let uri: sip_uri::Uri = uri_str.parse()?;
279 let display_name = if display_name.is_empty() {
280 None
281 } else {
282 Some(display_name)
283 };
284 let params = parse_header_params(trailing);
285 return Ok(SipHeaderAddr {
286 display_name,
287 uri,
288 params,
289 });
290 }
291
292 if s.starts_with('<') {
294 let (uri_str, trailing) = extract_angle_uri(s).ok_or_else(|| err("unclosed '<'"))?;
295 let uri: sip_uri::Uri = uri_str.parse()?;
296 let params = parse_header_params(trailing);
297 return Ok(SipHeaderAddr {
298 display_name: None,
299 uri,
300 params,
301 });
302 }
303
304 if let Some(angle_start) = s.find('<') {
306 let display_name = s[..angle_start].trim();
307 let display_name = if display_name.is_empty() {
308 None
309 } else {
310 Some(display_name.to_string())
311 };
312 let (uri_str, trailing) =
313 extract_angle_uri(&s[angle_start..]).ok_or_else(|| err("unclosed '<'"))?;
314 let uri: sip_uri::Uri = uri_str.parse()?;
315 let params = parse_header_params(trailing);
316 return Ok(SipHeaderAddr {
317 display_name,
318 uri,
319 params,
320 });
321 }
322
323 let uri: sip_uri::Uri = s.parse()?;
327 Ok(SipHeaderAddr {
328 display_name: None,
329 uri,
330 params: Vec::new(),
331 })
332 }
333}
334
335impl fmt::Display for SipHeaderAddr {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337 match self
338 .display_name
339 .as_deref()
340 {
341 Some(name) if !name.is_empty() => {
342 if needs_quoting(name) {
343 write!(f, "\"{}\" ", escape_display_name(name))?;
344 } else {
345 write!(f, "{name} ")?;
346 }
347 write!(f, "<{}>", self.uri)?;
348 }
349 _ => {
350 write!(f, "<{}>", self.uri)?;
351 }
352 }
353 for (key, value) in &self.params {
354 match value {
355 Some(v) => write!(f, ";{key}={v}")?,
356 None => write!(f, ";{key}")?,
357 }
358 }
359 Ok(())
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use std::borrow::Cow;
366
367 use super::*;
368
369 #[test]
370 fn quoted_display_name_with_tag() {
371 let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#
372 .parse()
373 .unwrap();
374 assert_eq!(addr.display_name(), Some("Alice"));
375 assert!(addr
376 .sip_uri()
377 .is_some());
378 assert_eq!(addr.tag(), Some("abc123"));
379 }
380
381 #[test]
382 fn angle_bracket_no_name_multiple_params() {
383 let addr: SipHeaderAddr = "<sip:user@host>;tag=xyz;expires=3600"
384 .parse()
385 .unwrap();
386 assert_eq!(addr.display_name(), None);
387 assert_eq!(addr.tag(), Some("xyz"));
388 assert_eq!(
389 addr.param("expires")
390 .unwrap()
391 .unwrap(),
392 Some(Cow::from("3600")),
393 );
394 }
395
396 #[test]
397 fn bare_addr_spec_no_params() {
398 let addr: SipHeaderAddr = "sip:user@host"
399 .parse()
400 .unwrap();
401 assert_eq!(addr.display_name(), None);
402 assert!(addr
403 .sip_uri()
404 .is_some());
405 assert_eq!(
406 addr.params()
407 .count(),
408 0
409 );
410 }
411
412 #[test]
413 fn unquoted_display_name_with_params() {
414 let addr: SipHeaderAddr = "Alice <sip:alice@example.com>;tag=abc"
415 .parse()
416 .unwrap();
417 assert_eq!(addr.display_name(), Some("Alice"));
418 assert_eq!(addr.tag(), Some("abc"));
419 }
420
421 #[test]
422 fn ng911_refer_to_serviceurn() {
423 let input = "<sip:user@esrp.example.com?Call-Info=x>;serviceurn=urn%3Aservice%3Apolice";
424 let addr: SipHeaderAddr = input
425 .parse()
426 .unwrap();
427 assert_eq!(addr.display_name(), None);
428 assert_eq!(
429 addr.param("serviceurn")
430 .unwrap()
431 .unwrap(),
432 Some(Cow::from("urn:service:police")),
433 );
434 assert_eq!(
435 addr.param_raw("serviceurn"),
436 Some(Some("urn%3Aservice%3Apolice")),
437 );
438 let sip = addr
439 .sip_uri()
440 .unwrap();
441 assert_eq!(
442 sip.host()
443 .to_string(),
444 "esrp.example.com"
445 );
446 }
447
448 #[test]
449 fn p_asserted_identity_uri_params_no_header_params() {
450 let input = r#""EXAMPLE CO" <sip:+15551234567;cpc=emergency@198.51.100.1;user=phone>"#;
451 let addr: SipHeaderAddr = input
452 .parse()
453 .unwrap();
454 assert_eq!(addr.display_name(), Some("EXAMPLE CO"));
455 assert_eq!(
456 addr.params()
457 .count(),
458 0
459 );
460 let sip = addr
461 .sip_uri()
462 .unwrap();
463 assert_eq!(sip.user(), Some("+15551234567"));
464 assert_eq!(sip.param("user"), Some(&Some("phone".to_string())));
465 }
466
467 #[test]
468 fn tel_uri_with_header_params() {
469 let addr: SipHeaderAddr = "<tel:+15551234567>;expires=3600"
470 .parse()
471 .unwrap();
472 assert!(addr
473 .tel_uri()
474 .is_some());
475 assert_eq!(
476 addr.param("expires")
477 .unwrap()
478 .unwrap(),
479 Some(Cow::from("3600")),
480 );
481 }
482
483 #[test]
484 fn flag_param_no_value() {
485 let addr: SipHeaderAddr = "<sip:user@host>;lr;tag=abc"
486 .parse()
487 .unwrap();
488 assert_eq!(
489 addr.param("lr")
490 .unwrap()
491 .unwrap(),
492 None
493 );
494 assert_eq!(addr.tag(), Some("abc"));
495 }
496
497 #[test]
498 fn urn_uri_no_params() {
499 let addr: SipHeaderAddr = "<urn:service:sos>"
500 .parse()
501 .unwrap();
502 assert!(addr
503 .urn_uri()
504 .is_some());
505 assert_eq!(
506 addr.params()
507 .count(),
508 0
509 );
510 }
511
512 #[test]
513 fn empty_input_fails() {
514 assert!(""
515 .parse::<SipHeaderAddr>()
516 .is_err());
517 }
518
519 #[test]
520 fn display_roundtrip_quoted_name_with_params() {
521 let input = r#""Alice" <sip:alice@example.com>;tag=abc123"#;
523 let addr: SipHeaderAddr = input
524 .parse()
525 .unwrap();
526 assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
527 }
528
529 #[test]
530 fn display_roundtrip_name_requiring_quotes() {
531 let input = r#""Alice Smith" <sip:alice@example.com>;tag=abc123"#;
532 let addr: SipHeaderAddr = input
533 .parse()
534 .unwrap();
535 assert_eq!(addr.to_string(), input);
536 }
537
538 #[test]
539 fn display_roundtrip_no_name_with_params() {
540 let input = "<sip:user@host>;tag=xyz;expires=3600";
541 let addr: SipHeaderAddr = input
542 .parse()
543 .unwrap();
544 assert_eq!(addr.to_string(), input);
545 }
546
547 #[test]
548 fn display_roundtrip_bare_uri() {
549 let input = "sip:user@host";
550 let addr: SipHeaderAddr = input
551 .parse()
552 .unwrap();
553 assert_eq!(addr.to_string(), "<sip:user@host>");
555 }
556
557 #[test]
558 fn display_roundtrip_flag_param() {
559 let input = "<sip:user@host>;lr;tag=abc";
560 let addr: SipHeaderAddr = input
561 .parse()
562 .unwrap();
563 assert_eq!(addr.to_string(), input);
564 }
565
566 #[test]
567 fn case_insensitive_param_lookup() {
568 let addr: SipHeaderAddr = "<sip:user@host>;Tag=ABC;Expires=3600"
569 .parse()
570 .unwrap();
571 assert_eq!(
572 addr.param("tag")
573 .unwrap()
574 .unwrap(),
575 Some(Cow::from("ABC")),
576 );
577 assert_eq!(
578 addr.param("TAG")
579 .unwrap()
580 .unwrap(),
581 Some(Cow::from("ABC")),
582 );
583 assert_eq!(
584 addr.param("expires")
585 .unwrap()
586 .unwrap(),
587 Some(Cow::from("3600")),
588 );
589 }
590
591 #[test]
592 fn tag_convenience_accessor() {
593 let with_tag: SipHeaderAddr = "<sip:user@host>;tag=xyz"
594 .parse()
595 .unwrap();
596 assert_eq!(with_tag.tag(), Some("xyz"));
597
598 let without_tag: SipHeaderAddr = "<sip:user@host>"
599 .parse()
600 .unwrap();
601 assert_eq!(without_tag.tag(), None);
602 }
603
604 #[test]
605 fn builder_new() {
606 let uri: sip_uri::Uri = "sip:alice@example.com"
607 .parse()
608 .unwrap();
609 let addr = SipHeaderAddr::new(uri);
610 assert_eq!(addr.display_name(), None);
611 assert_eq!(
612 addr.params()
613 .count(),
614 0
615 );
616 assert_eq!(addr.to_string(), "<sip:alice@example.com>");
617 }
618
619 #[test]
620 fn builder_with_display_name_and_params() {
621 let uri: sip_uri::Uri = "sip:alice@example.com"
622 .parse()
623 .unwrap();
624 let addr = SipHeaderAddr::new(uri)
625 .with_display_name("Alice")
626 .with_param("tag", Some("abc123"));
627 assert_eq!(addr.display_name(), Some("Alice"));
628 assert_eq!(addr.tag(), Some("abc123"));
629 assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
630 }
631
632 #[test]
633 fn builder_flag_param() {
634 let uri: sip_uri::Uri = "sip:proxy@example.com"
635 .parse()
636 .unwrap();
637 let addr = SipHeaderAddr::new(uri).with_param("lr", None::<String>);
638 assert_eq!(
639 addr.param("lr")
640 .unwrap()
641 .unwrap(),
642 None
643 );
644 assert_eq!(addr.to_string(), "<sip:proxy@example.com>;lr");
645 }
646
647 #[test]
648 fn escaped_quotes_in_display_name() {
649 let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
650 let addr: SipHeaderAddr = input
651 .parse()
652 .unwrap();
653 assert_eq!(addr.display_name(), Some(r#"Say "Hello""#));
654 assert_eq!(addr.tag(), Some("t"));
655 }
656
657 #[test]
658 fn display_roundtrip_escaped_quotes() {
659 let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
660 let addr: SipHeaderAddr = input
661 .parse()
662 .unwrap();
663 assert_eq!(addr.to_string(), input);
664 }
665
666 #[test]
667 fn trailing_semicolon_ignored() {
668 let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;"
669 .parse()
670 .unwrap();
671 assert_eq!(
672 addr.params()
673 .count(),
674 1
675 );
676 assert_eq!(addr.tag(), Some("abc"));
677 }
678
679 #[test]
680 fn display_roundtrip_percent_encoded_params() {
681 let input = "<sip:user@esrp.example.com>;serviceurn=urn%3Aservice%3Apolice";
682 let addr: SipHeaderAddr = input
683 .parse()
684 .unwrap();
685 assert_eq!(addr.to_string(), input);
686 }
687
688 #[test]
689 fn param_invalid_utf8_returns_err() {
690 let addr: SipHeaderAddr = "<sip:user@host>;data=%C0%80"
692 .parse()
693 .unwrap();
694 assert!(addr
695 .param("data")
696 .unwrap()
697 .is_err());
698 assert_eq!(addr.param_raw("data"), Some(Some("%C0%80")));
699 }
700
701 #[test]
702 fn param_iso_8859_fallback_to_raw() {
703 let addr: SipHeaderAddr = "<sip:user@host>;name=%E9"
705 .parse()
706 .unwrap();
707 assert!(addr
708 .param("name")
709 .unwrap()
710 .is_err());
711 assert_eq!(addr.param_raw("name"), Some(Some("%E9")));
712 }
713
714 #[test]
715 fn params_iterator() {
716 let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;lr;expires=60"
717 .parse()
718 .unwrap();
719 let params: Vec<_> = addr
720 .params()
721 .collect();
722 assert_eq!(params.len(), 3);
723 assert_eq!(params[0], ("tag", Some("abc")));
724 assert_eq!(params[1], ("lr", None));
725 assert_eq!(params[2], ("expires", Some("60")));
726 }
727}