wire-codec 1.1.0

Binary frame codec and protocol codec toolkit. Length-prefixed, varint-prefixed, delimiter-based, checksummed, and custom framing strategies. Built-in varint, zigzag, bitfield, packed struct, CRC, and COBS encoding. Allocation-free core, no_std, runtime-agnostic, with optional tokio-util and futures codec adapters.
Documentation
//! Property tests for the framing strategies.
//!
//! Invariants under test:
//! - LengthPrefixed: encode-then-decode is the identity on payload contents,
//!   and `Frame::consumed` equals the number of bytes written.
//! - LengthPrefixed: truncated wire input returns `Ok(None)` and never panics.
//! - LengthPrefixed: a payload over the configured limit yields
//!   `FrameTooLarge` on both encode and decode.
//! - Delimited: encode-then-decode is the identity, with `Frame::consumed`
//!   counting the delimiter.
//! - Delimited: arbitrary input bytes never panic the decoder.
//! - Delimited: a payload containing the delimiter is rejected on encode.
//! - Delimited: resumable byte-at-a-time decoding finds the same frames as
//!   one-shot decoding.

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)]
}

/// Cap payloads at u16-prefix max so u8/u16 widths can hold the worst case.
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),
    ) {
        // Any outcome is acceptable here except a panic; the result is
        // deliberately discarded.
        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),
    ) {
        // Payloads that contain the delimiter are rejected by `write_frame`;
        // that path is covered by `delimited_rejects_embedded_delimiter`.
        prop_assume!(!contains_subsequence(&payload, &delim));

        // Build a stable lifetime for `delim` by leaking it; tiny allocation
        // per case is acceptable for a property test.
        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);
            }
            // A self-overlapping delimiter (e.g. "aa") can merge with the
            // payload's tail; the framer refuses rather than corrupt it.
            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),
    ) {
        // A small alphabet makes delimiters (and near-misses) frequent.
        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());
        // Any outcome is acceptable here except a panic; the result is
        // deliberately discarded.
        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)
}