use std::fs;
use std::io::Write;
use libmagic_rs::{EvaluationConfig, MagicDatabase};
use tempfile::TempDir;
#[test]
fn test_searchbug_magic_loads_end_to_end() {
let db = MagicDatabase::load_from_file("third_party/tests/searchbug.magic")
.expect("searchbug.magic must load end-to-end");
let bytes = std::fs::read("third_party/tests/searchbug.testfile")
.expect("searchbug.testfile fixture must exist");
let result = db
.evaluate_buffer(&bytes)
.expect("evaluate_buffer on searchbug.testfile");
assert!(
!result.description.is_empty(),
"evaluation should produce some description"
);
assert!(
result.description.starts_with("Testfmt"),
"description should start with \"Testfmt\", got: {}",
result.description
);
}
#[test]
fn test_default_clear_synthetic_scenario() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("default.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r"0 byte 0xAA Real-Match").unwrap();
writeln!(f, r"0 default x DEFAULT-FALLBACK").unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let buf_no_match = [0x00u8, 0x01, 0x02, 0x03];
let result_default = db.evaluate_buffer(&buf_no_match).unwrap();
assert!(
result_default.description.contains("DEFAULT-FALLBACK"),
"default should fire when no sibling matched, got: {}",
result_default.description
);
let buf_match = [0xAAu8, 0x01, 0x02, 0x03];
let result_real = db.evaluate_buffer(&buf_match).unwrap();
assert!(
!result_real.description.contains("DEFAULT-FALLBACK"),
"default must not fire when a sibling matched, got: {}",
result_real.description
);
assert!(
result_real.description.contains("Real-Match"),
"real byte rule should still match, got: {}",
result_real.description
);
let clear_path = temp_dir.path().join("clear.magic");
let mut cf = fs::File::create(&clear_path).unwrap();
writeln!(cf, r"0 byte 0xAA Match-A").unwrap();
writeln!(cf, r"0 default x DEFAULT-SKIPPED").unwrap();
writeln!(cf, r"0 clear").unwrap();
writeln!(cf, r"0 default x DEFAULT-FIRES").unwrap();
let all_matches_config = EvaluationConfig::default().with_stop_at_first_match(false);
let clear_db =
MagicDatabase::load_from_file_with_config(&clear_path, all_matches_config).unwrap();
let buf_clear = [0xAAu8, 0x01, 0x02, 0x03];
let result_clear = clear_db.evaluate_buffer(&buf_clear).unwrap();
assert!(
result_clear.description.contains("Match-A"),
"byte rule should still match before clear, got: {}",
result_clear.description
);
assert!(
!result_clear.description.contains("DEFAULT-SKIPPED"),
"default immediately after a sibling match must remain silent, got: {}",
result_clear.description
);
assert!(
result_clear.description.contains("DEFAULT-FIRES"),
"clear must reset sibling-matched so a later default can fire, got: {}",
result_clear.description
);
}
#[test]
fn test_clear_with_message_emits_and_still_resets_flag() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("clear_msg.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r"0 string ZQX9 MATCH-A").unwrap();
writeln!(f, r">4 default x DEF-ONE").unwrap();
writeln!(f, r">4 clear x CLEARED-MSG").unwrap();
writeln!(f, r">4 default x DEF-TWO").unwrap();
let config = EvaluationConfig::default().with_stop_at_first_match(false);
let db = MagicDatabase::load_from_file_with_config(&magic_path, config).unwrap();
let buf = b"ZQX9rest-of-data";
let result = db.evaluate_buffer(buf).unwrap();
assert!(
result.description.contains("CLEARED-MSG"),
"message-bearing clear must emit its message, got: {}",
result.description
);
assert!(
result.description.contains("DEF-ONE"),
"first child default should fire, got: {}",
result.description
);
assert!(
result.description.contains("DEF-TWO"),
"clear must reset sibling-matched so the trailing default fires, got: {}",
result.description
);
}
#[test]
fn test_message_less_clear_emits_nothing() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("clear_bare.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r"0 string ZQX9 PARENT").unwrap();
writeln!(f, r">4 clear x").unwrap();
let config = EvaluationConfig::default().with_stop_at_first_match(false);
let db = MagicDatabase::load_from_file_with_config(&magic_path, config).unwrap();
let result = db.evaluate_buffer(b"ZQX9rest").unwrap();
assert_eq!(
result.description, "PARENT",
"a message-less clear must contribute no text, got: {}",
result.description
);
}
#[test]
fn test_indirect_synthetic_scenario() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("indirect.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r"0 byte 0x42 Inner-Match").unwrap();
writeln!(f, r"8 indirect x").unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let mut buf = vec![0u8; 16];
buf[8] = 0x42;
let result = db.evaluate_buffer(&buf).unwrap();
assert!(
result.description.contains("Inner-Match"),
"indirect must dispatch root rules at the resolved offset; got: {}",
result.description
);
}
#[test]
fn test_searchbug_matches_full_result_string() {
let config = EvaluationConfig::default().with_stop_at_first_match(false);
let db = MagicDatabase::load_from_file_with_config("third_party/tests/searchbug.magic", config)
.expect("searchbug.magic must load end-to-end");
let bytes = std::fs::read("third_party/tests/searchbug.testfile")
.expect("searchbug.testfile fixture must exist");
let expected = std::fs::read_to_string("third_party/tests/searchbug.result")
.expect("searchbug.result fixture must exist");
let result = db
.evaluate_buffer(&bytes)
.expect("evaluate_buffer on searchbug.testfile");
assert_eq!(result.description.trim(), expected.trim());
}
#[test]
fn test_gzip_multipart_description_end_to_end() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("gzip.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r"0 string \037\213").unwrap();
writeln!(f, r"# no FNAME/FCOMMENT bit -> binary gzip").unwrap();
writeln!(
f,
r"# (real rule masks with &0x18; a bare `byte 0` suffices here)"
)
.unwrap();
writeln!(f, r"# FLG byte at offset 3").unwrap();
writeln!(f, r">3 byte 0").unwrap();
writeln!(f, r">>10 default x gzip compressed data").unwrap();
writeln!(f, r">>>0 use gzip-info").unwrap();
writeln!(f, r">>-0 offset >48").unwrap();
writeln!(f, r">>>-4 ulelong x \b, original size modulo 2^32 %u").unwrap();
writeln!(f, r">>-0 offset <48 \b, truncated").unwrap();
writeln!(f, r"0 name gzip-info").unwrap();
writeln!(f, r">9 byte 3 \b, from Unix").unwrap();
drop(f);
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let mut buf = vec![0u8; 64];
buf[0] = 0x1f;
buf[1] = 0x8b;
buf[2] = 0x08; buf[3] = 0x00; buf[9] = 0x03; buf[60] = 0xE8; buf[61] = 0x03;
let result = db.evaluate_buffer(&buf).unwrap();
assert_eq!(
result.description, "gzip compressed data, from Unix, original size modulo 2^32 1000",
"all four fragments must render in file order; got {:?}",
result.description
);
let mut short = vec![0u8; 20];
short[0] = 0x1f;
short[1] = 0x8b;
short[9] = 0x03;
let short_result = db.evaluate_buffer(&short).unwrap();
assert!(
short_result.description.contains("truncated"),
"a <48-byte gzip must hit the `offset <48` truncated branch; got {:?}",
short_result.description
);
assert!(
!short_result.description.contains("original size"),
"the size branch must not fire for a truncated file; got {:?}",
short_result.description
);
}
#[test]
fn test_use_emits_name_line_message_with_no_separator() {
let cases: &[(&str, &str)] = &[
(
"0 string ZQXNAME9 Parent\n>0 use submsgtest\n0 name submsgtest SUBMSG\n>8 byte x child=%d\n",
"ParentSUBMSG child=5",
),
(
"0 string ZQXNAME9 Parent\n>0 use submsgtest\n0 name submsgtest \\b [\n>8 byte x child=%d\n",
"Parent [ child=5",
),
(
"0 string ZQXNAME9 Parent\n>0 use submsgtest USEMSG\n0 name submsgtest NAMEMSG\n>8 byte x child=%d\n",
"ParentNAMEMSG child=5",
),
(
"0 string ZQXNAME9 Parent\n>0 use submsgtest\n0 name submsgtest NAMEONLY\n",
"ParentNAMEONLY",
),
];
let temp_dir = TempDir::new().unwrap();
let buf = b"ZQXNAME9\x05";
for (i, (magic, expected)) in cases.iter().enumerate() {
let magic_path = temp_dir.path().join(format!("namemsg_{i}.magic"));
fs::write(&magic_path, magic).unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let result = db.evaluate_buffer(buf).unwrap();
assert_eq!(
result.description, *expected,
"case {i}: name-line message spacing must match GNU `file` for magic {magic:?}"
);
}
}
#[test]
fn test_use_nested_name_message_balances_brackets() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("nested.magic");
let magic = "0 string ZQXNAME9 Parent:\n\
>0 use outer\n\
0 name outer \\b [\n\
>0 use inner\n\
>8 byte x \\b]\n\
0 name inner INNERCPU\n";
fs::write(&magic_path, magic).unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let result = db.evaluate_buffer(b"ZQXNAME9\x05").unwrap();
assert_eq!(result.description, "Parent: [INNERCPU]");
}