Skip to main content

zerodds_http2/
settings.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! SETTINGS Frame — RFC 9113 §6.5.
5//!
6//! Spec §6.5.1: jeder Setting-Eintrag ist 6 Bytes (2 Byte Identifier
7//! + 4 Byte Value).
8
9use alloc::vec::Vec;
10
11use crate::error::Http2Error;
12
13/// Setting-Identifier (RFC 9113 §6.5.2).
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[repr(u16)]
16pub enum SettingId {
17    /// `SETTINGS_HEADER_TABLE_SIZE` (0x1).
18    HeaderTableSize = 0x1,
19    /// `SETTINGS_ENABLE_PUSH` (0x2).
20    EnablePush = 0x2,
21    /// `SETTINGS_MAX_CONCURRENT_STREAMS` (0x3).
22    MaxConcurrentStreams = 0x3,
23    /// `SETTINGS_INITIAL_WINDOW_SIZE` (0x4).
24    InitialWindowSize = 0x4,
25    /// `SETTINGS_MAX_FRAME_SIZE` (0x5).
26    MaxFrameSize = 0x5,
27    /// `SETTINGS_MAX_HEADER_LIST_SIZE` (0x6).
28    MaxHeaderListSize = 0x6,
29}
30
31impl SettingId {
32    /// `u16 -> SettingId`. Spec §6.5.2: Empfaenger sollen unbekannte
33    /// IDs ignorieren, aber wir liefern hier `None`, der Caller
34    /// entscheidet.
35    #[must_use]
36    pub fn from_u16(v: u16) -> Option<Self> {
37        match v {
38            0x1 => Some(Self::HeaderTableSize),
39            0x2 => Some(Self::EnablePush),
40            0x3 => Some(Self::MaxConcurrentStreams),
41            0x4 => Some(Self::InitialWindowSize),
42            0x5 => Some(Self::MaxFrameSize),
43            0x6 => Some(Self::MaxHeaderListSize),
44            _ => None,
45        }
46    }
47}
48
49/// Ein einzelner Setting-Eintrag.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Setting {
52    /// Identifier.
53    pub id: SettingId,
54    /// Value (32-bit).
55    pub value: u32,
56}
57
58/// Settings-Map mit Defaults laut Spec §6.5.2.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Settings {
61    /// `HEADER_TABLE_SIZE` (default 4096).
62    pub header_table_size: u32,
63    /// `ENABLE_PUSH` (default 1).
64    pub enable_push: u32,
65    /// `MAX_CONCURRENT_STREAMS` (default unlimited, repraesentiert als
66    /// `u32::MAX`).
67    pub max_concurrent_streams: u32,
68    /// `INITIAL_WINDOW_SIZE` (default 65_535).
69    pub initial_window_size: u32,
70    /// `MAX_FRAME_SIZE` (default 16_384).
71    pub max_frame_size: u32,
72    /// `MAX_HEADER_LIST_SIZE` (default unlimited, `u32::MAX`).
73    pub max_header_list_size: u32,
74}
75
76impl Default for Settings {
77    fn default() -> Self {
78        // Spec §6.5.2 — initial values.
79        Self {
80            header_table_size: 4096,
81            enable_push: 1,
82            max_concurrent_streams: u32::MAX,
83            initial_window_size: 65_535,
84            max_frame_size: 16_384,
85            max_header_list_size: u32::MAX,
86        }
87    }
88}
89
90impl Settings {
91    /// Wendet einen Setting-Eintrag an.
92    ///
93    /// # Errors
94    /// `Protocol(FlowControlError)` wenn `INITIAL_WINDOW_SIZE`
95    /// groesser als 2^31-1 (Spec §6.5.2). `Protocol(ProtocolError)`
96    /// bei `ENABLE_PUSH` != 0/1 oder `MAX_FRAME_SIZE` ausserhalb
97    /// [16384, 16777215].
98    pub fn apply(&mut self, s: Setting) -> Result<(), Http2Error> {
99        use crate::error::ErrorCode;
100        match s.id {
101            SettingId::HeaderTableSize => self.header_table_size = s.value,
102            SettingId::EnablePush => {
103                if s.value > 1 {
104                    return Err(Http2Error::Protocol(ErrorCode::ProtocolError));
105                }
106                self.enable_push = s.value;
107            }
108            SettingId::MaxConcurrentStreams => self.max_concurrent_streams = s.value,
109            SettingId::InitialWindowSize => {
110                if s.value > 0x7fff_ffff {
111                    return Err(Http2Error::Protocol(ErrorCode::FlowControlError));
112                }
113                self.initial_window_size = s.value;
114            }
115            SettingId::MaxFrameSize => {
116                if !(16_384..=16_777_215).contains(&s.value) {
117                    return Err(Http2Error::Protocol(ErrorCode::ProtocolError));
118                }
119                self.max_frame_size = s.value;
120            }
121            SettingId::MaxHeaderListSize => self.max_header_list_size = s.value,
122        }
123        Ok(())
124    }
125}
126
127/// Decodiert einen SETTINGS-Frame-Payload zu einer Liste von
128/// Settings. Spec §6.5.1.
129///
130/// # Errors
131/// `Protocol(FrameSizeError)` wenn die Length nicht ein Vielfaches
132/// von 6 ist.
133pub fn decode_settings(payload: &[u8]) -> Result<Vec<Setting>, Http2Error> {
134    use crate::error::ErrorCode;
135    if payload.len() % 6 != 0 {
136        return Err(Http2Error::Protocol(ErrorCode::FrameSizeError));
137    }
138    let mut out = Vec::with_capacity(payload.len() / 6);
139    let mut i = 0;
140    while i + 6 <= payload.len() {
141        let id_u = (u16::from(payload[i]) << 8) | u16::from(payload[i + 1]);
142        let value = (u32::from(payload[i + 2]) << 24)
143            | (u32::from(payload[i + 3]) << 16)
144            | (u32::from(payload[i + 4]) << 8)
145            | u32::from(payload[i + 5]);
146        if let Some(id) = SettingId::from_u16(id_u) {
147            out.push(Setting { id, value });
148        }
149        // Spec §6.5.2: unbekannte IDs werden ignoriert.
150        i += 6;
151    }
152    Ok(out)
153}
154
155/// Encode eine Liste von Settings zu einem SETTINGS-Frame-Payload.
156#[must_use]
157pub fn encode_settings(settings: &[Setting]) -> Vec<u8> {
158    let mut out = Vec::with_capacity(settings.len() * 6);
159    for s in settings {
160        let id = s.id as u16;
161        out.push((id >> 8) as u8);
162        out.push((id & 0xff) as u8);
163        out.push(((s.value >> 24) & 0xff) as u8);
164        out.push(((s.value >> 16) & 0xff) as u8);
165        out.push(((s.value >> 8) & 0xff) as u8);
166        out.push((s.value & 0xff) as u8);
167    }
168    out
169}
170
171#[cfg(test)]
172#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn defaults_match_spec() {
178        let s = Settings::default();
179        assert_eq!(s.header_table_size, 4096);
180        assert_eq!(s.enable_push, 1);
181        assert_eq!(s.initial_window_size, 65_535);
182        assert_eq!(s.max_frame_size, 16_384);
183    }
184
185    #[test]
186    fn apply_valid_settings() {
187        let mut s = Settings::default();
188        s.apply(Setting {
189            id: SettingId::MaxFrameSize,
190            value: 32_768,
191        })
192        .unwrap();
193        assert_eq!(s.max_frame_size, 32_768);
194    }
195
196    #[test]
197    fn invalid_enable_push_rejected() {
198        let mut s = Settings::default();
199        assert!(
200            s.apply(Setting {
201                id: SettingId::EnablePush,
202                value: 2,
203            })
204            .is_err()
205        );
206    }
207
208    #[test]
209    fn invalid_initial_window_size_rejected() {
210        let mut s = Settings::default();
211        assert!(
212            s.apply(Setting {
213                id: SettingId::InitialWindowSize,
214                value: 0x8000_0000,
215            })
216            .is_err()
217        );
218    }
219
220    #[test]
221    fn invalid_max_frame_size_rejected() {
222        let mut s = Settings::default();
223        assert!(
224            s.apply(Setting {
225                id: SettingId::MaxFrameSize,
226                value: 1,
227            })
228            .is_err()
229        );
230        assert!(
231            s.apply(Setting {
232                id: SettingId::MaxFrameSize,
233                value: 16_777_216,
234            })
235            .is_err()
236        );
237    }
238
239    #[test]
240    fn round_trip_encode_decode() {
241        let settings = alloc::vec![
242            Setting {
243                id: SettingId::MaxConcurrentStreams,
244                value: 100
245            },
246            Setting {
247                id: SettingId::InitialWindowSize,
248                value: 1_048_576,
249            },
250        ];
251        let payload = encode_settings(&settings);
252        let decoded = decode_settings(&payload).unwrap();
253        assert_eq!(decoded, settings);
254    }
255
256    #[test]
257    fn unknown_setting_id_ignored() {
258        let payload = alloc::vec![0x00, 0xff, 0x00, 0x00, 0x00, 0x01];
259        let decoded = decode_settings(&payload).unwrap();
260        assert!(decoded.is_empty());
261    }
262
263    #[test]
264    fn odd_length_payload_rejected() {
265        let payload = alloc::vec![0; 5];
266        assert!(decode_settings(&payload).is_err());
267    }
268}