use proptest::prelude::*;
use wire_codec::framing::{Delimited, Endian, Framer, LengthPrefixed, LengthWidth, StreamDecoder};
use wire_codec::{Error, WriteBuf};
fn width_strategy() -> impl Strategy<Value = LengthWidth> {
prop_oneof![
Just(LengthWidth::U8),
Just(LengthWidth::U16),
Just(LengthWidth::U24),
Just(LengthWidth::U32),
Just(LengthWidth::U64),
]
}
fn endian_strategy() -> impl Strategy<Value = Endian> {
prop_oneof![Just(Endian::Big), Just(Endian::Little)]
}
const MAX_PROP_PAYLOAD: usize = 255;
proptest! {
#[test]
fn length_prefixed_round_trip(
width in width_strategy(),
endian in endian_strategy(),
payload in proptest::collection::vec(any::<u8>(), 0..MAX_PROP_PAYLOAD),
) {
let framer = LengthPrefixed::new(width, endian);
let mut wire = vec![0u8; width.header_size() + payload.len()];
let mut buf = WriteBuf::new(&mut wire);
framer.write_frame(&payload, &mut buf).unwrap();
let written = buf.position();
prop_assert_eq!(written, width.header_size() + payload.len());
let frame = framer.next_frame(&wire[..written]).unwrap().unwrap();
prop_assert_eq!(frame.payload(), payload.as_slice());
prop_assert_eq!(frame.consumed(), written);
}
#[test]
fn length_prefixed_truncated_returns_none(
width in width_strategy(),
endian in endian_strategy(),
payload in proptest::collection::vec(any::<u8>(), 1..MAX_PROP_PAYLOAD),
cut in 0usize..MAX_PROP_PAYLOAD,
) {
let framer = LengthPrefixed::new(width, endian);
let mut wire = vec![0u8; width.header_size() + payload.len()];
let mut buf = WriteBuf::new(&mut wire);
framer.write_frame(&payload, &mut buf).unwrap();
let written = buf.position();
let truncated_to = cut.min(written.saturating_sub(1));
let result = framer.next_frame(&wire[..truncated_to]).unwrap();
prop_assert!(result.is_none(), "expected None on truncated input, got {result:?}");
}
#[test]
fn length_prefixed_rejects_over_limit(
width in width_strategy(),
endian in endian_strategy(),
cap in 0u64..32,
overhead in 1usize..16,
) {
let framer = LengthPrefixed::new(width, endian).with_max_payload(cap);
let payload = vec![0u8; (cap as usize) + overhead];
let mut wire = vec![0u8; width.header_size() + payload.len()];
let mut buf = WriteBuf::new(&mut wire);
prop_assert!(framer.write_frame(&payload, &mut buf).is_err());
}
#[test]
fn length_prefixed_decode_random_bytes_never_panics(
width in width_strategy(),
endian in endian_strategy(),
bytes in proptest::collection::vec(any::<u8>(), 0..256),
) {
let framer = LengthPrefixed::new(width, endian).with_max_payload(64);
let _ = framer.next_frame(&bytes);
}
#[test]
fn delimited_round_trip(
delim in proptest::collection::vec(1u8..=255, 1..4),
payload in proptest::collection::vec(any::<u8>(), 0..256),
) {
prop_assume!(!contains_subsequence(&payload, &delim));
let delim_static: &'static [u8] = Box::leak(delim.into_boxed_slice());
let framer = Delimited::new(delim_static).unwrap();
let mut wire = vec![0u8; payload.len() + delim_static.len()];
let mut buf = WriteBuf::new(&mut wire);
match framer.write_frame(&payload, &mut buf) {
Ok(()) => {
let written = buf.position();
let frame = framer.next_frame(&wire[..written]).unwrap().unwrap();
prop_assert_eq!(frame.payload(), payload.as_slice());
prop_assert_eq!(frame.consumed(), written);
}
Err(e) => {
prop_assert_eq!(e, Error::DelimiterInPayload);
let mut joined = payload.clone();
joined.extend_from_slice(delim_static);
prop_assert!(find(&joined, delim_static) < Some(payload.len()));
}
}
}
#[test]
fn delimited_rejects_embedded_delimiter(
delim in proptest::collection::vec(1u8..=255, 1..4),
head in proptest::collection::vec(any::<u8>(), 0..32),
tail in proptest::collection::vec(any::<u8>(), 0..32),
) {
let delim_static: &'static [u8] = Box::leak(delim.into_boxed_slice());
let framer = Delimited::new(delim_static).unwrap();
let mut payload = head;
payload.extend_from_slice(delim_static);
payload.extend_from_slice(&tail);
let mut wire = vec![0u8; payload.len() + delim_static.len()];
let mut buf = WriteBuf::new(&mut wire);
prop_assert_eq!(framer.write_frame(&payload, &mut buf), Err(Error::DelimiterInPayload));
prop_assert_eq!(buf.position(), 0);
}
#[test]
fn delimited_streaming_matches_batch(
delim in proptest::collection::vec(1u8..=4, 1..3),
bytes in proptest::collection::vec(0u8..=4, 0..128),
) {
let delim_static: &'static [u8] = Box::leak(delim.into_boxed_slice());
let framer = Delimited::new(delim_static).unwrap();
let mut batch = Vec::new();
let mut rest: &[u8] = &bytes;
while let Some(frame) = framer.next_frame(rest).unwrap() {
batch.push(frame.payload().to_vec());
rest = &rest[frame.consumed()..];
}
let mut decoder = StreamDecoder::new(framer);
let mut streamed = Vec::new();
let mut start = 0;
for end in 0..=bytes.len() {
while let Some(frame) = decoder.decode(&bytes[start..end]).unwrap() {
streamed.push(frame.payload().to_vec());
start += frame.consumed();
}
}
prop_assert_eq!(batch, streamed);
}
#[test]
fn delimited_decode_random_bytes_never_panics(
delim in proptest::collection::vec(1u8..=255, 1..3),
bytes in proptest::collection::vec(any::<u8>(), 0..256),
) {
let delim_static: &'static [u8] = Box::leak(delim.into_boxed_slice());
let framer = Delimited::new(delim_static).unwrap().with_max_payload(128);
let _ = framer.next_frame(&bytes);
}
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if haystack.len() < needle.len() {
return None;
}
(0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
}
fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || haystack.len() < needle.len() {
return false;
}
(0..=haystack.len() - needle.len()).any(|i| &haystack[i..i + needle.len()] == needle)
}