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