use rs_header::pe::export_index::{ExportIndexData, build_export_index};
use rs_header::pe::shared::ImageSectionHeader;
use crate::api::windows::export_index::{
ExportIndexRegistry, IndexedExport, MAX_FORWARDER_DEPTH, ModuleExportIndex,
};
fn section(va: u32, raw_ptr: u32, raw_size: u32) -> ImageSectionHeader {
let mut s = ImageSectionHeader {
name: [0; 8],
virtual_size: raw_size,
virtual_address: va,
size_of_raw_data: raw_size,
pointer_to_raw_data: raw_ptr,
pointer_to_relocations: 0,
pointer_to_linenumbers: 0,
number_of_relocations: 0,
number_of_linenumbers: 0,
characteristics: 0,
};
let n = b".text\0\0\0";
s.name[..n.len()].copy_from_slice(n);
s
}
fn write_export_dir(raw: &mut [u8], export_off: usize, exp: &[u8; 40]) {
raw[export_off..export_off + 40].copy_from_slice(exp);
}
fn build_export_table(
base: u32,
funcs: &[(u32, bool)], names: &[(&str, u16)], ) -> (Vec<u8>, Vec<ImageSectionHeader>, u32, u32) {
let mut raw = vec![0u8; 0x400];
let export_off = 0x040;
let func_off = 0x080;
let name_off_table = 0x0c0;
let ord_off = 0x100;
let fwd_str_off = 0x140;
let name_data_off = 0x180;
let export_va: u32 = 0x1040;
let func_va: u32 = 0x1080;
let name_table_va: u32 = 0x10c0;
let ord_table_va: u32 = 0x1100;
let fwd_str_va: u32 = 0x1140;
let name_data_va: u32 = 0x1180;
let mut exp = [0u8; 40];
exp[16..20].copy_from_slice(&base.to_le_bytes());
exp[20..24].copy_from_slice(&(funcs.len() as u32).to_le_bytes());
exp[24..28].copy_from_slice(&(names.len() as u32).to_le_bytes());
exp[28..32].copy_from_slice(&func_va.to_le_bytes());
exp[32..36].copy_from_slice(&name_table_va.to_le_bytes());
exp[36..40].copy_from_slice(&ord_table_va.to_le_bytes());
write_export_dir(&mut raw, export_off, &exp);
for (i, (rva, _)) in funcs.iter().enumerate() {
raw[func_off + i * 4..func_off + (i + 1) * 4].copy_from_slice(&rva.to_le_bytes());
}
let mut name_data_writer = name_data_off;
for (i, (name, ord_idx)) in names.iter().enumerate() {
let name_va = name_data_va + (name_data_writer - name_data_off) as u32;
raw[name_off_table + i * 4..name_off_table + (i + 1) * 4]
.copy_from_slice(&name_va.to_le_bytes());
let s = name.as_bytes();
raw[name_data_writer..name_data_writer + s.len()].copy_from_slice(s);
raw[name_data_writer + s.len()] = 0;
name_data_writer += s.len() + 1;
raw[ord_off + i * 2..ord_off + (i + 1) * 2].copy_from_slice(&ord_idx.to_le_bytes());
}
let mut fwd_writer = fwd_str_off;
for (i, (_, is_fwd)) in funcs.iter().enumerate() {
if *is_fwd {
let fwd_rva = fwd_str_va + (fwd_writer - fwd_str_off) as u32;
raw[func_off + i * 4..func_off + (i + 1) * 4].copy_from_slice(&fwd_rva.to_le_bytes());
let s = format!("BACKING.Missing{}", i);
let bytes = s.as_bytes();
raw[fwd_writer..fwd_writer + bytes.len()].copy_from_slice(bytes);
raw[fwd_writer + bytes.len()] = 0;
fwd_writer += bytes.len() + 1;
}
}
let sections = vec![section(0x1000, 0, raw.len() as u32)];
(raw, sections, export_va, 0x200)
}
fn parse_and_register(
reg: &mut ExportIndexRegistry,
module: &str,
base: u64,
raw: &[u8],
sections: &[ImageSectionHeader],
va: u32,
size: u32,
) {
let parsed = build_export_index(raw, sections, va, size).expect("parse");
let normalized = crate::api::windows::export_index::normalize_module_name(module);
let index = ModuleExportIndex::from_parsed(module.to_string(), normalized, base, &parsed);
reg.register(index);
}
#[test]
fn forwarder_chain_resolves_through_registered_target() {
let mut kernelbase_raw = vec![0u8; 0x400];
let export_off = 0x040;
let func_off = 0x080;
let name_off_table = 0x0c0;
let ord_off = 0x100;
let name_data_off = 0x180;
let mut exp = [0u8; 40];
exp[16..20].copy_from_slice(&1u32.to_le_bytes()); exp[20..24].copy_from_slice(&1u32.to_le_bytes()); exp[24..28].copy_from_slice(&1u32.to_le_bytes()); exp[28..32].copy_from_slice(&0x1080u32.to_le_bytes());
exp[32..36].copy_from_slice(&0x10c0u32.to_le_bytes());
exp[36..40].copy_from_slice(&0x1100u32.to_le_bytes());
write_export_dir(&mut kernelbase_raw, export_off, &exp);
kernelbase_raw[func_off..func_off + 4].copy_from_slice(&0x1500u32.to_le_bytes());
kernelbase_raw[name_off_table..name_off_table + 4].copy_from_slice(&0x1180u32.to_le_bytes());
kernelbase_raw[ord_off..ord_off + 2].copy_from_slice(&0u16.to_le_bytes());
let s = b"HeapAlloc\0";
kernelbase_raw[name_data_off..name_data_off + s.len()].copy_from_slice(s);
let sections = vec![section(0x1000, 0, kernelbase_raw.len() as u32)];
let (k32_raw, k32_sections, k32_va, k32_size) = build_export_table(
1,
&[(0x1500, false), (0x1140, true)],
&[("Direct", 0), ("ViaFwd", 1)],
);
let fwd_off = 0x140;
let s = b"kernelbase.HeapAlloc\0";
let mut k32_raw = k32_raw;
k32_raw[fwd_off..fwd_off + s.len()].copy_from_slice(s);
let mut reg = ExportIndexRegistry::new();
parse_and_register(
&mut reg,
"kernel32.dll",
0x10000,
&k32_raw,
&k32_sections,
k32_va,
k32_size,
);
parse_and_register(
&mut reg,
"kernelbase.dll",
0x50000,
&kernelbase_raw,
§ions,
0x1040,
0x200,
);
assert_eq!(reg.resolve_name_in_module("kernel32", "Direct"), 0x11500);
assert_eq!(reg.resolve_name_in_module("kernel32", "ViaFwd"), 0x51500);
}
#[test]
fn forwarder_chain_cycle_does_not_loop_forever() {
let raw1 = build_export_table(1, &[(0x1140, true)], &[("A", 0)]);
let raw2 = build_export_table(1, &[(0x1140, true)], &[("B", 0)]);
let mut r1 = raw1.0;
let mut r2 = raw2.0;
let s = b"modB.A\0";
r1[0x140..0x140 + s.len()].copy_from_slice(s);
let s = b"modA.B\0";
r2[0x140..0x140 + s.len()].copy_from_slice(s);
let mut reg = ExportIndexRegistry::new();
parse_and_register(&mut reg, "modA.dll", 0x10000, &r1, &raw1.1, raw1.2, raw1.3);
parse_and_register(&mut reg, "modB.dll", 0x50000, &r2, &raw2.1, raw2.2, raw2.3);
let start = std::time::Instant::now();
assert_eq!(reg.resolve_name_in_module("modA", "A"), 0);
assert!(start.elapsed().as_millis() < 100);
}
#[test]
fn forwarder_chain_depth_limit_returns_zero() {
let (raw, sections, va, size) = build_export_table(
1,
&[(0x1500, false), (0x1140, true)],
&[("Direct", 0), ("Chase", 1)],
);
let mut reg = ExportIndexRegistry::new();
parse_and_register(&mut reg, "test.dll", 0x10000, &raw, §ions, va, size);
assert_eq!(reg.resolve_name_in_module("test", "Chase"), 0);
}
#[test]
fn replacement_removes_old_base_but_preserves_order() {
let (raw, sections, va, size) = build_export_table(1, &[(0x1500, false)], &[("Fn", 0)]);
let mut reg = ExportIndexRegistry::new();
parse_and_register(&mut reg, "alpha.dll", 0x1000, &raw, §ions, va, size);
parse_and_register(&mut reg, "beta.dll", 0x2000, &raw, §ions, va, size);
parse_and_register(&mut reg, "alpha.dll", 0x3000, &raw, §ions, va, size);
assert!(reg.get_by_base(0x1000).is_none());
assert!(reg.get_by_base(0x3000).is_some());
let names: Vec<_> = reg
.iter_ordered()
.map(|m| m.normalized_name.clone())
.collect();
assert_eq!(names, vec!["alpha", "beta"]);
assert_eq!(reg.resolve_name_in_module("alpha", "Fn"), 0x3000 + 0x1500);
}
#[test]
fn removal_drops_module_from_all_indices() {
let (raw, sections, va, size) = build_export_table(1, &[(0x1500, false)], &[("Fn", 0)]);
let mut reg = ExportIndexRegistry::new();
parse_and_register(&mut reg, "alpha.dll", 0x1000, &raw, §ions, va, size);
parse_and_register(&mut reg, "beta.dll", 0x2000, &raw, §ions, va, size);
assert!(reg.remove("alpha"));
assert!(reg.get_by_name("alpha").is_none());
assert!(reg.get_by_base(0x1000).is_none());
assert_eq!(reg.resolve_name_in_module("alpha", "Fn"), 0);
assert_eq!(reg.resolve_name_in_module("beta", "Fn"), 0x2000 + 0x1500);
}
#[test]
fn unknown_name_returns_zero_and_does_not_scan() {
let (raw, sections, va, size) = build_export_table(1, &[(0x1500, false)], &[("Fn", 0)]);
let mut reg = ExportIndexRegistry::new();
parse_and_register(&mut reg, "alpha.dll", 0x1000, &raw, §ions, va, size);
assert_eq!(reg.resolve_name_in_module("alpha", "Missing"), 0);
assert_eq!(reg.resolve_name_global("Missing"), 0);
}
#[test]
fn ordinal_lookup_uses_export_base_and_handles_per_handle() {
let (raw, sections, va, size) = build_export_table(
5,
&[(0x1500, false), (0x1500, false)],
&[("Fn0", 0), ("Fn1", 1)],
);
let mut reg = ExportIndexRegistry::new();
parse_and_register(&mut reg, "mod.dll", 0x10000, &raw, §ions, va, size);
assert_eq!(reg.resolve_ordinal_by_base(0x10000, 5), 0x11500);
assert_eq!(reg.resolve_ordinal_by_base(0x10000, 6), 0x11500);
assert_eq!(reg.resolve_ordinal_by_base(0x99999, 5), 0);
}
#[test]
fn forwarder_max_depth_constant_matches_documented_limit() {
assert_eq!(MAX_FORWARDER_DEPTH, 8);
}
#[test]
fn indexed_export_helpers_classify_direct_vs_forwarder() {
let direct = IndexedExport::Direct { address: 0x42 };
assert!(direct.is_direct());
assert_eq!(direct.direct_address(), Some(0x42));
let fwd = IndexedExport::Forwarder {
value: "x.y".to_string(),
};
assert!(!fwd.is_direct());
assert_eq!(fwd.direct_address(), None);
}
#[test]
fn unindexed_module_lookup_is_zero() {
let reg = ExportIndexRegistry::new();
assert_eq!(reg.resolve_name_in_module("ghost", "Fn"), 0);
assert_eq!(reg.resolve_name_by_base(0xdead, "Fn"), 0);
assert_eq!(reg.resolve_ordinal_by_base(0xdead, 1), 0);
assert_eq!(reg.resolve_address(0xdead), None);
assert_eq!(reg.iter_ordered().count(), 0);
}
#[test]
fn empty_parsed_index_does_not_register() {
let reg = ExportIndexRegistry::new();
let empty = ExportIndexData::default();
assert!(empty.is_empty());
assert_eq!(reg.len(), 0);
}
#[test]
fn pe_loader_ordinal_resolution_uses_module_registry() {
crate::tests::helpers::setup();
let mut emu = crate::emu64();
let base = 0x1_0000_0000u64;
crate::tests::helpers::register_fake_export_module(&mut emu, base);
let resolved = <crate::emu::Emu as rs_header::pe::PeLoader>::resolve_api_ordinal_in_module(
&mut emu, "fake.dll", 5,
);
assert_eq!(resolved, base + 0x1500, "module ordinal did not resolve");
assert_eq!(
<crate::emu::Emu as rs_header::pe::PeLoader>::resolve_api_ordinal_in_module(
&mut emu, "fake.dll", 6,
),
0
);
assert_eq!(
<crate::emu::Emu as rs_header::pe::PeLoader>::resolve_api_ordinal_in_module(
&mut emu,
"missing.dll",
5,
),
0
);
}
#[test]
fn resolve_ordinal_in_module_uses_export_base_and_normalizes_name() {
let (raw, sections, va, size) = build_export_table(
5,
&[(0x1500, false), (0x1500, false)],
&[("Fn0", 0), ("Fn1", 1)],
);
let mut reg = ExportIndexRegistry::new();
parse_and_register(&mut reg, "mod.dll", 0x10000, &raw, §ions, va, size);
assert_eq!(reg.resolve_ordinal_in_module("mod", 5), 0x11500);
assert_eq!(reg.resolve_ordinal_in_module("mod", 6), 0x11500);
assert_eq!(reg.resolve_ordinal_in_module("mod", 4), 0);
assert_eq!(reg.resolve_ordinal_in_module("mod", 7), 0);
assert_eq!(
reg.resolve_ordinal_in_module("C:\\Windows\\MOD.DLL", 5),
0x11500
);
}
#[test]
fn resolve_ordinal_in_module_ordinal_forwarder_chases_target() {
let (fake_raw, fake_sections, fake_va, fake_size) =
build_export_table(1, &[(0x1140, true)], &[("Via", 0)]);
let mut fake_raw = fake_raw;
let s = b"backing.#5\0";
fake_raw[0x140..0x140 + s.len()].copy_from_slice(s);
let (backing_raw, backing_sections, backing_va, backing_size) =
build_export_table(5, &[(0x1500, false)], &[("Direct", 0)]);
let mut reg = ExportIndexRegistry::new();
parse_and_register(
&mut reg,
"fake.dll",
0x10000,
&fake_raw,
&fake_sections,
fake_va,
fake_size,
);
parse_and_register(
&mut reg,
"backing.dll",
0x20000,
&backing_raw,
&backing_sections,
backing_va,
backing_size,
);
assert_eq!(reg.resolve_ordinal_in_module("fake", 1), 0x20000 + 0x1500);
}
#[test]
fn resolve_ordinal_in_module_does_not_scan_other_modules() {
let (raw, sections, va, size) = build_export_table(1, &[(0x1500, false)], &[("Fn", 0)]);
let mut reg = ExportIndexRegistry::new();
parse_and_register(&mut reg, "present.dll", 0x1000, &raw, §ions, va, size);
parse_and_register(&mut reg, "absent.dll", 0x2000, &raw, §ions, va, size);
assert_eq!(reg.resolve_ordinal_in_module("present", 1), 0x1000 + 0x1500);
assert_eq!(reg.resolve_ordinal_in_module("absent", 5), 0);
assert_eq!(reg.resolve_ordinal_in_module("missing", 1), 0);
}