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/// Error from [`FramedRead::read`]: the frame ran out before the request could
53/// be satisfied. `shortfall` is how many more bytes were requested than the
54/// frame still holds; on this error the reader is left unadvanced.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct EndOfFrame {
57    pub shortfall: usize,
58}
59
60/// A source of framed payload bytes read on demand. This keeps the framing and
61/// word-transport concern in `core`, decoupled from any serializer — a bincode
62/// bridge over `FramedRead` lives in the codec crate, so `core` needs no
63/// dependency on bincode.
64pub trait FramedRead {
65    /// Fill `out` completely, or return [`EndOfFrame`] without advancing if the
66    /// frame does not hold that many more bytes.
67    fn read(&mut self, out: &mut [u8]) -> Result<(), EndOfFrame>;
68}
69
70/// Streaming counterpart to [`read_framed_bytes_with`]: a [`FramedRead`] source
71/// that pulls framed words on demand instead of first materializing the whole
72/// payload into a `Vec<u8>`. Only a single word is buffered at a time, so it
73/// holds O(1) memory regardless of payload size — letting a decoder run at ~1x
74/// peak memory rather than the ~2x of "buffer the blob, then decode it".
75///
76/// The word source must yield the frame length word first, then payload words,
77/// exactly as [`frame_words_from_bytes`] lays them out.
78pub struct FramedReader<F: FnMut() -> u32> {
79    read_word: F,
80    len: usize,
81    remaining: usize,
82    /// Payload words already pulled from the source, so [`Self::discard_rest_of_frame`]
83    /// can consume the rest of the frame without over- or under-reading.
84    pulled_words: usize,
85    word: [u8; WORD_BYTES],
86    /// Index of the next byte to hand out of `word`; `WORD_BYTES` means empty.
87    word_pos: usize,
88}
89
90impl<F: FnMut() -> u32> FramedReader<F> {
91    /// Consume the leading length word and prepare to stream the payload.
92    pub fn new(mut read_word: F) -> Self {
93        let len = read_word() as usize;
94        Self {
95            read_word,
96            len,
97            remaining: len,
98            pulled_words: 0,
99            word: [0u8; WORD_BYTES],
100            // Empty to start, so the first byte requested pulls a word.
101            word_pos: WORD_BYTES,
102        }
103    }
104
105    /// Total framed payload length in bytes (from the leading length word).
106    pub fn payload_len(&self) -> usize {
107        self.len
108    }
109
110    /// Payload bytes not yet handed to the decoder; zero once fully consumed.
111    /// A non-zero value after a successful decode means trailing bytes.
112    pub fn remaining(&self) -> usize {
113        self.remaining
114    }
115
116    /// Total number of payload words in the frame.
117    fn frame_words(&self) -> usize {
118        self.len.div_ceil(WORD_BYTES)
119    }
120
121    /// Pull and discard any payload words the decoder did not consume, leaving
122    /// the source positioned at the next frame's length word. Buffered/decoded
123    /// data is not touched, so [`Self::remaining`] still reports trailing bytes.
124    ///
125    /// Callers should invoke this before returning — success or failure — so a
126    /// rejected frame does not desync a subsequent read, matching the buffered
127    /// [`read_framed_bytes_with`] which always consumes the whole frame.
128    ///
129    /// Cost is bounded by the frame's length word: draining an honestly-framed
130    /// frame reads only the words the source actually holds, but a frame whose
131    /// length word is far larger than its real content will issue that many
132    /// `read_word` calls. This is only reachable via a caller that recovers from
133    /// an error and keeps reading; it is not a concern for honestly-framed input
134    /// (as the guest's is) where the length word reflects the payload.
135    pub fn discard_rest_of_frame(&mut self) {
136        let frame_words = self.frame_words();
137        while self.pulled_words < frame_words {
138            (self.read_word)();
139            self.pulled_words += 1;
140        }
141    }
142}
143
144impl<F: FnMut() -> u32> FramedRead for FramedReader<F> {
145    fn read(&mut self, out: &mut [u8]) -> Result<(), EndOfFrame> {
146        // Reject an unsatisfiable request up front so a failed read never leaves
147        // the reader partially advanced.
148        if self.remaining < out.len() {
149            return Err(EndOfFrame {
150                shortfall: out.len() - self.remaining,
151            });
152        }
153        let mut written = 0;
154        while written < out.len() {
155            if self.word_pos == WORD_BYTES {
156                self.word = (self.read_word)().to_be_bytes();
157                self.word_pos = 0;
158                self.pulled_words += 1;
159            }
160            // Bytes left in the current word, capped by unconsumed payload so
161            // the final word's zero padding is never handed to the decoder.
162            let available = (WORD_BYTES - self.word_pos).min(self.remaining);
163            let n = available.min(out.len() - written);
164            out[written..written + n].copy_from_slice(&self.word[self.word_pos..self.word_pos + n]);
165            self.word_pos += n;
166            self.remaining -= n;
167            written += n;
168        }
169        Ok(())
170    }
171}
172
173/// Frame payload bytes into input words consumed by the runtime.
174pub fn frame_words_from_bytes(bytes: &[u8]) -> Result<Vec<u32>, WireError> {
175    let len_word = frame_len_word(bytes.len())?;
176    let word_count = bytes.len().div_ceil(WORD_BYTES);
177    let mut words = Vec::with_capacity(1 + word_count);
178    words.push(len_word);
179    for chunk in bytes.chunks(WORD_BYTES) {
180        let mut padded = [0u8; WORD_BYTES];
181        padded[..chunk.len()].copy_from_slice(chunk);
182        words.push(u32::from_be_bytes(padded));
183    }
184    Ok(words)
185}
186
187#[cfg(test)]
188mod tests {
189    use super::{frame_len_word, frame_words_from_bytes, read_framed_bytes_with, WireError};
190
191    #[test]
192    fn framing_roundtrip() {
193        let bytes = b"airbender";
194        let words = frame_words_from_bytes(bytes).expect("frame words");
195        assert_eq!(words[0], bytes.len() as u32);
196        let mut cursor = 0;
197        let reconstructed = read_framed_bytes_with(|| {
198            let word = words[cursor];
199            cursor += 1;
200            word
201        });
202        assert_eq!(reconstructed, bytes);
203    }
204
205    #[test]
206    fn closure_reader_handles_partial_word() {
207        let bytes = [0x12u8, 0x34, 0x56];
208        let words = frame_words_from_bytes(&bytes).expect("frame words");
209        let mut cursor = 0;
210        let reconstructed = read_framed_bytes_with(|| {
211            let word = words[cursor];
212            cursor += 1;
213            word
214        });
215        assert_eq!(reconstructed, bytes);
216    }
217
218    #[test]
219    fn rejects_lengths_above_u32_max() {
220        let err = frame_len_word(usize::MAX).expect_err("must reject oversized length");
221        assert_eq!(err, WireError::PayloadTooLarge { len: usize::MAX });
222    }
223
224    #[test]
225    fn framed_reader_streams_same_bytes_as_buffered() {
226        use super::{FramedRead, FramedReader};
227
228        // Empty, aligned, and padded-final-word lengths.
229        for bytes in [b"".as_slice(), b"abcd", b"abcde", b"airbender!!"] {
230            let words = frame_words_from_bytes(bytes).expect("frame words");
231            let mut cursor = 0;
232            let mut reader = FramedReader::new(|| {
233                let word = words[cursor];
234                cursor += 1;
235                word
236            });
237            assert_eq!(reader.payload_len(), bytes.len());
238
239            let mut out = alloc::vec![0u8; bytes.len()];
240            reader.read(&mut out).expect("read payload");
241            assert_eq!(out, bytes);
242            assert_eq!(reader.remaining(), 0, "payload fully consumed");
243
244            // Reading past the frame errors rather than panicking or over-reading.
245            let mut extra = [0u8; 1];
246            assert!(reader.read(&mut extra).is_err());
247        }
248    }
249
250    #[test]
251    fn discard_consumes_exactly_the_rest_of_the_frame() {
252        use super::{FramedRead, FramedReader};
253
254        // Two frames back to back; partially read the first, then discard.
255        let first = frame_words_from_bytes(b"hello world").expect("frame 1"); // 11 bytes, 3 words
256        let second = frame_words_from_bytes(b"next").expect("frame 2");
257        let mut words = first.clone();
258        words.extend_from_slice(&second);
259
260        let mut cursor = 0;
261        let mut reader = FramedReader::new(|| {
262            let word = words[cursor];
263            cursor += 1;
264            word
265        });
266
267        // Read only the first 2 bytes, leaving the rest of frame 1 unread.
268        let mut out = [0u8; 2];
269        reader.read(&mut out).expect("partial read");
270        assert_eq!(&out, b"he");
271        reader.discard_rest_of_frame();
272
273        // The cursor must now sit exactly at frame 2's length word, i.e. all of
274        // frame 1's words (length + payload) have been consumed and no more.
275        assert_eq!(cursor, first.len());
276        assert_eq!(words[cursor], b"next".len() as u32);
277    }
278}