use ahash::AHashMap;
use rs_header::pe::export_index::{ExportIndexData, ExportTarget, NamedExport};
use serde::{Deserialize, Serialize};
pub const MAX_FORWARDER_DEPTH: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndexedExport {
Direct { address: u64 },
Forwarder { value: String },
}
impl IndexedExport {
pub fn is_direct(&self) -> bool {
matches!(self, IndexedExport::Direct { .. })
}
pub fn direct_address(&self) -> Option<u64> {
match self {
IndexedExport::Direct { address } => Some(*address),
IndexedExport::Forwarder { .. } => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleExportIndex {
pub module_name: String,
pub normalized_name: String,
pub base: u64,
pub export_base: u32,
pub by_name: AHashMap<String, u32>,
pub by_ordinal: Vec<Option<IndexedExport>>,
pub by_address: AHashMap<u64, String>,
display_names: Vec<(u32, String)>,
}
impl ModuleExportIndex {
pub fn from_parsed(
module_name: String,
normalized_name: String,
base: u64,
parsed: &ExportIndexData,
) -> Self {
let mut by_name: AHashMap<String, u32> = AHashMap::new();
let mut by_address: AHashMap<u64, String> = AHashMap::new();
for NamedExport {
name,
ordinal_index,
} in &parsed.named_exports
{
let lc = name.to_ascii_lowercase();
by_name.insert(lc, *ordinal_index);
}
let mut by_ordinal: Vec<Option<IndexedExport>> =
Vec::with_capacity(parsed.ordinal_targets.len());
for slot in &parsed.ordinal_targets {
let resolved = match slot {
None => None,
Some(ExportTarget::Direct { rva }) => {
let address = base.wrapping_add(*rva as u64);
Some(IndexedExport::Direct { address })
}
Some(ExportTarget::Forwarder { value }) => Some(IndexedExport::Forwarder {
value: value.clone(),
}),
};
by_ordinal.push(resolved);
}
for NamedExport {
name,
ordinal_index,
} in &parsed.named_exports
{
if let Some(Some(IndexedExport::Direct { address })) =
by_ordinal.get(*ordinal_index as usize)
{
by_address.entry(*address).or_insert_with(|| name.clone());
}
}
let mut display_names: Vec<(u32, String)> = Vec::new();
for NamedExport {
name,
ordinal_index,
} in &parsed.named_exports
{
display_names.push((*ordinal_index, name.clone()));
}
ModuleExportIndex {
module_name,
normalized_name,
base,
export_base: parsed.export_base,
by_name,
by_ordinal,
by_address,
display_names,
}
}
pub fn rebase(&mut self, new_base: u64) {
if self.base == new_base {
return;
}
self.by_address.clear();
let delta = new_base.wrapping_sub(self.base);
for slot in &mut self.by_ordinal {
if let Some(IndexedExport::Direct { address }) = slot {
*address = address.wrapping_add(delta);
}
}
for (ord_idx, name) in &self.display_names {
if let Some(Some(IndexedExport::Direct { address })) =
self.by_ordinal.get(*ord_idx as usize)
{
self.by_address
.entry(*address)
.or_insert_with(|| name.clone());
}
}
self.base = new_base;
}
pub fn resolve_name(&self, name_lc: &str) -> Option<&IndexedExport> {
self.by_name
.get(name_lc)
.and_then(|idx| self.by_ordinal.get(*idx as usize).and_then(|s| s.as_ref()))
}
pub fn resolve_ordinal(&self, ordinal: u32) -> Option<&IndexedExport> {
if ordinal < self.export_base {
return None;
}
let idx = ordinal - self.export_base;
self.by_ordinal.get(idx as usize).and_then(|s| s.as_ref())
}
pub fn resolve_address(&self, addr: u64) -> Option<&str> {
self.by_address.get(&addr).map(|s| s.as_str())
}
pub fn display_names_for_iter(&self) -> Vec<(String, u32)> {
self.display_names
.iter()
.map(|(idx, name)| (name.clone(), *idx))
.collect()
}
pub fn iter_for_dump(&self) -> Vec<(String, u64)> {
self.display_names
.iter()
.map(|(idx, name)| {
let va = self
.by_ordinal
.get(*idx as usize)
.and_then(|s| s.as_ref())
.and_then(IndexedExport::direct_address)
.unwrap_or(0);
(name.clone(), va)
})
.collect()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExportIndexRegistry {
by_name: AHashMap<String, ModuleExportIndex>,
by_base: AHashMap<u64, String>,
order: Vec<String>,
}
impl ExportIndexRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, index: ModuleExportIndex) {
let key = index.normalized_name.clone();
if let Some(existing) = self.by_name.get(&key) {
self.by_base.remove(&existing.base);
} else {
self.order.push(key.clone());
}
self.by_base.insert(index.base, key.clone());
self.by_name.insert(key, index);
}
pub fn remove(&mut self, module: &str) -> bool {
let key = normalize_module_name(module);
let Some(index) = self.by_name.remove(&key) else {
return false;
};
self.by_base.remove(&index.base);
if let Some(pos) = self.order.iter().position(|n| n == &key) {
self.order.remove(pos);
}
true
}
pub fn get_by_name(&self, module: &str) -> Option<&ModuleExportIndex> {
let key = normalize_module_name(module);
self.by_name.get(&key)
}
pub fn get_by_base(&self, base: u64) -> Option<&ModuleExportIndex> {
self.by_base.get(&base).and_then(|n| self.by_name.get(n))
}
pub fn iter_ordered(&self) -> impl Iterator<Item = &ModuleExportIndex> {
self.order.iter().filter_map(|n| self.by_name.get(n))
}
pub fn len(&self) -> usize {
self.by_name.len()
}
pub fn is_empty(&self) -> bool {
self.by_name.is_empty()
}
pub fn resolve_address(&self, addr: u64) -> Option<(String, String)> {
for module in self.iter_ordered() {
if let Some(name) = module.resolve_address(addr) {
return Some((module.normalized_name.clone(), name.to_string()));
}
}
None
}
pub fn resolve_name_in_module(&self, module: &str, name: &str) -> u64 {
let normalized = normalize_module_name(module);
let name_lc = normalize_export_name(name);
self.resolve_name_in_module_inner(
&normalized,
&name_lc,
&mut Vec::new(),
MAX_FORWARDER_DEPTH,
)
}
fn resolve_name_in_module_inner(
&self,
normalized_module: &str,
name_lc: &str,
visited: &mut Vec<(String, String)>,
depth: usize,
) -> u64 {
if depth == 0 {
return 0;
}
let Some(module) = self.by_name.get(normalized_module) else {
return 0;
};
let Some(target) = module.resolve_name(name_lc) else {
return 0;
};
match target {
IndexedExport::Direct { address } => *address,
IndexedExport::Forwarder { value } => {
let key = (normalized_module.to_string(), name_lc.to_string());
if visited.iter().any(|k| k == &key) {
return 0;
}
visited.push(key);
parse_forwarder_and_resolve(self, value, visited, depth - 1)
}
}
}
pub fn resolve_name_global(&self, name: &str) -> u64 {
let name_lc = normalize_export_name(name);
for module in self.iter_ordered() {
if let Some(target) = module.resolve_name(&name_lc) {
let addr = match target {
IndexedExport::Direct { address } => *address,
IndexedExport::Forwarder { value } => parse_forwarder_and_resolve(
self,
value,
&mut Vec::new(),
MAX_FORWARDER_DEPTH,
),
};
if addr != 0 {
return addr;
}
}
}
0
}
pub fn resolve_ordinal_by_base(&self, base: u64, ordinal: u32) -> u64 {
let Some(module) = self.get_by_base(base) else {
return 0;
};
match module.resolve_ordinal(ordinal) {
Some(IndexedExport::Direct { address }) => *address,
Some(IndexedExport::Forwarder { value }) => {
parse_forwarder_and_resolve(self, value, &mut Vec::new(), MAX_FORWARDER_DEPTH)
}
None => 0,
}
}
pub fn resolve_ordinal_in_module(&self, module: &str, ordinal: u32) -> u64 {
let key = normalize_module_name(module);
let Some(index) = self.by_name.get(&key) else {
return 0;
};
match index.resolve_ordinal(ordinal) {
Some(IndexedExport::Direct { address }) => *address,
Some(IndexedExport::Forwarder { value }) => {
parse_forwarder_and_resolve(self, value, &mut Vec::new(), MAX_FORWARDER_DEPTH)
}
None => 0,
}
}
pub fn resolve_name_by_base(&self, base: u64, name: &str) -> u64 {
let Some(module) = self.get_by_base(base) else {
return 0;
};
let name_lc = normalize_export_name(name);
let Some(target) = module.resolve_name(&name_lc) else {
return 0;
};
match target {
IndexedExport::Direct { address } => *address,
IndexedExport::Forwarder { value } => {
parse_forwarder_and_resolve(self, value, &mut Vec::new(), MAX_FORWARDER_DEPTH)
}
}
}
}
fn parse_forwarder_and_resolve(
registry: &ExportIndexRegistry,
value: &str,
visited: &mut Vec<(String, String)>,
depth: usize,
) -> u64 {
if depth == 0 {
return 0;
}
let Some((dll_part, sym_part)) = value.split_once('.') else {
return 0;
};
let normalized_dll = normalize_module_name(dll_part);
if let Some(stripped) = sym_part.strip_prefix('#') {
let Ok(ordinal) = stripped.parse::<u32>() else {
return 0;
};
if let Some(module) = registry.by_name.get(&normalized_dll) {
match module.resolve_ordinal(ordinal) {
Some(IndexedExport::Direct { address }) => return *address,
Some(IndexedExport::Forwarder { value: fwd_value }) => {
let key = (normalized_dll.clone(), format!("#{}", ordinal));
if visited.iter().any(|k| k == &key) {
return 0;
}
visited.push(key);
return parse_forwarder_and_resolve(registry, &fwd_value, visited, depth - 1);
}
None => return 0,
}
}
return 0;
}
registry.resolve_name_in_module_inner(
&normalized_dll,
&sym_part.to_ascii_lowercase(),
visited,
depth,
)
}
pub fn normalize_module_name(name: &str) -> String {
let n = name.trim().to_ascii_lowercase();
let n = n.rsplit_once('\\').map(|(_, b)| b).unwrap_or(&n);
let n = n.rsplit_once('/').map(|(_, b)| b).unwrap_or(n);
n.strip_suffix(".dll").unwrap_or(n).to_string()
}
pub fn normalize_export_name(name: &str) -> String {
name.trim().to_ascii_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
use rs_header::pe::export_index::build_export_index;
use rs_header::pe::shared::ImageSectionHeader;
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 build_raw() -> (Vec<u8>, Vec<ImageSectionHeader>, u32, u32) {
let mut raw = vec![0u8; 0x200];
let export_off = 0x040;
let func_off = 0x080;
let name_off_table = 0x090;
let ord_off = 0x0a8;
let fwd_str_off = 0x0c0;
let name_a_off = 0x0d0;
let name_b_off = 0x0e0;
let export_va: u32 = 0x1040;
let func_va: u32 = 0x1080;
let name_table_va: u32 = 0x1090;
let ord_table_va: u32 = 0x10a8;
let name_a_va: u32 = 0x10d0;
let name_b_va: u32 = 0x10e0;
raw[export_off + 16..export_off + 20].copy_from_slice(&1u32.to_le_bytes()); raw[export_off + 20..export_off + 24].copy_from_slice(&2u32.to_le_bytes()); raw[export_off + 24..export_off + 28].copy_from_slice(&2u32.to_le_bytes()); raw[export_off + 28..export_off + 32].copy_from_slice(&func_va.to_le_bytes());
raw[export_off + 32..export_off + 36].copy_from_slice(&name_table_va.to_le_bytes());
raw[export_off + 36..export_off + 40].copy_from_slice(&ord_table_va.to_le_bytes());
let func0_rva: u32 = 0x1500;
let func1_rva: u32 = 0x10c0;
raw[func_off..func_off + 4].copy_from_slice(&func0_rva.to_le_bytes());
raw[func_off + 4..func_off + 8].copy_from_slice(&func1_rva.to_le_bytes());
raw[name_off_table..name_off_table + 4].copy_from_slice(&name_a_va.to_le_bytes());
raw[name_off_table + 4..name_off_table + 8].copy_from_slice(&name_b_va.to_le_bytes());
raw[ord_off..ord_off + 2].copy_from_slice(&0u16.to_le_bytes()); raw[ord_off + 2..ord_off + 4].copy_from_slice(&1u16.to_le_bytes());
let s = b"KERNELBASE.HeapAlloc\0";
raw[fwd_str_off..fwd_str_off + s.len()].copy_from_slice(s);
let s = b"A\0";
raw[name_a_off..name_a_off + s.len()].copy_from_slice(s);
let s = b"B\0";
raw[name_b_off..name_b_off + s.len()].copy_from_slice(s);
let sections = vec![section(0x1000, 0, raw.len() as u32)];
(raw, sections, export_va, 0x100)
}
fn parsed() -> ExportIndexData {
let (raw, sections, va, size) = build_raw();
build_export_index(&raw, §ions, va, size).expect("parse")
}
fn make_index(base: u64, module: &str) -> ModuleExportIndex {
let normalized = normalize_module_name(module);
ModuleExportIndex::from_parsed(module.to_string(), normalized, base, &parsed())
}
#[test]
fn register_and_get_by_name() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
assert!(reg.get_by_name("kernel32").is_some());
assert!(reg.get_by_name("KERNEL32.DLL").is_some());
assert!(reg.get_by_name("kernelbase").is_none());
}
#[test]
fn get_by_base_returns_correct_module() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
assert!(reg.get_by_base(0x10000).is_some());
assert!(reg.get_by_base(0x20000).is_none());
}
#[test]
fn replacement_removes_old_base() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
reg.register(make_index(0x20000, "kernel32.dll"));
assert!(reg.get_by_base(0x10000).is_none(), "old base must be gone");
assert!(reg.get_by_base(0x20000).is_some());
assert_eq!(reg.len(), 1, "still one entry");
}
#[test]
fn removal_drops_both_maps_and_order() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
reg.register(make_index(0x20000, "ntdll.dll"));
assert!(reg.remove("kernel32"));
assert!(reg.get_by_name("kernel32").is_none());
assert!(reg.get_by_base(0x10000).is_none());
assert_eq!(reg.len(), 1);
let names: Vec<&str> = reg
.iter_ordered()
.map(|m| m.normalized_name.as_str())
.collect();
assert_eq!(names, vec!["ntdll"]);
}
#[test]
fn named_resolution_is_case_insensitive() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
assert_eq!(reg.resolve_name_in_module("kernel32", "A"), 0x11500);
assert_eq!(reg.resolve_name_in_module("kernel32", "a"), 0x11500);
}
#[test]
fn global_search_returns_first_match() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
reg.register(make_index(0x30000, "ntdll.dll"));
assert_eq!(reg.resolve_name_global("A"), 0x11500);
}
#[test]
fn base_lookup_honors_handle() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
assert_eq!(reg.resolve_name_by_base(0x10000, "A"), 0x11500);
assert_eq!(reg.resolve_name_by_base(0x99999, "A"), 0);
}
#[test]
fn ordinal_resolution_uses_export_base() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
assert_eq!(reg.resolve_ordinal_by_base(0x10000, 1), 0x11500);
assert_eq!(reg.resolve_ordinal_by_base(0x10000, 0), 0);
}
#[test]
fn address_to_name_reverse_lookup() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
assert_eq!(
reg.resolve_address(0x11500),
Some(("kernel32".to_string(), "A".to_string()))
);
assert_eq!(reg.resolve_address(0x99999), None);
}
#[test]
fn forwarder_string_preserved_when_target_missing() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
assert_eq!(reg.resolve_name_in_module("kernel32", "B"), 0);
}
#[test]
fn forwarder_resolves_to_registered_target() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
reg.register(make_index(0x50000, "kernelbase.dll"));
assert_eq!(reg.resolve_name_in_module("kernel32", "B"), 0);
}
#[test]
fn ordering_preserved_across_replace() {
let mut reg = ExportIndexRegistry::new();
reg.register(make_index(0x10000, "kernel32.dll"));
reg.register(make_index(0x20000, "ntdll.dll"));
reg.register(make_index(0x30000, "kernel32.dll")); let names: Vec<&str> = reg
.iter_ordered()
.map(|m| m.normalized_name.as_str())
.collect();
assert_eq!(names, vec!["kernel32", "ntdll"]);
}
#[test]
fn module_name_normalization_handles_paths_and_case() {
assert_eq!(normalize_module_name("KERNEL32.DLL"), "kernel32");
assert_eq!(
normalize_module_name("C:\\Windows\\kernel32.dll"),
"kernel32"
);
assert_eq!(normalize_module_name("/usr/lib/kernel32.dll"), "kernel32");
assert_eq!(normalize_module_name("kernel32"), "kernel32");
assert_eq!(normalize_module_name(" kernelbase.DLL "), "kernelbase");
}
#[test]
fn parsed_rebase_updates_addresses() {
let mut idx = make_index(0x10000, "kernel32.dll");
assert_eq!(
idx.resolve_name("a").unwrap().direct_address(),
Some(0x11500)
);
idx.rebase(0x40000);
assert_eq!(
idx.resolve_name("a").unwrap().direct_address(),
Some(0x41500)
);
assert_eq!(idx.base, 0x40000);
assert_eq!(idx.resolve_address(0x41500), Some("A"));
}
#[test]
fn unindexed_module_resolves_to_zero() {
let reg = ExportIndexRegistry::new();
assert_eq!(reg.resolve_name_in_module("nope", "A"), 0);
assert_eq!(reg.resolve_name_global("A"), 0);
}
#[test]
fn parsed_named_exports_preserve_case_in_address_map() {
let idx = make_index(0x10000, "kernel32.dll");
assert_eq!(idx.resolve_address(0x11500), Some("A"));
}
}