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}