//! Window into a *running* CS2 server — the runtime oracle that verifies the offline derivations against
//! ground truth. Needs ptrace access (same-user with `yama/ptrace_scope=0`, or `CAP_SYS_PTRACE`).
//!
//! **Mostly reading, but not only reading, and the difference is worth stating plainly.** The bulk of this
//! module reads `/proc/<pid>/mem`. Two things go further: `poke_bytes` writes bytes in with
//! `PTRACE_POKEDATA`, and [`call_remote`] ATTACHES, saves the main thread's registers, builds a call frame
//! and executes a function in the live process before restoring the thread exactly. Both exist because
//! some claims cannot be checked any other way — a lazy-init singleton is zeroed until something calls its
//! accessor — and both are used only against the narrow set of functions the derivation has already
//! measured as safe to call (nullary, `this`-only, no game state). Nothing is injected and nothing
//! persists: the process is left as it was found, and a faulting call is caught and the thread restored
//! rather than allowed to kill the server.
//!
//! Offline we resolve `.rela.dyn` by hand to recover as-loaded pointer values; the running process is
//! the authority on what those values actually are. So reading the same structures live and comparing
//! confirms both our relocation logic and the struct layout — and, because runtime-populated fields
//! (e.g. `m_pSchemaBinding`) are non-null live but zero on disk, proves we are reading live state.

use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs::File;
use std::os::unix::fs::FileExt;

pub struct LiveProcess {
    mem: File,
    bases: HashMap<String, u64>, // library filename -> load base (lowest mapping address)
    paths: HashMap<String, String>, // library filename -> the FULL path the process actually mapped
    writable: Vec<(u64, u64)>,   // rw anonymous regions (heap etc.) — where live objects live
    executable: Vec<(u64, u64)>, // r-x regions — where valid code/vtable-slot targets must land
}

/// The pathname field of a `/proc/<pid>/maps` line — everything after the fifth whitespace-delimited field.
/// It must NOT be read as "whatever follows the last whitespace": a mapped path may contain spaces (Steam's
/// default install directory is `.../common/Counter-Strike Global Offensive/...`), and taking the last field
/// yields a fragment that fails the leading-`/` test, so the library is silently dropped from the map and
/// every lookup for it then reports it as not mapped. Empty for an anonymous mapping.
fn maps_path(line: &str) -> &str {
    let mut rest = line;
    for _ in 0..5 {
        rest = rest.trim_start();
        match rest.find(char::is_whitespace) {
            Some(i) => rest = &rest[i..],
            None => return "",
        }
    }
    rest.trim_start()
}

/// The scheduler state character from `/proc/<pid>/stat` (`R`/`S`/`D`/`Z`/`T`/…), or `None` if the process
/// is gone entirely. Parsed from AFTER the final `)`, because the comm field is parenthesised and may itself
/// contain spaces and brackets — splitting the line on whitespace from the left gets this wrong for any
/// process whose name has a space in it.
fn proc_state(pid: u32) -> Option<char> {
    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
    stat[stat.rfind(')')? + 1..]
        .split_whitespace()
        .next()?
        .chars()
        .next()
}

