Build RE baseline and initial Rust workspace
This commit is contained in:
parent
8d1f280e2e
commit
ffaf155ef0
39 changed files with 5974 additions and 8 deletions
9
crates/rrt-cli/Cargo.toml
Normal file
9
crates/rrt-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "rrt-cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rrt-model = { path = "../rrt-model" }
|
||||
sha2.workspace = true
|
||||
145
crates/rrt-cli/src/main.rs
Normal file
145
crates/rrt-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rrt_model::{
|
||||
BINARY_SUMMARY_PATH, CANONICAL_EXE_PATH, CONTROL_LOOP_ATLAS_PATH, FUNCTION_MAP_PATH,
|
||||
REQUIRED_ATLAS_HEADINGS, REQUIRED_EXPORTS, load_binary_summary, load_function_map,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = real_main() {
|
||||
eprintln!("error: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let repo_root = parse_repo_root()?;
|
||||
validate_required_files(&repo_root)?;
|
||||
validate_binary_summary(&repo_root)?;
|
||||
validate_function_map(&repo_root)?;
|
||||
validate_control_loop_atlas(&repo_root)?;
|
||||
println!("baseline validation passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_repo_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let mut args = env::args().skip(1);
|
||||
match (args.next().as_deref(), args.next(), args.next()) {
|
||||
(None, None, None) => Ok(env::current_dir()?),
|
||||
(Some("validate"), None, None) => Ok(env::current_dir()?),
|
||||
(Some("validate"), Some(path), None) => Ok(PathBuf::from(path)),
|
||||
_ => Err("usage: rrt-cli [validate [repo-root]]".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_required_files(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut missing = Vec::new();
|
||||
for relative in REQUIRED_EXPORTS {
|
||||
let path = repo_root.join(relative);
|
||||
if !path.exists() {
|
||||
missing.push(path.display().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
return Err(format!("missing required exports: {}", missing.join(", ")).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_binary_summary(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let summary = load_binary_summary(&repo_root.join(BINARY_SUMMARY_PATH))?;
|
||||
let actual_exe = repo_root.join(CANONICAL_EXE_PATH);
|
||||
if !actual_exe.exists() {
|
||||
return Err(format!("canonical exe missing: {}", actual_exe.display()).into());
|
||||
}
|
||||
|
||||
let actual_hash = sha256_file(&actual_exe)?;
|
||||
if actual_hash != summary.sha256 {
|
||||
return Err(format!(
|
||||
"hash mismatch for {}: summary has {}, actual file is {}",
|
||||
actual_exe.display(),
|
||||
summary.sha256,
|
||||
actual_hash
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let docs_readme = fs::read_to_string(repo_root.join("docs/README.md"))?;
|
||||
if !docs_readme.contains(&summary.sha256) {
|
||||
return Err("docs/README.md does not include the canonical SHA-256".into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_function_map(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let records = load_function_map(&repo_root.join(FUNCTION_MAP_PATH))?;
|
||||
let mut seen = BTreeSet::new();
|
||||
|
||||
for record in records {
|
||||
if !(1..=5).contains(&record.confidence) {
|
||||
return Err(format!(
|
||||
"invalid confidence {} for {} {}",
|
||||
record.confidence, record.address, record.name
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if !seen.insert(record.address) {
|
||||
return Err(format!("duplicate function address {}", record.address).into());
|
||||
}
|
||||
|
||||
if record.name.trim().is_empty() {
|
||||
return Err(format!("blank function name at {}", record.address).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_control_loop_atlas(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let atlas = fs::read_to_string(repo_root.join(CONTROL_LOOP_ATLAS_PATH))?;
|
||||
for heading in REQUIRED_ATLAS_HEADINGS {
|
||||
if !atlas.contains(heading) {
|
||||
return Err(format!("missing atlas heading `{heading}`").into());
|
||||
}
|
||||
}
|
||||
|
||||
for marker in [
|
||||
"- Roots:",
|
||||
"- Trigger/Cadence:",
|
||||
"- Key Dispatchers:",
|
||||
"- State Anchors:",
|
||||
"- Subsystem Handoffs:",
|
||||
"- Evidence:",
|
||||
"- Open Questions:",
|
||||
] {
|
||||
if !atlas.contains(marker) {
|
||||
return Err(format!("atlas is missing field marker `{marker}`").into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sha256_file(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0_u8; 8192];
|
||||
loop {
|
||||
let read = file.read(&mut buffer)?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..read]);
|
||||
}
|
||||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
9
crates/rrt-hook/Cargo.toml
Normal file
9
crates/rrt-hook/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "rrt-hook"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "dinput8"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
170
crates/rrt-hook/src/lib.rs
Normal file
170
crates/rrt-hook/src/lib.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#![cfg_attr(not(windows), allow(dead_code))]
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_hook {
|
||||
use core::ffi::{c_char, c_void};
|
||||
use core::mem;
|
||||
use core::ptr;
|
||||
|
||||
const DLL_PROCESS_ATTACH: u32 = 1;
|
||||
const E_FAIL: i32 = 0x8000_4005_u32 as i32;
|
||||
const FILE_APPEND_DATA: u32 = 0x0000_0004;
|
||||
const FILE_SHARE_READ: u32 = 0x0000_0001;
|
||||
const OPEN_ALWAYS: u32 = 4;
|
||||
const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
|
||||
const INVALID_HANDLE_VALUE: isize = -1;
|
||||
const FILE_END: u32 = 2;
|
||||
|
||||
static LOG_PATH: &[u8] = b"rrt_hook_attach.log\0";
|
||||
static ATTACH_MESSAGE: &[u8] = b"rrt-hook: process attach\n";
|
||||
static DEBUG_MESSAGE: &[u8] = b"rrt-hook: DllMain process attach\0";
|
||||
static DIRECT_INPUT8_CREATE_NAME: &[u8] = b"DirectInput8Create\0";
|
||||
static mut REAL_DINPUT8_CREATE: Option<DirectInput8CreateFn> = None;
|
||||
|
||||
unsafe extern "system" {
|
||||
fn CreateFileA(
|
||||
lp_file_name: *const c_char,
|
||||
desired_access: u32,
|
||||
share_mode: u32,
|
||||
security_attributes: *mut c_void,
|
||||
creation_disposition: u32,
|
||||
flags_and_attributes: u32,
|
||||
template_file: *mut c_void,
|
||||
) -> isize;
|
||||
fn SetFilePointer(
|
||||
file: isize,
|
||||
distance: i32,
|
||||
distance_high: *mut i32,
|
||||
move_method: u32,
|
||||
) -> u32;
|
||||
fn WriteFile(
|
||||
file: isize,
|
||||
buffer: *const c_void,
|
||||
bytes_to_write: u32,
|
||||
bytes_written: *mut u32,
|
||||
overlapped: *mut c_void,
|
||||
) -> i32;
|
||||
fn CloseHandle(handle: isize) -> i32;
|
||||
fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
|
||||
fn GetSystemDirectoryA(buffer: *mut u8, size: u32) -> u32;
|
||||
fn GetProcAddress(module: isize, name: *const c_char) -> *mut c_void;
|
||||
fn LoadLibraryA(name: *const c_char) -> isize;
|
||||
fn OutputDebugStringA(output: *const c_char);
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Guid {
|
||||
data1: u32,
|
||||
data2: u16,
|
||||
data3: u16,
|
||||
data4: [u8; 8],
|
||||
}
|
||||
|
||||
type DirectInput8CreateFn = unsafe extern "system" fn(
|
||||
instance: *mut c_void,
|
||||
version: u32,
|
||||
riid: *const Guid,
|
||||
out: *mut *mut c_void,
|
||||
outer: *mut c_void,
|
||||
) -> i32;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn DllMain(
|
||||
module: *mut c_void,
|
||||
reason: u32,
|
||||
_reserved: *mut c_void,
|
||||
) -> i32 {
|
||||
if reason == DLL_PROCESS_ATTACH {
|
||||
unsafe {
|
||||
let _ = DisableThreadLibraryCalls(module);
|
||||
OutputDebugStringA(DEBUG_MESSAGE.as_ptr().cast());
|
||||
append_attach_log();
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn DirectInput8Create(
|
||||
instance: *mut c_void,
|
||||
version: u32,
|
||||
riid: *const Guid,
|
||||
out: *mut *mut c_void,
|
||||
outer: *mut c_void,
|
||||
) -> i32 {
|
||||
let direct_input8_create = unsafe { load_direct_input8_create() };
|
||||
match direct_input8_create {
|
||||
Some(callback) => unsafe { callback(instance, version, riid, out, outer) },
|
||||
None => E_FAIL,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn append_attach_log() {
|
||||
let handle = unsafe {
|
||||
CreateFileA(
|
||||
LOG_PATH.as_ptr().cast(),
|
||||
FILE_APPEND_DATA,
|
||||
FILE_SHARE_READ,
|
||||
ptr::null_mut(),
|
||||
OPEN_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = unsafe { SetFilePointer(handle, 0, ptr::null_mut(), FILE_END) };
|
||||
let mut bytes_written = 0_u32;
|
||||
let _ = unsafe {
|
||||
WriteFile(
|
||||
handle,
|
||||
ATTACH_MESSAGE.as_ptr().cast(),
|
||||
ATTACH_MESSAGE.len() as u32,
|
||||
&mut bytes_written,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
let _ = unsafe { CloseHandle(handle) };
|
||||
}
|
||||
|
||||
unsafe fn load_direct_input8_create() -> Option<DirectInput8CreateFn> {
|
||||
if let Some(callback) = unsafe { REAL_DINPUT8_CREATE } {
|
||||
return Some(callback);
|
||||
}
|
||||
|
||||
let mut system_directory = [0_u8; 260];
|
||||
let length = unsafe {
|
||||
GetSystemDirectoryA(system_directory.as_mut_ptr(), system_directory.len() as u32)
|
||||
};
|
||||
if length == 0 || length as usize >= system_directory.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut dll_path = system_directory[..length as usize].to_vec();
|
||||
dll_path.extend_from_slice(br"\dinput8.dll");
|
||||
dll_path.push(0);
|
||||
|
||||
let module = unsafe { LoadLibraryA(dll_path.as_ptr().cast()) };
|
||||
if module == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let symbol = unsafe { GetProcAddress(module, DIRECT_INPUT8_CREATE_NAME.as_ptr().cast()) };
|
||||
if symbol.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let callback: DirectInput8CreateFn = unsafe { mem::transmute(symbol) };
|
||||
unsafe {
|
||||
REAL_DINPUT8_CREATE = Some(callback);
|
||||
}
|
||||
Some(callback)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn host_build_marker() -> &'static str {
|
||||
"rrt-hook host build"
|
||||
}
|
||||
10
crates/rrt-model/Cargo.toml
Normal file
10
crates/rrt-model/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "rrt-model"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
csv.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
304
crates/rrt-model/src/lib.rs
Normal file
304
crates/rrt-model/src/lib.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::de::{self, Deserializer};
|
||||
|
||||
pub const CANONICAL_EXE_PATH: &str = "rt3_wineprefix/drive_c/rt3/RT3.exe";
|
||||
pub const CONTROL_LOOP_ATLAS_PATH: &str = "docs/control-loop-atlas.md";
|
||||
pub const FUNCTION_MAP_PATH: &str = "artifacts/exports/rt3-1.06/function-map.csv";
|
||||
pub const BINARY_SUMMARY_PATH: &str = "artifacts/exports/rt3-1.06/binary-summary.json";
|
||||
|
||||
pub const REQUIRED_EXPORTS: &[&str] = &[
|
||||
BINARY_SUMMARY_PATH,
|
||||
"artifacts/exports/rt3-1.06/sections.csv",
|
||||
"artifacts/exports/rt3-1.06/imported-dlls.txt",
|
||||
"artifacts/exports/rt3-1.06/imported-functions.csv",
|
||||
"artifacts/exports/rt3-1.06/interesting-strings.txt",
|
||||
"artifacts/exports/rt3-1.06/subsystem-inventory.md",
|
||||
FUNCTION_MAP_PATH,
|
||||
"artifacts/exports/rt3-1.06/ghidra-startup-functions.csv",
|
||||
"artifacts/exports/rt3-1.06/startup-call-chain.md",
|
||||
"artifacts/exports/rt3-1.06/analysis-context-functions.csv",
|
||||
"artifacts/exports/rt3-1.06/analysis-context-strings.csv",
|
||||
"artifacts/exports/rt3-1.06/analysis-context.md",
|
||||
"artifacts/exports/rt3-1.06/pending-template-store-functions.csv",
|
||||
"artifacts/exports/rt3-1.06/pending-template-store-record-kinds.csv",
|
||||
"artifacts/exports/rt3-1.06/pending-template-store-management.md",
|
||||
];
|
||||
|
||||
pub const REQUIRED_ATLAS_HEADINGS: &[&str] = &[
|
||||
"## CRT and Process Startup",
|
||||
"## Bootstrap and Shell Service Bring-Up",
|
||||
"## Shell UI Command and Deferred Work Flow",
|
||||
"## Presentation, Overlay, and Frame Timing",
|
||||
"## Map and Scenario Content Load",
|
||||
"## Multiplayer Session and Transport Flow",
|
||||
"## Input, Save/Load, and Simulation",
|
||||
"## Next Mapping Passes",
|
||||
];
|
||||
|
||||
pub const FUNCTION_MAP_HEADER: &[&str] = &[
|
||||
"address",
|
||||
"size",
|
||||
"name",
|
||||
"subsystem",
|
||||
"calling_convention",
|
||||
"prototype_status",
|
||||
"source_tool",
|
||||
"confidence",
|
||||
"notes",
|
||||
"verified_against",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Address(pub u32);
|
||||
|
||||
impl Address {
|
||||
pub fn parse_hex(value: &str) -> Result<Self, String> {
|
||||
let trimmed = value.trim();
|
||||
let digits = trimmed
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| trimmed.strip_prefix("0X"))
|
||||
.unwrap_or(trimmed);
|
||||
u32::from_str_radix(digits, 16)
|
||||
.map(Self)
|
||||
.map_err(|err| format!("invalid hex address `{trimmed}`: {err}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "0x{:08x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Self::parse_hex(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SubsystemId {
|
||||
Startup,
|
||||
Bootstrap,
|
||||
Shell,
|
||||
Support,
|
||||
Ui,
|
||||
Render,
|
||||
Audio,
|
||||
Input,
|
||||
Network,
|
||||
Filesystem,
|
||||
Resource,
|
||||
Map,
|
||||
Scenario,
|
||||
Save,
|
||||
Simulation,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl fmt::Display for SubsystemId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self {
|
||||
Self::Startup => "startup",
|
||||
Self::Bootstrap => "bootstrap",
|
||||
Self::Shell => "shell",
|
||||
Self::Support => "support",
|
||||
Self::Ui => "ui",
|
||||
Self::Render => "render",
|
||||
Self::Audio => "audio",
|
||||
Self::Input => "input",
|
||||
Self::Network => "network",
|
||||
Self::Filesystem => "filesystem",
|
||||
Self::Resource => "resource",
|
||||
Self::Map => "map",
|
||||
Self::Scenario => "scenario",
|
||||
Self::Save => "save",
|
||||
Self::Simulation => "simulation",
|
||||
Self::Unknown => "unknown",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SubsystemId {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"startup" => Ok(Self::Startup),
|
||||
"bootstrap" => Ok(Self::Bootstrap),
|
||||
"shell" => Ok(Self::Shell),
|
||||
"support" => Ok(Self::Support),
|
||||
"ui" => Ok(Self::Ui),
|
||||
"render" => Ok(Self::Render),
|
||||
"audio" => Ok(Self::Audio),
|
||||
"input" => Ok(Self::Input),
|
||||
"network" => Ok(Self::Network),
|
||||
"filesystem" => Ok(Self::Filesystem),
|
||||
"resource" => Ok(Self::Resource),
|
||||
"map" => Ok(Self::Map),
|
||||
"scenario" => Ok(Self::Scenario),
|
||||
"save" => Ok(Self::Save),
|
||||
"simulation" => Ok(Self::Simulation),
|
||||
"unknown" => Ok(Self::Unknown),
|
||||
other => Err(format!("unknown subsystem `{other}`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SubsystemId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw = String::deserialize(deserializer)?;
|
||||
raw.parse().map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ControlLoopId {
|
||||
CrtStartup,
|
||||
Bootstrap,
|
||||
ShellUi,
|
||||
PresentationFrame,
|
||||
MapScenarioLoad,
|
||||
MultiplayerTransport,
|
||||
InputSaveSimulation,
|
||||
}
|
||||
|
||||
impl fmt::Display for ControlLoopId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self {
|
||||
Self::CrtStartup => "crt-startup",
|
||||
Self::Bootstrap => "bootstrap",
|
||||
Self::ShellUi => "shell-ui",
|
||||
Self::PresentationFrame => "presentation-frame",
|
||||
Self::MapScenarioLoad => "map-scenario-load",
|
||||
Self::MultiplayerTransport => "multiplayer-transport",
|
||||
Self::InputSaveSimulation => "input-save-simulation",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum LoopRole {
|
||||
Root,
|
||||
Dispatcher,
|
||||
Cadence,
|
||||
StateAnchor,
|
||||
Gateway,
|
||||
}
|
||||
|
||||
impl fmt::Display for LoopRole {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self {
|
||||
Self::Root => "root",
|
||||
Self::Dispatcher => "dispatcher",
|
||||
Self::Cadence => "cadence",
|
||||
Self::StateAnchor => "state-anchor",
|
||||
Self::Gateway => "gateway",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FunctionRecord {
|
||||
#[serde(deserialize_with = "deserialize_address")]
|
||||
pub address: Address,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_u32")]
|
||||
pub size: Option<u32>,
|
||||
pub name: String,
|
||||
pub subsystem: SubsystemId,
|
||||
pub calling_convention: String,
|
||||
pub prototype_status: String,
|
||||
pub source_tool: String,
|
||||
pub confidence: u8,
|
||||
pub notes: String,
|
||||
pub verified_against: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BinarySummary {
|
||||
pub path: String,
|
||||
pub sha256: String,
|
||||
pub size_bytes: u64,
|
||||
#[serde(default)]
|
||||
pub summary: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn load_function_map(path: &Path) -> Result<Vec<FunctionRecord>, Box<dyn std::error::Error>> {
|
||||
let mut reader = csv::ReaderBuilder::new()
|
||||
.trim(csv::Trim::All)
|
||||
.from_path(path)?;
|
||||
let headers = reader.headers()?.clone();
|
||||
let header_values: Vec<&str> = headers.iter().collect();
|
||||
if header_values != FUNCTION_MAP_HEADER {
|
||||
return Err(format!("unexpected function-map header: {header_values:?}").into());
|
||||
}
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for record in reader.deserialize() {
|
||||
rows.push(record?);
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub fn load_binary_summary(path: &Path) -> Result<BinarySummary, Box<dyn std::error::Error>> {
|
||||
let file = File::open(path)?;
|
||||
Ok(serde_json::from_reader(file)?)
|
||||
}
|
||||
|
||||
fn deserialize_address<'de, D>(deserializer: D) -> Result<Address, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw = String::deserialize(deserializer)?;
|
||||
raw.parse().map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
fn deserialize_optional_u32<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw = String::deserialize(deserializer)?;
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let parsed = trimmed
|
||||
.parse::<u32>()
|
||||
.or_else(|_| u32::from_str_radix(trimmed.trim_start_matches("0x"), 16))
|
||||
.map_err(de::Error::custom)?;
|
||||
Ok(Some(parsed))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_hex_addresses() {
|
||||
let address = Address::parse_hex("0x005a313b").expect("address should parse");
|
||||
assert_eq!(address.0, 0x005a313b);
|
||||
assert_eq!(address.to_string(), "0x005a313b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_subsystems() {
|
||||
let subsystem: SubsystemId = "shell".parse().expect("subsystem should parse");
|
||||
assert_eq!(subsystem, SubsystemId::Shell);
|
||||
assert_eq!(subsystem.to_string(), "shell");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue