Skip to main content

ados_protocol/
frame.rs

1//! 4-byte big-endian length-prefixed framing.
2//!
3//! A frame is a 4-byte big-endian unsigned length followed by exactly that
4//! many payload bytes. This is the framing used by three contracts:
5//!
6//! - MAVLink socket: payload is one raw MAVLink frame, max 65536, zero-length
7//!   permitted (the Python server only checks the upper bound).
8//! - plugin RPC socket: payload is a msgpack envelope, max 4 MiB, zero-length
9//!   rejected (the Python reader treats `length == 0` as a protocol error).
10//! - state socket v2: payload is a msgpack state snapshot.
11//!
12//! The reject-zero behaviour differs per contract, so [`decode_len`] takes it
13//! as an argument rather than baking in one policy.
14
15use thiserror::Error;
16
17/// Size of the length prefix in bytes.
18pub const HEADER_SIZE: usize = 4;
19
20/// Maximum MAVLink frame payload (Contract A: `MAX_FRAME_SIZE`).
21pub const MAVLINK_MAX_FRAME: usize = 65536;
22
23/// Maximum plugin RPC envelope payload (Contract C: `MAX_FRAME_BYTES`).
24pub const PLUGIN_MAX_FRAME: usize = 4 * 1024 * 1024;
25
26#[derive(Debug, Error, PartialEq, Eq)]
27pub enum FrameError {
28    #[error("frame length {len} exceeds max {max}")]
29    TooLarge { len: usize, max: usize },
30    #[error("frame length is zero")]
31    ZeroLength,
32    #[error("payload of {0} bytes does not fit in a u32 length prefix")]
33    PayloadTooBig(usize),
34}
35
36/// Encode a payload as a length-prefixed frame: 4-byte big-endian length
37/// followed by the payload. Returns an error if the payload exceeds `max`
38/// or cannot be represented in a u32 length.
39pub fn encode_frame(payload: &[u8], max: usize) -> Result<Vec<u8>, FrameError> {
40    let n = payload.len();
41    if n > max {
42        return Err(FrameError::TooLarge { len: n, max });
43    }
44    let len = u32::try_from(n).map_err(|_| FrameError::PayloadTooBig(n))?;
45    let mut out = Vec::with_capacity(HEADER_SIZE + n);
46    out.extend_from_slice(&len.to_be_bytes());
47    out.extend_from_slice(payload);
48    Ok(out)
49}
50
51/// Decode the payload length from a 4-byte big-endian header.
52///
53/// `reject_zero` selects the per-contract policy: the plugin contract rejects
54/// zero-length frames, the MAVLink contract does not.
55pub fn decode_len(
56    header: [u8; HEADER_SIZE],
57    max: usize,
58    reject_zero: bool,
59) -> Result<usize, FrameError> {
60    let len = u32::from_be_bytes(header) as usize;
61    if reject_zero && len == 0 {
62        return Err(FrameError::ZeroLength);
63    }
64    if len > max {
65        return Err(FrameError::TooLarge { len, max });
66    }
67    Ok(len)
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn round_trip_header_matches_python_struct_pack() {
76        // Python: struct.pack("!I", len(data)) is big-endian unsigned 32-bit.
77        let payload = b"hello world";
78        let frame = encode_frame(payload, MAVLINK_MAX_FRAME).unwrap();
79        assert_eq!(&frame[..HEADER_SIZE], &[0, 0, 0, 11]);
80        assert_eq!(&frame[HEADER_SIZE..], payload);
81
82        let header: [u8; HEADER_SIZE] = frame[..HEADER_SIZE].try_into().unwrap();
83        let len = decode_len(header, MAVLINK_MAX_FRAME, false).unwrap();
84        assert_eq!(len, payload.len());
85    }
86
87    #[test]
88    fn empty_payload_encodes_zero_length() {
89        let frame = encode_frame(b"", MAVLINK_MAX_FRAME).unwrap();
90        assert_eq!(frame, vec![0, 0, 0, 0]);
91    }
92
93    #[test]
94    fn mavlink_allows_zero_length_plugin_rejects_it() {
95        let header = [0u8, 0, 0, 0];
96        // MAVLink contract: zero length is fine.
97        assert_eq!(decode_len(header, MAVLINK_MAX_FRAME, false).unwrap(), 0);
98        // Plugin contract: zero length is a protocol error.
99        assert_eq!(
100            decode_len(header, PLUGIN_MAX_FRAME, true),
101            Err(FrameError::ZeroLength)
102        );
103    }
104
105    #[test]
106    fn oversized_payload_is_rejected_on_encode() {
107        let payload = vec![0u8; MAVLINK_MAX_FRAME + 1];
108        assert_eq!(
109            encode_frame(&payload, MAVLINK_MAX_FRAME),
110            Err(FrameError::TooLarge {
111                len: MAVLINK_MAX_FRAME + 1,
112                max: MAVLINK_MAX_FRAME
113            })
114        );
115    }
116
117    #[test]
118    fn oversized_length_is_rejected_on_decode() {
119        let header = (PLUGIN_MAX_FRAME as u32 + 1).to_be_bytes();
120        assert!(matches!(
121            decode_len(header, PLUGIN_MAX_FRAME, true),
122            Err(FrameError::TooLarge { .. })
123        ));
124    }
125}