use std::fmt;
use percent_encoding::percent_decode_str;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SipReplacesError {
Empty,
InvalidFormat(String),
}
impl fmt::Display for SipReplacesError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "Replaces header is empty"),
Self::InvalidFormat(msg) => write!(f, "Invalid Replaces format: {}", msg),
}
}
}
impl std::error::Error for SipReplacesError {}
impl From<DialogIdError> for SipReplacesError {
fn from(e: DialogIdError) -> Self {
match e {
DialogIdError::Empty => Self::Empty,
DialogIdError::Invalid(msg) => Self::InvalidFormat(msg),
}
}
}
pub(crate) enum DialogIdError {
Empty,
Invalid(String),
}
pub(crate) struct DialogId {
pub call_id: String,
pub first_tag: String,
pub second_tag: String,
pub early_only: bool,
pub params: Vec<(String, Option<String>)>,
}
pub(crate) fn parse_dialog_id(
raw: &str,
first_tag_name: &str,
second_tag_name: &str,
with_early_only: bool,
) -> Result<DialogId, DialogIdError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(DialogIdError::Empty);
}
let mut segments = trimmed.split(';');
let call_id = segments
.next()
.unwrap_or("")
.trim();
if call_id.is_empty() {
return Err(DialogIdError::Invalid("missing call-id".to_string()));
}
let mut first_tag: Option<String> = None;
let mut second_tag: Option<String> = None;
let mut early_only = false;
let mut params = Vec::new();
for segment in segments {
let segment = segment.trim();
if segment.is_empty() {
continue;
}
if let Some((key, value)) = segment.split_once('=') {
let key = key
.trim()
.to_ascii_lowercase();
let value = value.trim();
let slot = if key == first_tag_name {
Some(&mut first_tag)
} else if key == second_tag_name {
Some(&mut second_tag)
} else {
None
};
match slot {
Some(slot) => {
if value.is_empty() {
return Err(DialogIdError::Invalid(format!("empty {}", key)));
}
if slot
.replace(value.to_string())
.is_some()
{
return Err(DialogIdError::Invalid(format!("duplicate {}", key)));
}
}
None => params.push((key, Some(value.to_string()))),
}
} else {
let key = segment.to_ascii_lowercase();
if with_early_only && key == "early-only" {
early_only = true;
} else {
params.push((key, None));
}
}
}
let first_tag =
first_tag.ok_or_else(|| DialogIdError::Invalid(format!("missing {}", first_tag_name)))?;
let second_tag =
second_tag.ok_or_else(|| DialogIdError::Invalid(format!("missing {}", second_tag_name)))?;
Ok(DialogId {
call_id: call_id.to_string(),
first_tag,
second_tag,
early_only,
params,
})
}
pub(crate) fn decode_uri_header_value(raw: &str) -> Result<String, DialogIdError> {
percent_decode_str(raw)
.decode_utf8()
.map(|s| s.into_owned())
.map_err(|e| DialogIdError::Invalid(format!("percent-decoded value is not UTF-8: {}", e)))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SipReplaces {
call_id: String,
to_tag: String,
from_tag: String,
early_only: bool,
params: Vec<(String, Option<String>)>,
uri_header_framing: bool,
}
impl SipReplaces {
pub fn parse(raw: &str) -> Result<Self, SipReplacesError> {
let id = parse_dialog_id(raw, "to-tag", "from-tag", true)?;
Ok(Self {
call_id: id.call_id,
to_tag: id.first_tag,
from_tag: id.second_tag,
early_only: id.early_only,
params: id.params,
uri_header_framing: false,
})
}
pub fn parse_uri_header(raw: &str) -> Result<Self, SipReplacesError> {
let decoded = decode_uri_header_value(raw)?;
let mut parsed = Self::parse(&decoded)?;
parsed.uri_header_framing = true;
Ok(parsed)
}
pub fn call_id(&self) -> &str {
&self.call_id
}
pub fn host(&self) -> Option<&str> {
self.call_id
.split_once('@')
.map(|(_, host)| host)
}
pub fn to_tag(&self) -> &str {
&self.to_tag
}
pub fn from_tag(&self) -> &str {
&self.from_tag
}
pub fn early_only(&self) -> bool {
self.early_only
}
pub fn params(&self) -> &[(String, Option<String>)] {
&self.params
}
pub fn param(&self, key: &str) -> Option<Option<&str>> {
let key_lower = key.to_ascii_lowercase();
self.params
.iter()
.find(|(k, _)| k == &key_lower)
.map(|(_, v)| v.as_deref())
}
fn wire_form(&self) -> String {
let mut s = format!(
"{};to-tag={};from-tag={}",
self.call_id, self.to_tag, self.from_tag
);
if self.early_only {
s.push_str(";early-only");
}
write_params(&mut s, &self.params);
s
}
}
pub(crate) fn write_params(s: &mut String, params: &[(String, Option<String>)]) {
for (key, value) in params {
s.push(';');
s.push_str(key);
if let Some(value) = value {
s.push('=');
s.push_str(value);
}
}
}
impl fmt::Display for SipReplaces {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let wire = self.wire_form();
if self.uri_header_framing {
f.write_str(&sip_uri::encode_uri_header(&wire))
} else {
f.write_str(&wire)
}
}
}
impl_from_str_via_parse!(SipReplaces, SipReplacesError);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic() {
let r = SipReplaces::parse("abc123@203.0.113.5;to-tag=t1;from-tag=f1").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");
assert!(!r.early_only());
}
#[test]
fn host_none_without_at() {
let r = SipReplaces::parse("abc123;to-tag=t1;from-tag=f1").unwrap();
assert_eq!(r.call_id(), "abc123");
assert_eq!(r.host(), None);
}
#[test]
fn early_only_flag() {
let r = SipReplaces::parse("abc@example.com;to-tag=t1;from-tag=f1;early-only").unwrap();
assert!(r.early_only());
}
#[test]
fn param_names_case_insensitive_values_preserved() {
let r = SipReplaces::parse("abc@example.com;TO-TAG=T1abc;From-Tag=F1xyz").unwrap();
assert_eq!(r.to_tag(), "T1abc");
assert_eq!(r.from_tag(), "F1xyz");
}
#[test]
fn generic_params_preserved() {
let r = SipReplaces::parse("abc@example.com;to-tag=t1;from-tag=f1;foo=bar;flag").unwrap();
assert_eq!(r.param("foo"), Some(Some("bar")));
assert_eq!(r.param("flag"), Some(None));
assert_eq!(r.param("missing"), None);
assert_eq!(
r.params()
.len(),
2
);
}
#[test]
fn missing_to_tag_fails() {
assert!(SipReplaces::parse("abc@example.com;from-tag=f1").is_err());
}
#[test]
fn missing_from_tag_fails() {
assert!(SipReplaces::parse("abc@example.com;to-tag=t1").is_err());
}
#[test]
fn duplicate_to_tag_fails() {
assert!(SipReplaces::parse("abc@example.com;to-tag=t1;to-tag=t2;from-tag=f1").is_err());
}
#[test]
fn empty_fails() {
assert!(matches!(
SipReplaces::parse(""),
Err(SipReplacesError::Empty)
));
assert!(matches!(
SipReplaces::parse(" "),
Err(SipReplacesError::Empty)
));
}
#[test]
fn parse_uri_header_encoded() {
let r = SipReplaces::parse_uri_header("abc123%40203.0.113.5%3Bto-tag%3Dt1%3Bfrom-tag%3Df1")
.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 parse_uri_header_lowercase_hex() {
let r = SipReplaces::parse_uri_header("abc123%40203.0.113.5%3bto-tag%3dt1%3bfrom-tag%3df1")
.unwrap();
assert_eq!(r.host(), Some("203.0.113.5"));
assert_eq!(r.to_tag(), "t1");
}
#[test]
fn parse_uri_header_early_only() {
let r = SipReplaces::parse_uri_header(
"abc123%40203.0.113.5%3Bto-tag%3Dt1%3Bfrom-tag%3Df1%3Bearly-only",
)
.unwrap();
assert!(r.early_only());
}
#[test]
fn parse_uri_header_invalid_utf8_fails() {
assert!(SipReplaces::parse_uri_header("abc%C0%80;to-tag=t1;from-tag=f1").is_err());
}
#[test]
fn display_roundtrip_wire() {
let input = "abc123@203.0.113.5;to-tag=t1;from-tag=f1;early-only;foo=bar";
let r = SipReplaces::parse(input).unwrap();
assert_eq!(r.to_string(), input);
assert_eq!(SipReplaces::parse(&r.to_string()).unwrap(), r);
}
#[test]
fn display_roundtrip_uri_header() {
let input = "abc123%40203.0.113.5%3Bto-tag%3Dt1%3Bfrom-tag%3Df1";
let r = SipReplaces::parse_uri_header(input).unwrap();
assert_eq!(r.to_string(), input);
assert_eq!(SipReplaces::parse_uri_header(&r.to_string()).unwrap(), r);
}
#[test]
fn from_str_is_wire_framing() {
let r: SipReplaces = "abc123@203.0.113.5;to-tag=t1;from-tag=f1"
.parse()
.unwrap();
assert_eq!(r.call_id(), "abc123@203.0.113.5");
}
}