use anyhow::{bail, Context, Result};
use minidump::{Minidump, MinidumpMemory64List, MinidumpModuleList};
use std::fs;
use std::path::{Path, PathBuf};
pub fn handle_minidump_to_exe(input: &Path, output: Option<PathBuf>, base: &str) -> Result<()> {
let base_addr = if base.starts_with("0x") || base.starts_with("0X") {
u64::from_str_radix(&base[2..], 16)
.with_context(|| format!("Invalid hex base address: {}", base))?
} else {
base.parse::<u64>()
.with_context(|| format!("Invalid base address: {}", base))?
};
println!("Extracting PE from minidump: {:?}", input);
println!("Base address: {:#x}", base_addr);
let dump = Minidump::read_path(input)
.with_context(|| format!("Failed to read minidump: {:?}", input))?;
if let Ok(modules) = dump.get_stream::<MinidumpModuleList>() {
println!("\nModules in target range:");
for module in modules.iter() {
if module.raw.base_of_image >= base_addr
&& module.raw.base_of_image < base_addr + 0x100000000
{
println!(
" {:#x} - {:#x}: {}",
module.raw.base_of_image,
module.raw.base_of_image + module.raw.size_of_image as u64,
module.name
);
}
}
}
let memory = dump
.get_stream::<MinidumpMemory64List>()
.context("Failed to get Memory64List from minidump (not a full dump?)")?;
println!("\nSearching for PE at base {:#x}...", base_addr);
let mut regions: Vec<_> = memory
.iter()
.filter(|mem| mem.base_address >= base_addr && mem.base_address < base_addr + 0x40000000)
.collect();
regions.sort_by_key(|m| m.base_address);
if regions.is_empty() {
bail!("No memory regions found at base address {:#x}", base_addr);
}
println!("Found {} memory regions in PE range", regions.len());
let first_region = regions
.iter()
.find(|m| m.base_address == base_addr)
.context("No memory region at exact base address")?;
let header_data = first_region.bytes;
if header_data.len() < 64 || &header_data[0..2] != b"MZ" {
bail!("No valid MZ header found at base address");
}
println!("Found MZ header at {:#x}", base_addr);
let e_lfanew = u32::from_le_bytes(header_data[60..64].try_into().unwrap()) as usize;
if e_lfanew + 4 + 20 + 60 > header_data.len() {
bail!("PE header extends beyond first memory region");
}
let optional_header_offset = e_lfanew + 4 + 20;
let size_of_image = u32::from_le_bytes(
header_data[optional_header_offset + 56..optional_header_offset + 60]
.try_into()
.unwrap(),
) as u64;
println!(
"Size of image: {} bytes ({:.1} MB)",
size_of_image,
size_of_image as f64 / 1024.0 / 1024.0
);
let mut image = vec![0u8; size_of_image as usize];
let mut bytes_copied: u64 = 0;
for region in ®ions {
let offset = region.base_address - base_addr;
if offset >= size_of_image {
continue;
}
let end = (offset + region.bytes.len() as u64).min(size_of_image);
let copy_len = (end - offset) as usize;
image[offset as usize..offset as usize + copy_len]
.copy_from_slice(®ion.bytes[..copy_len]);
bytes_copied += copy_len as u64;
}
println!(
"Copied {} bytes ({:.1} MB), {} gaps",
bytes_copied,
bytes_copied as f64 / 1024.0 / 1024.0,
size_of_image.saturating_sub(bytes_copied)
);
let output_path = output.unwrap_or_else(|| {
let stem = input.file_stem().unwrap_or_default().to_string_lossy();
input.with_file_name(format!("{}_extracted.exe", stem))
});
fs::write(&output_path, &image)
.with_context(|| format!("Failed to write: {:?}", output_path))?;
println!("\nExtracted PE written to: {:?}", output_path);
println!("Note: This is a memory image - data directories may have invalid RVAs");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handle_minidump_to_exe_missing_file() {
let result =
handle_minidump_to_exe(Path::new("/nonexistent/dump.dmp"), None, "0x140000000");
assert!(result.is_err());
}
}