brec 0.6.0

A flexible binary format for storing and streaming structured data as packets with CRC protection and recoverability from corruption. Built for extensibility and robustness.
Documentation
use crate::payload::{NextChunk, PayloadHeader, SafeHeaderReader};
use crate::*;

impl ReadFrom for PayloadHeader {
    /// Reads a complete `PayloadHeader` from the stream.
    ///
    /// This implementation assumes the entire header is available and performs no pre-checks
    /// on input length. It will fail immediately if any part of the header cannot be read.
    ///
    /// # Field Order
    /// 1. Signature length (1 byte)
    /// 2. Signature (`sig_len` bytes)
    /// 3. CRC length (1 byte)
    /// 4. CRC (`crc_len` bytes)
    /// 5. Payload length (4 bytes, LE)
    ///
    /// # Errors
    /// - `Error::InvalidCapacity` if signature or CRC length is invalid
    /// - `Error::InvalidLength` if payload length exceeds `S::MAX_PAYLOAD_LEN`
    /// - `std::io::Error` if reading fails
    /// - Any conversion error from `ByteBlock::try_into()`
    fn read<R: std::io::Read, S: ProtocolSchema>(buf: &mut R) -> Result<Self, Error> {
        let mut sig_len = [0u8; 1];
        buf.read_exact(&mut sig_len)?;
        let sig_len = sig_len[0];
        ByteBlock::is_valid_capacity(sig_len)?;
        let mut sig = vec![0u8; sig_len as usize];
        buf.read_exact(&mut sig)?;
        let mut crc_len = [0u8; 1];
        buf.read_exact(&mut crc_len)?;
        let crc_len = crc_len[0];
        ByteBlock::is_valid_capacity(crc_len)?;
        let mut crc = vec![0u8; crc_len as usize];
        buf.read_exact(&mut crc)?;
        let mut len = [0u8; 4];
        buf.read_exact(&mut len)?;
        let len = u32::from_le_bytes(len);
        if len > S::MAX_PAYLOAD_LEN {
            Err(Error::InvalidLength)
        } else {
            Ok(Self {
                crc: crc.try_into()?,
                len,
                sig: sig.try_into()?,
            })
        }
    }
}

impl TryReadFrom for PayloadHeader {
    /// Tries to read a `PayloadHeader` from a seekable stream using a safe, chunked approach.
    ///
    /// Uses `SafeHeaderReader` and `NextChunk` to avoid over-reading when data is incomplete.
    ///
    /// # Returns
    /// - `ReadStatus::Success(header)` if fully parsed
    /// - `ReadStatus::NotEnoughData(n)` if more bytes are required
    /// - `Error::FailToReadPayloadHeader` if structure is invalid or inconsistent
    /// - `Error::InvalidCapacity` for unsupported signature or CRC lengths
    /// - `Error::InvalidLength` if payload length exceeds `S::MAX_PAYLOAD_LEN`
    fn try_read<T: std::io::Read + std::io::Seek, S: ProtocolSchema>(
        buf: &mut T,
    ) -> Result<ReadStatus<Self>, Error> {
        let mut reader = SafeHeaderReader::new(buf)?;
        let sig_len = match reader.next_u8()? {
            NextChunk::NotEnoughData(n) => return Ok(ReadStatus::NotEnoughData(n)),
            NextChunk::U8(v) => v,
            _ => return Err(Error::FailToReadPayloadHeader),
        };
        ByteBlock::is_valid_capacity(sig_len)?;
        let sig = match reader.next_bytes(sig_len as u64)? {
            NextChunk::NotEnoughData(n) => return Ok(ReadStatus::NotEnoughData(n)),
            NextChunk::Bytes(v) => v,
            _ => return Err(Error::FailToReadPayloadHeader),
        };
        let crc_len = match reader.next_u8()? {
            NextChunk::NotEnoughData(n) => return Ok(ReadStatus::NotEnoughData(n)),
            NextChunk::U8(v) => v,
            _ => return Err(Error::FailToReadPayloadHeader),
        };
        ByteBlock::is_valid_capacity(crc_len)?;
        let crc = match reader.next_bytes(crc_len as u64)? {
            NextChunk::NotEnoughData(n) => return Ok(ReadStatus::NotEnoughData(n)),
            NextChunk::Bytes(v) => v,
            _ => return Err(Error::FailToReadPayloadHeader),
        };
        let len = match reader.next_u32()? {
            NextChunk::NotEnoughData(n) => return Ok(ReadStatus::NotEnoughData(n)),
            NextChunk::U32(v) => v,
            _ => return Err(Error::FailToReadPayloadHeader),
        };
        if len > S::MAX_PAYLOAD_LEN {
            return Err(Error::InvalidLength);
        }
        Ok(ReadStatus::Success(Self {
            crc: crc.try_into()?,
            len,
            sig: sig.try_into()?,
        }))
    }
}

