oneyaml_core/limits.rs
1//! Resource limits.
2
3/// Resource limits: secondary safeguards against resource exhaustion.
4///
5/// Because parsing is iterative and O(n), no limit is needed for
6/// *safety*; these bound memory and time on hostile input. Every limit that
7/// is exceeded yields a limit diagnostic (`Y2xxx`) whose help names the
8/// setting to raise, e.g. `raise Limits::max_depth (currently 128)`.
9///
10/// Three profiles:
11///
12/// | Limit | [`Limits::default()`] | [`Limits::untrusted()`] | [`Limits::unlimited()`] |
13/// |---|---|---|---|
14/// | `max_depth` | 1,024 | 128 | none |
15/// | `alias_budget` | max(10 × own nodes, 1,000,000) | max(2 × own nodes, 100,000) | none |
16/// | `alias_byte_budget` | max(10 × input bytes, 64 MiB) | max(2 × input bytes, 8 MiB) | none |
17/// | `max_document_size` | 256 MiB | 16 MiB | none |
18/// | `max_scalar_len` | 64 MiB | 1 MiB | none |
19/// | `max_documents` | none | 1,000 | none |
20/// | `max_typed_depth` | 128 | 64 | none |
21/// | `max_diagnostics` | 100,000 | 1,000 | none |
22///
23/// The alias budgets bound what `Value` and serde reads materialise by
24/// expanding aliases (nodes, and bytes beyond the input; see
25/// [`Limits::alias_byte_budget`]). They are counted per document and
26/// incrementally from the document's start, so a read of one node never
27/// refuses what the whole document's read accepts. Each document of a
28/// stream has its own budgets: reading a whole stream expands at most the
29/// sum of its documents' budgets, so at most `max_documents` × the largest
30/// one (with `untrusted()` and small documents: 1,000 × 100,000 nodes and
31/// 1,000 × 8 MiB).
32/// An alias whose whole expansion would exceed a budget is refused before
33/// any of it is replayed, so a refused read costs about the input it read.
34///
35/// A numeric limit of `usize::MAX` means "no limit". Use
36/// [`Limits::untrusted()`] for servers, webhooks, and APIs that parse input
37/// from untrusted sources.
38///
39/// **`Limits::unlimited()`** removes every bound, and it is the only way to
40/// disable the alias budgets (nodes and bytes; `alias_budget(usize::MAX)`
41/// and `alias_byte_budget(usize::MAX)` make them unreachable instead). Use
42/// it only for input you trust completely.
43///
44/// Compatibility presets never change limits: safety is not a compat
45/// behaviour.
46///
47/// # Examples
48///
49/// ```
50/// use oneyaml_core::Limits;
51///
52/// // A service reading small uploads: the untrusted profile, tightened.
53/// let l = Limits::untrusted()
54/// .max_depth(32)
55/// .max_typed_depth(32)
56/// .max_document_size(64 * 1024)
57/// .max_scalar_len(4 * 1024)
58/// .max_documents(1)
59/// .max_diagnostics(10)
60/// .alias_budget(1_000)
61/// .alias_byte_budget(256 * 1024);
62/// assert_eq!(l.get_max_depth(), 32);
63/// assert_eq!(l.get_max_typed_depth(), 32);
64/// assert_eq!(l.get_max_document_size(), 64 * 1024);
65/// assert_eq!(l.get_max_scalar_len(), 4 * 1024);
66/// assert_eq!(l.get_max_documents(), 1);
67/// assert_eq!(l.get_max_diagnostics(), 10);
68/// // A fixed budget ignores the document's size ...
69/// assert_eq!(l.get_alias_budget(1_000_000), Some(1_000));
70/// assert_eq!(l.get_alias_byte_budget(1 << 30), Some(256 * 1024));
71/// // ... a ratio scales with it, above a floor.
72/// let r = l.alias_budget_ratio(3, 100).alias_byte_budget_ratio(2, 4096);
73/// assert_eq!(r.get_alias_budget(10), Some(100));
74/// assert_eq!(r.get_alias_budget(1_000), Some(3_000));
75/// assert_eq!(r.get_alias_byte_budget(10_000), Some(20_000));
76/// // `unlimited()` is the only profile without alias budgets.
77/// assert_eq!(Limits::unlimited().get_alias_budget(1), None);
78/// assert_eq!(Limits::unlimited().get_alias_byte_budget(1), None);
79/// ```
80// Value type, private fields, chainable `const fn` setters.
81#[derive(Clone, Debug, PartialEq, Eq, Hash)]
82pub struct Limits {
83 max_depth: usize,
84 alias_factor: usize,
85 alias_floor: usize,
86 alias_enabled: bool,
87 alias_bytes_factor: usize,
88 alias_bytes_floor: usize,
89 alias_bytes_enabled: bool,
90 max_document_size: usize,
91 max_scalar_len: usize,
92 max_documents: usize,
93 max_typed_depth: usize,
94 max_diagnostics: usize,
95}
96
97const MIB: usize = 1024 * 1024;
98
99impl Limits {
100 /// The default profile. Same as [`Limits::default()`], usable in `const`.
101 #[must_use]
102 pub const fn new() -> Self {
103 Self {
104 max_depth: 1024,
105 alias_factor: 10,
106 alias_floor: 1_000_000,
107 alias_enabled: true,
108 alias_bytes_factor: 10,
109 alias_bytes_floor: 64 * MIB,
110 alias_bytes_enabled: true,
111 max_document_size: 256 * MIB,
112 max_scalar_len: 64 * MIB,
113 max_documents: usize::MAX,
114 max_typed_depth: 128,
115 max_diagnostics: 100_000,
116 }
117 }
118
119 /// The profile for untrusted input.
120 #[must_use]
121 pub const fn untrusted() -> Self {
122 Self {
123 max_depth: 128,
124 alias_factor: 2,
125 alias_floor: 100_000,
126 alias_enabled: true,
127 alias_bytes_factor: 2,
128 alias_bytes_floor: 8 * MIB,
129 alias_bytes_enabled: true,
130 max_document_size: 16 * MIB,
131 max_scalar_len: MIB,
132 max_documents: 1000,
133 max_typed_depth: 64,
134 max_diagnostics: 1000,
135 }
136 }
137
138 /// No limits at all, including no alias budget. Only for trusted input.
139 #[must_use]
140 pub const fn unlimited() -> Self {
141 Self {
142 max_depth: usize::MAX,
143 alias_factor: 0,
144 alias_floor: usize::MAX,
145 alias_enabled: false,
146 alias_bytes_factor: 0,
147 alias_bytes_floor: usize::MAX,
148 alias_bytes_enabled: false,
149 max_document_size: usize::MAX,
150 max_scalar_len: usize::MAX,
151 max_documents: usize::MAX,
152 max_typed_depth: usize::MAX,
153 max_diagnostics: usize::MAX,
154 }
155 }
156
157 /// Maximum nesting depth of collections, counting block and flow
158 /// collections together; a top-level collection has depth 1.
159 ///
160 /// Consumed by the parser now; the tree builder, `Value`/serde, and the
161 /// emitter will enforce it too.
162 #[must_use]
163 pub const fn max_depth(mut self, depth: usize) -> Self {
164 self.max_depth = depth;
165 self
166 }
167
168 /// A fixed alias budget: at most `nodes` nodes may be produced by alias
169 /// expansion (including merge keys through aliases).
170 ///
171 /// Applies to `Value` and serde only, never to `Document`.
172 #[must_use]
173 pub const fn alias_budget(mut self, nodes: usize) -> Self {
174 self.alias_factor = 0;
175 self.alias_floor = nodes;
176 self.alias_enabled = true;
177 self
178 }
179
180 /// A ratio alias budget: `max(factor × own nodes, floor)`, where "own
181 /// nodes" is the number of nodes the document itself contains.
182 ///
183 /// Applies to `Value` and serde only, never to `Document`.
184 #[must_use]
185 pub const fn alias_budget_ratio(mut self, factor: usize, floor: usize) -> Self {
186 self.alias_factor = factor;
187 self.alias_floor = floor;
188 self.alias_enabled = true;
189 self
190 }
191
192 /// A fixed alias **byte** budget: at most `bytes` bytes may
193 /// be materialised by alias expansion beyond the input.
194 ///
195 /// Counted per document, incrementally, by `Value` and serde reads only
196 /// (never by `Document`, which expands nothing):
197 ///
198 /// * every scalar value, key, and tag that alias expansion copies
199 /// (merge keys through aliases included), in full;
200 /// * each tag use's `%TAG` prefix, which a tag shares rather than
201 /// copies but which its logical text (and so any output of it)
202 /// repeats;
203 /// * the text of a `%TAG` tag joined into an enum variant name that
204 /// names no variant of the target (`#[serde(other)]`, hand-written
205 /// visitors).
206 ///
207 /// Exceeding it is `Y2009` (`code::ALIAS_BYTE_BUDGET_EXCEEDED`).
208 /// Independent of [`Limits::alias_budget`], which counts nodes.
209 #[must_use]
210 pub const fn alias_byte_budget(mut self, bytes: usize) -> Self {
211 self.alias_bytes_factor = 0;
212 self.alias_bytes_floor = bytes;
213 self.alias_bytes_enabled = true;
214 self
215 }
216
217 /// A ratio alias byte budget: `max(factor × input bytes, floor)`, where
218 /// "input bytes" is how much of the document (from its start, `---` or
219 /// its first content) has been read at the point of the charge, so a
220 /// read of one node has the budget the whole read has at the same
221 /// place. See [`Limits::alias_byte_budget`] for what is charged.
222 #[must_use]
223 pub const fn alias_byte_budget_ratio(mut self, factor: usize, floor: usize) -> Self {
224 self.alias_bytes_factor = factor;
225 self.alias_bytes_floor = floor;
226 self.alias_bytes_enabled = true;
227 self
228 }
229
230 /// Maximum input size in bytes (the whole stream, after decoding to
231 /// UTF-8).
232 ///
233 /// Consumed by the parser now; readers will also check it before
234 /// reading.
235 #[must_use]
236 pub const fn max_document_size(mut self, bytes: usize) -> Self {
237 self.max_document_size = bytes;
238 self
239 }
240
241 /// Maximum length in bytes of one scalar's value (after escapes and
242 /// folding). Consumed by the parser.
243 #[must_use]
244 pub const fn max_scalar_len(mut self, bytes: usize) -> Self {
245 self.max_scalar_len = bytes;
246 self
247 }
248
249 /// Maximum number of documents in one stream. Consumed by the parser.
250 #[must_use]
251 pub const fn max_documents(mut self, documents: usize) -> Self {
252 self.max_documents = documents;
253 self
254 }
255
256 /// Maximum nesting of collections that serde visits through the target
257 /// type's own `Deserialize` impls, i.e. how deep a *recursive user
258 /// type* such as `struct Tree(Vec<Tree>)` may follow the input.
259 ///
260 /// serde's visitor model recurses once per such level, so this bounds
261 /// the native stack: measured at about 1.9 KiB per level in release
262 /// builds and 7.4 KiB in debug builds (`examples/stack_probe.rs` in
263 /// `oneyaml`; the cost depends on the target type). The defaults (128;
264 /// 64 untrusted) fit a 2 MiB thread stack in both. **Before raising it,
265 /// make sure the deserializing thread's stack holds `max_typed_depth ×`
266 /// the per-level cost**, e.g. run on a thread built with
267 /// `std::thread::Builder::stack_size`; [`Limits::unlimited()`] removes
268 /// the bound entirely, so only use it with non-recursive targets or
269 /// input you trust. `Value` targets (and `Value` fields) are built
270 /// iteratively and are bounded only by `max_depth`.
271 #[must_use]
272 pub const fn max_typed_depth(mut self, depth: usize) -> Self {
273 self.max_typed_depth = depth;
274 self
275 }
276
277 /// Maximum number of diagnostics (errors and warnings together) one
278 /// parse or read keeps.
279 ///
280 /// The budget is per **stream**, shared by errors and warnings, and
281 /// counts every kind (decoding, settings, syntax, meaning). A list of
282 /// diagnostics keeps its first `max_diagnostics` entries in the order
283 /// it reports them; when more were found, it also keeps the first
284 /// error (whatever its place: it is the one a strict parse fails with)
285 /// and then ends with **one summary** warning, `Y2010`
286 /// (`code::DIAGNOSTICS_SUPPRESSED`): "N more diagnostics suppressed",
287 /// located where the first suppressed one was, whose help names this
288 /// setting. The summary does not count toward the limit (a capped list
289 /// holds at most `max_diagnostics + 2` entries) and N is exact.
290 /// Exceeding the limit is never an error by itself: strict parses still
291 /// fail exactly at the first error, and only then.
292 ///
293 /// **Meaning never depends on it**: what a read accepts or
294 /// refuses, `has_errors`, restricted mode, which edits are accepted, and
295 /// a read's cost are the same under every limit. Past it, the syntax
296 /// tree keeps a diagnostic as a 32-byte skeleton (code, severity,
297 /// spans) without its texts, and an error reported from there has its
298 /// code's summary for a message. Paths that discard warnings build at most 16.
299 #[must_use]
300 pub const fn max_diagnostics(mut self, diagnostics: usize) -> Self {
301 self.max_diagnostics = diagnostics;
302 self
303 }
304
305 /// The `max_diagnostics` limit (`usize::MAX`: none).
306 #[must_use]
307 pub const fn get_max_diagnostics(&self) -> usize {
308 self.max_diagnostics
309 }
310
311 /// The `max_typed_depth` limit (`usize::MAX`: none).
312 #[must_use]
313 pub const fn get_max_typed_depth(&self) -> usize {
314 self.max_typed_depth
315 }
316
317 /// The `max_depth` limit (`usize::MAX`: none).
318 #[must_use]
319 pub const fn get_max_depth(&self) -> usize {
320 self.max_depth
321 }
322
323 /// The alias budget for a document with `own_nodes` nodes, or `None`
324 /// when the budget is disabled ([`Limits::unlimited()`]).
325 #[must_use]
326 pub const fn get_alias_budget(&self, own_nodes: usize) -> Option<usize> {
327 if !self.alias_enabled {
328 return None;
329 }
330 let scaled = self.alias_factor.saturating_mul(own_nodes);
331 Some(if scaled > self.alias_floor {
332 scaled
333 } else {
334 self.alias_floor
335 })
336 }
337
338 /// The alias byte budget after `input_bytes` bytes of the document, or
339 /// `None` when it is disabled ([`Limits::unlimited()`]).
340 #[must_use]
341 pub const fn get_alias_byte_budget(&self, input_bytes: usize) -> Option<usize> {
342 if !self.alias_bytes_enabled {
343 return None;
344 }
345 let scaled = self.alias_bytes_factor.saturating_mul(input_bytes);
346 Some(if scaled > self.alias_bytes_floor {
347 scaled
348 } else {
349 self.alias_bytes_floor
350 })
351 }
352
353 /// The `max_document_size` limit in bytes (`usize::MAX`: none).
354 #[must_use]
355 pub const fn get_max_document_size(&self) -> usize {
356 self.max_document_size
357 }
358
359 /// The `max_scalar_len` limit in bytes (`usize::MAX`: none).
360 #[must_use]
361 pub const fn get_max_scalar_len(&self) -> usize {
362 self.max_scalar_len
363 }
364
365 /// The `max_documents` limit (`usize::MAX`: none).
366 #[must_use]
367 pub const fn get_max_documents(&self) -> usize {
368 self.max_documents
369 }
370}
371
372impl Default for Limits {
373 fn default() -> Self {
374 Self::new()
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn profiles_have_the_q4_5_values() {
384 let d = Limits::default();
385 assert_eq!(d, Limits::new());
386 assert_eq!(d.get_max_depth(), 1024);
387 assert_eq!(d.get_alias_budget(10), Some(1_000_000));
388 assert_eq!(d.get_alias_budget(200_000), Some(2_000_000));
389 assert_eq!(d.get_max_document_size(), 256 * 1024 * 1024);
390 assert_eq!(d.get_max_scalar_len(), 64 * 1024 * 1024);
391 assert_eq!(d.get_max_documents(), usize::MAX);
392 assert_eq!(d.get_max_typed_depth(), 128);
393 assert_eq!(d.get_max_diagnostics(), 100_000);
394 assert_eq!(d.get_alias_byte_budget(0), Some(64 * 1024 * 1024));
395 assert_eq!(
396 d.get_alias_byte_budget(10 * 1024 * 1024),
397 Some(100 * 1024 * 1024)
398 );
399
400 let u = Limits::untrusted();
401 assert_eq!(u.get_max_depth(), 128);
402 assert_eq!(u.get_alias_budget(10), Some(100_000));
403 assert_eq!(u.get_alias_budget(60_000), Some(120_000));
404 assert_eq!(u.get_max_document_size(), 16 * 1024 * 1024);
405 assert_eq!(u.get_max_scalar_len(), 1024 * 1024);
406 assert_eq!(u.get_max_documents(), 1000);
407 assert_eq!(u.get_max_typed_depth(), 64);
408 assert_eq!(u.get_max_diagnostics(), 1000);
409 assert_eq!(u.get_alias_byte_budget(1000), Some(8 * 1024 * 1024));
410 assert_eq!(
411 u.get_alias_byte_budget(5 * 1024 * 1024),
412 Some(10 * 1024 * 1024)
413 );
414
415 let n = Limits::unlimited();
416 assert_eq!(n.get_max_depth(), usize::MAX);
417 assert_eq!(n.get_alias_budget(usize::MAX), None);
418 assert_eq!(n.get_max_document_size(), usize::MAX);
419 assert_eq!(n.get_max_scalar_len(), usize::MAX);
420 assert_eq!(n.get_max_documents(), usize::MAX);
421 assert_eq!(n.get_max_typed_depth(), usize::MAX);
422 assert_eq!(n.get_max_diagnostics(), usize::MAX);
423 assert_eq!(n.get_alias_byte_budget(usize::MAX), None);
424 }
425
426 #[test]
427 fn setters_chain_and_are_const() {
428 const L: Limits = Limits::untrusted()
429 .max_depth(7)
430 .max_document_size(8)
431 .max_scalar_len(9)
432 .max_documents(10)
433 .alias_budget(11)
434 .max_typed_depth(12)
435 .max_diagnostics(13);
436 assert_eq!(L.get_max_typed_depth(), 12);
437 assert_eq!(L.get_max_diagnostics(), 13);
438 assert_eq!(
439 (
440 L.get_max_depth(),
441 L.get_max_document_size(),
442 L.get_max_scalar_len(),
443 L.get_max_documents(),
444 L.get_alias_budget(1_000),
445 ),
446 (7, 8, 9, 10, Some(11))
447 );
448 let r = Limits::unlimited().alias_budget_ratio(3, 5);
449 assert_eq!(r.get_alias_budget(1), Some(5));
450 assert_eq!(r.get_alias_budget(4), Some(12));
451 assert_eq!(r.get_alias_budget(usize::MAX), Some(usize::MAX));
452 assert_ne!(r, Limits::unlimited());
453 // The byte budget is set on its own; the node budget stays off.
454 const B: Limits = Limits::unlimited().alias_byte_budget(7);
455 assert_eq!(B.get_alias_byte_budget(1_000_000), Some(7));
456 assert_eq!(B.get_alias_budget(1), None);
457 let b = Limits::unlimited().alias_byte_budget_ratio(3, 5);
458 assert_eq!(b.get_alias_byte_budget(1), Some(5));
459 assert_eq!(b.get_alias_byte_budget(4), Some(12));
460 assert_eq!(b.get_alias_byte_budget(usize::MAX), Some(usize::MAX));
461 assert_eq!(b.get_alias_budget(1), None);
462 // And the node budget leaves the byte budget alone.
463 let n = Limits::default().alias_budget(3);
464 assert_eq!(n.get_alias_byte_budget(0), Some(64 * 1024 * 1024));
465 }
466}