impl LiveProcess {
    pub fn attach(pid: u32) -> Result<Self> {
        let maps = std::fs::read_to_string(format!("/proc/{pid}/maps"))
            .with_context(|| format!("read /proc/{pid}/maps (is pid {pid} running?)"))?;
        let mut bases: HashMap<String, u64> = HashMap::new();
        let mut paths: HashMap<String, String> = HashMap::new();
        let mut writable: Vec<(u64, u64)> = Vec::new();
        let mut executable: Vec<(u64, u64)> = Vec::new();
        for line in maps.lines() {
            // format: START-END perms offset dev inode  path
            let (range, rest) = match line.split_once(' ') {
                Some(x) => x,
                None => continue,
            };
            let perms = rest.split(' ').next().unwrap_or("");
            let path = maps_path(line);
            let Some((start, end)) = range.split_once('-').and_then(|(a, b)| {
                Some((
                    u64::from_str_radix(a, 16).ok()?,
                    u64::from_str_radix(b, 16).ok()?,
                ))
            }) else {
                continue;
            };
            if path.ends_with(".so") && path.starts_with('/') {
                let fname = path.rsplit('/').next().unwrap_or(path).to_string();
                paths
                    .entry(fname.clone())
                    .or_insert_with(|| path.to_string());
                bases
                    .entry(fname)
                    .and_modify(|b| *b = (*b).min(start))
                    .or_insert(start);
            }
            // writable anonymous memory = the heap where runtime objects (entities) are allocated
            if perms.starts_with("rw") && (path.is_empty() || path == "[heap]") {
                writable.push((start, end));
            }
            if perms.starts_with('r') && perms.contains('x') {
                executable.push((start, end));
            }
        }
        executable.sort_unstable();
        // Distinguish the two ways this fails, because they call for opposite responses and the kernel
        // reports BOTH as EACCES. If the process is gone, `/proc/<pid>` is gone with it — so check that
        // first: a server that CRASHED mid-derive otherwise reads as a permissions problem, and the
        // operator goes off tuning `ptrace_scope` for a fault that had nothing to do with it. (Seen: a
        // CS2 server crashed in Steam auth and this line blamed ptrace.)
        let mem = File::open(format!("/proc/{pid}/mem")).with_context(|| {
            match proc_state(pid) {
                // A crashed child stays a ZOMBIE until the parent reaps it, so `/proc/<pid>` still exists
                // and only `mem` is unreadable — an existence check alone reports it as a permissions
                // fault. Read the state instead.
                Some('Z') | None => format!(
                    "the game process {pid} DIED during the live stage — it is {}, so there is nothing \
                     left to read. This is NOT a ptrace-permission problem: check the server's own log \
                     and /tmp/dumps for a minidump.",
                    if proc_state(pid) == Some('Z') {
                        "a zombie (crashed, not yet reaped)"
                    } else {
                        "gone"
                    }
                ),
                Some(_) => format!(
                    "open /proc/{pid}/mem — needs ptrace access (yama ptrace_scope=0 or run as root)"
                ),
            }
        })?;
        Ok(Self {
            mem,
            bases,
            paths,
            writable,
            executable,
        })
    }

    /// Is `addr` inside an executable mapping? A valid function pointer / vtable slot target must be.
    pub fn is_exec(&self, addr: u64) -> bool {
        self.executable
            .binary_search_by(|&(s, e)| {
                if addr < s {
                    std::cmp::Ordering::Greater
                } else if addr >= e {
                    std::cmp::Ordering::Less
                } else {
                    std::cmp::Ordering::Equal
                }
            })
            .is_ok()
    }

    /// Best-effort read of `n` bytes at runtime `addr` (short/empty on an unmapped page).
    pub fn read_bytes(&self, addr: u64, n: usize) -> Vec<u8> {
        let mut buf = vec![0u8; n];
        let got = self.mem.read_at(&mut buf, addr).unwrap_or(0);
        buf.truncate(got);
        buf
    }

