use bytes::Bytes;
pub use crate::rfc4475::{Expect, Fault};
#[derive(Debug, Clone, Copy)]
pub struct Case {
pub name: &'static str,
pub section: &'static str,
pub title: &'static str,
pub expect: Expect,
pub bytes: &'static [u8],
}
impl Case {
#[must_use]
pub fn wire(&self) -> Bytes {
let mut out = Vec::with_capacity(self.bytes.len() + self.bytes.len() / 8 + 2);
for &b in self.bytes {
if b == b'\n' && out.last() != Some(&b'\r') {
out.push(b'\r');
}
out.push(b);
}
if !out.windows(4).any(|w| w == b"\r\n\r\n") {
out.extend_from_slice(b"\r\n");
}
let Some(separator) = out.windows(4).position(|w| w == b"\r\n\r\n") else {
return Bytes::from(out);
};
let (headers, body) = out.split_at(separator + 4);
let body_len = body.len();
let mut result = Vec::with_capacity(out.len() + 8);
for (i, line) in headers.split(|&b| b == b'\n').enumerate() {
if i > 0 {
result.push(b'\n');
}
let name_len = line.iter().position(|&b| b == b':');
let is_content_length = name_len
.and_then(|c| line.get(..c))
.is_some_and(|name| name.eq_ignore_ascii_case(b"Content-Length"));
match name_len.filter(|_| is_content_length) {
Some(colon) => {
result.extend_from_slice(line.get(..=colon).unwrap_or(line));
result.push(b' ');
result.extend_from_slice(body_len.to_string().as_bytes());
result.push(b'\r');
}
None => result.extend_from_slice(line),
}
}
result.extend_from_slice(body);
Bytes::from(result)
}
#[must_use]
pub fn lossy(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(self.bytes)
}
#[must_use]
pub fn is_classified(&self) -> bool {
self.expect != Expect::Unreferenced
}
#[must_use]
pub fn has_sdp(&self) -> bool {
matches!(
self.name,
"ipv6-in-sdp" | "mult-ip-in-sdp" | "ipv4-mapped-ipv6"
)
}
}
macro_rules! corpus {
($($name:literal => $section:literal, $title:literal, $expect:expr;)*) => {
pub static CASES: &[Case] = &[$(
Case {
name: $name,
section: $section,
title: $title,
expect: $expect,
bytes: include_bytes!(concat!("../corpus/rfc5118/", $name)),
},
)*];
};
}
use Expect::{ParseErr, ParseOk};
use Fault::StartLine;
corpus! {
"ipv6-good" => "4.1", "Valid SIP Message with an IPv6 Reference", ParseOk;
"ipv6-bad" => "4.2", "Invalid SIP Message with an IPv6 Reference", ParseErr(StartLine);
"port-ambiguous" => "4.3", "Port Ambiguous in a SIP URI", ParseOk;
"port-unambiguous" => "4.4", "Port Unambiguous in a SIP URI", ParseOk;
"via-received-param-with-delim" => "4.5", "IPv6 Reference Delimiters in Via Header", ParseOk;
"via-received-param-no-delim" => "4.5", "IPv6 Reference Delimiters in Via Header", ParseOk;
"ipv6-in-sdp" => "4.6", "SIP Request with IPv6 Addresses in Session Description Protocol (SDP) Body", ParseOk;
"mult-ip-in-header" => "4.7", "Multiple IP Addresses in SIP Headers", ParseOk;
"mult-ip-in-sdp" => "4.8", "Multiple IP Addresses in SDP", ParseOk;
"ipv4-mapped-ipv6" => "4.9", "IPv4-Mapped IPv6 Addresses", ParseOk;
"ipv6-bug-abnf-3-colons" => "4.10", "IPv6 Reference Bug in RFC 3261 ABNF", ParseOk;
"ipv6-correct-abnf-2-colons" => "4.10", "IPv6 Reference Bug in RFC 3261 ABNF", ParseOk;
}
#[derive(Debug, Clone, Copy)]
pub struct Deviation {
pub case: &'static str,
pub rfc_requires: &'static str,
pub sipx_does: &'static str,
pub why_recorded: &'static str,
}
pub static DEVIATIONS: &[Deviation] = &[];
#[must_use]
pub fn deviation(name: &str) -> Option<&'static Deviation> {
DEVIATIONS.iter().find(|d| d.case == name)
}
#[must_use]
pub fn deviates(name: &str) -> bool {
deviation(name).is_some()
}
pub fn classified() -> impl Iterator<Item = &'static Case> {
CASES.iter().filter(|c| c.is_classified())
}
pub fn conforming() -> impl Iterator<Item = &'static Case> {
CASES.iter().filter(|c| !deviates(c.name))
}
pub fn expecting(expect: Expect) -> impl Iterator<Item = &'static Case> {
CASES.iter().filter(move |c| c.expect == expect)
}
pub fn with_sdp() -> impl Iterator<Item = &'static Case> {
CASES.iter().filter(|c| c.has_sdp())
}
#[must_use]
pub fn case(name: &str) -> Option<&'static Case> {
CASES.iter().find(|c| c.name == name)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
// Counting newlines in a 600-byte fixture does not warrant a dependency.
clippy::naive_bytecount
)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn corpus_is_complete() {
assert_eq!(CASES.len(), 12, "Appendix A's archive holds 12 files");
assert_eq!(
classified().count(),
12,
"every file is referenced by a section"
);
let sections: HashSet<_> = CASES.iter().map(|c| c.section).collect();
assert_eq!(sections.len(), 10, "RFC 5118 section 4 has ten subsections");
for n in 1..=10 {
let section = if n == 10 {
"4.10".to_owned()
} else {
format!("4.{n}")
};
assert!(
CASES.iter().any(|c| c.section == section),
"no case for RFC 5118 section {section}"
);
}
for section in ["4.5", "4.10"] {
assert_eq!(
CASES.iter().filter(|c| c.section == section).count(),
2,
"section {section} contrasts two messages"
);
}
}
#[test]
fn only_section_4_2_is_a_rejection() {
let rejected: Vec<_> = CASES
.iter()
.filter(|c| matches!(c.expect, Expect::ParseErr(_)))
.map(|c| c.name)
.collect();
assert_eq!(
rejected,
vec!["ipv6-bad"],
"RFC 5118 titles exactly one message invalid (§4.2)"
);
assert_eq!(
expecting(ParseOk).count(),
11,
"the other eleven are demonstrations a parser must accept"
);
}
#[test]
fn deviations_name_real_and_valid_cases() {
for d in DEVIATIONS {
let c = case(d.case).unwrap_or_else(|| panic!("{} is not in the corpus", d.case));
assert_eq!(
c.expect, ParseOk,
"{}: a deviation only makes sense for a message the RFC calls valid",
d.case
);
assert!(
!d.rfc_requires.is_empty() && !d.sipx_does.is_empty() && !d.why_recorded.is_empty(),
"{}: a deviation has to say what the RFC wants, what sipx does, and why it stands",
d.case
);
}
assert_eq!(
conforming().count() + DEVIATIONS.len(),
CASES.len(),
"every case is either conforming or a recorded deviation"
);
}
#[test]
fn case_names_are_unique() {
let names: HashSet<_> = CASES.iter().map(|c| c.name).collect();
assert_eq!(names.len(), CASES.len(), "duplicate case name");
}
#[test]
fn table_matches_the_imported_directory() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/rfc5118");
let mut on_disk: Vec<String> = std::fs::read_dir(dir)
.expect("corpus directory")
.filter_map(Result::ok)
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|name| name != "README.md")
.collect();
on_disk.sort();
let mut in_table: Vec<String> = CASES.iter().map(|c| c.name.to_owned()).collect();
in_table.sort();
assert_eq!(
on_disk, in_table,
"corpus directory and case table disagree"
);
}
#[test]
fn the_archive_is_lf_terminated_which_is_why_wire_exists() {
for c in CASES {
assert!(!c.bytes.is_empty(), "{} is empty", c.name);
assert!(
!c.bytes.contains(&b'\r'),
"{} carries a CR; RFC 5118's archive has none, so `wire` needs revisiting",
c.name
);
}
}
#[test]
fn wire_terminates_every_line_and_the_header_section() {
for c in CASES {
let wire = c.wire();
assert_eq!(
wire.iter().filter(|&&b| b == b'\n').count(),
c.bytes.iter().filter(|&&b| b == b'\n').count()
+ usize::from(!c.bytes.windows(2).any(|w| w == b"\n\n")),
"{}: wire must not invent or lose lines",
c.name
);
for (i, &b) in wire.iter().enumerate() {
if b == b'\n' {
assert_eq!(
i.checked_sub(1).and_then(|j| wire.get(j)),
Some(&b'\r'),
"{}: bare LF at offset {i} in the wire form",
c.name
);
}
}
assert!(
wire.windows(4).any(|w| w == b"\r\n\r\n"),
"{}: wire must terminate the header section",
c.name
);
}
}
#[test]
fn wire_changes_only_terminators_and_content_length() {
for c in CASES {
let reduce = |b: &[u8]| -> Vec<Vec<u8>> {
let mut lines: Vec<Vec<u8>> = b
.split(|&b| b == b'\n')
.map(|line| {
line.iter()
.copied()
.filter(|&b| b != b'\r')
.collect::<Vec<u8>>()
})
.filter(|line| !starts_with_ignore_case(line, b"Content-Length:"))
.collect();
while lines.last().is_some_and(Vec::is_empty) {
lines.pop();
}
lines
};
assert_eq!(
reduce(&c.wire()),
reduce(c.bytes),
"{}: wire altered more than line terminators and the Content-Length value",
c.name
);
}
}
fn starts_with_ignore_case(line: &[u8], prefix: &[u8]) -> bool {
line.get(..prefix.len())
.is_some_and(|head| head.eq_ignore_ascii_case(prefix))
}
#[test]
fn wire_content_length_matches_the_body_it_ships() {
for c in CASES {
let wire = c.wire();
let separator = wire
.windows(4)
.position(|w| w == b"\r\n\r\n")
.expect("wire terminates the header section");
let body_len = wire.len() - (separator + 4);
let declared: Option<usize> = wire
.get(..separator)
.unwrap_or(&[])
.split(|&b| b == b'\n')
.find(|line| starts_with_ignore_case(line, b"Content-Length:"))
.and_then(|line| {
let value = line.split(|&b| b == b':').nth(1)?;
std::str::from_utf8(value).ok()?.trim().parse().ok()
});
assert_eq!(
declared,
Some(body_len),
"{}: wire's Content-Length must match its body",
c.name
);
}
}
#[test]
fn the_rfc_declares_wrong_content_lengths_for_its_sdp_messages() {
let declared_in_archive = |c: &Case| -> Option<usize> {
c.bytes
.split(|&b| b == b'\n')
.find(|line| starts_with_ignore_case(line, b"Content-Length:"))
.and_then(|line| {
let value = line.split(|&b| b == b':').nth(1)?;
std::str::from_utf8(value).ok()?.trim().parse().ok()
})
};
for (name, declared, actual) in [
("ipv6-in-sdp", 268, 242),
("mult-ip-in-sdp", 181, 180),
("ipv4-mapped-ipv6", 236, 236),
] {
let c = case(name).expect("in corpus");
assert_eq!(
declared_in_archive(c),
Some(declared),
"{name}: RFC 5118 declares Content-Length {declared}"
);
let separator = c
.bytes
.windows(2)
.position(|w| w == b"\n\n")
.expect("an SDP-bearing message has a body");
assert_eq!(
c.bytes.len() - (separator + 2),
actual,
"{name}: the archive's body is {actual} bytes as shipped"
);
}
let mapped = case("ipv4-mapped-ipv6").expect("in corpus");
let wire = mapped.wire();
let separator = wire
.windows(4)
.position(|w| w == b"\r\n\r\n")
.expect("terminated");
assert_eq!(
wire.len() - (separator + 4),
245,
"§4.9's body is 245 bytes once CRLF-terminated, not the 236 it declares"
);
}
}