//! Chunked sweeping of a remote address space, plus the two things we sweep
//! for: byte patterns and printable strings.
//!
//! # Why chunking, and the off-by-one that ruins scanners
//!
//! The target has roughly 3 GB resident. Reading a region in one allocation is
//! wasteful and can fail outright, so regions are walked in 4 MiB chunks.
//!
//! The classic bug in every hand-rolled scanner is that a pattern straddling a
//! chunk boundary is never found: the tail of chunk N holds the first few bytes
//! and the head of chunk N+1 holds the rest, and neither buffer contains the
//! whole thing. The fix is to overlap consecutive chunks by `pattern_len - 1`
//! bytes.
//!
//! That specific overlap is exactly right, and it is worth showing why it is
//! neither too small nor too large. Let a chunk cover `[0, n)` and the pattern
//! have length `P`. A match starting at index `s` occupies `s ..= s + P - 1`, so
//! the last match fully inside the chunk starts at `s = n - P`. Any match
//! starting at `s > n - P` runs off the end and must be caught by the next
//! chunk, so the next chunk has to begin at or before `n - P + 1`. Advancing by
//! `n - (P - 1)` starts it at precisely `n - P + 1`:
//!
//!   * Nothing is missed: every straddling match starts at `s >= n - P + 1`,
//!     which is inside the next chunk.
//!   * Nothing is double-reported: the first index of the overlap is
//!     `n - P + 1`, which is strictly greater than `n - P`, the last index that
//!     can host a complete match in this chunk. The two windows of *reportable*
//!     match starts are disjoint even though the byte windows overlap.
//!
//! Overlapping by `P` instead would report every boundary-straddling match
//! twice; overlapping by `P - 2` would miss one alignment. Hence `P - 1`.
//!
//! # Holes
//!
//! A region marked readable in `/proc/<pid>/maps` is frequently not readable in
//! practice: guard pages, Wine's special mappings, and pages Denuvo has not
//! faulted in all return `EIO`. These are counted and stepped over a page at a
//! time, never propagated as errors, because in a sweep this size they are
//! routine. The counts are reported so the user knows the sweep was partial and
//! does not read a zero-hit result as proof of absence.

use crate::image::Module;
use crate::maps::Region;
use crate::mem::{ChunkRead, ProcMem, PAGE};

pub const CHUNK: usize = 4 * 1024 * 1024;

#[derive(Default, Debug)]
pub struct SweepStats {
    pub regions_scanned: usize,
    /// Regions from which not a single byte could be read.
    pub regions_skipped: usize,
    /// Individual chunk reads that hit an unreadable hole.
    pub holes: usize,
    pub bytes_read: u64,
}

impl SweepStats {
    pub fn summary(&self) -> String {
        format!(
            "scanned {} regions ({}), skipped {} unreadable regions, {} holes stepped over",
            self.regions_scanned,
            crate::maps::human(self.bytes_read),
            self.regions_skipped,
            self.holes
        )
    }
}

fn align_up(va: u64, align: u64) -> u64 {
    va.div_ceil(align) * align
}

/// Walk one region in chunks, invoking `f(chunk_va, bytes, contiguous)`.
///
/// `contiguous` is true when this chunk's data continues directly from the
/// previous callback with no gap, which string extraction needs in order to
/// join a run that spans a boundary. `overlap` is `pattern_len - 1` for pattern
/// search and 0 for stateful scanners that track continuity themselves.
///
/// Returns early (`false`) if `f` signals it has seen enough.
fn sweep_region<F>(
    mem: &ProcMem,
    region: &Region,
    overlap: usize,
    buf: &mut [u8],
    stats: &mut SweepStats,
    f: &mut F,
) -> bool
where
    F: FnMut(u64, &[u8], bool) -> bool,
{
    let mut pos = region.start;
    let mut contiguous = false;
    let mut read_anything = false;

    while pos < region.end {
        let want = (buf.len() as u64).min(region.end - pos) as usize;
        match mem.read_chunk(pos, &mut buf[..want]) {
            ChunkRead::Hole => {
                stats.holes += 1;
                contiguous = false;
                // Step to the next page; the current one is unreadable.
                pos = align_up(pos + 1, PAGE);
            }
            ChunkRead::Got(n) => {
                read_anything = true;
                stats.bytes_read += n as u64;
                if !f(pos, &buf[..n], contiguous) {
                    return false;
                }
                if pos + n as u64 >= region.end {
                    break;
                }
                if n < want {
                    // Short read: an unmapped hole begins at pos + n. No pattern
                    // can span a hole, so no overlap is needed here; resume on
                    // the next page boundary.
                    contiguous = false;
                    pos = align_up(pos + n as u64 + 1, PAGE);
                } else {
                    if n <= overlap {
                        break; // cannot make forward progress
                    }
                    contiguous = true;
                    pos += (n - overlap) as u64;
                }
            }
        }
    }

    if read_anything {
        stats.regions_scanned += 1;
    } else {
        stats.regions_skipped += 1;
    }
    true
}

/// Which regions a sweep should touch.
pub fn scan_targets(regions: &[Region], module: Option<&Module>, anon_only: bool) -> Vec<Region> {
    regions
        .iter()
        .filter(|r| r.readable() && !r.pseudo())
        .filter(|r| !anon_only || r.anonymous())
        .filter_map(|r| match module {
            None => Some(r.clone()),
            // Clip the region to the module's image span rather than dropping
            // it: under Wine a module's sections live in large anonymous
            // regions that may extend past the image.
            Some(m) => {
                let start = r.start.max(m.base);
                let end = r.end.min(m.end());
                if start < end {
                    let mut clipped = (*r).clone();
                    clipped.start = start;
                    clipped.end = end;
                    Some(clipped)
                } else {
                    None
                }
            }
        })
        .collect::<Vec<_>>()
}

