libmagic_rs/config.rs
1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Evaluation configuration for magic rule processing.
5//!
6//! Defines [`EvaluationConfig`], which controls recursion depth, string length
7//! limits, matching strategy, MIME type mapping, and timeouts during rule
8//! evaluation. Extracted from `lib.rs` to keep that module under the project's
9//! file-size limit.
10
11use crate::Result;
12use crate::error::LibmagicError;
13
14/// Configuration for rule evaluation
15///
16/// This struct controls various aspects of magic rule evaluation behavior,
17/// including performance limits, output options, and matching strategies.
18///
19/// # Forward compatibility
20///
21/// This struct is marked `#[non_exhaustive]`: new configuration fields may
22/// be added in any release without it being a breaking change. Construct
23/// instances via one of the factory constructors
24/// ([`EvaluationConfig::default()`], [`EvaluationConfig::new()`],
25/// [`EvaluationConfig::performance()`],
26/// [`EvaluationConfig::comprehensive()`]) and then chain `with_*`
27/// builder-style setters:
28///
29/// ```rust
30/// use libmagic_rs::EvaluationConfig;
31///
32/// let custom_config = EvaluationConfig::default()
33/// .with_max_recursion_depth(10)
34/// .with_timeout_ms(Some(5_000));
35/// ```
36///
37/// Direct struct-literal construction (`EvaluationConfig { .. }`) is
38/// rejected by the compiler from outside this crate because of
39/// `#[non_exhaustive]`.
40#[derive(Debug, Clone, PartialEq, Eq)]
41#[non_exhaustive]
42pub struct EvaluationConfig {
43 /// Maximum recursion depth for nested rules
44 ///
45 /// This prevents infinite recursion in malformed magic files and limits
46 /// the depth of rule hierarchy traversal. Default is 20.
47 pub max_recursion_depth: u32,
48
49 /// Maximum string length to read
50 ///
51 /// Caps the buffer-length allocation for scan-mode reads of
52 /// `TypeKind::String` (both the unflagged `(None, _)` arm and the
53 /// flagged `/c`/`/C`/`/w`/`/W`/`/T`/`/f` arm). Without this cap, a
54 /// `string x` rule against an attacker-controlled NUL-free buffer
55 /// could allocate up to the full buffer length -- the CWE-770
56 /// control documented at this field. Default is 8192 bytes.
57 ///
58 /// Does NOT apply to:
59 /// - `TypeKind::PString`: returns `TypeReadError::BufferOverrun`
60 /// rather than truncating when the length prefix exceeds the
61 /// remaining buffer (per GOTCHAS S3.8's pstring anchor-clamp
62 /// invariant).
63 /// - `TypeKind::String16`: bounded by a hardcoded
64 /// `STRING16_MAX_UNITS = 8192` ceiling at 2 bytes per unit.
65 pub max_string_length: usize,
66
67 /// Stop at first match or continue for all matches
68 ///
69 /// When `true`, evaluation stops after the first matching rule.
70 /// When `false`, all rules are evaluated to find all matches.
71 /// Default is `true` for performance.
72 ///
73 /// # Semantics
74 ///
75 /// "First match" refers to the first *top-level* rule that matches.
76 /// Children of the first matching top-level rule are always evaluated
77 /// before the stop check; the stop check applies to subsequent
78 /// top-level rules. In other words, `stop_at_first_match = true` does
79 /// not truncate the child subtree of the matching rule -- it only
80 /// prevents later sibling top-level rules from being evaluated. A
81 /// successful top-level match therefore returns one parent `RuleMatch`
82 /// plus any descendant `RuleMatch` values its children produced.
83 pub stop_at_first_match: bool,
84
85 /// Enable MIME type mapping in results
86 ///
87 /// When `true`, the evaluator will attempt to map file type descriptions
88 /// to standard MIME types. Default is `false`.
89 pub enable_mime_types: bool,
90
91 /// Timeout for evaluation in milliseconds
92 ///
93 /// If set, evaluation will be aborted if it takes longer than this duration.
94 /// `None` means no timeout. Default is `None`.
95 pub timeout_ms: Option<u64>,
96}
97
98impl Default for EvaluationConfig {
99 /// Returns the default evaluation configuration.
100 ///
101 /// # Security
102 ///
103 /// The default configuration has no timeout. When processing untrusted
104 /// input, use [`EvaluationConfig::performance()`] or set `timeout_ms`
105 /// explicitly to prevent denial of service.
106 fn default() -> Self {
107 Self {
108 max_recursion_depth: 20,
109 max_string_length: 8192,
110 stop_at_first_match: true,
111 enable_mime_types: false,
112 timeout_ms: None,
113 }
114 }
115}
116
117impl EvaluationConfig {
118 /// Create a new configuration with default values
119 ///
120 /// # Examples
121 ///
122 /// ```rust
123 /// use libmagic_rs::EvaluationConfig;
124 ///
125 /// let config = EvaluationConfig::new();
126 /// assert_eq!(config.max_recursion_depth, 20);
127 /// assert_eq!(config.max_string_length, 8192);
128 /// assert!(config.stop_at_first_match);
129 /// assert!(!config.enable_mime_types);
130 /// assert_eq!(config.timeout_ms, None);
131 /// ```
132 #[must_use]
133 pub fn new() -> Self {
134 Self::default()
135 }
136
137 /// Create a configuration optimized for performance
138 ///
139 /// This configuration prioritizes speed over completeness:
140 /// - Lower recursion depth limit
141 /// - Smaller string length limit
142 /// - Stop at first match
143 /// - No MIME type mapping
144 /// - Short timeout
145 ///
146 /// # Examples
147 ///
148 /// ```rust
149 /// use libmagic_rs::EvaluationConfig;
150 ///
151 /// let config = EvaluationConfig::performance();
152 /// assert_eq!(config.max_recursion_depth, 10);
153 /// assert_eq!(config.max_string_length, 1024);
154 /// assert!(config.stop_at_first_match);
155 /// assert!(!config.enable_mime_types);
156 /// assert_eq!(config.timeout_ms, Some(1000));
157 /// ```
158 #[must_use]
159 pub const fn performance() -> Self {
160 Self {
161 max_recursion_depth: 10,
162 max_string_length: 1024,
163 stop_at_first_match: true,
164 enable_mime_types: false,
165 timeout_ms: Some(1000), // 1 second
166 }
167 }
168
169 /// Create a configuration optimized for completeness
170 ///
171 /// This configuration prioritizes finding all matches over speed:
172 /// - Higher recursion depth limit
173 /// - Larger string length limit
174 /// - Find all matches
175 /// - Enable MIME type mapping
176 /// - Longer timeout
177 ///
178 /// # Examples
179 ///
180 /// ```rust
181 /// use libmagic_rs::EvaluationConfig;
182 ///
183 /// let config = EvaluationConfig::comprehensive();
184 /// assert_eq!(config.max_recursion_depth, 50);
185 /// assert_eq!(config.max_string_length, 32768);
186 /// assert!(!config.stop_at_first_match);
187 /// assert!(config.enable_mime_types);
188 /// assert_eq!(config.timeout_ms, Some(30000));
189 /// ```
190 #[must_use]
191 pub const fn comprehensive() -> Self {
192 Self {
193 max_recursion_depth: 50,
194 max_string_length: 32768,
195 stop_at_first_match: false,
196 enable_mime_types: true,
197 timeout_ms: Some(30000), // 30 seconds
198 }
199 }
200
201 /// Sets the maximum recursion depth for nested rule evaluation.
202 ///
203 /// Builder-style setter for consumers outside this crate. Direct
204 /// struct-literal construction is blocked by `#[non_exhaustive]`, so
205 /// chain `with_*` calls after one of the factory constructors
206 /// (`default`, `performance`, `comprehensive`, `new`).
207 #[must_use]
208 pub const fn with_max_recursion_depth(mut self, depth: u32) -> Self {
209 self.max_recursion_depth = depth;
210 self
211 }
212
213 /// Sets the maximum string length (in bytes) read for string types.
214 #[must_use]
215 pub const fn with_max_string_length(mut self, length: usize) -> Self {
216 self.max_string_length = length;
217 self
218 }
219
220 /// Sets whether evaluation stops after the first top-level match.
221 #[must_use]
222 pub const fn with_stop_at_first_match(mut self, stop: bool) -> Self {
223 self.stop_at_first_match = stop;
224 self
225 }
226
227 /// Enables or disables MIME type mapping in results.
228 #[must_use]
229 pub const fn with_mime_types(mut self, enable: bool) -> Self {
230 self.enable_mime_types = enable;
231 self
232 }
233
234 /// Sets the evaluation timeout in milliseconds. Pass `None` for
235 /// unbounded evaluation (not recommended on untrusted input).
236 #[must_use]
237 pub const fn with_timeout_ms(mut self, timeout_ms: Option<u64>) -> Self {
238 self.timeout_ms = timeout_ms;
239 self
240 }
241
242 /// Validate the configuration settings
243 ///
244 /// Performs comprehensive security validation of all configuration values
245 /// to prevent malicious configurations that could lead to resource exhaustion,
246 /// denial of service, or other security issues.
247 ///
248 /// # Security
249 ///
250 /// This validation prevents:
251 /// - Stack overflow attacks through excessive recursion depth
252 /// - Memory exhaustion through oversized string limits
253 /// - Denial of service through excessive timeouts
254 /// - Integer overflow in configuration calculations
255 ///
256 /// # Errors
257 ///
258 /// Returns `LibmagicError::ConfigError` if any configuration values
259 /// are invalid or out of reasonable bounds.
260 ///
261 /// # Examples
262 ///
263 /// ```rust
264 /// use libmagic_rs::EvaluationConfig;
265 ///
266 /// let config = EvaluationConfig::default();
267 /// assert!(config.validate().is_ok());
268 ///
269 /// let invalid_config = EvaluationConfig::default().with_max_recursion_depth(0);
270 /// assert!(invalid_config.validate().is_err());
271 /// ```
272 pub fn validate(&self) -> Result<()> {
273 self.validate_recursion_depth()?;
274 self.validate_string_length()?;
275 self.validate_timeout()?;
276 self.validate_resource_combination()?;
277 Ok(())
278 }
279
280 /// Validate recursion depth to prevent stack overflow attacks
281 fn validate_recursion_depth(&self) -> Result<()> {
282 const MAX_SAFE_RECURSION_DEPTH: u32 = 1000;
283
284 if self.max_recursion_depth == 0 {
285 return Err(LibmagicError::ConfigError {
286 reason: "max_recursion_depth must be greater than 0".to_string(),
287 });
288 }
289
290 if self.max_recursion_depth > MAX_SAFE_RECURSION_DEPTH {
291 return Err(LibmagicError::ConfigError {
292 reason: format!(
293 "max_recursion_depth must not exceed {MAX_SAFE_RECURSION_DEPTH} to prevent stack overflow"
294 ),
295 });
296 }
297
298 Ok(())
299 }
300
301 /// Validate string length to prevent memory exhaustion
302 fn validate_string_length(&self) -> Result<()> {
303 const MAX_SAFE_STRING_LENGTH: usize = 1_048_576; // 1MB
304
305 if self.max_string_length == 0 {
306 return Err(LibmagicError::ConfigError {
307 reason: "max_string_length must be greater than 0".to_string(),
308 });
309 }
310
311 if self.max_string_length > MAX_SAFE_STRING_LENGTH {
312 return Err(LibmagicError::ConfigError {
313 reason: format!(
314 "max_string_length must not exceed {MAX_SAFE_STRING_LENGTH} bytes to prevent memory exhaustion"
315 ),
316 });
317 }
318
319 Ok(())
320 }
321
322 /// Validate timeout to prevent denial of service
323 fn validate_timeout(&self) -> Result<()> {
324 const MAX_SAFE_TIMEOUT_MS: u64 = 300_000; // 5 minutes
325
326 if let Some(timeout) = self.timeout_ms {
327 if timeout == 0 {
328 return Err(LibmagicError::ConfigError {
329 reason: "timeout_ms must be greater than 0 if specified".to_string(),
330 });
331 }
332
333 if timeout > MAX_SAFE_TIMEOUT_MS {
334 return Err(LibmagicError::ConfigError {
335 reason: format!(
336 "timeout_ms must not exceed {MAX_SAFE_TIMEOUT_MS} (5 minutes) to prevent denial of service"
337 ),
338 });
339 }
340 }
341
342 Ok(())
343 }
344
345 /// Validate resource combination to prevent resource exhaustion
346 fn validate_resource_combination(&self) -> Result<()> {
347 const HIGH_RECURSION_THRESHOLD: u32 = 100;
348 const LARGE_STRING_THRESHOLD: usize = 65536;
349
350 if self.max_recursion_depth > HIGH_RECURSION_THRESHOLD
351 && self.max_string_length > LARGE_STRING_THRESHOLD
352 {
353 return Err(LibmagicError::ConfigError {
354 reason: format!(
355 "High recursion depth (>{HIGH_RECURSION_THRESHOLD}) combined with large string length (>{LARGE_STRING_THRESHOLD}) may cause resource exhaustion"
356 ),
357 });
358 }
359
360 Ok(())
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 // ── Presets ──────────────────────────────────────────────────
369
370 #[test]
371 fn test_default_validates() {
372 assert!(EvaluationConfig::default().validate().is_ok());
373 }
374
375 #[test]
376 fn test_performance_validates() {
377 assert!(EvaluationConfig::performance().validate().is_ok());
378 }
379
380 #[test]
381 fn test_comprehensive_validates() {
382 assert!(EvaluationConfig::comprehensive().validate().is_ok());
383 }
384
385 // ── Recursion depth boundaries ──────────────────────────────
386
387 #[test]
388 fn test_recursion_depth_zero_rejected() {
389 let cfg = EvaluationConfig {
390 max_recursion_depth: 0,
391 ..Default::default()
392 };
393 assert!(cfg.validate().is_err());
394 }
395
396 #[test]
397 fn test_recursion_depth_one_accepted() {
398 let cfg = EvaluationConfig {
399 max_recursion_depth: 1,
400 ..Default::default()
401 };
402 assert!(cfg.validate().is_ok());
403 }
404
405 #[test]
406 fn test_recursion_depth_at_max_accepted() {
407 let cfg = EvaluationConfig {
408 max_recursion_depth: 1000,
409 ..Default::default()
410 };
411 assert!(cfg.validate().is_ok());
412 }
413
414 #[test]
415 fn test_recursion_depth_above_max_rejected() {
416 let cfg = EvaluationConfig {
417 max_recursion_depth: 1001,
418 ..Default::default()
419 };
420 assert!(cfg.validate().is_err());
421 }
422
423 // ── String length boundaries ────────────────────────────────
424
425 #[test]
426 fn test_string_length_zero_rejected() {
427 let cfg = EvaluationConfig {
428 max_string_length: 0,
429 ..Default::default()
430 };
431 assert!(cfg.validate().is_err());
432 }
433
434 #[test]
435 fn test_string_length_one_accepted() {
436 let cfg = EvaluationConfig {
437 max_string_length: 1,
438 ..Default::default()
439 };
440 assert!(cfg.validate().is_ok());
441 }
442
443 #[test]
444 fn test_string_length_at_max_accepted() {
445 let cfg = EvaluationConfig {
446 max_string_length: 1_048_576,
447 ..Default::default()
448 };
449 assert!(cfg.validate().is_ok());
450 }
451
452 #[test]
453 fn test_string_length_above_max_rejected() {
454 let cfg = EvaluationConfig {
455 max_string_length: 1_048_577,
456 ..Default::default()
457 };
458 assert!(cfg.validate().is_err());
459 }
460
461 // ── Timeout boundaries ──────────────────────────────────────
462
463 #[test]
464 fn test_timeout_none_accepted() {
465 let cfg = EvaluationConfig {
466 timeout_ms: None,
467 ..Default::default()
468 };
469 assert!(cfg.validate().is_ok());
470 }
471
472 #[test]
473 fn test_timeout_zero_rejected() {
474 let cfg = EvaluationConfig {
475 timeout_ms: Some(0),
476 ..Default::default()
477 };
478 assert!(cfg.validate().is_err());
479 }
480
481 #[test]
482 fn test_timeout_one_accepted() {
483 let cfg = EvaluationConfig {
484 timeout_ms: Some(1),
485 ..Default::default()
486 };
487 assert!(cfg.validate().is_ok());
488 }
489
490 #[test]
491 fn test_timeout_at_max_accepted() {
492 let cfg = EvaluationConfig {
493 timeout_ms: Some(300_000),
494 ..Default::default()
495 };
496 assert!(cfg.validate().is_ok());
497 }
498
499 #[test]
500 fn test_timeout_above_max_rejected() {
501 let cfg = EvaluationConfig {
502 timeout_ms: Some(300_001),
503 ..Default::default()
504 };
505 assert!(cfg.validate().is_err());
506 }
507
508 // ── Resource combination guard ──────────────────────────────
509
510 #[test]
511 fn test_high_recursion_with_large_string_rejected() {
512 let cfg = EvaluationConfig {
513 max_recursion_depth: 101,
514 max_string_length: 65537,
515 ..Default::default()
516 };
517 assert!(cfg.validate().is_err());
518 }
519
520 #[test]
521 fn test_high_recursion_with_normal_string_accepted() {
522 let cfg = EvaluationConfig {
523 max_recursion_depth: 101,
524 max_string_length: 65536,
525 ..Default::default()
526 };
527 assert!(cfg.validate().is_ok());
528 }
529
530 #[test]
531 fn test_normal_recursion_with_large_string_accepted() {
532 let cfg = EvaluationConfig {
533 max_recursion_depth: 100,
534 max_string_length: 65537,
535 ..Default::default()
536 };
537 assert!(cfg.validate().is_ok());
538 }
539
540 // ── evaluate_rules_with_config rejects invalid config ───────
541
542 #[test]
543 fn test_evaluate_rules_with_config_rejects_invalid() {
544 use crate::evaluator::evaluate_rules_with_config;
545
546 let invalid_cfg = EvaluationConfig {
547 max_recursion_depth: 0,
548 ..Default::default()
549 };
550 let result = evaluate_rules_with_config(&[], &[], &invalid_cfg);
551 assert!(result.is_err());
552 }
553}