use callgraph_verify::{has_direct_branch_target, Pe};
use std::time::Instant;
const BASE: u64 = 0x0040_0000;
const TEXT_RVA: u32 = 0x1000;
fn pe_with_text(fill: &[u8], code: &[u8], text_len: usize) -> Pe {
assert!(fill.len() + code.len() <= text_len);
let mut text: Vec<u8> = Vec::with_capacity(text_len);
text.extend_from_slice(code);
while text.len() < text_len {
let room = text_len - text.len();
let take = fill.len().min(room);
text.extend_from_slice(&fill[..take]);
}
let text_raw = ((text.len() + 0x1ff) & !0x1ff) as u32;
let e_lfanew: u32 = 0x40;
let mut data: Vec<u8> = vec![0; 0x400 + text_raw as usize * 2];
data[0..2].copy_from_slice(b"MZ");
data[0x3c..0x40].copy_from_slice(&e_lfanew.to_le_bytes());
let pe_off = e_lfanew as usize;
data[pe_off..pe_off + 4].copy_from_slice(b"PE\0\0");
let coff = pe_off + 4;
data[coff..coff + 2].copy_from_slice(&0x014cu16.to_le_bytes());
data[coff + 2..coff + 4].copy_from_slice(&1u16.to_le_bytes());
data[coff + 16..coff + 18].copy_from_slice(&0x60u16.to_le_bytes());
let opt = coff + 20;
data[opt..opt + 2].copy_from_slice(&0x010bu16.to_le_bytes());
data[opt + 28..opt + 32].copy_from_slice(&(BASE as u32).to_le_bytes());
let sec_off = opt + 0x60;
data[sec_off..sec_off + 8].copy_from_slice(b".text\0\0\0");
data[sec_off + 8..sec_off + 12].copy_from_slice(&(text.len() as u32).to_le_bytes());
data[sec_off + 12..sec_off + 16].copy_from_slice(&TEXT_RVA.to_le_bytes());
data[sec_off + 16..sec_off + 20].copy_from_slice(&text_raw.to_le_bytes());
data[sec_off + 20..sec_off + 24].copy_from_slice(&text_raw.to_le_bytes());
data[text_raw as usize..text_raw as usize + text.len()].copy_from_slice(&text);
Pe::parse(data).expect("parse")
}
const NOP_FILL: &[u8] = &[0x0F, 0x1F, 0x00];
const TEXT_LEN: usize = 4 * 1024 * 1024;
#[test]
fn has_direct_branch_target_repeat_lookups_are_not_rescans() {
let pe = pe_with_text(NOP_FILL, &[], TEXT_LEN);
let first = BASE + TEXT_RVA as u64;
let t0 = Instant::now();
assert!(!has_direct_branch_target(&pe, first));
let warmup = t0.elapsed();
assert!(
warmup.as_millis() > 0,
"warmup did no measurable work — test PE too small to calibrate against"
);
const N: u32 = 200;
let t1 = Instant::now();
for k in 0..N {
let a = BASE + TEXT_RVA as u64 + (k as u64 * 16) + 1;
assert!(!has_direct_branch_target(&pe, a));
}
let batch = t1.elapsed();
assert!(
batch * 10 < warmup,
"200 repeat lookups took {batch:?} vs {warmup:?} for the first (warmup) call — \
has_direct_branch_target is rescanning .text per call; the all-branch-targets \
cache in lib.rs was dropped or its key stopped matching"
);
}
#[test]
fn has_direct_branch_target_cache_distinguishes_same_layout_pes() {
let pe_a = pe_with_text(NOP_FILL, &[], 0x1000);
let pe_b = pe_with_text(NOP_FILL, &[0xE8, 0x00, 0x00, 0x00, 0x00, 0xC3], 0x1000);
let call_target = BASE + TEXT_RVA as u64 + 5;
assert!(!has_direct_branch_target(&pe_a, call_target));
assert!(
has_direct_branch_target(&pe_b, call_target),
"PE B's CALL target not found — the branch-target cache treated PE B \
as PE A (cache key ignores PE identity/content)"
);
let pe_c = pe_with_text(NOP_FILL, &[0xE8, 0x00, 0x00, 0x00, 0x00, 0xC3], 0x2000);
let pe_d = pe_with_text(NOP_FILL, &[], 0x2000);
assert!(has_direct_branch_target(&pe_c, call_target));
assert!(!has_direct_branch_target(&pe_d, call_target));
}
#[test]
fn has_direct_branch_target_finds_hits_in_large_text() {
let mut code = vec![0x0F, 0x1F, 0x00];
code.extend_from_slice(&[0xE8, 0x00, 0x00, 0x00, 0x00]);
let pe = pe_with_text(NOP_FILL, &code, TEXT_LEN);
let call_target = BASE + TEXT_RVA as u64 + 8;
assert!(
has_direct_branch_target(&pe, call_target),
"the buried CALL target must be found in the cached branch-target set"
);
assert!(!has_direct_branch_target(
&pe,
BASE + TEXT_RVA as u64 + 0x40
));
}