1use log::warn;
10
11use crate::error::ParseError;
12use crate::parser::ParsedMagic;
13use crate::parser::name_table::NameTable;
14use std::io::Read;
15use std::path::{Path, PathBuf};
16
17use super::format::{MagicFileFormat, detect_format, has_binary_magic_header};
18
19pub const MAX_MAGIC_FILE_SIZE: u64 = 1024 * 1024 * 1024;
31
32fn read_magic_file_bounded(path: &Path) -> Result<String, ParseError> {
49 let metadata = std::fs::metadata(path).map_err(|e| {
50 ParseError::IoError(std::io::Error::new(
51 e.kind(),
52 format!("Failed to read metadata for '{}': {}", path.display(), e),
53 ))
54 })?;
55
56 if metadata.len() > MAX_MAGIC_FILE_SIZE {
57 return Err(ParseError::invalid_syntax(
58 0,
59 format!(
60 "Magic file '{}' is too large: {} bytes (maximum allowed: {} bytes)",
61 path.display(),
62 metadata.len(),
63 MAX_MAGIC_FILE_SIZE
64 ),
65 ));
66 }
67
68 let bytes = std::fs::read(path).map_err(ParseError::from)?;
69
70 Ok(decode_magic_bytes(bytes, Some(path)))
71}
72
73fn reader_io_error(error: &std::io::Error) -> ParseError {
78 ParseError::IoError(std::io::Error::new(
79 error.kind(),
80 format!("Failed to read magic database from reader: {error}"),
81 ))
82}
83
84fn read_magic_reader_bounded<R: Read>(reader: R) -> Result<String, ParseError> {
85 read_magic_reader_with_limit(reader, MAX_MAGIC_FILE_SIZE)
86}
87
88fn read_magic_reader_with_limit<R: Read>(reader: R, max_size: u64) -> Result<String, ParseError> {
89 let read_limit = max_size.checked_add(1).ok_or_else(|| {
93 ParseError::invalid_syntax(0, "Magic database input size limit is too large")
94 })?;
95 let mut reader = reader.take(read_limit);
96 let mut bytes = Vec::new();
97 reader
98 .by_ref()
99 .take(4)
100 .read_to_end(&mut bytes)
101 .map_err(|error| reader_io_error(&error))?;
102 if has_binary_magic_header(&bytes) {
104 return Err(unsupported_binary_magic_error());
105 }
106 reader
107 .read_to_end(&mut bytes)
108 .map_err(|error| reader_io_error(&error))?;
109 decode_magic_bytes_with_limit(bytes, max_size)
110}
111
112fn decode_magic_bytes_bounded(bytes: Vec<u8>) -> Result<String, ParseError> {
113 decode_magic_bytes_with_limit(bytes, MAX_MAGIC_FILE_SIZE)
114}
115
116fn decode_magic_bytes_with_limit(bytes: Vec<u8>, max_size: u64) -> Result<String, ParseError> {
117 if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > max_size {
118 return Err(ParseError::invalid_syntax(
119 0,
120 format!("Magic database input is too large: more than {max_size} bytes"),
121 ));
122 }
123 if has_binary_magic_header(&bytes) {
124 return Err(unsupported_binary_magic_error());
125 }
126 Ok(decode_magic_bytes(bytes, None))
127}
128
129fn decode_magic_bytes(bytes: Vec<u8>, source: Option<&Path>) -> String {
130 match String::from_utf8(bytes) {
131 Ok(content) => content,
132 Err(error) => {
133 if let Some(path) = source {
134 warn!(
135 "Magic file '{}' contains non-UTF-8 bytes; they were replaced with U+FFFD. \
136 Rule parsing proceeds, but replacements inside rule bodies may alter matching.",
137 path.display()
138 );
139 } else {
140 warn!(
141 "Magic database input contains non-UTF-8 bytes; they were replaced with U+FFFD. \
142 Rule parsing proceeds, but replacements inside rule bodies may alter matching."
143 );
144 }
145 String::from_utf8_lossy(&error.into_bytes()).into_owned()
146 }
147 }
148}
149
150fn unsupported_binary_magic_error() -> ParseError {
151 ParseError::unsupported_format(
152 0,
153 "binary .mgc file",
154 "Binary compiled magic files (.mgc) are not supported for parsing.\n\
155 Use the --use-builtin option to use the built-in magic rules instead,\n\
156 or provide a text-based magic file or directory.",
157 )
158}
159
160fn has_rule_lines(contents: &str) -> bool {
169 contents.lines().any(|line| {
170 let trimmed = line.trim();
171 !trimmed.is_empty() && !trimmed.starts_with('#') && !trimmed.starts_with("!:")
172 })
173}
174
175#[allow(clippy::too_many_lines)]
245pub fn load_magic_directory(dir_path: &Path) -> Result<ParsedMagic, ParseError> {
246 use std::fs;
247
248 let entries = fs::read_dir(dir_path).map_err(|e| {
250 ParseError::invalid_syntax(
251 0,
252 format!("Failed to read directory '{}': {}", dir_path.display(), e),
253 )
254 })?;
255
256 let mut file_paths: Vec<std::path::PathBuf> = Vec::new();
258 for entry in entries {
259 let entry = entry.map_err(|e| {
260 ParseError::invalid_syntax(
261 0,
262 format!(
263 "Failed to read directory entry in '{}': {}",
264 dir_path.display(),
265 e
266 ),
267 )
268 })?;
269
270 let path = entry.path();
271 let file_type = entry.file_type().map_err(|e| {
272 ParseError::invalid_syntax(
273 0,
274 format!("Failed to read file type for '{}': {}", path.display(), e),
275 )
276 })?;
277
278 #[allow(clippy::filetype_is_file)]
282 if file_type.is_file() && !file_type.is_symlink() {
283 file_paths.push(path);
284 }
285 }
286
287 file_paths.sort_by_key(|path| path.file_name().map(std::ffi::OsStr::to_os_string));
289
290 let mut all_rules = Vec::new();
292 let mut merged_table = NameTable::empty();
293 let mut parse_failures: Vec<(PathBuf, ParseError)> = Vec::new();
294 let mut empty_files: Vec<PathBuf> = Vec::new();
299 let mut any_success = false;
300 let file_count = file_paths.len();
301
302 for path in file_paths {
303 let contents = match read_magic_file_bounded(&path) {
305 Ok(contents) => contents,
306 Err(e) => {
307 return Err(ParseError::invalid_syntax(
309 0,
310 format!("Failed to read file '{}': {}", path.display(), e),
311 ));
312 }
313 };
314
315 match super::parse_text_magic_file_tolerant(&contents, Some(&path)) {
318 Ok(parsed) => {
319 if parsed.rules.is_empty() && parsed.name_table.is_empty() {
320 if has_rule_lines(&contents) {
327 empty_files.push(path);
328 }
329 } else {
330 any_success = true;
331 all_rules.extend(parsed.rules);
332 merged_table.merge(parsed.name_table);
333 }
334 }
335 Err(e) => {
336 parse_failures.push((path, e));
338 }
339 }
340 }
341
342 if !any_success && (!parse_failures.is_empty() || !empty_files.is_empty()) {
351 use std::fmt::Write;
352
353 let mut problems: Vec<String> = parse_failures
354 .iter()
355 .map(|(path, e)| format!(" - {}: {}", path.display(), e))
356 .collect();
357 problems.extend(
358 empty_files
359 .iter()
360 .map(|path| format!(" - {}: no usable rules (all skipped)", path.display())),
361 );
362
363 let mut message = format!(
364 "All {file_count} magic file(s) in directory failed to parse or produced no usable rules"
365 );
366 let shown = problems.iter().take(3).cloned().collect::<Vec<_>>();
367 if !shown.is_empty() {
368 message.push_str(":\n");
369 message.push_str(&shown.join("\n"));
370 if problems.len() > 3 {
371 #[allow(clippy::let_underscore_must_use)]
374 let _ = write!(
375 message,
376 "\n ... and {} more",
377 problems.len().saturating_sub(3)
378 );
379 }
380 }
381
382 return Err(ParseError::invalid_syntax(0, message));
383 }
384
385 for (path, e) in &parse_failures {
387 warn!("Failed to parse '{}': {}", path.display(), e);
388 }
389
390 Ok(ParsedMagic {
391 rules: all_rules,
392 name_table: merged_table,
393 })
394}
395
396pub fn load_magic_file(path: &Path) -> Result<ParsedMagic, ParseError> {
490 let format = detect_format(path)?;
492
493 match format {
495 MagicFileFormat::Text => {
496 let content = read_magic_file_bounded(path)?;
498 super::parse_text_magic_file_tolerant(&content, Some(path))
499 }
500 MagicFileFormat::Directory => {
501 load_magic_directory(path)
503 }
504 MagicFileFormat::Binary => {
505 Err(unsupported_binary_magic_error())
507 }
508 }
509}
510
511pub(crate) fn load_magic_reader<R: Read>(reader: R) -> Result<ParsedMagic, ParseError> {
513 let content = read_magic_reader_bounded(reader)?;
514 super::parse_text_magic_file_tolerant(&content, None)
515}
516
517pub(crate) fn load_magic_bytes(bytes: Vec<u8>) -> Result<ParsedMagic, ParseError> {
519 let content = decode_magic_bytes_bounded(bytes)?;
520 super::parse_text_magic_file_tolerant(&content, None)
521}
522
523#[cfg(test)]
524mod tests {
525 #![allow(clippy::create_dir)]
528
529 use super::*;
530
531 #[test]
532 fn test_read_magic_reader_is_bounded() {
533 let content = read_magic_reader_with_limit(&b"1234"[..], 4)
534 .expect("reader input at the limit must be accepted");
535 assert_eq!(content, "1234");
536
537 let error = read_magic_reader_with_limit(&b"12345"[..], 4)
538 .expect_err("reader input above the limit must fail");
539
540 assert!(matches!(error, ParseError::InvalidSyntax { line: 0, .. }));
541 assert!(error.to_string().contains("more than 4 bytes"));
542 }
543
544 #[test]
545 fn test_decode_magic_bytes_is_bounded() {
546 let content = decode_magic_bytes_with_limit(b"1234".to_vec(), 4)
547 .expect("owned bytes at the limit must be accepted");
548 assert_eq!(content, "1234");
549
550 let oversized = decode_magic_bytes_with_limit(b"12345".to_vec(), 4)
551 .expect_err("owned bytes above the limit must fail");
552 assert!(matches!(
553 oversized,
554 ParseError::InvalidSyntax { line: 0, .. }
555 ));
556 assert!(oversized.to_string().contains("more than 4 bytes"));
557 }
558
559 #[test]
564 fn test_load_directory_critical_error_io() {
565 use std::path::Path;
566
567 let non_existent = Path::new("/this/should/not/exist/anywhere/at/all");
568 let result = load_magic_directory(non_existent);
569
570 assert!(
571 result.is_err(),
572 "Should return error for non-existent directory"
573 );
574 let err = result.unwrap_err();
575 assert!(err.to_string().contains("Failed to read directory"));
576 }
577
578 #[test]
579 fn test_load_directory_non_critical_error_parse() {
580 use std::fs;
581 use tempfile::TempDir;
582
583 let temp_dir = TempDir::new().expect("Failed to create temp dir");
584
585 let valid_path = temp_dir.path().join("valid.magic");
587 fs::write(&valid_path, "0 string \\x01\\x02 valid\n").expect("Failed to write valid file");
588
589 let invalid_path = temp_dir.path().join("invalid.magic");
591 fs::write(&invalid_path, "this is invalid syntax\n").expect("Failed to write invalid file");
592
593 let parsed = load_magic_directory(temp_dir.path()).expect("Should load valid files");
595
596 assert_eq!(parsed.rules.len(), 1, "Should load only valid file");
597 assert_eq!(parsed.rules[0].message, "valid");
598 }
599
600 #[test]
601 fn test_load_directory_empty_files() {
602 use std::fs;
603 use tempfile::TempDir;
604
605 let temp_dir = TempDir::new().expect("Failed to create temp dir");
606
607 let empty_path = temp_dir.path().join("empty.magic");
609 fs::write(&empty_path, "").expect("Failed to write empty file");
610
611 let comments_path = temp_dir.path().join("comments.magic");
613 fs::write(&comments_path, "# Just comments\n# Nothing else\n")
614 .expect("Failed to write comments file");
615
616 let parsed = load_magic_directory(temp_dir.path()).expect("Should handle empty files");
618
619 assert_eq!(
620 parsed.rules.len(),
621 0,
622 "Empty files should contribute no rules"
623 );
624 }
625
626 #[test]
627 fn test_load_directory_all_content_bearing_but_all_rules_skipped_errors() {
628 use std::fs;
629 use tempfile::TempDir;
630
631 let temp_dir = TempDir::new().expect("Failed to create temp dir");
632
633 fs::write(
644 temp_dir.path().join("bad1.magic"),
645 "notanoffset badtype whatever\nalso not a valid rule line\n",
646 )
647 .expect("Failed to write bad1");
648 fs::write(
649 temp_dir.path().join("bad2.magic"),
650 "still invalid syntax here\n",
651 )
652 .expect("Failed to write bad2");
653
654 let err = load_magic_directory(temp_dir.path()).expect_err(
655 "a directory whose content-bearing files all parse to zero rules must fail",
656 );
657 let msg = err.to_string();
658 assert!(
659 msg.contains("failed to parse"),
660 "error must report the all-failed contract: {msg}"
661 );
662 assert!(
663 msg.contains("no usable rules (all skipped)"),
664 "error must attribute the content-bearing-but-all-skipped files: {msg}"
665 );
666 }
667
668 #[test]
669 fn test_load_directory_binary_files() {
670 use std::fs;
671 use tempfile::TempDir;
672
673 let temp_dir = TempDir::new().expect("Failed to create temp dir");
674
675 let binary_path = temp_dir.path().join("binary.dat");
680 fs::write(&binary_path, [0xFF, 0xFE, 0xFF, 0xFE]).expect("Failed to write binary file");
681
682 let valid_path = temp_dir.path().join("valid.magic");
684 fs::write(&valid_path, "0 string \\x01\\x02 valid\n").expect("Failed to write valid file");
685
686 let parsed = load_magic_directory(temp_dir.path())
687 .expect("Directory with a binary file alongside a valid file should still load");
688
689 assert_eq!(
690 parsed.rules.len(),
691 1,
692 "Only the valid magic file should contribute rules"
693 );
694 assert_eq!(parsed.rules[0].message, "valid");
695 }
696
697 #[test]
698 fn test_load_directory_mixed_extensions() {
699 use std::fs;
700 use tempfile::TempDir;
701
702 let temp_dir = TempDir::new().expect("Failed to create temp dir");
703
704 fs::write(
706 temp_dir.path().join("file.magic"),
707 "0 string \\x01\\x02 magic\n",
708 )
709 .expect("Failed to write .magic file");
710 fs::write(
711 temp_dir.path().join("file.txt"),
712 "0 string \\x03\\x04 txt\n",
713 )
714 .expect("Failed to write .txt file");
715 fs::write(temp_dir.path().join("noext"), "0 string \\x05\\x06 noext\n")
716 .expect("Failed to write no-ext file");
717
718 let parsed = load_magic_directory(temp_dir.path())
719 .expect("Should load all files regardless of extension");
720
721 assert_eq!(
722 parsed.rules.len(),
723 3,
724 "Should process all files regardless of extension"
725 );
726
727 let messages: Vec<&str> = parsed.rules.iter().map(|r| r.message.as_str()).collect();
728 assert!(messages.contains(&"magic"));
729 assert!(messages.contains(&"txt"));
730 assert!(messages.contains(&"noext"));
731 }
732
733 #[test]
734 fn test_load_directory_alphabetical_ordering() {
735 use std::fs;
736 use tempfile::TempDir;
737
738 let temp_dir = TempDir::new().expect("Failed to create temp dir");
739
740 fs::write(
742 temp_dir.path().join("03-third"),
743 "0 string \\x07\\x08\\x09 third\n",
744 )
745 .expect("Failed to write third file");
746 fs::write(
747 temp_dir.path().join("01-first"),
748 "0 string \\x01\\x02\\x03 first\n",
749 )
750 .expect("Failed to write first file");
751 fs::write(
752 temp_dir.path().join("02-second"),
753 "0 string \\x04\\x05\\x06 second\n",
754 )
755 .expect("Failed to write second file");
756
757 let parsed = load_magic_directory(temp_dir.path()).expect("Should load directory in order");
758
759 assert_eq!(parsed.rules.len(), 3);
760 assert_eq!(parsed.rules[0].message, "first");
762 assert_eq!(parsed.rules[1].message, "second");
763 assert_eq!(parsed.rules[2].message, "third");
764 }
765
766 #[test]
771 fn test_load_magic_file_text_format() {
772 use std::fs;
773 use tempfile::TempDir;
774
775 let temp_dir = TempDir::new().expect("Failed to create temp dir");
776 let magic_file = temp_dir.path().join("magic.txt");
777
778 fs::write(&magic_file, "0 string \\x7fELF ELF executable\n")
780 .expect("Failed to write magic file");
781
782 let parsed = load_magic_file(&magic_file).expect("Failed to load text magic file");
784
785 assert_eq!(parsed.rules.len(), 1);
786 assert_eq!(parsed.rules[0].message, "ELF executable");
787 }
788
789 #[test]
790 fn test_load_magic_file_directory_format() {
791 use std::fs;
792 use tempfile::TempDir;
793
794 let temp_dir = TempDir::new().expect("Failed to create temp dir");
795 let magic_dir = temp_dir.path().join("magic.d");
796 fs::create_dir(&magic_dir).expect("Failed to create magic directory");
797
798 fs::write(
800 magic_dir.join("00_elf"),
801 "0 string \\x7fELF ELF executable\n",
802 )
803 .expect("Failed to write elf file");
804 fs::write(
805 magic_dir.join("01_zip"),
806 "0 string \\x50\\x4b\\x03\\x04 ZIP archive\n",
807 )
808 .expect("Failed to write zip file");
809
810 let parsed = load_magic_file(&magic_dir).expect("Failed to load directory");
812
813 assert_eq!(parsed.rules.len(), 2);
814 assert_eq!(parsed.rules[0].message, "ELF executable");
815 assert_eq!(parsed.rules[1].message, "ZIP archive");
816 }
817
818 #[test]
819 fn test_load_magic_file_binary_format_error() {
820 use std::fs::File;
821 use std::io::Write;
822 use tempfile::TempDir;
823
824 let temp_dir = TempDir::new().expect("Failed to create temp dir");
825 let binary_file = temp_dir.path().join("magic.mgc");
826
827 let mut file = File::create(&binary_file).expect("Failed to create binary file");
829 let magic_number: [u8; 4] = [0x1C, 0x04, 0x1E, 0xF1]; file.write_all(&magic_number)
831 .expect("Failed to write magic number");
832
833 let result = load_magic_file(&binary_file);
835
836 assert!(result.is_err(), "Should fail to load binary .mgc file");
837
838 let error = result.unwrap_err();
839 let error_msg = error.to_string();
840
841 assert!(
843 error_msg.contains("Binary") || error_msg.contains("binary"),
844 "Error should mention binary format: {error_msg}",
845 );
846 assert!(
847 error_msg.contains("--use-builtin") || error_msg.contains("built-in"),
848 "Error should mention --use-builtin option: {error_msg}",
849 );
850 }
851
852 #[test]
853 fn test_load_magic_file_io_error() {
854 use std::path::Path;
855
856 let non_existent = Path::new("/this/path/should/not/exist/magic.txt");
858 let result = load_magic_file(non_existent);
859
860 assert!(result.is_err(), "Should fail for non-existent file");
861 }
862
863 #[test]
864 fn test_load_magic_file_tolerates_unparseable_rule_and_keeps_valid_ones() {
865 use std::fs;
866 use tempfile::TempDir;
867
868 let temp_dir = TempDir::new().expect("Failed to create temp dir");
869 let mixed_file = temp_dir.path().join("mixed.magic");
870
871 fs::write(
878 &mixed_file,
879 "0 string GOOD1 first good rule\nstring test invalid\n0 string GOOD2 second good rule\n",
880 )
881 .expect("Failed to write file");
882
883 let parsed = load_magic_file(&mixed_file)
884 .expect("runtime load must tolerate an unparseable rule, not abort the whole file");
885 let msgs: Vec<&str> = parsed.rules.iter().map(|r| r.message.as_str()).collect();
886 assert!(
887 msgs.contains(&"first good rule"),
888 "a valid rule before the bad one must survive: {msgs:?}"
889 );
890 assert!(
891 msgs.contains(&"second good rule"),
892 "a valid rule after the bad one must survive: {msgs:?}"
893 );
894 assert_eq!(
895 parsed.rules.len(),
896 2,
897 "the unparseable rule must be dropped, keeping exactly the two valid ones: {msgs:?}"
898 );
899 }
900
901 #[test]
902 fn test_tolerant_skip_warning_includes_source_file_path() {
903 use std::fs;
904 use tempfile::TempDir;
905
906 let temp_dir = TempDir::new().expect("Failed to create temp dir");
911 let bad_file = temp_dir.path().join("has_bad_rule.magic");
912 fs::write(&bad_file, "0 string GOOD good rule\nstring test invalid\n")
913 .expect("Failed to write file");
914
915 testing_logger::setup();
916 let _ = load_magic_file(&bad_file).expect("tolerant load must not abort");
917 let path_str = bad_file.display().to_string();
918 testing_logger::validate(|captured_logs| {
919 let skip_warns: Vec<_> = captured_logs
920 .iter()
921 .filter(|l| l.body.contains("skipping unparseable magic rule"))
922 .collect();
923 assert_eq!(
924 skip_warns.len(),
925 1,
926 "expected exactly one skip warning, got: {:?}",
927 captured_logs.iter().map(|l| &l.body).collect::<Vec<_>>()
928 );
929 assert_eq!(skip_warns[0].level, log::Level::Warn);
930 assert!(
931 skip_warns[0].body.contains(&path_str),
932 "skip warning must include the source file path '{path_str}', got: {}",
933 skip_warns[0].body
934 );
935 });
936 }
937
938 #[test]
939 fn test_tolerant_skip_warning_omits_path_clause_when_source_is_none() {
940 testing_logger::setup();
945 let ParsedMagic { rules, .. } = super::super::parse_text_magic_file_tolerant(
946 "0 string GOOD good\nstring test bad\n",
947 None,
948 )
949 .expect("tolerant parse must not abort");
950 assert_eq!(
951 rules.len(),
952 1,
953 "the good rule survives, the bad one is dropped"
954 );
955 testing_logger::validate(|captured_logs| {
956 let skip_warns: Vec<_> = captured_logs
957 .iter()
958 .filter(|l| l.body.contains("skipping unparseable magic rule"))
959 .collect();
960 assert_eq!(skip_warns.len(), 1);
961 assert!(
965 !skip_warns[0].body.contains("magic rule in "),
966 "with source=None the warning must carry no ' in <path>' clause, got: {}",
967 skip_warns[0].body
968 );
969 });
970 }
971
972 #[test]
973 fn test_load_magic_file_drops_subtree_of_unparseable_rule_without_reattaching() {
974 use std::fs;
975 use tempfile::TempDir;
976
977 let temp_dir = TempDir::new().expect("Failed to create temp dir");
978 let file = temp_dir.path().join("subtree.magic");
979
980 fs::write(
989 &file,
990 "0 string GOOD1 parent rule\n\
991 notanoffset badtype orphan parent\n\
992 >0 byte x orphaned child that must be dropped\n\
993 0 string GOOD2 sibling after the dropped subtree\n",
994 )
995 .expect("Failed to write file");
996
997 let parsed = load_magic_file(&file)
998 .expect("runtime load must tolerate the unparseable rule and its subtree");
999
1000 let top_msgs: Vec<&str> = parsed.rules.iter().map(|r| r.message.as_str()).collect();
1001 assert_eq!(
1002 parsed.rules.len(),
1003 2,
1004 "exactly the two valid top-level rules survive: {top_msgs:?}"
1005 );
1006 assert!(
1007 top_msgs.contains(&"parent rule"),
1008 "the valid rule before the bad one must survive: {top_msgs:?}"
1009 );
1010 assert!(
1011 top_msgs.contains(&"sibling after the dropped subtree"),
1012 "the sibling after the dropped subtree must parse (threshold reset): {top_msgs:?}"
1013 );
1014
1015 let good1 = parsed
1018 .rules
1019 .iter()
1020 .find(|r| r.message == "parent rule")
1021 .expect("GOOD1 must be present");
1022 assert!(
1023 good1.children.is_empty(),
1024 "the dropped child must not re-attach to the previous level-0 rule: {:?}",
1025 good1
1026 .children
1027 .iter()
1028 .map(|c| c.message.as_str())
1029 .collect::<Vec<_>>()
1030 );
1031 let orphan_reattached = parsed
1032 .rules
1033 .iter()
1034 .flat_map(|r| r.children.iter())
1035 .any(|c| c.message.contains("orphaned child"));
1036 assert!(
1037 !orphan_reattached,
1038 "the orphaned child of the unparseable rule must be dropped entirely"
1039 );
1040 }
1041
1042 #[test]
1043 fn test_max_magic_file_size_matches_file_buffer_limit() {
1044 assert_eq!(
1049 MAX_MAGIC_FILE_SIZE,
1050 crate::io::FileBuffer::MAX_FILE_SIZE,
1051 "MAX_MAGIC_FILE_SIZE must match FileBuffer::MAX_FILE_SIZE"
1052 );
1053 }
1054
1055 #[test]
1056 fn test_load_magic_file_rejects_oversized_file() {
1057 use std::fs::File;
1058 use tempfile::TempDir;
1059
1060 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1061 let oversized = temp_dir.path().join("huge.magic");
1062
1063 let file = File::create(&oversized).expect("Failed to create oversized file");
1066 file.set_len(MAX_MAGIC_FILE_SIZE + 1)
1067 .expect("Failed to set sparse file length");
1068 drop(file);
1069
1070 let result = load_magic_file(&oversized);
1071
1072 assert!(
1073 result.is_err(),
1074 "Loading a file larger than MAX_MAGIC_FILE_SIZE must fail"
1075 );
1076
1077 let err_msg = result.unwrap_err().to_string();
1078 assert!(
1079 err_msg.contains("too large"),
1080 "Error should indicate size limit violation, got: {err_msg}"
1081 );
1082 assert!(
1083 err_msg.contains(&MAX_MAGIC_FILE_SIZE.to_string()),
1084 "Error should mention the maximum allowed size, got: {err_msg}"
1085 );
1086 }
1087
1088 #[test]
1089 fn test_load_magic_file_tolerates_non_utf8_in_comment() {
1090 use std::fs;
1097 use tempfile::TempDir;
1098
1099 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1100 let magic_path = temp_dir.path().join("with-latin1-comment.magic");
1101
1102 let mut bytes: Vec<u8> = Vec::new();
1103 bytes.extend_from_slice(b"# From: Thomas Wei");
1104 bytes.push(0xdf); bytes.extend_from_slice(b"schuh <thomas@example.invalid>\n");
1106 bytes.extend_from_slice(b"0 string \\x7fELF ELF executable\n");
1107 fs::write(&magic_path, &bytes).expect("Failed to write magic file with non-UTF-8 byte");
1108
1109 let parsed = load_magic_file(&magic_path)
1110 .expect("Magic file with non-UTF-8 bytes in a comment must still load");
1111
1112 assert_eq!(
1113 parsed.rules.len(),
1114 1,
1115 "The ELF rule should be parsed; the comment is stripped"
1116 );
1117 assert_eq!(parsed.rules[0].message, "ELF executable");
1118 }
1119
1120 #[test]
1121 fn test_load_directory_merges_name_tables() {
1122 use std::fs;
1123 use tempfile::TempDir;
1124
1125 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1126
1127 fs::write(
1129 temp_dir.path().join("00_first"),
1130 "0 name sub_a\n>0 byte 1 a-body\n",
1131 )
1132 .expect("Failed to write sub_a file");
1133 fs::write(
1134 temp_dir.path().join("01_second"),
1135 "0 name sub_b\n>0 byte 2 b-body\n",
1136 )
1137 .expect("Failed to write sub_b file");
1138
1139 let parsed =
1140 load_magic_directory(temp_dir.path()).expect("Should load both name subroutines");
1141
1142 assert_eq!(parsed.rules.len(), 0);
1144 assert!(parsed.name_table.get("sub_a").is_some());
1145 assert!(parsed.name_table.get("sub_b").is_some());
1146 }
1147}