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.
65pub fn read(input: &mut impl Read, limit: NonZeroU32) -> io::Result<Option<Vec<u8>>> {
66    let mut header = [0_u8; 4];
67    loop {
68        match input.read(&mut header[..1]) {
69            Ok(0) => return Ok(None),
70            Ok(_) => break,
71            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
72            Err(error) => return Err(error),
73        }
74    }
75    exact(input, &mut header[1..])?;
76    let length = u32::from_be_bytes(header);
77    if length == 0 {
78        return Err(io::Error::new(io::ErrorKind::InvalidData, Fault::Empty));
79    }
80    if length > limit.get() {
81        let limit = limit.get();
82        return Err(io::Error::new(
83            io::ErrorKind::InvalidData,
84            Fault::TooLarge { length, limit },
85        ));
86    }
87    let size = usize::try_from(length).map_err(io::Error::other)?;
88    let mut body = Vec::new();
89    body.try_reserve_exact(size).map_err(io::Error::other)?;
90    body.resize(size, 0);
91    exact(input, &mut body)?;
92    Ok(Some(body))
93}
94
95/// Write a tag and payload, validating their combined size before any output.
96pub fn write(
97    output: &mut impl Write,
98    tag: u8,
99    payload: &[u8],
100    limit: NonZeroU32,
101) -> io::Result<()> {
102    let size = payload
103        .len()
104        .checked_add(1)
105        .ok_or_else(|| io::Error::other("frame length overflow"))?;
106    let length = u32::try_from(size).map_err(io::Error::other)?;
107    if length > limit.get() {
108        return Err(io::Error::new(
109            io::ErrorKind::InvalidData,
110            "frame exceeds its limit",
111        ));
112    }
113    output.write_all(&length.to_be_bytes())?;
114    output.write_all(&[tag])?;
115    output.write_all(payload)?;
116    output.flush()
117}