    /// Scan the writable/heap regions for object instances whose vtable pointer is `vtable` — i.e.
    /// live instances of the class that owns that vtable. Returns the object base addresses (an
    /// object's first qword is its vtable pointer). Stops at `max` hits.
    pub fn find_instances(&self, vtable: u64, max: usize) -> Vec<u64> {
        let mut hits = Vec::new();
        let mut buf = vec![0u8; 1 << 20]; // 1 MiB window
        let needle = vtable.to_le_bytes();
        'outer: for &(start, end) in &self.writable {
            let mut addr = start;
            while addr < end {
                let n = ((end - addr) as usize).min(buf.len());
                // `buf` is reused across windows, so scanning past a SHORT read matches stale bytes from the
                // previous window and reports addresses that hold nothing of the sort. Bind both the scan and
                // the advance to what was actually read.
                let got = self.mem.read_at(&mut buf[..n], addr).unwrap_or(0);
                if got < 8 {
                    addr += n as u64;
                    continue;
                }
                // objects are pointer-aligned, so only 8-aligned positions can be a vtable slot
                let mut i = 0;
                while i + 8 <= got {
                    if buf[i..i + 8] == needle {
                        hits.push(addr + i as u64);
                        if hits.len() >= max {
                            break 'outer;
                        }
                    }
                    i += 8;
                }
                // advance by the 8-aligned prefix consumed: a persistently short-reading region still makes
                // progress (never re-reads the same bytes), and the skipped tail is retried on the next pass.
                addr += (got & !7) as u64;
            }
        }
        hits
    }

    /// Load base (slide) of a library — its lowest mapping address. Since CS2 `.so` files link at
    /// vaddr 0, the runtime address of a file vaddr `v` is simply `base + v`.
    pub fn base(&self, lib: &str) -> Option<u64> {
        self.bases.get(lib).copied()
    }

    /// The full path the process actually mapped for `lib` (a basename). The authority on WHICH file of a
    /// given name is loaded when several exist on disk — a game tree can hold the engine's own
    /// `libserver.so` and a loader shim of the same name several directories away.
    pub fn mapped_path(&self, lib: &str) -> Option<&str> {
        self.paths.get(lib).map(String::as_str)
    }

    fn read(&self, addr: u64, buf: &mut [u8]) -> Result<()> {
        self.mem
            .read_exact_at(buf, addr)
            .with_context(|| format!("read {} bytes at {addr:#x}", buf.len()))
    }

    pub fn read_u64(&self, addr: u64) -> Result<u64> {
        let mut b = [0u8; 8];
        self.read(addr, &mut b)?;
        Ok(u64::from_le_bytes(b))
    }

    pub fn read_i32(&self, addr: u64) -> Result<i32> {
        let mut b = [0u8; 4];
        self.read(addr, &mut b)?;
        Ok(i32::from_le_bytes(b))
    }

    pub fn read_u16(&self, addr: u64) -> Result<u16> {
        let mut b = [0u8; 2];
        self.read(addr, &mut b)?;
        Ok(u16::from_le_bytes(b))
    }

    /// NUL-terminated string at runtime `addr` (bounded). Reads may land near an unmapped page, so a
    /// short read is fine — we take whatever came back up to the terminator.
    pub fn read_cstr(&self, addr: u64) -> Result<String> {
        let mut buf = [0u8; 256];
        let n = self.mem.read_at(&mut buf, addr).unwrap_or(0);
        let end = buf[..n].iter().position(|&c| c == 0).unwrap_or(n);
        Ok(String::from_utf8_lossy(&buf[..end]).into_owned())
    }
}

/// Result of a remote call: the return value (RAX) and whether the function returned cleanly to our
/// trap (vs faulting internally on a bad argument).
pub struct CallResult {
    pub rax: u64,
    pub clean_return: bool,
}

/// One argument to a remote call.
///
/// [`Arg::Scratch`] exists because a callee that takes a POINTER needs a structure to point at, and the
/// address of that structure is not known until the call frame is laid out. Naming it relative to the
/// scratch base lets the caller describe "argument 5 points at my blob" without knowing where the blob
/// will land.
#[derive(Clone, Copy)]
pub enum Arg {
    Val(u64),
    /// `scratch_base + addend`.
    Scratch(i64),
}

/// A blob placed in the target's stack scratch before the call.
pub struct Scratch<'a> {
    pub bytes: &'a [u8],
    /// `(offset, addend)` — write `scratch_base + addend` as a little-endian u64 at `offset` in the blob.
    /// This is how a pointer INSIDE the blob becomes absolute; an array-of-pointers argument is otherwise
    /// impossible to build, since every element has to name an address that does not exist yet.
    pub relocs: &'a [(usize, i64)],
}

/// How long an injected call may run before it is abandoned and the thread restored.
///
/// Generous by design: every call site here is a nullary accessor or a `this`-only query, which returns in
/// microseconds, so a second is four orders of magnitude of headroom and only a genuinely stuck callee
/// reaches it. `clean_return: false` is then the honest verdict — the same one a faulting call gets.
const CALL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);

