Skip to main content

sip_header/
contact.rs

1//! SIP Contact header value parser (RFC 3261 §20.10).
2//!
3//! Contact can be either `*` (wildcard, used in REGISTER with Expires: 0)
4//! or a comma-separated list of `name-addr / addr-spec` entries with
5//! optional parameters.
6
7use crate::header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
8use std::fmt;
9
10/// A single Contact header value: either the `*` wildcard or an address.
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ContactValue {
14    /// The `*` wildcard (RFC 3261 §10.2.2, used in REGISTER).
15    Wildcard,
16    /// A `name-addr` or `addr-spec` with optional contact parameters.
17    Addr(Box<SipHeaderAddr>),
18}
19
20impl fmt::Display for ContactValue {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::Wildcard => f.write_str("*"),
24            Self::Addr(addr) => write!(f, "{addr}"),
25        }
26    }
27}
28
29/// Parse a single contact entry (after comma-splitting).
30fn parse_contact_entry(raw: &str) -> Result<ContactValue, ParseSipHeaderAddrError> {
31    let trimmed = raw.trim();
32    if trimmed == "*" {
33        Ok(ContactValue::Wildcard)
34    } else {
35        trimmed
36            .parse::<SipHeaderAddr>()
37            .map(|a| ContactValue::Addr(Box::new(a)))
38    }
39}
40
41/// Parse a comma-separated Contact header value into a list of [`ContactValue`].
42pub fn parse_contact_list(raw: &str) -> Result<Vec<ContactValue>, ParseSipHeaderAddrError> {
43    parse_contact_entries(crate::split_comma_entries(raw.trim()))
44}
45
46/// Build from entries a transport already split; each is `*` or one `contact-param`.
47///
48/// `*` is only valid alone (RFC 3261 §20.10 `STAR / (contact-param *(COMMA contact-param))`).
49pub fn parse_contact_entries<'a>(
50    entries: impl IntoIterator<Item = &'a str>,
51) -> Result<Vec<ContactValue>, ParseSipHeaderAddrError> {
52    let values: Vec<_> = entries
53        .into_iter()
54        .map(parse_contact_entry)
55        .collect::<Result<_, _>>()?;
56    if values.len() > 1
57        && values
58            .iter()
59            .any(|v| matches!(v, ContactValue::Wildcard))
60    {
61        return Err(ParseSipHeaderAddrError(
62            "Contact wildcard must be the only value".to_string(),
63        ));
64    }
65    Ok(values)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn wildcard() {
74        let contacts = parse_contact_list("*").unwrap();
75        assert_eq!(contacts.len(), 1);
76        assert!(matches!(contacts[0], ContactValue::Wildcard));
77    }
78
79    #[test]
80    fn single_addr() {
81        let contacts = parse_contact_list("<sip:alice@198.51.100.1>").unwrap();
82        assert_eq!(contacts.len(), 1);
83        match &contacts[0] {
84            ContactValue::Addr(addr) => {
85                assert!(addr
86                    .uri()
87                    .to_string()
88                    .contains("alice"));
89            }
90            _ => panic!("expected Addr"),
91        }
92    }
93
94    #[test]
95    fn multiple_addrs() {
96        let contacts =
97            parse_contact_list("<sip:alice@198.51.100.1>, \"Bob\" <sip:bob@198.51.100.2>").unwrap();
98        assert_eq!(contacts.len(), 2);
99        match &contacts[1] {
100            ContactValue::Addr(addr) => {
101                assert_eq!(addr.display_name(), Some("Bob"));
102            }
103            _ => panic!("expected Addr"),
104        }
105    }
106
107    #[test]
108    fn display_wildcard() {
109        assert_eq!(ContactValue::Wildcard.to_string(), "*");
110    }
111
112    #[test]
113    fn display_addr() {
114        let addr = "sip:alice@198.51.100.1"
115            .parse::<sip_uri::Uri>()
116            .unwrap();
117        let cv = ContactValue::Addr(Box::new(SipHeaderAddr::new(addr)));
118        assert!(cv
119            .to_string()
120            .contains("alice"));
121    }
122
123    #[test]
124    fn entries_matches_list() {
125        let a = "<sip:alice@198.51.100.1>";
126        let b = "\"Bob\" <sip:bob@example.com>;expires=60";
127        let split = parse_contact_entries([a, b]).unwrap();
128        let joined = parse_contact_list(&format!("{a}, {b}")).unwrap();
129        assert_eq!(split, joined);
130        assert_eq!(split.len(), 2);
131    }
132
133    #[test]
134    fn entries_empty_is_empty_list() {
135        let contacts = parse_contact_entries(std::iter::empty::<&str>()).unwrap();
136        assert!(contacts.is_empty());
137    }
138
139    #[test]
140    fn entries_lone_wildcard() {
141        let contacts = parse_contact_entries(["*"]).unwrap();
142        assert_eq!(contacts, vec![ContactValue::Wildcard]);
143    }
144
145    #[test]
146    fn wildcard_mixed_with_addr_is_error() {
147        assert!(parse_contact_entries(["*", "<sip:alice@example.com>"]).is_err());
148        assert!(parse_contact_entries(["<sip:alice@example.com>", "*"]).is_err());
149        assert!(parse_contact_list("*, <sip:alice@example.com>").is_err());
150    }
151}