Skip to main content

vane_core/h2/
frame.rs

1//! HTTP/2 frame codec (RFC 9113 §6) — header parsing, typed payload
2//! validation, and the constants shared by the connection state
3//! machine.
4//!
5//! Every frame starts with a 9-byte header:
6//!
7//! ```text
8//! +-----------------------------------------------+
9//! |                 Length (24)                   |
10//! +---------------+---------------+---------------+
11//! |   Type (8)    |   Flags (8)   |
12//! +-+-------------+---------------+-------------------------------+
13//! |R|                 Stream Identifier (31)                      |
14//! +=+=============================================================+
15//! ```
16//!
17//! All decode paths are panic-free on arbitrary bytes (fuzz target:
18//! `fuzz/frame.rs`).
19
20/// Frame header size in bytes.
21pub const FRAME_HEADER_LEN: usize = 9;
22
23/// Default max frame payload size (RFC 9113 §6.1) before
24/// SETTINGS_MAX_FRAME_SIZE raises it (peer-advertised, still capped at
25/// 16 MiB).
26pub const DEFAULT_MAX_FRAME_SIZE: u32 = 16_384;
27/// Hard ceiling for SETTINGS_MAX_FRAME_SIZE.
28pub const MAX_ALLOWED_FRAME_SIZE: u32 = 16_777_215;
29
30/// Frame types (RFC 9113 §6).
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum FrameKind {
33    /// Stream data.
34    Data,
35    /// Header block fragment.
36    Headers,
37    /// Deprecated priority hint (parsed, ignored).
38    Priority,
39    /// Stream reset.
40    RstStream,
41    /// Connection settings.
42    Settings,
43    /// Server push (rejected by clients; unsupported here).
44    PushPromise,
45    /// Liveness ping (opaque 8-byte payload).
46    Ping,
47    /// Graceful shutdown notice.
48    GoAway,
49    /// Flow-control window credit.
50    WindowUpdate,
51    /// CONTINUATION of a HEADERS block.
52    Continuation,
53    /// Unknown/extension type — payload skipped.
54    Unknown(u8),
55}
56
57impl FrameKind {
58    fn from_u8(b: u8) -> Self {
59        match b {
60            0x0 => Self::Data,
61            0x1 => Self::Headers,
62            0x2 => Self::Priority,
63            0x3 => Self::RstStream,
64            0x4 => Self::Settings,
65            0x5 => Self::PushPromise,
66            0x6 => Self::Ping,
67            0x7 => Self::GoAway,
68            0x8 => Self::WindowUpdate,
69            0x9 => Self::Continuation,
70            other => Self::Unknown(other),
71        }
72    }
73
74    /// Wire byte for this frame type.
75    #[must_use]
76    pub fn as_u8(self) -> u8 {
77        match self {
78            Self::Data => 0x0,
79            Self::Headers => 0x1,
80            Self::Priority => 0x2,
81            Self::RstStream => 0x3,
82            Self::Settings => 0x4,
83            Self::PushPromise => 0x5,
84            Self::Ping => 0x6,
85            Self::GoAway => 0x7,
86            Self::WindowUpdate => 0x8,
87            Self::Continuation => 0x9,
88            Self::Unknown(b) => b,
89        }
90    }
91}
92
93/// Frame flags — bit meanings depend on `FrameKind`.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
95pub struct FrameFlags(u8);
96
97impl FrameFlags {
98    /// No flags set.
99    pub const EMPTY: Self = Self(0);
100
101    /// DATA: END_STREAM. HEADERS: END_STREAM.
102    #[must_use]
103    pub fn end_stream(self) -> bool {
104        self.0 & 0x01 != 0
105    }
106    /// HEADERS: END_HEADERS. CONTINUATION: END_HEADERS.
107    #[must_use]
108    pub fn end_headers(self) -> bool {
109        self.0 & 0x04 != 0
110    }
111    /// DATA: PADDED. HEADERS: PADDED. PUSH_PROMISE: PADDED.
112    #[must_use]
113    pub fn padded(self) -> bool {
114        self.0 & 0x08 != 0
115    }
116    /// DATA: END_STREAM (same bit).
117    #[must_use]
118    pub fn ack(self) -> bool {
119        self.0 & 0x01 != 0
120    }
121    /// HEADERS: priority fields present.
122    #[must_use]
123    pub fn priority(self) -> bool {
124        self.0 & 0x20 != 0
125    }
126
127    /// Wraps a raw flags byte.
128    #[must_use]
129    pub fn from_u8(b: u8) -> Self {
130        Self(b)
131    }
132
133    /// Raw flags byte.
134    #[must_use]
135    pub fn as_u8(self) -> u8 {
136        self.0
137    }
138}
139
140/// Parsed frame header.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub struct FrameHeader {
143    /// Payload length (0..=16_777_215).
144    pub length: u32,
145    /// Frame type.
146    pub kind: FrameKind,
147    /// Frame flags.
148    pub flags: FrameFlags,
149    /// Stream identifier with the reserved bit masked off.
150    pub stream_id: u32,
151}
152
153/// Frame parse errors.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum FrameError {
156    /// Fewer than 9 bytes / payload shorter than the declared length.
157    Truncated,
158    /// Stream id 0 used where prohibited (DATA/HEADERS/CONTINUATION/
159    /// RST_STREAM/PUSH_PROMISE) or non-zero where prohibited
160    /// (SETTINGS/PING/GOAWAY).
161    InvalidStreamId,
162    /// Payload length invalid for the frame type (e.g. WINDOW_UPDATE
163    /// != 4, RST_STREAM != 4, PING != 8).
164    InvalidPayloadLength,
165    /// Pad length >= payload length (DATA/HEADERS).
166    InvalidPadding,
167    /// SETTINGS frame with a payload not a multiple of 6, or ACK with
168    /// a payload.
169    InvalidSettings,
170    /// WINDOW_UPDATE with an increment of 0.
171    InvalidWindowIncrement,
172}
173
174/// Parses a 9-byte frame header from `data` (which may be longer; only
175/// the first 9 bytes are read). Returns `Err(Truncated)` if fewer than
176/// 9 bytes.
177/// Parses a 9-byte frame header.
178///
179/// # Errors
180/// [`FrameError::Truncated`] when fewer than 9 bytes are provided.
181pub fn parse_header(data: &[u8]) -> Result<FrameHeader, FrameError> {
182    let hdr = data.get(..FRAME_HEADER_LEN).ok_or(FrameError::Truncated)?;
183    let length = u32::from_be_bytes([0, hdr[0], hdr[1], hdr[2]]);
184    let kind = FrameKind::from_u8(hdr[3]);
185    let flags = FrameFlags::from_u8(hdr[4]);
186    // Reserved bit masked off (RFC 9113 §4.1).
187    let stream_id = u32::from_be_bytes([hdr[5] & 0x7f, hdr[6], hdr[7], hdr[8]]);
188    Ok(FrameHeader {
189        length,
190        kind,
191        flags,
192        stream_id,
193    })
194}
195
196/// Serializes a frame header.
197pub fn write_header(
198    out: &mut Vec<u8>,
199    length: u32,
200    kind: FrameKind,
201    flags: FrameFlags,
202    stream_id: u32,
203) {
204    debug_assert!(length <= MAX_ALLOWED_FRAME_SIZE);
205    debug_assert!(stream_id <= 0x7fff_ffff);
206    let b = length.to_be_bytes();
207    out.extend_from_slice(&b[1..4]);
208    out.push(kind.as_u8());
209    out.push(flags.as_u8());
210    out.extend_from_slice(&stream_id.to_be_bytes());
211}
212
213/// Structural validation + payload split for a received frame. `data`
214/// is the frame payload exactly (`header.length` bytes).
215///
216/// Returns pad-length / payload bounds per RFC §6: for padded frames,
217/// `pad` is the first byte and the content is the middle slice; the
218/// trailing `pad` bytes are dropped.
219/// Structural validation + payload split for a received frame.
220///
221/// # Errors
222/// [`FrameError::Truncated`] (payload shorter than declared),
223/// [`FrameError::InvalidPayloadLength`], [`FrameError::InvalidSettings`],
224/// [`FrameError::InvalidPadding`], or
225/// [`FrameError::InvalidWindowIncrement`] per RFC 9113 §6 rules.
226pub fn validate_payload(
227    header: &FrameHeader,
228    data: &[u8],
229    peer_max_frame: u32,
230) -> Result<PayloadSplit, FrameError> {
231    if data.len() as u32 != header.length {
232        return Err(FrameError::Truncated);
233    }
234    // Frame-size ceiling: ours is what WE accept; the spec cap is hard.
235    if header.length > peer_max_frame.max(MAX_ALLOWED_FRAME_SIZE) {
236        return Err(FrameError::InvalidPayloadLength);
237    }
238
239    let padded = match header.kind {
240        FrameKind::Data => header.flags.padded(),
241        FrameKind::Headers => header.flags.padded(),
242        FrameKind::PushPromise => header.flags.padded(),
243        _ => false,
244    };
245    if padded {
246        let Some(&pad_len) = data.first() else {
247            return Err(FrameError::Truncated);
248        };
249        // pad_len must be < total payload (the pad byte itself counts).
250        let content_len = (data.len() as u32)
251            .checked_sub(1 + u32::from(pad_len))
252            .ok_or(FrameError::InvalidPadding)?;
253        return Ok(PayloadSplit {
254            pad_len: Some(pad_len),
255            content_start: 1,
256            content_end: 1 + content_len as usize,
257        });
258    }
259
260    match header.kind {
261        FrameKind::Data | FrameKind::Headers | FrameKind::Continuation => {}
262        FrameKind::RstStream => {
263            if header.length != 4 {
264                return Err(FrameError::InvalidPayloadLength);
265            }
266        }
267        FrameKind::Settings => {
268            if header.flags.ack() {
269                if header.length != 0 {
270                    return Err(FrameError::InvalidSettings);
271                }
272            } else if header.length % 6 != 0 {
273                return Err(FrameError::InvalidSettings);
274            }
275        }
276        FrameKind::Ping => {
277            if header.length != 8 {
278                return Err(FrameError::InvalidPayloadLength);
279            }
280        }
281        FrameKind::GoAway => {
282            if header.length < 8 {
283                return Err(FrameError::InvalidPayloadLength);
284            }
285        }
286        FrameKind::WindowUpdate => {
287            if header.length != 4 {
288                return Err(FrameError::InvalidPayloadLength);
289            }
290            if data.len() == 4 {
291                let inc = u32::from_be_bytes([data[0] & 0x7f, data[1], data[2], data[3]]);
292                if inc == 0 {
293                    return Err(FrameError::InvalidWindowIncrement);
294                }
295            }
296        }
297        FrameKind::Priority => {
298            if header.length != 5 {
299                return Err(FrameError::InvalidPayloadLength);
300            }
301        }
302        FrameKind::PushPromise | FrameKind::Unknown(_) => {}
303    }
304
305    Ok(PayloadSplit {
306        pad_len: None,
307        content_start: 0,
308        content_end: data.len(),
309    })
310}
311
312/// Bounds of the content region within the payload (padding removed).
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub struct PayloadSplit {
315    /// Pad length byte value (`None` = unpadded frame).
316    pub pad_len: Option<u8>,
317    /// Content start offset within the payload.
318    pub content_start: usize,
319    /// Content end offset (exclusive) within the payload.
320    pub content_end: usize,
321}
322
323/// SETTINGS id/value pairs (RFC 9113 §6.5.2).
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum Setting {
326    /// `SETTINGS_HEADER_TABLE_SIZE` (0x1).
327    HeaderTableSize(u32),
328    /// `SETTINGS_ENABLE_PUSH` (0x2). Raw value — the setting is
329    /// only valid as 0 or 1; receivers must PROTOCOL_ERROR otherwise.
330    EnablePush(u32),
331    /// `SETTINGS_MAX_CONCURRENT_STREAMS` (0x3).
332    MaxConcurrentStreams(u32),
333    /// `SETTINGS_INITIAL_WINDOW_SIZE` (0x4).
334    InitialWindowSize(u32),
335    /// `SETTINGS_MAX_FRAME_SIZE` (0x5).
336    MaxFrameSize(u32),
337    /// `SETTINGS_MAX_HEADER_LIST_SIZE` (0x6).
338    MaxHeaderListSize(u32),
339    /// Unknown/extension setting — must be ignored.
340    Unknown(u16, u32),
341}
342
343/// Parses SETTINGS payload into id/value pairs (6 bytes each).
344/// Parses SETTINGS payload into id/value pairs (6 bytes each).
345///
346/// # Errors
347/// [`FrameError::InvalidSettings`] when the payload is not a multiple
348/// of 6.
349pub fn parse_settings(data: &[u8]) -> Result<Vec<Setting>, FrameError> {
350    if data.len() % 6 != 0 {
351        return Err(FrameError::InvalidSettings);
352    }
353    let mut out = Vec::with_capacity(data.len() / 6);
354    for pair in data.chunks_exact(6) {
355        let id = u16::from_be_bytes([pair[0], pair[1]]);
356        let value = u32::from_be_bytes([pair[2], pair[3], pair[4], pair[5]]);
357        out.push(match id {
358            0x1 => Setting::HeaderTableSize(value),
359            0x2 => Setting::EnablePush(value),
360            0x3 => Setting::MaxConcurrentStreams(value),
361            0x4 => Setting::InitialWindowSize(value),
362            0x5 => Setting::MaxFrameSize(value),
363            0x6 => Setting::MaxHeaderListSize(value),
364            other => Setting::Unknown(other, value),
365        });
366    }
367    Ok(out)
368}
369
370/// Serializes one SETTINGS id/value pair.
371pub fn write_setting(out: &mut Vec<u8>, id: u16, value: u32) {
372    out.extend_from_slice(&id.to_be_bytes());
373    out.extend_from_slice(&value.to_be_bytes());
374}
375
376/// Parses a WINDOW_UPDATE payload: reserved bit + 31-bit increment.
377/// (The stream id lives in the frame header, not the payload.)
378///
379/// # Errors
380/// [`FrameError::InvalidPayloadLength`] when not exactly 4 bytes.
381pub fn parse_window_update(data: &[u8]) -> Result<u32, FrameError> {
382    if data.len() != 4 {
383        return Err(FrameError::InvalidPayloadLength);
384    }
385    Ok(u32::from_be_bytes([
386        data[0] & 0x7f,
387        data[1],
388        data[2],
389        data[3],
390    ]))
391}
392
393/// Parses a RST_STREAM error code.
394///
395/// # Errors
396/// [`FrameError::InvalidPayloadLength`] when not exactly 4 bytes.
397pub fn parse_rst_stream(data: &[u8]) -> Result<u32, FrameError> {
398    if data.len() != 4 {
399        return Err(FrameError::InvalidPayloadLength);
400    }
401    Ok(u32::from_be_bytes([data[0], data[1], data[2], data[3]]))
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    fn header_bytes(length: u32, kind: FrameKind, flags: FrameFlags, stream_id: u32) -> Vec<u8> {
409        let mut out = Vec::with_capacity(FRAME_HEADER_LEN);
410        write_header(&mut out, length, kind, flags, stream_id);
411        out
412    }
413
414    #[test]
415    fn header_roundtrip() {
416        let raw = header_bytes(
417            0x000102,
418            FrameKind::Headers,
419            FrameFlags::from_u8(0xff),
420            0x7fff_ffff,
421        );
422        let hdr = parse_header(&raw).expect("parse");
423        assert_eq!(hdr.length, 0x000102);
424        assert_eq!(hdr.kind, FrameKind::Headers);
425        assert_eq!(hdr.flags.as_u8(), 0xff);
426        assert_eq!(hdr.stream_id, 0x7fff_ffff, "reserved bit masked");
427    }
428
429    #[test]
430    fn header_truncated() {
431        assert_eq!(parse_header(&[0; 8]), Err(FrameError::Truncated));
432    }
433
434    #[test]
435    fn settings_roundtrip() {
436        let mut payload = Vec::new();
437        write_setting(&mut payload, 0x1, 4096);
438        write_setting(&mut payload, 0x4, 1_048_576);
439        write_setting(&mut payload, 0xff, 7); // unknown ignored
440        let raw = header_bytes(
441            payload.len() as u32,
442            FrameKind::Settings,
443            FrameFlags::EMPTY,
444            0,
445        );
446        let hdr = parse_header(&raw).expect("parse");
447        assert!(validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE).is_ok());
448        let settings = parse_settings(&payload).expect("parse settings");
449        assert_eq!(settings.len(), 3);
450        assert_eq!(settings[0], Setting::HeaderTableSize(4096));
451        assert_eq!(settings[1], Setting::InitialWindowSize(1_048_576));
452        assert_eq!(settings[2], Setting::Unknown(0xff, 7));
453    }
454
455    #[test]
456    fn settings_bad_length() {
457        let raw = header_bytes(7, FrameKind::Settings, FrameFlags::EMPTY, 0);
458        let hdr = parse_header(&raw).expect("parse");
459        assert!(matches!(
460            validate_payload(&hdr, &[0u8; 7], DEFAULT_MAX_FRAME_SIZE),
461            Err(FrameError::InvalidSettings)
462        ));
463        // ACK with payload.
464        let raw = header_bytes(6, FrameKind::Settings, FrameFlags::from_u8(0x01), 0);
465        let hdr = parse_header(&raw).expect("parse");
466        assert!(matches!(
467            validate_payload(&hdr, &[0u8; 6], DEFAULT_MAX_FRAME_SIZE),
468            Err(FrameError::InvalidSettings)
469        ));
470    }
471
472    #[test]
473    fn data_padding_split() {
474        // payload: pad_len=3, "hi", 3 pad bytes → length 6.
475        let payload = [3, b'h', b'i', 0, 0, 0];
476        let raw = header_bytes(
477            payload.len() as u32,
478            FrameKind::Data,
479            FrameFlags::from_u8(0x08),
480            1,
481        );
482        let hdr = parse_header(&raw).expect("parse");
483        let split = validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE).expect("valid");
484        assert_eq!(&payload[split.content_start..split.content_end], b"hi");
485    }
486
487    #[test]
488    fn data_pad_overflow_is_invalid() {
489        let payload = [9, b'h', b'i', 0, 0];
490        let raw = header_bytes(
491            payload.len() as u32,
492            FrameKind::Data,
493            FrameFlags::from_u8(0x08),
494            1,
495        );
496        let hdr = parse_header(&raw).expect("parse");
497        assert!(matches!(
498            validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE),
499            Err(FrameError::InvalidPadding)
500        ));
501    }
502
503    #[test]
504    fn window_update_zero_increment_invalid() {
505        let payload = [0, 0, 0, 0];
506        let raw = header_bytes(4, FrameKind::WindowUpdate, FrameFlags::EMPTY, 0);
507        let hdr = parse_header(&raw).expect("parse");
508        assert!(matches!(
509            validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE),
510            Err(FrameError::InvalidWindowIncrement)
511        ));
512    }
513
514    #[test]
515    fn window_update_stream_zero_ok() {
516        let payload = [0, 0, 0, 16];
517        let raw = header_bytes(4, FrameKind::WindowUpdate, FrameFlags::EMPTY, 0);
518        let hdr = parse_header(&raw).expect("parse");
519        assert!(validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE).is_ok());
520    }
521
522    #[test]
523    fn oversize_frame_rejected() {
524        let raw = header_bytes(
525            DEFAULT_MAX_FRAME_SIZE + 1,
526            FrameKind::Data,
527            FrameFlags::EMPTY,
528            1,
529        );
530        let hdr = parse_header(&raw).expect("parse");
531        // Declared length beyond both our peer cap and the spec hard cap:
532        // rejected structurally (the peer can never legally send this).
533        assert!(matches!(
534            validate_payload(&hdr, &[], DEFAULT_MAX_FRAME_SIZE),
535            Err(FrameError::InvalidPayloadLength) | Err(FrameError::Truncated)
536        ));
537    }
538
539    #[test]
540    fn rst_stream_requires_4_bytes() {
541        let raw = header_bytes(3, FrameKind::RstStream, FrameFlags::EMPTY, 1);
542        let hdr = parse_header(&raw).expect("parse");
543        assert!(matches!(
544            validate_payload(&hdr, &[0u8; 3], DEFAULT_MAX_FRAME_SIZE),
545            Err(FrameError::InvalidPayloadLength)
546        ));
547        let raw = header_bytes(4, FrameKind::RstStream, FrameFlags::EMPTY, 1);
548        let hdr = parse_header(&raw).expect("parse");
549        let payload = [0, 0, 0, 8]; // CANCEL
550        assert!(validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE).is_ok());
551        assert_eq!(parse_rst_stream(&payload), Ok(8));
552    }
553
554    #[test]
555    fn ping_requires_8_bytes() {
556        let raw = header_bytes(9, FrameKind::Ping, FrameFlags::EMPTY, 0);
557        let hdr = parse_header(&raw).expect("parse");
558        assert!(matches!(
559            validate_payload(&hdr, &[0u8; 9], DEFAULT_MAX_FRAME_SIZE),
560            Err(FrameError::InvalidPayloadLength)
561        ));
562    }
563
564    #[test]
565    fn unknown_frame_type_skipped() {
566        let payload = [1, 2, 3];
567        let raw = header_bytes(3, FrameKind::Unknown(0xab), FrameFlags::EMPTY, 4);
568        let hdr = parse_header(&raw).expect("parse");
569        assert!(validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE).is_ok());
570    }
571
572    #[test]
573    fn goaway_requires_at_least_8_bytes() {
574        let raw = header_bytes(4, FrameKind::GoAway, FrameFlags::EMPTY, 0);
575        let hdr = parse_header(&raw).expect("parse");
576        assert!(matches!(
577            validate_payload(&hdr, &[0u8; 4], DEFAULT_MAX_FRAME_SIZE),
578            Err(FrameError::InvalidPayloadLength)
579        ));
580        let raw = header_bytes(8, FrameKind::GoAway, FrameFlags::EMPTY, 0);
581        let hdr = parse_header(&raw).expect("parse");
582        assert!(validate_payload(&hdr, &[0u8; 8], DEFAULT_MAX_FRAME_SIZE).is_ok());
583    }
584
585    #[test]
586    fn window_update_requires_4_bytes() {
587        let raw = header_bytes(5, FrameKind::WindowUpdate, FrameFlags::EMPTY, 1);
588        let hdr = parse_header(&raw).expect("parse");
589        assert!(matches!(
590            validate_payload(&hdr, &[0u8; 5], DEFAULT_MAX_FRAME_SIZE),
591            Err(FrameError::InvalidPayloadLength)
592        ));
593    }
594
595    #[test]
596    fn priority_requires_5_bytes() {
597        let raw = header_bytes(4, FrameKind::Priority, FrameFlags::EMPTY, 1);
598        let hdr = parse_header(&raw).expect("parse");
599        assert!(matches!(
600            validate_payload(&hdr, &[0u8; 4], DEFAULT_MAX_FRAME_SIZE),
601            Err(FrameError::InvalidPayloadLength)
602        ));
603        let raw = header_bytes(5, FrameKind::Priority, FrameFlags::EMPTY, 1);
604        let hdr = parse_header(&raw).expect("parse");
605        assert!(validate_payload(&hdr, &[0u8; 5], DEFAULT_MAX_FRAME_SIZE).is_ok());
606    }
607
608    #[test]
609    fn push_promise_padding_splits_like_data() {
610        // payload: pad_len=1, one content byte, 1 pad byte → length 3.
611        let payload = [1, b'x', 0];
612        let raw = header_bytes(
613            payload.len() as u32,
614            FrameKind::PushPromise,
615            FrameFlags::from_u8(0x08),
616            3,
617        );
618        let hdr = parse_header(&raw).expect("parse");
619        let split = validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE).expect("valid");
620        assert_eq!(split.pad_len, Some(1));
621        assert_eq!(&payload[split.content_start..split.content_end], b"x");
622    }
623
624    #[test]
625    fn padded_frame_with_empty_payload_truncated() {
626        let raw = header_bytes(0, FrameKind::Data, FrameFlags::from_u8(0x08), 1);
627        let hdr = parse_header(&raw).expect("parse");
628        assert!(matches!(
629            validate_payload(&hdr, &[], DEFAULT_MAX_FRAME_SIZE),
630            Err(FrameError::Truncated)
631        ));
632    }
633
634    #[test]
635    fn spec_hard_frame_cap_rejected_even_with_matching_payload() {
636        // The length check precedes the payload match, so the payload
637        // must be materialized at the declared (over-cap) size. The
638        // header is built directly: cap+1 is not expressible in the
639        // 24-bit wire field (the cap IS that limit), so it can only
640        // arise from an in-process bug — exactly what this guards.
641        let len = MAX_ALLOWED_FRAME_SIZE + 1;
642        let hdr = FrameHeader {
643            length: len,
644            kind: FrameKind::Data,
645            flags: FrameFlags::EMPTY,
646            stream_id: 1,
647        };
648        let payload = vec![0u8; len as usize];
649        assert!(matches!(
650            validate_payload(&hdr, &payload, DEFAULT_MAX_FRAME_SIZE),
651            Err(FrameError::InvalidPayloadLength)
652        ));
653    }
654
655    #[test]
656    fn parse_settings_rejects_non_multiple_of_six() {
657        assert_eq!(parse_settings(&[0u8; 7]), Err(FrameError::InvalidSettings));
658    }
659
660    #[test]
661    fn parse_settings_maps_all_known_ids() {
662        let mut payload = Vec::new();
663        write_setting(&mut payload, 0x2, 1);
664        write_setting(&mut payload, 0x5, 16_384);
665        write_setting(&mut payload, 0x6, 4096);
666        let settings = parse_settings(&payload).expect("parse settings");
667        assert_eq!(settings[0], Setting::EnablePush(1));
668        assert_eq!(settings[1], Setting::MaxFrameSize(16_384));
669        assert_eq!(settings[2], Setting::MaxHeaderListSize(4096));
670    }
671
672    #[test]
673    fn parse_window_update_and_rst_reject_bad_lengths() {
674        assert_eq!(
675            parse_window_update(&[0u8; 3]),
676            Err(FrameError::InvalidPayloadLength)
677        );
678        assert_eq!(parse_window_update(&[0, 0, 0, 9]), Ok(9));
679        assert_eq!(
680            parse_rst_stream(&[0u8; 5]),
681            Err(FrameError::InvalidPayloadLength)
682        );
683    }
684
685    #[test]
686    fn garbage_never_panics() {
687        let mut x: u64 = 0x9e37_79b9_7f4a_7c15;
688        for len in 0..400u64 {
689            let mut data = Vec::with_capacity(len as usize);
690            for _ in 0..len {
691                x ^= x << 13;
692                x ^= x >> 7;
693                x ^= x << 17;
694                data.push(x as u8);
695            }
696            if let Ok(hdr) = parse_header(&data) {
697                let _ = validate_payload(
698                    &hdr,
699                    data.get(FRAME_HEADER_LEN..).unwrap_or(&[]),
700                    DEFAULT_MAX_FRAME_SIZE,
701                );
702                let _ = FrameKind::from_u8(hdr.kind.as_u8());
703            }
704        }
705    }
706}