Skip to main content

sip_header/
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/// Unlike [`sip_uri::NameAddr`] (which only handles the `name-addr` portion),
32/// this type also parses header-level parameters after `>` and can
33/// round-trip real SIP header values.
34///
35/// ```
36/// use sip_header::SipHeaderAddr;
37///
38/// let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#.parse().unwrap();
39/// assert_eq!(addr.display_name(), Some("Alice"));
40/// assert_eq!(addr.tag(), Some("abc123"));
41/// ```
42///
43/// [`Display`](std::fmt::Display) always emits angle brackets around the URI,
44/// even for bare addr-spec input. This is the canonical form required by
45/// RFC 3261 when header-level parameters are present.
46#[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/// Error returned when parsing a SIP header address value fails.
55#[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    /// Create a new `SipHeaderAddr` with the given URI and no display name or params.
80    pub fn new(uri: sip_uri::Uri) -> Self {
81        SipHeaderAddr {
82            display_name: None,
83            uri,
84            params: Vec::new(),
85        }
86    }
87
88    /// Set the display name.
89    pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
90        self.display_name = Some(name.into());
91        self
92    }
93
94    /// Add a header-level parameter. The key is lowercased on insertion.
95    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    /// The display name, if present.
106    pub fn display_name(&self) -> Option<&str> {
107        self.display_name
108            .as_deref()
109    }
110
111    /// The URI.
112    pub fn uri(&self) -> &sip_uri::Uri {
113        &self.uri
114    }
115
116    /// If the URI is a SIP/SIPS URI, return a reference to it.
117    pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
118        self.uri
119            .as_sip()
120    }
121
122    /// If the URI is a tel: URI, return a reference to it.
123    pub fn tel_uri(&self) -> Option<&sip_uri::TelUri> {
124        self.uri
125            .as_tel()
126    }
127
128    /// If the URI is a URN, return a reference to it.
129    pub fn urn_uri(&self) -> Option<&sip_uri::UrnUri> {
130        self.uri
131            .as_urn()
132    }
133
134    /// Iterator over header-level parameters as `(key, raw_value)` pairs.
135    /// Keys are lowercased; values retain their original percent-encoding.
136    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    /// Look up a header-level parameter by name (case-insensitive).
143    ///
144    /// Values are percent-decoded and validated as UTF-8. Returns `Err` if
145    /// the percent-decoded octets are not valid UTF-8. For non-UTF-8 values
146    /// or raw wire access, use [`param_raw()`](Self::param_raw).
147    ///
148    /// Returns `None` if the param is not present, `Some(Ok(None))` for
149    /// flag params (no value), `Some(Ok(Some(decoded)))` for valued params.
150    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    /// Look up a raw percent-encoded parameter value (case-insensitive).
164    ///
165    /// Returns the raw value without percent-decoding. Use this when
166    /// round-trip fidelity matters or the value may not be valid UTF-8.
167    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    /// The `tag` parameter value, if present.
176    ///
177    /// Tag values are simple tokens (never percent-encoded in practice),
178    /// so this returns `&str` directly.
179    pub fn tag(&self) -> Option<&str> {
180        self.param_raw("tag")
181            .flatten()
182    }
183}
184
185/// Parse a quoted string, returning (unescaped content, rest after closing quote).
186fn 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
214/// Extract the URI from `<...>`, returning `(uri_str, rest_after_>)`.
215fn 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
221/// Parse header-level parameters from the trailing portion after `>`.
222/// Values are stored as raw percent-encoded strings for round-trip fidelity.
223fn 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
238/// Check if a display name needs quoting (contains SIP special chars or whitespace).
239fn 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
249/// Escape a display name for use within double quotes.
250fn 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        // Case 1: quoted display name followed by <URI> and optional params
273        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        // Case 2: <URI> without display name, with optional params
293        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        // Case 3: unquoted display name followed by <URI> and optional params
305        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        // Case 4: bare addr-spec (no angle brackets, no display name)
324        // RFC 3261 mandates angle brackets when URI has params, so all
325        // ;params are parsed as part of the URI itself.
326        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        // "Alice" doesn't need quoting, so Display normalizes to unquoted
522        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        // Bare URIs get angle-bracketed in Display
554        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        // %C0%80 is an overlong encoding of U+0000, invalid UTF-8
691        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        // %E9 = é in ISO-8859-1, but lone byte is invalid UTF-8
704        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}