1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Expect {
28 ParseOk,
32 ParseErr(Fault),
34 HeaderErr(&'static str),
36 ValidateErr(&'static str),
39 Unreferenced,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Fault {
47 StartLine,
49 HeaderSyntax,
51 Framing,
54}
55
56#[derive(Debug, Clone, Copy)]
58pub struct Case {
59 pub name: &'static str,
61 pub section: &'static str,
63 pub title: &'static str,
65 pub expect: Expect,
67 pub bytes: &'static [u8],
69}
70
71impl Case {
72 #[must_use]
75 pub fn lossy(&self) -> std::borrow::Cow<'_, str> {
76 String::from_utf8_lossy(self.bytes)
77 }
78
79 #[must_use]
81 pub fn is_classified(&self) -> bool {
82 self.expect != Expect::Unreferenced
83 }
84}
85
86macro_rules! corpus {
87 ($($name:literal => $section:literal, $title:literal, $expect:expr;)*) => {
88 pub static CASES: &[Case] = &[$(
90 Case {
91 name: $name,
92 section: $section,
93 title: $title,
94 expect: $expect,
95 bytes: include_bytes!(concat!("../corpus/rfc4475/", $name, ".dat")),
96 },
97 )*];
98 };
99}
100
101use Expect::{HeaderErr, ParseErr, ParseOk, Unreferenced, ValidateErr};
102use Fault::{Framing, StartLine};
103
104corpus! {
105 "wsinv" => "3.1.1.1", "A Short Tortuous INVITE", ParseOk;
107 "intmeth" => "3.1.1.2", "Wide Range of Valid Characters", ParseOk;
108 "esc01" => "3.1.1.3", "Valid Use of the % Escaping Mechanism", ParseOk;
109 "escnull" => "3.1.1.4", "Escaped Nulls in URIs", ParseOk;
110 "esc02" => "3.1.1.5", "Use of % When It Is Not an Escape", ParseOk;
111 "lwsdisp" => "3.1.1.6", "Message with No LWS between Display Name and <", ParseOk;
112 "longreq" => "3.1.1.7", "Long Values in Header Fields", ParseOk;
113 "dblreq" => "3.1.1.8", "Extra Trailing Octets in a UDP Datagram", ParseOk;
114 "semiuri" => "3.1.1.9", "Semicolon-Separated Parameters in URI User Part", ParseOk;
115 "transports" => "3.1.1.10", "Varied and Unknown Transport Types", ParseOk;
116 "mpart01" => "3.1.1.11", "Multipart MIME Message", ParseOk;
117 "unreason" => "3.1.1.12", "Unusual Reason Phrase", ParseOk;
118 "noreason" => "3.1.1.13", "Empty Reason Phrase", ParseOk;
119
120 "clerr" => "3.1.2.2", "Content Length Larger Than Message", ParseErr(Framing);
123 "ncl" => "3.1.2.3", "Negative Content-Length", ParseErr(Framing);
124 "ltgtruri" => "3.1.2.7", "<> Enclosing Request-URI", ParseErr(StartLine);
125 "lwsruri" => "3.1.2.8", "Malformed SIP Request-URI (embedded LWS)", ParseErr(StartLine);
126 "lwsstart" => "3.1.2.9", "Multiple SP Separating Request-Line Elements", ParseErr(StartLine);
127 "trws" => "3.1.2.10", "SP Characters at End of Request-Line", ParseErr(StartLine);
128 "bigcode" => "3.1.2.19", "Overlarge Response Code", ParseErr(StartLine);
129
130 "badinv01" => "3.1.2.1", "Extraneous Header Field Separators", HeaderErr("Via");
138 "scalar02" => "3.1.2.4", "Request Scalar Fields with Overlarge Values", HeaderErr("CSeq");
139 "scalarlg" => "3.1.2.5", "Response Scalar Fields with Overlarge Values", HeaderErr("CSeq");
140 "quotbal" => "3.1.2.6", "Unterminated Quoted String in Display Name", HeaderErr("To");
141 "baddate" => "3.1.2.12", "Invalid Time Zone in Date Header Field", HeaderErr("Date");
142 "regbadct" => "3.1.2.13", "Failure to Enclose name-addr URI in <>", HeaderErr("Contact");
143 "badaspec" => "3.1.2.14", "Spaces within addr-spec", HeaderErr("To");
144 "baddn" => "3.1.2.15", "Non-token Characters in Display Name", ParseErr(Framing);
154
155 "escruri" => "3.1.2.11", "Escaped Headers in SIP Request-URI",
157 ValidateErr("a Request-URI may not carry headers (RFC 3261 19.1.1)");
158 "badvers" => "3.1.2.16", "Unknown Protocol Version",
159 ValidateErr("unsupported SIP version; answer 505");
160 "mismatch01" => "3.1.2.17", "Start Line and CSeq Method Mismatch",
161 ValidateErr("CSeq method must match the request line");
162 "mismatch02" => "3.1.2.18", "Unknown Method with CSeq Method Mismatch",
163 ValidateErr("CSeq method must match the request line");
164
165 "badbranch" => "3.2.1", "Missing Transaction Identifier", ParseOk;
167
168 "insuf" => "3.3.1", "Missing Required Header Fields",
170 ValidateErr("To, From, Call-ID, CSeq and Via are required");
171 "unkscm" => "3.3.2", "Request-URI with Unknown Scheme", ParseOk;
172 "novelsc" => "3.3.3", "Request-URI with Known but Atypical Scheme", ParseOk;
173 "unksm2" => "3.3.4", "Unknown URI Schemes in Header Fields", ParseOk;
174 "bext01" => "3.3.5", "Proxy-Require and Require", ParseOk;
175 "invut" => "3.3.6", "Unknown Content-Type", ParseOk;
176 "regaut01" => "3.3.7", "Unknown Authorization Scheme", ParseOk;
177 "multi01" => "3.3.8", "Multiple Values in Single Value Required Fields",
178 ValidateErr("single-value headers must not be repeated");
179 "mcl01" => "3.3.9", "Multiple Content-Length Values", ParseErr(Framing);
183 "bcast" => "3.3.10", "200 OK Response with Broadcast Via Header Field Value", ParseOk;
184 "zeromf" => "3.3.11", "Max-Forwards of Zero", ParseOk;
185 "cparam01" => "3.3.12", "REGISTER with a Contact Header Parameter", ParseOk;
186 "cparam02" => "3.3.13", "REGISTER with a url-parameter", ParseOk;
187 "regescrt" => "3.3.14", "REGISTER with a URL Escaped Header", ParseOk;
188 "sdp01" => "3.3.15", "Unacceptable Accept Offering", ParseOk;
189
190 "inv2543" => "3.4.1", "INVITE with RFC 2543 Syntax", ParseOk;
192
193 "test" => "-", "(not referenced by RFC 4475)", Unreferenced;
195}
196
197pub fn classified() -> impl Iterator<Item = &'static Case> {
199 CASES.iter().filter(|c| c.is_classified())
200}
201
202pub fn expecting(expect: Expect) -> impl Iterator<Item = &'static Case> {
204 CASES.iter().filter(move |c| c.expect == expect)
205}
206
207#[must_use]
209pub fn case(name: &str) -> Option<&'static Case> {
210 CASES.iter().find(|c| c.name == name)
211}
212
213#[cfg(test)]
214#[allow(
217 clippy::unwrap_used,
218 clippy::expect_used,
219 clippy::panic,
220 clippy::indexing_slicing
221)]
222mod tests {
223 use super::*;
224 use std::collections::HashSet;
225
226 #[test]
230 fn corpus_is_complete() {
231 assert_eq!(CASES.len(), 50, "archive holds 50 files");
232 assert_eq!(classified().count(), 49, "49 are referenced by a section");
233
234 let valid = CASES
235 .iter()
236 .filter(|c| c.section.starts_with("3.1.1"))
237 .count();
238 let invalid = CASES
239 .iter()
240 .filter(|c| c.section.starts_with("3.1.2"))
241 .count();
242 assert_eq!(valid, 13, "RFC 4475 3.1.1 defines 13 valid messages");
243 assert_eq!(invalid, 19, "RFC 4475 3.1.2 defines 19 invalid messages");
244 assert_eq!(
245 CASES
246 .iter()
247 .filter(|c| c.section.starts_with("3.3"))
248 .count(),
249 15,
250 "RFC 4475 3.3 defines 15 application-layer messages"
251 );
252 }
253
254 #[test]
255 fn case_names_and_sections_are_unique() {
256 let names: HashSet<_> = CASES.iter().map(|c| c.name).collect();
257 assert_eq!(names.len(), CASES.len(), "duplicate case name");
258
259 let sections: HashSet<_> = classified().map(|c| c.section).collect();
260 assert_eq!(sections.len(), 49, "duplicate section reference");
261 }
262
263 #[test]
266 fn table_matches_the_imported_directory() {
267 let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/rfc4475");
268 let mut on_disk: Vec<String> = std::fs::read_dir(dir)
269 .expect("corpus directory")
270 .filter_map(Result::ok)
271 .filter_map(|e| {
272 let name = e.file_name().to_string_lossy().into_owned();
273 name.strip_suffix(".dat").map(str::to_owned)
274 })
275 .collect();
276 on_disk.sort();
277
278 let mut in_table: Vec<String> = CASES.iter().map(|c| c.name.to_owned()).collect();
279 in_table.sort();
280
281 assert_eq!(
282 on_disk, in_table,
283 "corpus directory and case table disagree"
284 );
285 }
286
287 #[test]
288 fn every_case_has_content() {
289 for c in CASES {
290 assert!(!c.bytes.is_empty(), "{} is empty", c.name);
291 assert!(
292 c.bytes.windows(2).any(|w| w == b"\r\n"),
293 "{} has no CRLF; the import may have mangled line endings",
294 c.name
295 );
296 }
297 }
298}