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::{self, Write as _};
5use std::str::{FromStr, Utf8Error};
6
7use percent_encoding::percent_decode_str;
8
9use crate::replaces::{SipReplaces, SipReplacesError};
10
11/// Parsed SIP `name-addr` (RFC 3261 §25.1) with header-level parameters.
12///
13/// The `name-addr` production from RFC 3261 §25.1 combines an optional
14/// display name with a URI in angle brackets:
15///
16/// ```text
17/// name-addr      = [ display-name ] LAQUOT addr-spec RAQUOT
18/// display-name   = *(token LWS) / quoted-string
19/// ```
20///
21/// In SIP headers (`From`, `To`, `Contact`, `P-Asserted-Identity`,
22/// `Refer-To`), the `name-addr` is followed by header-level parameters
23/// (RFC 3261 §20):
24///
25/// ```text
26/// from-spec  = ( name-addr / addr-spec ) *( SEMI from-param )
27/// from-param = tag-param / generic-param
28/// ```
29///
30/// This type handles the full production including those trailing
31/// parameters (`;tag=`, `;expires=`, `;serviceurn=`, etc.).
32///
33/// Unlike [`sip_uri::NameAddr`] (which only handles the `name-addr` portion),
34/// this type also parses header-level parameters after `>` and can
35/// round-trip real SIP header values.
36///
37/// ```
38/// use sip_header::SipHeaderAddr;
39///
40/// let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#.parse().unwrap();
41/// assert_eq!(addr.display_name(), Some("Alice"));
42/// assert_eq!(addr.tag(), Some("abc123"));
43/// ```
44///
45/// [`Display`](std::fmt::Display) always emits angle brackets around the URI,
46/// even for bare addr-spec input. This is the canonical form required by
47/// RFC 3261 when header-level parameters are present.
48#[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/// Error returned when parsing a SIP header address value fails.
57#[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    /// Create a new `SipHeaderAddr` with the given URI and no display name or params.
82    pub fn new(uri: sip_uri::Uri) -> Self {
83        SipHeaderAddr {
84            display_name: None,
85            uri,
86            params: Vec::new(),
87        }
88    }
89
90    /// Set the display name, unchecked; prefer
91    /// [`try_with_display_name`](Self::try_with_display_name), which rejects CR/LF.
92    pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
93        self.display_name = Some(name.into());
94        self
95    }
96
97    /// Set the display name, rejecting what an RFC 3261 §25.1
98    /// `quoted-string` cannot carry.
99    ///
100    /// Any text is accepted except CR and LF: characters outside `qdtext`
101    /// are emitted as `quoted-pair`. [`Display`](fmt::Display) quotes the
102    /// name unless it is a single `token`.
103    ///
104    /// ```
105    /// use sip_header::SipHeaderAddr;
106    ///
107    /// let addr = SipHeaderAddr::new("sip:alice@example.com".parse()?)
108    ///     .try_with_display_name("Alice Smith")?;
109    /// assert_eq!(addr.to_string(), r#""Alice Smith" <sip:alice@example.com>"#);
110    /// assert!(SipHeaderAddr::new("sip:alice@example.com".parse()?)
111    ///     .try_with_display_name("a\r\nb")
112    ///     .is_err());
113    /// # Ok::<(), Box<dyn std::error::Error>>(())
114    /// ```
115    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    /// Add a header-level parameter, unchecked, lowercasing the key; prefer
130    /// [`try_with_param`](Self::try_with_param), which validates both.
131    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    /// Add a header-level `generic-param` (RFC 3261 §25.1), lowercasing the key.
142    ///
143    /// The key must be a `token`. A value, when given, must be a `token`, a
144    /// host (`token` characters plus `:`, `[` and `]`), or a complete
145    /// `quoted-string` including its quotes. It is stored and emitted as
146    /// given, like a parsed value, so percent-encoding is the caller's.
147    ///
148    /// ```
149    /// use sip_header::SipHeaderAddr;
150    ///
151    /// let addr = SipHeaderAddr::new("sip:alice@example.com".parse()?)
152    ///     .try_with_param("tag", Some("abc123"))?
153    ///     .try_with_param("lr", None::<&str>)?;
154    /// assert_eq!(addr.to_string(), "<sip:alice@example.com>;tag=abc123;lr");
155    /// assert!(SipHeaderAddr::new("sip:alice@example.com".parse()?)
156    ///     .try_with_param("tag", Some("a;b"))
157    ///     .is_err());
158    /// # Ok::<(), Box<dyn std::error::Error>>(())
159    /// ```
160    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    /// The display name, if present.
185    pub fn display_name(&self) -> Option<&str> {
186        self.display_name
187            .as_deref()
188    }
189
190    /// The URI.
191    pub fn uri(&self) -> &sip_uri::Uri {
192        &self.uri
193    }
194
195    /// If the URI is a SIP/SIPS URI, return a reference to it.
196    pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
197        self.uri
198            .as_sip()
199    }
200
201    /// If the URI is a tel: URI, return a reference to it.
202    pub fn tel_uri(&self) -> Option<&sip_uri::TelUri> {
203        self.uri
204            .as_tel()
205    }
206
207    /// If the URI is a URN, return a reference to it.
208    pub fn urn_uri(&self) -> Option<&sip_uri::UrnUri> {
209        self.uri
210            .as_urn()
211    }
212
213    /// Iterator over header-level parameters as `(key, raw_value)` pairs.
214    /// Keys are lowercased; values retain their original percent-encoding.
215    ///
216    /// Header-level parameter names (after `>`) are RFC 3261 §25
217    /// `generic-param` tokens, where `%` is a literal character — names are
218    /// never percent-decoded here. URI parameters inside `<…>` are the
219    /// opposite: [`sip_uri`] decodes their names. The same feature tag
220    /// therefore keys as `+urn%3aemergency%3a…` in this list but
221    /// `+urn:emergency:…` as a URI parameter.
222    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    /// Look up a header-level parameter by name (case-insensitive).
229    ///
230    /// The name is matched against the raw token from the wire — parameter
231    /// names are not percent-decoded (see [`params()`](Self::params)), so a
232    /// percent-encoded name must be looked up in its encoded form.
233    ///
234    /// Values are percent-decoded and validated as UTF-8. Returns `Err` if
235    /// the percent-decoded octets are not valid UTF-8. For non-UTF-8 values
236    /// or raw wire access, use [`param_raw()`](Self::param_raw).
237    ///
238    /// Returns `None` if the param is not present, `Some(Ok(None))` for
239    /// flag params (no value), `Some(Ok(Some(decoded)))` for valued params.
240    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    /// Look up a raw percent-encoded parameter value (case-insensitive).
254    ///
255    /// Returns the raw value without percent-decoding. Use this when
256    /// round-trip fidelity matters or the value may not be valid UTF-8.
257    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    /// Parse a `Replaces` URI header (`<sip:…?Replaces=…>`), if present.
266    ///
267    /// Returns `None` when the URI is not a SIP/SIPS URI or carries no
268    /// `Replaces` header; `Some(Err)` when the value doesn't conform to
269    /// RFC 3891 §6.1.
270    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    /// Parse a comma-separated list of `name-addr` / `addr-spec` values.
278    ///
279    /// Splits on commas at bracket depth zero (via [`split_comma_entries`](crate::split_comma_entries)),
280    /// then parses each entry as a [`SipHeaderAddr`]. Returns an empty `Vec`
281    /// for empty input. Fails on the first unparseable entry.
282    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    /// The `tag` parameter value, if present.
300    ///
301    /// Tag values are simple tokens (never percent-encoded in practice),
302    /// so this returns `&str` directly.
303    pub fn tag(&self) -> Option<&str> {
304        self.param_raw("tag")
305            .flatten()
306    }
307}
308
309/// Parse a quoted string, returning (unescaped content, rest after closing quote).
310fn 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
338/// Extract the URI from `<...>`, returning `(uri_str, rest_after_>)`.
339fn 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
345/// Parse header-level parameters from the trailing portion after `>`.
346/// Keys are lowercased; values stay raw (quotes and percent-encoding intact).
347fn 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
361/// RFC 3261 §25.1 `token` character.
362fn is_token_char(c: char) -> bool {
363    c.is_ascii_alphanumeric()
364        || matches!(
365            c,
366            '-' | '.' | '!' | '%' | '*' | '_' | '+' | '`' | '\'' | '~'
367        )
368}
369
370/// A display name needs quoting unless it is a single `token`.
371fn needs_quoting(name: &str) -> bool {
372    !name
373        .chars()
374        .all(is_token_char)
375}
376
377/// RFC 3261 §25.1 `quoted-string` with its quotes, excluding CR and LF.
378fn 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        // Case 1: quoted display name followed by <URI> and optional params
433        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        // Case 2: <URI> without display name, with optional params
453        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        // Case 3: unquoted display name followed by <URI> and optional params
465        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        // Case 4: bare addr-spec (no angle brackets, no display name)
484        // RFC 3261 mandates angle brackets when URI has params, so all
485        // ;params are parsed as part of the URI itself.
486        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        // "Alice" doesn't need quoting, so Display normalizes to unquoted
680        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        // Bare URIs get angle-bracketed in Display
712        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        // %C0%80 is an overlong encoding of U+0000, invalid UTF-8
984        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        // %E9 = é in ISO-8859-1, but lone byte is invalid UTF-8
997        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}