Skip to main content

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}