/// Write `data` into the target at `addr`, a word at a time.
///
/// A trailing partial word is read back and merged rather than zero-filled: `PTRACE_POKEDATA` writes a
/// whole word, so writing the tail without preserving the bytes past it would clobber memory the caller
/// never asked to touch.
unsafe fn poke_bytes(pid: i32, addr: u64, data: &[u8]) -> Result<()> {
    use anyhow::bail;
    let mut i = 0usize;
    while i < data.len() {
        let at = addr + i as u64;
        let n = (data.len() - i).min(8);
        let mut word = if n == 8 {
            [0u8; 8]
        } else {
            // PEEKDATA returns -1 both for an error and for a word whose value IS -1, so errno is the
            // only way to tell them apart and it must be cleared first.
            unsafe { *libc::__errno_location() = 0 };
            let cur = unsafe { libc::ptrace(libc::PTRACE_PEEKDATA, pid, at as usize, 0usize) };
            if cur == -1 && errno() != 0 {
                bail!("PEEKDATA at {at:#x} failed (errno {})", errno());
            }
            (cur as u64).to_le_bytes()
        };
        word[..n].copy_from_slice(&data[i..i + n]);
        let w = u64::from_le_bytes(word) as usize;
        if unsafe { libc::ptrace(libc::PTRACE_POKEDATA, pid, at as usize, w) } < 0 {
            bail!("POKEDATA at {at:#x} failed (errno {})", errno());
        }
        i += n;
    }
    Ok(())
}

/// Call the function at runtime address `func` inside process `pid` with `args` (SysV: up to 6 in
/// registers), via ptrace. Attaches, saves the main thread's registers, sets up a call frame whose
/// return address is 0 (so the function traps on return, where we read RAX), runs it, then restores
/// the thread exactly — the SIGSEGV from the return trap is suppressed. Needs ptrace permission
/// (owned child, or same-user with ptrace_scope=0). UNSAFE: only call leaf-ish functions with valid args.
pub fn call_remote(pid: i32, func: u64, args: &[u64]) -> Result<CallResult> {
    let regs: Vec<Arg> = args.iter().map(|&v| Arg::Val(v)).collect();
    call_remote_ex(pid, func, &regs, &[], None)
}

