use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use super::symbol_table::{parse_symbol_table_from_bytes, SymbolTable};
#[derive(Debug, Clone)]
pub struct HotpatchModuleCache {
pub lib: PathBuf,
pub symbols: SymbolTable,
pub aslr_reference: u64,
}
impl HotpatchModuleCache {
pub fn from_path(path: impl Into<PathBuf>) -> Result<Self> {
let path = path.into();
let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
let symbols = parse_symbol_table_from_bytes(&bytes)
.with_context(|| format!("parse {} symbols", path.display()))?;
let aslr_reference = symbols
.by_name
.get("whisker_aslr_anchor")
.or_else(|| symbols.by_name.get("_whisker_aslr_anchor"))
.map(|s| s.address)
.unwrap_or(0);
Ok(Self {
lib: path,
symbols,
aslr_reference,
})
}
pub fn symbol_count(&self) -> usize {
self.symbols.by_name.len()
}
pub fn lib_path(&self) -> &Path {
&self.lib
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use std::time::Instant;
fn ensure_whisker_binary() -> PathBuf {
let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let bin = workspace_root.join("target/debug/whisker");
if !bin.is_file() {
let status = Command::new("cargo")
.args(["build", "-p", "whisker-cli", "--bin", "whisker"])
.current_dir(&workspace_root)
.status()
.expect("spawn cargo");
assert!(status.success());
}
bin
}
#[test]
fn from_path_loads_symbols_and_aslr_reference() {
let bin = ensure_whisker_binary();
let cache = HotpatchModuleCache::from_path(&bin).expect("cache");
assert_eq!(cache.lib, bin);
assert!(
cache.symbol_count() > 100,
"expected hundreds of symbols in a debug build, got {}",
cache.symbol_count(),
);
let _ = cache.aslr_reference;
}
#[test]
fn cached_symbol_lookup_is_cheap_after_construction() {
let bin = ensure_whisker_binary();
let t0 = Instant::now();
let cache = HotpatchModuleCache::from_path(&bin).expect("cache");
let parse_time = t0.elapsed();
let t1 = Instant::now();
let mut found = 0_usize;
for _ in 0..1_000 {
if cache.symbols.by_name.contains_key("nonexistent_xyz") {
found += 1;
}
}
let lookup_time = t1.elapsed();
assert_eq!(found, 0);
assert!(
lookup_time * 50 < parse_time,
"1000 lookups ({lookup_time:?}) should be much faster than \
one parse ({parse_time:?}); cache benefit unclear",
);
}
#[test]
fn missing_path_errors_out() {
let err = HotpatchModuleCache::from_path("/no/such/binary/exists").unwrap_err();
assert!(
err.to_string().contains("read") || err.to_string().contains("/no/such"),
"{err}",
);
}
#[test]
fn lib_path_round_trips_the_original_argument() {
let bin = ensure_whisker_binary();
let cache = HotpatchModuleCache::from_path(&bin).expect("cache");
assert_eq!(cache.lib_path(), bin.as_path());
}
}