#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Expect {
ParseOk,
ParseErr(Fault),
HeaderErr(&'static str),
ValidateErr(&'static str),
Unreferenced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fault {
StartLine,
HeaderSyntax,
Framing,
}
#[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 lossy(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(self.bytes)
}
#[must_use]
pub fn is_classified(&self) -> bool {
self.expect != Expect::Unreferenced
}
}
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/rfc4475/", $name, ".dat")),
},
)*];
};
}
use Expect::{HeaderErr, ParseErr, ParseOk, Unreferenced, ValidateErr};
use Fault::{Framing, StartLine};
corpus! {
"wsinv" => "3.1.1.1", "A Short Tortuous INVITE", ParseOk;
"intmeth" => "3.1.1.2", "Wide Range of Valid Characters", ParseOk;
"esc01" => "3.1.1.3", "Valid Use of the % Escaping Mechanism", ParseOk;
"escnull" => "3.1.1.4", "Escaped Nulls in URIs", ParseOk;
"esc02" => "3.1.1.5", "Use of % When It Is Not an Escape", ParseOk;
"lwsdisp" => "3.1.1.6", "Message with No LWS between Display Name and <", ParseOk;
"longreq" => "3.1.1.7", "Long Values in Header Fields", ParseOk;
"dblreq" => "3.1.1.8", "Extra Trailing Octets in a UDP Datagram", ParseOk;
"semiuri" => "3.1.1.9", "Semicolon-Separated Parameters in URI User Part", ParseOk;
"transports" => "3.1.1.10", "Varied and Unknown Transport Types", ParseOk;
"mpart01" => "3.1.1.11", "Multipart MIME Message", ParseOk;
"unreason" => "3.1.1.12", "Unusual Reason Phrase", ParseOk;
"noreason" => "3.1.1.13", "Empty Reason Phrase", ParseOk;
"clerr" => "3.1.2.2", "Content Length Larger Than Message", ParseErr(Framing);
"ncl" => "3.1.2.3", "Negative Content-Length", ParseErr(Framing);
"ltgtruri" => "3.1.2.7", "<> Enclosing Request-URI", ParseErr(StartLine);
"lwsruri" => "3.1.2.8", "Malformed SIP Request-URI (embedded LWS)", ParseErr(StartLine);
"lwsstart" => "3.1.2.9", "Multiple SP Separating Request-Line Elements", ParseErr(StartLine);
"trws" => "3.1.2.10", "SP Characters at End of Request-Line", ParseErr(StartLine);
"bigcode" => "3.1.2.19", "Overlarge Response Code", ParseErr(StartLine);
"badinv01" => "3.1.2.1", "Extraneous Header Field Separators", HeaderErr("Via");
"scalar02" => "3.1.2.4", "Request Scalar Fields with Overlarge Values", HeaderErr("CSeq");
"scalarlg" => "3.1.2.5", "Response Scalar Fields with Overlarge Values", HeaderErr("CSeq");
"quotbal" => "3.1.2.6", "Unterminated Quoted String in Display Name", HeaderErr("To");
"baddate" => "3.1.2.12", "Invalid Time Zone in Date Header Field", HeaderErr("Date");
"regbadct" => "3.1.2.13", "Failure to Enclose name-addr URI in <>", HeaderErr("Contact");
"badaspec" => "3.1.2.14", "Spaces within addr-spec", HeaderErr("To");
"baddn" => "3.1.2.15", "Non-token Characters in Display Name", ParseErr(Framing);
"escruri" => "3.1.2.11", "Escaped Headers in SIP Request-URI",
ValidateErr("a Request-URI may not carry headers (RFC 3261 19.1.1)");
"badvers" => "3.1.2.16", "Unknown Protocol Version",
ValidateErr("unsupported SIP version; answer 505");
"mismatch01" => "3.1.2.17", "Start Line and CSeq Method Mismatch",
ValidateErr("CSeq method must match the request line");
"mismatch02" => "3.1.2.18", "Unknown Method with CSeq Method Mismatch",
ValidateErr("CSeq method must match the request line");
"badbranch" => "3.2.1", "Missing Transaction Identifier", ParseOk;
"insuf" => "3.3.1", "Missing Required Header Fields",
ValidateErr("To, From, Call-ID, CSeq and Via are required");
"unkscm" => "3.3.2", "Request-URI with Unknown Scheme", ParseOk;
"novelsc" => "3.3.3", "Request-URI with Known but Atypical Scheme", ParseOk;
"unksm2" => "3.3.4", "Unknown URI Schemes in Header Fields", ParseOk;
"bext01" => "3.3.5", "Proxy-Require and Require", ParseOk;
"invut" => "3.3.6", "Unknown Content-Type", ParseOk;
"regaut01" => "3.3.7", "Unknown Authorization Scheme", ParseOk;
"multi01" => "3.3.8", "Multiple Values in Single Value Required Fields",
ValidateErr("single-value headers must not be repeated");
"mcl01" => "3.3.9", "Multiple Content-Length Values", ParseErr(Framing);
"bcast" => "3.3.10", "200 OK Response with Broadcast Via Header Field Value", ParseOk;
"zeromf" => "3.3.11", "Max-Forwards of Zero", ParseOk;
"cparam01" => "3.3.12", "REGISTER with a Contact Header Parameter", ParseOk;
"cparam02" => "3.3.13", "REGISTER with a url-parameter", ParseOk;
"regescrt" => "3.3.14", "REGISTER with a URL Escaped Header", ParseOk;
"sdp01" => "3.3.15", "Unacceptable Accept Offering", ParseOk;
"inv2543" => "3.4.1", "INVITE with RFC 2543 Syntax", ParseOk;
"test" => "-", "(not referenced by RFC 4475)", Unreferenced;
}
pub fn classified() -> impl Iterator<Item = &'static Case> {
CASES.iter().filter(|c| c.is_classified())
}
pub fn expecting(expect: Expect) -> impl Iterator<Item = &'static Case> {
CASES.iter().filter(move |c| c.expect == expect)
}
#[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
)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn corpus_is_complete() {
assert_eq!(CASES.len(), 50, "archive holds 50 files");
assert_eq!(classified().count(), 49, "49 are referenced by a section");
let valid = CASES
.iter()
.filter(|c| c.section.starts_with("3.1.1"))
.count();
let invalid = CASES
.iter()
.filter(|c| c.section.starts_with("3.1.2"))
.count();
assert_eq!(valid, 13, "RFC 4475 3.1.1 defines 13 valid messages");
assert_eq!(invalid, 19, "RFC 4475 3.1.2 defines 19 invalid messages");
assert_eq!(
CASES
.iter()
.filter(|c| c.section.starts_with("3.3"))
.count(),
15,
"RFC 4475 3.3 defines 15 application-layer messages"
);
}
#[test]
fn case_names_and_sections_are_unique() {
let names: HashSet<_> = CASES.iter().map(|c| c.name).collect();
assert_eq!(names.len(), CASES.len(), "duplicate case name");
let sections: HashSet<_> = classified().map(|c| c.section).collect();
assert_eq!(sections.len(), 49, "duplicate section reference");
}
#[test]
fn table_matches_the_imported_directory() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/rfc4475");
let mut on_disk: Vec<String> = std::fs::read_dir(dir)
.expect("corpus directory")
.filter_map(Result::ok)
.filter_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
name.strip_suffix(".dat").map(str::to_owned)
})
.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 every_case_has_content() {
for c in CASES {
assert!(!c.bytes.is_empty(), "{} is empty", c.name);
assert!(
c.bytes.windows(2).any(|w| w == b"\r\n"),
"{} has no CRLF; the import may have mangled line endings",
c.name
);
}
}
}