diff --git a/Cargo.toml b/Cargo.toml index aeecf1d..aaadbf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ rustls-pemfile = "2" [dev-dependencies] # integration tests spawn the built binary and act as a TLS client against it openssl = "0.10" +# property-based fuzzing (parsers) + deterministic S2S simulation (convergence). +proptest = "1" [profile.release] opt-level = 3 diff --git a/src/channels.rs b/src/channels.rs index b7d0bfc..bf84e80 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -1469,6 +1469,18 @@ pub fn normalize_ban_mask(m: &str) -> String { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; + + proptest! { + // Fuzz ban-mask normalisation: no input panics, and it's idempotent + // (normalising an already-normalised mask changes nothing). + #[test] + fn normalize_ban_mask_no_panic_and_idempotent(m in ".*") { + let once = normalize_ban_mask(&m); + let twice = normalize_ban_mask(&once); + prop_assert_eq!(once, twice); + } + } #[test] fn account_extban_mask_is_left_verbatim() { diff --git a/src/message.rs b/src/message.rs index e562d60..5cdb3b1 100644 --- a/src/message.rs +++ b/src/message.rs @@ -121,6 +121,27 @@ pub fn parse(line: &str) -> Option { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; + + proptest! { + // Fuzz: no arbitrary line may panic the parser. + #[test] + fn parse_never_panics(line in ".*") { + let _ = parse(&line); + } + + // Round-trip: a parsed line, re-serialised and re-parsed, yields the same + // source/command/params (to_wire drops tags by design, so we don't compare those). + #[test] + fn parse_roundtrips_core_fields(line in ".*") { + if let Some(m) = parse(&line) { + let again = parse(&m.to_wire()); + prop_assert_eq!(again.as_ref().map(|x| x.source.clone()), Some(m.source.clone())); + prop_assert_eq!(again.as_ref().map(|x| x.command.clone()), Some(m.command.clone())); + prop_assert_eq!(again.as_ref().map(|x| x.params.clone()), Some(m.params.clone())); + } + } + } #[test] fn simple_command() { diff --git a/src/proxy.rs b/src/proxy.rs index 4a47159..15d9b8d 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -202,6 +202,17 @@ fn parse_v2_tlvs(mut tlv: &[u8]) -> (bool, Option) { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; + + proptest! { + // Fuzz the PROXY-header parser: no byte prefix may panic, and it must never + // claim to have consumed more than it was given. + #[test] + fn proxy_parse_never_panics_and_bounds_consumed(buf in prop::collection::vec(any::(), 0..400)) { + let (_p, n) = parse(&buf); + prop_assert!(n <= buf.len()); + } + } fn src(p: &Parsed) -> Option { match p { diff --git a/src/regex.rs b/src/regex.rs index dc85260..81356ae 100644 --- a/src/regex.rs +++ b/src/regex.rs @@ -545,6 +545,19 @@ impl Parser { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; + + proptest! { + // Fuzz the regex engine: no pattern may panic the compiler, and no + // (pattern, text) pair may panic the matcher. The NFA is linear-time, so a + // pathological pattern can't hang it either. + #[test] + fn regex_new_and_match_never_panic(pat in ".*", text in ".*") { + if let Ok(re) = Regex::new(&pat) { + let _ = re.is_match(&text); + } + } + } fn m(pat: &str, text: &str) -> bool { Regex::new(pat).unwrap().is_match(text) diff --git a/src/websocket.rs b/src/websocket.rs index 1ad7eaa..d32c5bc 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -640,6 +640,18 @@ fn header(head: &str, name: &str) -> Option { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; + + proptest! { + // Fuzz the WebSocket frame parser: arbitrary bytes must not panic, and a + // decoded frame must never report consuming past the buffer. + #[test] + fn ws_parse_frame_never_panics_and_bounds(buf in prop::collection::vec(any::(), 0..600)) { + if let Ok(Some((_f, n))) = parse_frame(&buf) { + prop_assert!(n <= buf.len()); + } + } + } #[test] fn accept_key_matches_rfc_example() { diff --git a/src/xline.rs b/src/xline.rs index 7c0da21..488751a 100644 --- a/src/xline.rs +++ b/src/xline.rs @@ -371,3 +371,17 @@ impl Server { } } } + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + proptest! { + // Fuzz the duration parser: no arbitrary string may panic it. + #[test] + fn parse_duration_never_panics(s in ".*") { + let _ = parse_duration(&s); + } + } +}