Build RE baseline and initial Rust workspace

This commit is contained in:
Jan Petykiewicz 2026-04-02 23:11:15 -07:00
commit ffaf155ef0
39 changed files with 5974 additions and 8 deletions

View 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
View 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()))
}

View 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
View 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"
}

View 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
View 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");
}
}