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#[cfg(test)]
226mod unit_tests {
227    use super::*;
228
229    // ============================================================
230    // Tests for parse_text_magic_file (10+ test cases)
231    // ============================================================
232
233    #[test]
234    fn test_parse_text_magic_file_single_rule() {
235        let input = "0 string 0 ZIP archive";
236        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
237        assert_eq!(rules.len(), 1);
238        assert_eq!(rules[0].message, "ZIP archive");
239    }
240
241    #[test]
242    fn test_parse_text_magic_file_hierarchical_rules() {
243        let input = r"
2440 string 0 ELF
245>4 byte 1 32-bit
246>4 byte 2 64-bit
247";
248        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
249        assert_eq!(rules.len(), 1);
250        assert_eq!(rules[0].children.len(), 2);
251    }
252
253    #[test]
254    fn test_parse_text_magic_file_with_comments() {
255        let input = r"
256# ELF file format
2570 string 0 ELF
258>4 byte 1 32-bit
259";
260        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
261        assert_eq!(rules.len(), 1);
262        assert_eq!(rules[0].children.len(), 1);
263    }
264
265    #[test]
266    fn test_parse_text_magic_file_multiple_roots() {
267        let input = r"
2680 byte 1 ELF
269>4 byte 1 32-bit
270
2710 byte 2 PDF
272>5 byte 1 v1
273";
274        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
275        assert_eq!(rules.len(), 2);
276    }
277
278    #[test]
279    fn test_parse_text_magic_file_empty_input() {
280        let input = "";
281        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
282        assert_eq!(rules.len(), 0);
283    }
284
285    #[test]
286    fn test_parse_text_magic_file_only_comments() {
287        let input = r"
288# Comment 1
289# Comment 2
290# Comment 3
291";
292        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
293        assert_eq!(rules.len(), 0);
294    }
295
296    #[test]
297    fn test_parse_text_magic_file_empty_lines_only() {
298        let input = r"
299
300
3010 string 0 Test file
302
303
304";
305        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
306        assert_eq!(rules.len(), 1);
307    }
308
309    #[test]
310    fn test_parse_text_magic_file_with_message_spaces() {
311        let input = "0 string 0 Long message continued here";
312        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
313        assert!(rules[0].message.contains("continued"));
314    }
315
316    #[test]
317    fn test_parse_text_magic_file_mixed_indentation() {
318        let input = r"
3190 byte 1 Root1
320>4 byte 1 Child1
321>4 byte 2 Child2
322>>6 byte 3 Grandchild
323
3240 byte 2 Root2
325>4 byte 4 Child3
326";
327        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
328        assert_eq!(rules.len(), 2);
329        assert_eq!(rules[0].children.len(), 2);
330        assert_eq!(rules[0].children[1].children.len(), 1);
331        assert_eq!(rules[1].children.len(), 1);
332    }
333
334    #[test]
335    fn test_parse_text_magic_file_complex_real_world() {
336        let input = r"
337# Magic file for common formats
338
339# ELF binaries
3400 byte 0x7f ELF executable
341>4 byte 1 Intel 80386
342>4 byte 2 x86-64
343>>5 byte 1 LSB
344>>5 byte 2 MSB
345
346# PDF files
3470 byte 0x25 PDF document
348>5 byte 0x31 version 1.0
349>5 byte 0x34 version 1.4
350>5 byte 0x32 version 2.0
351";
352        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
353        assert_eq!(rules.len(), 2);
354        assert_eq!(rules[0].message, "ELF executable");
355        assert!(rules[0].children.len() > 1);
356    }
357
358    // ============================================================
359    // Strength directive integration tests
360    // ============================================================
361
362    #[test]
363    fn test_parse_text_magic_file_with_strength_directive() {
364        let input = r"
365!:strength +10
3660 string \\x7fELF ELF executable
367";
368        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
369        assert_eq!(rules.len(), 1);
370        assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Add(10)));
371    }
372
373    #[test]
374    fn test_parse_text_magic_file_strength_applies_to_next_rule() {
375        let input = r"
376!:strength *2
3770 string \\x7fELF ELF executable
3780 string \\x50\\x4b ZIP archive
379";
380        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
381        assert_eq!(rules.len(), 2);
382        // Strength should only apply to the immediately following rule
383        assert_eq!(
384            rules[0].strength_modifier,
385            Some(StrengthModifier::Multiply(2))
386        );
387        assert_eq!(rules[1].strength_modifier, None);
388    }
389
390    #[test]
391    fn test_parse_text_magic_file_strength_with_child_rules() {
392        let input = r"
393!:strength =50
3940 string \\x7fELF ELF executable
395>4 byte 1 32-bit
396>4 byte 2 64-bit
397";
398        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
399        assert_eq!(rules.len(), 1);
400        // Strength applies to root rule
401        assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Set(50)));
402        // Children should not have strength modifier
403        assert_eq!(rules[0].children[0].strength_modifier, None);
404        assert_eq!(rules[0].children[1].strength_modifier, None);
405    }
406
407    #[test]
408    fn test_parse_text_magic_file_multiple_strength_directives() {
409        let input = r"
410!:strength +10
4110 string \\x7fELF ELF executable
412!:strength -5
4130 string \\x50\\x4b ZIP archive
414";
415        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
416        assert_eq!(rules.len(), 2);
417        assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Add(10)));
418        assert_eq!(
419            rules[1].strength_modifier,
420            Some(StrengthModifier::Subtract(5))
421        );
422    }
423
424    #[test]
425    fn test_parse_text_magic_file_strength_all_operators() {
426        let inputs = [
427            ("!:strength +20\n0 byte 1 Test", StrengthModifier::Add(20)),
428            (
429                "!:strength -15\n0 byte 1 Test",
430                StrengthModifier::Subtract(15),
431            ),
432            (
433                "!:strength *3\n0 byte 1 Test",
434                StrengthModifier::Multiply(3),
435            ),
436            ("!:strength /2\n0 byte 1 Test", StrengthModifier::Divide(2)),
437            ("!:strength =100\n0 byte 1 Test", StrengthModifier::Set(100)),
438            ("!:strength 50\n0 byte 1 Test", StrengthModifier::Set(50)),
439        ];
440
441        for (input, expected_modifier) in inputs {
442            let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
443            assert_eq!(
444                rules[0].strength_modifier,
445                Some(expected_modifier),
446                "Failed for input: {input}"
447            );
448        }
449    }
450
451    // ============================================================
452    // Integration and edge case tests
453    // ============================================================
454
455    #[test]
456    fn test_continuation_with_indentation() {
457        let input = r">4 byte 1 Message \
458continued";
459        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
460        assert_eq!(rules.len(), 1);
461    }
462
463    #[test]
464    fn test_multiple_hex_offsets() {
465        let input = r"
4660x100 string 0 At 256
4670x200 string 0 At 512
468";
469        let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
470        assert_eq!(rules.len(), 2);
471    }
472
473    // ============================================================
474    // Overflow protection tests
475    // ============================================================
476
477    #[test]
478    fn test_overflow_decimal_too_many_digits() {
479        use crate::parser::grammar::parse_number;
480        // Test exactly 20 digits (should fail - over i64 max)
481        let result = parse_number("12345678901234567890");
482        assert!(result.is_err(), "Should reject 20+ decimal digits");
483    }
484
485    #[test]
486    fn test_overflow_hex_too_many_digits() {
487        use crate::parser::grammar::parse_number;
488        // Test 17 hex digits (should fail)
489        let result = parse_number("0x10000000000000000");
490        assert!(result.is_err(), "Should reject 17+ hex digits");
491    }
492
493    #[test]
494    fn test_overflow_i64_max() {
495        use crate::parser::grammar::parse_number;
496        // i64::MAX = 9223372036854775807
497        let result = parse_number("9223372036854775807");
498        assert!(result.is_ok(), "Should accept i64::MAX");
499    }
500
501    #[test]
502    fn test_overflow_i64_max_plus_one() {
503        use crate::parser::grammar::parse_number;
504        // i64::MAX + 1 should fail
505        let result = parse_number("9223372036854775808");
506        assert!(result.is_err(), "Should reject i64::MAX + 1");
507    }
508
509    // ============================================================
510    // Line number accuracy test (uses parse_text_magic_file)
511    // ============================================================
512
513    #[test]
514    fn test_error_reports_correct_line_for_continuation() {
515        // When a continued rule fails to parse, error should show the starting line
516        let input = "0 string 0 valid\n0 invalid \\\nsyntax here\n0 string 0 valid2";
517        let result = parse_text_magic_file(input);
518
519        match result {
520            Err(ref e) => {
521                // Error should mention line 2 (start of the bad rule), not line 3
522                let error_str = format!("{e:?}");
523                assert!(
524                    error_str.contains("line 2") || error_str.contains("line: 2"),
525                    "Error should reference line 2, got: {error_str}"
526                );
527            }
528            Ok(_) => panic!("Expected InvalidSyntax error"),
529        }
530    }
531}
532
533#[cfg(test)]
534mod output_test {
535    use crate::parser::{
536        ParsedMagic, build_rule_hierarchy, parse_text_magic_file, preprocess_lines,
537    };
538
539    #[test]
540    fn demo_show_all_parser_outputs() {
541        let input = r"
542# ELF file
5430 string 0 ELF
544>4 byte 1 32-bit
545>4 byte 2 64-bit
546
5470 string 0 ZIP
548>0 byte 3 zipped
549";
550
551        println!("\n================ RAW INPUT ================\n");
552        println!("{input}");
553
554        // --------------------------------------------------
555        // 1. preprocess_lines
556        // --------------------------------------------------
557        println!("\n================ PREPROCESS LINES ================\n");
558
559        let lines = preprocess_lines(input).expect("preprocess_lines failed");
560
561        for (idx, line) in lines.iter().enumerate() {
562            println!(
563                "[{}] line_no={} is_comment={} content='{}'",
564                idx, line.line_number, line.is_comment, line.content
565            );
566        }
567
568        // --------------------------------------------------
569        // 2. parse_text_magic_file (full pipeline)
570        // --------------------------------------------------
571        println!("\n================ PARSED MAGIC RULES ================\n");
572
573        let ParsedMagic { rules, .. } =
574            parse_text_magic_file(input).expect("parse_text_magic_file failed");
575
576        for (i, rule) in rules.iter().enumerate() {
577            println!("ROOT RULE [{i}]:");
578            print_rule(rule, 1);
579        }
580
581        // --------------------------------------------------
582        // 3. build_rule_hierarchy (explicit)
583        // --------------------------------------------------
584        println!("\n================ EXPLICIT HIERARCHY BUILD ================\n");
585
586        let rebuilt = build_rule_hierarchy(lines).expect("build_rule_hierarchy failed");
587
588        for (i, rule) in rebuilt.iter().enumerate() {
589            println!("ROOT [{i}]:");
590            print_rule(rule, 1);
591        }
592    }
593
594    // Helper to pretty-print rule trees
595    fn print_rule(rule: &crate::parser::MagicRule, indent: usize) {
596        let pad = "  ".repeat(indent);
597
598        println!(
599            "{}- level={} offset={:?} type={:?} op={:?} value={:?} message='{}'",
600            pad, rule.level, rule.offset, rule.typ, rule.op, rule.value, rule.message
601        );
602
603        for child in &rule.children {
604            print_rule(child, indent + 1);
605        }
606    }
607}