source2rosetta/src/locate.rs
Kamal Tufekcic a2922b8bad
All checks were successful
CI / fuzz (push) Successful in 1m41s
CI / lint (push) Successful in 16s
CI / test (push) Successful in 22s
initial commit
2026-07-27 10:12:04 +03:00

95 lines
4.3 KiB
Rust

//! 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 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<u64> {
let mut set: BTreeSet<u64> = 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<PathBuf> {
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<CodeImage> {
// 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)
}