Skip to main content

haem_frames/
frame.rs

1//! Length framing leaves deadlines and process ownership to the caller.
2
3use std::fmt;
4use std::io::{self, Read, Write};
5use std::num::NonZeroU32;
6
7/// Why a frame could not be read.
8///
9/// It is the cause inside the `io::Error` that [`read`] returns. A caller
10/// tells the causes apart with [`Fault::of`], never by the error's message.
11/// Each consumer decides its own policy for each.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Fault {
14    /// The header names more bytes than the reader allows. Found on the header,
15    /// before any body is allocated or read.
16    TooLarge { length: u32, limit: u32 },
17    /// The header names no bytes, so the frame has no tag.
18    Empty,
19    /// The input ended inside the header or the body.
20    Cut,
21}
22
23impl Fault {
24    /// The fault a [`read`] error carries; `None` for an I/O failure that is not
25    /// about the frame's format.
26    #[must_use]
27    pub fn of(error: &io::Error) -> Option<Self> {
28        error
29            .get_ref()
30            .and_then(|inner| inner.downcast_ref::<Self>())
31            .copied()
32    }
33}
34
35impl fmt::Display for Fault {
36    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::TooLarge { length, limit } => {
39                write!(
40                    formatter,
41                    "frame of {length} bytes exceeds its {limit}-byte limit"
42                )
43            }
44            Self::Empty => formatter.write_str("frame length is zero"),
45            Self::Cut => formatter.write_str("input ended inside a frame"),
46        }
47    }
48}
49
50impl std::error::Error for Fault {}
51
52/// An input that ends inside a frame becomes [`Fault::Cut`]; any other failure
53/// is passed on as it is.
54fn exact(input: &mut impl Read, bytes: &mut [u8]) -> io::Result<()> {
55    input.read_exact(bytes).map_err(|error| {
56        if error.kind() == io::ErrorKind::UnexpectedEof {
57            io::Error::new(io::ErrorKind::UnexpectedEof, Fault::Cut)
58        } else {
59            error
60        }
61    })
62}
63
64/// Read a complete tagged frame. EOF is clean only before its header.
65///
66/// # Errors
67/// Returns a frame fault for empty, oversized or truncated frames; propagates
68/// other input, size-conversion and allocation failures.
69pub fn read(input: &mut impl Read, limit: NonZeroU32) -> io::Result<Option<Vec<u8>>> {
70    let mut header = [0_u8; 4];
71    loop {
72        match input.read(&mut header[..1]) {
73            Ok(0) => return Ok(None),
74            Ok(_) => break,
75            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
76            Err(error) => return Err(error),
77        }
78    }
79    exact(input, &mut header[1..])?;
80    let length = u32::from_be_bytes(header);
81    if length == 0 {
82        return Err(io::Error::new(io::ErrorKind::InvalidData, Fault::Empty));
83    }
84    if length > limit.get() {
85        let limit = limit.get();
86        return Err(io::Error::new(
87            io::ErrorKind::InvalidData,
88            Fault::TooLarge { length, limit },
89        ));
90    }
91    let size = usize::try_from(length).map_err(io::Error::other)?;
92    let mut body = Vec::new();
93    body.try_reserve_exact(size).map_err(io::Error::other)?;
94    body.resize(size, 0);
95    exact(input, &mut body)?;
96    Ok(Some(body))
97}
98
99/// Write a tag and payload, validating their combined size before any output.
100///
101/// # Errors
102/// Refuses a size overflow or a frame above the limit before writing; propagates
103/// output and flush failures.
104pub fn write(
105    output: &mut impl Write,
106    tag: u8,
107    payload: &[u8],
108    limit: NonZeroU32,
109) -> io::Result<()> {
110    let size = payload
111        .len()
112        .checked_add(1)
113        .ok_or_else(|| io::Error::other("frame length overflow"))?;
114    let length = u32::try_from(size).map_err(io::Error::other)?;
115    if length > limit.get() {
116        return Err(io::Error::new(
117            io::ErrorKind::InvalidData,
118            "frame exceeds its limit",
119        ));
120    }
121    output.write_all(&length.to_be_bytes())?;
122    output.write_all(&[tag])?;
123    output.write_all(payload)?;
124    output.flush()
125}