Skip to main content

zerodds_grpc_bridge/
frame.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! gRPC Length-Prefixed-Message — Spec §"Requests" + §"Responses".
5
6use alloc::vec::Vec;
7use core::fmt;
8
9/// Spec — `Length-Prefixed-Message` Header-Layout:
10/// ```text
11/// +-----+--------+--------+--------+--------+
12/// | CF  |       Message-Length              |
13/// +-----+--------+--------+--------+--------+
14/// |          Message bytes...                |
15/// +------------------------------------------+
16/// ```
17/// CF = Compressed-Flag (1 byte; 0 = uncompressed, 1 = compressed).
18/// Message-Length = 4-byte big-endian unsigned integer.
19///
20/// gRPC-Web extension: CF mit MSB=1 (`0x80`) markiert Trailers-Frame
21/// (LPM-encoded HTTP-Trailer-Section).
22pub const HEADER_LEN: usize = 5;
23
24/// Codec-Fehler.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum FrameError {
27    /// Header < 5 Bytes.
28    HeaderTooShort,
29    /// Message-Body reicht nicht in die verfuegbaren Bytes.
30    BodyTruncated,
31    /// Message > u32::MAX.
32    MessageTooLarge,
33}
34
35impl fmt::Display for FrameError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::HeaderTooShort => f.write_str("LPM header < 5 bytes"),
39            Self::BodyTruncated => f.write_str("LPM body truncated"),
40            Self::MessageTooLarge => f.write_str("message length exceeds u32"),
41        }
42    }
43}
44
45#[cfg(feature = "std")]
46impl std::error::Error for FrameError {}
47
48/// Encodiert einen Message-Body als Length-Prefixed-Message.
49///
50/// `compressed` setzt das Compressed-Flag-Byte (Spec §"Compressed-
51/// Flag"). Wenn `true`, MUST `Message-Encoding`-Header (Caller) auf
52/// einen non-identity-Encoding-Wert gesetzt sein.
53///
54/// # Errors
55/// `MessageTooLarge` wenn `payload.len() > u32::MAX`.
56pub fn encode_message(payload: &[u8], compressed: bool) -> Result<Vec<u8>, FrameError> {
57    if payload.len() > u32::MAX as usize {
58        return Err(FrameError::MessageTooLarge);
59    }
60    let mut out = Vec::with_capacity(HEADER_LEN + payload.len());
61    out.push(if compressed { 1 } else { 0 });
62    #[allow(clippy::cast_possible_truncation)]
63    out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
64    out.extend_from_slice(payload);
65    Ok(out)
66}
67
68/// Encodiert einen gRPC-Web-Trailers-Frame (CF-MSB=1).
69///
70/// `trailers` ist die UTF-8-encoded Trailer-Section
71/// (`grpc-status: 0\r\ngrpc-message: \r\n` etc.).
72///
73/// # Errors
74/// `MessageTooLarge` wenn `trailers.len() > u32::MAX`.
75pub fn encode_web_trailers(trailers: &[u8]) -> Result<Vec<u8>, FrameError> {
76    if trailers.len() > u32::MAX as usize {
77        return Err(FrameError::MessageTooLarge);
78    }
79    let mut out = Vec::with_capacity(HEADER_LEN + trailers.len());
80    out.push(0x80); // gRPC-Web Trailer-Marker.
81    #[allow(clippy::cast_possible_truncation)]
82    out.extend_from_slice(&(trailers.len() as u32).to_be_bytes());
83    out.extend_from_slice(trailers);
84    Ok(out)
85}
86
87/// Decodiert eine Length-Prefixed-Message. Liefert
88/// `(compressed_flag, message_bytes, consumed_bytes)`.
89///
90/// # Errors
91/// Siehe [`FrameError`].
92pub fn decode_message(bytes: &[u8]) -> Result<(u8, Vec<u8>, usize), FrameError> {
93    if bytes.len() < HEADER_LEN {
94        return Err(FrameError::HeaderTooShort);
95    }
96    let flag = bytes[0];
97    let len = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
98    let total = HEADER_LEN + len;
99    if bytes.len() < total {
100        return Err(FrameError::BodyTruncated);
101    }
102    Ok((flag, bytes[HEADER_LEN..total].to_vec(), total))
103}
104
105#[cfg(test)]
106#[allow(clippy::expect_used)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn empty_message_encodes_to_5_byte_header() {
112        // Spec §"Length-Prefixed-Message" — 5-byte header, length=0.
113        let bytes = encode_message(&[], false).expect("encode");
114        assert_eq!(bytes, alloc::vec![0, 0, 0, 0, 0]);
115    }
116
117    #[test]
118    fn uncompressed_message_has_compressed_flag_zero() {
119        let bytes = encode_message(b"hello", false).expect("encode");
120        assert_eq!(bytes[0], 0);
121        assert_eq!(&bytes[1..5], &5u32.to_be_bytes());
122        assert_eq!(&bytes[5..], b"hello");
123    }
124
125    #[test]
126    fn compressed_message_has_compressed_flag_one() {
127        let bytes = encode_message(b"compressed", true).expect("encode");
128        assert_eq!(bytes[0], 1);
129    }
130
131    #[test]
132    fn round_trip_message() {
133        for payload in [
134            alloc::vec![],
135            alloc::vec![0u8],
136            alloc::vec![1, 2, 3, 4],
137            alloc::vec![0xAB; 1000],
138        ] {
139            let bytes = encode_message(&payload, false).expect("encode");
140            let (flag, decoded, consumed) = decode_message(&bytes).expect("decode");
141            assert_eq!(flag, 0);
142            assert_eq!(decoded, payload);
143            assert_eq!(consumed, bytes.len());
144        }
145    }
146
147    #[test]
148    fn message_length_uses_big_endian_4_bytes() {
149        // Spec — 4-byte BE.
150        let bytes = encode_message(&alloc::vec![0; 256], false).expect("encode");
151        // length=256 -> 0x00 0x00 0x01 0x00.
152        assert_eq!(&bytes[1..5], &[0x00, 0x00, 0x01, 0x00]);
153    }
154
155    #[test]
156    fn header_too_short_decode_fails() {
157        assert_eq!(decode_message(&[]), Err(FrameError::HeaderTooShort));
158        assert_eq!(decode_message(&[0; 4]), Err(FrameError::HeaderTooShort));
159    }
160
161    #[test]
162    fn body_truncated_decode_fails() {
163        // Length=10, but only 3 body bytes.
164        let bytes = [0u8, 0, 0, 0, 10, 1, 2, 3];
165        assert_eq!(decode_message(&bytes), Err(FrameError::BodyTruncated));
166    }
167
168    #[test]
169    fn web_trailers_encoded_with_msb_set() {
170        // gRPC-Web — CF-MSB=1 (0x80).
171        let trailers = b"grpc-status: 0\r\n";
172        let bytes = encode_web_trailers(trailers).expect("encode");
173        assert_eq!(bytes[0], 0x80);
174        assert_eq!(&bytes[1..5], &(trailers.len() as u32).to_be_bytes());
175    }
176
177    #[test]
178    fn back_to_back_messages_can_be_decoded_sequentially() {
179        // Spec: "*Length-Prefixed-Message" — Stream von Messages.
180        let m1 = encode_message(b"first", false).expect("encode");
181        let m2 = encode_message(b"second", false).expect("encode");
182        let mut combined = m1.clone();
183        combined.extend_from_slice(&m2);
184
185        let (_, decoded1, consumed1) = decode_message(&combined).expect("decode 1");
186        assert_eq!(decoded1, b"first");
187        let (_, decoded2, _) = decode_message(&combined[consumed1..]).expect("decode 2");
188        assert_eq!(decoded2, b"second");
189    }
190}