use crate::schema::{Import, PeInfo, Section};
use goblin::pe::PE;
use md5::{Digest, Md5};
fn machine_name(machine: u16) -> String {
match machine {
0x8664 => "x64 (AMD64)".to_string(),
0x014C => "x86 (I386)".to_string(),
0xAA64 => "ARM64".to_string(),
0x01C0 => "ARM".to_string(),
0x01C4 => "ARMNT".to_string(),
other => format!("0x{:04X}", other),
}
}
fn strip_lib_ext(dll_lower: &str) -> &str {
for ext in [".dll", ".ocx", ".sys"] {
if let Some(base) = dll_lower.strip_suffix(ext) {
return base;
}
}
dll_lower
}
pub fn parse_pe(data: &[u8], file_path: &str) -> Result<PeInfo, String> {
let pe = PE::parse(data).map_err(|e| format!("PE parse error: {e}"))?;
let machine = machine_name(pe.header.coff_header.machine);
let sections: Vec<Section> = pe
.sections
.iter()
.map(|s| Section {
name: s.name().unwrap_or("?").to_string(),
virtual_size: s.virtual_size,
virtual_address: s.virtual_address,
size_of_raw_data: s.size_of_raw_data,
pointer_to_raw_data: s.pointer_to_raw_data,
})
.collect();
let mut imports: Vec<Import> = Vec::new();
let mut imphash_entries: Vec<String> = Vec::new();
for import in &pe.imports {
let dll_display = import.dll.to_string();
let dll_lower = import.dll.to_lowercase();
let lib_base = strip_lib_ext(&dll_lower).to_string();
let by_ordinal = import.name.is_empty() || import.name.starts_with("ORDINAL ");
let func = if by_ordinal {
format!("ord{}", import.ordinal)
} else {
import.name.to_lowercase()
};
imphash_entries.push(format!("{lib_base}.{func}"));
let func_display = if by_ordinal {
format!("ord{}", import.ordinal)
} else {
import.name.to_string()
};
match imports.iter_mut().find(|i| i.dll == dll_display) {
Some(existing) => existing.functions.push(func_display),
None => imports.push(Import {
dll: dll_display,
functions: vec![func_display],
}),
}
}
let imp_hash = if imphash_entries.is_empty() {
None
} else {
let mut hasher = Md5::new();
hasher.update(imphash_entries.join(",").as_bytes());
Some(format!("{:x}", hasher.finalize()))
};
let resource_size = pe
.header
.optional_header
.as_ref()
.and_then(|oh| oh.data_directories.get_resource_table())
.map(|d| d.size)
.unwrap_or(0);
let last_section_end = pe
.sections
.iter()
.map(|s| (s.pointer_to_raw_data as u64) + (s.size_of_raw_data as u64))
.max()
.unwrap_or(0);
let file_len = data.len() as u64;
let overlay_size = file_len.saturating_sub(last_section_end);
let has_overlay = overlay_size > 0;
let overlay_offset = if has_overlay {
Some(last_section_end)
} else {
None
};
Ok(PeInfo {
file: file_path.to_string(),
file_size: file_len,
machine,
is_pe32_plus: pe.is_64,
entry_point_rva: pe.entry as u32,
entry_point_hex: format!("0x{:X}", pe.entry),
image_base: pe.image_base as u64,
section_count: sections.len() as u32,
imported_dll_count: imports.len() as u32,
sections,
imports,
imp_hash,
resource_size,
has_overlay,
overlay_offset,
overlay_size,
mapped_offset: None, mapped_section: None,
warnings: Vec::new(),
})
}
pub fn offset_to_section(data: &[u8], offset: u64) -> Result<Option<String>, String> {
let pe = PE::parse(data).map_err(|e| format!("PE parse error: {e}"))?;
for section in &pe.sections {
let start = section.pointer_to_raw_data as u64;
let end = start + section.size_of_raw_data as u64;
if section.size_of_raw_data > 0 && offset >= start && offset < end {
return Ok(Some(section.name().unwrap_or("?").to_string()));
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_known_library_extensions() {
assert_eq!(strip_lib_ext("kernel32.dll"), "kernel32");
assert_eq!(strip_lib_ext("comctl32.ocx"), "comctl32");
assert_eq!(strip_lib_ext("driver.sys"), "driver");
assert_eq!(strip_lib_ext("noextension"), "noextension");
}
#[test]
fn maps_known_machine_ids_to_display_strings() {
assert_eq!(machine_name(0x8664), "x64 (AMD64)");
assert_eq!(machine_name(0x014C), "x86 (I386)");
assert_eq!(machine_name(0xAA64), "ARM64");
assert!(machine_name(0x1234).starts_with("0x"));
}
#[test]
fn rejects_a_non_pe_buffer() {
assert!(parse_pe(b"this is not a PE image at all", "sample").is_err());
}
}