//! Turning `/proc/<pid>/maps` lines into a usable module table, and turning an
//! address back into `module+offset`.
//!
//! # The Wine mapping gotcha this module exists to work around
//!
//! Under Wine, only a PE's 4 KiB header stays file-backed. Wine copies every
//! section into ANONYMOUS memory. So `grep CardsDLL /proc/<pid>/maps` returns
//! exactly one line, 4 KiB long, and a module table built naively from path
//! grouping will report CardsDLL as a 4 KiB module. It is really 0x31d000 bytes.
//! An agent who trusts the maps extent concludes the module is "barely mapped"
//! and gives up, or computes a wrong module size and mis-attributes every hit.
//!
//! The fix: read `SizeOfImage` out of the live PE header at the module base.
//! That field is authoritative for the module's real extent, and the header is
//! the one part of the image that is reliably readable.
//!
//! # Deriving the slide automatically
//!
//! Wine rewrites the `ImageBase` field of the *live* header to the actual load
//! address, so the live header cannot tell us where the module wanted to load.
//! The on-disk file still can, and the maps line gives us its path. Reading the
//! on-disk `ImageBase` and subtracting gives the relocation slide:
//!
//! ```text
//! slide = live_base - disk_image_base
//! live_va = static_va + slide
//! ```
//!
//! For CardsDLL that is `0x6ffffc140000 - 0x180000000 = 0x6ffe7c140000`, the
//! number every Ghidra-derived address in this project has to be adjusted by.
//! Printing it removes the most error-prone manual step in the workflow.

use crate::maps::Region;
use crate::mem::ProcMem;
use std::fs;

#[derive(Debug, Clone)]
pub struct Module {
    /// Bare file name, e.g. `CardsDLL_Win64_retail.dll`.
    pub name: String,
    pub path: String,
    /// Lowest mapped address carrying this path. For a PE this is the header.
    pub base: u64,
    /// Highest address still carrying this path in the maps. Badly understates
    /// the truth under Wine; see the module docs.
    pub maps_end: u64,
    /// Number of separate maps lines mentioning this path.
    pub region_count: usize,
    /// `SizeOfImage` from the live PE header, the real extent.
    pub size_of_image: Option<u64>,
    /// `ImageBase` from the on-disk file: where the module was linked to load.
    pub disk_image_base: Option<u64>,
}

impl Module {
    /// Best available end address: PE-derived when we have it, maps otherwise.
    pub fn end(&self) -> u64 {
        match self.size_of_image {
            Some(size) => self.base + size,
            None => self.maps_end,
        }
    }

    /// The relocation slide: add this to a static (Ghidra) VA to get a live VA.
    pub fn slide(&self) -> Option<i128> {
        self.disk_image_base
            .map(|disk| self.base as i128 - disk as i128)
    }

    /// Is this actually a PE image, as opposed to a device node, font or `.nls`
    /// data file that merely happens to be mapped?
    pub fn is_pe(&self) -> bool {
        self.size_of_image.is_some()
    }

    /// Only PE images claim an address range.
    ///
    /// Without the `is_pe` guard this mis-attributes badly. `/dev/nvidia0` is
    /// mapped at many scattered addresses, so its min..max span covers gigabytes
    /// of unrelated anonymous memory, and every hit in there would be reported
    /// as `nvidia0+0x...`. A non-PE mapping only ever owns the exact regions
    /// listed for it in the maps, which `describe` handles as a fallback.
    pub fn contains(&self, va: u64) -> bool {
        self.is_pe() && va >= self.base && va < self.end()
    }
}

/// Little-endian scalar helpers. Returning `Option` keeps a truncated or
/// malformed header from panicking the whole run.
fn u16_at(buf: &[u8], off: usize) -> Option<u16> {
    buf.get(off..off + 2)
        .map(|s| u16::from_le_bytes([s[0], s[1]]))
}
fn u32_at(buf: &[u8], off: usize) -> Option<u32> {
    buf.get(off..off + 4)
        .map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
fn u64_at(buf: &[u8], off: usize) -> Option<u64> {
    buf.get(off..off + 8)
        .map(|s| u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]))
}

