#![allow(clippy::indexing_slicing)]
use std::fs;
use std::io::Write;
use libmagic_rs::MagicDatabase;
use tempfile::TempDir;
fn build_pe_like_buffer() -> Vec<u8> {
let mut buf = vec![0u8; 0x84];
buf[0] = b'M';
buf[1] = b'Z';
buf[0x3c] = 0x80;
buf[0x3d] = 0x00;
buf[0x3e] = 0x00;
buf[0x3f] = 0x00;
buf[0x80] = b'P';
buf[0x81] = b'E';
buf[0x82] = 0x00;
buf[0x83] = 0x00;
buf
}
#[test]
fn test_indirect_offset_pe_detection_via_magic_file() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("pe.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r#"0 string "MZ" DOS executable"#).unwrap();
writeln!(f, r#">(0x3c.l) string "PE" (PE)"#).unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let buf = build_pe_like_buffer();
let result = db.evaluate_buffer(&buf).unwrap();
assert!(
result.description.contains("DOS executable"),
"Expected DOS executable match, got: {}",
result.description
);
assert!(
result.description.contains("(PE)"),
"Expected PE child match via indirect offset, got: {}",
result.description
);
}
#[test]
fn test_indirect_offset_no_match_when_pointer_out_of_bounds() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("pe.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r#"0 string "MZ" DOS executable"#).unwrap();
writeln!(f, r#">(0x3c.l) string "PE" (PE)"#).unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let mut buf = vec![0u8; 0x40];
buf[0] = b'M';
buf[1] = b'Z';
buf[0x3c] = 0xFF;
buf[0x3d] = 0x00;
buf[0x3e] = 0x00;
buf[0x3f] = 0x00;
let result = db.evaluate_buffer(&buf).unwrap();
assert!(
result.description.contains("DOS executable"),
"Expected DOS match even when child fails, got: {}",
result.description
);
assert!(
!result.description.contains("(PE)"),
"PE child should not match when pointer is out of bounds, got: {}",
result.description
);
}
#[test]
fn test_indirect_offset_with_adjustment_after_paren() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("adj.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r#"(0.l)+4 string "MAGIC" Adjusted match"#).unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let mut buf = vec![0u8; 20];
buf[0] = 0x06;
buf[1] = 0x00;
buf[2] = 0x00;
buf[3] = 0x00;
buf[10] = b'M';
buf[11] = b'A';
buf[12] = b'G';
buf[13] = b'I';
buf[14] = b'C';
let result = db.evaluate_buffer(&buf).unwrap();
assert!(
result.description.contains("Adjusted match"),
"Expected adjusted indirect match, got: {}",
result.description
);
}
#[test]
fn test_indirect_offset_byte_specifier() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("byte_ptr.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r#"(0.b) string "OK" Byte pointer match"#).unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let mut buf = vec![0u8; 10];
buf[0] = 5;
buf[5] = b'O';
buf[6] = b'K';
let result = db.evaluate_buffer(&buf).unwrap();
assert!(
result.description.contains("Byte pointer match"),
"Expected byte pointer match, got: {}",
result.description
);
}
#[test]
fn test_indirect_offset_loading_does_not_error() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("load.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r#"(0.b) string "A" byte LE ptr"#).unwrap();
writeln!(f, r#"(0.B) string "A" Byte LE ptr"#).unwrap();
writeln!(f, r#"(0.s) string "A" short LE ptr"#).unwrap();
writeln!(f, r#"(0.S) string "A" short BE ptr"#).unwrap();
writeln!(f, r#"(0.l) string "A" long LE ptr"#).unwrap();
writeln!(f, r#"(0.L) string "A" long BE ptr"#).unwrap();
writeln!(f, r#"(0.q) string "A" quad LE ptr"#).unwrap();
writeln!(f, r#"(0.Q) string "A" quad BE ptr"#).unwrap();
let result = MagicDatabase::load_from_file(&magic_path);
assert!(
result.is_ok(),
"Loading magic file with all indirect specifiers should succeed: {:?}",
result.err()
);
}
#[test]
fn test_indirect_offset_child_with_adjustment_after_paren() {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("pe_adj.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, r#"0 string "MZ" DOS executable"#).unwrap();
writeln!(f, r#">(0x3c.l)+4 string "PE" (PE+4)"#).unwrap();
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let mut buf = vec![0u8; 0x84];
buf[0] = b'M';
buf[1] = b'Z';
buf[0x3c] = 0x7C;
buf[0x3d] = 0x00;
buf[0x3e] = 0x00;
buf[0x3f] = 0x00;
buf[0x80] = b'P';
buf[0x81] = b'E';
let result = db.evaluate_buffer(&buf).unwrap();
assert!(
result.description.contains("DOS executable"),
"Expected DOS match, got: {}",
result.description
);
assert!(
result.description.contains("(PE+4)"),
"Expected child match with adjustment, got: {}",
result.description
);
}
#[test]
fn test_indirect_pointer_endianness_mismatch_is_an_error() {
use libmagic_rs::evaluator::offset::resolve_offset;
use libmagic_rs::parser::ast::{Endianness, IndirectAdjustmentOp, OffsetSpec, TypeKind};
let mismatched = |pointer_type: TypeKind| OffsetSpec::Indirect {
base_offset: 0,
base_relative: false,
pointer_type,
adjustment: 0,
adjustment_op: IndirectAdjustmentOp::Add,
result_relative: false,
endian: Endianness::Little,
};
let buffer = [0u8; 16];
let big = Endianness::Big;
for typ in [
TypeKind::Short {
endian: big,
signed: true,
},
TypeKind::Long {
endian: big,
signed: true,
},
TypeKind::Id3 { endian: big },
TypeKind::Quad {
endian: big,
signed: true,
},
] {
assert!(
resolve_offset(&mismatched(typ.clone()), &buffer).is_err(),
"mismatched endianness on {typ:?} must be rejected"
);
}
}
#[test]
fn test_id3_pointer_resolves_synchsafe_offset() {
const SYNCHSAFE_TARGET: usize = 2084 + 10;
const PLAIN_LONG_TARGET: usize = 4132 + 10;
let cases: &[(&str, &str, [u8; 4], &str)] = &[
(
"`I` big-endian",
"6.I+10",
[0x00, 0x00, 0x10, 0x24],
"Tag at=0xaa",
),
(
"`i` little-endian",
"6.i+10",
[0x24, 0x10, 0x00, 0x00],
"Tag at=0xaa",
),
(
"set high bits are masked",
"6.I+10",
[0x80, 0x80, 0x90, 0xa4],
"Tag at=0xaa",
),
(
"plain `L` reads the raw long",
"6.L+10",
[0x00, 0x00, 0x10, 0x24],
"Tag at=0xbb",
),
(
"decoded offset past the buffer",
"6.I+10",
[0x7f, 0x7f, 0x7f, 0x7f],
"Tag",
),
];
for (name, pointer, size_bytes, expected) in cases {
let temp_dir = TempDir::new().unwrap();
let magic_path = temp_dir.path().join("id3.magic");
let mut f = fs::File::create(&magic_path).unwrap();
writeln!(f, "0 string ID3 Tag").unwrap();
writeln!(f, ">({pointer}) ubyte x at=0x%02x").unwrap();
drop(f);
let db = MagicDatabase::load_from_file(&magic_path).unwrap();
let mut buf = vec![0u8; PLAIN_LONG_TARGET + 8];
buf[..3].copy_from_slice(b"ID3");
buf[6..10].copy_from_slice(size_bytes);
buf[SYNCHSAFE_TARGET] = 0xAA;
buf[PLAIN_LONG_TARGET] = 0xBB;
let result = db.evaluate_buffer(&buf).unwrap();
assert_eq!(result.description, *expected, "case: {name}");
}
}