#![allow(clippy::expect_used)]
use std::io::Write;
use libmagic_rs::{EvaluationConfig, MagicDatabase};
use tempfile::NamedTempFile;
const FAT_MAGIC: &str = "0\tbelong\t0xcafebabe\n\
>4\tbelong\t2\tFat binary with %d architectures:\n\
>>8\tuse\tarch\t\\b\n\
>>28\tuse\tarch\t\\b\n\
0\tname\tarch\t\\b [\n\
>0\tuse\tcpu\t\\b\n\
>(8.L)\tindirect\tx\t\\b:\n\
>0\tbelong\tx\t\\b]\n\
0\tname\tcpu\n\
>0\tbelong\t0x01000007\tx86_64\n\
>0\tbelong\t0x0100000c\tarm64e\n\
0\tbelong\t0xfeedfacf\tMachO\n\
>12\tbelong\t2\t\\b 64-bit executable\n\
>4\tuse\tcpu\n";
const CPU_X86_64: u32 = 0x0100_0007;
const CPU_ARM64E: u32 = 0x0100_000c;
const MACH_MAGIC_64: u32 = 0xfeed_facf;
const FAT_MAGIC_BE: u32 = 0xcafe_babe;
const MH_EXECUTE: u32 = 2;
const MAX_FIXTURE_LEN: usize = 512;
struct Arch {
cputype: u32,
offset: u32,
}
fn build_fat(arches: &[Arch]) -> Vec<u8> {
let header_len = 8 + arches.len() * 20;
let mut buf = Vec::new();
buf.extend_from_slice(&FAT_MAGIC_BE.to_be_bytes());
buf.extend_from_slice(
&u32::try_from(arches.len())
.expect("arch count")
.to_be_bytes(),
);
for a in arches {
buf.extend_from_slice(&a.cputype.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&a.offset.to_be_bytes());
buf.extend_from_slice(&16u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); }
debug_assert_eq!(buf.len(), header_len, "fat header must be 8 + 20 per arch");
for a in arches {
let at = a.offset as usize;
if at < header_len || at + 16 > MAX_FIXTURE_LEN {
continue;
}
if buf.len() < at + 16 {
buf.resize(at + 16, 0);
}
let header: Vec<u8> = MACH_MAGIC_64
.to_be_bytes()
.into_iter()
.chain(a.cputype.to_be_bytes())
.chain(0u32.to_be_bytes()) .chain(MH_EXECUTE.to_be_bytes())
.collect();
buf.get_mut(at..at + 16)
.expect("buffer was just grown to cover this arch's header")
.copy_from_slice(&header);
}
buf
}
fn db() -> MagicDatabase {
let mut f = NamedTempFile::new().expect("temp magic file");
f.write_all(FAT_MAGIC.as_bytes()).expect("write magic");
f.flush().expect("flush magic");
MagicDatabase::load_from_file_with_config(f.path(), EvaluationConfig::default())
.expect("magic must load")
}
fn describe(buf: &[u8]) -> String {
db().evaluate_buffer(buf).expect("evaluate").description
}
#[test]
fn two_architecture_fat_binary_renders_per_arch_inner_classification() {
let buf = build_fat(&[
Arch {
cputype: CPU_X86_64,
offset: 48,
},
Arch {
cputype: CPU_ARM64E,
offset: 64,
},
]);
assert_eq!(
describe(&buf),
"Fat binary with 2 architectures: \
[x86_64:MachO 64-bit executable x86_64] \
[arm64e:MachO 64-bit executable arm64e]",
"each arch must render [<cpu>:<inner classification>] with no stray spaces"
);
}
#[test]
fn opening_bracket_has_no_stray_space() {
let buf = build_fat(&[
Arch {
cputype: CPU_X86_64,
offset: 48,
},
Arch {
cputype: CPU_ARM64E,
offset: 64,
},
]);
let desc = describe(&buf);
assert!(
desc.contains("[x86_64:"),
"expected `[x86_64:` with no interior spaces, got: {desc}"
);
assert!(
!desc.contains("[ "),
"no bracket may be followed by a space, got: {desc}"
);
}
#[test]
fn arch_offset_past_end_of_buffer_skips_inner_detail_without_erroring() {
let buf = build_fat(&[
Arch {
cputype: CPU_X86_64,
offset: 9999,
},
Arch {
cputype: CPU_ARM64E,
offset: 9999,
},
]);
let desc = describe(&buf);
assert!(
desc.starts_with("Fat binary with 2 architectures:"),
"outer classification must survive an out-of-range arch offset, got: {desc}"
);
assert!(
!desc.contains("MachO"),
"no inner classification is reachable, got: {desc}"
);
}
#[test]
fn arch_offset_pointing_at_unrecognized_bytes_keeps_brackets_balanced() {
let mut buf = build_fat(&[
Arch {
cputype: CPU_X86_64,
offset: 48,
},
Arch {
cputype: CPU_ARM64E,
offset: 64,
},
]);
buf.get_mut(64..68)
.expect("fixture places arch[1]'s mach header at offset 64")
.fill(0);
let desc = describe(&buf);
assert_eq!(
desc.matches('[').count(),
desc.matches(']').count(),
"brackets must stay balanced when an arch fails to classify, got: {desc}"
);
assert!(
desc.contains("[arm64e:]"),
"the indirect rule still matches (its test is `x`) so its `:` prints \
with nothing after it, got: {desc}"
);
}
#[test]
fn indirect_pointer_site_is_read_per_arch_not_from_a_shared_base() {
let buf = build_fat(&[
Arch {
cputype: CPU_X86_64,
offset: 48,
},
Arch {
cputype: CPU_ARM64E,
offset: 64,
},
]);
let desc = describe(&buf);
assert!(
desc.contains("[x86_64:MachO 64-bit executable x86_64]"),
"arch[0] must dereference its own offset field, got: {desc}"
);
assert!(
desc.contains("[arm64e:MachO 64-bit executable arm64e]"),
"arch[1] must dereference its own offset field at base(28) + 8, got: {desc}"
);
}