synkit_core/
config.rs

1//! Parser configuration for resource limits and behavior tuning.
2//!
3//! This module provides [`ParseConfig`] for controlling parser behavior,
4//! including recursion limits to prevent stack overflow attacks.
5//!
6//! # Recursion Limits
7//!
8//! Following the pattern established by `serde_json`, parsers should enforce
9//! a maximum recursion depth to prevent malicious or malformed input from
10//! causing stack overflows. The default limit of 128 balances security with
11//! practical use cases.
12//!
13//! # Example
14//!
15//! ```ignore
16//! use synkit_core::config::ParseConfig;
17//!
18//! // Use default limits (recursion depth: 128)
19//! let config = ParseConfig::default();
20//!
21//! // Increase limit for deeply nested data
22//! let config = ParseConfig::new()
23//!     .with_max_recursion_depth(256);
24//!
25//! // Disable recursion limit (use with caution!)
26//! let config = ParseConfig::new()
27//!     .with_max_recursion_depth(usize::MAX);
28//! ```
29
30use crate::Error;
31
32/// Configuration for parser behavior and resource limits.
33///
34/// Controls limits on recursion depth, token count, and other resources
35/// to prevent denial-of-service attacks via malformed input.
36///
37/// # Default Values
38///
39/// | Setting | Default | Rationale |
40/// |---------|---------|-----------|
41/// | `max_recursion_depth` | 128 | Matches serde_json default |
42/// | `max_tokens` | `usize::MAX` | No limit by default |
43///
44/// # Security Considerations
45///
46/// Without recursion limits, deeply nested input like `[[[[[[...]]]]]]` can
47/// cause stack overflow. The default limit of 128 prevents most attacks while
48/// allowing reasonable nesting for typical use cases.
49///
50/// # Example
51///
52/// ```ignore
53/// let config = ParseConfig::default();
54/// let mut parser = TokenStream::with_config(tokens, config);
55///
56/// // In recursive parse implementation:
57/// fn parse_nested(stream: &mut TokenStream) -> Result<Nested, Error> {
58///     stream.enter_nested()?; // Increments depth, checks limit
59///     let inner = stream.parse()?;
60///     stream.exit_nested(); // Decrements depth
61///     Ok(Nested { inner })
62/// }
63/// ```
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct ParseConfig {
66    /// Maximum allowed recursion depth.
67    ///
68    /// When parsing nested structures, each level of nesting increments a
69    /// depth counter. If the counter exceeds this limit, parsing fails with
70    /// [`Error::RecursionLimitExceeded`].
71    ///
72    /// Default: 128 (matching serde_json)
73    pub max_recursion_depth: usize,
74
75    /// Maximum number of tokens to process.
76    ///
77    /// If the parser consumes more than this many tokens, parsing fails.
78    /// This can prevent resource exhaustion from extremely long inputs.
79    ///
80    /// Default: `usize::MAX` (no limit)
81    pub max_tokens: usize,
82}
83
84impl Default for ParseConfig {
85    /// Returns the default configuration.
86    ///
87    /// - `max_recursion_depth`: 128
88    /// - `max_tokens`: `usize::MAX`
89    #[inline]
90    fn default() -> Self {
91        Self::DEFAULT
92    }
93}
94
95impl ParseConfig {
96    /// Default configuration, usable in const contexts.
97    ///
98    /// Equivalent to `ParseConfig::default()` but available at compile time.
99    pub const DEFAULT: Self = Self {
100        max_recursion_depth: 128,
101        max_tokens: usize::MAX,
102    };
103
104    /// Creates a new configuration with default values.
105    #[inline]
106    pub const fn new() -> Self {
107        Self::DEFAULT
108    }
109
110    /// Sets the maximum recursion depth.
111    ///
112    /// # Arguments
113    ///
114    /// * `depth` - Maximum nesting level. Use `usize::MAX` to disable the limit.
115    ///
116    /// # Example
117    ///
118    /// ```ignore
119    /// let config = ParseConfig::new()
120    ///     .with_max_recursion_depth(256);
121    /// ```
122    #[inline]
123    pub const fn with_max_recursion_depth(mut self, depth: usize) -> Self {
124        self.max_recursion_depth = depth;
125        self
126    }
127
128    /// Sets the maximum token count.
129    ///
130    /// # Arguments
131    ///
132    /// * `count` - Maximum tokens to process. Use `usize::MAX` to disable.
133    #[inline]
134    pub const fn with_max_tokens(mut self, count: usize) -> Self {
135        self.max_tokens = count;
136        self
137    }
138
139    /// Disables the recursion limit.
140    ///
141    /// # Warning
142    ///
143    /// Only use this when parsing trusted input! Untrusted deeply-nested
144    /// input can cause stack overflow.
145    #[inline]
146    pub const fn disable_recursion_limit(self) -> Self {
147        self.with_max_recursion_depth(usize::MAX)
148    }
149}
150
151/// Tracks recursion depth during parsing.
152///
153/// This is a lightweight wrapper that parsers use to track and enforce
154/// recursion limits. It pairs with [`ParseConfig`] to provide the limit.
155///
156/// # Example
157///
158/// ```ignore
159/// struct MyParser {
160///     depth: RecursionGuard,
161///     config: ParseConfig,
162/// }
163///
164/// impl MyParser {
165///     fn parse_nested(&mut self) -> Result<(), Error> {
166///         self.depth.enter(self.config.max_recursion_depth)?;
167///         // ... parse nested content ...
168///         self.depth.exit();
169///         Ok(())
170///     }
171/// }
172/// ```
173#[derive(Debug, Clone, Copy, Default)]
174pub struct RecursionGuard {
175    /// Current recursion depth.
176    depth: usize,
177}
178
179impl RecursionGuard {
180    /// Creates a new guard with depth 0.
181    #[inline]
182    pub const fn new() -> Self {
183        Self { depth: 0 }
184    }
185
186    /// Current recursion depth.
187    #[inline]
188    pub const fn depth(&self) -> usize {
189        self.depth
190    }
191
192    /// Enter a nested context, incrementing depth.
193    ///
194    /// Returns `Err(Error::RecursionLimitExceeded)` if the new depth would
195    /// exceed the limit.
196    ///
197    /// # Arguments
198    ///
199    /// * `limit` - Maximum allowed depth (from `ParseConfig::max_recursion_depth`)
200    #[inline]
201    pub fn enter(&mut self, limit: usize) -> Result<(), Error> {
202        self.depth = self.depth.saturating_add(1);
203        if self.depth > limit {
204            Err(Error::RecursionLimitExceeded {
205                depth: self.depth,
206                limit,
207            })
208        } else {
209            Ok(())
210        }
211    }
212
213    /// Exit a nested context, decrementing depth.
214    ///
215    /// Uses saturating subtraction so extra `exit()` calls don't underflow.
216    #[inline]
217    pub fn exit(&mut self) {
218        self.depth = self.depth.saturating_sub(1);
219    }
220
221    /// Reset depth to zero.
222    ///
223    /// Useful when reusing a parser for multiple inputs.
224    #[inline]
225    pub fn reset(&mut self) {
226        self.depth = 0;
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_parse_config_defaults() {
236        let config = ParseConfig::default();
237        assert_eq!(config.max_recursion_depth, 128);
238        assert_eq!(config.max_tokens, usize::MAX);
239    }
240
241    #[test]
242    fn test_parse_config_builder() {
243        let config = ParseConfig::new()
244            .with_max_recursion_depth(256)
245            .with_max_tokens(10000);
246
247        assert_eq!(config.max_recursion_depth, 256);
248        assert_eq!(config.max_tokens, 10000);
249    }
250
251    #[test]
252    fn test_parse_config_disable_recursion() {
253        let config = ParseConfig::new().disable_recursion_limit();
254        assert_eq!(config.max_recursion_depth, usize::MAX);
255    }
256
257    #[test]
258    fn test_recursion_guard_basic() {
259        let mut guard = RecursionGuard::new();
260        assert_eq!(guard.depth(), 0);
261
262        guard.enter(128).unwrap();
263        assert_eq!(guard.depth(), 1);
264
265        guard.enter(128).unwrap();
266        assert_eq!(guard.depth(), 2);
267
268        guard.exit();
269        assert_eq!(guard.depth(), 1);
270
271        guard.exit();
272        assert_eq!(guard.depth(), 0);
273    }
274
275    #[test]
276    fn test_recursion_guard_limit_exceeded() {
277        let mut guard = RecursionGuard::new();
278
279        // Fill to limit
280        for _ in 0..3 {
281            guard.enter(3).unwrap();
282        }
283        assert_eq!(guard.depth(), 3);
284
285        // Next should fail
286        let result = guard.enter(3);
287        assert!(matches!(
288            result,
289            Err(Error::RecursionLimitExceeded { depth: 4, limit: 3 })
290        ));
291    }
292
293    #[test]
294    fn test_recursion_guard_exit_saturates() {
295        let mut guard = RecursionGuard::new();
296
297        // Extra exits don't underflow
298        guard.exit();
299        guard.exit();
300        assert_eq!(guard.depth(), 0);
301    }
302
303    #[test]
304    fn test_recursion_guard_reset() {
305        let mut guard = RecursionGuard::new();
306        guard.enter(128).unwrap();
307        guard.enter(128).unwrap();
308        assert_eq!(guard.depth(), 2);
309
310        guard.reset();
311        assert_eq!(guard.depth(), 0);
312    }
313}