nerve-ipc 0.2.0

Binary framing protocol for local IPC over Unix Domain Sockets
Documentation
//! Codec for NERVE frames.
//!
//! Provides `encode` and `decode` for the wire format.
//! Ensures single allocation on encode and enforces limits
//! like `MAX_PAYLOAD_SIZE`, validating header invariants.

use crate::constants::{MAGIC, MAX_PAYLOAD_SIZE, VERSION};
use crate::error::ProtocolError;
use crate::frame::{Frame, FrameHeader};
use crate::types::{FrameFlags, MessageType, ProtocolErrorKind, RequestId};

/// Encode a frame into a contiguous buffer.
///
/// Allocation: exactly one `Vec`.
///
/// # Errors
///
/// Returns [`ProtocolErrorKind::PayloadTooLarge`] if `payload.len()` exceeds
/// `MAX_PAYLOAD_SIZE` (1 MiB).
pub fn encode(
    msg_type: MessageType,
    flags: FrameFlags,
    request_id: RequestId,
    payload: &[u8],
) -> Result<Vec<u8>, ProtocolError> {
    if payload.len() > MAX_PAYLOAD_SIZE {
        return Err(ProtocolError::new(ProtocolErrorKind::PayloadTooLarge));
    }

    let mut buf = Vec::with_capacity(FrameHeader::SIZE + payload.len());

    // header (little-endian)
    buf.extend_from_slice(&MAGIC.to_le_bytes());
    buf.extend_from_slice(&VERSION.to_le_bytes());
    buf.push(msg_type as u8);
    buf.push(flags.bits());
    buf.extend_from_slice(&request_id.0.to_le_bytes());
    // payload.len() ≤ MAX_PAYLOAD_SIZE (1 MiB), which fits in u32.
    buf.extend_from_slice(&u32::try_from(payload.len()).unwrap().to_le_bytes());

    // payload
    buf.extend_from_slice(payload);

    debug_assert_eq!(buf.len(), FrameHeader::SIZE + payload.len());

    Ok(buf)
}

/// Decode a frame from a byte slice.
///
/// # Errors
///
/// | Condition | Error kind |
/// |-----------|-----------|
/// | `buffer.len() < 20` | [`ProtocolErrorKind::MalformedFrame`] |
/// | Magic bytes mismatch | [`ProtocolErrorKind::InvalidMagic`] |
/// | Version mismatch | [`ProtocolErrorKind::UnsupportedVersion`] |
/// | Unknown `msg_type` byte | [`ProtocolErrorKind::UnknownMessageType`] |
/// | `payload_length > MAX_PAYLOAD_SIZE` | [`ProtocolErrorKind::PayloadTooLarge`] |
/// | Buffer shorter than `20 + payload_length` | [`ProtocolErrorKind::MalformedFrame`] |
///
/// # Panics
///
/// Never panics.  All slice indexing follows the `buffer.len() >= FrameHeader::SIZE`
/// guard at the top of the function, and the fixed-width `try_into()` conversions
/// (e.g. `buffer[0..4].try_into()`) are infallible for the exact slice lengths used.
pub fn decode(buffer: &[u8]) -> Result<Frame<'_>, ProtocolError> {
    if buffer.len() < FrameHeader::SIZE {
        return Err(ProtocolError::new(ProtocolErrorKind::MalformedFrame));
    }

    // header fields
    let magic = u32::from_le_bytes(buffer[0..4].try_into().unwrap());
    if magic != MAGIC {
        return Err(ProtocolError::new(ProtocolErrorKind::InvalidMagic));
    }

    let version = u16::from_le_bytes(buffer[4..6].try_into().unwrap());
    if version != VERSION {
        return Err(ProtocolError::new(ProtocolErrorKind::UnsupportedVersion));
    }

    let msg_type = MessageType::try_from(buffer[6])
        .map_err(|()| ProtocolError::new(ProtocolErrorKind::UnknownMessageType))?;

    let flags = FrameFlags::from_bits_truncate(buffer[7]);

    let request_id = RequestId(u64::from_le_bytes(buffer[8..16].try_into().unwrap()));

    let payload_length_u32 = u32::from_le_bytes(buffer[16..20].try_into().unwrap());
    let payload_length = payload_length_u32 as usize;
    if payload_length > MAX_PAYLOAD_SIZE {
        return Err(ProtocolError::new(ProtocolErrorKind::PayloadTooLarge));
    }

    let total_len = FrameHeader::SIZE + payload_length;
    if buffer.len() < total_len {
        return Err(ProtocolError::new(ProtocolErrorKind::MalformedFrame));
    }

    let payload = &buffer[FrameHeader::SIZE..total_len];

    Ok(Frame {
        header: FrameHeader {
            magic,
            version,
            msg_type: msg_type as u8,
            flags: flags.bits(),
            request_id: request_id.0,
            payload_length: payload_length_u32,
        },
        payload,
    })
}