use std::fmt;
use std::str::Utf8Error;
use percent_encoding::percent_decode_str;
use crate::header_addr::{ParseSipHeaderAddrError, SipHeaderAddr};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum HistoryInfoError {
Empty,
InvalidEntry(ParseSipHeaderAddrError),
}
impl fmt::Display for HistoryInfoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "empty History-Info header"),
Self::InvalidEntry(e) => write!(f, "invalid History-Info entry: {e}"),
}
}
}
impl std::error::Error for HistoryInfoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidEntry(e) => Some(e),
_ => None,
}
}
}
impl From<ParseSipHeaderAddrError> for HistoryInfoError {
fn from(e: ParseSipHeaderAddrError) -> Self {
Self::InvalidEntry(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryInfoReason {
protocol: String,
cause: Option<u16>,
text: Option<String>,
}
impl HistoryInfoReason {
pub fn protocol(&self) -> &str {
&self.protocol
}
pub fn cause(&self) -> Option<u16> {
self.cause
}
pub fn text(&self) -> Option<&str> {
self.text
.as_deref()
}
}
fn parse_reason(decoded: &str) -> HistoryInfoReason {
let (protocol, rest) = decoded
.split_once(';')
.unwrap_or((decoded, ""));
let mut cause = None;
let mut text = None;
if let Some(idx) = rest.find("cause=") {
let val_start = idx + 6;
let val_end = rest[val_start..]
.find(';')
.map(|i| val_start + i)
.unwrap_or(rest.len());
cause = rest[val_start..val_end]
.trim()
.parse::<u16>()
.ok();
}
if let Some(idx) = rest.find("text=") {
let val_start = idx + 5;
let val = rest[val_start..].trim_start();
if let Some(inner) = val.strip_prefix('"') {
if let Some(end) = inner.find('"') {
text = Some(inner[..end].to_string());
} else {
text = Some(inner.to_string());
}
} else {
let end = val
.find(';')
.unwrap_or(val.len());
text = Some(val[..end].to_string());
}
}
HistoryInfoReason {
protocol: protocol
.trim()
.to_string(),
cause,
text,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryInfoEntry {
addr: SipHeaderAddr,
}
impl HistoryInfoEntry {
pub fn addr(&self) -> &SipHeaderAddr {
&self.addr
}
pub fn uri(&self) -> &sip_uri::Uri {
self.addr
.uri()
}
pub fn sip_uri(&self) -> Option<&sip_uri::SipUri> {
self.addr
.sip_uri()
}
pub fn index(&self) -> Option<&str> {
self.addr
.param_raw("index")
.flatten()
}
pub fn reason_raw(&self) -> Option<&str> {
self.addr
.sip_uri()?
.header("Reason")
}
pub fn reason(&self) -> Option<Result<HistoryInfoReason, Utf8Error>> {
let raw = self.reason_raw()?;
let raw = raw.replace('+', " ");
Some(
percent_decode_str(&raw)
.decode_utf8()
.map(|decoded| parse_reason(&decoded)),
)
}
}
impl fmt::Display for HistoryInfoEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.addr)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryInfo(Vec<HistoryInfoEntry>);
impl HistoryInfo {
pub fn parse(raw: &str) -> Result<Self, HistoryInfoError> {
let raw = raw.trim();
if raw.is_empty() {
return Err(HistoryInfoError::Empty);
}
Self::from_entries(crate::split_comma_entries(raw))
}
pub fn from_entries<'a>(
entries: impl IntoIterator<Item = &'a str>,
) -> Result<Self, HistoryInfoError> {
let entries: Vec<_> = entries
.into_iter()
.map(parse_entry)
.collect::<Result<_, _>>()?;
if entries.is_empty() {
return Err(HistoryInfoError::Empty);
}
Ok(Self(entries))
}
pub fn entries(&self) -> &[HistoryInfoEntry] {
&self.0
}
pub fn into_entries(self) -> Vec<HistoryInfoEntry> {
self.0
}
pub fn len(&self) -> usize {
self.0
.len()
}
pub fn is_empty(&self) -> bool {
self.0
.is_empty()
}
}
fn parse_entry(raw: &str) -> Result<HistoryInfoEntry, HistoryInfoError> {
let addr: SipHeaderAddr = raw
.trim()
.parse()?;
Ok(HistoryInfoEntry { addr })
}
impl fmt::Display for HistoryInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
crate::fmt_joined(f, &self.0, ",")
}
}
impl<'a> IntoIterator for &'a HistoryInfo {
type Item = &'a HistoryInfoEntry;
type IntoIter = std::slice::Iter<'a, HistoryInfoEntry>;
fn into_iter(self) -> Self::IntoIter {
self.0
.iter()
}
}
impl IntoIterator for HistoryInfo {
type Item = HistoryInfoEntry;
type IntoIter = std::vec::IntoIter<HistoryInfoEntry>;
fn into_iter(self) -> Self::IntoIter {
self.0
.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
const EXAMPLE_1: &str = "\
<sip:user1@esrp.example.com?Reason=RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22>;index=1,\
<sip:sos@psap.example.com>;index=2";
const EXAMPLE_2: &str = "\
<sip:lsrg.example.com?Reason=SIP%3Bcause%3D200%3Btext%3D%22Legacy+routing%22>;index=1,\
<sip:user1@esrp2.example.com;lr;transport=udp?Reason=RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22>;index=1.1,\
<sip:sos@psap.example.com>;index=1.2";
#[test]
fn parse_two_entries() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
assert_eq!(hi.len(), 2);
}
#[test]
fn parse_three_entries() {
let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
assert_eq!(hi.len(), 3);
}
#[test]
fn parse_single_entry() {
let hi = HistoryInfo::parse("<sip:alice@example.com>;index=1").unwrap();
assert_eq!(hi.len(), 1);
}
#[test]
fn empty_input() {
assert!(matches!(
HistoryInfo::parse(""),
Err(HistoryInfoError::Empty)
));
}
#[test]
fn index_simple() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
assert_eq!(hi.entries()[0].index(), Some("1"));
assert_eq!(hi.entries()[1].index(), Some("2"));
}
#[test]
fn index_hierarchical() {
let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
assert_eq!(hi.entries()[0].index(), Some("1"));
assert_eq!(hi.entries()[1].index(), Some("1.1"));
assert_eq!(hi.entries()[2].index(), Some("1.2"));
}
#[test]
fn index_absent() {
let hi = HistoryInfo::parse("<sip:alice@example.com>").unwrap();
assert_eq!(hi.entries()[0].index(), None);
}
#[test]
fn uri_with_user() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
let sip = hi.entries()[0]
.sip_uri()
.unwrap();
assert_eq!(sip.user(), Some("user1"));
assert_eq!(
sip.host()
.to_string(),
"esrp.example.com"
);
}
#[test]
fn uri_without_user() {
let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
let sip = hi.entries()[0]
.sip_uri()
.unwrap();
assert_eq!(sip.user(), None);
assert_eq!(
sip.host()
.to_string(),
"lsrg.example.com"
);
}
#[test]
fn uri_with_params() {
let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
let sip = hi.entries()[1]
.sip_uri()
.unwrap();
assert_eq!(sip.user(), Some("user1"));
assert_eq!(
sip.host()
.to_string(),
"esrp2.example.com"
);
assert!(sip
.param("lr")
.is_some());
assert_eq!(sip.param("transport"), Some(&Some("udp".to_string())));
}
#[test]
fn reason_raw_present() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
assert_eq!(
hi.entries()[0].reason_raw(),
Some("RouteAction%3Bcause%3D200%3Btext%3D%22Normal+Next+Hop%22")
);
}
#[test]
fn reason_raw_absent() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
assert_eq!(hi.entries()[1].reason_raw(), None);
}
#[test]
fn reason_parsed_route_action() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
let reason = hi.entries()[0]
.reason()
.unwrap()
.unwrap();
assert_eq!(reason.protocol(), "RouteAction");
assert_eq!(reason.cause(), Some(200));
assert_eq!(reason.text(), Some("Normal Next Hop"));
}
#[test]
fn reason_parsed_sip() {
let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
let reason = hi.entries()[0]
.reason()
.unwrap()
.unwrap();
assert_eq!(reason.protocol(), "SIP");
assert_eq!(reason.cause(), Some(200));
assert_eq!(reason.text(), Some("Legacy routing"));
}
#[test]
fn reason_absent_returns_none() {
let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
assert!(hi.entries()[2]
.reason()
.is_none());
}
#[test]
fn reason_multiple_entries() {
let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
let r0 = hi.entries()[0]
.reason()
.unwrap()
.unwrap();
let r1 = hi.entries()[1]
.reason()
.unwrap()
.unwrap();
assert_eq!(r0.protocol(), "SIP");
assert_eq!(r1.protocol(), "RouteAction");
assert!(hi.entries()[2]
.reason()
.is_none());
}
#[test]
fn display_roundtrip_simple() {
let raw = "<sip:alice@example.com>;index=1";
let hi = HistoryInfo::parse(raw).unwrap();
assert_eq!(hi.to_string(), raw);
}
#[test]
fn display_entry_count_matches_commas() {
let hi = HistoryInfo::parse(EXAMPLE_2).unwrap();
let s = hi.to_string();
assert_eq!(
s.matches(',')
.count()
+ 1,
hi.len()
);
}
#[test]
fn display_roundtrip_real_world() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
let reparsed = HistoryInfo::parse(&hi.to_string()).unwrap();
assert_eq!(hi.len(), reparsed.len());
for (a, b) in hi
.entries()
.iter()
.zip(reparsed.entries())
{
assert_eq!(a.index(), b.index());
assert_eq!(a.reason_raw(), b.reason_raw());
}
}
#[test]
fn iter_by_ref() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
let indices: Vec<_> = hi
.entries()
.iter()
.map(|e| e.index())
.collect();
assert_eq!(indices, vec![Some("1"), Some("2")]);
}
#[test]
fn into_entries() {
let hi = HistoryInfo::parse(EXAMPLE_1).unwrap();
let entries = hi.into_entries();
assert_eq!(entries.len(), 2);
}
#[test]
fn parse_reason_full() {
let r = parse_reason("SIP;cause=302;text=\"Moved\"");
assert_eq!(r.protocol(), "SIP");
assert_eq!(r.cause(), Some(302));
assert_eq!(r.text(), Some("Moved"));
}
#[test]
fn parse_reason_no_text() {
let r = parse_reason("Q.850;cause=16");
assert_eq!(r.protocol(), "Q.850");
assert_eq!(r.cause(), Some(16));
assert_eq!(r.text(), None);
}
#[test]
fn parse_reason_protocol_only() {
let r = parse_reason("SIP");
assert_eq!(r.protocol(), "SIP");
assert_eq!(r.cause(), None);
assert_eq!(r.text(), None);
}
#[test]
fn parse_reason_unquoted_text() {
let r = parse_reason("SIP;cause=200;text=OK");
assert_eq!(r.text(), Some("OK"));
}
}