/// [`call_remote`] plus stack arguments and a scratch blob placed in the target.
///
/// Needed for callees that take more than six integer arguments or a pointer to a structure the caller has
/// to build — neither of which the register-only form can express.
///
/// **Stack geometry**, descending from the interrupted `rsp`, chosen so three regions cannot collide:
/// the 128-byte red zone is left alone (the interrupted frame lives there); the scratch blob sits at
/// `rsp-1024`; the call frame starts at `rsp-2048`, so the callee's own stack — which grows DOWN from
/// there — can never reach the scratch ABOVE it. Entry keeps SysV's `rsp % 16 == 8`, with the return
/// address at `[rsp]` and stack argument *i* at `[rsp + 8 + 8i]`.
pub fn call_remote_ex(
    pid: i32,
    func: u64,
    regs_in: &[Arg],
    stack_in: &[Arg],
    scratch: Option<Scratch<'_>>,
) -> Result<CallResult> {
    use anyhow::bail;
    let dbg = std::env::var("SOURCE2ROSETTA_DBG").is_ok();
    if regs_in.len() > 6 {
        bail!("{} register arguments; SysV has 6", regs_in.len());
    }
    if let Some(s) = &scratch {
        // The blob lives in the 1 KiB between the frame and the red zone. Refuse rather than silently
        // overlap the call frame, which would corrupt the return address mid-call.
        if s.bytes.len() > 768 {
            bail!(
                "scratch blob is {} bytes; the reserved window is 768",
                s.bytes.len()
            );
        }
    }
    unsafe {
        if libc::ptrace(libc::PTRACE_ATTACH, pid, 0usize, 0usize) < 0 {
            bail!(
                "PTRACE_ATTACH {pid} failed (errno {}) — need ptrace permission",
                errno()
            );
        }
        let mut status = 0i32;
        if libc::waitpid(pid, &mut status, 0) < 0 {
            libc::ptrace(libc::PTRACE_DETACH, pid, 0usize, 0usize);
            bail!("waitpid(attach) failed");
        }
        if dbg {
            eprintln!(
                "[call] attached; stop status {status:#x} (stopped={})",
                libc::WIFSTOPPED(status)
            );
        }
        let mut saved: libc::user_regs_struct = std::mem::zeroed();
        if libc::ptrace(libc::PTRACE_GETREGS, pid, 0usize, &mut saved as *mut _) < 0 {
            libc::ptrace(libc::PTRACE_DETACH, pid, 0usize, 0usize);
            bail!("PTRACE_GETREGS failed");
        }
        let restore = |saved: &libc::user_regs_struct| {
            libc::ptrace(libc::PTRACE_SETREGS, pid, 0usize, saved as *const _);
            libc::ptrace(libc::PTRACE_DETACH, pid, 0usize, 0usize);
        };

        let mut regs = saved;
        // If we attached mid-syscall, orig_rax holds the syscall number and the kernel would run its
        // syscall-restart logic on our injected rip. Setting it to -1 says "no syscall in progress".
        regs.orig_rax = u64::MAX;

        // Place the scratch blob first: every Arg::Scratch resolves against its base.
        let scratch_base = (saved.rsp - 1024) & !0xfu64;
        if let Some(s) = &scratch {
            let mut blob = s.bytes.to_vec();
            for &(off, addend) in s.relocs {
                let Some(dst) = blob.get_mut(off..off + 8) else {
                    restore(&saved);
                    bail!(
                        "scratch reloc at {off} runs past the {}-byte blob",
                        s.bytes.len()
                    );
                };
                dst.copy_from_slice(&scratch_base.wrapping_add(addend as u64).to_le_bytes());
            }
            if let Err(e) = poke_bytes(pid, scratch_base, &blob) {
                restore(&saved);
                return Err(e.context(format!("placing scratch at {scratch_base:#x}")));
            }
        }
        let resolve = |a: Arg| match a {
            Arg::Val(v) => v,
            Arg::Scratch(addend) => scratch_base.wrapping_add(addend as u64),
        };

        let slots = [
            &mut regs.rdi as *mut u64,
            &mut regs.rsi,
            &mut regs.rdx,
            &mut regs.rcx,
            &mut regs.r8,
            &mut regs.r9,
        ];
        for (i, &a) in regs_in.iter().enumerate() {
            *slots[i] = resolve(a);
        }
        // Call frame well below the scratch, so the callee's downward stack growth cannot reach it. Write a
        // return address of 0 and keep SysV's `rsp % 16 == 8` at function entry.
        let mut sp = (saved.rsp - 2048) & !0xfu64;
        sp -= 8;
        if libc::ptrace(libc::PTRACE_POKEDATA, pid, sp as usize, 0usize) < 0 {
            restore(&saved);
            bail!(
                "POKEDATA(return addr) at {sp:#x} failed (errno {})",
                errno()
            );
        }
        // Stack arguments sit immediately above the return address, which is where the callee reads them.
        for (i, &a) in stack_in.iter().enumerate() {
            let at = sp + 8 + 8 * i as u64;
            if libc::ptrace(libc::PTRACE_POKEDATA, pid, at as usize, resolve(a) as usize) < 0 {
                restore(&saved);
                bail!(
                    "POKEDATA(stack arg {i}) at {at:#x} failed (errno {})",
                    errno()
                );
            }
        }
        let wrote = libc::ptrace(libc::PTRACE_PEEKDATA, pid, sp as usize, 0usize);
        regs.rsp = sp;
        regs.rip = func;
        if libc::ptrace(libc::PTRACE_SETREGS, pid, 0usize, &regs as *const _) < 0 {
            restore(&saved);
            bail!("PTRACE_SETREGS failed");
        }
        if dbg {
            eprintln!(
                "[call] rip={func:#x} rsp={sp:#x} rdi={:#x} retaddr-slot={wrote:#x} (want 0)",
                regs.rdi
            );
        }

        // Run, absorbing any spurious signals, until the function returns into our null trap — or until
        // the deadline. BOUNDED, because the alternative is unbounded: the injected callee is chosen to
        // be leaf-ish, but "chosen to be" is not "proven to be", and one that blocks on a lock, a socket
        // or a condition variable would park this `waitpid` forever with the tracee STOPPED — hanging a
        // CI derive with no output and no timeout above it. A live check that cannot finish is a failed
        // live check, not a reason to stop the release from ever being decided.
        let deadline = std::time::Instant::now() + CALL_TIMEOUT;
        loop {
            libc::ptrace(libc::PTRACE_CONT, pid, 0usize, 0usize);
            // Polled rather than blocking, so the deadline is observable at all.
            let waited = loop {
                let r = libc::waitpid(pid, &mut status, libc::WNOHANG);
                if r != 0 {
                    break r;
                }
                if std::time::Instant::now() >= deadline {
                    break 0;
                }
                std::thread::sleep(std::time::Duration::from_millis(1));
            };
            if waited == 0 {
                // Still RUNNING, so `restore` would fail ESRCH — stop it first, then put it back exactly.
                libc::kill(pid, libc::SIGSTOP);
                libc::waitpid(pid, &mut status, 0);
                restore(&saved);
                return Ok(CallResult {
                    rax: 0,
                    clean_return: false,
                });
            }
            if waited < 0 || !libc::WIFSTOPPED(status) {
                restore(&saved);
                bail!("target vanished mid-call (status {status:#x})");
            }
            let sig = libc::WSTOPSIG(status);
            let mut cur: libc::user_regs_struct = std::mem::zeroed();
            libc::ptrace(libc::PTRACE_GETREGS, pid, 0usize, &mut cur as *mut _);
            if dbg {
                eprintln!(
                    "[call] stop sig={sig} rip={:#x} rax={:#x}",
                    cur.rip, cur.rax
                );
            }
            if cur.rip == 0 {
                let r = CallResult {
                    rax: cur.rax,
                    clean_return: true,
                };
                restore(&saved);
                return Ok(r);
            }
            if sig == libc::SIGSEGV || sig == libc::SIGILL || sig == libc::SIGBUS {
                let r = CallResult {
                    rax: cur.rax,
                    clean_return: false,
                };
                restore(&saved);
                return Ok(r);
            }
            // any other signal (SIGSTOP/timer/…): swallow it and keep running the call
        }
    }
}

