use std::fmt;
use crate::diagnostic::{Field, ParseWarning, Parsed, WarningCode};
use crate::error::{FaultCode, ParseError};
use crate::traits::{sealed, HeaderParse};
fn is_word_char(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'-' | '.'
| '!'
| '%'
| '*'
| '_'
| '+'
| '`'
| '\''
| '~'
| '('
| ')'
| '<'
| '>'
| ':'
| '\\'
| '"'
| '/'
| '['
| ']'
| '?'
| '{'
| '}'
)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(into = "String"))]
pub struct SipCallId {
value: String,
at: Option<usize>,
}
impl SipCallId {
pub fn new(value: impl AsRef<str>) -> Result<Self, ParseError> {
let value = value.as_ref();
if value.is_empty() {
return Err(ParseError::empty(Field::CallId));
}
let mut warnings = Vec::new();
let id = read(value.to_owned(), 0, &mut warnings);
match warnings.first() {
Some(w) => Err(ParseError::malformed(
Field::CallId,
FaultCode::InvalidChar,
w.position,
)),
None => Ok(id),
}
}
pub fn as_str(&self) -> &str {
&self.value
}
pub fn local(&self) -> &str {
&self.value[..self
.at
.unwrap_or(
self.value
.len(),
)]
}
pub fn host(&self) -> Option<&str> {
self.at
.map(|at| &self.value[at + 1..])
}
}
fn read(value: String, offset: usize, warnings: &mut Vec<ParseWarning>) -> SipCallId {
let at = value.find('@');
report_breach(&value, offset, warnings);
SipCallId { value, at }
}
pub(crate) fn report_breach(value: &str, offset: usize, warnings: &mut Vec<ParseWarning>) {
let (local, host) = match value.split_once('@') {
Some((local, host)) => (local, Some(host)),
None => (value, None),
};
let words = std::iter::once((0, local)).chain(host.map(|h| (local.len() + 1, h)));
for (start, word) in words {
let breach = if word.is_empty() {
Some(0)
} else {
word.find(|c| !is_word_char(c))
};
if let Some(i) = breach {
warnings.push(
ParseWarning::new(Field::CallId, WarningCode::InvalidToken).at(offset + start + i),
);
return;
}
}
}
impl fmt::Display for SipCallId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.value)
}
}
impl AsRef<str> for SipCallId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl sealed::Sealed for SipCallId {}
impl HeaderParse for SipCallId {
fn parse_with_warnings(input: &str) -> Result<Parsed<Self>, ParseError> {
crate::scrub::parse_scrubbed(input, |s| {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(ParseError::empty(Field::CallId));
}
let mut warnings = Vec::new();
let id = read(
trimmed.to_string(),
crate::offset_in(s, trimmed),
&mut warnings,
);
Ok(Parsed::new(id, warnings))
})
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for SipCallId {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = crate::serde_parts::leaf(
deserializer,
"Call-ID",
"a string",
<String as serde::Deserialize>::deserialize,
)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
impl TryFrom<String> for SipCallId {
type Error = ParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
SipCallId::new(value)
}
}
impl From<SipCallId> for String {
fn from(id: SipCallId) -> Self {
id.value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splits_on_the_first_at() {
let id = SipCallId::parse("a84b4c76e66710@example.com").unwrap();
assert_eq!(id.local(), "a84b4c76e66710");
assert_eq!(id.host(), Some("example.com"));
assert_eq!(id.as_str(), "a84b4c76e66710@example.com");
assert_eq!(id.to_string(), "a84b4c76e66710@example.com");
}
#[test]
fn the_host_word_is_optional() {
let id = SipCallId::parse("f81d4fae7dec11d0a76500a0c91e6bf6").unwrap();
assert_eq!(id.local(), "f81d4fae7dec11d0a76500a0c91e6bf6");
assert_eq!(id.host(), None);
}
#[test]
fn a_second_word_that_is_not_a_host_is_accepted() {
for raw in [
"abc@example.com/1",
"abc@[2001:db8::1]",
"abc@a:b",
"abc@{tag}",
] {
let id = SipCallId::parse_strict(raw).unwrap_or_else(|e| panic!("{raw:?}: {e}"));
assert_eq!(id.local(), "abc");
}
}
#[test]
fn a_second_at_is_kept_with_a_warning() {
let parsed = SipCallId::parse_with_warnings("a@b@c").unwrap();
assert_eq!(
parsed
.value
.local(),
"a"
);
assert_eq!(
parsed
.value
.host(),
Some("b@c")
);
assert_eq!(
parsed.warnings,
vec![ParseWarning::new(Field::CallId, WarningCode::InvalidToken).at(3)]
);
assert_eq!(
SipCallId::new("a@b@c"),
Err(ParseError::malformed(
Field::CallId,
FaultCode::InvalidChar,
Some(3)
))
);
}
#[test]
fn an_empty_word_is_a_breach() {
assert_eq!(SipCallId::parse(""), Err(ParseError::empty(Field::CallId)));
for (raw, pos) in [("abc@", 4), ("@example.com", 0)] {
let parsed = SipCallId::parse_with_warnings(raw).unwrap();
assert_eq!(parsed.warnings[0].position, Some(pos), "{raw:?}");
assert!(SipCallId::new(raw).is_err(), "{raw:?}");
}
}
#[test]
fn warning_positions_count_the_leading_whitespace() {
let parsed = SipCallId::parse_with_warnings(" a b").unwrap();
assert_eq!(
parsed
.value
.as_str(),
"a b"
);
assert_eq!(parsed.warnings[0].position, Some(3));
}
#[test]
fn separators_and_whitespace_are_not_word_characters() {
for raw in [
"a;to-tag=t2",
"a,b",
"a b",
"a\r\nSubject: x",
"a@ b",
"a=b",
] {
assert!(SipCallId::new(raw).is_err(), "accepted {raw:?}");
assert!(SipCallId::parse_strict(raw).is_err(), "accepted {raw:?}");
}
}
}