/// `SizeOfImage` and `ImageBase` from a PE header blob.
///
/// Layout: `e_lfanew` at 0x3c points at the `PE\0\0` signature; the 20-byte
/// COFF header follows; the optional header starts at signature+24. Within the
/// optional header `SizeOfImage` sits at 0x38 for both PE32 and PE32+ (the
/// layouts diverge only between 0x18 and 0x20). `ImageBase` is 8 bytes at 0x18
/// for PE32+ and 4 bytes at 0x1c for PE32.
fn parse_pe(buf: &[u8]) -> Option<(u64, u64)> {
    if buf.get(0..2)? != b"MZ" {
        return None;
    }
    let nt = u32_at(buf, 0x3c)? as usize;
    if buf.get(nt..nt + 4)? != b"PE\0\0" {
        return None;
    }
    let opt = nt + 24;
    let magic = u16_at(buf, opt)?;
    let size_of_image = u32_at(buf, opt + 0x38)? as u64;
    let image_base = match magic {
        0x20b => u64_at(buf, opt + 0x18)?,        // PE32+
        0x10b => u32_at(buf, opt + 0x1c)? as u64, // PE32
        _ => return None,
    };
    Some((size_of_image, image_base))
}

fn pe_from_disk(path: &str) -> Option<(u64, u64)> {
    // 4 KiB is more than enough for MZ + PE + optional header on any real image.
    let data = fs::read(path).ok()?;
    parse_pe(&data[..data.len().min(4096)])
}

/// Build the module table. Modules are returned sorted by base address.
pub fn modules(regions: &[Region], mem: &ProcMem) -> Vec<Module> {
    use std::collections::HashMap;
    let mut by_path: HashMap<&str, (u64, u64, usize)> = HashMap::new();

    for r in regions {
        let Some(path) = r.path.as_deref() else {
            continue;
        };
        if r.pseudo() {
            continue;
        }
        let entry = by_path.entry(path).or_insert((u64::MAX, 0, 0));
        entry.0 = entry.0.min(r.start);
        entry.1 = entry.1.max(r.end);
        entry.2 += 1;
    }

    let mut out: Vec<Module> = by_path
        .into_iter()
        .map(|(path, (base, maps_end, region_count))| {
            // The live header gives the true extent; the on-disk header gives
            // the link-time base, which is what the slide is measured against.
            let live = mem.read_partial(base, 4096);
            let live_pe = parse_pe(&live);
            let disk_pe = pe_from_disk(path);
            Module {
                name: path.rsplit('/').next().unwrap_or(path).to_string(),
                path: path.to_string(),
                base,
                maps_end,
                region_count,
                size_of_image: live_pe.map(|(s, _)| s).or(disk_pe.map(|(s, _)| s)),
                disk_image_base: disk_pe.map(|(_, b)| b),
            }
        })
        .collect();

    out.sort_by_key(|m| m.base);
    out
}

/// Case-insensitive lookup by name substring, e.g. `cardsdll`.
pub fn find_module<'a>(mods: &'a [Module], needle: &str) -> Option<&'a Module> {
    let needle = needle.to_ascii_lowercase();
    mods.iter()
        .find(|m| m.name.to_ascii_lowercase().contains(&needle))
}

/// Describe an address as `module+0xoff`, falling back to the region kind.
///
/// Checking module image spans BEFORE the region list is essential here: a hit
/// inside CardsDLL's `.rdata` lands in an anonymous region as far as the maps
/// are concerned, and would otherwise be reported as `anon`, throwing away the
/// single most useful piece of context.
pub fn describe(va: u64, mods: &[Module], regions: &[Region]) -> String {
    if let Some(m) = mods.iter().find(|m| m.contains(va)) {
        return format!("{}+{:#x}", m.name, va - m.base);
    }
    match regions.iter().find(|r| va >= r.start && va < r.end) {
        Some(r) => match r.path.as_deref() {
            Some(p) => format!("{}+{:#x}", p.rsplit('/').next().unwrap_or(p), va - r.start),
            None => format!("anon:{:#x}({})", r.start, r.perms),
        },
        None => "unmapped".to_string(),
    }
}
