//! Read-only access to another process's address space.
//!
//! # The safety property this module exists to guarantee
//!
//! A live FIFA 17 session may be running while this tool is used. Corrupting it
//! costs the user their progress and their patience. So the guarantee here is
//! structural, not a matter of being careful:
//!
//!   * `/proc/<pid>/mem` is opened with [`File::open`], which is `O_RDONLY`.
//!     There is no [`std::fs::OpenOptions`] anywhere in this crate.
//!   * [`ProcMem`] exposes `&self` read methods only. It hands out no `&mut File`
//!     and no raw fd, so no caller outside this module can upgrade the handle.
//!   * Nothing in the crate calls `ptrace`, sends a signal, or writes to any
//!     path under `/proc`.
//!
//! Even if a caller tried to write, the kernel would reject it on an `O_RDONLY`
//! descriptor. The type system and the open mode agree, which is the point.
//!
//! # Why pread and not seek + read
//!
//! [`FileExt::read_at`] is `pread(2)`: it takes the offset as an argument
//! instead of mutating a shared file cursor. That means a `&ProcMem` can be
//! shared across threads later without a mutex and without one thread's seek
//! corrupting another's read. It also removes a whole class of "forgot to seek"
//! bugs. There is never a reason to prefer seek+read here.

use std::fs::File;
use std::io;
use std::os::unix::fs::FileExt;

/// The page size we assume when stepping over an unreadable hole. Every x86-64
/// mapping is a multiple of this, so it is a safe granularity for recovery.
pub const PAGE: u64 = 4096;

/// A read-only handle on a process's memory.
pub struct ProcMem {
    file: File,
}

/// What a single chunk read produced.
pub enum ChunkRead {
    /// `n` bytes landed in the buffer. May be shorter than requested when the
    /// read ran into an unmapped hole partway through.
    Got(usize),
    /// Nothing readable at this address at all.
    Hole,
}

impl ProcMem {
    /// Open the target read-only. See the module docs for why this is
    /// `File::open` and must stay that way.
    pub fn open(pid: i32) -> io::Result<Self> {
        let file = File::open(format!("/proc/{pid}/mem")).map_err(|e| {
            io::Error::new(
                e.kind(),
                format!("opening /proc/{pid}/mem: {e} (same-user or CAP_SYS_PTRACE required)"),
            )
        })?;
        Ok(Self { file })
    }

    /// Best-effort read. Never fatal: a hole reports [`ChunkRead::Hole`] rather
    /// than propagating an error, because in a 3 GB sweep unreadable regions are
    /// the normal case, not an exceptional one.
    ///
    /// Guard pages, Wine's special mappings and pages Denuvo has not faulted in
    /// are all marked readable in `/proc/<pid>/maps` yet return `EIO` here. The
    /// caller counts these and reports the total so the user knows the sweep was
    /// partial.
    pub fn read_chunk(&self, va: u64, buf: &mut [u8]) -> ChunkRead {
        match self.file.read_at(buf, va) {
            Ok(0) | Err(_) => ChunkRead::Hole,
            Ok(n) => ChunkRead::Got(n),
        }
    }

    /// Strict read for cases where a short read is genuinely an error, such as
    /// an explicit `futmem read <va> <len>` the user asked for by hand.
    pub fn read_exact(&self, va: u64, len: usize) -> io::Result<Vec<u8>> {
        let mut buf = vec![0u8; len];
        self.file.read_exact_at(&mut buf, va).map_err(|e| {
            io::Error::new(
                e.kind(),
                format!("reading {len} bytes at {va:#x}: {e} (address may be unmapped)"),
            )
        })?;
        Ok(buf)
    }

    /// Read up to `len` bytes, returning however many were actually available.
    /// Used for printing context around a hit that sits near the end of a region.
    pub fn read_partial(&self, va: u64, len: usize) -> Vec<u8> {
        let mut buf = vec![0u8; len];
        match self.file.read_at(&mut buf, va) {
            Ok(n) => {
                buf.truncate(n);
                buf
            }
            Err(_) => Vec::new(),
        }
    }
}
