rrt/crates/rrt-model/src/lib.rs

308 lines
8.9 KiB
Rust
Raw Normal View History

pub mod finance;
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",
"artifacts/exports/rt3-1.06/event-effects-table.json",
"artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json",
];
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");
}
}