Skip to main content

msquic_h3/
config.rs

1//! Public, validated memory-budget configuration for the adapter.
2//!
3//! [`H3Config`] carries the per-stream send-size ceiling, the per-stream
4//! receive-byte budget, and the per-stream receive-unit (allocation/message)
5//! cap. Its fields are **private** and can only be produced via [`Default`] or
6//! the validated [`H3ConfigBuilder`], so no invalid configuration is ever
7//! constructible by a literal or a field mutation. The defaults preserve the
8//! adapter's historical send ceiling ([`MAX_ADAPTER_SEND`]) and receive-**byte**
9//! budget ([`MAX_RECV_BUFFER`]) while **adding** a protective receive-unit cap
10//! ([`DEFAULT_MAX_RECV_UNITS`], 16384). A default-configured adapter therefore
11//! keeps the prior send and receive-byte behavior, but the new unit cap does
12//! bound highly fragmented tiny-frame input that was previously unbounded — that
13//! is the point of the cap.
14//!
15//! The send side is bounded by `max_send_bytes × concurrent streams` (there is
16//! no aggregate/per-connection send budget — the h3 trait contract calls the
17//! synchronous `send_data` before the only async readiness hook, so a send
18//! cannot be deferred as backpressure); applications that need to bound send
19//! memory should also cap their concurrent stream count.
20
21use std::fmt;
22
23use crate::error::MAX_ADAPTER_SEND;
24use crate::stream::MAX_RECV_BUFFER;
25
26/// Default per-stream receive-unit (buffered allocation/message) cap.
27///
28/// Bounds the number of queued receive indications per stream, independently of
29/// their byte total, so a flood of tiny frames cannot amplify into unbounded
30/// per-stream allocations. The byte budget alone does not bound this.
31pub(crate) const DEFAULT_MAX_RECV_UNITS: usize = 16384;
32
33/// Validated, `Copy` memory-budget configuration threaded from connection /
34/// listener construction into every per-connection and per-stream state.
35///
36/// Fields are private; construct via [`H3Config::default`] or
37/// [`H3Config::builder`]. All accessors are read-only, so a value cannot be
38/// mutated after construction.
39///
40/// The receive side is enforced per stream as real backpressure (both a byte
41/// budget and a unit-count budget). The **send** side is only bounded by
42/// `max_send_bytes` per outstanding send; there is no aggregate send cap, so
43/// per-connection send memory scales as `max_send_bytes × concurrent
44/// in-flight streams`. The adapter cannot enforce an aggregate send budget as
45/// backpressure because the h3 trait contract calls the synchronous
46/// `send_data` (which copies and takes ownership) before its only async
47/// readiness hook — applications that need to bound send memory should choose
48/// `max_send_bytes` and cap their concurrent stream count.
49#[derive(Copy, Clone, Debug, PartialEq, Eq)]
50pub struct H3Config {
51    /// Maximum single `send_data` payload accepted before rejecting with
52    /// `OversizedSend`. Must be non-zero and below `u32::MAX`.
53    max_send_bytes: u64,
54    /// Maximum undrained, adapter-buffered received bytes per stream before the
55    /// receive callback pends. Must be non-zero.
56    max_recv_bytes: usize,
57    /// Maximum buffered receive units (allocations/messages) per stream. Must be
58    /// non-zero.
59    max_recv_units: usize,
60}
61
62impl Default for H3Config {
63    fn default() -> Self {
64        Self {
65            max_send_bytes: MAX_ADAPTER_SEND,
66            max_recv_bytes: MAX_RECV_BUFFER,
67            max_recv_units: DEFAULT_MAX_RECV_UNITS,
68        }
69    }
70}
71
72impl H3Config {
73    /// Start building a validated [`H3Config`].
74    pub fn builder() -> H3ConfigBuilder {
75        H3ConfigBuilder::default()
76    }
77
78    /// Construct a validated [`H3Config`] directly from its three caps.
79    ///
80    /// Returns a [`ConfigError`] if any value is out of range (see
81    /// [`H3ConfigBuilder::build`]).
82    pub fn try_new(
83        max_send_bytes: u64,
84        max_recv_bytes: usize,
85        max_recv_units: usize,
86    ) -> Result<Self, ConfigError> {
87        validate(max_send_bytes, max_recv_bytes, max_recv_units)
88    }
89
90    /// Maximum single `send_data` payload accepted before rejecting with
91    /// `OversizedSend`.
92    pub fn max_send_bytes(&self) -> u64 {
93        self.max_send_bytes
94    }
95
96    /// Maximum undrained, adapter-buffered received bytes per stream before the
97    /// receive callback pends.
98    pub fn max_recv_bytes(&self) -> usize {
99        self.max_recv_bytes
100    }
101
102    /// Maximum buffered receive units (allocations/messages) per stream.
103    pub fn max_recv_units(&self) -> usize {
104        self.max_recv_units
105    }
106}
107
108/// Chainable builder for a validated [`H3Config`].
109///
110/// Each `with_*` setter overrides one cap; unset caps keep their default. The
111/// terminal [`build`](H3ConfigBuilder::build) validates all three and yields a
112/// [`ConfigError`] on the first out-of-range value.
113#[derive(Copy, Clone, Debug)]
114pub struct H3ConfigBuilder {
115    max_send_bytes: u64,
116    max_recv_bytes: usize,
117    max_recv_units: usize,
118}
119
120impl Default for H3ConfigBuilder {
121    fn default() -> Self {
122        let d = H3Config::default();
123        Self {
124            max_send_bytes: d.max_send_bytes,
125            max_recv_bytes: d.max_recv_bytes,
126            max_recv_units: d.max_recv_units,
127        }
128    }
129}
130
131impl H3ConfigBuilder {
132    /// Override the per-send-size ceiling.
133    pub fn with_max_send_bytes(mut self, max_send_bytes: u64) -> Self {
134        self.max_send_bytes = max_send_bytes;
135        self
136    }
137
138    /// Override the per-stream receive-byte budget.
139    pub fn with_max_recv_bytes(mut self, max_recv_bytes: usize) -> Self {
140        self.max_recv_bytes = max_recv_bytes;
141        self
142    }
143
144    /// Override the per-stream receive-unit cap.
145    pub fn with_max_recv_units(mut self, max_recv_units: usize) -> Self {
146        self.max_recv_units = max_recv_units;
147        self
148    }
149
150    /// Validate the accumulated caps and produce an [`H3Config`].
151    ///
152    /// Rejects `max_send_bytes == 0` or `>= u32::MAX`
153    /// ([`ConfigError::SendSize`]), `max_recv_bytes == 0`
154    /// ([`ConfigError::RecvBytes`]), and `max_recv_units == 0`
155    /// ([`ConfigError::RecvUnits`]).
156    pub fn build(self) -> Result<H3Config, ConfigError> {
157        validate(
158            self.max_send_bytes,
159            self.max_recv_bytes,
160            self.max_recv_units,
161        )
162    }
163}
164
165/// Shared validation for [`H3ConfigBuilder::build`] and [`H3Config::try_new`].
166fn validate(
167    max_send_bytes: u64,
168    max_recv_bytes: usize,
169    max_recv_units: usize,
170) -> Result<H3Config, ConfigError> {
171    if max_send_bytes == 0 || max_send_bytes >= u32::MAX as u64 {
172        return Err(ConfigError::SendSize);
173    }
174    if max_recv_bytes == 0 {
175        return Err(ConfigError::RecvBytes);
176    }
177    if max_recv_units == 0 {
178        return Err(ConfigError::RecvUnits);
179    }
180    Ok(H3Config {
181        max_send_bytes,
182        max_recv_bytes,
183        max_recv_units,
184    })
185}
186
187/// A rejected [`H3Config`] cap value.
188#[derive(Copy, Clone, Debug, PartialEq, Eq)]
189pub enum ConfigError {
190    /// `max_send_bytes` was zero or `>= u32::MAX` (the native send-length
191    /// ceiling, above which a `BufferRef` length would truncate).
192    SendSize,
193    /// `max_recv_bytes` was zero (a stream could never buffer any receive data).
194    RecvBytes,
195    /// `max_recv_units` was zero (a stream could never buffer any receive unit).
196    RecvUnits,
197}
198
199impl fmt::Display for ConfigError {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match self {
202            ConfigError::SendSize => write!(
203                f,
204                "max_send_bytes must be non-zero and less than u32::MAX ({})",
205                u32::MAX
206            ),
207            ConfigError::RecvBytes => write!(f, "max_recv_bytes must be non-zero"),
208            ConfigError::RecvUnits => write!(f, "max_recv_units must be non-zero"),
209        }
210    }
211}
212
213impl std::error::Error for ConfigError {}
214
215#[cfg(test)]
216mod tests {
217    use super::{ConfigError, DEFAULT_MAX_RECV_UNITS, H3Config};
218    use crate::error::MAX_ADAPTER_SEND;
219    use crate::stream::MAX_RECV_BUFFER;
220
221    #[test]
222    fn defaults_equal_the_historical_constants() {
223        let c = H3Config::default();
224        assert_eq!(c.max_send_bytes(), MAX_ADAPTER_SEND);
225        assert_eq!(c.max_recv_bytes(), MAX_RECV_BUFFER);
226        assert_eq!(c.max_recv_units(), DEFAULT_MAX_RECV_UNITS);
227        assert_eq!(c.max_recv_units(), 16384);
228    }
229
230    #[test]
231    fn builder_defaults_match_default() {
232        let built = H3Config::builder().build().unwrap();
233        let def = H3Config::default();
234        assert_eq!(built.max_send_bytes(), def.max_send_bytes());
235        assert_eq!(built.max_recv_bytes(), def.max_recv_bytes());
236        assert_eq!(built.max_recv_units(), def.max_recv_units());
237    }
238
239    #[test]
240    fn valid_builder_round_trips_accessors() {
241        let c = H3Config::builder()
242            .with_max_send_bytes(4096)
243            .with_max_recv_bytes(8192)
244            .with_max_recv_units(7)
245            .build()
246            .unwrap();
247        assert_eq!(c.max_send_bytes(), 4096);
248        assert_eq!(c.max_recv_bytes(), 8192);
249        assert_eq!(c.max_recv_units(), 7);
250    }
251
252    #[test]
253    fn try_new_round_trips() {
254        let c = H3Config::try_new(4096, 8192, 7).unwrap();
255        assert_eq!(c.max_send_bytes(), 4096);
256        assert_eq!(c.max_recv_bytes(), 8192);
257        assert_eq!(c.max_recv_units(), 7);
258    }
259
260    #[test]
261    fn zero_send_size_is_rejected() {
262        assert_eq!(
263            H3Config::builder().with_max_send_bytes(0).build(),
264            Err(ConfigError::SendSize)
265        );
266    }
267
268    #[test]
269    fn send_size_at_or_above_u32_max_is_rejected() {
270        assert_eq!(
271            H3Config::builder()
272                .with_max_send_bytes(u32::MAX as u64)
273                .build(),
274            Err(ConfigError::SendSize)
275        );
276        assert_eq!(
277            H3Config::builder()
278                .with_max_send_bytes(u32::MAX as u64 + 1)
279                .build(),
280            Err(ConfigError::SendSize)
281        );
282        // One below u32::MAX is the largest accepted ceiling.
283        assert!(
284            H3Config::builder()
285                .with_max_send_bytes(u32::MAX as u64 - 1)
286                .build()
287                .is_ok()
288        );
289    }
290
291    #[test]
292    fn zero_recv_bytes_is_rejected() {
293        assert_eq!(
294            H3Config::builder().with_max_recv_bytes(0).build(),
295            Err(ConfigError::RecvBytes)
296        );
297    }
298
299    #[test]
300    fn zero_recv_units_is_rejected() {
301        assert_eq!(
302            H3Config::builder().with_max_recv_units(0).build(),
303            Err(ConfigError::RecvUnits)
304        );
305    }
306
307    #[test]
308    fn config_error_displays_a_message_per_variant() {
309        assert!(!ConfigError::SendSize.to_string().is_empty());
310        assert!(!ConfigError::RecvBytes.to_string().is_empty());
311        assert!(!ConfigError::RecvUnits.to_string().is_empty());
312    }
313}