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(crate) use loader::{load_magic_bytes, load_magic_reader};
155pub use loader::{load_magic_directory, load_magic_file};
156
157// Internal re-exports for sibling modules and tests
158pub(crate) use hierarchy::build_rule_hierarchy;
159pub(crate) use preprocessing::preprocess_lines;
160
161use crate::error::ParseError;
162
163/// Result of parsing a text magic file.
164///
165/// Contains the top-level rule list with any `name`-declared subroutines
166/// hoisted into a separate crate-internal `NameTable` keyed by identifier.
167/// The rule list preserves the original ordering of all non-`Name` top-level
168/// rules, so strength-based sorting and evaluation semantics are unchanged
169/// for magic files that do not use the `name`/`use` directive pair.
170// The mixed visibility is deliberate: `name_table` is pub(crate) so external
171// consumers cannot inject subroutine tables (see GOTCHAS S3.10).
172#[allow(clippy::partial_pub_fields)]
173#[derive(Debug)]
174pub struct ParsedMagic {
175 /// Top-level rules after `Name` subroutines have been removed.
176 pub rules: Vec<MagicRule>,
177 /// Extracted `name` subroutine definitions, consulted by the evaluator
178 /// when a rule of type `TypeKind::Meta(MetaType::Use { .. })` is reached.
179 pub(crate) name_table: name_table::NameTable,
180}
181
182/// Parses a complete magic file from raw text input.
183///
184/// This is the main public-facing parser function that orchestrates the complete
185/// parsing pipeline: preprocessing, parsing individual rules, and building the
186/// hierarchical structure.
187///
188/// # Arguments
189///
190/// * `input` - The raw magic file content as a string
191///
192/// # Returns
193///
194/// `Result<ParsedMagic, ParseError>` - A [`ParsedMagic`] value containing
195/// the top-level rules (with `name`-declared subroutines hoisted out) and
196/// the resulting name table.
197///
198/// # Errors
199///
200/// Returns an error if any stage of parsing fails:
201/// - Preprocessing errors
202/// - Rule parsing errors
203/// - Hierarchy building errors
204///
205/// # Example
206///
207/// ```ignore
208/// use libmagic_rs::parser::parse_text_magic_file;
209///
210/// let magic = r#"0 string \x7fELF ELF file
211/// >4 byte 1 32-bit
212/// >4 byte 2 64-bit"#;
213///
214/// let parsed = parse_text_magic_file(magic)?;
215/// assert_eq!(parsed.rules.len(), 1);
216/// assert_eq!(parsed.rules[0].message, "ELF file");
217/// # Ok::<(), Box<dyn std::error::Error>>(())
218/// ```
219pub fn parse_text_magic_file(input: &str) -> Result<ParsedMagic, ParseError> {
220 let lines = preprocess_lines(input)?;
221 let rules = build_rule_hierarchy(lines)?;
222 let (rules, name_table) = name_table::extract_name_table(rules);
223 Ok(ParsedMagic { rules, name_table })
224}
225
226/// Line-tolerant variant of [`parse_text_magic_file`] for **runtime** loading
227/// of external magic files/directories.
228///
229/// Unlike [`parse_text_magic_file`] (which is fail-fast, so the crate's own
230/// build-time codegen rejects a malformed builtin rule), this skips an
231/// unparseable rule and its subtree with a `warn!` and keeps the rest of the
232/// file -- matching GNU `file`. Real system magic databases routinely mix
233/// rules this parser fully supports with a handful using constructs it does
234/// not yet handle (`guid`, `der`, middle-endian dates, some indirect-offset
235/// forms); without tolerance, one such rule would drop the entire file --
236/// e.g. losing all of `compress`'s gzip/bzip2 detection over its lone `ustring`
237/// XZ rule. See GOTCHAS S3.11.
238///
239/// `source` is an optional label for the magic file being parsed (its path),
240/// threaded into the skipped-rule `warn!` so a directory load can locate the
241/// offending rule. Pass `None` for in-memory/direct-API input with no source
242/// file.
243pub(crate) fn parse_text_magic_file_tolerant(
244 input: &str,
245 source: Option<&std::path::Path>,
246) -> Result<ParsedMagic, ParseError> {
247 let lines = preprocess_lines(input)?;
248 let rules = hierarchy::build_rule_hierarchy_tolerant(lines, source)?;
249 let (rules, name_table) = name_table::extract_name_table(rules);
250 Ok(ParsedMagic { rules, name_table })
251}
252
253#[cfg(test)]
254mod unit_tests {
255 use super::*;
256
257 // ============================================================
258 // Tests for parse_text_magic_file (10+ test cases)
259 // ============================================================
260
261 #[test]
262 fn test_parse_text_magic_file_single_rule() {
263 let input = "0 string 0 ZIP archive";
264 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
265 assert_eq!(rules.len(), 1);
266 assert_eq!(rules[0].message, "ZIP archive");
267 }
268
269 #[test]
270 fn test_parse_text_magic_file_hierarchical_rules() {
271 let input = r"
2720 string 0 ELF
273>4 byte 1 32-bit
274>4 byte 2 64-bit
275";
276 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
277 assert_eq!(rules.len(), 1);
278 assert_eq!(rules[0].children.len(), 2);
279 }
280
281 #[test]
282 fn test_parse_text_magic_file_with_comments() {
283 let input = r"
284# ELF file format
2850 string 0 ELF
286>4 byte 1 32-bit
287";
288 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
289 assert_eq!(rules.len(), 1);
290 assert_eq!(rules[0].children.len(), 1);
291 }
292
293 #[test]
294 fn test_parse_text_magic_file_multiple_roots() {
295 let input = r"
2960 byte 1 ELF
297>4 byte 1 32-bit
298
2990 byte 2 PDF
300>5 byte 1 v1
301";
302 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
303 assert_eq!(rules.len(), 2);
304 }
305
306 #[test]
307 fn test_parse_text_magic_file_empty_input() {
308 let input = "";
309 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
310 assert_eq!(rules.len(), 0);
311 }
312
313 #[test]
314 fn test_parse_text_magic_file_only_comments() {
315 let input = r"
316# Comment 1
317# Comment 2
318# Comment 3
319";
320 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
321 assert_eq!(rules.len(), 0);
322 }
323
324 #[test]
325 fn test_parse_text_magic_file_empty_lines_only() {
326 let input = r"
327
328
3290 string 0 Test file
330
331
332";
333 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
334 assert_eq!(rules.len(), 1);
335 }
336
337 #[test]
338 fn test_parse_text_magic_file_with_message_spaces() {
339 let input = "0 string 0 Long message continued here";
340 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
341 assert!(rules[0].message.contains("continued"));
342 }
343
344 #[test]
345 fn test_parse_text_magic_file_mixed_indentation() {
346 let input = r"
3470 byte 1 Root1
348>4 byte 1 Child1
349>4 byte 2 Child2
350>>6 byte 3 Grandchild
351
3520 byte 2 Root2
353>4 byte 4 Child3
354";
355 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
356 assert_eq!(rules.len(), 2);
357 assert_eq!(rules[0].children.len(), 2);
358 assert_eq!(rules[0].children[1].children.len(), 1);
359 assert_eq!(rules[1].children.len(), 1);
360 }
361
362 #[test]
363 fn test_parse_text_magic_file_complex_real_world() {
364 let input = r"
365# Magic file for common formats
366
367# ELF binaries
3680 byte 0x7f ELF executable
369>4 byte 1 Intel 80386
370>4 byte 2 x86-64
371>>5 byte 1 LSB
372>>5 byte 2 MSB
373
374# PDF files
3750 byte 0x25 PDF document
376>5 byte 0x31 version 1.0
377>5 byte 0x34 version 1.4
378>5 byte 0x32 version 2.0
379";
380 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
381 assert_eq!(rules.len(), 2);
382 assert_eq!(rules[0].message, "ELF executable");
383 assert!(rules[0].children.len() > 1);
384 }
385
386 // ============================================================
387 // Strength directive integration tests
388 // ============================================================
389
390 #[test]
391 fn test_parse_text_magic_file_with_strength_directive() {
392 let input = r"
393!:strength +10
3940 string \\x7fELF ELF executable
395";
396 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
397 assert_eq!(rules.len(), 1);
398 assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Add(10)));
399 }
400
401 #[test]
402 fn test_parse_text_magic_file_strength_applies_to_next_rule() {
403 let input = r"
404!:strength *2
4050 string \\x7fELF ELF executable
4060 string \\x50\\x4b ZIP archive
407";
408 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
409 assert_eq!(rules.len(), 2);
410 // Strength should only apply to the immediately following rule
411 assert_eq!(
412 rules[0].strength_modifier,
413 Some(StrengthModifier::Multiply(2))
414 );
415 assert_eq!(rules[1].strength_modifier, None);
416 }
417
418 #[test]
419 fn test_parse_text_magic_file_strength_with_child_rules() {
420 let input = r"
421!:strength =50
4220 string \\x7fELF ELF executable
423>4 byte 1 32-bit
424>4 byte 2 64-bit
425";
426 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
427 assert_eq!(rules.len(), 1);
428 // Strength applies to root rule
429 assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Set(50)));
430 // Children should not have strength modifier
431 assert_eq!(rules[0].children[0].strength_modifier, None);
432 assert_eq!(rules[0].children[1].strength_modifier, None);
433 }
434
435 #[test]
436 fn test_parse_text_magic_file_multiple_strength_directives() {
437 let input = r"
438!:strength +10
4390 string \\x7fELF ELF executable
440!:strength -5
4410 string \\x50\\x4b ZIP archive
442";
443 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
444 assert_eq!(rules.len(), 2);
445 assert_eq!(rules[0].strength_modifier, Some(StrengthModifier::Add(10)));
446 assert_eq!(
447 rules[1].strength_modifier,
448 Some(StrengthModifier::Subtract(5))
449 );
450 }
451
452 #[test]
453 fn test_parse_text_magic_file_strength_all_operators() {
454 let inputs = [
455 ("!:strength +20\n0 byte 1 Test", StrengthModifier::Add(20)),
456 (
457 "!:strength -15\n0 byte 1 Test",
458 StrengthModifier::Subtract(15),
459 ),
460 (
461 "!:strength *3\n0 byte 1 Test",
462 StrengthModifier::Multiply(3),
463 ),
464 ("!:strength /2\n0 byte 1 Test", StrengthModifier::Divide(2)),
465 ("!:strength =100\n0 byte 1 Test", StrengthModifier::Set(100)),
466 ("!:strength 50\n0 byte 1 Test", StrengthModifier::Set(50)),
467 ];
468
469 for (input, expected_modifier) in inputs {
470 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
471 assert_eq!(
472 rules[0].strength_modifier,
473 Some(expected_modifier),
474 "Failed for input: {input}"
475 );
476 }
477 }
478
479 // ============================================================
480 // Integration and edge case tests
481 // ============================================================
482
483 #[test]
484 fn test_continuation_with_indentation() {
485 let input = r">4 byte 1 Message \
486continued";
487 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
488 assert_eq!(rules.len(), 1);
489 }
490
491 #[test]
492 fn test_multiple_hex_offsets() {
493 let input = r"
4940x100 string 0 At 256
4950x200 string 0 At 512
496";
497 let ParsedMagic { rules, .. } = parse_text_magic_file(input).unwrap();
498 assert_eq!(rules.len(), 2);
499 }
500
501 // ============================================================
502 // Overflow protection tests
503 // ============================================================
504
505 #[test]
506 fn test_overflow_decimal_too_many_digits() {
507 use crate::parser::grammar::parse_number;
508 // Test exactly 20 digits (should fail - over i64 max)
509 let result = parse_number("12345678901234567890");
510 assert!(result.is_err(), "Should reject 20+ decimal digits");
511 }
512
513 #[test]
514 fn test_overflow_hex_too_many_digits() {
515 use crate::parser::grammar::parse_number;
516 // Test 17 hex digits (should fail)
517 let result = parse_number("0x10000000000000000");
518 assert!(result.is_err(), "Should reject 17+ hex digits");
519 }
520
521 #[test]
522 fn test_overflow_i64_max() {
523 use crate::parser::grammar::parse_number;
524 // i64::MAX = 9223372036854775807
525 let result = parse_number("9223372036854775807");
526 assert!(result.is_ok(), "Should accept i64::MAX");
527 }
528
529 #[test]
530 fn test_overflow_i64_max_plus_one() {
531 use crate::parser::grammar::parse_number;
532 // i64::MAX + 1 should fail
533 let result = parse_number("9223372036854775808");
534 assert!(result.is_err(), "Should reject i64::MAX + 1");
535 }
536
537 // ============================================================
538 // Line number accuracy test (uses parse_text_magic_file)
539 // ============================================================
540
541 #[test]
542 fn test_error_reports_correct_line_for_continuation() {
543 // When a continued rule fails to parse, error should show the starting line
544 let input = "0 string 0 valid\n0 invalid \\\nsyntax here\n0 string 0 valid2";
545 let result = parse_text_magic_file(input);
546
547 match result {
548 Err(ref e) => {
549 // Error should mention line 2 (start of the bad rule), not line 3
550 let error_str = format!("{e:?}");
551 assert!(
552 error_str.contains("line 2") || error_str.contains("line: 2"),
553 "Error should reference line 2, got: {error_str}"
554 );
555 }
556 Ok(_) => panic!("Expected InvalidSyntax error"),
557 }
558 }
559}
560
561#[cfg(test)]
562mod output_test {
563 use crate::parser::{
564 ParsedMagic, build_rule_hierarchy, parse_text_magic_file, preprocess_lines,
565 };
566
567 #[test]
568 fn demo_show_all_parser_outputs() {
569 let input = r"
570# ELF file
5710 string 0 ELF
572>4 byte 1 32-bit
573>4 byte 2 64-bit
574
5750 string 0 ZIP
576>0 byte 3 zipped
577";
578
579 println!("\n================ RAW INPUT ================\n");
580 println!("{input}");
581
582 // --------------------------------------------------
583 // 1. preprocess_lines
584 // --------------------------------------------------
585 println!("\n================ PREPROCESS LINES ================\n");
586
587 let lines = preprocess_lines(input).expect("preprocess_lines failed");
588
589 for (idx, line) in lines.iter().enumerate() {
590 println!(
591 "[{}] line_no={} is_comment={} content='{}'",
592 idx, line.line_number, line.is_comment, line.content
593 );
594 }
595
596 // --------------------------------------------------
597 // 2. parse_text_magic_file (full pipeline)
598 // --------------------------------------------------
599 println!("\n================ PARSED MAGIC RULES ================\n");
600
601 let ParsedMagic { rules, .. } =
602 parse_text_magic_file(input).expect("parse_text_magic_file failed");
603
604 for (i, rule) in rules.iter().enumerate() {
605 println!("ROOT RULE [{i}]:");
606 print_rule(rule, 1);
607 }
608
609 // --------------------------------------------------
610 // 3. build_rule_hierarchy (explicit)
611 // --------------------------------------------------
612 println!("\n================ EXPLICIT HIERARCHY BUILD ================\n");
613
614 let rebuilt = build_rule_hierarchy(lines).expect("build_rule_hierarchy failed");
615
616 for (i, rule) in rebuilt.iter().enumerate() {
617 println!("ROOT [{i}]:");
618 print_rule(rule, 1);
619 }
620 }
621
622 // Helper to pretty-print rule trees
623 fn print_rule(rule: &crate::parser::MagicRule, indent: usize) {
624 let pad = " ".repeat(indent);
625
626 println!(
627 "{}- level={} offset={:?} type={:?} op={:?} value={:?} message='{}'",
628 pad, rule.level, rule.offset, rule.typ, rule.op, rule.value, rule.message
629 );
630
631 for child in &rule.children {
632 print_rule(child, indent + 1);
633 }
634 }
635}