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
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.
91    pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
92        self.display_name = Some(name.into());
93        self
94    }
95
96    /// Add a header-level parameter. The key is lowercased on insertion.
97    pub fn with_param(mut self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
98        self.params
99            .push((
100                key.into()
101                    .to_ascii_lowercase(),
102                value.map(Into::into),
103            ));
104        self
105    }
106
107    /// The display name, if present.
108    pub fn display_name(&self) -> Option<&str> {
109        self.display_name
110            .as_deref()
111    }
112
113    /// The URI.
114    pub fn uri(&self) -> &sip_uri::Uri {
115        &self.uri
116    }
117
118    /// If the URI is a SIP/SIPS URI, return a reference to it.
119    pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
120        self.uri
121            .as_sip()
122    }
123
124    /// If the URI is a tel: URI, return a reference to it.
125    pub fn tel_uri(&self) -> Option<&sip_uri::TelUri> {
126        self.uri
127            .as_tel()
128    }
129
130    /// If the URI is a URN, return a reference to it.
131    pub fn urn_uri(&self) -> Option<&sip_uri::UrnUri> {
132        self.uri
133            .as_urn()
134    }
135
136    /// Iterator over header-level parameters as `(key, raw_value)` pairs.
137    /// Keys are lowercased; values retain their original percent-encoding.
138    ///
139    /// Header-level parameter names (after `>`) are RFC 3261 §25
140    /// `generic-param` tokens, where `%` is a literal character — names are
141    /// never percent-decoded here. URI parameters inside `<…>` are the
142    /// opposite: [`sip_uri`] decodes their names. The same feature tag
143    /// therefore keys as `+urn%3aemergency%3a…` in this list but
144    /// `+urn:emergency:…` as a URI parameter.
145    pub fn params(&self) -> impl Iterator<Item = (&str, Option<&str>)> {
146        self.params
147            .iter()
148            .map(|(k, v)| (k.as_str(), v.as_deref()))
149    }
150
151    /// Look up a header-level parameter by name (case-insensitive).
152    ///
153    /// The name is matched against the raw token from the wire — parameter
154    /// names are not percent-decoded (see [`params()`](Self::params)), so a
155    /// percent-encoded name must be looked up in its encoded form.
156    ///
157    /// Values are percent-decoded and validated as UTF-8. Returns `Err` if
158    /// the percent-decoded octets are not valid UTF-8. For non-UTF-8 values
159    /// or raw wire access, use [`param_raw()`](Self::param_raw).
160    ///
161    /// Returns `None` if the param is not present, `Some(Ok(None))` for
162    /// flag params (no value), `Some(Ok(Some(decoded)))` for valued params.
163    pub fn param(&self, name: &str) -> Option<Result<Option<Cow<'_, str>>, Utf8Error>> {
164        let needle = name.to_ascii_lowercase();
165        self.params
166            .iter()
167            .find(|(k, _)| *k == needle)
168            .map(|(_, v)| match v {
169                Some(raw) => percent_decode_str(raw)
170                    .decode_utf8()
171                    .map(Some),
172                None => Ok(None),
173            })
174    }
175
176    /// Look up a raw percent-encoded parameter value (case-insensitive).
177    ///
178    /// Returns the raw value without percent-decoding. Use this when
179    /// round-trip fidelity matters or the value may not be valid UTF-8.
180    pub fn param_raw(&self, name: &str) -> Option<Option<&str>> {
181        let needle = name.to_ascii_lowercase();
182        self.params
183            .iter()
184            .find(|(k, _)| *k == needle)
185            .map(|(_, v)| v.as_deref())
186    }
187
188    /// Parse a `Replaces` URI header (`<sip:…?Replaces=…>`), if present.
189    ///
190    /// Returns `None` when the URI is not a SIP/SIPS URI or carries no
191    /// `Replaces` header; `Some(Err)` when the value doesn't conform to
192    /// RFC 3891 §6.1.
193    pub fn replaces(&self) -> Option<Result<SipReplaces, SipReplacesError>> {
194        let value = self
195            .sip_uri()?
196            .header("Replaces")?;
197        Some(SipReplaces::parse_uri_header(value))
198    }
199
200    /// Parse a comma-separated list of `name-addr` / `addr-spec` values.
201    ///
202    /// Splits on commas at bracket depth zero (via [`split_comma_entries`](crate::split_comma_entries)),
203    /// then parses each entry as a [`SipHeaderAddr`]. Returns an empty `Vec`
204    /// for empty input. Fails on the first unparseable entry.
205    pub fn parse_list(raw: &str) -> Result<Vec<SipHeaderAddr>, ParseSipHeaderAddrError> {
206        if raw
207            .trim()
208            .is_empty()
209        {
210            return Ok(Vec::new());
211        }
212        crate::split_comma_entries(raw)
213            .into_iter()
214            .map(|entry| {
215                entry
216                    .trim()
217                    .parse()
218            })
219            .collect()
220    }
221
222    /// The `tag` parameter value, if present.
223    ///
224    /// Tag values are simple tokens (never percent-encoded in practice),
225    /// so this returns `&str` directly.
226    pub fn tag(&self) -> Option<&str> {
227        self.param_raw("tag")
228            .flatten()
229    }
230}
231
232/// Parse a quoted string, returning (unescaped content, rest after closing quote).
233fn parse_quoted_string(s: &str) -> Result<(String, &str), String> {
234    if !s.starts_with('"') {
235        return Err("expected opening quote".into());
236    }
237
238    let mut result = String::new();
239    let mut chars = s[1..].char_indices();
240
241    while let Some((i, c)) = chars.next() {
242        match c {
243            '"' => {
244                return Ok((result, &s[i + 2..]));
245            }
246            '\\' => {
247                let (_, escaped) = chars
248                    .next()
249                    .ok_or("unterminated escape in quoted string")?;
250                result.push(escaped);
251            }
252            _ => {
253                result.push(c);
254            }
255        }
256    }
257
258    Err("unterminated quoted string".into())
259}
260
261/// Extract the URI from `<...>`, returning `(uri_str, rest_after_>)`.
262fn extract_angle_uri(s: &str) -> Option<(&str, &str)> {
263    let s = s.strip_prefix('<')?;
264    let end = s.find('>')?;
265    Some((&s[..end], &s[end + 1..]))
266}
267
268/// Parse header-level parameters from the trailing portion after `>`.
269/// Values are stored as raw percent-encoded strings for round-trip fidelity.
270fn parse_header_params(s: &str) -> Vec<(String, Option<String>)> {
271    let mut params = Vec::new();
272    for segment in s.split(';') {
273        if segment.is_empty() {
274            continue;
275        }
276        if let Some((key, value)) = segment.split_once('=') {
277            params.push((key.to_ascii_lowercase(), Some(value.to_string())));
278        } else {
279            params.push((segment.to_ascii_lowercase(), None));
280        }
281    }
282    params
283}
284
285/// Check if a display name needs quoting (contains SIP special chars or whitespace).
286fn needs_quoting(name: &str) -> bool {
287    name.bytes()
288        .any(|b| {
289            matches!(
290                b,
291                b'"' | b'\\' | b'<' | b'>' | b',' | b';' | b':' | b'@' | b' ' | b'\t'
292            )
293        })
294}
295
296/// Escape a display name for use within double quotes.
297fn escape_display_name(name: &str) -> String {
298    let mut out = String::with_capacity(name.len());
299    for c in name.chars() {
300        if matches!(c, '"' | '\\') {
301            out.push('\\');
302        }
303        out.push(c);
304    }
305    out
306}
307
308impl FromStr for SipHeaderAddr {
309    type Err = ParseSipHeaderAddrError;
310
311    fn from_str(input: &str) -> Result<Self, Self::Err> {
312        let err = |msg: &str| ParseSipHeaderAddrError(msg.to_string());
313        let s = input.trim();
314
315        if s.is_empty() {
316            return Err(err("empty input"));
317        }
318
319        // Case 1: quoted display name followed by <URI> and optional params
320        if s.starts_with('"') {
321            let (display_name, rest) = parse_quoted_string(s).map_err(|e| err(&e))?;
322            let rest = rest.trim_start();
323            let (uri_str, trailing) = extract_angle_uri(rest)
324                .ok_or_else(|| err("expected '<URI>' after quoted display name"))?;
325            let uri: sip_uri::Uri = uri_str.parse()?;
326            let display_name = if display_name.is_empty() {
327                None
328            } else {
329                Some(display_name)
330            };
331            let params = parse_header_params(trailing);
332            return Ok(SipHeaderAddr {
333                display_name,
334                uri,
335                params,
336            });
337        }
338
339        // Case 2: <URI> without display name, with optional params
340        if s.starts_with('<') {
341            let (uri_str, trailing) = extract_angle_uri(s).ok_or_else(|| err("unclosed '<'"))?;
342            let uri: sip_uri::Uri = uri_str.parse()?;
343            let params = parse_header_params(trailing);
344            return Ok(SipHeaderAddr {
345                display_name: None,
346                uri,
347                params,
348            });
349        }
350
351        // Case 3: unquoted display name followed by <URI> and optional params
352        if let Some(angle_start) = s.find('<') {
353            let display_name = s[..angle_start].trim();
354            let display_name = if display_name.is_empty() {
355                None
356            } else {
357                Some(display_name.to_string())
358            };
359            let (uri_str, trailing) =
360                extract_angle_uri(&s[angle_start..]).ok_or_else(|| err("unclosed '<'"))?;
361            let uri: sip_uri::Uri = uri_str.parse()?;
362            let params = parse_header_params(trailing);
363            return Ok(SipHeaderAddr {
364                display_name,
365                uri,
366                params,
367            });
368        }
369
370        // Case 4: bare addr-spec (no angle brackets, no display name)
371        // RFC 3261 mandates angle brackets when URI has params, so all
372        // ;params are parsed as part of the URI itself.
373        let uri: sip_uri::Uri = s.parse()?;
374        Ok(SipHeaderAddr {
375            display_name: None,
376            uri,
377            params: Vec::new(),
378        })
379    }
380}
381
382impl fmt::Display for SipHeaderAddr {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        match self
385            .display_name
386            .as_deref()
387        {
388            Some(name) if !name.is_empty() => {
389                if needs_quoting(name) {
390                    write!(f, "\"{}\" ", escape_display_name(name))?;
391                } else {
392                    write!(f, "{name} ")?;
393                }
394                write!(f, "<{}>", self.uri)?;
395            }
396            _ => {
397                write!(f, "<{}>", self.uri)?;
398            }
399        }
400        for (key, value) in &self.params {
401            match value {
402                Some(v) => write!(f, ";{key}={v}")?,
403                None => write!(f, ";{key}")?,
404            }
405        }
406        Ok(())
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use std::borrow::Cow;
413
414    use super::*;
415
416    #[test]
417    fn quoted_display_name_with_tag() {
418        let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#
419            .parse()
420            .unwrap();
421        assert_eq!(addr.display_name(), Some("Alice"));
422        assert!(addr
423            .sip_uri()
424            .is_some());
425        assert_eq!(addr.tag(), Some("abc123"));
426    }
427
428    #[test]
429    fn angle_bracket_no_name_multiple_params() {
430        let addr: SipHeaderAddr = "<sip:user@host>;tag=xyz;expires=3600"
431            .parse()
432            .unwrap();
433        assert_eq!(addr.display_name(), None);
434        assert_eq!(addr.tag(), Some("xyz"));
435        assert_eq!(
436            addr.param("expires")
437                .unwrap()
438                .unwrap(),
439            Some(Cow::from("3600")),
440        );
441    }
442
443    #[test]
444    fn bare_addr_spec_no_params() {
445        let addr: SipHeaderAddr = "sip:user@host"
446            .parse()
447            .unwrap();
448        assert_eq!(addr.display_name(), None);
449        assert!(addr
450            .sip_uri()
451            .is_some());
452        assert_eq!(
453            addr.params()
454                .count(),
455            0
456        );
457    }
458
459    #[test]
460    fn unquoted_display_name_with_params() {
461        let addr: SipHeaderAddr = "Alice <sip:alice@example.com>;tag=abc"
462            .parse()
463            .unwrap();
464        assert_eq!(addr.display_name(), Some("Alice"));
465        assert_eq!(addr.tag(), Some("abc"));
466    }
467
468    #[test]
469    fn ng911_refer_to_serviceurn() {
470        let input = "<sip:user@esrp.example.com?Call-Info=x>;serviceurn=urn%3Aservice%3Apolice";
471        let addr: SipHeaderAddr = input
472            .parse()
473            .unwrap();
474        assert_eq!(addr.display_name(), None);
475        assert_eq!(
476            addr.param("serviceurn")
477                .unwrap()
478                .unwrap(),
479            Some(Cow::from("urn:service:police")),
480        );
481        assert_eq!(
482            addr.param_raw("serviceurn"),
483            Some(Some("urn%3Aservice%3Apolice")),
484        );
485        let sip = addr
486            .sip_uri()
487            .unwrap();
488        assert_eq!(
489            sip.host()
490                .to_string(),
491            "esrp.example.com"
492        );
493    }
494
495    #[test]
496    fn p_asserted_identity_uri_params_no_header_params() {
497        let input = r#""EXAMPLE CO" <sip:+15551234567;cpc=emergency@198.51.100.1;user=phone>"#;
498        let addr: SipHeaderAddr = input
499            .parse()
500            .unwrap();
501        assert_eq!(addr.display_name(), Some("EXAMPLE CO"));
502        assert_eq!(
503            addr.params()
504                .count(),
505            0
506        );
507        let sip = addr
508            .sip_uri()
509            .unwrap();
510        assert_eq!(sip.user(), Some("+15551234567"));
511        assert_eq!(sip.param("user"), Some(&Some("phone".to_string())));
512    }
513
514    #[test]
515    fn tel_uri_with_header_params() {
516        let addr: SipHeaderAddr = "<tel:+15551234567>;expires=3600"
517            .parse()
518            .unwrap();
519        assert!(addr
520            .tel_uri()
521            .is_some());
522        assert_eq!(
523            addr.param("expires")
524                .unwrap()
525                .unwrap(),
526            Some(Cow::from("3600")),
527        );
528    }
529
530    #[test]
531    fn flag_param_no_value() {
532        let addr: SipHeaderAddr = "<sip:user@host>;lr;tag=abc"
533            .parse()
534            .unwrap();
535        assert_eq!(
536            addr.param("lr")
537                .unwrap()
538                .unwrap(),
539            None
540        );
541        assert_eq!(addr.tag(), Some("abc"));
542    }
543
544    #[test]
545    fn urn_uri_no_params() {
546        let addr: SipHeaderAddr = "<urn:service:sos>"
547            .parse()
548            .unwrap();
549        assert!(addr
550            .urn_uri()
551            .is_some());
552        assert_eq!(
553            addr.params()
554                .count(),
555            0
556        );
557    }
558
559    #[test]
560    fn empty_input_fails() {
561        assert!(""
562            .parse::<SipHeaderAddr>()
563            .is_err());
564    }
565
566    #[test]
567    fn display_roundtrip_quoted_name_with_params() {
568        // "Alice" doesn't need quoting, so Display normalizes to unquoted
569        let input = r#""Alice" <sip:alice@example.com>;tag=abc123"#;
570        let addr: SipHeaderAddr = input
571            .parse()
572            .unwrap();
573        assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
574    }
575
576    #[test]
577    fn display_roundtrip_name_requiring_quotes() {
578        let input = r#""Alice Smith" <sip:alice@example.com>;tag=abc123"#;
579        let addr: SipHeaderAddr = input
580            .parse()
581            .unwrap();
582        assert_eq!(addr.to_string(), input);
583    }
584
585    #[test]
586    fn display_roundtrip_no_name_with_params() {
587        let input = "<sip:user@host>;tag=xyz;expires=3600";
588        let addr: SipHeaderAddr = input
589            .parse()
590            .unwrap();
591        assert_eq!(addr.to_string(), input);
592    }
593
594    #[test]
595    fn display_roundtrip_bare_uri() {
596        let input = "sip:user@host";
597        let addr: SipHeaderAddr = input
598            .parse()
599            .unwrap();
600        // Bare URIs get angle-bracketed in Display
601        assert_eq!(addr.to_string(), "<sip:user@host>");
602    }
603
604    #[test]
605    fn display_roundtrip_flag_param() {
606        let input = "<sip:user@host>;lr;tag=abc";
607        let addr: SipHeaderAddr = input
608            .parse()
609            .unwrap();
610        assert_eq!(addr.to_string(), input);
611    }
612
613    #[test]
614    fn case_insensitive_param_lookup() {
615        let addr: SipHeaderAddr = "<sip:user@host>;Tag=ABC;Expires=3600"
616            .parse()
617            .unwrap();
618        assert_eq!(
619            addr.param("tag")
620                .unwrap()
621                .unwrap(),
622            Some(Cow::from("ABC")),
623        );
624        assert_eq!(
625            addr.param("TAG")
626                .unwrap()
627                .unwrap(),
628            Some(Cow::from("ABC")),
629        );
630        assert_eq!(
631            addr.param("expires")
632                .unwrap()
633                .unwrap(),
634            Some(Cow::from("3600")),
635        );
636    }
637
638    #[test]
639    fn tag_convenience_accessor() {
640        let with_tag: SipHeaderAddr = "<sip:user@host>;tag=xyz"
641            .parse()
642            .unwrap();
643        assert_eq!(with_tag.tag(), Some("xyz"));
644
645        let without_tag: SipHeaderAddr = "<sip:user@host>"
646            .parse()
647            .unwrap();
648        assert_eq!(without_tag.tag(), None);
649    }
650
651    #[test]
652    fn builder_new() {
653        let uri: sip_uri::Uri = "sip:alice@example.com"
654            .parse()
655            .unwrap();
656        let addr = SipHeaderAddr::new(uri);
657        assert_eq!(addr.display_name(), None);
658        assert_eq!(
659            addr.params()
660                .count(),
661            0
662        );
663        assert_eq!(addr.to_string(), "<sip:alice@example.com>");
664    }
665
666    #[test]
667    fn builder_with_display_name_and_params() {
668        let uri: sip_uri::Uri = "sip:alice@example.com"
669            .parse()
670            .unwrap();
671        let addr = SipHeaderAddr::new(uri)
672            .with_display_name("Alice")
673            .with_param("tag", Some("abc123"));
674        assert_eq!(addr.display_name(), Some("Alice"));
675        assert_eq!(addr.tag(), Some("abc123"));
676        assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
677    }
678
679    #[test]
680    fn builder_flag_param() {
681        let uri: sip_uri::Uri = "sip:proxy@example.com"
682            .parse()
683            .unwrap();
684        let addr = SipHeaderAddr::new(uri).with_param("lr", None::<String>);
685        assert_eq!(
686            addr.param("lr")
687                .unwrap()
688                .unwrap(),
689            None
690        );
691        assert_eq!(addr.to_string(), "<sip:proxy@example.com>;lr");
692    }
693
694    #[test]
695    fn escaped_quotes_in_display_name() {
696        let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
697        let addr: SipHeaderAddr = input
698            .parse()
699            .unwrap();
700        assert_eq!(addr.display_name(), Some(r#"Say "Hello""#));
701        assert_eq!(addr.tag(), Some("t"));
702    }
703
704    #[test]
705    fn display_roundtrip_escaped_quotes() {
706        let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
707        let addr: SipHeaderAddr = input
708            .parse()
709            .unwrap();
710        assert_eq!(addr.to_string(), input);
711    }
712
713    #[test]
714    fn trailing_semicolon_ignored() {
715        let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;"
716            .parse()
717            .unwrap();
718        assert_eq!(
719            addr.params()
720                .count(),
721            1
722        );
723        assert_eq!(addr.tag(), Some("abc"));
724    }
725
726    #[test]
727    fn display_roundtrip_percent_encoded_params() {
728        let input = "<sip:user@esrp.example.com>;serviceurn=urn%3Aservice%3Apolice";
729        let addr: SipHeaderAddr = input
730            .parse()
731            .unwrap();
732        assert_eq!(addr.to_string(), input);
733    }
734
735    #[test]
736    fn param_invalid_utf8_returns_err() {
737        // %C0%80 is an overlong encoding of U+0000, invalid UTF-8
738        let addr: SipHeaderAddr = "<sip:user@host>;data=%C0%80"
739            .parse()
740            .unwrap();
741        assert!(addr
742            .param("data")
743            .unwrap()
744            .is_err());
745        assert_eq!(addr.param_raw("data"), Some(Some("%C0%80")));
746    }
747
748    #[test]
749    fn param_iso_8859_fallback_to_raw() {
750        // %E9 = é in ISO-8859-1, but lone byte is invalid UTF-8
751        let addr: SipHeaderAddr = "<sip:user@host>;name=%E9"
752            .parse()
753            .unwrap();
754        assert!(addr
755            .param("name")
756            .unwrap()
757            .is_err());
758        assert_eq!(addr.param_raw("name"), Some(Some("%E9")));
759    }
760
761    #[test]
762    fn parse_list_multiple_entries() {
763        let input = r#""Alice" <sip:alice@example.com>;tag=a, <sip:bob@example.com>, sip:carol@example.com"#;
764        let addrs = SipHeaderAddr::parse_list(input).unwrap();
765        assert_eq!(addrs.len(), 3);
766        assert_eq!(addrs[0].display_name(), Some("Alice"));
767        assert_eq!(addrs[0].tag(), Some("a"));
768        assert_eq!(addrs[1].display_name(), None);
769        assert_eq!(
770            addrs[1]
771                .sip_uri()
772                .unwrap()
773                .user(),
774            Some("bob"),
775        );
776        assert_eq!(
777            addrs[2]
778                .sip_uri()
779                .unwrap()
780                .user(),
781            Some("carol"),
782        );
783    }
784
785    #[test]
786    fn parse_list_single_entry() {
787        let addrs = SipHeaderAddr::parse_list("<sip:alice@example.com>").unwrap();
788        assert_eq!(addrs.len(), 1);
789    }
790
791    #[test]
792    fn parse_list_empty_returns_empty() {
793        let addrs = SipHeaderAddr::parse_list("").unwrap();
794        assert!(addrs.is_empty());
795    }
796
797    #[test]
798    fn parse_list_propagates_parse_error() {
799        assert!(SipHeaderAddr::parse_list("not-a-uri, <sip:ok@example.com>").is_err());
800    }
801
802    #[test]
803    fn replaces_uri_header() {
804        let addr: SipHeaderAddr =
805            "<sip:bob@203.0.113.9?Replaces=abc123%40203.0.113.5%3Bto-tag%3Dt1%3Bfrom-tag%3Df1>"
806                .parse()
807                .unwrap();
808        let r = addr
809            .replaces()
810            .unwrap()
811            .unwrap();
812        assert_eq!(r.call_id(), "abc123@203.0.113.5");
813        assert_eq!(r.host(), Some("203.0.113.5"));
814        assert_eq!(r.to_tag(), "t1");
815        assert_eq!(r.from_tag(), "f1");
816    }
817
818    #[test]
819    fn replaces_uri_header_absent() {
820        let addr: SipHeaderAddr = "<sip:bob@203.0.113.9>"
821            .parse()
822            .unwrap();
823        assert!(addr
824            .replaces()
825            .is_none());
826    }
827
828    #[test]
829    fn host_only_ipv4_with_feature_tag_param() {
830        let input =
831            "<sip:198.51.100.7:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
832        let addr: SipHeaderAddr = input
833            .parse()
834            .unwrap();
835        let sip = addr
836            .sip_uri()
837            .unwrap();
838        assert_eq!(sip.user(), None);
839        assert_eq!(
840            sip.host()
841                .to_string(),
842            "198.51.100.7"
843        );
844        assert_eq!(sip.port(), Some(5060));
845        assert_eq!(sip.param("transport"), Some(&Some("udp".to_string())));
846        assert_eq!(
847            addr.param_raw("+urn%3Aemergency%3Amedia-feature.psap-call-control"),
848            Some(None),
849        );
850    }
851
852    #[test]
853    fn host_only_ipv6_with_feature_tag_param() {
854        let input =
855            "<sip:[2001:db8::7]:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
856        let addr: SipHeaderAddr = input
857            .parse()
858            .unwrap();
859        let sip = addr
860            .sip_uri()
861            .unwrap();
862        assert_eq!(sip.user(), None);
863        assert_eq!(
864            sip.host()
865                .to_string(),
866            "[2001:db8::7]"
867        );
868        assert_eq!(sip.port(), Some(5060));
869        assert_eq!(
870            addr.param_raw("+urn%3Aemergency%3Amedia-feature.psap-call-control"),
871            Some(None),
872        );
873    }
874
875    #[test]
876    fn parse_list_host_only_ipv4_with_feature_tag_param() {
877        let input =
878            "<sip:198.51.100.7:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
879        let addrs = SipHeaderAddr::parse_list(input).unwrap();
880        assert_eq!(addrs.len(), 1);
881        assert_eq!(
882            addrs[0]
883                .sip_uri()
884                .unwrap()
885                .host()
886                .to_string(),
887            "198.51.100.7"
888        );
889    }
890
891    #[test]
892    fn params_iterator() {
893        let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;lr;expires=60"
894            .parse()
895            .unwrap();
896        let params: Vec<_> = addr
897            .params()
898            .collect();
899        assert_eq!(params.len(), 3);
900        assert_eq!(params[0], ("tag", Some("abc")));
901        assert_eq!(params[1], ("lr", None));
902        assert_eq!(params[2], ("expires", Some("60")));
903    }
904}