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
mod defaults;
mod header;

pub use header::*;

/// Default protocol context used by payloads that require no runtime state.
pub type DefaultProtocolContext = ();

/// Returns the default protocol context value, equivalent to `()`.
pub const fn default_payload_context() -> DefaultProtocolContext {}

/// Associates a payload type with the runtime context it expects during processing.
pub trait ProtocolSchema {
    /// Runtime context passed into payload encode, decode, and size operations.
    type Context<'a>;

    /// Maximum payload body length accepted by this protocol schema.
    const MAX_PAYLOAD_LEN: u32 = crate::DEFAULT_MAX_PAYLOAD_LEN;

    /// Maximum packet body length accepted by this protocol schema.
    const MAX_PACKET_LEN: u64 = crate::DEFAULT_MAX_PACKET_LEN;

    /// Initial allocation used by packet buffer readers for this schema.
    const INITIAL_PACKET_BUFFER_CAPACITY: usize = crate::DEFAULT_INITIAL_PACKET_BUFFER_CAPACITY;
}

impl ProtocolSchema for () {
    type Context<'a> = ();
}

/// Represents an encoded payload body, either borrowed from the original value
/// or materialized into an owned buffer.
pub enum EncodedPayload<'a> {
    /// Bytes borrowed from an existing encoded representation.
    Borrowed(&'a [u8]),
    /// Bytes materialized into an owned buffer.
    Owned(Vec<u8>),
}

impl EncodedPayload<'_> {
    /// Returns the encoded bytes as a contiguous slice.
    pub fn as_slice(&self) -> &[u8] {
        match self {
            Self::Borrowed(bytes) => bytes,
            Self::Owned(bytes) => bytes.as_slice(),
        }
    }

    /// Returns the length of the encoded payload body.
    pub fn len(&self) -> usize {
        self.as_slice().len()
    }

    /// Returns `true` when the encoded payload body is empty.
    pub fn is_empty(&self) -> bool {
        self.as_slice().is_empty()
    }
}

/// Optional lifecycle hooks for payload encoding and decoding.
///
/// These hooks can be used to prepare the payload before serialization
/// or to perform post-processing after deserialization.
///
/// They are **never required** to do anything - by default, they are no-ops.
///
/// Implement this trait when you want to:
/// - Reset or update internal state before encoding
/// - Validate or transform data after decoding
pub trait PayloadHooks {
    /// Called before encoding begins.
    ///
    /// Can be used to perform cleanup, compute checksums, or update fields.
    fn before_encode(&mut self) -> std::io::Result<()> {
        Ok(())
    }

    /// Called after decoding is complete.
    ///
    /// Can be used to validate, fix up or normalize decoded data.
    fn after_decode(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

/// Trait for serializing a payload into a byte buffer.
///
/// Requires `PayloadHooks`, so `before_encode()` will always be invoked before encoding.
pub trait PayloadEncode: PayloadHooks + ProtocolSchema {
    /// Serializes the payload body into a standalone byte buffer.
    fn encode(&self, ctx: &mut Self::Context<'_>) -> std::io::Result<Vec<u8>>;
}

/// Provides an optional reference to an already-encoded payload.
///
/// This is a performance optimization: if the payload was already serialized,
/// this trait can return a reference to the existing bytes and skip re-encoding.
///
/// Useful in zero-copy or deferred encoding scenarios.
pub trait PayloadEncodeReferred: ProtocolSchema {
    /// Returns a borrowed encoded payload body when one is already available.
    fn encode(&self, ctx: &mut Self::Context<'_>) -> std::io::Result<Option<&[u8]>>;
}

/// Resolves the payload body into either a borrowed slice or an owned buffer.
///
/// This provides a single internal representation that can be reused for
/// computing the payload length, CRC, and actual write operations without
/// repeating the same encode step multiple times.
pub trait PayloadEncoded: PayloadEncode + PayloadEncodeReferred {
    /// Returns the encoded payload body.
    fn encoded(&self, ctx: &mut Self::Context<'_>) -> std::io::Result<EncodedPayload<'_>> {
        if let Some(bytes) = PayloadEncodeReferred::encode(self, ctx)? {
            Ok(EncodedPayload::Borrowed(bytes))
        } else {
            Ok(EncodedPayload::Owned(PayloadEncode::encode(self, ctx)?))
        }
    }
}

impl<T> PayloadEncoded for T where T: PayloadEncode + PayloadEncodeReferred {}

/// Trait for decoding a payload from a byte buffer.
///
/// Requires `PayloadHooks`, so `after_decode()` will always be called after decoding.
pub trait PayloadDecode<T>: PayloadHooks + ProtocolSchema {
    /// Reconstructs a payload value from encoded payload bytes.
    fn decode(buf: &[u8], ctx: &mut Self::Context<'_>) -> std::io::Result<T>;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn encoded_payload_is_empty_for_borrowed_and_owned() {
        let empty_borrowed = EncodedPayload::Borrowed(&[]);
        assert!(empty_borrowed.is_empty());

        let non_empty_borrowed = EncodedPayload::Borrowed(&[1]);
        assert!(!non_empty_borrowed.is_empty());

        let empty_owned = EncodedPayload::Owned(Vec::new());
        assert!(empty_owned.is_empty());

        let non_empty_owned = EncodedPayload::Owned(vec![1, 2, 3]);
        assert!(!non_empty_owned.is_empty());
    }

    #[derive(Default)]
    struct NoopHooksPayload;

    impl ProtocolSchema for NoopHooksPayload {
        type Context<'a> = ();
    }

    impl PayloadHooks for NoopHooksPayload {}

    #[test]
    fn default_payload_hooks_are_noop_ok() {
        let mut payload = NoopHooksPayload;
        assert!(payload.before_encode().is_ok());
        assert!(payload.after_decode().is_ok());
    }
}