use std::borrow::Cow;
use std::fmt::{self, Write as _};
use std::str::{FromStr, Utf8Error};
use percent_encoding::percent_decode_str;
use crate::replaces::{SipReplaces, SipReplacesError};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SipHeaderAddr {
display_name: Option<String>,
uri: sip_uri::Uri,
params: Vec<(String, Option<String>)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseSipHeaderAddrError(pub String);
impl fmt::Display for ParseSipHeaderAddrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid SIP header address: {}", self.0)
}
}
impl std::error::Error for ParseSipHeaderAddrError {}
impl From<sip_uri::ParseUriError> for ParseSipHeaderAddrError {
fn from(e: sip_uri::ParseUriError) -> Self {
Self(e.to_string())
}
}
impl From<sip_uri::ParseSipUriError> for ParseSipHeaderAddrError {
fn from(e: sip_uri::ParseSipUriError) -> Self {
Self(e.to_string())
}
}
impl SipHeaderAddr {
pub fn new(uri: sip_uri::Uri) -> Self {
SipHeaderAddr {
display_name: None,
uri,
params: Vec::new(),
}
}
pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
self.display_name = Some(name.into());
self
}
pub fn try_with_display_name(
mut self,
name: impl Into<String>,
) -> Result<Self, ParseSipHeaderAddrError> {
let name = name.into();
if name.contains(['\r', '\n']) {
return Err(ParseSipHeaderAddrError(
"display name contains CR or LF".to_string(),
));
}
self.display_name = Some(name);
Ok(self)
}
pub fn with_param(mut self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
self.params
.push((
key.into()
.to_ascii_lowercase(),
value.map(Into::into),
));
self
}
pub fn try_with_param(
mut self,
key: impl Into<String>,
value: Option<impl Into<String>>,
) -> Result<Self, ParseSipHeaderAddrError> {
let key = key.into();
if key.is_empty()
|| !key
.chars()
.all(is_token_char)
{
return Err(ParseSipHeaderAddrError(
"parameter name is not a token".to_string(),
));
}
let value = value.map(Into::into);
if let Some(v) = &value {
validate_param_value(v)?;
}
self.params
.push((key.to_ascii_lowercase(), value));
Ok(self)
}
pub fn display_name(&self) -> Option<&str> {
self.display_name
.as_deref()
}
pub fn uri(&self) -> &sip_uri::Uri {
&self.uri
}
pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
self.uri
.as_sip()
}
pub fn tel_uri(&self) -> Option<&sip_uri::TelUri> {
self.uri
.as_tel()
}
pub fn urn_uri(&self) -> Option<&sip_uri::UrnUri> {
self.uri
.as_urn()
}
pub fn params(&self) -> impl Iterator<Item = (&str, Option<&str>)> {
self.params
.iter()
.map(|(k, v)| (k.as_str(), v.as_deref()))
}
pub fn param(&self, name: &str) -> Option<Result<Option<Cow<'_, str>>, Utf8Error>> {
let needle = name.to_ascii_lowercase();
self.params
.iter()
.find(|(k, _)| *k == needle)
.map(|(_, v)| match v {
Some(raw) => percent_decode_str(raw)
.decode_utf8()
.map(Some),
None => Ok(None),
})
}
pub fn param_raw(&self, name: &str) -> Option<Option<&str>> {
let needle = name.to_ascii_lowercase();
self.params
.iter()
.find(|(k, _)| *k == needle)
.map(|(_, v)| v.as_deref())
}
pub fn replaces(&self) -> Option<Result<SipReplaces, SipReplacesError>> {
let value = self
.sip_uri()?
.header("Replaces")?;
Some(SipReplaces::parse_uri_header(value))
}
pub fn parse_list(raw: &str) -> Result<Vec<SipHeaderAddr>, ParseSipHeaderAddrError> {
if raw
.trim()
.is_empty()
{
return Ok(Vec::new());
}
crate::split_comma_entries(raw)
.into_iter()
.map(|entry| {
entry
.trim()
.parse()
})
.collect()
}
pub fn tag(&self) -> Option<&str> {
self.param_raw("tag")
.flatten()
}
}
fn parse_quoted_string(s: &str) -> Result<(String, &str), String> {
if !s.starts_with('"') {
return Err("expected opening quote".into());
}
let mut result = String::new();
let mut chars = s[1..].char_indices();
while let Some((i, c)) = chars.next() {
match c {
'"' => {
return Ok((result, &s[i + 2..]));
}
'\\' => {
let (_, escaped) = chars
.next()
.ok_or("unterminated escape in quoted string")?;
result.push(escaped);
}
_ => {
result.push(c);
}
}
}
Err("unterminated quoted string".into())
}
fn extract_angle_uri(s: &str) -> Option<(&str, &str)> {
let s = s.strip_prefix('<')?;
let end = s.find('>')?;
Some((&s[..end], &s[end + 1..]))
}
fn parse_header_params(s: &str) -> Vec<(String, Option<String>)> {
crate::parse_params(s)
.into_iter()
.map(|p| {
(
p.key
.to_ascii_lowercase(),
p.value
.map(str::to_string),
)
})
.collect()
}
fn is_token_char(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'-' | '.' | '!' | '%' | '*' | '_' | '+' | '`' | '\'' | '~'
)
}
fn needs_quoting(name: &str) -> bool {
!name
.chars()
.all(is_token_char)
}
fn is_quoted_string(v: &str) -> bool {
let Some(inner) = v.strip_prefix('"') else {
return false;
};
let mut chars = inner.chars();
while let Some(c) = chars.next() {
match c {
'"' => {
return chars
.as_str()
.is_empty()
}
'\\' => match chars.next() {
Some(e) if e.is_ascii() && !matches!(e, '\r' | '\n') => {}
_ => return false,
},
'\r' | '\n' => return false,
c if crate::is_quoted_pair_only(c) => return false,
_ => {}
}
}
false
}
fn validate_param_value(v: &str) -> Result<(), ParseSipHeaderAddrError> {
if v.is_empty() {
return Err(ParseSipHeaderAddrError(
"parameter value is empty".to_string(),
));
}
let host_like = v
.chars()
.all(|c| is_token_char(c) || matches!(c, ':' | '[' | ']'));
if host_like || is_quoted_string(v) {
Ok(())
} else {
Err(ParseSipHeaderAddrError(format!(
"parameter value ({} bytes) is not a token, host or quoted-string",
v.len()
)))
}
}
impl FromStr for SipHeaderAddr {
type Err = ParseSipHeaderAddrError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let err = |msg: &str| ParseSipHeaderAddrError(msg.to_string());
let s = input.trim();
if s.is_empty() {
return Err(err("empty input"));
}
if s.starts_with('"') {
let (display_name, rest) = parse_quoted_string(s).map_err(|e| err(&e))?;
let rest = rest.trim_start();
let (uri_str, trailing) = extract_angle_uri(rest)
.ok_or_else(|| err("expected '<URI>' after quoted display name"))?;
let uri: sip_uri::Uri = uri_str.parse()?;
let display_name = if display_name.is_empty() {
None
} else {
Some(display_name)
};
let params = parse_header_params(trailing);
return Ok(SipHeaderAddr {
display_name,
uri,
params,
});
}
if s.starts_with('<') {
let (uri_str, trailing) = extract_angle_uri(s).ok_or_else(|| err("unclosed '<'"))?;
let uri: sip_uri::Uri = uri_str.parse()?;
let params = parse_header_params(trailing);
return Ok(SipHeaderAddr {
display_name: None,
uri,
params,
});
}
if let Some(angle_start) = s.find('<') {
let display_name = s[..angle_start].trim();
let display_name = if display_name.is_empty() {
None
} else {
Some(display_name.to_string())
};
let (uri_str, trailing) =
extract_angle_uri(&s[angle_start..]).ok_or_else(|| err("unclosed '<'"))?;
let uri: sip_uri::Uri = uri_str.parse()?;
let params = parse_header_params(trailing);
return Ok(SipHeaderAddr {
display_name,
uri,
params,
});
}
let uri: sip_uri::Uri = s.parse()?;
Ok(SipHeaderAddr {
display_name: None,
uri,
params: Vec::new(),
})
}
}
impl fmt::Display for SipHeaderAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self
.display_name
.as_deref()
{
Some(name) if !name.is_empty() => {
if needs_quoting(name) {
crate::write_quoted_pair(f, name)?;
f.write_char(' ')?;
} else {
write!(f, "{name} ")?;
}
write!(f, "<{}>", self.uri)?;
}
_ => {
write!(f, "<{}>", self.uri)?;
}
}
for (key, value) in &self.params {
crate::write_param(f, key, value.as_deref(), false)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
#[test]
fn quoted_display_name_with_tag() {
let addr: SipHeaderAddr = r#""Alice" <sip:alice@example.com>;tag=abc123"#
.parse()
.unwrap();
assert_eq!(addr.display_name(), Some("Alice"));
assert!(addr
.sip_uri()
.is_some());
assert_eq!(addr.tag(), Some("abc123"));
}
#[test]
fn angle_bracket_no_name_multiple_params() {
let addr: SipHeaderAddr = "<sip:user@host>;tag=xyz;expires=3600"
.parse()
.unwrap();
assert_eq!(addr.display_name(), None);
assert_eq!(addr.tag(), Some("xyz"));
assert_eq!(
addr.param("expires")
.unwrap()
.unwrap(),
Some(Cow::from("3600")),
);
}
#[test]
fn bare_addr_spec_no_params() {
let addr: SipHeaderAddr = "sip:user@host"
.parse()
.unwrap();
assert_eq!(addr.display_name(), None);
assert!(addr
.sip_uri()
.is_some());
assert_eq!(
addr.params()
.count(),
0
);
}
#[test]
fn unquoted_display_name_with_params() {
let addr: SipHeaderAddr = "Alice <sip:alice@example.com>;tag=abc"
.parse()
.unwrap();
assert_eq!(addr.display_name(), Some("Alice"));
assert_eq!(addr.tag(), Some("abc"));
}
#[test]
fn ng911_refer_to_serviceurn() {
let input = "<sip:user@esrp.example.com?Call-Info=x>;serviceurn=urn%3Aservice%3Apolice";
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.display_name(), None);
assert_eq!(
addr.param("serviceurn")
.unwrap()
.unwrap(),
Some(Cow::from("urn:service:police")),
);
assert_eq!(
addr.param_raw("serviceurn"),
Some(Some("urn%3Aservice%3Apolice")),
);
let sip = addr
.sip_uri()
.unwrap();
assert_eq!(
sip.host()
.to_string(),
"esrp.example.com"
);
}
#[test]
fn p_asserted_identity_uri_params_no_header_params() {
let input = r#""EXAMPLE CO" <sip:+15551234567;cpc=emergency@198.51.100.1;user=phone>"#;
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.display_name(), Some("EXAMPLE CO"));
assert_eq!(
addr.params()
.count(),
0
);
let sip = addr
.sip_uri()
.unwrap();
assert_eq!(sip.user(), Some("+15551234567"));
assert_eq!(sip.param("user"), Some(&Some("phone".to_string())));
}
#[test]
fn tel_uri_with_header_params() {
let addr: SipHeaderAddr = "<tel:+15551234567>;expires=3600"
.parse()
.unwrap();
assert!(addr
.tel_uri()
.is_some());
assert_eq!(
addr.param("expires")
.unwrap()
.unwrap(),
Some(Cow::from("3600")),
);
}
#[test]
fn flag_param_no_value() {
let addr: SipHeaderAddr = "<sip:user@host>;lr;tag=abc"
.parse()
.unwrap();
assert_eq!(
addr.param("lr")
.unwrap()
.unwrap(),
None
);
assert_eq!(addr.tag(), Some("abc"));
}
#[test]
fn urn_uri_no_params() {
let addr: SipHeaderAddr = "<urn:service:sos>"
.parse()
.unwrap();
assert!(addr
.urn_uri()
.is_some());
assert_eq!(
addr.params()
.count(),
0
);
}
#[test]
fn empty_input_fails() {
assert!(""
.parse::<SipHeaderAddr>()
.is_err());
}
#[test]
fn display_roundtrip_quoted_name_with_params() {
let input = r#""Alice" <sip:alice@example.com>;tag=abc123"#;
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
}
#[test]
fn display_roundtrip_name_requiring_quotes() {
let input = r#""Alice Smith" <sip:alice@example.com>;tag=abc123"#;
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), input);
}
#[test]
fn display_roundtrip_no_name_with_params() {
let input = "<sip:user@host>;tag=xyz;expires=3600";
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), input);
}
#[test]
fn display_roundtrip_bare_uri() {
let input = "sip:user@host";
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), "<sip:user@host>");
}
#[test]
fn display_roundtrip_flag_param() {
let input = "<sip:user@host>;lr;tag=abc";
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), input);
}
#[test]
fn case_insensitive_param_lookup() {
let addr: SipHeaderAddr = "<sip:user@host>;Tag=ABC;Expires=3600"
.parse()
.unwrap();
assert_eq!(
addr.param("tag")
.unwrap()
.unwrap(),
Some(Cow::from("ABC")),
);
assert_eq!(
addr.param("TAG")
.unwrap()
.unwrap(),
Some(Cow::from("ABC")),
);
assert_eq!(
addr.param("expires")
.unwrap()
.unwrap(),
Some(Cow::from("3600")),
);
}
#[test]
fn tag_convenience_accessor() {
let with_tag: SipHeaderAddr = "<sip:user@host>;tag=xyz"
.parse()
.unwrap();
assert_eq!(with_tag.tag(), Some("xyz"));
let without_tag: SipHeaderAddr = "<sip:user@host>"
.parse()
.unwrap();
assert_eq!(without_tag.tag(), None);
}
#[test]
fn builder_new() {
let uri: sip_uri::Uri = "sip:alice@example.com"
.parse()
.unwrap();
let addr = SipHeaderAddr::new(uri);
assert_eq!(addr.display_name(), None);
assert_eq!(
addr.params()
.count(),
0
);
assert_eq!(addr.to_string(), "<sip:alice@example.com>");
}
fn example_addr() -> SipHeaderAddr {
SipHeaderAddr::new(
"sip:alice@example.com"
.parse()
.unwrap(),
)
}
#[test]
fn try_builder_display_name_and_params() {
let addr = example_addr()
.try_with_display_name("Alice")
.unwrap()
.try_with_param("Tag", Some("abc123"))
.unwrap()
.try_with_param("lr", None::<String>)
.unwrap();
assert_eq!(addr.display_name(), Some("Alice"));
assert_eq!(addr.tag(), Some("abc123"));
assert_eq!(
addr.to_string(),
"Alice <sip:alice@example.com>;tag=abc123;lr"
);
}
#[test]
fn try_with_display_name_accepts_qdtext_and_quoted_pair() {
for name in [
"José",
r#"Say "Hi""#,
"a\\b",
"a\tb",
"a\u{1}b",
"a\u{7f}b",
"",
] {
let addr = example_addr()
.try_with_display_name(name)
.unwrap();
let reparsed: SipHeaderAddr = addr
.to_string()
.parse()
.unwrap();
let expected = if name.is_empty() { None } else { Some(name) };
assert_eq!(reparsed.display_name(), expected, "{name:?}");
}
}
#[test]
fn display_escapes_control_chars_as_quoted_pair() {
let addr = example_addr()
.try_with_display_name("a\u{1}b")
.unwrap();
assert_eq!(addr.to_string(), "\"a\\\u{1}b\" <sip:alice@example.com>");
}
#[test]
fn try_with_display_name_rejects_line_breaks() {
for name in ["a\r\nSubject: evil", "a\nSubject: evil", "a\rSubject: evil"] {
let e = example_addr()
.try_with_display_name(name)
.unwrap_err();
assert!(
!e.to_string()
.contains("Subject"),
"{e}"
);
}
}
#[test]
fn try_with_param_accepts_token_host_and_quoted() {
for (key, value) in [
("tag", Some("abc123")),
("lr", None),
("serviceurn", Some("urn%3Aservice%3Apolice")),
("received", Some("[2001:db8::1]")),
("maddr", Some("198.51.100.1:5060")),
("foo", Some(r#""a;b, c""#)),
("+sip.instance", Some(r#""<urn:uuid:TEST>""#)),
("t", Some(r#""say \"hi\"""#)),
("e", Some(r#""""#)),
] {
let addr = example_addr()
.try_with_param(key, value)
.unwrap();
assert_eq!(addr.param_raw(key), Some(value), "{key}");
let reparsed: SipHeaderAddr = addr
.to_string()
.parse()
.unwrap();
assert_eq!(reparsed, addr, "{key}");
}
}
#[test]
fn try_with_param_rejects_bad_key() {
for key in ["", "a b", "a;b", "a=b", "a\r\nSubject: evil", "a\"b"] {
let e = example_addr()
.try_with_param(key, Some("x"))
.unwrap_err();
assert!(
!e.to_string()
.contains("Subject"),
"{e}"
);
}
}
#[test]
fn try_with_param_rejects_bad_value() {
for value in [
"",
"a;b",
"a b",
"a,b",
"a>b",
"\"unterminated",
"\"a\"b\"",
"\"a\\\"",
"\"a\r\nb\"",
"x\r\nSubject: evil",
"\"a\\\nb\"",
] {
let e = example_addr()
.try_with_param("k", Some(value))
.unwrap_err();
assert!(
!e.to_string()
.contains("Subject"),
"{e}"
);
}
}
#[test]
fn builder_with_display_name_and_params() {
let uri: sip_uri::Uri = "sip:alice@example.com"
.parse()
.unwrap();
let addr = SipHeaderAddr::new(uri)
.with_display_name("Alice")
.with_param("tag", Some("abc123"));
assert_eq!(addr.display_name(), Some("Alice"));
assert_eq!(addr.tag(), Some("abc123"));
assert_eq!(addr.to_string(), "Alice <sip:alice@example.com>;tag=abc123");
}
#[test]
fn builder_flag_param() {
let uri: sip_uri::Uri = "sip:proxy@example.com"
.parse()
.unwrap();
let addr = SipHeaderAddr::new(uri).with_param("lr", None::<String>);
assert_eq!(
addr.param("lr")
.unwrap()
.unwrap(),
None
);
assert_eq!(addr.to_string(), "<sip:proxy@example.com>;lr");
}
#[test]
fn escaped_quotes_in_display_name() {
let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.display_name(), Some(r#"Say "Hello""#));
assert_eq!(addr.tag(), Some("t"));
}
#[test]
fn display_roundtrip_escaped_quotes() {
let input = r#""Say \"Hello\"" <sip:u@h>;tag=t"#;
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), input);
}
#[test]
fn trailing_semicolon_ignored() {
let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;"
.parse()
.unwrap();
assert_eq!(
addr.params()
.count(),
1
);
assert_eq!(addr.tag(), Some("abc"));
}
#[test]
fn display_roundtrip_percent_encoded_params() {
let input = "<sip:user@esrp.example.com>;serviceurn=urn%3Aservice%3Apolice";
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), input);
}
#[test]
fn param_invalid_utf8_returns_err() {
let addr: SipHeaderAddr = "<sip:user@host>;data=%C0%80"
.parse()
.unwrap();
assert!(addr
.param("data")
.unwrap()
.is_err());
assert_eq!(addr.param_raw("data"), Some(Some("%C0%80")));
}
#[test]
fn param_iso_8859_fallback_to_raw() {
let addr: SipHeaderAddr = "<sip:user@host>;name=%E9"
.parse()
.unwrap();
assert!(addr
.param("name")
.unwrap()
.is_err());
assert_eq!(addr.param_raw("name"), Some(Some("%E9")));
}
#[test]
fn parse_list_multiple_entries() {
let input = r#""Alice" <sip:alice@example.com>;tag=a, <sip:bob@example.com>, sip:carol@example.com"#;
let addrs = SipHeaderAddr::parse_list(input).unwrap();
assert_eq!(addrs.len(), 3);
assert_eq!(addrs[0].display_name(), Some("Alice"));
assert_eq!(addrs[0].tag(), Some("a"));
assert_eq!(addrs[1].display_name(), None);
assert_eq!(
addrs[1]
.sip_uri()
.unwrap()
.user(),
Some("bob"),
);
assert_eq!(
addrs[2]
.sip_uri()
.unwrap()
.user(),
Some("carol"),
);
}
#[test]
fn parse_list_single_entry() {
let addrs = SipHeaderAddr::parse_list("<sip:alice@example.com>").unwrap();
assert_eq!(addrs.len(), 1);
}
#[test]
fn parse_list_empty_returns_empty() {
let addrs = SipHeaderAddr::parse_list("").unwrap();
assert!(addrs.is_empty());
}
#[test]
fn parse_list_propagates_parse_error() {
assert!(SipHeaderAddr::parse_list("not-a-uri, <sip:ok@example.com>").is_err());
}
#[test]
fn replaces_uri_header() {
let addr: SipHeaderAddr =
"<sip:bob@203.0.113.9?Replaces=abc123%40203.0.113.5%3Bto-tag%3Dt1%3Bfrom-tag%3Df1>"
.parse()
.unwrap();
let r = addr
.replaces()
.unwrap()
.unwrap();
assert_eq!(r.call_id(), "abc123@203.0.113.5");
assert_eq!(r.host(), Some("203.0.113.5"));
assert_eq!(r.to_tag(), "t1");
assert_eq!(r.from_tag(), "f1");
}
#[test]
fn replaces_uri_header_absent() {
let addr: SipHeaderAddr = "<sip:bob@203.0.113.9>"
.parse()
.unwrap();
assert!(addr
.replaces()
.is_none());
}
#[test]
fn host_only_ipv4_with_feature_tag_param() {
let input =
"<sip:198.51.100.7:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
let addr: SipHeaderAddr = input
.parse()
.unwrap();
let sip = addr
.sip_uri()
.unwrap();
assert_eq!(sip.user(), None);
assert_eq!(
sip.host()
.to_string(),
"198.51.100.7"
);
assert_eq!(sip.port(), Some(5060));
assert_eq!(sip.param("transport"), Some(&Some("udp".to_string())));
assert_eq!(
addr.param_raw("+urn%3Aemergency%3Amedia-feature.psap-call-control"),
Some(None),
);
}
#[test]
fn host_only_ipv6_with_feature_tag_param() {
let input =
"<sip:[2001:db8::7]:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
let addr: SipHeaderAddr = input
.parse()
.unwrap();
let sip = addr
.sip_uri()
.unwrap();
assert_eq!(sip.user(), None);
assert_eq!(
sip.host()
.to_string(),
"[2001:db8::7]"
);
assert_eq!(sip.port(), Some(5060));
assert_eq!(
addr.param_raw("+urn%3Aemergency%3Amedia-feature.psap-call-control"),
Some(None),
);
}
#[test]
fn parse_list_host_only_ipv4_with_feature_tag_param() {
let input =
"<sip:198.51.100.7:5060;transport=udp>;+urn%3Aemergency%3Amedia-feature.psap-call-control";
let addrs = SipHeaderAddr::parse_list(input).unwrap();
assert_eq!(addrs.len(), 1);
assert_eq!(
addrs[0]
.sip_uri()
.unwrap()
.host()
.to_string(),
"198.51.100.7"
);
}
#[test]
fn display_quotes_non_token_display_name() {
for name in ["José", "A/B", "a=b", "x(y)", "a?b"] {
let input = format!(r#""{name}" <sip:a@example.com>"#);
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), input);
}
}
#[test]
fn display_keeps_token_display_name_bare() {
let input = "A-b.c!%*_+`'~ <sip:a@example.com>";
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.to_string(), input);
}
#[test]
fn params_sws_around_semi_and_equal() {
let addr: SipHeaderAddr = "<sip:a@example.com> ; tag = x"
.parse()
.unwrap();
assert_eq!(addr.tag(), Some("x"));
assert_eq!(
addr.params()
.collect::<Vec<_>>(),
vec![("tag", Some("x"))]
);
}
#[test]
fn params_quoted_value_keeps_semicolon() {
let input = r#"<sip:a@example.com>;foo="a;b";tag=x"#;
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(addr.param_raw("foo"), Some(Some(r#""a;b""#)));
assert_eq!(addr.tag(), Some("x"));
assert_eq!(addr.to_string(), input);
}
#[test]
fn params_quoted_instance_kept_raw() {
let input = r#"<sip:a@198.51.100.1>;+sip.instance="<urn:uuid:00000000-0000-0000-0000-000000000001>";expires=60"#;
let addr: SipHeaderAddr = input
.parse()
.unwrap();
assert_eq!(
addr.param_raw("+sip.instance"),
Some(Some(r#""<urn:uuid:00000000-0000-0000-0000-000000000001>""#))
);
assert_eq!(addr.to_string(), input);
}
#[test]
fn params_iterator() {
let addr: SipHeaderAddr = "<sip:user@host>;tag=abc;lr;expires=60"
.parse()
.unwrap();
let params: Vec<_> = addr
.params()
.collect();
assert_eq!(params.len(), 3);
assert_eq!(params[0], ("tag", Some("abc")));
assert_eq!(params[1], ("lr", None));
assert_eq!(params[2], ("expires", Some("60")));
}
}