//! Locating primitives: where a library file lives ON DISK, and where the function entries live //! INSIDE an image. //! //! On disk: `find_file` resolves a lib by name under a build tree (nearest-depth-wins, so a Metamod //! shim can't shadow the real engine lib) and `load_lib` turns a build dir *or* a bare `.so` into a //! loaded [`CodeImage`]. Both are leaf primitives (they touch only `elf` + the filesystem), so the //! low-level readers — `schema` especially — depend on THIS module rather than up on the engine. //! //! In an image: [`candidate_entries`] enumerates plausible function starts without symbols — relocation //! values that point into code (vtable slots + function pointers — covers virtual functions) unioned //! with the targets of direct near `call`s found by a linear sweep. `xref` unions this with `.eh_frame` //! starts to index the whole binary. use crate::elf::CodeImage; use anyhow::{Context, Result}; use iced_x86::{Decoder, DecoderOptions, FlowControl, OpKind}; use std::collections::BTreeSet; use std::path::{Path, PathBuf}; /// Every plausible function ENTRY in the image, sorted and deduped: relocation code-pointers (every vtable /// slot, every stored function pointer) ∪ decoded `call` targets ∪ `.eh_frame` FDE starts. /// /// The union is the point, and it is why this is one function rather than four lines repeated. CS2 strips /// `.eh_frame` from the game code — the FDE list covers the statically-linked runtime tail, roughly 8,327 /// of libserver's ~70,000 functions — so an FDE-only list misses the entire gameplay region, while a /// relocation/call-target-only list misses the runtime tail that has no code pointer taken. Six callers /// need exactly this set: the xref index, the ConVar and VScript readers, the change digest, and both /// anchor passes. A fork adding PLT or ifunc entries edits here, once. pub fn function_entries(img: &CodeImage) -> Vec { let mut entries = candidate_entries(img); entries.extend(img.eh_frame_functions().into_iter().map(|(s, _)| s)); entries.sort_unstable(); entries.dedup(); entries } /// Every plausible function entry address in `img`: relocation values that point into code, plus /// the targets of direct near `call`s found by a linear sweep. Sorted, de-duplicated. pub fn candidate_entries(img: &CodeImage) -> Vec { let mut set: BTreeSet = img.code_pointer_targets().into_iter().collect(); for (va, code) in img.exec_blocks() { let mut dec = Decoder::with_ip(64, code, va, DecoderOptions::NONE); while dec.can_decode() { let insn = dec.decode(); // iced advances one byte on invalid, so the sweep self-resyncs if insn.flow_control() == FlowControl::Call && matches!( insn.op0_kind(), OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64 ) { let t = insn.near_branch_target(); if img.is_code(t) { set.insert(t); } } } } set.into_iter().collect() } /// Shallowest file named `name` under `dir` (bounded depth), ties broken by sorted path. /// /// NEAREST-DEPTH-WINS, not first-`read_dir`-hit: a game install legitimately holds several files of the same /// basename, and the shallowest is the real one. A CS2 tree has the engine's own /// `csgo/bin/linuxsteamrt64/libserver.so` at depth 3 and Metamod's ~300 KB loader shim of the SAME name at /// `csgo/addons/metamod/bin/linuxsteamrt64/libserver.so` (depth 5, plus any `bin.*.bak` siblings). Depth-first /// order made which one you derive from a property of directory-entry order — deriving against the shim would /// yield garbage — and plain sorting is WORSE, since `addons` sorts before `bin`. Sorting is only the tie-break /// among equally-shallow candidates, so the result never depends on filesystem enumeration order. pub(crate) fn find_file(dir: &Path, name: &str, depth: usize) -> Option { let mut level = vec![dir.to_path_buf()]; for _ in 0..depth { let (mut hits, mut next) = (Vec::new(), Vec::new()); for d in &level { let Ok(rd) = std::fs::read_dir(d) else { continue; }; for e in rd.flatten() { let p = e.path(); if p.is_dir() { next.push(p); } else if p.file_name().and_then(|s| s.to_str()) == Some(name) { hits.push(p); } } } if !hits.is_empty() { hits.sort(); return hits.into_iter().next(); } if next.is_empty() { return None; } next.sort(); level = next; } None } /// Locate `lib` under `dir` (depth 8) and load it as a `CodeImage` — the `find_file` + load pattern the /// command entry points share. pub(crate) fn load_lib(path: &Path, lib: &str) -> Result { // Accept a build DIR (find `lib` within, depth 8) or a direct `.so` FILE (load as-is), so callers can // pass `path/to/build_dir` or `path/to/libserver.so` interchangeably (e.g. `classify-change --prev`). let file = if path.is_file() { path.to_path_buf() } else { find_file(path, lib, 8) .with_context(|| format!("{lib} not found under {}", path.display()))? }; CodeImage::load(&file) }