/// Search every target region for `pattern`. Calls `hit(va)` per match.
pub fn find_pattern<F>(
    mem: &ProcMem,
    targets: &[Region],
    pattern: &[u8],
    max: Option<usize>,
    mut hit: F,
) -> SweepStats
where
    F: FnMut(u64),
{
    let mut stats = SweepStats::default();
    if pattern.is_empty() {
        return stats;
    }
    let finder = memchr::memmem::Finder::new(pattern);
    let overlap = pattern.len() - 1;
    // The buffer must comfortably exceed the overlap or progress stalls.
    let mut buf = vec![0u8; CHUNK.max(pattern.len() * 4)];
    let mut found = 0usize;

    for region in targets {
        let keep_going = sweep_region(
            mem,
            region,
            overlap,
            &mut buf,
            &mut stats,
            &mut |base, data, _contiguous| {
                for off in finder.find_iter(data) {
                    hit(base + off as u64);
                    found += 1;
                    if max.is_some_and(|m| found >= m) {
                        return false;
                    }
                }
                true
            },
        );
        if !keep_going {
            break;
        }
    }
    stats
}

fn printable(b: u8) -> bool {
    (0x20..=0x7e).contains(&b)
}

/// Extracts printable runs, carrying an unfinished run across contiguous chunks
/// so a string straddling a boundary is still emitted whole.
struct StringScanner {
    utf16: bool,
    min: usize,
    run: Vec<u8>,
    run_start: u64,
    open: bool,
    /// UTF-16 only: a low byte at the very end of a chunk whose high byte will
    /// arrive in the next one.
    carry: Option<(u64, u8)>,
}

impl StringScanner {
    fn new(utf16: bool, min: usize) -> Self {
        Self {
            utf16,
            min,
            run: Vec::with_capacity(256),
            run_start: 0,
            open: false,
            carry: None,
        }
    }

    fn flush<F: FnMut(u64, &str)>(&mut self, emit: &mut F) {
        if self.open && self.run.len() >= self.min {
            // Runs are printable ASCII by construction, so this cannot fail.
            if let Ok(s) = std::str::from_utf8(&self.run) {
                emit(self.run_start, s);
            }
        }
        self.run.clear();
        self.open = false;
    }

    fn push<F: FnMut(u64, &str)>(&mut self, va: u64, b: u8, emit: &mut F) {
        if !self.open {
            self.open = true;
            self.run_start = va;
        }
        self.run.push(b);
        // Guard against a pathological all-printable megabyte eating memory.
        if self.run.len() >= 4096 {
            self.flush(emit);
        }
    }

    fn feed<F: FnMut(u64, &str)>(
        &mut self,
        base: u64,
        data: &[u8],
        contiguous: bool,
        emit: &mut F,
    ) {
        if !contiguous {
            self.flush(emit);
            self.carry = None;
        }
        if self.utf16 {
            self.feed_utf16(base, data, emit);
        } else {
            for (i, &b) in data.iter().enumerate() {
                if printable(b) {
                    self.push(base + i as u64, b, emit);
                } else {
                    self.flush(emit);
                }
            }
        }
    }

    fn feed_utf16<F: FnMut(u64, &str)>(&mut self, base: u64, data: &[u8], emit: &mut F) {
        let mut i = 0usize;
        // A pair split across the chunk boundary: complete it if the high byte
        // is the expected 0x00, otherwise the run ends here.
        if let Some((addr, lo)) = self.carry.take() {
            if data.first() == Some(&0) && printable(lo) {
                self.push(addr, lo, emit);
                i = 1;
            } else {
                self.flush(emit);
            }
        }
        while i + 1 < data.len() {
            let (lo, hi) = (data[i], data[i + 1]);
            if hi == 0 && printable(lo) {
                self.push(base + i as u64, lo, emit);
                i += 2;
            } else {
                self.flush(emit);
                i += 1;
            }
        }
        if i < data.len() {
            self.carry = Some((base + i as u64, data[i]));
        }
    }
}

/// Extract strings from every target region. Calls `emit(va, text)`.
pub fn find_strings<F>(
    mem: &ProcMem,
    targets: &[Region],
    utf16: bool,
    min: usize,
    grep: Option<&str>,
    max: Option<usize>,
    mut emit: F,
) -> SweepStats
where
    F: FnMut(u64, &str),
{
    let mut stats = SweepStats::default();
    let mut buf = vec![0u8; CHUNK];
    let grep_lower = grep.map(|g| g.to_ascii_lowercase());
    let mut count = 0usize;

    for region in targets {
        let mut scanner = StringScanner::new(utf16, min);
        let mut stop = false;
        // overlap 0: the scanner tracks continuity itself via `contiguous`.
        let keep_going = sweep_region(
            mem,
            region,
            0,
            &mut buf,
            &mut stats,
            &mut |base, data, contiguous| {
                scanner.feed(base, data, contiguous, &mut |va, s| {
                    let matches = match &grep_lower {
                        Some(g) => s.to_ascii_lowercase().contains(g.as_str()),
                        None => true,
                    };
                    if matches {
                        emit(va, s);
                        count += 1;
                        if max.is_some_and(|m| count >= m) {
                            stop = true;
                        }
                    }
                });
                !stop
            },
        );
        scanner.flush(&mut |va, s| {
            let matches = match &grep_lower {
                Some(g) => s.to_ascii_lowercase().contains(g.as_str()),
                None => true,
            };
            if matches {
                emit(va, s);
            }
        });
        if !keep_going || stop {
            break;
        }
    }
    stats
}
