haem-frames 0.1.1

Shared framing and embedding messages for isolated workers
Documentation
//! Length framing leaves deadlines and process ownership to the caller.

use std::fmt;
use std::io::{self, Read, Write};
use std::num::NonZeroU32;

/// Why a frame could not be read.
///
/// It is the cause inside the `io::Error` that [`read`] returns. A caller
/// tells the causes apart with [`Fault::of`], never by the error's message.
/// Each consumer decides its own policy for each.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Fault {
    /// The header names more bytes than the reader allows. Found on the header,
    /// before any body is allocated or read.
    TooLarge { length: u32, limit: u32 },
    /// The header names no bytes, so the frame has no tag.
    Empty,
    /// The input ended inside the header or the body.
    Cut,
}

impl Fault {
    /// The fault a [`read`] error carries; `None` for an I/O failure that is not
    /// about the frame's format.
    #[must_use]
    pub fn of(error: &io::Error) -> Option<Self> {
        error
            .get_ref()
            .and_then(|inner| inner.downcast_ref::<Self>())
            .copied()
    }
}

impl fmt::Display for Fault {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::TooLarge { length, limit } => {
                write!(
                    formatter,
                    "frame of {length} bytes exceeds its {limit}-byte limit"
                )
            }
            Self::Empty => formatter.write_str("frame length is zero"),
            Self::Cut => formatter.write_str("input ended inside a frame"),
        }
    }
}

impl std::error::Error for Fault {}

/// An input that ends inside a frame becomes [`Fault::Cut`]; any other failure
/// is passed on as it is.
fn exact(input: &mut impl Read, bytes: &mut [u8]) -> io::Result<()> {
    input.read_exact(bytes).map_err(|error| {
        if error.kind() == io::ErrorKind::UnexpectedEof {
            io::Error::new(io::ErrorKind::UnexpectedEof, Fault::Cut)
        } else {
            error
        }
    })
}

/// Read a complete tagged frame. EOF is clean only before its header.
///
/// # Errors
/// Returns a frame fault for empty, oversized or truncated frames; propagates
/// other input, size-conversion and allocation failures.
pub fn read(input: &mut impl Read, limit: NonZeroU32) -> io::Result<Option<Vec<u8>>> {
    let mut header = [0_u8; 4];
    loop {
        match input.read(&mut header[..1]) {
            Ok(0) => return Ok(None),
            Ok(_) => break,
            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
            Err(error) => return Err(error),
        }
    }
    exact(input, &mut header[1..])?;
    let length = u32::from_be_bytes(header);
    if length == 0 {
        return Err(io::Error::new(io::ErrorKind::InvalidData, Fault::Empty));
    }
    if length > limit.get() {
        let limit = limit.get();
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            Fault::TooLarge { length, limit },
        ));
    }
    let size = usize::try_from(length).map_err(io::Error::other)?;
    let mut body = Vec::new();
    body.try_reserve_exact(size).map_err(io::Error::other)?;
    body.resize(size, 0);
    exact(input, &mut body)?;
    Ok(Some(body))
}

/// Write a tag and payload, validating their combined size before any output.
///
/// # Errors
/// Refuses a size overflow or a frame above the limit before writing; propagates
/// output and flush failures.
pub fn write(
    output: &mut impl Write,
    tag: u8,
    payload: &[u8],
    limit: NonZeroU32,
) -> io::Result<()> {
    let size = payload
        .len()
        .checked_add(1)
        .ok_or_else(|| io::Error::other("frame length overflow"))?;
    let length = u32::try_from(size).map_err(io::Error::other)?;
    if length > limit.get() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "frame exceeds its limit",
        ));
    }
    output.write_all(&length.to_be_bytes())?;
    output.write_all(&[tag])?;
    output.write_all(payload)?;
    output.flush()
}