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}