refactor: switch export database from markdown to JSON parsing

Add serde/serde_json for structured parsing of the export database,
replacing the hand-rolled markdown table parser with type-safe JSON
deserialization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-03-29 17:44:52 +02:00
parent b6ee119824
commit eacd39158e
4 changed files with 33669 additions and 74 deletions

65
Cargo.lock generated
View File

@@ -194,6 +194,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.183" version = "0.2.183"
@@ -206,6 +212,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9" checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "once_cell_polyfill" name = "once_cell_polyfill"
version = "1.70.2" version = "1.70.2"
@@ -230,6 +242,49 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -288,10 +343,18 @@ dependencies = [
[[package]] [[package]]
name = "xex2tractor" name = "xex2tractor"
version = "0.8.0" version = "0.8.1"
dependencies = [ dependencies = [
"aes", "aes",
"cbc", "cbc",
"clap", "clap",
"lzxd", "lzxd",
"serde",
"serde_json",
] ]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "xex2tractor" name = "xex2tractor"
version = "0.8.0" version = "0.8.1"
edition = "2024" edition = "2024"
description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files" description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files"
license = "MIT" license = "MIT"
@@ -10,3 +10,5 @@ aes = "0.8.4"
cbc = "0.1.2" cbc = "0.1.2"
clap = { version = "4.6.0", features = ["derive"] } clap = { version = "4.6.0", features = ["derive"] }
lzxd = "0.2.6" lzxd = "0.2.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

33562
doc/xbox360_exports.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,15 @@
/// Xbox 360 system export database. /// Xbox 360 system export database.
/// ///
/// Parses the embedded `doc/xbox360_exports.md` at first access and provides /// Parses the embedded `doc/xbox360_exports.json` at first access and provides
/// ordinal-to-name lookups for xboxkrnl.exe, xam.xex, and xbdm.xex. /// ordinal-to-name lookups for xboxkrnl.exe, xam.xex, and xbdm.xex.
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::OnceLock; use std::sync::OnceLock;
use serde::Deserialize;
static EXPORT_DB: OnceLock<ExportDatabase> = OnceLock::new(); static EXPORT_DB: OnceLock<ExportDatabase> = OnceLock::new();
const EXPORTS_MD: &str = include_str!("../doc/xbox360_exports.md"); const EXPORTS_JSON: &str = include_str!("../doc/xbox360_exports.json");
/// Information about a single Xbox 360 system export. /// Information about a single Xbox 360 system export.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -46,77 +48,52 @@ pub fn lookup(library: &str, ordinal: u16) -> Option<&'static ExportInfo> {
} }
fn get_db() -> &'static ExportDatabase { fn get_db() -> &'static ExportDatabase {
EXPORT_DB.get_or_init(|| parse_exports_md(EXPORTS_MD)) EXPORT_DB.get_or_init(|| parse_exports_json(EXPORTS_JSON))
} }
/// Parses the markdown export database into a lookup structure. #[derive(Deserialize)]
/// struct JsonRoot {
/// Expected format per module section: modules: HashMap<String, JsonModule>,
/// ```text }
/// ## module_name (filename.exe)
/// ... #[derive(Deserialize)]
/// | 0xNNN | FunctionName | function | status | ... | struct JsonModule {
/// ``` file: String,
fn parse_exports_md(md: &str) -> ExportDatabase { exports: Vec<JsonExport>,
}
#[derive(Deserialize)]
struct JsonExport {
ordinal: u32,
name: String,
#[serde(rename = "type")]
export_type: String,
}
fn parse_exports_json(json: &str) -> ExportDatabase {
let root: JsonRoot = serde_json::from_str(json).expect("invalid exports JSON");
let mut modules: HashMap<String, HashMap<u16, ExportInfo>> = HashMap::new(); let mut modules: HashMap<String, HashMap<u16, ExportInfo>> = HashMap::new();
let mut current_file: Option<String> = None;
for line in md.lines() { for module in root.modules.values() {
// Detect section headers: "## xboxkrnl (xboxkrnl.exe)" let file_key = module.file.to_ascii_lowercase();
if let Some(rest) = line.strip_prefix("## ") { let entries = modules.entry(file_key).or_default();
if let (Some(paren_start), Some(paren_end)) =
(rest.find('('), rest.find(')')) for export in &module.exports {
{ let ordinal = export.ordinal as u16;
let filename = rest[paren_start + 1..paren_end].trim().to_ascii_lowercase(); entries.insert(
current_file = Some(filename); ordinal,
} else { ExportInfo {
current_file = None; ordinal,
} name: export.name.clone(),
continue; is_function: export.export_type == "function",
},
);
} }
// Parse table rows: "| 0xNNN | Name | function/variable | ... |"
let Some(ref file) = current_file else {
continue;
};
if !line.starts_with("| 0x") {
continue;
}
let cols: Vec<&str> = line.split('|').collect();
// cols[0] is empty (before first |), cols[1] = ordinal, cols[2] = name, cols[3] = type
if cols.len() < 4 {
continue;
}
let ordinal_str = cols[1].trim();
let name = cols[2].trim();
let type_str = cols[3].trim();
let Some(ordinal) = parse_hex_ordinal(ordinal_str) else {
continue;
};
let entry = ExportInfo {
ordinal,
name: name.to_string(),
is_function: type_str == "function",
};
modules
.entry(file.clone())
.or_default()
.insert(ordinal, entry);
} }
ExportDatabase { modules } ExportDatabase { modules }
} }
fn parse_hex_ordinal(s: &str) -> Option<u16> {
let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))?;
u16::from_str_radix(hex, 16).ok()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -182,13 +159,4 @@ mod tests {
fn test_lookup_unknown_library() { fn test_lookup_unknown_library() {
assert!(lookup("nonexistent.dll", 0x001).is_none()); assert!(lookup("nonexistent.dll", 0x001).is_none());
} }
#[test]
fn test_parse_hex_ordinal() {
assert_eq!(parse_hex_ordinal("0x001"), Some(1));
assert_eq!(parse_hex_ordinal("0x3A3"), Some(0x3A3));
assert_eq!(parse_hex_ordinal("0xFFFF"), Some(0xFFFF));
assert_eq!(parse_hex_ordinal("invalid"), None);
assert_eq!(parse_hex_ordinal(""), None);
}
} }