libmagic_rs/parser/mod.rs
1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Magic file parser module
5//!
6//! This module handles parsing of magic files into an Abstract Syntax Tree (AST)
7//! that can be evaluated against file buffers for type identification.
8//!
9//! # Overview
10//!
11//! The parser implements a complete pipeline for transforming magic file text into
12//! a hierarchical rule structure suitable for evaluation. The pipeline consists of:
13//!
14//! 1. **Preprocessing**: Line handling, comment removal, continuation processing
15//! 2. **Parsing**: Individual magic rule parsing using nom combinators
16//! 3. **Hierarchy Building**: Constructing parent-child relationships based on indentation
17//! 4. **Validation**: Type checking and offset resolution
18//!
19//! # Format Detection and Loading
20//!
21//! The module automatically detects and handles three types of magic file formats:
22//! - **Text files**: Human-readable magic rule definitions
23//! - **Directories**: Collections of magic files (Magdir pattern)
24//! - **Binary files**: Compiled .mgc files (currently unsupported)
25//!
26//! ## Unified Loading API
27//!
28//! The recommended entry point for loading magic files is [`load_magic_file()`], which
29//! automatically detects the format and dispatches to the appropriate handler:
30//!
31//! ```ignore
32//! use libmagic_rs::parser::load_magic_file;
33//! use std::path::Path;
34//!
35//! // Works with text files
36//! let rules = load_magic_file(Path::new("/usr/share/misc/magic"))?;
37//!
38//! // Also works with directories
39//! let rules = load_magic_file(Path::new("/usr/share/misc/magic.d"))?;
40//!
41//! // Binary .mgc files return an error with guidance
42//! match load_magic_file(Path::new("/usr/share/misc/magic.mgc")) {
43//! Ok(rules) => { /* ... */ },
44//! Err(e) => eprintln!("Use --use-builtin for binary files: {}", e),
45//! }
46//! # Ok::<(), Box<dyn std::error::Error>>(())
47//! ```
48//!
49//! ## Three-Tier Loading Strategy
50//!
51//! The loading process works as follows:
52//!
53//! 1. **Format Detection**: [`detect_format()`] examines the path to determine the file type
54//! 2. **Dispatch to Handler**:
55//! - Text files -> [`parse_text_magic_file()`] after reading contents
56//! - Directories -> [`load_magic_directory()`] to load and merge all files
57//! - Binary files -> Returns error suggesting `--use-builtin` option
58//! 3. **Return Merged Rules**: All rules are returned in a single `Vec<MagicRule>`
59//!
60//! # Examples
61//!
62//! ## Loading Magic Files (Recommended)
63//!
64//! Use the unified [`load_magic_file()`] API for automatic format detection:
65//!
66//! ```ignore
67//! use libmagic_rs::parser::load_magic_file;
68//! use std::path::Path;
69//!
70//! let rules = load_magic_file(Path::new("/usr/share/misc/magic"))?;
71//! println!("Loaded {} magic rules", rules.len());
72//! # Ok::<(), Box<dyn std::error::Error>>(())
73//! ```
74//!
75//! ## Parsing Text Content Directly
76//!
77//! For parsing magic rule text that's already in memory:
78//!
79//! ```ignore
80//! use libmagic_rs::parser::parse_text_magic_file;
81//!
82//! let magic_content = r#"
83//! 0 string \x7fELF ELF executable
84//! >4 byte 1 32-bit
85//! >4 byte 2 64-bit
86//! "#;
87//!
88//! let rules = parse_text_magic_file(magic_content)?;
89//! assert_eq!(rules.len(), 1);
90//! assert_eq!(rules[0].children.len(), 2);
91//! # Ok::<(), Box<dyn std::error::Error>>(())
92//! ```
93//!
94//! ## Loading a Directory Explicitly
95//!
96//! For Magdir-style directories containing multiple magic files:
97//!
98//! ```ignore
99//! use libmagic_rs::parser::load_magic_directory;
100//! use std::path::Path;
101//!
102//! // Directory structure:
103//! // /usr/share/file/magic.d/
104//! // ├── elf
105//! // ├── archive
106//! // └── text
107//!
108//! let rules = load_magic_directory(Path::new("/usr/share/file/magic.d"))?;
109//! // Rules from all files are merged in alphabetical order by filename
110//! # Ok::<(), Box<dyn std::error::Error>>(())
111//! ```
112//!
113//! ## Migration Note
114//!
115//! **For users upgrading from direct function calls:**
116//!
117//! - **Old approach**: Call `detect_format()` then dispatch manually
118//! - **New approach**: Use `load_magic_file()` for automatic dispatching
119//!
120//! The individual functions (`parse_text_magic_file()`, `load_magic_directory()`)
121//! remain available for advanced use cases where you need direct control.
122//!
123//! **Key differences:**
124//! - `load_magic_file()`: Unified API with automatic format detection (recommended)
125//! - `parse_text_magic_file()`: Parses a single text string containing magic rules
126//! - `load_magic_directory()`: Loads and merges all magic files from a directory
127//! - `detect_format()`: Low-level format detection (now called internally by `load_magic_file()`)
128//!
129//! **Error handling in `load_magic_directory()`:**
130//! - Critical errors (I/O failures, invalid UTF-8): Returns `ParseError` immediately
131//! - Non-critical errors (parse failures in individual files): Logs warning to stderr and continues
132
133pub mod ast;
134#[allow(dead_code)]
135pub(crate) mod codegen;
136mod format;
137// `grammar` exposes nom-based parser combinators that are implementation
138// details of the magic-file parsing pipeline. Keep them visible to the rest
139// of the crate (for sibling modules and unit tests) but never to external
140// consumers -- the only supported parser entry points are the
141// `parse_text_magic_file` / `load_magic_file` functions in this module.
142pub(crate) mod grammar;
143mod hierarchy;
144mod loader;
145pub(crate) mod name_table;
146pub(crate) mod preprocessing;
147pub mod types;
148
149// Re-export AST types for convenience
150pub use ast::{Endianness, MagicRule, OffsetSpec, Operator, StrengthModifier, TypeKind, Value};
151
152// Re-export format detection and loading
153pub use format::{MagicFileFormat, detect_format};
154pub use loader::{load_magic_directory, load_magic_file};
155
156// Internal re-exports for sibling modules and tests
157pub(crate) use hierarchy::build_rule_hierarchy;
158pub(crate) use preprocessing::preprocess_lines;
159
160use crate::error::ParseError;
161
162/// Result of parsing a text magic file.
163///
164/// Contains the top-level rule list with any `name`-declared subroutines
165/// hoisted into a separate crate-internal `NameTable` keyed by identifier.
166/// The rule list preserves the original ordering of all non-`Name` top-level
167/// rules, so strength-based sorting and evaluation semantics are unchanged
168/// for magic files that do not use the `name`/`use` directive pair.
169// The mixed visibility is deliberate: `name_table` is pub(crate) so external
170// consumers cannot inject subroutine tables (see GOTCHAS S3.10).
171#[allow(clippy::partial_pub_fields)]
172#[derive(Debug)]
173pub struct ParsedMagic {
174 /// Top-level rules after `Name` subroutines have been removed.
175 pub rules: Vec<MagicRule>,
176 /// Extracted `name` subroutine definitions, consulted by the evaluator
177 /// when a rule of type `TypeKind::Meta(MetaType::Use { .. })` is reached.
178 pub(crate) name_table: name_table::NameTable,
179}
180
181/// Parses a complete magic file from raw text input.
182///
183/// This is the main public-facing parser function that orchestrates the complete
184/// parsing pipeline: preprocessing, parsing individual rules, and building the
185/// hierarchical structure.
186///
187/// # Arguments
188///
189/// * `input` - The raw magic file content as a string
190///
191/// # Returns
192///
193/// `Result<ParsedMagic, ParseError>` - A [`ParsedMagic`] value containing
194/// the top-level rules (with `name`-declared subroutines hoisted out) and
195/// the resulting name table.
196///
197/// # Errors
198///
199/// Returns an error if any stage of parsing fails:
200/// - Preprocessing errors
201/// - Rule parsing errors
202/// - Hierarchy building errors
203///
204/// # Example
205///
206/// ```ignore
207/// use libmagic_rs::parser::parse_text_magic_file;
208///
209/// let magic = r#"0 string \x7fELF ELF file
210/// >4 byte 1 32-bit
211/// >4 byte 2 64-bit"#;
212///
213/// let parsed = parse_text_magic_file(magic)?;
214/// assert_eq!(parsed.rules.len(), 1);
215/// assert_eq!(parsed.rules[0].message, "ELF file");
216/// # Ok::<(), Box<dyn std::error::Error>>(())
217/// ```
218pub fn parse_text_magic_file(input: &str) -> Result<ParsedMagic, ParseError> {
219 let lines = preprocess_lines(input)?;
220 let rules = build_rule_hierarchy(lines)?;
221 let (rules, name_table) = name_table::extract_name_table(rules);
222 Ok(ParsedMagic { rules, name_table })
223}
224
225/// Line-tolerant variant of [`parse_text_magic_file`] for **runtime** loading
226/// of external magic files/directories.
227///
228/// Unlike [`parse_text_magic_file`] (which is fail-fast, so the crate's own
229/// build-time codegen rejects a malformed builtin rule), this skips an
230/// unparseable rule and its subtree with a `warn!` and keeps the rest of the
231/// file -- matching GNU `file`. Real system magic databases routinely mix
232/// rules this parser fully supports with a handful using constructs it does
233/// not yet handle (`guid`, `der`, middle-endian dates, some indirect-offset
234/// forms); without tolerance, one such rule would drop the entire file --
235/// e.g. losing all of `compress`'s gzip/bzip2 detection over its lone `ustring`
236/// XZ rule. See GOTCHAS S3.11.
237pub(crate) fn parse_text_magic_file_tolerant(input: &str) -> Result<ParsedMagic, ParseError> {
238 let lines = preprocess_lines(input)?;
239 let rules = hierarchy::build_rule_hierarchy_tolerant(lines)?;
240 let (rules, name_table) = name_table::extract_name_table(rules);
241 Ok(ParsedMagic { rules, name_table })
242}
243
244#[cfg(test)]
245mod unit_tests {
246 use super::*;
247
248 // ============================================================
249 // Tests for parse_text_magic_file (10+ test cases)
250 // ============================================================
251
252 #[test]
253 fn test_parse_text_magic_file_single_rule() {
254 let input = "0 string 0 ZIP archive";
255 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
256 assert_eq!(rules.len(), 1);
257 assert_eq!(rules[0].message, "ZIP archive");
258 }
259
260 #[test]
261 fn test_parse_text_magic_file_hierarchical_rules() {
262 let input = r"
2630 string 0 ELF
264>4 byte 1 32-bit
265>4 byte 2 64-bit
266";
267 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
268 assert_eq!(rules.len(), 1);
269 assert_eq!(rules[0].children.len(), 2);
270 }
271
272 #[test]
273 fn test_parse_text_magic_file_with_comments() {
274 let input = r"
275# ELF file format
2760 string 0 ELF
277>4 byte 1 32-bit
278";
279 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
280 assert_eq!(rules.len(), 1);
281 assert_eq!(rules[0].children.len(), 1);
282 }
283
284 #[test]
285 fn test_parse_text_magic_file_multiple_roots() {
286 let input = r"
2870 byte 1 ELF
288>4 byte 1 32-bit
289
2900 byte 2 PDF
291>5 byte 1 v1
292";
293 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
294 assert_eq!(rules.len(), 2);
295 }
296
297 #[test]
298 fn test_parse_text_magic_file_empty_input() {
299 let input = "";
300 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
301 assert_eq!(rules.len(), 0);
302 }
303
304 #[test]
305 fn test_parse_text_magic_file_only_comments() {
306 let input = r"
307# Comment 1
308# Comment 2
309# Comment 3
310";
311 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
312 assert_eq!(rules.len(), 0);
313 }
314
315 #[test]
316 fn test_parse_text_magic_file_empty_lines_only() {
317 let input = r"
318
319
3200 string 0 Test file
321
322
323";
324 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
325 assert_eq!(rules.len(), 1);
326 }
327
328 #[test]
329 fn test_parse_text_magic_file_with_message_spaces() {
330 let input = "0 string 0 Long message continued here";
331 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
332 assert!(rules[0].message.contains("continued"));
333 }
334
335 #[test]
336 fn test_parse_text_magic_file_mixed_indentation() {
337 let input = r"
3380 byte 1 Root1
339>4 byte 1 Child1
340>4 byte 2 Child2
341>>6 byte 3 Grandchild
342
3430 byte 2 Root2
344>4 byte 4 Child3
345";
346 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
347 assert_eq!(rules.len(), 2);
348 assert_eq!(rules[0].children.len(), 2);
349 assert_eq!(rules[0].children[1].children.len(), 1);
350 assert_eq!(rules[1].children.len(), 1);
351 }
352
353 #[test]
354 fn test_parse_text_magic_file_complex_real_world() {
355 let input = r"
356# Magic file for common formats
357
358# ELF binaries
3590 byte 0x7f ELF executable
360>4 byte 1 Intel 80386
361>4 byte 2 x86-64
362>>5 byte 1 LSB
363>>5 byte 2 MSB
364
365# PDF files
3660 byte 0x25 PDF document
367>5 byte 0x31 version 1.0
368>5 byte 0x34 version 1.4
369>5 byte 0x32 version 2.0
370";
371 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
372 assert_eq!(rules.len(), 2);
373 assert_eq!(rules[0].message, "ELF executable");
374 assert!(rules[0].children.len() > 1);
375 }
376
377 // ============================================================
378 // Strength directive integration tests
379 // ============================================================
380
381 #[test]
382 fn test_parse_text_magic_file_with_strength_directive() {
383 let input = r"
384!:strength +10
3850 string \\x7fELF ELF executable
386";
387 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
388 assert_eq!(rules.len(), 1);
389 assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Add(10)));
390 }
391
392 #[test]
393 fn test_parse_text_magic_file_strength_applies_to_next_rule() {
394 let input = r"
395!:strength *2
3960 string \\x7fELF ELF executable
3970 string \\x50\\x4b ZIP archive
398";
399 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
400 assert_eq!(rules.len(), 2);
401 // Strength should only apply to the immediately following rule
402 assert_eq!(
403 rules[0].strength_modifier,
404 Some(StrengthModifier::Multiply(2))
405 );
406 assert_eq!(rules[1].strength_modifier, None);
407 }
408
409 #[test]
410 fn test_parse_text_magic_file_strength_with_child_rules() {
411 let input = r"
412!:strength =50
4130 string \\x7fELF ELF executable
414>4 byte 1 32-bit
415>4 byte 2 64-bit
416";
417 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
418 assert_eq!(rules.len(), 1);
419 // Strength applies to root rule
420 assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Set(50)));
421 // Children should not have strength modifier
422 assert_eq!(rules[0].children[0].strength_modifier, None);
423 assert_eq!(rules[0].children[1].strength_modifier, None);
424 }
425
426 #[test]
427 fn test_parse_text_magic_file_multiple_strength_directives() {
428 let input = r"
429!:strength +10
4300 string \\x7fELF ELF executable
431!:strength -5
4320 string \\x50\\x4b ZIP archive
433";
434 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
435 assert_eq!(rules.len(), 2);
436 assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Add(10)));
437 assert_eq!(
438 rules[1].strength_modifier,
439 Some(StrengthModifier::Subtract(5))
440 );
441 }
442
443 #[test]
444 fn test_parse_text_magic_file_strength_all_operators() {
445 let inputs = [
446 ("!:strength +20\n0 byte 1 Test", StrengthModifier::Add(20)),
447 (
448 "!:strength -15\n0 byte 1 Test",
449 StrengthModifier::Subtract(15),
450 ),
451 (
452 "!:strength *3\n0 byte 1 Test",
453 StrengthModifier::Multiply(3),
454 ),
455 ("!:strength /2\n0 byte 1 Test", StrengthModifier::Divide(2)),
456 ("!:strength =100\n0 byte 1 Test", StrengthModifier::Set(100)),
457 ("!:strength 50\n0 byte 1 Test", StrengthModifier::Set(50)),
458 ];
459
460 for (input, expected_modifier) in inputs {
461 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
462 assert_eq!(
463 rules[0].strength_modifier,
464 Some(expected_modifier),
465 "Failed for input: {input}"
466 );
467 }
468 }
469
470 // ============================================================
471 // Integration and edge case tests
472 // ============================================================
473
474 #[test]
475 fn test_continuation_with_indentation() {
476 let input = r">4 byte 1 Message \
477continued";
478 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
479 assert_eq!(rules.len(), 1);
480 }
481
482 #[test]
483 fn test_multiple_hex_offsets() {
484 let input = r"
4850x100 string 0 At 256
4860x200 string 0 At 512
487";
488 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
489 assert_eq!(rules.len(), 2);
490 }
491
492 // ============================================================
493 // Overflow protection tests
494 // ============================================================
495
496 #[test]
497 fn test_overflow_decimal_too_many_digits() {
498 use crate::parser::grammar::parse_number;
499 // Test exactly 20 digits (should fail - over i64 max)
500 let result = parse_number("12345678901234567890");
501 assert!(result.is_err(), "Should reject 20+ decimal digits");
502 }
503
504 #[test]
505 fn test_overflow_hex_too_many_digits() {
506 use crate::parser::grammar::parse_number;
507 // Test 17 hex digits (should fail)
508 let result = parse_number("0x10000000000000000");
509 assert!(result.is_err(), "Should reject 17+ hex digits");
510 }
511
512 #[test]
513 fn test_overflow_i64_max() {
514 use crate::parser::grammar::parse_number;
515 // i64::MAX = 9223372036854775807
516 let result = parse_number("9223372036854775807");
517 assert!(result.is_ok(), "Should accept i64::MAX");
518 }
519
520 #[test]
521 fn test_overflow_i64_max_plus_one() {
522 use crate::parser::grammar::parse_number;
523 // i64::MAX + 1 should fail
524 let result = parse_number("9223372036854775808");
525 assert!(result.is_err(), "Should reject i64::MAX + 1");
526 }
527
528 // ============================================================
529 // Line number accuracy test (uses parse_text_magic_file)
530 // ============================================================
531
532 #[test]
533 fn test_error_reports_correct_line_for_continuation() {
534 // When a continued rule fails to parse, error should show the starting line
535 let input = "0 string 0 valid\n0 invalid \\\nsyntax here\n0 string 0 valid2";
536 let result = parse_text_magic_file(input);
537
538 match result {
539 Err(ref e) => {
540 // Error should mention line 2 (start of the bad rule), not line 3
541 let error_str = format!("{e:?}");
542 assert!(
543 error_str.contains("line 2") || error_str.contains("line: 2"),
544 "Error should reference line 2, got: {error_str}"
545 );
546 }
547 Ok(_) => panic!("Expected InvalidSyntax error"),
548 }
549 }
550}
551
552#[cfg(test)]
553mod output_test {
554 use crate::parser::{
555 ParsedMagic, build_rule_hierarchy, parse_text_magic_file, preprocess_lines,
556 };
557
558 #[test]
559 fn demo_show_all_parser_outputs() {
560 let input = r"
561# ELF file
5620 string 0 ELF
563>4 byte 1 32-bit
564>4 byte 2 64-bit
565
5660 string 0 ZIP
567>0 byte 3 zipped
568";
569
570 println!("\n================ RAW INPUT ================\n");
571 println!("{input}");
572
573 // --------------------------------------------------
574 // 1. preprocess_lines
575 // --------------------------------------------------
576 println!("\n================ PREPROCESS LINES ================\n");
577
578 let lines = preprocess_lines(input).expect("preprocess_lines failed");
579
580 for (idx, line) in lines.iter().enumerate() {
581 println!(
582 "[{}] line_no={} is_comment={} content='{}'",
583 idx, line.line_number, line.is_comment, line.content
584 );
585 }
586
587 // --------------------------------------------------
588 // 2. parse_text_magic_file (full pipeline)
589 // --------------------------------------------------
590 println!("\n================ PARSED MAGIC RULES ================\n");
591
592 let ParsedMagic { rules, .. } =
593 parse_text_magic_file(input).expect("parse_text_magic_file failed");
594
595 for (i, rule) in rules.iter().enumerate() {
596 println!("ROOT RULE [{i}]:");
597 print_rule(rule, 1);
598 }
599
600 // --------------------------------------------------
601 // 3. build_rule_hierarchy (explicit)
602 // --------------------------------------------------
603 println!("\n================ EXPLICIT HIERARCHY BUILD ================\n");
604
605 let rebuilt = build_rule_hierarchy(lines).expect("build_rule_hierarchy failed");
606
607 for (i, rule) in rebuilt.iter().enumerate() {
608 println!("ROOT [{i}]:");
609 print_rule(rule, 1);
610 }
611 }
612
613 // Helper to pretty-print rule trees
614 fn print_rule(rule: &crate::parser::MagicRule, indent: usize) {
615 let pad = " ".repeat(indent);
616
617 println!(
618 "{}- level={} offset={:?} type={:?} op={:?} value={:?} message='{}'",
619 pad, rule.level, rule.offset, rule.typ, rule.op, rule.value, rule.message
620 );
621
622 for child in &rule.children {
623 print_rule(child, indent + 1);
624 }
625 }
626}