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    /// Parse a comma-separated list of `name-addr` / `addr-spec` values.
176    ///
177    /// Splits on commas at bracket depth zero (via [`split_comma_entries`](crate::split_comma_entries)),
178    /// then parses each entry as a [`SipHeaderAddr`]. Returns an empty `Vec`
179    /// for empty input. Fails on the first unparseable entry.
180    pub fn parse_list(raw: &str) -> Result<Vec<SipHeaderAddr>, ParseSipHeaderAddrError> {
181        if raw
182            .trim()
183            .is_empty()
184        {
185            return Ok(Vec::new());
186        }
187        crate::split_comma_entries(raw)
188            .into_iter()
189            .map(|entry| {
190                entry
191                    .trim()
192                    .parse()
193            })
194            .collect()
195    }
196
197    /// The `tag` parameter value, if present.
198    ///
199    /// Tag values are simple tokens (never percent-encoded in practice),
200    /// so this returns `&str` directly.
201    pub fn tag(&self) -> Option<&str> {
202        self.param_raw("tag")
203            .flatten()
204    }
205}
206
207/// Parse a quoted string, returning (unescaped content, rest after closing quote).
208fn parse_quoted_string(s: &str) -> Result<(String, &str), String> {
209    if !s.starts_with('"') {
210        return Err("expected opening quote".into());
211    }
212
213    let mut result = String::new();
214    let mut chars = s[1..].char_indices();
215
216    while let Some((i, c)) = chars.next() {
217        match c {
218            '"' => {
219                return Ok((result, &s[i + 2..]));
220            }
221            '\\' => {
222                let (_, escaped) = chars
223                    .next()
224                    .ok_or("unterminated escape in quoted string")?;
225                result.push(escaped);
226            }
227            _ => {
228                result.push(c);
229            }
230        }
231    }
232
233    Err("unterminated quoted string".into())
234}
235
236/// Extract the URI from `<...>`, returning `(uri_str, rest_after_>)`.
237fn extract_angle_uri(s: &str) -> Option<(&str, &str)> {
238    let s = s.strip_prefix('<')?;
239    let end = s.find('>')?;
240    Some((&s[..end], &s[end + 1..]))
241}
242
243/// Parse header-level parameters from the trailing portion after `>`.
244/// Values are stored as raw percent-encoded strings for round-trip fidelity.
245fn parse_header_params(s: &str) -> Vec<(String, Option<String>)> {
246    let mut params = Vec::new();
247    for segment in s.split(';') {
248        if segment.is_empty() {
249            continue;
250        }
251        if let Some((key, value)) = segment.split_once('=') {
252            params.push((key.to_ascii_lowercase(), Some(value.to_string())));
253        } else {
254            params.push((segment.to_ascii_lowercase(), None));
255        }
256    }
257    params
258}
259
260/// Check if a display name needs quoting (contains SIP special chars or whitespace).
261fn needs_quoting(name: &str) -> bool {
262    name.bytes()
263        .any(|b| {
264            matches!(
265                b,
266                b'"' | b'\\' | b'<' | b'>' | b',' | b';' | b':' | b'@' | b' ' | b'\t'
267            )
268        })
269}
270
271/// Escape a display name for use within double quotes.
272fn escape_display_name(name: &str) -> String {
273    let mut out = String::with_capacity(name.len());
274    for c in name.chars() {
275        if matches!(c, '"' | '\\') {
276            out.push('\\');
277        }
278        out.push(c);
279    }
280    out
281}
282
283impl FromStr for SipHeaderAddr {
284    type Err = ParseSipHeaderAddrError;
285
286    fn from_str(input: &str) -> Result<Self, Self::Err> {
287        let err = |msg: &str| ParseSipHeaderAddrError(msg.to_string());
288        let s = input.trim();
289
290        if s.is_empty() {
291            return Err(err("empty input"));
292        }
293
294        // Case 1: quoted display name followed by <URI> and optional params
295        if s.starts_with('"') {
296            let (display_name, rest) = parse_quoted_string(s).map_err(|e| err(&e))?;
297            let rest = rest.trim_start();
298            let (uri_str, trailing) = extract_angle_uri(rest)
299                .ok_or_else(|| err("expected '<URI>' after quoted display name"))?;
300            let uri: sip_uri::Uri = uri_str.parse()?;
301            let display_name = if display_name.is_empty() {
302                None
303            } else {
304                Some(display_name)
305            };
306            let params = parse_header_params(trailing);
307            return Ok(SipHeaderAddr {
308                display_name,
309                uri,
310                params,
311            });
312        }
313
314        // Case 2: <URI> without display name, with optional params
315        if s.starts_with('<') {
316            let (uri_str, trailing) = extract_angle_uri(s).ok_or_else(|| err("unclosed '<'"))?;
317            let uri: sip_uri::Uri = uri_str.parse()?;
318            let params = parse_header_params(trailing);
319            return Ok(SipHeaderAddr {
320                display_name: None,
321                uri,
322                params,
323            });
324        }
325
326        // Case 3: unquoted display name followed by <URI> and optional params
327        if let Some(angle_start) = s.find('<') {
328            let display_name = s[..angle_start].trim();
329            let display_name = if display_name.is_empty() {
330                None
331            } else {
332                Some(display_name.to_string())
333            };
334            let (uri_str, trailing) =
335                extract_angle_uri(&s[angle_start..]).ok_or_else(|| err("unclosed '<'"))?;
336            let uri: sip_uri::Uri = uri_str.parse()?;
337            let params = parse_header_params(trailing);
338            return Ok(SipHeaderAddr {
339                display_name,
340                uri,
341                params,
342            });
343        }
344
345        // Case 4: bare addr-spec (no angle brackets, no display name)
346        // RFC 3261 mandates angle brackets when URI has params, so all
347        // ;params are parsed as part of the URI itself.
348        let uri: sip_uri::Uri = s.parse()?;
349        Ok(SipHeaderAddr {
350            display_name: None,
351            uri,
352            params: Vec::new(),
353        })
354    }
355}
356
357impl fmt::Display for SipHeaderAddr {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        match self
360            .display_name
361            .as_deref()
362        {
363            Some(name) if !name.is_empty() => {
364                if needs_quoting(name) {
365                    write!(f, "\"{}\" ", escape_display_name(name))?;
366                } else {
367                    write!(f, "{name} ")?;
368                }
369                write!(f, "<{}>", self.uri)?;
370            }
371            _ => {
372                write!(f, "<{}>", self.uri)?;
373            }
374        }
375        for (key, value) in &self.params {
376            match value {
377                Some(v) => write!(f, ";{key}={v}")?,
378                None => write!(f, ";{key}")?,
379            }
380        }
381        Ok(())
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use std::borrow::Cow;
388
389    use super::*;
390
391    #[test]
392    fn quoted_display_name_with_tag() {
393        let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#
394            .parse()
395            .unwrap();
396        assert_eq!(addr.display_name(), Some("Alice"));
397        assert!(addr
398            .sip_uri()
399            .is_some());
400        assert_eq!(addr.tag(), Some("abc123"));
401    }
402
403    #[test]
404    fn angle_bracket_no_name_multiple_params() {
405        let addr: SipHeaderAddr = "<sip:user@host>;tag=xyz;expires=3600"
406            .parse()
407            .unwrap();
408        assert_eq!(addr.display_name(), None);
409        assert_eq!(addr.tag(), Some("xyz"));
410        assert_eq!(
411            addr.param("expires")
412                .unwrap()
413                .unwrap(),
414            Some(Cow::from("3600")),
415        );
416    }
417
418    #[test]
419    fn bare_addr_spec_no_params() {
420        let addr: SipHeaderAddr = "sip:user@host"
421            .parse()
422            .unwrap();
423        assert_eq!(addr.display_name(), None);
424        assert!(addr
425            .sip_uri()
426            .is_some());
427        assert_eq!(
428            addr.params()
429                .count(),
430            0
431        );
432    }
433
434    #[test]
435    fn unquoted_display_name_with_params() {
436        let addr: SipHeaderAddr = "Alice <sip:alice@example.com>;tag=abc"
437            .parse()
438            .unwrap();
439        assert_eq!(addr.display_name(), Some("Alice"));
440        assert_eq!(addr.tag(), Some("abc"));
441    }
442
443    #[test]
444    fn ng911_refer_to_serviceurn() {
445        let input = "<sip:user@esrp.example.com?Call-Info=x>;serviceurn=urn%3Aservice%3Apolice";
446        let addr: SipHeaderAddr = input
447            .parse()
448            .unwrap();
449        assert_eq!(addr.display_name(), None);
450        assert_eq!(
451            addr.param("serviceurn")
452                .unwrap()
453                .unwrap(),
454            Some(Cow::from("urn:service:police")),
455        );
456        assert_eq!(
457            addr.param_raw("serviceurn"),
458            Some(Some("urn%3Aservice%3Apolice")),
459        );
460        let sip = addr
461            .sip_uri()
462            .unwrap();
463        assert_eq!(
464            sip.host()
465                .to_string(),
466            "esrp.example.com"
467        );
468    }
469
470    #[test]
471    fn p_asserted_identity_uri_params_no_header_params() {
472        let input = r#""EXAMPLE CO" <sip:+15551234567;cpc=emergency@198.51.100.1;user=phone>"#;
473        let addr: SipHeaderAddr = input
474            .parse()
475            .unwrap();
476        assert_eq!(addr.display_name(), Some("EXAMPLE CO"));
477        assert_eq!(
478            addr.params()
479                .count(),
480            0
481        );
482        let sip = addr
483            .sip_uri()
484            .unwrap();
485        assert_eq!(sip.user(), Some("+15551234567"));
486        assert_eq!(sip.param("user"), Some(&Some("phone".to_string())));
487    }
488
489    #[test]
490    fn tel_uri_with_header_params() {
491        let addr: SipHeaderAddr = "<tel:+15551234567>;expires=3600"
492            .parse()
493            .unwrap();
494        assert!(addr
495            .tel_uri()
496            .is_some());
497        assert_eq!(
498            addr.param("expires")
499                .unwrap()
500                .unwrap(),
501            Some(Cow::from("3600")),
502        );
503    }
504
505    #[test]
506    fn flag_param_no_value() {
507        let addr: SipHeaderAddr = "<sip:user@host>;lr;tag=abc"
508            .parse()
509            .unwrap();
510        assert_eq!(
511            addr.param("lr")
512                .unwrap()
513                .unwrap(),
514            None
515        );
516        assert_eq!(addr.tag(), Some("abc"));
517    }
518
519    #[test]
520    fn urn_uri_no_params() {
521        let addr: SipHeaderAddr = "<urn:service:sos>"
522            .parse()
523            .unwrap();
524        assert!(addr
525            .urn_uri()
526            .is_some());
527        assert_eq!(
528            addr.params()
529                .count(),
530            0
531        );
532    }
533
534    #[test]
535    fn empty_input_fails() {
536        assert!(""
537            .parse::<SipHeaderAddr>()
538            .is_err());
539    }
540
541    #[test]
542    fn display_roundtrip_quoted_name_with_params() {
543        // "Alice" doesn't need quoting, so Display normalizes to unquoted
544        let input = r#""Alice" <sip:alice@example.com>;tag=abc123"#;
545        let addr: SipHeaderAddr = input
546            .parse()
547            .unwrap();
548        assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
549    }
550
551    #[test]
552    fn display_roundtrip_name_requiring_quotes() {
553        let input = r#""Alice Smith" <sip:alice@example.com>;tag=abc123"#;
554        let addr: SipHeaderAddr = input
555            .parse()
556            .unwrap();
557        assert_eq!(addr.to_string(), input);
558    }
559
560    #[test]
561    fn display_roundtrip_no_name_with_params() {
562        let input = "<sip:user@host>;tag=xyz;expires=3600";
563        let addr: SipHeaderAddr = input
564            .parse()
565            .unwrap();
566        assert_eq!(addr.to_string(), input);
567    }
568
569    #[test]
570    fn display_roundtrip_bare_uri() {
571        let input = "sip:user@host";
572        let addr: SipHeaderAddr = input
573            .parse()
574            .unwrap();
575        // Bare URIs get angle-bracketed in Display
576        assert_eq!(addr.to_string(), "<sip:user@host>");
577    }
578
579    #[test]
580    fn display_roundtrip_flag_param() {
581        let input = "<sip:user@host>;lr;tag=abc";
582        let addr: SipHeaderAddr = input
583            .parse()
584            .unwrap();
585        assert_eq!(addr.to_string(), input);
586    }
587
588    #[test]
589    fn case_insensitive_param_lookup() {
590        let addr: SipHeaderAddr = "<sip:user@host>;Tag=ABC;Expires=3600"
591            .parse()
592            .unwrap();
593        assert_eq!(
594            addr.param("tag")
595                .unwrap()
596                .unwrap(),
597            Some(Cow::from("ABC")),
598        );
599        assert_eq!(
600            addr.param("TAG")
601                .unwrap()
602                .unwrap(),
603            Some(Cow::from("ABC")),
604        );
605        assert_eq!(
606            addr.param("expires")
607                .unwrap()
608                .unwrap(),
609            Some(Cow::from("3600")),
610        );
611    }
612
613    #[test]
614    fn tag_convenience_accessor() {
615        let with_tag: SipHeaderAddr = "<sip:user@host>;tag=xyz"
616            .parse()
617            .unwrap();
618        assert_eq!(with_tag.tag(), Some("xyz"));
619
620        let without_tag: SipHeaderAddr = "<sip:user@host>"
621            .parse()
622            .unwrap();
623        assert_eq!(without_tag.tag(), None);
624    }
625
626    #[test]
627    fn builder_new() {
628        let uri: sip_uri::Uri = "sip:alice@example.com"
629            .parse()
630            .unwrap();
631        let addr = SipHeaderAddr::new(uri);
632        assert_eq!(addr.display_name(), None);
633        assert_eq!(
634            addr.params()
635                .count(),
636            0
637        );
638        assert_eq!(addr.to_string(), "<sip:alice@example.com>");
639    }
640
641    #[test]
642    fn builder_with_display_name_and_params() {
643        let uri: sip_uri::Uri = "sip:alice@example.com"
644            .parse()
645            .unwrap();
646        let addr = SipHeaderAddr::new(uri)
647            .with_display_name("Alice")
648            .with_param("tag", Some("abc123"));
649        assert_eq!(addr.display_name(), Some("Alice"));
650        assert_eq!(addr.tag(), Some("abc123"));
651        assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
652    }
653
654    #[test]
655    fn builder_flag_param() {
656        let uri: sip_uri::Uri = "sip:proxy@example.com"
657            .parse()
658            .unwrap();
659        let addr = SipHeaderAddr::new(uri).with_param("lr", None::<String>);
660        assert_eq!(
661            addr.param("lr")
662                .unwrap()
663                .unwrap(),
664            None
665        );
666        assert_eq!(addr.to_string(), "<sip:proxy@example.com>;lr");
667    }
668
669    #[test]
670    fn escaped_quotes_in_display_name() {
671        let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
672        let addr: SipHeaderAddr = input
673            .parse()
674            .unwrap();
675        assert_eq!(addr.display_name(), Some(r#"Say "Hello""#));
676        assert_eq!(addr.tag(), Some("t"));
677    }
678
679    #[test]
680    fn display_roundtrip_escaped_quotes() {
681        let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
682        let addr: SipHeaderAddr = input
683            .parse()
684            .unwrap();
685        assert_eq!(addr.to_string(), input);
686    }
687
688    #[test]
689    fn trailing_semicolon_ignored() {
690        let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;"
691            .parse()
692            .unwrap();
693        assert_eq!(
694            addr.params()
695                .count(),
696            1
697        );
698        assert_eq!(addr.tag(), Some("abc"));
699    }
700
701    #[test]
702    fn display_roundtrip_percent_encoded_params() {
703        let input = "<sip:user@esrp.example.com>;serviceurn=urn%3Aservice%3Apolice";
704        let addr: SipHeaderAddr = input
705            .parse()
706            .unwrap();
707        assert_eq!(addr.to_string(), input);
708    }
709
710    #[test]
711    fn param_invalid_utf8_returns_err() {
712        // %C0%80 is an overlong encoding of U+0000, invalid UTF-8
713        let addr: SipHeaderAddr = "<sip:user@host>;data=%C0%80"
714            .parse()
715            .unwrap();
716        assert!(addr
717            .param("data")
718            .unwrap()
719            .is_err());
720        assert_eq!(addr.param_raw("data"), Some(Some("%C0%80")));
721    }
722
723    #[test]
724    fn param_iso_8859_fallback_to_raw() {
725        // %E9 = é in ISO-8859-1, but lone byte is invalid UTF-8
726        let addr: SipHeaderAddr = "<sip:user@host>;name=%E9"
727            .parse()
728            .unwrap();
729        assert!(addr
730            .param("name")
731            .unwrap()
732            .is_err());
733        assert_eq!(addr.param_raw("name"), Some(Some("%E9")));
734    }
735
736    #[test]
737    fn parse_list_multiple_entries() {
738        let input = r#""Alice" <sip:alice@example.com>;tag=a, <sip:bob@example.com>, sip:carol@example.com"#;
739        let addrs = SipHeaderAddr::parse_list(input).unwrap();
740        assert_eq!(addrs.len(), 3);
741        assert_eq!(addrs[0].display_name(), Some("Alice"));
742        assert_eq!(addrs[0].tag(), Some("a"));
743        assert_eq!(addrs[1].display_name(), None);
744        assert_eq!(
745            addrs[1]
746                .sip_uri()
747                .unwrap()
748                .user(),
749            Some("bob"),
750        );
751        assert_eq!(
752            addrs[2]
753                .sip_uri()
754                .unwrap()
755                .user(),
756            Some("carol"),
757        );
758    }
759
760    #[test]
761    fn parse_list_single_entry() {
762        let addrs = SipHeaderAddr::parse_list("<sip:alice@example.com>").unwrap();
763        assert_eq!(addrs.len(), 1);
764    }
765
766    #[test]
767    fn parse_list_empty_returns_empty() {
768        let addrs = SipHeaderAddr::parse_list("").unwrap();
769        assert!(addrs.is_empty());
770    }
771
772    #[test]
773    fn parse_list_propagates_parse_error() {
774        assert!(SipHeaderAddr::parse_list("not-a-uri, <sip:ok@example.com>").is_err());
775    }
776
777    #[test]
778    fn params_iterator() {
779        let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;lr;expires=60"
780            .parse()
781            .unwrap();
782        let params: Vec<_> = addr
783            .params()
784            .collect();
785        assert_eq!(params.len(), 3);
786        assert_eq!(params[0], ("tag", Some("abc")));
787        assert_eq!(params[1], ("lr", None));
788        assert_eq!(params[2], ("expires", Some("60")));
789    }
790}