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}