Skip to main content

zerodds_http2/
frame.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Frame-Layer — RFC 9113 §4 + §6.
5//!
6//! Frame-Header (9 Bytes):
7//! ```text
8//!  +-----------------------------------------------+
9//!  |                 Length (24)                   |
10//!  +---------------+---------------+---------------+
11//!  |   Type (8)    |   Flags (8)   |
12//!  +-+-------------+---------------+-------------------------------+
13//!  |R|                 Stream Identifier (31)                      |
14//!  +=+=============================================================+
15//!  |                   Frame Payload (0...)                      ...
16//!  +---------------------------------------------------------------+
17//! ```
18
19use crate::error::Http2Error;
20
21/// Frame-Header-Laenge (Bytes).
22pub const FRAME_HEADER_LEN: usize = 9;
23
24/// Default Max-Frame-Size (RFC 9113 §6.5.2: SETTINGS_MAX_FRAME_SIZE
25/// initial value).
26pub const DEFAULT_MAX_FRAME_SIZE: u32 = 16_384;
27
28/// Frame-Type — RFC 9113 §6.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[repr(u8)]
31pub enum FrameType {
32    /// `DATA` (0x0).
33    Data = 0x0,
34    /// `HEADERS` (0x1).
35    Headers = 0x1,
36    /// `PRIORITY` (0x2).
37    Priority = 0x2,
38    /// `RST_STREAM` (0x3).
39    RstStream = 0x3,
40    /// `SETTINGS` (0x4).
41    Settings = 0x4,
42    /// `PUSH_PROMISE` (0x5).
43    PushPromise = 0x5,
44    /// `PING` (0x6).
45    Ping = 0x6,
46    /// `GOAWAY` (0x7).
47    GoAway = 0x7,
48    /// `WINDOW_UPDATE` (0x8).
49    WindowUpdate = 0x8,
50    /// `CONTINUATION` (0x9).
51    Continuation = 0x9,
52}
53
54impl FrameType {
55    /// Mapping `u8 -> FrameType`.
56    ///
57    /// # Errors
58    /// `UnknownFrameType` wenn der Code nicht in 0x0..=0x9 liegt.
59    pub fn from_u8(v: u8) -> Result<Self, Http2Error> {
60        match v {
61            0x0 => Ok(Self::Data),
62            0x1 => Ok(Self::Headers),
63            0x2 => Ok(Self::Priority),
64            0x3 => Ok(Self::RstStream),
65            0x4 => Ok(Self::Settings),
66            0x5 => Ok(Self::PushPromise),
67            0x6 => Ok(Self::Ping),
68            0x7 => Ok(Self::GoAway),
69            0x8 => Ok(Self::WindowUpdate),
70            0x9 => Ok(Self::Continuation),
71            other => Err(Http2Error::UnknownFrameType(other)),
72        }
73    }
74}
75
76/// Frame-Flags (8 Bits, Bedeutung pro Frame-Type).
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub struct Flags(pub u8);
79
80impl Flags {
81    /// `END_STREAM` (DATA/HEADERS, Bit 0).
82    pub const END_STREAM: u8 = 0x1;
83    /// `END_HEADERS` (HEADERS/CONTINUATION/PUSH_PROMISE, Bit 2).
84    pub const END_HEADERS: u8 = 0x4;
85    /// `PADDED` (DATA/HEADERS/PUSH_PROMISE, Bit 3).
86    pub const PADDED: u8 = 0x8;
87    /// `PRIORITY` (HEADERS, Bit 5).
88    pub const PRIORITY: u8 = 0x20;
89    /// `ACK` (SETTINGS/PING, Bit 0).
90    pub const ACK: u8 = 0x1;
91
92    /// Prueft ob ein Bit gesetzt ist.
93    #[must_use]
94    pub fn has(self, bit: u8) -> bool {
95        (self.0 & bit) == bit
96    }
97}
98
99/// Frame-Header (9 Bytes).
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct FrameHeader {
102    /// Payload-Length (24-bit unsigned).
103    pub length: u32,
104    /// Frame-Type.
105    pub frame_type: FrameType,
106    /// Flags.
107    pub flags: Flags,
108    /// Stream-Id (R-Bit + 31 Bit Stream-ID).
109    pub stream_id: u32,
110}
111
112/// Frame mit Header + Borrow-Slice auf das Payload (zero-copy).
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct Frame<'a> {
115    /// Header.
116    pub header: FrameHeader,
117    /// Payload-Slice.
118    pub payload: &'a [u8],
119}
120
121/// Decodiert einen Frame aus einem Byte-Slice. Spec §4.1.
122///
123/// Liefert den Frame und die Anzahl konsumierter Bytes (Header + Payload).
124///
125/// # Errors
126/// * `ShortFrameHeader` wenn weniger als 9 Bytes verfuegbar.
127/// * `ShortPayload` wenn die Length groesser als der verfuegbare
128///   Buffer ist.
129/// * `FrameTooLarge` wenn die Length `max_frame_size` ueberschreitet.
130/// * `UnknownFrameType` wenn das Type-Byte nicht bekannt ist.
131pub fn decode_frame(input: &[u8], max_frame_size: u32) -> Result<(Frame<'_>, usize), Http2Error> {
132    if input.len() < FRAME_HEADER_LEN {
133        return Err(Http2Error::ShortFrameHeader);
134    }
135    let length = (u32::from(input[0]) << 16) | (u32::from(input[1]) << 8) | u32::from(input[2]);
136    if length > max_frame_size {
137        return Err(Http2Error::FrameTooLarge {
138            got: length,
139            max: max_frame_size,
140        });
141    }
142    let frame_type = FrameType::from_u8(input[3])?;
143    let flags = Flags(input[4]);
144    // R-Bit (MSB) abschneiden — Spec §4.1.
145    let stream_id = ((u32::from(input[5]) & 0x7f) << 24)
146        | (u32::from(input[6]) << 16)
147        | (u32::from(input[7]) << 8)
148        | u32::from(input[8]);
149    let total = FRAME_HEADER_LEN + length as usize;
150    if input.len() < total {
151        return Err(Http2Error::ShortPayload);
152    }
153    let payload = &input[FRAME_HEADER_LEN..total];
154    Ok((
155        Frame {
156            header: FrameHeader {
157                length,
158                frame_type,
159                flags,
160                stream_id,
161            },
162            payload,
163        },
164        total,
165    ))
166}
167
168/// Encodiert einen Frame in einen Output-Buffer. Spec §4.1.
169///
170/// # Errors
171/// * `FrameTooLarge` wenn `payload.len() > max_frame_size`.
172/// * `ShortPayload` wenn der Output-Buffer zu klein ist.
173pub fn encode_frame(
174    header: &FrameHeader,
175    payload: &[u8],
176    out: &mut [u8],
177    max_frame_size: u32,
178) -> Result<usize, Http2Error> {
179    let len = payload.len();
180    if len > max_frame_size as usize {
181        return Err(Http2Error::FrameTooLarge {
182            got: len as u32,
183            max: max_frame_size,
184        });
185    }
186    let total = FRAME_HEADER_LEN + len;
187    if out.len() < total {
188        return Err(Http2Error::ShortPayload);
189    }
190    let l = len as u32;
191    out[0] = ((l >> 16) & 0xff) as u8;
192    out[1] = ((l >> 8) & 0xff) as u8;
193    out[2] = (l & 0xff) as u8;
194    out[3] = header.frame_type as u8;
195    out[4] = header.flags.0;
196    let sid = header.stream_id & 0x7fff_ffff;
197    out[5] = ((sid >> 24) & 0xff) as u8;
198    out[6] = ((sid >> 16) & 0xff) as u8;
199    out[7] = ((sid >> 8) & 0xff) as u8;
200    out[8] = (sid & 0xff) as u8;
201    out[FRAME_HEADER_LEN..total].copy_from_slice(payload);
202    Ok(total)
203}
204
205#[cfg(test)]
206#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
207mod tests {
208    use super::*;
209    use alloc::vec;
210
211    #[test]
212    fn from_u8_round_trip() {
213        for ft in [
214            FrameType::Data,
215            FrameType::Headers,
216            FrameType::Priority,
217            FrameType::RstStream,
218            FrameType::Settings,
219            FrameType::PushPromise,
220            FrameType::Ping,
221            FrameType::GoAway,
222            FrameType::WindowUpdate,
223            FrameType::Continuation,
224        ] {
225            assert_eq!(FrameType::from_u8(ft as u8).unwrap(), ft);
226        }
227    }
228
229    #[test]
230    fn unknown_frame_type_rejected() {
231        assert!(matches!(
232            FrameType::from_u8(0xff),
233            Err(Http2Error::UnknownFrameType(0xff))
234        ));
235    }
236
237    #[test]
238    fn flags_has_detects_bits() {
239        let f = Flags(Flags::END_STREAM | Flags::END_HEADERS);
240        assert!(f.has(Flags::END_STREAM));
241        assert!(f.has(Flags::END_HEADERS));
242        assert!(!f.has(Flags::PADDED));
243    }
244
245    #[test]
246    fn encode_decode_round_trip() {
247        let h = FrameHeader {
248            length: 5,
249            frame_type: FrameType::Data,
250            flags: Flags(Flags::END_STREAM),
251            stream_id: 7,
252        };
253        let payload = vec![1, 2, 3, 4, 5];
254        let mut buf = vec![0u8; 32];
255        let n = encode_frame(&h, &payload, &mut buf, DEFAULT_MAX_FRAME_SIZE).unwrap();
256        assert_eq!(n, FRAME_HEADER_LEN + 5);
257        let (frame, consumed) = decode_frame(&buf[..n], DEFAULT_MAX_FRAME_SIZE).unwrap();
258        assert_eq!(consumed, n);
259        assert_eq!(frame.header.length, 5);
260        assert_eq!(frame.header.stream_id, 7);
261        assert_eq!(frame.header.frame_type, FrameType::Data);
262        assert!(frame.header.flags.has(Flags::END_STREAM));
263        assert_eq!(frame.payload, &payload[..]);
264    }
265
266    #[test]
267    fn r_bit_stripped_from_stream_id() {
268        // Construct frame with R-bit set (MSB of stream id byte 5).
269        let mut buf = vec![0u8; 9];
270        buf[3] = FrameType::Settings as u8;
271        buf[5] = 0x80; // R-bit + stream 0
272        let (frame, _) = decode_frame(&buf, DEFAULT_MAX_FRAME_SIZE).unwrap();
273        assert_eq!(frame.header.stream_id, 0, "R-bit must be stripped");
274    }
275
276    #[test]
277    fn frame_too_large_rejected() {
278        let mut buf = vec![0u8; FRAME_HEADER_LEN + 100];
279        // Length = 100
280        buf[2] = 100;
281        let r = decode_frame(&buf, 50);
282        assert!(matches!(
283            r,
284            Err(Http2Error::FrameTooLarge { got: 100, max: 50 })
285        ));
286    }
287
288    #[test]
289    fn short_header_rejected() {
290        let buf = vec![0u8; 5];
291        assert!(matches!(
292            decode_frame(&buf, DEFAULT_MAX_FRAME_SIZE),
293            Err(Http2Error::ShortFrameHeader)
294        ));
295    }
296
297    #[test]
298    fn short_payload_rejected() {
299        // Header says 100 bytes but buffer only has 9 + 50.
300        let mut buf = vec![0u8; FRAME_HEADER_LEN + 50];
301        buf[2] = 100;
302        assert!(matches!(
303            decode_frame(&buf, DEFAULT_MAX_FRAME_SIZE),
304            Err(Http2Error::ShortPayload)
305        ));
306    }
307
308    #[test]
309    fn encode_into_too_small_buffer_rejected() {
310        let h = FrameHeader {
311            length: 0,
312            frame_type: FrameType::Ping,
313            flags: Flags(0),
314            stream_id: 0,
315        };
316        let mut buf = vec![0u8; 5];
317        assert!(encode_frame(&h, b"PINGPING", &mut buf, DEFAULT_MAX_FRAME_SIZE).is_err());
318    }
319}