use std::borrow::Cow;
use sip_header_catalog::{NameMatcher, RowError, SipHeader, SipHeaderFields, SipHeaderRows};
use sip_uri::UriParse;
use crate::diagnostic::{Field, ParseWarning, Parsed, WarningCode};
use crate::error::{Fault, FaultCode, ParseError};
use crate::span::Span;
fn split_at_blank_line(message: &str) -> (&str, Option<usize>) {
let mut offset = 0;
for line in message.split('\n') {
let stripped = line
.strip_suffix('\r')
.unwrap_or(line);
if stripped.is_empty() {
let body_start = offset + line.len() + 1;
return (
&message[..offset],
(body_start <= message.len()).then_some(body_start),
);
}
offset += line.len() + 1;
}
(message, None)
}
pub fn extract_body(message: &str) -> Option<&str> {
let (_, body_start) = split_at_blank_line(message);
let body = &message[body_start?..];
(!body.is_empty()).then_some(body)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SipMessageHeaders<'a> {
fields: SipHeaderFields<'a>,
skipped: Vec<usize>,
}
impl<'a> SipMessageHeaders<'a> {
pub fn new(message: &'a str) -> Self {
Self::read(message, true)
}
pub fn from_header_block(block: &'a str) -> Self {
Self::read(block, false)
}
fn read(message: &'a str, start_line: bool) -> Self {
let (block, _) = split_at_blank_line(message);
let mut fields = SipHeaderFields::new();
let mut skipped = Vec::new();
let mut open: Option<(&'a str, Cow<'a, str>)> = None;
let mut offset = 0;
for (i, raw_line) in block
.split('\n')
.enumerate()
{
let at = offset;
offset += raw_line.len() + 1;
let line = raw_line
.strip_suffix('\r')
.unwrap_or(raw_line);
if line.is_empty() {
continue;
}
if line.starts_with([' ', '\t']) {
match &mut open {
Some((_, value)) => append_folded(value, line),
None => skipped.push(at),
}
continue;
}
if let Some((name, value)) = open.take() {
fields.push(name, value);
}
match header_line(line) {
Some((name, value)) => open = Some((name, Cow::Borrowed(value))),
None if i == 0 && start_line && is_start_line(line) => {}
None => skipped.push(at),
}
}
if let Some((name, value)) = open {
fields.push(name, value);
}
SipMessageHeaders { fields, skipped }
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &str)> + '_ {
self.fields
.iter()
}
pub fn len(&self) -> usize {
self.fields
.len()
}
pub fn is_empty(&self) -> bool {
self.fields
.is_empty()
}
pub fn fields(&self) -> &SipHeaderFields<'a> {
&self.fields
}
pub fn into_fields(self) -> SipHeaderFields<'a> {
self.fields
}
pub fn skipped(&self) -> &[usize] {
&self.skipped
}
}
impl SipHeaderRows for SipMessageHeaders<'_> {
fn sip_header_rows_str<'a>(&'a self, name: &str) -> Result<Vec<&'a str>, RowError> {
self.fields
.sip_header_rows_str(name)
}
}
fn header_line(line: &str) -> Option<(&str, &str)> {
let (name, value) = line.split_once(':')?;
let name = name.trim_end_matches([' ', '\t']);
crate::is_token(name).then(|| (name, value.trim()))
}
fn append_folded(value: &mut Cow<'_, str>, line: &str) {
let line = line.trim();
if line.is_empty() {
return;
}
let value = value.to_mut();
if !value.is_empty() {
value.push(' ');
}
value.push_str(line);
}
pub fn extract_header(message: &str, name: &str) -> Vec<String> {
let matcher = NameMatcher::new(name);
SipMessageHeaders::new(message)
.iter()
.filter(|(wire, _)| matcher.matches(wire))
.map(|(_, value)| value.to_string())
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractedHeaders {
pub headers: SipHeaderFields<'static>,
pub skipped: Vec<usize>,
}
pub fn extract_all_headers(message: &str) -> ExtractedHeaders {
owned(SipMessageHeaders::new(message))
}
pub fn extract_header_block(block: &str) -> ExtractedHeaders {
owned(SipMessageHeaders::from_header_block(block))
}
fn owned(headers: SipMessageHeaders<'_>) -> ExtractedHeaders {
let SipMessageHeaders { fields, skipped } = headers;
ExtractedHeaders {
headers: fields.into_owned(),
skipped,
}
}
pub fn extract_request_uri(message: &str) -> Result<Option<sip_uri::Uri>, ParseError> {
extract_request_line(message)?
.map(|line| line.uri())
.transpose()
}
pub fn extract_request_uri_strict(message: &str) -> Result<Option<sip_uri::Uri>, ParseError> {
extract_request_line(message)?
.map(|line| line.uri_strict())
.transpose()
}
pub fn extract_request_uri_with_warnings(
message: &str,
) -> Result<Option<Parsed<sip_uri::Uri>>, ParseError> {
extract_request_line(message)?
.map(|line| line.uri_with_warnings())
.transpose()
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RequestLine<'a> {
method: &'a str,
uri_text: &'a str,
version: &'a str,
method_span: Span,
uri_span: Span,
version_span: Span,
warnings: Vec<ParseWarning>,
}
impl<'a> RequestLine<'a> {
pub fn method(&self) -> &'a str {
self.method
}
pub fn uri_text(&self) -> &'a str {
self.uri_text
}
pub fn version(&self) -> &'a str {
self.version
}
pub fn method_span(&self) -> Span {
self.method_span
}
pub fn uri_span(&self) -> Span {
self.uri_span
}
pub fn version_span(&self) -> Span {
self.version_span
}
pub fn warnings(&self) -> &[ParseWarning] {
&self.warnings
}
pub fn uri(&self) -> Result<sip_uri::Uri, ParseError> {
self.uri_with_warnings()
.map(|p| p.value)
}
pub fn uri_strict(&self) -> Result<sip_uri::Uri, ParseError> {
self.uri_with_warnings()?
.into_strict()
}
pub fn uri_with_warnings(&self) -> Result<Parsed<sip_uri::Uri>, ParseError> {
let uri_at = self
.uri_span
.range()
.start;
let parsed = sip_uri::Uri::parse_with_warnings(self.uri_text).map_err(|e| {
ParseError::uri(
e,
uri_at,
self.uri_text
.len(),
)
})?;
let warnings = self
.warnings
.iter()
.copied()
.chain(
parsed
.warnings
.into_iter()
.map(|w| ParseWarning::from_uri(w, uri_at)),
)
.collect();
Ok(Parsed::new(parsed.value, warnings))
}
}
pub fn extract_request_line(message: &str) -> Result<Option<RequestLine<'_>>, ParseError> {
let first = message
.split('\n')
.next()
.unwrap_or_default();
request_line(
first
.strip_suffix('\r')
.unwrap_or(first),
)
}
fn is_start_line(line: &str) -> bool {
let mut parts = line.split_whitespace();
let status = parts
.next()
.is_some_and(|v| v.starts_with("SIP/"))
&& parts
.next()
.is_some_and(|code| {
code.len() == 3
&& code
.bytes()
.all(|b| b.is_ascii_digit())
});
status || matches!(request_line(line), Ok(Some(_)))
}
fn request_line(first: &str) -> Result<Option<RequestLine<'_>>, ParseError> {
if first.is_empty() {
return Err(ParseError::empty(Field::Value));
}
let parts: Vec<&str> = first
.split_whitespace()
.collect();
if parts
.first()
.is_some_and(|p| p.starts_with("SIP/"))
{
return Ok(None);
}
let [method, uri_text, version] = parts.as_slice() else {
return Err(ParseError::Malformed(
Fault::new(Field::Value, FaultCode::Missing)
.at(0)
.to(first.len()),
));
};
if !crate::is_token(method) {
return Err(ParseError::malformed(
Field::Value,
FaultCode::InvalidChar,
Some(0),
));
}
if !version.starts_with("SIP/") {
return Err(ParseError::malformed(
Field::Value,
FaultCode::Missing,
Some(crate::offset_in(first, version)),
));
}
let span = |part: &str| {
let at = crate::offset_in(first, part);
Span::new(at..at + part.len())
};
Ok(Some(RequestLine {
method,
uri_text,
version,
method_span: span(method),
uri_span: span(uri_text),
version_span: span(version),
warnings: spacing_breaches(first, &parts),
}))
}
fn spacing_breaches(line: &str, parts: &[&str]) -> Vec<ParseWarning> {
let mut warnings = Vec::new();
let mut end = 0;
for (i, part) in parts
.iter()
.enumerate()
{
let start = crate::offset_in(line, part);
let gap = &line[end..start];
if gap != if i == 0 { "" } else { " " } {
warnings
.push(ParseWarning::new(Field::Value, WarningCode::RequestLineWhitespace).at(end));
}
end = start + part.len();
}
if end < line.len() {
warnings.push(ParseWarning::new(Field::Value, WarningCode::RequestLineWhitespace).at(end));
}
warnings
}
mod sealed {
pub trait Sealed {}
}
impl sealed::Sealed for SipHeader {}
pub trait SipHeaderExtract: sealed::Sealed {
fn extract_from(&self, message: &str) -> Vec<String>;
}
impl SipHeaderExtract for SipHeader {
fn extract_from(&self, message: &str) -> Vec<String> {
extract_header(message, self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::HeaderParse;
const SAMPLE_INVITE: &str = "\
INVITE sip:bob@biloxi.example.com SIP/2.0\r\n\
Via: SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds\r\n\
Via: SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8\r\n\
Max-Forwards: 70\r\n\
To: Bob <sip:bob@biloxi.example.com>\r\n\
From: Alice <sip:alice@atlanta.example.com>;tag=1928301774\r\n\
Call-ID: a84b4c76e66710@pc33.atlanta.example.com\r\n\
CSeq: 314159 INVITE\r\n\
Contact: <sip:alice@pc33.atlanta.example.com>\r\n\
Content-Type: application/sdp\r\n\
Content-Length: 142\r\n\
\r\n\
v=0\r\n\
o=alice 2890844526 2890844526 IN IP4 pc33.atlanta.example.com\r\n";
#[test]
fn basic_extraction() {
let from = extract_header(SAMPLE_INVITE, "From");
assert_eq!(from.len(), 1);
assert_eq!(
from[0],
"Alice <sip:alice@atlanta.example.com>;tag=1928301774"
);
let call_id = extract_header(SAMPLE_INVITE, "Call-ID");
assert_eq!(call_id.len(), 1);
assert_eq!(call_id[0], "a84b4c76e66710@pc33.atlanta.example.com");
let cseq = extract_header(SAMPLE_INVITE, "CSeq");
assert_eq!(cseq.len(), 1);
assert_eq!(cseq[0], "314159 INVITE");
}
#[test]
fn case_insensitive_name() {
let expected = "Alice <sip:alice@atlanta.example.com>;tag=1928301774";
assert_eq!(extract_header(SAMPLE_INVITE, "from")[0], expected);
assert_eq!(extract_header(SAMPLE_INVITE, "FROM")[0], expected);
assert_eq!(extract_header(SAMPLE_INVITE, "From")[0], expected);
}
#[test]
fn header_folding() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Subject: I know you're there,\r\n",
" pick up the phone\r\n",
" and talk to me!\r\n",
"\r\n",
);
let result = extract_header(msg, "Subject");
assert_eq!(result.len(), 1);
assert_eq!(
result[0],
"I know you're there, pick up the phone and talk to me!"
);
}
#[test]
fn multiple_occurrences_separate() {
let via = extract_header(SAMPLE_INVITE, "Via");
assert_eq!(via.len(), 2);
assert_eq!(
via[0],
"SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds"
);
assert_eq!(
via[1],
"SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8"
);
}
#[test]
fn stops_at_blank_line() {
assert!(extract_header(SAMPLE_INVITE, "o").is_empty());
}
#[test]
fn bare_lf_line_endings() {
let msg = "SIP/2.0 200 OK\n\
From: Alice <sip:alice@host>\n\
To: Bob <sip:bob@host>\n\
\n\
body\n";
let from = extract_header(msg, "From");
assert_eq!(from.len(), 1);
assert_eq!(from[0], "Alice <sip:alice@host>");
}
#[test]
fn missing_header_returns_empty() {
assert!(extract_header(SAMPLE_INVITE, "X-Custom").is_empty());
}
#[test]
fn empty_message() {
assert!(extract_header("", "From").is_empty());
}
#[test]
fn request_line_not_matched() {
assert!(extract_header(SAMPLE_INVITE, "INVITE sip").is_empty());
}
#[test]
fn value_leading_whitespace_trimmed() {
let msg = "SIP/2.0 200 OK\r\n\
From: Alice <sip:alice@host>\r\n\
\r\n";
let from = extract_header(msg, "From");
assert_eq!(from.len(), 1);
assert_eq!(from[0], "Alice <sip:alice@host>");
}
#[test]
fn folding_on_multiple_occurrence() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Via: SIP/2.0/UDP first.example.com\r\n",
" ;branch=z9hG4bKaaa\r\n",
"Via: SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb\r\n",
"\r\n",
);
let via = extract_header(msg, "Via");
assert_eq!(via.len(), 2);
assert_eq!(via[0], "SIP/2.0/UDP first.example.com ;branch=z9hG4bKaaa");
assert_eq!(via[1], "SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb");
}
#[test]
fn empty_header_value() {
let msg = "SIP/2.0 200 OK\r\n\
Subject:\r\n\
From: Alice <sip:alice@host>\r\n\
\r\n";
let subject = extract_header(msg, "Subject");
assert_eq!(subject.len(), 1);
assert_eq!(subject[0], "");
}
#[test]
fn tab_folding() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Subject: hello\r\n",
"\tworld\r\n",
"\r\n",
);
let subject = extract_header(msg, "Subject");
assert_eq!(subject.len(), 1);
assert_eq!(subject[0], "hello world");
}
#[test]
fn compact_form_from() {
let msg = "SIP/2.0 200 OK\r\nf: Alice <sip:alice@host>\r\n\r\n";
assert_eq!(extract_header(msg, "From")[0], "Alice <sip:alice@host>");
assert_eq!(extract_header(msg, "f")[0], "Alice <sip:alice@host>");
}
#[test]
fn compact_form_via() {
let msg = "SIP/2.0 200 OK\r\nv: SIP/2.0/UDP host\r\n\r\n";
assert_eq!(extract_header(msg, "Via")[0], "SIP/2.0/UDP host");
assert_eq!(extract_header(msg, "v")[0], "SIP/2.0/UDP host");
}
#[test]
fn compact_form_mixed_with_full() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"f: Alice <sip:alice@host>;tag=a\r\n",
"t: Bob <sip:bob@host>;tag=b\r\n",
"i: call-1@host\r\n",
"m: <sip:alice@192.0.2.1>\r\n",
"Content-Type: application/sdp\r\n",
"\r\n",
);
assert_eq!(
extract_header(msg, "From")[0],
"Alice <sip:alice@host>;tag=a"
);
assert_eq!(extract_header(msg, "To")[0], "Bob <sip:bob@host>;tag=b");
assert_eq!(extract_header(msg, "Call-ID")[0], "call-1@host");
assert_eq!(extract_header(msg, "Contact")[0], "<sip:alice@192.0.2.1>");
assert_eq!(extract_header(msg, "Content-Type")[0], "application/sdp");
assert_eq!(extract_header(msg, "c")[0], "application/sdp");
}
#[test]
fn compact_form_case_insensitive() {
let msg = "SIP/2.0 200 OK\r\nF: Alice <sip:alice@host>\r\n\r\n";
assert_eq!(extract_header(msg, "From")[0], "Alice <sip:alice@host>");
}
#[test]
fn compact_form_unknown_single_char() {
let msg = "SIP/2.0 200 OK\r\nz: something\r\n\r\n";
assert_eq!(extract_header(msg, "z")[0], "something");
assert!(extract_header(msg, "From").is_empty());
}
const NG911_INVITE: &str = concat!(
"INVITE sip:urn:service:sos@bcf.example.com SIP/2.0\r\n",
"Via: SIP/2.0/TLS proxy.example.com;branch=z9hG4bK776\r\n",
"From: \"Caller Name\" <sip:+15551234567@orig.example.com>;tag=abc123\r\n",
"To: <sip:urn:service:sos@bcf.example.com>\r\n",
"Call-ID: ng911-call-42@orig.example.com\r\n",
"P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
"Call-Info: <urn:emergency:uid:callid:abc:bcf.example.com>;purpose=emergency-CallId,",
"<https://adr.example.com/serviceInfo?t=x>;purpose=EmergencyCallData.ServiceInfo\r\n",
"Geolocation: <cid:loc-id-1234>, <https://lis.example.com/held/test>\r\n",
"Content-Type: application/sdp\r\n",
"\r\n",
"v=0\r\n",
);
#[test]
fn extract_and_parse_call_info() {
use crate::uri_info::UriInfo;
let raw = extract_header(NG911_INVITE, "Call-Info");
assert_eq!(raw.len(), 1);
let ci = UriInfo::parse(&raw[0]).unwrap();
assert_eq!(ci.len(), 2);
assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
assert!(ci
.entries()
.iter()
.any(|e| e.purpose() == Some("EmergencyCallData.ServiceInfo")));
}
#[test]
fn extract_and_parse_p_asserted_identity() {
use crate::header_addr::SipHeaderAddr;
let raw = extract_header(NG911_INVITE, "P-Asserted-Identity");
assert_eq!(raw.len(), 1);
let pai = SipHeaderAddr::parse(&raw[0]).unwrap();
assert_eq!(pai.display_name(), Some("EXAMPLE CO"));
assert!(pai
.uri()
.to_string()
.contains("+15551234567"));
}
#[test]
fn extract_and_parse_multi_pai() {
use crate::header_addr::SipHeaderAddr;
let msg = concat!(
"INVITE sip:sos@psap.example.com SIP/2.0\r\n",
"P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
"P-Asserted-Identity: <tel:+15551234567>\r\n",
"\r\n",
);
let raw = extract_header(msg, "P-Asserted-Identity");
assert_eq!(raw.len(), 2);
let pai0 = SipHeaderAddr::parse(&raw[0]).unwrap();
assert_eq!(pai0.display_name(), Some("EXAMPLE CO"));
let pai1 = SipHeaderAddr::parse(&raw[1]).unwrap();
assert!(pai1
.uri()
.to_string()
.contains("+15551234567"));
}
#[test]
fn extract_and_parse_geolocation() {
use crate::geolocation::SipGeolocation;
let raw = extract_header(NG911_INVITE, "Geolocation");
assert_eq!(raw.len(), 1);
let geo = SipGeolocation::parse(&raw[0]).unwrap();
assert_eq!(geo.len(), 2);
assert_eq!(geo.cid(), Some("loc-id-1234"));
assert!(geo
.url()
.unwrap()
.to_string()
.contains("lis.example.com"));
}
#[test]
fn extract_and_parse_from_to() {
use crate::header_addr::SipHeaderAddr;
let from_raw = extract_header(NG911_INVITE, "From");
assert_eq!(from_raw.len(), 1);
let from = SipHeaderAddr::parse(&from_raw[0]).unwrap();
assert_eq!(from.display_name(), Some("Caller Name"));
assert_eq!(from.tag(), Some("abc123"));
let to_raw = extract_header(NG911_INVITE, "To");
assert_eq!(to_raw.len(), 1);
let to = SipHeaderAddr::parse(&to_raw[0]).unwrap();
assert_eq!(
to.uri()
.to_string(),
"sip:urn:service%3Asos@bcf.example.com"
);
}
fn request_uri(msg: &str) -> Option<String> {
extract_request_uri(msg)
.unwrap()
.map(|u| u.to_string())
}
#[test]
fn extract_request_uri_invite() {
let msg = "INVITE urn:service:sos SIP/2.0\r\nTo: <urn:service:sos>\r\n\r\n";
assert_eq!(request_uri(msg), Some("urn:service:sos".into()));
}
#[test]
fn extract_request_uri_sip() {
let msg = "INVITE sip:+15550001234@198.51.100.1:5060 SIP/2.0\r\n\r\n";
assert_eq!(
request_uri(msg),
Some("sip:+15550001234@198.51.100.1:5060".into()),
);
}
#[test]
fn extract_request_uri_status_line() {
assert_eq!(request_uri("SIP/2.0 200 OK\r\n\r\n"), None);
}
#[test]
fn extract_request_uri_malformed_line() {
assert_eq!(
extract_request_uri(""),
Err(ParseError::empty(Field::Value))
);
let line = "INVITE sip:a@example.com SIP/2.0 x";
assert_eq!(
extract_request_uri(&format!("{line}\r\n")),
Err(ParseError::Malformed(
Fault::new(Field::Value, FaultCode::Missing)
.at(0)
.to(line.len())
))
);
assert_eq!(
extract_request_uri("INVITE sip:a@example.com HTTP/1.1\r\n"),
Err(ParseError::malformed(
Field::Value,
FaultCode::Missing,
Some(25)
))
);
}
fn spacing(at: usize) -> ParseWarning {
ParseWarning::new(Field::Value, WarningCode::RequestLineWhitespace).at(at)
}
#[test]
fn request_line_spacing_is_warned_and_strictly_refused() {
let msg = "INVITE sip:a@example.com\tSIP/2.0 \r\n\r\n";
let parsed = extract_request_uri_with_warnings(msg)
.unwrap()
.unwrap();
assert_eq!(
parsed
.value
.to_string(),
"sip:a@example.com"
);
assert_eq!(parsed.warnings, [spacing(6), spacing(25), spacing(33)]);
assert_eq!(extract_request_uri(msg), Ok(Some(parsed.value)));
assert_eq!(
extract_request_uri_strict(msg),
Err(ParseError::NonConformant(spacing(6)))
);
let msg = " INVITE sip:a@example.com SIP/2.0\r\n";
assert_eq!(
extract_request_uri_with_warnings(msg)
.unwrap()
.unwrap()
.warnings,
[spacing(0)]
);
let msg = "INVITE sip:a@example.com SIP/2.0\r\n";
assert!(extract_request_uri_strict(msg).is_ok());
let status = "SIP/2.0 200 OK\r\n";
assert_eq!(extract_request_uri_with_warnings(status), Ok(None));
assert_eq!(extract_request_uri_strict(status), Ok(None));
}
#[test]
fn request_uri_warnings_pass_through_at_line_positions() {
let uri = "sip:a@example.com:";
let own = sip_uri::Uri::parse_with_warnings(uri).unwrap();
assert!(!own
.warnings
.is_empty());
let msg = format!("OPTIONS {uri} SIP/2.0\r\n");
let parsed = extract_request_uri_with_warnings(&msg)
.unwrap()
.unwrap();
let shifted: Vec<_> = own
.warnings
.into_iter()
.map(|w| ParseWarning::from_uri(w, "OPTIONS ".len()))
.collect();
assert_eq!(parsed.warnings, shifted);
assert!(extract_request_uri_strict(&msg).is_err());
}
fn all_pairs(msg: &str) -> Vec<(String, String)> {
extract_all_headers(msg)
.headers
.iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.collect()
}
#[test]
fn extract_all_headers_basic() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Via: SIP/2.0/UDP host\r\n",
"From: Alice <sip:alice@example.com>\r\n",
"To: Bob <sip:bob@example.com>\r\n",
"\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 3);
assert_eq!(headers[0], ("Via".into(), "SIP/2.0/UDP host".into()));
assert_eq!(
headers[1],
("From".into(), "Alice <sip:alice@example.com>".into())
);
assert_eq!(
headers[2],
("To".into(), "Bob <sip:bob@example.com>".into())
);
}
#[test]
fn extract_all_headers_folding() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Subject: I know you're there,\r\n",
" pick up the phone\r\n",
" and talk to me!\r\n",
"From: Alice <sip:alice@example.com>\r\n",
"\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 2);
assert_eq!(
headers[0].1,
"I know you're there, pick up the phone and talk to me!"
);
}
#[test]
fn extract_all_headers_compact_forms_verbatim() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"f: Alice <sip:alice@example.com>\r\n",
"t: Bob <sip:bob@example.com>\r\n",
"i: call-1@host\r\n",
"\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 3);
assert_eq!(headers[0].0, "f");
assert_eq!(headers[1].0, "t");
assert_eq!(headers[2].0, "i");
}
#[test]
fn extract_all_headers_stops_at_blank_line() {
let msg = concat!(
"INVITE sip:bob@example.com SIP/2.0\r\n",
"From: Alice <sip:alice@example.com>\r\n",
"\r\n",
"v=0\r\n",
"o=alice 123 456 IN IP4 198.51.100.1\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0, "From");
}
#[test]
fn extract_all_headers_multiple_same_name() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Via: SIP/2.0/UDP first.example.com\r\n",
"Via: SIP/2.0/UDP second.example.com\r\n",
"\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 2);
assert_eq!(
headers[0],
("Via".into(), "SIP/2.0/UDP first.example.com".into())
);
assert_eq!(
headers[1],
("Via".into(), "SIP/2.0/UDP second.example.com".into())
);
}
#[test]
fn extract_all_headers_empty_message() {
assert!(extract_all_headers("")
.headers
.is_empty());
}
#[test]
fn extract_all_headers_skips_request_line() {
let msg = concat!(
"INVITE sip:bob@example.com SIP/2.0\r\n",
"From: Alice <sip:alice@example.com>\r\n",
"\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0, "From");
}
#[test]
fn extract_all_headers_skips_status_line() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"From: Alice <sip:alice@example.com>\r\n",
"\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0, "From");
}
#[test]
fn extract_all_headers_tab_folding() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Subject: hello\r\n",
"\tworld\r\n",
"\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].1, "hello world");
}
#[test]
fn extract_all_headers_empty_value() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Subject:\r\n",
"From: Alice <sip:alice@example.com>\r\n",
"\r\n",
);
let headers = all_pairs(msg);
assert_eq!(headers.len(), 2);
assert_eq!(headers[0], ("Subject".into(), "".into()));
}
#[test]
fn value_trailing_whitespace_trimmed() {
let msg = "SIP/2.0 200 OK\r\nSubject: hi \r\nFrom: <sip:a@example.com>\t\r\n\r\n";
assert_eq!(extract_header(msg, "Subject"), vec!["hi"]);
let headers = all_pairs(msg);
assert_eq!(headers[0].1, "hi");
assert_eq!(headers[1].1, "<sip:a@example.com>");
}
#[test]
fn folded_value_trailing_whitespace_trimmed() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Subject: hello \r\n",
" world \r\n",
"\r\n",
);
assert_eq!(extract_header(msg, "Subject"), vec!["hello world"]);
assert_eq!(all_pairs(msg)[0].1, "hello world");
}
#[test]
fn extract_body_crlf() {
assert_eq!(
extract_body(SAMPLE_INVITE),
Some("v=0\r\no=alice 2890844526 2890844526 IN IP4 pc33.atlanta.example.com\r\n")
);
}
#[test]
fn extract_body_bare_lf() {
let msg = "SIP/2.0 200 OK\n\
From: Alice <sip:alice@host>\n\
\n\
v=0\no=alice 123 456 IN IP4 198.51.100.1\n";
assert_eq!(
extract_body(msg),
Some("v=0\no=alice 123 456 IN IP4 198.51.100.1\n")
);
}
#[test]
fn extract_body_folded_headers() {
let msg = concat!(
"SIP/2.0 200 OK\r\n",
"Subject: I know you're there,\r\n",
" pick up the phone\r\n",
"\r\n",
"body text\r\n",
);
assert_eq!(extract_body(msg), Some("body text\r\n"));
}
#[test]
fn extract_body_no_blank_line() {
let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
From: Alice <sip:alice@example.com>\r\n";
assert_eq!(extract_body(msg), None);
}
#[test]
fn extract_body_blank_line_nothing_after() {
let msg = "SIP/2.0 200 OK\r\n\
From: Alice <sip:alice@host>\r\n\
\r\n";
assert_eq!(extract_body(msg), None);
}
#[test]
fn extract_body_empty_message() {
assert_eq!(extract_body(""), None);
}
#[test]
fn extract_body_multipart_blank_lines_kept() {
let msg = concat!(
"INVITE sip:sos@psap.example.com SIP/2.0\r\n",
"Content-Type: multipart/mixed;boundary=b1\r\n",
"\r\n",
"--b1\r\n",
"Content-Type: application/sdp\r\n",
"\r\n",
"v=0\r\n",
"--b1--\r\n",
);
assert_eq!(
extract_body(msg),
Some("--b1\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n--b1--\r\n")
);
}
#[test]
fn extract_body_header_looking_line_in_body() {
let msg = "SIP/2.0 200 OK\r\n\
\r\n\
From: not a header\r\n";
assert_eq!(extract_body(msg), Some("From: not a header\r\n"));
}
#[test]
fn extract_body_after_request_line() {
let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
\r\n\
v=0\r\n";
assert_eq!(extract_body(msg), Some("v=0\r\n"));
}
#[test]
fn extract_all_headers_bare_lf() {
let msg = "SIP/2.0 200 OK\n\
From: Alice <sip:alice@example.com>\n\
\n\
body\n";
let headers = all_pairs(msg);
assert_eq!(headers.len(), 1);
assert_eq!(
headers[0],
("From".into(), "Alice <sip:alice@example.com>".into())
);
}
#[test]
fn continuation_after_non_header_line_not_folded() {
let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
From: <sip:alice@example.com>\r\n\
not a header\r\n\
\x20continued\r\n\
\r\n";
let all = extract_all_headers(msg);
assert_eq!(
all.headers,
SipHeaderFields::from(vec![("From", "<sip:alice@example.com>")])
);
let at = |s: &str| {
msg.find(s)
.unwrap()
};
assert_eq!(all.skipped, [at("not a header"), at(" continued")]);
assert_eq!(extract_header(msg, "f"), vec!["<sip:alice@example.com>"]);
}
#[test]
fn folded_rows_alone_are_owned() {
let msg = "SIP/2.0 200 OK\r\nSubject: a\r\n b\r\nFrom: <sip:a@example.com>\r\n\r\n";
let borrowed = |value: &str| {
msg.as_bytes()
.as_ptr_range()
.contains(&value.as_ptr())
};
let headers = SipMessageHeaders::new(msg);
let values: Vec<&str> = headers
.iter()
.map(|(_, value)| value)
.collect();
assert_eq!(values, ["a b", "<sip:a@example.com>"]);
assert!(!borrowed(values[0]));
assert!(borrowed(values[1]));
}
#[test]
fn a_name_that_is_not_a_token_is_skipped() {
let msg = "SIP/2.0 200 OK\r\nX Bad: a\r\nVia : SIP/2.0/UDP h\r\n\r\n";
let headers = SipMessageHeaders::new(msg);
assert_eq!(headers.skipped(), [16]);
assert_eq!(
headers
.iter()
.collect::<Vec<_>>(),
[("Via", "SIP/2.0/UDP h")]
);
}
#[test]
fn extract_from_sip_message() {
let msg = concat!(
"INVITE sip:bob@host SIP/2.0\r\n",
"Call-Info: <urn:emergency:uid:callid:abc>;purpose=emergency-CallId\r\n",
"History-Info: <sip:esrp@example.com>;index=1\r\n",
"P-Asserted-Identity: \"Corp\" <sip:+15551234567@198.51.100.1>\r\n",
"\r\n",
);
let ci = SipHeader::CallInfo.extract_from(msg);
assert_eq!(ci.len(), 1);
assert_eq!(
ci[0],
"<urn:emergency:uid:callid:abc>;purpose=emergency-CallId"
);
let hi = SipHeader::HistoryInfo.extract_from(msg);
assert_eq!(hi.len(), 1);
assert_eq!(hi[0], "<sip:esrp@example.com>;index=1");
let pai = SipHeader::PAssertedIdentity.extract_from(msg);
assert_eq!(pai.len(), 1);
assert_eq!(pai[0], "\"Corp\" <sip:+15551234567@198.51.100.1>");
}
}