Skip to main content

stackforge_core/layer/http2/
frames.rs

1//! HTTP/2 frame parsing (RFC 7540 Section 4).
2//!
3//! HTTP/2 frames have a fixed 9-byte header:
4//! ```text
5//! +-----------------------------------------------+
6//! |                 Length (24)                   |
7//! +---------------+---------------+---------------+
8//! |   Type (8)    |   Flags (8)   |
9//! +-+-------------+---------------+-------------------------------+
10//! |R|                 Stream Identifier (31)                      |
11//! +=+=============================================================+
12//! |                   Frame Payload (0...)                      ...
13//! +---------------------------------------------------------------+
14//! ```
15//!
16//! This module provides types and functions for parsing all HTTP/2 frame types.
17
18// ============================================================================
19// Constants
20// ============================================================================
21
22/// The HTTP/2 client connection preface (RFC 7540 Section 3.5).
23pub const HTTP2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
24
25/// Length of the HTTP/2 frame header in bytes.
26pub const HTTP2_FRAME_HEADER_LEN: usize = 9;
27
28// ============================================================================
29// Frame Type
30// ============================================================================
31
32/// HTTP/2 frame type identifiers (RFC 7540 Section 6).
33///
34/// Note: Explicit discriminants are not used because the enum has a non-unit
35/// variant (`Unknown`). Use `as_u8()` to get the numeric frame type value.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum Http2FrameType {
38    /// DATA frame (type=0x0).
39    Data,
40    /// HEADERS frame (type=0x1).
41    Headers,
42    /// PRIORITY frame (type=0x2).
43    Priority,
44    /// RST_STREAM frame (type=0x3).
45    RstStream,
46    /// SETTINGS frame (type=0x4).
47    Settings,
48    /// PUSH_PROMISE frame (type=0x5).
49    PushPromise,
50    /// PING frame (type=0x6).
51    Ping,
52    /// GOAWAY frame (type=0x7).
53    GoAway,
54    /// WINDOW_UPDATE frame (type=0x8).
55    WindowUpdate,
56    /// CONTINUATION frame (type=0x9).
57    Continuation,
58    /// Unknown frame type with raw byte value.
59    Unknown(u8),
60}
61
62impl Http2FrameType {
63    /// Convert a raw u8 byte to an `Http2FrameType`.
64    pub fn from_u8(t: u8) -> Self {
65        match t {
66            0 => Self::Data,
67            1 => Self::Headers,
68            2 => Self::Priority,
69            3 => Self::RstStream,
70            4 => Self::Settings,
71            5 => Self::PushPromise,
72            6 => Self::Ping,
73            7 => Self::GoAway,
74            8 => Self::WindowUpdate,
75            9 => Self::Continuation,
76            _ => Self::Unknown(t),
77        }
78    }
79
80    /// Get the human-readable name of this frame type.
81    pub fn name(&self) -> &'static str {
82        match self {
83            Self::Data => "DATA",
84            Self::Headers => "HEADERS",
85            Self::Priority => "PRIORITY",
86            Self::RstStream => "RST_STREAM",
87            Self::Settings => "SETTINGS",
88            Self::PushPromise => "PUSH_PROMISE",
89            Self::Ping => "PING",
90            Self::GoAway => "GOAWAY",
91            Self::WindowUpdate => "WINDOW_UPDATE",
92            Self::Continuation => "CONTINUATION",
93            Self::Unknown(_) => "UNKNOWN",
94        }
95    }
96
97    /// Convert to raw u8 byte value.
98    pub fn as_u8(&self) -> u8 {
99        match self {
100            Self::Data => 0,
101            Self::Headers => 1,
102            Self::Priority => 2,
103            Self::RstStream => 3,
104            Self::Settings => 4,
105            Self::PushPromise => 5,
106            Self::Ping => 6,
107            Self::GoAway => 7,
108            Self::WindowUpdate => 8,
109            Self::Continuation => 9,
110            Self::Unknown(t) => *t,
111        }
112    }
113}
114
115impl std::fmt::Display for Http2FrameType {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(f, "{}", self.name())
118    }
119}
120
121// ============================================================================
122// Frame Flags
123// ============================================================================
124
125/// Flag constants for HTTP/2 frame types (RFC 7540 Section 6).
126pub mod flags {
127    /// DATA frame: END_STREAM flag (0x1) - indicates the last DATA frame for a stream.
128    pub const DATA_END_STREAM: u8 = 0x01;
129    /// DATA frame: PADDED flag (0x8) - indicates padding is present.
130    pub const DATA_PADDED: u8 = 0x08;
131
132    /// HEADERS frame: END_STREAM flag (0x1) - last HEADERS/CONTINUATION for this stream.
133    pub const HEADERS_END_STREAM: u8 = 0x01;
134    /// HEADERS frame: END_HEADERS flag (0x4) - the frame contains a complete header block.
135    pub const HEADERS_END_HEADERS: u8 = 0x04;
136    /// HEADERS frame: PADDED flag (0x8) - padding is present.
137    pub const HEADERS_PADDED: u8 = 0x08;
138    /// HEADERS frame: PRIORITY flag (0x20) - priority fields are present.
139    pub const HEADERS_PRIORITY: u8 = 0x20;
140
141    /// SETTINGS frame: ACK flag (0x1) - this is an acknowledgment of a SETTINGS frame.
142    pub const SETTINGS_ACK: u8 = 0x01;
143
144    /// PING frame: ACK flag (0x1) - this is a PING response.
145    pub const PING_ACK: u8 = 0x01;
146
147    /// PUSH_PROMISE frame: END_HEADERS flag (0x4) - complete header block.
148    pub const PUSH_PROMISE_END_HEADERS: u8 = 0x04;
149    /// PUSH_PROMISE frame: PADDED flag (0x8) - padding is present.
150    pub const PUSH_PROMISE_PADDED: u8 = 0x08;
151
152    /// CONTINUATION frame: END_HEADERS flag (0x4) - complete header block.
153    pub const CONTINUATION_END_HEADERS: u8 = 0x04;
154}
155
156// ============================================================================
157// HTTP/2 Settings identifiers
158// ============================================================================
159
160/// SETTINGS parameter identifiers (RFC 7540 Section 6.5.2).
161pub mod settings_id {
162    /// SETTINGS_HEADER_TABLE_SIZE (0x1): initial value 4096.
163    pub const HEADER_TABLE_SIZE: u16 = 0x0001;
164    /// SETTINGS_ENABLE_PUSH (0x2): initial value 1.
165    pub const ENABLE_PUSH: u16 = 0x0002;
166    /// SETTINGS_MAX_CONCURRENT_STREAMS (0x3): initially unlimited.
167    pub const MAX_CONCURRENT_STREAMS: u16 = 0x0003;
168    /// SETTINGS_INITIAL_WINDOW_SIZE (0x4): initial value 65535.
169    pub const INITIAL_WINDOW_SIZE: u16 = 0x0004;
170    /// SETTINGS_MAX_FRAME_SIZE (0x5): initial value 16384.
171    pub const MAX_FRAME_SIZE: u16 = 0x0005;
172    /// SETTINGS_MAX_HEADER_LIST_SIZE (0x6): initially unlimited.
173    pub const MAX_HEADER_LIST_SIZE: u16 = 0x0006;
174
175    /// Get the human-readable name of a settings ID.
176    pub fn name(id: u16) -> &'static str {
177        match id {
178            HEADER_TABLE_SIZE => "HEADER_TABLE_SIZE",
179            ENABLE_PUSH => "ENABLE_PUSH",
180            MAX_CONCURRENT_STREAMS => "MAX_CONCURRENT_STREAMS",
181            INITIAL_WINDOW_SIZE => "INITIAL_WINDOW_SIZE",
182            MAX_FRAME_SIZE => "MAX_FRAME_SIZE",
183            MAX_HEADER_LIST_SIZE => "MAX_HEADER_LIST_SIZE",
184            _ => "UNKNOWN",
185        }
186    }
187}
188
189// ============================================================================
190// HTTP/2 Error Codes
191// ============================================================================
192
193/// HTTP/2 error codes (RFC 7540 Section 7).
194pub mod error_codes {
195    pub const NO_ERROR: u32 = 0x0;
196    pub const PROTOCOL_ERROR: u32 = 0x1;
197    pub const INTERNAL_ERROR: u32 = 0x2;
198    pub const FLOW_CONTROL_ERROR: u32 = 0x3;
199    pub const SETTINGS_TIMEOUT: u32 = 0x4;
200    pub const STREAM_CLOSED: u32 = 0x5;
201    pub const FRAME_SIZE_ERROR: u32 = 0x6;
202    pub const REFUSED_STREAM: u32 = 0x7;
203    pub const CANCEL: u32 = 0x8;
204    pub const COMPRESSION_ERROR: u32 = 0x9;
205    pub const CONNECT_ERROR: u32 = 0xa;
206    pub const ENHANCE_YOUR_CALM: u32 = 0xb;
207    pub const INADEQUATE_SECURITY: u32 = 0xc;
208    pub const HTTP_1_1_REQUIRED: u32 = 0xd;
209
210    /// Get the human-readable name for an error code.
211    pub fn name(code: u32) -> &'static str {
212        match code {
213            NO_ERROR => "NO_ERROR",
214            PROTOCOL_ERROR => "PROTOCOL_ERROR",
215            INTERNAL_ERROR => "INTERNAL_ERROR",
216            FLOW_CONTROL_ERROR => "FLOW_CONTROL_ERROR",
217            SETTINGS_TIMEOUT => "SETTINGS_TIMEOUT",
218            STREAM_CLOSED => "STREAM_CLOSED",
219            FRAME_SIZE_ERROR => "FRAME_SIZE_ERROR",
220            REFUSED_STREAM => "REFUSED_STREAM",
221            CANCEL => "CANCEL",
222            COMPRESSION_ERROR => "COMPRESSION_ERROR",
223            CONNECT_ERROR => "CONNECT_ERROR",
224            ENHANCE_YOUR_CALM => "ENHANCE_YOUR_CALM",
225            INADEQUATE_SECURITY => "INADEQUATE_SECURITY",
226            HTTP_1_1_REQUIRED => "HTTP_1_1_REQUIRED",
227            _ => "UNKNOWN",
228        }
229    }
230}
231
232// ============================================================================
233// Http2Frame
234// ============================================================================
235
236/// A parsed HTTP/2 frame, holding the frame header fields and a reference
237/// to where the payload resides in the original buffer.
238#[derive(Debug, Clone)]
239pub struct Http2Frame {
240    /// 24-bit payload length.
241    pub length: u32,
242    /// Frame type.
243    pub frame_type: Http2FrameType,
244    /// Flags byte.
245    pub flags: u8,
246    /// 31-bit stream identifier (R bit masked out).
247    pub stream_id: u32,
248    /// Byte offset in the original buffer where the payload starts.
249    pub payload_offset: usize,
250    /// Total frame size in bytes: 9 (header) + payload length.
251    pub total_size: usize,
252}
253
254impl Http2Frame {
255    /// Parse one HTTP/2 frame from `buf` starting at `offset`.
256    ///
257    /// Returns `None` if there are not enough bytes for the frame header
258    /// or if the payload extends beyond the buffer.
259    pub fn parse_at(buf: &[u8], offset: usize) -> Option<Self> {
260        if offset + HTTP2_FRAME_HEADER_LEN > buf.len() {
261            return None;
262        }
263
264        let header = &buf[offset..offset + HTTP2_FRAME_HEADER_LEN];
265
266        // Length is 3 bytes big-endian
267        let length = ((header[0] as u32) << 16) | ((header[1] as u32) << 8) | (header[2] as u32);
268
269        let frame_type = Http2FrameType::from_u8(header[3]);
270        let flags = header[4];
271        // Stream ID: 4 bytes, R bit (MSB) masked out
272        let stream_id =
273            u32::from_be_bytes([header[5], header[6], header[7], header[8]]) & 0x7FFFFFFF;
274
275        let payload_offset = offset + HTTP2_FRAME_HEADER_LEN;
276        let total_size = HTTP2_FRAME_HEADER_LEN + length as usize;
277
278        // Verify the payload is within the buffer
279        if payload_offset + length as usize > buf.len() {
280            return None;
281        }
282
283        Some(Http2Frame {
284            length,
285            frame_type,
286            flags,
287            stream_id,
288            payload_offset,
289            total_size,
290        })
291    }
292
293    /// Get the payload bytes from the original buffer.
294    pub fn payload<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
295        let end = self.payload_offset + self.length as usize;
296        if end <= buf.len() {
297            &buf[self.payload_offset..end]
298        } else {
299            &[]
300        }
301    }
302
303    /// Returns true if the END_STREAM flag is set.
304    ///
305    /// Valid for DATA and HEADERS frames.
306    pub fn is_end_stream(&self) -> bool {
307        (self.flags & 0x01) != 0
308    }
309
310    /// Returns true if the END_HEADERS flag is set.
311    ///
312    /// Valid for HEADERS, PUSH_PROMISE, and CONTINUATION frames.
313    pub fn is_end_headers(&self) -> bool {
314        (self.flags & 0x04) != 0
315    }
316
317    /// Returns true if the ACK flag is set.
318    ///
319    /// Valid for SETTINGS and PING frames.
320    pub fn is_ack(&self) -> bool {
321        (self.flags & 0x01) != 0
322    }
323
324    /// Returns true if the PADDED flag is set.
325    ///
326    /// Valid for DATA, HEADERS, and PUSH_PROMISE frames.
327    pub fn is_padded(&self) -> bool {
328        (self.flags & 0x08) != 0
329    }
330
331    /// Returns true if the PRIORITY flag is set.
332    ///
333    /// Valid for HEADERS frames.
334    pub fn has_priority(&self) -> bool {
335        (self.flags & 0x20) != 0
336    }
337
338    /// Get a human-readable summary of this frame.
339    pub fn summary(&self) -> String {
340        format!(
341            "{} stream={} length={} flags={:#04x}",
342            self.frame_type.name(),
343            self.stream_id,
344            self.length,
345            self.flags
346        )
347    }
348}
349
350// ============================================================================
351// Multi-frame parsing
352// ============================================================================
353
354/// Parse all HTTP/2 frames from a buffer.
355///
356/// If the buffer starts with the HTTP/2 connection preface (24 bytes), it is
357/// skipped before parsing frames.
358///
359/// Returns a list of successfully parsed frames. Stops at the first truncated
360/// or malformed frame.
361pub fn parse_all_frames(buf: &[u8]) -> Vec<Http2Frame> {
362    let mut frames = Vec::new();
363    let start = if buf.starts_with(HTTP2_PREFACE) {
364        HTTP2_PREFACE.len()
365    } else {
366        0
367    };
368
369    let mut offset = start;
370
371    loop {
372        match Http2Frame::parse_at(buf, offset) {
373            Some(frame) => {
374                let total = frame.total_size;
375                frames.push(frame);
376                offset += total;
377            },
378            None => break,
379        }
380    }
381
382    frames
383}
384
385// ============================================================================
386// Frame-specific payload parsers
387// ============================================================================
388
389/// Parse a SETTINGS frame payload into a list of (id, value) pairs.
390///
391/// Each settings entry is 6 bytes: 2-byte identifier + 4-byte value.
392pub fn parse_settings(payload: &[u8]) -> Vec<(u16, u32)> {
393    let mut settings = Vec::new();
394    let mut pos = 0;
395
396    while pos + 6 <= payload.len() {
397        let id = u16::from_be_bytes([payload[pos], payload[pos + 1]]);
398        let value = u32::from_be_bytes([
399            payload[pos + 2],
400            payload[pos + 3],
401            payload[pos + 4],
402            payload[pos + 5],
403        ]);
404        settings.push((id, value));
405        pos += 6;
406    }
407
408    settings
409}
410
411/// Parse a GOAWAY frame payload.
412///
413/// Returns `Some((last_stream_id, error_code))` or `None` if the payload is too short.
414pub fn parse_goaway(payload: &[u8]) -> Option<(u32, u32)> {
415    if payload.len() < 8 {
416        return None;
417    }
418    let last_stream_id =
419        u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) & 0x7FFFFFFF;
420    let error_code = u32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
421    Some((last_stream_id, error_code))
422}
423
424/// Parse a WINDOW_UPDATE frame payload.
425///
426/// Returns `Some(window_size_increment)` or `None` if the payload is too short.
427pub fn parse_window_update(payload: &[u8]) -> Option<u32> {
428    if payload.len() < 4 {
429        return None;
430    }
431    // R bit (MSB) must be masked out
432    let increment =
433        u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) & 0x7FFFFFFF;
434    Some(increment)
435}
436
437/// Parse a RST_STREAM frame payload.
438///
439/// Returns `Some(error_code)` or `None` if the payload is too short.
440pub fn parse_rst_stream(payload: &[u8]) -> Option<u32> {
441    if payload.len() < 4 {
442        return None;
443    }
444    let error_code = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
445    Some(error_code)
446}
447
448/// Parse a PRIORITY frame payload.
449///
450/// Returns `Some((exclusive, stream_dependency, weight))` or `None` if too short.
451pub fn parse_priority(payload: &[u8]) -> Option<(bool, u32, u8)> {
452    if payload.len() < 5 {
453        return None;
454    }
455    let word = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
456    let exclusive = (word & 0x80000000) != 0;
457    let stream_dependency = word & 0x7FFFFFFF;
458    let weight = payload[4];
459    Some((exclusive, stream_dependency, weight))
460}
461
462/// Extract the header block fragment from a HEADERS frame payload.
463///
464/// Strips padding and priority fields if the corresponding flags are set.
465/// Returns the HPACK-encoded header block fragment slice.
466pub fn headers_fragment<'a>(frame: &Http2Frame, buf: &'a [u8]) -> Option<&'a [u8]> {
467    if frame.frame_type != Http2FrameType::Headers {
468        return None;
469    }
470
471    let payload = frame.payload(buf);
472    let mut start = 0;
473
474    let pad_length = if frame.is_padded() {
475        if payload.is_empty() {
476            return None;
477        }
478        let pl = payload[0] as usize;
479        start += 1;
480        pl
481    } else {
482        0
483    };
484
485    if frame.has_priority() {
486        start += 5; // 4 bytes stream dependency + 1 byte weight
487    }
488
489    let end = payload.len().saturating_sub(pad_length);
490    if start > end {
491        return None;
492    }
493
494    Some(&payload[start..end])
495}
496
497// ============================================================================
498// Tests
499// ============================================================================
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    fn make_frame(frame_type: u8, flags: u8, stream_id: u32, payload: &[u8]) -> Vec<u8> {
506        let len = payload.len() as u32;
507        let mut out = Vec::new();
508        out.push(((len >> 16) & 0xFF) as u8);
509        out.push(((len >> 8) & 0xFF) as u8);
510        out.push((len & 0xFF) as u8);
511        out.push(frame_type);
512        out.push(flags);
513        out.extend_from_slice(&(stream_id & 0x7FFFFFFF).to_be_bytes());
514        out.extend_from_slice(payload);
515        out
516    }
517
518    #[test]
519    fn test_parse_settings_frame() {
520        // SETTINGS frame with no settings (empty payload), no flags, stream=0
521        let frame_bytes = make_frame(4, 0, 0, &[]);
522        let frame = Http2Frame::parse_at(&frame_bytes, 0).unwrap();
523
524        assert_eq!(frame.frame_type, Http2FrameType::Settings);
525        assert_eq!(frame.flags, 0);
526        assert_eq!(frame.stream_id, 0);
527        assert_eq!(frame.length, 0);
528    }
529
530    #[test]
531    fn test_parse_settings_ack() {
532        let frame_bytes = make_frame(4, flags::SETTINGS_ACK, 0, &[]);
533        let frame = Http2Frame::parse_at(&frame_bytes, 0).unwrap();
534
535        assert_eq!(frame.frame_type, Http2FrameType::Settings);
536        assert!(frame.is_ack());
537    }
538
539    #[test]
540    fn test_parse_data_frame() {
541        let payload = b"Hello, HTTP/2!";
542        let frame_bytes = make_frame(0, flags::DATA_END_STREAM, 1, payload);
543        let frame = Http2Frame::parse_at(&frame_bytes, 0).unwrap();
544
545        assert_eq!(frame.frame_type, Http2FrameType::Data);
546        assert!(frame.is_end_stream());
547        assert_eq!(frame.stream_id, 1);
548        assert_eq!(frame.payload(&frame_bytes), payload);
549    }
550
551    #[test]
552    fn test_parse_headers_frame() {
553        let hpack_data = vec![0x82u8]; // ":method: GET"
554        let frame_bytes = make_frame(
555            1,
556            flags::HEADERS_END_HEADERS | flags::HEADERS_END_STREAM,
557            1,
558            &hpack_data,
559        );
560        let frame = Http2Frame::parse_at(&frame_bytes, 0).unwrap();
561
562        assert_eq!(frame.frame_type, Http2FrameType::Headers);
563        assert!(frame.is_end_headers());
564        assert!(frame.is_end_stream());
565        assert_eq!(frame.stream_id, 1);
566    }
567
568    #[test]
569    fn test_parse_all_frames_with_preface() {
570        let mut buf = Vec::new();
571        buf.extend_from_slice(HTTP2_PREFACE);
572        // SETTINGS frame
573        buf.extend_from_slice(&make_frame(4, 0, 0, &[]));
574        // SETTINGS ACK
575        buf.extend_from_slice(&make_frame(4, flags::SETTINGS_ACK, 0, &[]));
576
577        let frames = parse_all_frames(&buf);
578        assert_eq!(frames.len(), 2);
579        assert_eq!(frames[0].frame_type, Http2FrameType::Settings);
580        assert_eq!(frames[1].frame_type, Http2FrameType::Settings);
581        assert!(frames[1].is_ack());
582    }
583
584    #[test]
585    fn test_parse_settings_payload() {
586        // SETTINGS with two entries: INITIAL_WINDOW_SIZE=65535, MAX_FRAME_SIZE=16384
587        let mut payload = Vec::new();
588        payload.extend_from_slice(&settings_id::INITIAL_WINDOW_SIZE.to_be_bytes());
589        payload.extend_from_slice(&65535u32.to_be_bytes());
590        payload.extend_from_slice(&settings_id::MAX_FRAME_SIZE.to_be_bytes());
591        payload.extend_from_slice(&16384u32.to_be_bytes());
592
593        let settings = parse_settings(&payload);
594        assert_eq!(settings.len(), 2);
595        assert_eq!(settings[0], (settings_id::INITIAL_WINDOW_SIZE, 65535));
596        assert_eq!(settings[1], (settings_id::MAX_FRAME_SIZE, 16384));
597    }
598
599    #[test]
600    fn test_parse_goaway_frame() {
601        let mut payload = Vec::new();
602        payload.extend_from_slice(&1u32.to_be_bytes()); // last_stream_id = 1
603        payload.extend_from_slice(&error_codes::NO_ERROR.to_be_bytes());
604
605        let (last_id, error) = parse_goaway(&payload).unwrap();
606        assert_eq!(last_id, 1);
607        assert_eq!(error, error_codes::NO_ERROR);
608    }
609
610    #[test]
611    fn test_parse_window_update() {
612        let mut payload = Vec::new();
613        payload.extend_from_slice(&65535u32.to_be_bytes());
614        let increment = parse_window_update(&payload).unwrap();
615        assert_eq!(increment, 65535);
616    }
617
618    #[test]
619    fn test_parse_rst_stream() {
620        let mut payload = Vec::new();
621        payload.extend_from_slice(&error_codes::CANCEL.to_be_bytes());
622        let error = parse_rst_stream(&payload).unwrap();
623        assert_eq!(error, error_codes::CANCEL);
624    }
625
626    #[test]
627    fn test_frame_type_names() {
628        assert_eq!(Http2FrameType::Data.name(), "DATA");
629        assert_eq!(Http2FrameType::Headers.name(), "HEADERS");
630        assert_eq!(Http2FrameType::Settings.name(), "SETTINGS");
631        assert_eq!(Http2FrameType::GoAway.name(), "GOAWAY");
632        assert_eq!(Http2FrameType::Unknown(0xff).name(), "UNKNOWN");
633    }
634
635    #[test]
636    fn test_frame_type_roundtrip() {
637        for t in 0..=9u8 {
638            let ft = Http2FrameType::from_u8(t);
639            assert_eq!(ft.as_u8(), t);
640        }
641    }
642
643    #[test]
644    fn test_parse_multiple_frames() {
645        let data_payload = b"test data";
646        let mut buf = Vec::new();
647        buf.extend_from_slice(&make_frame(0, 0x01, 1, data_payload)); // DATA END_STREAM
648        buf.extend_from_slice(&make_frame(4, 0x01, 0, &[])); // SETTINGS ACK
649
650        let frames = parse_all_frames(&buf);
651        assert_eq!(frames.len(), 2);
652        assert_eq!(frames[0].frame_type, Http2FrameType::Data);
653        assert_eq!(frames[1].frame_type, Http2FrameType::Settings);
654    }
655
656    #[test]
657    fn test_frame_too_short() {
658        // 8 bytes: not enough for a 9-byte header
659        let buf = [0u8; 8];
660        let result = Http2Frame::parse_at(&buf, 0);
661        assert!(result.is_none());
662    }
663
664    #[test]
665    fn test_frame_payload_truncated() {
666        // 9-byte header saying payload length=100, but no payload bytes
667        let buf = [
668            0x00, 0x00, 0x64, // length = 100
669            0x00, // type = DATA
670            0x00, // flags = 0
671            0x00, 0x00, 0x00, 0x01, // stream_id = 1
672        ];
673        let result = Http2Frame::parse_at(&buf, 0);
674        assert!(result.is_none());
675    }
676
677    #[test]
678    fn test_http2_preface_const() {
679        assert_eq!(HTTP2_PREFACE.len(), 24);
680        assert_eq!(HTTP2_PREFACE, b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
681    }
682
683    #[test]
684    fn test_ping_frame() {
685        let ping_data = [0u8; 8];
686        let frame_bytes = make_frame(6, 0, 0, &ping_data);
687        let frame = Http2Frame::parse_at(&frame_bytes, 0).unwrap();
688
689        assert_eq!(frame.frame_type, Http2FrameType::Ping);
690        assert!(!frame.is_ack());
691        assert_eq!(frame.payload(&frame_bytes).len(), 8);
692    }
693
694    #[test]
695    fn test_settings_id_names() {
696        assert_eq!(
697            settings_id::name(settings_id::HEADER_TABLE_SIZE),
698            "HEADER_TABLE_SIZE"
699        );
700        assert_eq!(settings_id::name(settings_id::ENABLE_PUSH), "ENABLE_PUSH");
701        assert_eq!(settings_id::name(0x99), "UNKNOWN");
702    }
703
704    #[test]
705    fn test_error_code_names() {
706        assert_eq!(error_codes::name(error_codes::NO_ERROR), "NO_ERROR");
707        assert_eq!(
708            error_codes::name(error_codes::PROTOCOL_ERROR),
709            "PROTOCOL_ERROR"
710        );
711        assert_eq!(error_codes::name(0x99), "UNKNOWN");
712    }
713}