use std::fmt;
use std::hash::{Hash, Hasher};
use sip_uri::{Host, UriParse};
use crate::diagnostic::{Field, ParseWarning, WarningCode};
use crate::error::{FaultCode, ParseError};
use crate::list::CommaList;
use crate::params::{checked_token, HeaderParams};
use crate::redact::{HeaderRedaction, Redact, RedactedList};
use crate::span::{relocated, Located, Relocation, Span};
use crate::traits::sealed;
use crate::{is_token, RawParam};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SipViaEntry {
protocol_name: String,
protocol_version: String,
transport: String,
host: Host,
port: Option<u16>,
params: HeaderParams,
rport: Option<Option<u16>>,
span: Option<Span>,
host_span: Option<Span>,
}
const RESERVED: &[&str] = &["rport"];
header_params!(SipViaEntry, reserved: RESERVED, clear_spans: host_span);
impl PartialEq for SipViaEntry {
fn eq(&self, other: &Self) -> bool {
self.protocol_name == other.protocol_name
&& self.protocol_version == other.protocol_version
&& self.transport == other.transport
&& self.host == other.host
&& self.port == other.port
&& self.params == other.params
&& self.rport == other.rport
}
}
impl Eq for SipViaEntry {}
impl Hash for SipViaEntry {
fn hash<H: Hasher>(&self, state: &mut H) {
self.protocol_name
.hash(state);
self.protocol_version
.hash(state);
self.transport
.hash(state);
self.host
.hash(state);
self.port
.hash(state);
self.params
.hash(state);
self.rport
.hash(state);
}
}
impl Located for SipViaEntry {
fn relocate_spans(&mut self, to: &Relocation<'_>) {
relocated(&mut self.span, to);
relocated(&mut self.host_span, to);
self.params
.relocate_spans(to);
}
}
impl SipViaEntry {
pub fn new(
protocol: impl AsRef<str>,
version: impl AsRef<str>,
transport: impl AsRef<str>,
host: Host,
) -> Result<Self, ParseError> {
let token = |part: &str| checked_token(Field::SentProtocol, part);
let entry = SipViaEntry::unchecked(
token(protocol.as_ref())?,
token(version.as_ref())?,
token(transport.as_ref())?,
host,
);
if Host::parse_strict(
&entry
.host
.to_string(),
)
.ok()
.as_ref()
!= Some(&entry.host)
{
return Err(ParseError::malformed(
Field::SentBy,
FaultCode::InvalidChar,
None,
));
}
Ok(entry)
}
fn unchecked(
protocol_name: String,
protocol_version: String,
transport: String,
host: Host,
) -> Self {
SipViaEntry {
protocol_name,
protocol_version,
transport,
host,
port: None,
params: HeaderParams::default(),
rport: None,
span: None,
host_span: None,
}
}
pub fn with_port(mut self, port: u16) -> Self {
self.port = Some(port);
self.clear_spans();
self
}
pub fn with_rport(mut self, rport: Option<u16>) -> Self {
self.params
.replace("rport", rport.map(|p| p.to_string()), false);
self.rport = Some(rport);
self.clear_spans();
self
}
fn clear_spans(&mut self) {
self.span = None;
self.host_span = None;
self.params
.clear_spans();
}
pub fn protocol(&self) -> &str {
&self.protocol_name
}
pub fn version(&self) -> &str {
&self.protocol_version
}
pub fn transport(&self) -> &str {
&self.transport
}
pub fn host(&self) -> &Host {
&self.host
}
pub fn span(&self) -> Option<Span> {
self.span
}
pub fn host_span(&self) -> Option<Span> {
self.host_span
}
pub fn port(&self) -> Option<u16> {
self.port
}
pub fn branch(&self) -> Option<&str> {
self.param("branch")
.flatten()
}
pub fn received(&self) -> Option<&str> {
self.param("received")
.flatten()
}
pub fn rport(&self) -> Option<Option<u16>> {
self.rport
}
}
impl SipViaEntry {
fn write_with(
&self,
f: &mut fmt::Formatter<'_>,
host: &dyn fmt::Display,
params: &dyn fmt::Display,
) -> fmt::Result {
write!(
f,
"{}/{}/{} {host}",
self.protocol_name, self.protocol_version, self.transport
)?;
if let Some(port) = self.port {
write!(f, ":{port}")?;
}
write!(f, "{params}")
}
}
impl fmt::Display for SipViaEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_with(f, &self.host, &self.params)
}
}
impl sealed::Sealed for SipViaEntry {}
impl Redact for SipViaEntry {
fn redacted<'a>(&'a self, how: &'a HeaderRedaction) -> impl fmt::Display + 'a {
RedactedVia { via: self, how }
}
}
struct RedactedVia<'a> {
via: &'a SipViaEntry,
how: &'a HeaderRedaction,
}
impl fmt::Display for RedactedVia<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let params = self
.via
.params
.masked_by(self.how, HeaderRedaction::masks_via_param);
if self
.how
.masks_via_host()
{
self.via
.write_with(f, &"***", ¶ms)
} else {
self.via
.write_with(
f,
&self
.via
.host,
¶ms,
)
}
}
}
impl Redact for SipVia {
fn redacted<'a>(&'a self, how: &'a HeaderRedaction) -> impl fmt::Display + 'a {
RedactedList(self.entries(), how)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct SipVia(Vec<SipViaEntry>);
list_type!(SipVia, SipViaEntry, non_empty);
#[cfg(feature = "serde")]
serde_parts!(SipViaEntry, SipViaEntryParts);
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct SipViaEntryParts {
#[serde(deserialize_with = "crate::serde_parts::field::protocol")]
protocol: String,
#[serde(deserialize_with = "crate::serde_parts::field::version")]
version: String,
#[serde(deserialize_with = "crate::serde_parts::field::transport")]
transport: String,
host: Host,
#[serde(default, deserialize_with = "crate::serde_parts::field::port")]
port: Option<u16>,
#[serde(default, deserialize_with = "crate::params::deserialize_unchecked")]
params: HeaderParams,
}
#[cfg(feature = "serde")]
impl SipViaEntryParts {
fn into_value(p: Self) -> Result<SipViaEntry, ParseError> {
let params = p.params;
let rport = params
.get("rport")
.and_then(rport_of);
let via = SipViaEntry {
port: p.port,
params,
rport,
..SipViaEntry::unchecked(p.protocol, p.version, p.transport, p.host)
};
crate::list::entry_reads_back::<SipVia>(via)
}
fn from_value(e: SipViaEntry) -> Self {
SipViaEntryParts {
protocol: e.protocol_name,
version: e.protocol_version,
transport: e.transport,
host: e.host,
port: e.port,
params: e.params,
}
}
}
fn parse_via_entry(
entry: &str,
warnings: &mut Vec<ParseWarning>,
) -> Result<SipViaEntry, ParseError> {
let trimmed = entry.trim();
if trimmed.is_empty() {
return Err(ParseError::malformed(
Field::Entry,
FaultCode::Missing,
None,
));
}
let (main_part, params_part) = crate::split_at_params(trimmed);
let (protocol_name, protocol_version, transport, sent_by) =
parse_sent_protocol(entry, main_part, warnings)?;
let (host, host_span, port) = parse_host_port(entry, sent_by, warnings)?;
let at = crate::offset_in(entry, trimmed);
let mut via = SipViaEntry {
port,
span: Some(Span::new(at..at + trimmed.len())),
host_span: Some(host_span),
..SipViaEntry::unchecked(protocol_name, protocol_version, transport, host)
};
let mut rport_seen = false;
for p in crate::parse_params(params_part) {
via.params
.push_raw(entry, &p, warnings);
if !rport_seen
&& p.name()
.eq_ignore_ascii_case("rport")
{
rport_seen = true;
via.rport = read_rport(entry, &p, warnings);
}
}
Ok(via)
}
fn rport_of(value: Option<&str>) -> Option<Option<u16>> {
match value {
None => Some(None),
Some(v) => port_number(v).map(Some),
}
}
fn read_rport(
entry: &str,
p: &RawParam<'_>,
warnings: &mut Vec<ParseWarning>,
) -> Option<Option<u16>> {
let value = p.unquoted();
let rport = rport_of(
value
.as_ref()
.map(|u| {
u.value
.as_str()
}),
);
if rport.is_none() {
let mut w = ParseWarning::new(Field::Param, WarningCode::InvalidRport);
if let Some(v) = p.value {
w = w.at(crate::offset_in(entry, v));
}
warnings.push(w);
}
rport
}
fn parse_sent_protocol<'a>(
entry: &str,
main: &'a str,
warnings: &mut Vec<ParseWarning>,
) -> Result<(String, String, String, &'a str), ParseError> {
let missing_slash = || ParseError::malformed(Field::SentProtocol, FaultCode::Missing, None);
let (name_raw, rest) = main
.split_once('/')
.ok_or_else(missing_slash)?;
let (version_raw, after_slash) = rest
.split_once('/')
.ok_or_else(missing_slash)?;
let (name, version) = (name_raw.trim(), version_raw.trim());
let rest = after_slash.trim_start();
let (mut transport, mut sent_by) = rest.split_at(
rest.find(char::is_whitespace)
.unwrap_or(rest.len()),
);
sent_by = sent_by.trim();
if sent_by.is_empty()
&& after_slash.starts_with(char::is_whitespace)
&& name == name_raw
&& version == version_raw
{
(transport, sent_by) = (&after_slash[..0], transport);
}
let parts = [name, version, transport].map(|part| {
let at = crate::offset_in(entry, part);
if let Some(i) = part.find(|c: char| c == '/' || c.is_whitespace()) {
return Err(ParseError::malformed(
Field::SentProtocol,
FaultCode::InvalidChar,
Some(at + i),
));
}
let text = crate::token_field(entry, part, Field::SentProtocol, warnings).into_owned();
if !is_token(&text) {
warnings.push(ParseWarning::new(Field::SentProtocol, WarningCode::InvalidToken).at(at));
}
Ok(text)
});
let [name, version, transport] = parts;
Ok((name?, version?, transport?, sent_by))
}
impl CommaList for SipVia {
type Entry = SipViaEntry;
fn parse_entry(
entry: &str,
warnings: &mut Vec<ParseWarning>,
) -> Result<SipViaEntry, ParseError> {
parse_via_entry(entry, warnings)
}
fn from_parsed(entries: Vec<SipViaEntry>) -> Result<Self, ParseError> {
Self::new(entries)
}
}
list_parse!(SipVia);
entry_parse!(SipViaEntry, SipVia);
type SentBy = (Host, Span, Option<u16>);
fn parse_host_port(
entry: &str,
sent_by: &str,
warnings: &mut Vec<ParseWarning>,
) -> Result<SentBy, ParseError> {
let at = |code, part: &str| {
ParseError::malformed(Field::SentBy, code, Some(crate::offset_in(entry, part)))
};
let (host, port) = if let Some(inner) = sent_by.strip_prefix('[') {
let close = inner
.find(']')
.ok_or_else(|| at(FaultCode::Unterminated, sent_by))?;
let rest = inner[close + 1..].trim_start();
let port = if rest.is_empty() {
None
} else {
Some(
rest.strip_prefix(':')
.ok_or_else(|| at(FaultCode::InvalidChar, rest))?,
)
};
(&sent_by[..close + 2], port)
} else {
match sent_by.split_once(':') {
Some((_, port)) if port.contains(':') => {
return Err(at(FaultCode::Ambiguous, sent_by));
}
Some((host, port)) => (host.trim_end(), Some(port)),
None => (sent_by, None),
}
};
if let Some(i) = host.find(char::is_whitespace) {
return Err(at(FaultCode::InvalidChar, &host[i..]));
}
if host.is_empty() {
return Err(at(FaultCode::Missing, host));
}
let at = crate::offset_in(entry, host);
let span = Span::new(at..at + host.len());
let host = read_host(host, at, warnings)?;
let port = port.and_then(|p| {
let p = p.trim_start();
let port = port_number(p);
if port.is_none() {
warnings.push(
ParseWarning::new(Field::SentBy, WarningCode::InvalidPort)
.at(crate::offset_in(entry, p))
.covering(Span::within(entry, p.trim_end())),
);
}
port
});
Ok((host, span, port))
}
fn port_number(text: &str) -> Option<u16> {
if text.is_empty()
|| !text
.bytes()
.all(|b| b.is_ascii_digit())
{
return None;
}
text.parse()
.ok()
}
fn read_host(
host: &str,
offset: usize,
warnings: &mut Vec<ParseWarning>,
) -> Result<Host, ParseError> {
let parsed =
Host::parse_with_warnings(host).map_err(|e| ParseError::uri(e, offset, host.len()))?;
warnings.extend(
parsed
.warnings
.into_iter()
.map(|w| ParseWarning::from_uri(w, offset)),
);
Ok(parsed.value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostic::WarningCode;
use crate::list::testing::{self, Seen};
use crate::{HeaderParse, ListParse};
use sip_uri::WarningKind;
#[test]
fn test_single_via() {
let via = SipVia::parse("SIP/2.0/UDP 198.51.100.1:5060").unwrap();
assert_eq!(via.len(), 1);
let entry = &via.entries()[0];
assert_eq!(entry.protocol(), "SIP");
assert_eq!(entry.version(), "2.0");
assert_eq!(entry.transport(), "UDP");
assert_eq!(
entry
.host()
.bare()
.to_string(),
"198.51.100.1"
);
assert_eq!(entry.port(), Some(5060));
assert!(entry
.params()
.is_empty());
}
#[test]
fn test_multiple_vias() {
let via = SipVia::parse("SIP/2.0/UDP 198.51.100.1:5060, SIP/2.0/TCP 203.0.113.5").unwrap();
assert_eq!(via.len(), 2);
let entry1 = &via.entries()[0];
assert_eq!(
entry1
.host()
.bare()
.to_string(),
"198.51.100.1"
);
assert_eq!(entry1.port(), Some(5060));
assert_eq!(entry1.transport(), "UDP");
let entry2 = &via.entries()[1];
assert_eq!(
entry2
.host()
.bare()
.to_string(),
"203.0.113.5"
);
assert_eq!(entry2.port(), None);
assert_eq!(entry2.transport(), "TCP");
}
#[test]
fn test_via_with_params() {
let via = SipVia::parse(
"SIP/2.0/UDP 198.51.100.1:5060;branch=z9hG4bKnashds8;received=203.0.113.10;rport=5061",
)
.unwrap();
let entry = &via.entries()[0];
assert_eq!(entry.branch(), Some("z9hG4bKnashds8"));
assert_eq!(entry.received(), Some("203.0.113.10"));
assert_eq!(entry.rport(), Some(Some(5061)));
}
#[test]
fn test_via_with_rport_no_value() {
let via = SipVia::parse("SIP/2.0/UDP 198.51.100.1:5060;rport").unwrap();
let entry = &via.entries()[0];
assert_eq!(entry.rport(), Some(None));
}
#[test]
fn test_via_without_rport() {
let via = SipVia::parse("SIP/2.0/UDP 198.51.100.1:5060").unwrap();
let entry = &via.entries()[0];
assert_eq!(entry.rport(), None);
}
#[test]
fn test_via_ipv6() {
let via = SipVia::parse("SIP/2.0/UDP [2001:db8::1]:5060").unwrap();
let entry = &via.entries()[0];
assert_eq!(
entry
.host()
.bare()
.to_string(),
"2001:db8::1"
);
assert_eq!(entry.port(), Some(5060));
}
#[test]
fn test_via_ipv6_no_port() {
let via = SipVia::parse("SIP/2.0/UDP [2001:db8::1]").unwrap();
let entry = &via.entries()[0];
assert_eq!(
entry
.host()
.bare()
.to_string(),
"2001:db8::1"
);
assert_eq!(entry.port(), None);
}
#[test]
fn test_via_hostname() {
let via = SipVia::parse("SIP/2.0/TLS example.com:5061").unwrap();
let entry = &via.entries()[0];
assert_eq!(
entry
.host()
.bare()
.to_string(),
"example.com"
);
assert_eq!(entry.port(), Some(5061));
assert_eq!(entry.transport(), "TLS");
}
#[test]
fn test_empty_via() {
assert_eq!(SipVia::parse(""), Err(ParseError::empty(Field::Value)));
}
#[test]
fn test_empty_via_whitespace() {
assert_eq!(SipVia::parse(" "), Err(ParseError::empty(Field::Value)));
}
#[test]
fn test_invalid_format() {
assert_eq!(
SipVia::parse("invalid"),
Err(
ParseError::malformed(Field::SentProtocol, FaultCode::Missing, Some(0)).in_entry(0)
)
);
}
#[test]
fn rport_that_is_no_port_is_kept_as_a_parameter() {
for value in ["garbage", "70000", "-1", "+5"] {
let raw = format!("SIP/2.0/UDP 198.51.100.1:5060;rport={value};branch=z9hG4bK1");
let (via, seen) = lenient(&raw);
let entry = &via.entries()[0];
assert_eq!(entry.rport(), None, "{raw}");
assert_eq!(entry.param("rport"), Some(Some(value)), "{raw}");
assert_eq!(entry.branch(), Some("z9hG4bK1"), "{raw}");
let at = raw
.find("rport=")
.map(|i| i + "rport=".len());
assert_eq!(
entry
.params()
.value_span("rport")
.map(|s| s.get(&raw)),
Some(Ok(value)),
"{raw}"
);
assert_eq!(
seen,
vec![(
Field::Param,
WarningCode::InvalidRport,
WarningKind::Recovered,
at,
Some(0)
)],
"{raw}"
);
assert_eq!(SipVia::parse(&via.to_string()), Ok(via), "{raw}");
}
}
#[test]
fn sent_by_port_that_is_no_port_is_dropped() {
for port in ["99999", "", "5o60", "50 60", "+5060"] {
let bad = format!("SIP/2.0/UDP 198.51.100.1:{port};branch=z9hG4bK1");
let (via, seen) = lenient(&bad);
let entry = &via.entries()[0];
assert_eq!(entry.port(), None, "{bad}");
assert_eq!(entry.branch(), Some("z9hG4bK1"), "{bad}");
assert_eq!(
entry
.span()
.map(|s| s.get(&bad)),
Some(Ok(bad.as_str()))
);
assert_eq!(
seen,
vec![(
Field::SentBy,
WarningCode::InvalidPort,
WarningKind::Lost,
Some("SIP/2.0/UDP 198.51.100.1:".len()),
Some(0)
)],
"{bad}"
);
assert_eq!(SipVia::parse_strict(&via.to_string()), Ok(via), "{bad}");
}
let bad = "SIP/2.0/UDP [2001:db8::1]:99999";
let parsed = SipVia::from_entries_with_warnings(["SIP/2.0/UDP 203.0.113.5", bad]).unwrap();
assert_eq!(
parsed
.value
.len(),
2
);
let w = parsed.warnings[0];
assert_eq!(
(w.code, w.position, w.row, w.entry),
(
WarningCode::InvalidPort,
bad.find("99999"),
Some(1),
Some(1)
)
);
}
#[test]
fn test_display_roundtrip() {
let original =
"SIP/2.0/UDP 198.51.100.1:5060;branch=z9hG4bKnashds8;received=203.0.113.10;rport";
let via = SipVia::parse(original).unwrap();
let displayed = via.to_string();
let reparsed = SipVia::parse(&displayed).unwrap();
assert_eq!(via, reparsed);
}
#[test]
fn test_into_iterator() {
let via = SipVia::parse("SIP/2.0/UDP 198.51.100.1:5060, SIP/2.0/TCP 203.0.113.5").unwrap();
let mut count = 0;
for entry in &via {
assert!(["198.51.100.1", "203.0.113.5"].contains(
&entry
.host()
.to_string()
.as_str()
));
count += 1;
}
assert_eq!(count, 2);
let entries: Vec<_> = via
.into_iter()
.collect();
assert_eq!(entries.len(), 2);
}
#[test]
fn test_into_entries() {
let via = SipVia::parse("SIP/2.0/UDP 198.51.100.1:5060, SIP/2.0/TCP 203.0.113.5").unwrap();
let entries = via.into_entries();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0]
.host()
.bare()
.to_string(),
"198.51.100.1"
);
assert_eq!(
entries[1]
.host()
.bare()
.to_string(),
"203.0.113.5"
);
}
#[test]
fn test_param_case_insensitive() {
let via = SipVia::parse("SIP/2.0/UDP 198.51.100.1:5060;Branch=test").unwrap();
let entry = &via.entries()[0];
assert_eq!(entry.param("branch"), Some(Some("test")));
assert_eq!(entry.param("BRANCH"), Some(Some("test")));
}
#[test]
fn test_display_ipv6() {
let via = SipVia::parse("SIP/2.0/UDP [2001:db8::1]:5060").unwrap();
let displayed = via.to_string();
assert!(displayed.contains("[2001:db8::1]"));
}
#[test]
fn from_entries_matches_parse() {
let a = "SIP/2.0/UDP 198.51.100.1:5060;branch=z9hG4bK1";
let b = "SIP/2.0/TCP 203.0.113.5";
let split = SipVia::from_entries([a, b]).unwrap();
let joined = SipVia::parse(&format!("{a}, {b}")).unwrap();
assert_eq!(split, joined);
assert_eq!(split.to_string(), format!("{a}, {b}"));
}
#[test]
fn from_entries_bad_entry_is_skipped() {
let parsed =
SipVia::from_entries_with_warnings(["SIP/2.0/UDP 198.51.100.1", "invalid"]).unwrap();
assert_eq!(
parsed
.value
.len(),
1
);
let w = parsed.warnings[0];
assert_eq!(
(w.code, w.row, w.entry),
(WarningCode::SkippedEntry, Some(1), Some(1))
);
}
#[test]
fn sws_around_protocol_slashes() {
let via = SipVia::parse("SIP / 2.0 / UDP example.com").unwrap();
let entry = &via.entries()[0];
assert_eq!(entry.protocol(), "SIP");
assert_eq!(entry.version(), "2.0");
assert_eq!(entry.transport(), "UDP");
assert_eq!(
entry
.host()
.bare()
.to_string(),
"example.com"
);
assert_eq!(entry.port(), None);
}
#[test]
fn sws_around_sent_by_colon() {
let via = SipVia::parse("SIP/2.0/UDP example.com : 5060;branch=z9hG4bK1").unwrap();
let entry = &via.entries()[0];
assert_eq!(
entry
.host()
.bare()
.to_string(),
"example.com"
);
assert_eq!(entry.port(), Some(5060));
assert_eq!(entry.branch(), Some("z9hG4bK1"));
}
#[test]
fn sws_around_ipv6_reference_colon() {
let via = SipVia::parse("SIP/2.0/UDP [2001:db8::1] : 5060").unwrap();
let entry = &via.entries()[0];
assert_eq!(
entry
.host()
.bare()
.to_string(),
"2001:db8::1"
);
assert_eq!(entry.port(), Some(5060));
}
#[test]
fn empty_transport_before_sent_by_accepted() {
let via = SipVia::parse("SIP/2.0/ example.com:5060").unwrap();
let entry = &via.entries()[0];
assert_eq!(entry.transport(), "");
assert_eq!(
entry
.host()
.bare()
.to_string(),
"example.com"
);
assert_eq!(entry.port(), Some(5060));
}
#[test]
fn junk_after_sent_by_is_error() {
assert!(SipVia::parse("SIP/2.0/UDP example.com extra").is_err());
assert!(SipVia::parse("SIP/2.0/UDP/X example.com").is_err());
}
fn lenient(raw: &str) -> (SipVia, Vec<Seen>) {
testing::lenient(raw)
}
#[test]
fn entry_without_host_is_skipped() {
for bad in [
" SIP/2.0/UDP :5060;branch=z9hG4bK1",
" SIP/2.0/UDP ",
" SIP/2.0/UDP ;branch=z9hG4bK1",
] {
let raw = format!("SIP/2.0/UDP 198.51.100.1,{bad}");
let (via, seen) = lenient(&raw);
assert_eq!(via.len(), 1, "{bad}");
let host_at = "SIP/2.0/UDP 198.51.100.1, SIP/2.0/UDP".len()
+ bad[" SIP/2.0/UDP".len()..].len()
- bad[" SIP/2.0/UDP".len()..]
.trim_start()
.len();
assert!(
matches!(
seen.as_slice(),
[(Field::SentBy, WarningCode::SkippedEntry, WarningKind::Lost, Some(at), Some(1))]
if (raw.len() - bad.len()..=host_at).contains(at)
),
"{bad}: {seen:?}"
);
assert!(
matches!(
SipVia::parse(bad),
Err(ParseError::Malformed(f))
if (f.field, f.code, f.entry) == (Field::SentBy, FaultCode::Missing, Some(0))
&& f.position.is_some()
),
"{bad}"
);
}
}
#[test]
fn host_breach_forwarded_from_sip_uri() {
let raw = "SIP/2.0/UDP 198.51.100.1, SIP/2.0/UDP exa_mple.com:5060";
let (via, seen) = lenient(raw);
assert_eq!(
via.entries()[1]
.host()
.bare()
.to_string(),
"exa_mple.com"
);
assert_eq!(
seen,
vec![(
Field::Uri(sip_uri::Component::Host),
WarningCode::Uri(sip_uri::WarningCode::InvalidChar),
WarningKind::Recovered,
raw.find('_'),
Some(1)
)]
);
}
#[test]
fn unreadable_bracketed_host_is_uri_error() {
let Err(ParseError::Uri(fault)) = SipVia::parse("SIP/2.0/UDP [zz]:5060") else {
panic!("not a URI error");
};
assert_eq!((fault.position(), fault.entry()), (Some(12), Some(0)));
}
#[test]
fn conformant_hosts_have_no_warning() {
for raw in [
"SIP/2.0/UDP 198.51.100.1:5060",
"SIP/2.0/UDP [2001:db8::1]:5060",
"SIP/2.0/TLS example.com.",
] {
assert!(
!SipVia::parse_with_warnings(raw)
.unwrap()
.has_warnings(),
"{raw}"
);
}
}
#[test]
fn param_unterminated_quote_is_warned() {
let raw = r#"SIP/2.0/UDP example.com;x="a;branch=z9hG4bK1"#;
let (via, seen) = lenient(raw);
assert_eq!(via.entries()[0].branch(), Some("z9hG4bK1"));
assert_eq!(
seen,
vec![(
Field::Param,
WarningCode::UnterminatedQuote,
WarningKind::Recovered,
raw.find('"'),
Some(0)
)]
);
}
#[test]
fn unbracketed_ipv6_is_error() {
assert!(matches!(
SipVia::parse("SIP/2.0/UDP 2001:db8::1:5060"),
Err(ParseError::Malformed(f)) if f.code == FaultCode::Ambiguous
));
assert!(SipVia::parse("SIP/2.0/UDP 2001:db8::1").is_err());
}
#[test]
fn params_keep_trimmed_unquoted_values() {
let via = SipVia::parse("SIP/2.0/UDP example.com ; Branch = z9hG4bK1 ; rport ; x=\"a;b\"")
.unwrap();
let entry = &via.entries()[0];
assert_eq!(
entry
.params()
.iter()
.collect::<Vec<_>>(),
[
("branch", Some("z9hG4bK1")),
("rport", None),
("x", Some("a;b")),
]
);
assert_eq!(entry.rport(), Some(None));
assert_eq!(
via.to_string(),
"SIP/2.0/UDP example.com;branch=z9hG4bK1;rport;x=\"a;b\""
);
}
#[test]
fn error_display_omits_input() {
for raw in [
"SIP/2.0/UDP secret.example.com extra",
"SIP/2.0/UDP [2001:db8::1]secret",
"secret",
] {
let err = SipVia::parse(raw).unwrap_err();
assert!(!err
.to_string()
.contains("secret"));
}
}
#[test]
fn builders_clear_spans() {
let raw = "SIP/2.0/UDP Example.COM:5060;branch=z9hG4bK1";
let parsed = SipVia::parse(raw)
.unwrap()
.entries()[0]
.clone();
assert_eq!(
parsed
.host_span()
.map(|s| s.get(raw)),
Some(Ok("Example.COM"))
);
assert_eq!(
parsed
.span()
.map(|s| s.get(raw)),
Some(Ok(raw))
);
let built = [
parsed
.clone()
.with_port(5070),
parsed
.clone()
.with_rport(None),
parsed
.clone()
.with_param("x", Some("1"))
.unwrap(),
parsed
.clone()
.with_quoted_param("x", "1")
.unwrap(),
];
let branch = |e: &SipViaEntry| {
e.params()
.value_span("branch")
};
assert!(branch(&parsed).is_some());
for entry in built {
assert_eq!((entry.span(), entry.host_span()), (None, None), "{entry}");
assert_eq!(branch(&entry), None, "{entry}");
}
let mut kept = parsed.clone();
assert!(kept
.params_mut()
.push("branch", Some("x"))
.is_err());
assert_eq!(kept.host_span(), parsed.host_span());
assert_eq!(branch(&kept), branch(&parsed));
let mut changed = parsed;
changed
.params_mut()
.push("x", None)
.unwrap();
assert_eq!((changed.span(), changed.host_span()), (None, None));
assert_eq!(branch(&changed), None);
}
#[test]
fn from_entries_empty_is_empty_error() {
assert_eq!(
SipVia::from_entries(std::iter::empty::<&str>()),
Err(ParseError::empty(Field::Value))
);
}
#[test]
fn warnings_api_on_conformant_input() {
let raw = "SIP/2.0/UDP 198.51.100.1:5060;branch=z9hG4bK1, SIP/2.0/TCP 203.0.113.5";
let parsed = SipVia::parse_with_warnings(raw).unwrap();
assert!(!parsed.has_warnings());
assert_eq!(parsed.value, SipVia::parse(raw).unwrap());
assert_eq!(SipVia::parse_strict(raw), Ok(parsed.value));
let split = SipVia::from_entries_with_warnings(["SIP/2.0/UDP 198.51.100.1"]).unwrap();
assert_eq!(
split
.value
.len(),
1
);
assert_eq!(
SipVia::parse_with_warnings(" "),
Err(ParseError::empty(Field::Value))
);
}
}