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