Skip to main content

freeswitch_types/
sip_header_addr.rs

1//! RFC 3261 `name-addr` parser with header-level parameter support.
2
3use std::borrow::Cow;
4use std::fmt;
5use std::str::{FromStr, Utf8Error};
6
7use percent_encoding::percent_decode_str;
8
9/// Parsed SIP `name-addr` (RFC 3261 §25.1) with header-level parameters.
10///
11/// The `name-addr` production from RFC 3261 §25.1 combines an optional
12/// display name with a URI in angle brackets:
13///
14/// ```text
15/// name-addr      = [ display-name ] LAQUOT addr-spec RAQUOT
16/// display-name   = *(token LWS) / quoted-string
17/// ```
18///
19/// In SIP headers (`From`, `To`, `Contact`, `P-Asserted-Identity`,
20/// `Refer-To`), the `name-addr` is followed by header-level parameters
21/// (RFC 3261 §20):
22///
23/// ```text
24/// from-spec  = ( name-addr / addr-spec ) *( SEMI from-param )
25/// from-param = tag-param / generic-param
26/// ```
27///
28/// This type handles the full production including those trailing
29/// parameters (`;tag=`, `;expires=`, `;serviceurn=`, etc.).
30///
31/// Replaces [`sip_uri::NameAddr`] which was deprecated in sip-uri 0.2.0
32/// because it only parsed the `name-addr` portion and rejected header-level
33/// parameters after `>`, making it unable to round-trip real SIP header
34/// values.
35///
36/// ```
37/// use freeswitch_types::SipHeaderAddr;
38///
39/// let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#.parse().unwrap();
40/// assert_eq!(addr.display_name(), Some("Alice"));
41/// assert_eq!(addr.tag(), Some("abc123"));
42/// ```
43///
44/// [`Display`](std::fmt::Display) always emits angle brackets around the URI,
45/// even for bare addr-spec input. This is the canonical form required by
46/// RFC 3261 when header-level parameters are present.
47#[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/// Error returned when parsing a SIP header address value fails.
56#[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    /// Create a new `SipHeaderAddr` with the given URI and no display name or params.
81    pub fn new(uri: sip_uri::Uri) -> Self {
82        SipHeaderAddr {
83            display_name: None,
84            uri,
85            params: Vec::new(),
86        }
87    }
88
89    /// Set the display name.
90    pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
91        self.display_name = Some(name.into());
92        self
93    }
94
95    /// Add a header-level parameter. The key is lowercased on insertion.
96    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    /// The display name, if present.
107    pub fn display_name(&self) -> Option<&str> {
108        self.display_name
109            .as_deref()
110    }
111
112    /// The URI.
113    pub fn uri(&self) -> &sip_uri::Uri {
114        &self.uri
115    }
116
117    /// If the URI is a SIP/SIPS URI, return a reference to it.
118    pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
119        self.uri
120            .as_sip()
121    }
122
123    /// If the URI is a tel: URI, return a reference to it.
124    pub fn tel_uri(&self) -> Option<&sip_uri::TelUri> {
125        self.uri
126            .as_tel()
127    }
128
129    /// If the URI is a URN, return a reference to it.
130    pub fn urn_uri(&self) -> Option<&sip_uri::UrnUri> {
131        self.uri
132            .as_urn()
133    }
134
135    /// Iterator over header-level parameters as `(key, raw_value)` pairs.
136    /// Keys are lowercased; values are raw percent-encoded wire format.
137    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    /// Look up a header-level parameter by name (case-insensitive).
144    ///
145    /// Values are percent-decoded and validated as UTF-8. Returns `Err` if
146    /// the percent-decoded octets are not valid UTF-8. For non-UTF-8 values
147    /// or raw wire access, use [`param_raw()`](Self::param_raw).
148    ///
149    /// Returns `None` if the param is not present, `Some(Ok(None))` for
150    /// flag params (no value), `Some(Ok(Some(decoded)))` for valued params.
151    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    /// Look up a raw percent-encoded parameter value (case-insensitive).
165    ///
166    /// Returns the wire-format value without percent-decoding. Use this
167    /// for non-UTF-8 values or when round-trip fidelity matters.
168    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    /// The `tag` parameter value, if present.
177    ///
178    /// Tag values are simple tokens (never percent-encoded in practice),
179    /// so this returns `&str` directly.
180    pub fn tag(&self) -> Option<&str> {
181        self.param_raw("tag")
182            .flatten()
183    }
184}
185
186/// Parse a quoted string, returning (unescaped content, rest after closing quote).
187fn 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
215/// Extract the URI from `<...>`, returning `(uri_str, rest_after_>)`.
216fn 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
222/// Parse header-level parameters from the trailing portion after `>`.
223/// Values are stored as raw percent-encoded strings for round-trip fidelity.
224fn 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
239/// Check if a display name needs quoting (contains SIP special chars or whitespace).
240fn 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
250/// Escape a display name for use within double quotes.
251fn 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        // Case 1: quoted display name followed by <URI> and optional params
274        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        // Case 2: <URI> without display name, with optional params
294        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        // Case 3: unquoted display name followed by <URI> and optional params
306        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        // Case 4: bare addr-spec (no angle brackets, no display name)
325        // RFC 3261 mandates angle brackets when URI has params, so all
326        // ;params are parsed as part of the URI itself.
327        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        // "Alice" doesn't need quoting, so Display normalizes to unquoted
523        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        // Bare URIs get angle-bracketed in Display
555        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        // %C0%80 is an overlong encoding of U+0000, invalid UTF-8
692        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        // %E9 = é in ISO-8859-1, but lone byte is invalid UTF-8
705        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}