fn errno() -> i32 {
    unsafe { *libc::__errno_location() }
}

#[cfg(test)]
mod tests {
    use super::maps_path;

    #[test]
    fn maps_path_survives_spaces_in_the_mapped_path() {
        // Steam's default install directory contains spaces; taking the last whitespace-delimited field
        // yields "Offensive/..." which fails the leading-`/` test, so the library silently vanishes from
        // the map and the live oracle reports it as not mapped.
        let spaced = "7f1a2b000000-7f1a2c000000 r-xp 00000000 08:01 12345      \
                      /home/cs2/.steam/SteamApps/common/Counter-Strike Global Offensive/game/csgo/bin/linuxsteamrt64/libserver.so";
        assert_eq!(
            maps_path(spaced),
            "/home/cs2/.steam/SteamApps/common/Counter-Strike Global Offensive/game/csgo/bin/linuxsteamrt64/libserver.so"
        );

        // The no-space case must be unchanged.
        let plain = "7f1a2b000000-7f1a2c000000 r-xp 00000000 08:01 12345      /home/snake/game/csgo/bin/linuxsteamrt64/libserver.so";
        assert_eq!(
            maps_path(plain),
            "/home/snake/game/csgo/bin/linuxsteamrt64/libserver.so"
        );

        // An anonymous mapping has no pathname — it must read as EMPTY, since that is what marks the
        // writable heap regions the instance scan walks.
        assert_eq!(
            maps_path("24557800000-24597800000 rw-p 00000000 00:00 0 "),
            ""
        );
        assert_eq!(
            maps_path("29618000-29639000 rw-p 00000000 00:00 0     [heap]"),
            "[heap]"
        );
    }
}