impl TryReadFromBuffered for PayloadHeader {
    /// Tries to read a `PayloadHeader` from a buffered reader (`BufRead`) without consuming it.
    ///
    /// This implementation works purely on the internal buffer (via `fill_buf`) and returns
    /// `NotEnoughData` if more bytes are required to complete the header.
    ///
    /// After confirming that enough bytes are available, it delegates to
    /// `PayloadHeader::read::<_, S>()` using a `Cursor`, so the same schema
    /// length validation applies.
    ///
    /// # Returns
    /// - `ReadStatus::Success(header)` - fully parsed header
    /// - `ReadStatus::NotEnoughData(bytes)` - indicates how many more bytes are needed
    /// - `Error::InvalidCapacity` if signature or CRC size is unsupported
    /// - `Error::InvalidLength` if payload length exceeds `S::MAX_PAYLOAD_LEN`
    fn try_read<T: std::io::BufRead, S: ProtocolSchema>(
        reader: &mut T,
    ) -> Result<ReadStatus<Self>, Error> {
        /// Helper function used in `try_read` to early-return if not enough bytes are in buffer.
        ///
        /// It calculates the required number of bytes and returns `ReadStatus::NotEnoughData` if needed.
        fn ensure_available(buffer: &[u8], required: usize) -> Option<ReadStatus<PayloadHeader>> {
            if buffer.len() < required {
                Some(ReadStatus::NotEnoughData((required - buffer.len()) as u64))
            } else {
                None
            }
        }
        let buffer = reader.fill_buf()?;
        let mut required = 1; // Signature size (u8)
        if let Some(rs) = ensure_available(buffer, required) {
            return Ok(rs);
        }
        let sig_len = buffer[required - 1];
        ByteBlock::is_valid_capacity(sig_len)?;

        required += sig_len as usize; // Add signature len
        if let Some(rs) = ensure_available(buffer, required) {
            return Ok(rs);
        }

        required += 1; // CRC size (u8)
        if let Some(rs) = ensure_available(buffer, required) {
            return Ok(rs);
        }

        let crc_len = buffer[required - 1];
        ByteBlock::is_valid_capacity(crc_len)?;

        required += crc_len as usize; // Add CRC len
        if let Some(rs) = ensure_available(buffer, required) {
            return Ok(rs);
        }

        required += 4; // Payload length (u32)
        if let Some(rs) = ensure_available(buffer, required) {
            return Ok(rs);
        }

        let header = PayloadHeader::read::<_, S>(&mut std::io::Cursor::new(buffer))?;
        Ok(ReadStatus::Success(header))
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        ByteBlock, Error, PayloadHeader, ReadFrom, ReadStatus, TryReadFrom, TryReadFromBuffered,
    };
    use std::io::{BufReader, Cursor, Seek};

    fn sample_header_bytes() -> Vec<u8> {
        PayloadHeader {
            sig: ByteBlock::Len4(*b"ABCD"),
            crc: ByteBlock::Len4([9, 8, 7, 6]),
            len: 1234,
        }
        .as_vec()
    }

    #[test]
    fn read_and_try_read_success() {
        let bytes = sample_header_bytes();

        let mut cursor = Cursor::new(bytes.clone());
        let header = PayloadHeader::read::<_, ()>(&mut cursor).expect("read header");
        assert_eq!(header.payload_len(), 1234);
        assert_eq!(header.sig.as_slice(), b"ABCD");
        assert_eq!(header.crc.as_slice(), &[9, 8, 7, 6]);

        let mut cursor = Cursor::new(bytes);
        match <PayloadHeader as TryReadFrom>::try_read::<_, ()>(&mut cursor).expect("try_read") {
            ReadStatus::Success(header) => {
                assert_eq!(header.payload_len(), 1234);
                assert_eq!(header.sig.as_slice(), b"ABCD");
            }
            ReadStatus::NotEnoughData(_) => panic!("expected Success"),
        }
    }

    #[test]
    fn read_and_try_read_detect_invalid_capacity() {
        let bad_sig_len = vec![3, 1, 2, 3, 4, 0, 0, 0, 0];
        let mut cursor = Cursor::new(bad_sig_len.clone());
        assert!(matches!(
            PayloadHeader::read::<_, ()>(&mut cursor),
            Err(Error::InvalidCapacity(_, _))
        ));

        let mut cursor = Cursor::new(bad_sig_len);
        assert!(matches!(
            <PayloadHeader as TryReadFrom>::try_read::<_, ()>(&mut cursor),
            Err(Error::InvalidCapacity(_, _))
        ));
    }

    #[test]
    fn try_read_and_buffered_try_read_not_enough_keep_stream_position() {
        let bytes = sample_header_bytes();
        let short = bytes[..2].to_vec();

        let mut cursor = Cursor::new(short.clone());
        match <PayloadHeader as TryReadFrom>::try_read::<_, ()>(&mut cursor)
            .expect("try_read short")
        {
            ReadStatus::NotEnoughData(need) => assert!(need > 0),
            ReadStatus::Success(_) => panic!("expected NotEnoughData"),
        }
        assert_eq!(cursor.stream_position().expect("pos"), 0);

        let mut reader = BufReader::new(Cursor::new(short));
        match <PayloadHeader as TryReadFromBuffered>::try_read::<_, ()>(&mut reader)
            .expect("buffered try_read short")
        {
            ReadStatus::NotEnoughData(need) => assert!(need > 0),
            ReadStatus::Success(_) => panic!("expected NotEnoughData"),
        }

        let mut reader = BufReader::new(Cursor::new(bytes));
        match <PayloadHeader as TryReadFromBuffered>::try_read::<_, ()>(&mut reader)
            .expect("buffered try_read full")
        {
            ReadStatus::Success(header) => {
                assert_eq!(header.payload_len(), 1234);
                assert_eq!(header.sig.as_slice(), b"ABCD");
            }
            ReadStatus::NotEnoughData(_) => panic!("expected Success"),
        }
    }
}