Skip to main content

airbender_core/
wire.rs

1//! Canonical host/guest input wire format.
2//!
3//! The input stream is encoded as `u32` words where:
4//! - the first word stores payload byte length,
5//! - each following word stores up to 4 payload bytes in big-endian order,
6//! - the final word is zero-padded when payload length is not a multiple of 4.
7
8use alloc::vec::Vec;
9use core::fmt;
10
11const WORD_BYTES: usize = 4;
12
13/// Errors that can occur while framing input payloads.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum WireError {
16    PayloadTooLarge { len: usize },
17}
18
19impl fmt::Display for WireError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            WireError::PayloadTooLarge { len } => {
23                write!(f, "payload length {len} exceeds u32 framing limit")
24            }
25        }
26    }
27}
28
29fn frame_len_word(len: usize) -> Result<u32, WireError> {
30    u32::try_from(len).map_err(|_| WireError::PayloadTooLarge { len })
31}
32
33/// Read one framed payload from a word source.
34///
35/// The provided callback must yield the frame length word first, then payload words.
36pub fn read_framed_bytes_with(mut read_word: impl FnMut() -> u32) -> Vec<u8> {
37    let len = read_word() as usize;
38    let words_needed = len.div_ceil(WORD_BYTES);
39
40    let mut bytes = Vec::with_capacity(len);
41    let mut remaining = len;
42    for _ in 0..words_needed {
43        let word_bytes = read_word().to_be_bytes();
44        let bytes_to_take = remaining.min(WORD_BYTES);
45        bytes.extend_from_slice(&word_bytes[..bytes_to_take]);
46        remaining -= bytes_to_take;
47    }
48
49    bytes
50}
51
52/// Frame payload bytes into input words consumed by the runtime.
53pub fn frame_words_from_bytes(bytes: &[u8]) -> Result<Vec<u32>, WireError> {
54    let len_word = frame_len_word(bytes.len())?;
55    let word_count = bytes.len().div_ceil(WORD_BYTES);
56    let mut words = Vec::with_capacity(1 + word_count);
57    words.push(len_word);
58    for chunk in bytes.chunks(WORD_BYTES) {
59        let mut padded = [0u8; WORD_BYTES];
60        padded[..chunk.len()].copy_from_slice(chunk);
61        words.push(u32::from_be_bytes(padded));
62    }
63    Ok(words)
64}
65
66#[cfg(test)]
67mod tests {
68    use super::{frame_len_word, frame_words_from_bytes, read_framed_bytes_with, WireError};
69
70    #[test]
71    fn framing_roundtrip() {
72        let bytes = b"airbender";
73        let words = frame_words_from_bytes(bytes).expect("frame words");
74        assert_eq!(words[0], bytes.len() as u32);
75        let mut cursor = 0;
76        let reconstructed = read_framed_bytes_with(|| {
77            let word = words[cursor];
78            cursor += 1;
79            word
80        });
81        assert_eq!(reconstructed, bytes);
82    }
83
84    #[test]
85    fn closure_reader_handles_partial_word() {
86        let bytes = [0x12u8, 0x34, 0x56];
87        let words = frame_words_from_bytes(&bytes).expect("frame words");
88        let mut cursor = 0;
89        let reconstructed = read_framed_bytes_with(|| {
90            let word = words[cursor];
91            cursor += 1;
92            word
93        });
94        assert_eq!(reconstructed, bytes);
95    }
96
97    #[test]
98    fn rejects_lengths_above_u32_max() {
99        let err = frame_len_word(usize::MAX).expect_err("must reject oversized length");
100        assert_eq!(err, WireError::PayloadTooLarge { len: usize::MAX });
101    }
102}