Skip to main content

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}