use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SipSecurityMechanism {
mechanism: String,
params: Vec<(String, Option<String>)>,
quoted: Vec<bool>,
}
impl SipSecurityMechanism {
pub fn mechanism(&self) -> &str {
&self.mechanism
}
pub fn params(&self) -> &[(String, Option<String>)] {
&self.params
}
pub fn param(&self, key: &str) -> Option<Option<&str>> {
self.params
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v.as_deref())
}
pub fn q(&self) -> Option<&str> {
self.param("q")
.flatten()
}
pub fn d_alg(&self) -> Option<&str> {
self.param("d-alg")
.flatten()
}
pub fn d_qop(&self) -> Option<&str> {
self.param("d-qop")
.flatten()
}
}
impl fmt::Display for SipSecurityMechanism {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.mechanism)?;
for ((key, value), quoted) in self
.params
.iter()
.zip(&self.quoted)
{
crate::write_param(f, key, value.as_deref(), *quoted)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SipSecurityError {
Empty,
InvalidFormat(String),
}
impl fmt::Display for SipSecurityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "empty security mechanism value"),
Self::InvalidFormat(raw) => {
write!(f, "invalid security mechanism ({} bytes)", raw.len())
}
}
}
}
impl std::error::Error for SipSecurityError {}
fn parse_mechanism(raw: &str) -> Result<SipSecurityMechanism, SipSecurityError> {
let raw = raw.trim();
if raw.is_empty() {
return Err(SipSecurityError::InvalidFormat(raw.to_string()));
}
let (mechanism_part, params_part) = match raw.split_once(';') {
Some((m, p)) => (m.trim(), Some(p)),
None => (raw, None),
};
if mechanism_part.is_empty() {
return Err(SipSecurityError::InvalidFormat(raw.to_string()));
}
let mechanism = mechanism_part.to_ascii_lowercase();
let (params, quoted) = crate::parse_params(params_part.unwrap_or(""))
.into_iter()
.map(|p| {
let (value, quoted) = p
.unquoted()
.map_or((None, false), |(v, q)| (Some(v), q));
(
(
p.key
.to_ascii_lowercase(),
value,
),
quoted,
)
})
.unzip();
Ok(SipSecurityMechanism {
mechanism,
params,
quoted,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SipSecurity(Vec<SipSecurityMechanism>);
impl SipSecurity {
pub fn parse(raw: &str) -> Result<Self, SipSecurityError> {
let raw = raw.trim();
if raw.is_empty() {
return Err(SipSecurityError::Empty);
}
Self::from_entries(crate::split_comma_entries(raw))
}
pub fn from_entries<'a>(
entries: impl IntoIterator<Item = &'a str>,
) -> Result<Self, SipSecurityError> {
let entries: Vec<_> = entries
.into_iter()
.map(parse_mechanism)
.collect::<Result<_, _>>()?;
if entries.is_empty() {
return Err(SipSecurityError::Empty);
}
Ok(Self(entries))
}
pub fn entries(&self) -> &[SipSecurityMechanism] {
&self.0
}
pub fn into_entries(self) -> Vec<SipSecurityMechanism> {
self.0
}
pub fn len(&self) -> usize {
self.0
.len()
}
pub fn is_empty(&self) -> bool {
self.0
.is_empty()
}
}
impl fmt::Display for SipSecurity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
crate::fmt_joined(f, &self.0, ", ")
}
}
impl_from_str_via_parse!(SipSecurity, SipSecurityError);
impl<'a> IntoIterator for &'a SipSecurity {
type Item = &'a SipSecurityMechanism;
type IntoIter = std::slice::Iter<'a, SipSecurityMechanism>;
fn into_iter(self) -> Self::IntoIter {
self.0
.iter()
}
}
impl IntoIterator for SipSecurity {
type Item = SipSecurityMechanism;
type IntoIter = std::vec::IntoIter<SipSecurityMechanism>;
fn into_iter(self) -> Self::IntoIter {
self.0
.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_mechanism() {
let sec = SipSecurity::parse("digest;d-qop=auth-int;q=0.1").unwrap();
assert_eq!(sec.len(), 1);
assert_eq!(sec.entries()[0].mechanism(), "digest");
assert_eq!(sec.entries()[0].d_qop(), Some("auth-int"));
assert_eq!(sec.entries()[0].q(), Some("0.1"));
}
#[test]
fn multiple_mechanisms() {
let sec = SipSecurity::parse("tls;q=0.2, digest;d-qop=auth;q=0.1").unwrap();
assert_eq!(sec.len(), 2);
assert_eq!(sec.entries()[0].mechanism(), "tls");
assert_eq!(sec.entries()[1].mechanism(), "digest");
}
#[test]
fn mechanism_no_params() {
let sec = SipSecurity::parse("tls").unwrap();
assert_eq!(sec.len(), 1);
assert_eq!(sec.entries()[0].mechanism(), "tls");
assert!(sec.entries()[0]
.params()
.is_empty());
}
#[test]
fn empty_input() {
assert!(matches!(
SipSecurity::parse(""),
Err(SipSecurityError::Empty)
));
}
#[test]
fn display_roundtrip() {
let raw = "digest;d-qop=auth;q=0.1";
let sec = SipSecurity::parse(raw).unwrap();
assert_eq!(sec.to_string(), raw);
}
#[test]
fn from_str() {
let sec: SipSecurity = "tls;q=0.2"
.parse()
.unwrap();
assert_eq!(sec.len(), 1);
}
#[test]
fn d_alg_param() {
let sec = SipSecurity::parse("digest;d-alg=MD5;d-qop=auth").unwrap();
assert_eq!(sec.entries()[0].d_alg(), Some("MD5"));
}
#[test]
fn from_entries_matches_parse() {
let split = SipSecurity::from_entries(["tls;q=0.2", "digest;d-qop=auth;q=0.1"]).unwrap();
let joined = SipSecurity::parse("tls;q=0.2, digest;d-qop=auth;q=0.1").unwrap();
assert_eq!(split, joined);
}
#[test]
fn from_entries_bad_entry_is_error() {
assert!(matches!(
SipSecurity::from_entries(["tls", " "]),
Err(SipSecurityError::InvalidFormat(_))
));
}
#[test]
fn from_entries_empty_is_empty_error() {
assert!(matches!(
SipSecurity::from_entries(std::iter::empty::<&str>()),
Err(SipSecurityError::Empty)
));
}
}
#[cfg(test)]
mod param_tests {
use super::*;
#[test]
fn d_ver_stays_quoted_on_display() {
let input = r#"ipsec-3gpp;d-ver="0000000000000000000000000000abcd";q=0.1"#;
let sec = SipSecurity::parse(input).unwrap();
assert_eq!(
sec.entries()[0].param("d-ver"),
Some(Some("0000000000000000000000000000abcd"))
);
assert_eq!(sec.to_string(), input);
}
#[test]
fn quoted_value_keeps_semicolon() {
let sec = SipSecurity::parse(r#"digest;x="a;b";q=0.5"#).unwrap();
let mech = &sec.entries()[0];
assert_eq!(mech.param("x"), Some(Some("a;b")));
assert_eq!(mech.q(), Some("0.5"));
}
#[test]
fn quoted_pair_unescaped() {
let sec = SipSecurity::parse(r#"digest;x="a\"b""#).unwrap();
assert_eq!(sec.entries()[0].param("x"), Some(Some(r#"a"b"#)));
assert_eq!(sec.to_string(), r#"digest;x="a\"b""#);
}
#[test]
fn error_display_omits_input() {
let err = SipSecurity::parse(";secret-value").unwrap_err();
assert!(!err
.to_string()
.contains("secret-value"));
}
}