sip-header 0.3.12

SIP header field parsers: Via, Warning, Auth, Accept, Contact, Call-Info, History-Info, Geolocation, Security, and full IANA header catalog
Documentation
//! SIP Contact header value parser (RFC 3261 §20.10).
//!
//! Contact can be either `*` (wildcard, used in REGISTER with Expires: 0)
//! or a comma-separated list of `name-addr / addr-spec` entries with
//! optional parameters.

use crate::header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
use std::fmt;

/// A single Contact header value: either the `*` wildcard or an address.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContactValue {
    /// The `*` wildcard (RFC 3261 §10.2.2, used in REGISTER).
    Wildcard,
    /// A `name-addr` or `addr-spec` with optional contact parameters.
    Addr(Box<SipHeaderAddr>),
}

impl fmt::Display for ContactValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Wildcard => f.write_str("*"),
            Self::Addr(addr) => write!(f, "{addr}"),
        }
    }
}

/// Parse a single contact entry (after comma-splitting).
fn parse_contact_entry(raw: &str) -> Result<ContactValue, ParseSipHeaderAddrError> {
    let trimmed = raw.trim();
    if trimmed == "*" {
        Ok(ContactValue::Wildcard)
    } else {
        trimmed
            .parse::<SipHeaderAddr>()
            .map(|a| ContactValue::Addr(Box::new(a)))
    }
}

/// Parse a comma-separated Contact header value into a list of [`ContactValue`].
pub fn parse_contact_list(raw: &str) -> Result<Vec<ContactValue>, ParseSipHeaderAddrError> {
    parse_contact_entries(crate::split_comma_entries(raw.trim()))
}

/// Build from entries a transport already split; each is `*` or one `contact-param`.
///
/// `*` is only valid alone (RFC 3261 §20.10 `STAR / (contact-param *(COMMA contact-param))`).
pub fn parse_contact_entries<'a>(
    entries: impl IntoIterator<Item = &'a str>,
) -> Result<Vec<ContactValue>, ParseSipHeaderAddrError> {
    let values: Vec<_> = entries
        .into_iter()
        .map(parse_contact_entry)
        .collect::<Result<_, _>>()?;
    if values.len() > 1
        && values
            .iter()
            .any(|v| matches!(v, ContactValue::Wildcard))
    {
        return Err(ParseSipHeaderAddrError(
            "Contact wildcard must be the only value".to_string(),
        ));
    }
    Ok(values)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wildcard() {
        let contacts = parse_contact_list("*").unwrap();
        assert_eq!(contacts.len(), 1);
        assert!(matches!(contacts[0], ContactValue::Wildcard));
    }

    #[test]
    fn single_addr() {
        let contacts = parse_contact_list("<sip:alice@198.51.100.1>").unwrap();
        assert_eq!(contacts.len(), 1);
        match &contacts[0] {
            ContactValue::Addr(addr) => {
                assert!(addr
                    .uri()
                    .to_string()
                    .contains("alice"));
            }
            _ => panic!("expected Addr"),
        }
    }

    #[test]
    fn multiple_addrs() {
        let contacts =
            parse_contact_list("<sip:alice@198.51.100.1>, \"Bob\" <sip:bob@198.51.100.2>").unwrap();
        assert_eq!(contacts.len(), 2);
        match &contacts[1] {
            ContactValue::Addr(addr) => {
                assert_eq!(addr.display_name(), Some("Bob"));
            }
            _ => panic!("expected Addr"),
        }
    }

    #[test]
    fn display_wildcard() {
        assert_eq!(ContactValue::Wildcard.to_string(), "*");
    }

    #[test]
    fn display_addr() {
        let addr = "sip:alice@198.51.100.1"
            .parse::<sip_uri::Uri>()
            .unwrap();
        let cv = ContactValue::Addr(Box::new(SipHeaderAddr::new(addr)));
        assert!(cv
            .to_string()
            .contains("alice"));
    }

    #[test]
    fn entries_matches_list() {
        let a = "<sip:alice@198.51.100.1>";
        let b = "\"Bob\" <sip:bob@example.com>;expires=60";
        let split = parse_contact_entries([a, b]).unwrap();
        let joined = parse_contact_list(&format!("{a}, {b}")).unwrap();
        assert_eq!(split, joined);
        assert_eq!(split.len(), 2);
    }

    #[test]
    fn entries_empty_is_empty_list() {
        let contacts = parse_contact_entries(std::iter::empty::<&str>()).unwrap();
        assert!(contacts.is_empty());
    }

    #[test]
    fn entries_lone_wildcard() {
        let contacts = parse_contact_entries(["*"]).unwrap();
        assert_eq!(contacts, vec![ContactValue::Wildcard]);
    }

    #[test]
    fn wildcard_mixed_with_addr_is_error() {
        assert!(parse_contact_entries(["*", "<sip:alice@example.com>"]).is_err());
        assert!(parse_contact_entries(["<sip:alice@example.com>", "*"]).is_err());
        assert!(parse_contact_list("*, <sip:alice@example.com>").is_err());
    }
}