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