libmagic_rs/parser/loader.rs
1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! File and directory loading for magic files.
5//!
6//! Provides functions for loading magic rules from individual files and
7//! directories, with automatic format detection and error handling.
8
9use log::warn;
10
11use crate::error::ParseError;
12use crate::parser::ParsedMagic;
13use crate::parser::name_table::NameTable;
14use std::path::{Path, PathBuf};
15
16use super::format::{MagicFileFormat, detect_format};
17
18/// Maximum magic file size (1 GB).
19///
20/// Applied before loading a magic file (or any file within a magic directory)
21/// into memory to prevent memory-exhaustion `DoS` from maliciously oversized
22/// inputs.
23///
24/// This value is kept in sync with `crate::io::FileBuffer::MAX_FILE_SIZE`.
25/// The constant is duplicated (rather than imported) because this module is
26/// also pulled in by `build.rs` via `#[path]` and the build script cannot
27/// reference lib-only modules such as `crate::io`. A unit test below asserts
28/// the two constants remain equal.
29pub const MAX_MAGIC_FILE_SIZE: u64 = 1024 * 1024 * 1024;
30
31/// Reads a magic file into a `String` after verifying its size does not
32/// exceed [`MAX_MAGIC_FILE_SIZE`].
33///
34/// Returns a `ParseError` if metadata cannot be read, the file exceeds the
35/// size limit, or the file contents cannot be read.
36///
37/// # Encoding
38///
39/// Magic files are parsed as byte streams (matching GNU `file`/libmagic
40/// behavior). Real-world magic files frequently contain non-UTF-8 bytes in
41/// comments and attribution lines (e.g., Latin-1 author names). Rather than
42/// rejecting such files, invalid UTF-8 sequences are replaced with U+FFFD
43/// via [`String::from_utf8_lossy`] and a warning is logged. ASCII rule
44/// syntax is preserved byte-for-byte; replacements only affect non-ASCII
45/// text which, in practice, appears almost exclusively inside comments
46/// that are stripped before tokenization.
47fn read_magic_file_bounded(path: &Path) -> Result<String, ParseError> {
48 let metadata = std::fs::metadata(path).map_err(|e| {
49 ParseError::IoError(std::io::Error::new(
50 e.kind(),
51 format!("Failed to read metadata for '{}': {}", path.display(), e),
52 ))
53 })?;
54
55 if metadata.len() > MAX_MAGIC_FILE_SIZE {
56 return Err(ParseError::invalid_syntax(
57 0,
58 format!(
59 "Magic file '{}' is too large: {} bytes (maximum allowed: {} bytes)",
60 path.display(),
61 metadata.len(),
62 MAX_MAGIC_FILE_SIZE
63 ),
64 ));
65 }
66
67 let bytes = std::fs::read(path).map_err(ParseError::from)?;
68
69 match String::from_utf8(bytes) {
70 Ok(s) => Ok(s),
71 Err(e) => {
72 warn!(
73 "Magic file '{}' contains non-UTF-8 bytes; they were replaced with U+FFFD. \
74 Rule parsing proceeds, but replacements inside rule bodies may alter matching.",
75 path.display()
76 );
77 Ok(String::from_utf8_lossy(&e.into_bytes()).into_owned())
78 }
79 }
80}
81
82/// Loads and parses all magic files from a directory, merging them into a single rule set.
83///
84/// This function reads all regular files in the specified directory, parses each as a magic file,
85/// and combines the resulting rules into a single `Vec<MagicRule>`. Files are processed in
86/// alphabetical order by filename to ensure deterministic results.
87///
88/// # Error Handling Strategy
89///
90/// This function distinguishes between critical and non-critical errors:
91///
92/// - **Critical errors** (I/O failures, directory access issues, encoding problems):
93/// These cause immediate failure and return a `ParseError`. The function stops processing
94/// and propagates the error to the caller.
95///
96/// - **Non-critical errors** (individual file parse failures):
97/// These are logged at warn level and the file is skipped. Processing
98/// continues with remaining files.
99///
100/// # Behavior
101///
102/// - Subdirectories are skipped (not recursively processed)
103/// - Symbolic links are skipped
104/// - Empty directories return an empty rules vector
105/// - Files are processed in alphabetical order by filename
106/// - All successfully parsed rules are merged in order
107///
108/// # Examples
109///
110/// Loading a directory of magic files:
111///
112/// ```rust,no_run
113/// use libmagic_rs::parser::load_magic_directory;
114/// use std::path::Path;
115///
116/// let parsed = load_magic_directory(Path::new("/usr/share/file/magic.d"))?;
117/// println!("Loaded {} rules from directory", parsed.rules.len());
118/// # Ok::<(), libmagic_rs::ParseError>(())
119/// ```
120///
121/// Creating a Magdir-style directory structure:
122///
123/// ```rust,no_run
124/// use libmagic_rs::parser::load_magic_directory;
125/// use std::path::Path;
126///
127/// // Directory structure:
128/// // magic.d/
129/// // ├── 01-elf
130/// // ├── 02-archive
131/// // └── 03-text
132///
133/// let parsed = load_magic_directory(Path::new("./magic.d"))?;
134/// // Rules from all three files are merged in alphabetical order
135/// # Ok::<(), libmagic_rs::ParseError>(())
136/// ```
137///
138/// # Errors
139///
140/// Returns `ParseError` if:
141/// - The directory does not exist or cannot be accessed
142/// - Directory entries cannot be read
143/// - A file cannot be read due to I/O errors
144/// - A file contains invalid UTF-8 encoding
145///
146/// # Panics
147///
148/// This function does not panic under normal operation.
149pub fn load_magic_directory(dir_path: &Path) -> Result<ParsedMagic, ParseError> {
150 use std::fs;
151
152 // Read directory entries
153 let entries = fs::read_dir(dir_path).map_err(|e| {
154 ParseError::invalid_syntax(
155 0,
156 format!("Failed to read directory '{}': {}", dir_path.display(), e),
157 )
158 })?;
159
160 // Collect and sort entries by filename for deterministic ordering
161 let mut file_paths: Vec<std::path::PathBuf> = Vec::new();
162 for entry in entries {
163 let entry = entry.map_err(|e| {
164 ParseError::invalid_syntax(
165 0,
166 format!(
167 "Failed to read directory entry in '{}': {}",
168 dir_path.display(),
169 e
170 ),
171 )
172 })?;
173
174 let path = entry.path();
175 let file_type = entry.file_type().map_err(|e| {
176 ParseError::invalid_syntax(
177 0,
178 format!("Failed to read file type for '{}': {}", path.display(), e),
179 )
180 })?;
181
182 // Only process regular files, skip directories and symlinks.
183 // `is_file()` (not `!is_dir()`) is deliberate: sockets, FIFOs, and
184 // device nodes must also be excluded from magic-file discovery.
185 #[allow(clippy::filetype_is_file)]
186 if file_type.is_file() && !file_type.is_symlink() {
187 file_paths.push(path);
188 }
189 }
190
191 // Sort by filename for deterministic ordering
192 file_paths.sort_by_key(|path| path.file_name().map(std::ffi::OsStr::to_os_string));
193
194 // Accumulate rules and name tables from all files
195 let mut all_rules = Vec::new();
196 let mut merged_table = NameTable::empty();
197 let mut parse_failures: Vec<(PathBuf, ParseError)> = Vec::new();
198 let mut any_success = false;
199 let file_count = file_paths.len();
200
201 for path in file_paths {
202 // Read file contents (size-bounded to prevent memory exhaustion)
203 let contents = match read_magic_file_bounded(&path) {
204 Ok(contents) => contents,
205 Err(e) => {
206 // I/O errors (including oversized files) are critical
207 return Err(ParseError::invalid_syntax(
208 0,
209 format!("Failed to read file '{}': {}", path.display(), e),
210 ));
211 }
212 };
213
214 // Parse the file
215 match super::parse_text_magic_file(&contents) {
216 Ok(parsed) => {
217 any_success = true;
218 all_rules.extend(parsed.rules);
219 merged_table.merge(parsed.name_table);
220 }
221 Err(e) => {
222 // Track parse failures for reporting
223 parse_failures.push((path, e));
224 }
225 }
226 }
227
228 // If all files failed to parse, return an error.
229 // Use `any_success` rather than `all_rules.is_empty()` so that directories
230 // whose files parse successfully but contain only meta-type definitions
231 // (e.g. a directory of pure `name`-subroutine files) are not mistaken for
232 // complete failure.
233 if !any_success && !parse_failures.is_empty() {
234 use std::fmt::Write;
235
236 let failure_details: Vec<String> = parse_failures
237 .iter()
238 .take(3) // Limit to first 3 failures for brevity
239 .map(|(path, e)| format!(" - {}: {}", path.display(), e))
240 .collect();
241
242 let mut message = format!("All {file_count} magic file(s) in directory failed to parse");
243 if !failure_details.is_empty() {
244 message.push_str(":\n");
245 message.push_str(&failure_details.join("\n"));
246 if parse_failures.len() > 3 {
247 // fmt::Write to a String is infallible; discard the Result
248 // rather than unwrap so the no-panic policy holds regardless.
249 #[allow(clippy::let_underscore_must_use)]
250 let _ = write!(
251 message,
252 "\n ... and {} more",
253 parse_failures.len().saturating_sub(3)
254 );
255 }
256 }
257
258 return Err(ParseError::invalid_syntax(0, message));
259 }
260
261 // Log warnings for partial failures (some files parsed, some failed)
262 for (path, e) in &parse_failures {
263 warn!("Failed to parse '{}': {}", path.display(), e);
264 }
265
266 Ok(ParsedMagic {
267 rules: all_rules,
268 name_table: merged_table,
269 })
270}
271
272/// Loads magic rules from a file or directory, automatically detecting the format.
273///
274/// This is the unified entry point for loading magic rules from the filesystem. It
275/// automatically detects whether the path points to a text magic file, a directory
276/// containing magic files, or a binary compiled magic file, and dispatches to the
277/// appropriate handler.
278///
279/// # Format Detection and Handling
280///
281/// The function uses [`detect_format()`] to determine the file type and handles each
282/// format as follows:
283///
284/// - **Text format**: Reads the file contents and parses using [`super::parse_text_magic_file()`]
285/// - **Directory format**: Loads all magic files from the directory using [`load_magic_directory()`]
286/// - **Binary format**: Returns an error with guidance to use the `--use-builtin` option
287///
288/// # Arguments
289///
290/// * `path` - Path to a magic file or directory. Can be absolute or relative.
291///
292/// # Returns
293///
294/// Returns `Ok(Vec<MagicRule>)` containing all successfully parsed magic rules. For
295/// directories, rules from all files are merged in alphabetical order by filename.
296///
297/// # Errors
298///
299/// This function returns a [`ParseError`] in the following cases:
300///
301/// - **File not found**: The specified path does not exist
302/// - **Unsupported format**: The file is a binary compiled magic file (`.mgc`)
303/// - **Parse errors**: The magic file contains syntax errors or invalid rules
304/// - **I/O errors**: File system errors during reading (permissions, disk errors, etc.)
305///
306/// # Examples
307///
308/// ## Loading a text magic file
309///
310/// ```no_run
311/// use libmagic_rs::parser::load_magic_file;
312/// use std::path::Path;
313///
314/// let parsed = load_magic_file(Path::new("/usr/share/misc/magic"))?;
315/// println!("Loaded {} magic rules", parsed.rules.len());
316/// # Ok::<(), libmagic_rs::ParseError>(())
317/// ```
318///
319/// ## Loading a directory of magic files
320///
321/// ```no_run
322/// use libmagic_rs::parser::load_magic_file;
323/// use std::path::Path;
324///
325/// let parsed = load_magic_file(Path::new("/usr/share/misc/magic.d"))?;
326/// println!("Loaded {} rules from directory", parsed.rules.len());
327/// # Ok::<(), libmagic_rs::ParseError>(())
328/// ```
329///
330/// ## Handling binary format errors
331///
332/// ```no_run
333/// use libmagic_rs::parser::load_magic_file;
334/// use std::path::Path;
335///
336/// match load_magic_file(Path::new("/usr/share/misc/magic.mgc")) {
337/// Ok(parsed) => println!("Loaded {} rules", parsed.rules.len()),
338/// Err(e) => {
339/// eprintln!("Error loading magic file: {}", e);
340/// eprintln!("Hint: Use --use-builtin for binary files");
341/// }
342/// }
343/// # Ok::<(), libmagic_rs::ParseError>(())
344/// ```
345///
346/// # Security
347///
348/// This function delegates to [`super::parse_text_magic_file()`] or [`load_magic_directory()`]
349/// based on format detection. Security considerations are handled by those functions:
350///
351/// - Rule hierarchy depth is bounded during parsing
352/// - Invalid syntax is rejected with descriptive errors
353/// - Binary `.mgc` files are rejected (not parsed)
354///
355/// A 1 GB size limit (`MAX_MAGIC_FILE_SIZE`, module-internal) is enforced on each file loaded
356/// (both standalone files and files within a directory) to prevent memory
357/// exhaustion from maliciously oversized inputs. Files exceeding the limit are
358/// rejected with a `ParseError` before their contents are read.
359///
360/// # See Also
361///
362/// - [`detect_format()`] - Format detection logic
363/// - [`super::parse_text_magic_file()`] - Text file parser
364/// - [`load_magic_directory()`] - Directory loader
365pub fn load_magic_file(path: &Path) -> Result<ParsedMagic, ParseError> {
366 // Detect the magic file format
367 let format = detect_format(path)?;
368
369 // Dispatch to appropriate handler based on format
370 match format {
371 MagicFileFormat::Text => {
372 // Read file contents (size-bounded) and parse as text magic file
373 let content = read_magic_file_bounded(path)?;
374 super::parse_text_magic_file(&content)
375 }
376 MagicFileFormat::Directory => {
377 // Load all magic files from directory
378 load_magic_directory(path)
379 }
380 MagicFileFormat::Binary => {
381 // Binary compiled magic files are not supported
382 Err(ParseError::unsupported_format(
383 0,
384 "binary .mgc file",
385 "Binary compiled magic files (.mgc) are not supported for parsing.\n\
386 Use the --use-builtin option to use the built-in magic rules instead,\n\
387 or provide a text-based magic file or directory.",
388 ))
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 // Restriction lints without an allow-*-in-tests config option;
396 // tests create exactly one level under a fresh TempDir.
397 #![allow(clippy::create_dir)]
398
399 use super::*;
400
401 // ============================================================
402 // Tests for load_magic_directory (6+ test cases)
403 // ============================================================
404
405 #[test]
406 fn test_load_directory_critical_error_io() {
407 use std::path::Path;
408
409 let non_existent = Path::new("/this/should/not/exist/anywhere/at/all");
410 let result = load_magic_directory(non_existent);
411
412 assert!(
413 result.is_err(),
414 "Should return error for non-existent directory"
415 );
416 let err = result.unwrap_err();
417 assert!(err.to_string().contains("Failed to read directory"));
418 }
419
420 #[test]
421 fn test_load_directory_non_critical_error_parse() {
422 use std::fs;
423 use tempfile::TempDir;
424
425 let temp_dir = TempDir::new().expect("Failed to create temp dir");
426
427 // Create a valid file
428 let valid_path = temp_dir.path().join("valid.magic");
429 fs::write(&valid_path, "0 string \\x01\\x02 valid\n").expect("Failed to write valid file");
430
431 // Create an invalid file
432 let invalid_path = temp_dir.path().join("invalid.magic");
433 fs::write(&invalid_path, "this is invalid syntax\n").expect("Failed to write invalid file");
434
435 // Should succeed, loading only the valid file
436 let parsed = load_magic_directory(temp_dir.path()).expect("Should load valid files");
437
438 assert_eq!(parsed.rules.len(), 1, "Should load only valid file");
439 assert_eq!(parsed.rules[0].message, "valid");
440 }
441
442 #[test]
443 fn test_load_directory_empty_files() {
444 use std::fs;
445 use tempfile::TempDir;
446
447 let temp_dir = TempDir::new().expect("Failed to create temp dir");
448
449 // Create an empty file
450 let empty_path = temp_dir.path().join("empty.magic");
451 fs::write(&empty_path, "").expect("Failed to write empty file");
452
453 // Create a file with only comments
454 let comments_path = temp_dir.path().join("comments.magic");
455 fs::write(&comments_path, "# Just comments\n# Nothing else\n")
456 .expect("Failed to write comments file");
457
458 // Should succeed with no rules
459 let parsed = load_magic_directory(temp_dir.path()).expect("Should handle empty files");
460
461 assert_eq!(
462 parsed.rules.len(),
463 0,
464 "Empty files should contribute no rules"
465 );
466 }
467
468 #[test]
469 fn test_load_directory_binary_files() {
470 use std::fs;
471 use tempfile::TempDir;
472
473 let temp_dir = TempDir::new().expect("Failed to create temp dir");
474
475 // Create a binary file (invalid UTF-8). Lossy conversion turns this
476 // into U+FFFD characters that the grammar parser cannot interpret as
477 // a rule; the directory loader treats that as a non-critical parse
478 // failure and skips the file.
479 let binary_path = temp_dir.path().join("binary.dat");
480 fs::write(&binary_path, [0xFF, 0xFE, 0xFF, 0xFE]).expect("Failed to write binary file");
481
482 // Create a valid text file
483 let valid_path = temp_dir.path().join("valid.magic");
484 fs::write(&valid_path, "0 string \\x01\\x02 valid\n").expect("Failed to write valid file");
485
486 let parsed = load_magic_directory(temp_dir.path())
487 .expect("Directory with a binary file alongside a valid file should still load");
488
489 assert_eq!(
490 parsed.rules.len(),
491 1,
492 "Only the valid magic file should contribute rules"
493 );
494 assert_eq!(parsed.rules[0].message, "valid");
495 }
496
497 #[test]
498 fn test_load_directory_mixed_extensions() {
499 use std::fs;
500 use tempfile::TempDir;
501
502 let temp_dir = TempDir::new().expect("Failed to create temp dir");
503
504 // Create files with different extensions
505 fs::write(
506 temp_dir.path().join("file.magic"),
507 "0 string \\x01\\x02 magic\n",
508 )
509 .expect("Failed to write .magic file");
510 fs::write(
511 temp_dir.path().join("file.txt"),
512 "0 string \\x03\\x04 txt\n",
513 )
514 .expect("Failed to write .txt file");
515 fs::write(temp_dir.path().join("noext"), "0 string \\x05\\x06 noext\n")
516 .expect("Failed to write no-ext file");
517
518 let parsed = load_magic_directory(temp_dir.path())
519 .expect("Should load all files regardless of extension");
520
521 assert_eq!(
522 parsed.rules.len(),
523 3,
524 "Should process all files regardless of extension"
525 );
526
527 let messages: Vec<&str> = parsed.rules.iter().map(|r| r.message.as_str()).collect();
528 assert!(messages.contains(&"magic"));
529 assert!(messages.contains(&"txt"));
530 assert!(messages.contains(&"noext"));
531 }
532
533 #[test]
534 fn test_load_directory_alphabetical_ordering() {
535 use std::fs;
536 use tempfile::TempDir;
537
538 let temp_dir = TempDir::new().expect("Failed to create temp dir");
539
540 // Create files in non-alphabetical order - using valid magic syntax with hex escapes
541 fs::write(
542 temp_dir.path().join("03-third"),
543 "0 string \\x07\\x08\\x09 third\n",
544 )
545 .expect("Failed to write third file");
546 fs::write(
547 temp_dir.path().join("01-first"),
548 "0 string \\x01\\x02\\x03 first\n",
549 )
550 .expect("Failed to write first file");
551 fs::write(
552 temp_dir.path().join("02-second"),
553 "0 string \\x04\\x05\\x06 second\n",
554 )
555 .expect("Failed to write second file");
556
557 let parsed = load_magic_directory(temp_dir.path()).expect("Should load directory in order");
558
559 assert_eq!(parsed.rules.len(), 3);
560 // Should be sorted alphabetically by filename
561 assert_eq!(parsed.rules[0].message, "first");
562 assert_eq!(parsed.rules[1].message, "second");
563 assert_eq!(parsed.rules[2].message, "third");
564 }
565
566 // ============================================================
567 // Tests for load_magic_file (5+ test cases)
568 // ============================================================
569
570 #[test]
571 fn test_load_magic_file_text_format() {
572 use std::fs;
573 use tempfile::TempDir;
574
575 let temp_dir = TempDir::new().expect("Failed to create temp dir");
576 let magic_file = temp_dir.path().join("magic.txt");
577
578 // Create text magic file with valid content
579 fs::write(&magic_file, "0 string \\x7fELF ELF executable\n")
580 .expect("Failed to write magic file");
581
582 // Load using load_magic_file
583 let parsed = load_magic_file(&magic_file).expect("Failed to load text magic file");
584
585 assert_eq!(parsed.rules.len(), 1);
586 assert_eq!(parsed.rules[0].message, "ELF executable");
587 }
588
589 #[test]
590 fn test_load_magic_file_directory_format() {
591 use std::fs;
592 use tempfile::TempDir;
593
594 let temp_dir = TempDir::new().expect("Failed to create temp dir");
595 let magic_dir = temp_dir.path().join("magic.d");
596 fs::create_dir(&magic_dir).expect("Failed to create magic directory");
597
598 // Create multiple files in directory
599 fs::write(
600 magic_dir.join("00_elf"),
601 "0 string \\x7fELF ELF executable\n",
602 )
603 .expect("Failed to write elf file");
604 fs::write(
605 magic_dir.join("01_zip"),
606 "0 string \\x50\\x4b\\x03\\x04 ZIP archive\n",
607 )
608 .expect("Failed to write zip file");
609
610 // Load using load_magic_file
611 let parsed = load_magic_file(&magic_dir).expect("Failed to load directory");
612
613 assert_eq!(parsed.rules.len(), 2);
614 assert_eq!(parsed.rules[0].message, "ELF executable");
615 assert_eq!(parsed.rules[1].message, "ZIP archive");
616 }
617
618 #[test]
619 fn test_load_magic_file_binary_format_error() {
620 use std::fs::File;
621 use std::io::Write;
622 use tempfile::TempDir;
623
624 let temp_dir = TempDir::new().expect("Failed to create temp dir");
625 let binary_file = temp_dir.path().join("magic.mgc");
626
627 // Create binary file with .mgc magic number
628 let mut file = File::create(&binary_file).expect("Failed to create binary file");
629 let magic_number: [u8; 4] = [0x1C, 0x04, 0x1E, 0xF1]; // Little-endian 0xF11E041C
630 file.write_all(&magic_number)
631 .expect("Failed to write magic number");
632
633 // Attempt to load binary file
634 let result = load_magic_file(&binary_file);
635
636 assert!(result.is_err(), "Should fail to load binary .mgc file");
637
638 let error = result.unwrap_err();
639 let error_msg = error.to_string();
640
641 // Verify error mentions unsupported format and --use-builtin
642 assert!(
643 error_msg.contains("Binary") || error_msg.contains("binary"),
644 "Error should mention binary format: {error_msg}",
645 );
646 assert!(
647 error_msg.contains("--use-builtin") || error_msg.contains("built-in"),
648 "Error should mention --use-builtin option: {error_msg}",
649 );
650 }
651
652 #[test]
653 fn test_load_magic_file_io_error() {
654 use std::path::Path;
655
656 // Try to load non-existent file
657 let non_existent = Path::new("/this/path/should/not/exist/magic.txt");
658 let result = load_magic_file(non_existent);
659
660 assert!(result.is_err(), "Should fail for non-existent file");
661 }
662
663 #[test]
664 fn test_load_magic_file_parse_error_propagation() {
665 use std::fs;
666 use tempfile::TempDir;
667
668 let temp_dir = TempDir::new().expect("Failed to create temp dir");
669 let invalid_file = temp_dir.path().join("invalid.magic");
670
671 // Create file with invalid syntax (missing offset)
672 fs::write(&invalid_file, "string test invalid\n").expect("Failed to write invalid file");
673
674 // Attempt to load file with parse errors
675 let result = load_magic_file(&invalid_file);
676
677 assert!(result.is_err(), "Should fail for file with parse errors");
678
679 // Error should be a parse error (not I/O error)
680 let error = result.unwrap_err();
681 let error_msg = format!("{error:?}");
682 assert!(
683 error_msg.contains("InvalidSyntax") || error_msg.contains("syntax"),
684 "Error should be parse error: {error_msg}",
685 );
686 }
687
688 #[test]
689 fn test_max_magic_file_size_matches_file_buffer_limit() {
690 // Ensure the duplicated limit stays in sync with FileBuffer::MAX_FILE_SIZE.
691 // loader.rs cannot `use crate::io::FileBuffer` at module scope because
692 // build.rs pulls this file in via `#[path]`, but tests compile as part
693 // of the library and can reach it fine.
694 assert_eq!(
695 MAX_MAGIC_FILE_SIZE,
696 crate::io::FileBuffer::MAX_FILE_SIZE,
697 "MAX_MAGIC_FILE_SIZE must match FileBuffer::MAX_FILE_SIZE"
698 );
699 }
700
701 #[test]
702 fn test_load_magic_file_rejects_oversized_file() {
703 use std::fs::File;
704 use tempfile::TempDir;
705
706 let temp_dir = TempDir::new().expect("Failed to create temp dir");
707 let oversized = temp_dir.path().join("huge.magic");
708
709 // Create a sparse file whose reported size exceeds MAX_MAGIC_FILE_SIZE
710 // without actually consuming that much disk space.
711 let file = File::create(&oversized).expect("Failed to create oversized file");
712 file.set_len(MAX_MAGIC_FILE_SIZE + 1)
713 .expect("Failed to set sparse file length");
714 drop(file);
715
716 let result = load_magic_file(&oversized);
717
718 assert!(
719 result.is_err(),
720 "Loading a file larger than MAX_MAGIC_FILE_SIZE must fail"
721 );
722
723 let err_msg = result.unwrap_err().to_string();
724 assert!(
725 err_msg.contains("too large"),
726 "Error should indicate size limit violation, got: {err_msg}"
727 );
728 assert!(
729 err_msg.contains(&MAX_MAGIC_FILE_SIZE.to_string()),
730 "Error should mention the maximum allowed size, got: {err_msg}"
731 );
732 }
733
734 #[test]
735 fn test_load_magic_file_tolerates_non_utf8_in_comment() {
736 // Regression: /usr/share/file/magic/filesystems on macOS contains a
737 // Latin-1 `ß` (0xdf) in a contributor attribution comment. Previously
738 // this was rejected by `fs::read_to_string` with an opaque "stream
739 // did not contain valid UTF-8" error. The loader must now tolerate
740 // non-UTF-8 bytes in comments (and anywhere else they appear) by
741 // lossily replacing them.
742 use std::fs;
743 use tempfile::TempDir;
744
745 let temp_dir = TempDir::new().expect("Failed to create temp dir");
746 let magic_path = temp_dir.path().join("with-latin1-comment.magic");
747
748 let mut bytes: Vec<u8> = Vec::new();
749 bytes.extend_from_slice(b"# From: Thomas Wei");
750 bytes.push(0xdf); // invalid UTF-8 (Latin-1 encoding of `ß`)
751 bytes.extend_from_slice(b"schuh <thomas@example.invalid>\n");
752 bytes.extend_from_slice(b"0 string \\x7fELF ELF executable\n");
753 fs::write(&magic_path, &bytes).expect("Failed to write magic file with non-UTF-8 byte");
754
755 let parsed = load_magic_file(&magic_path)
756 .expect("Magic file with non-UTF-8 bytes in a comment must still load");
757
758 assert_eq!(
759 parsed.rules.len(),
760 1,
761 "The ELF rule should be parsed; the comment is stripped"
762 );
763 assert_eq!(parsed.rules[0].message, "ELF executable");
764 }
765
766 #[test]
767 fn test_load_directory_merges_name_tables() {
768 use std::fs;
769 use tempfile::TempDir;
770
771 let temp_dir = TempDir::new().expect("Failed to create temp dir");
772
773 // Each file defines a different named subroutine.
774 fs::write(
775 temp_dir.path().join("00_first"),
776 "0 name sub_a\n>0 byte 1 a-body\n",
777 )
778 .expect("Failed to write sub_a file");
779 fs::write(
780 temp_dir.path().join("01_second"),
781 "0 name sub_b\n>0 byte 2 b-body\n",
782 )
783 .expect("Failed to write sub_b file");
784
785 let parsed =
786 load_magic_directory(temp_dir.path()).expect("Should load both name subroutines");
787
788 // Both `name` rules are hoisted out, so top-level rules list is empty.
789 assert_eq!(parsed.rules.len(), 0);
790 assert!(parsed.name_table.get("sub_a").is_some());
791 assert!(parsed.name_table.get("sub_b").is_some());
792 }
793}