diff --git a/examples/game_title/main.rs b/examples/game_title/main.rs index 177fbab..43d06fa 100644 --- a/examples/game_title/main.rs +++ b/examples/game_title/main.rs @@ -1,6 +1,5 @@ use anyhow::{Context, Error}; use clap::{command, Parser}; -use hex; use iso2god::game_list; @@ -25,16 +24,14 @@ struct Cli { fn main() -> Result<(), Error> { let args = Cli::parse(); - let title_id: [u8; 4] = hex::decode(&args.title_id)? - .try_into() - .map_err(|_| Error::msg("invalid title ID"))?; + let title_id = u32::from_str_radix("4D530064", 16)?; match args.source { Source::BuiltIn => { println!("querying the built-in DB for title ID {}", args.title_id); if let Some(name) = game_list::find_title_by_id(title_id) { - let title_id = hex::encode_upper(title_id); + let title_id = format!("{:08X}", title_id); println!("Title ID: {title_id}"); println!(" Name: {name}"); } else { @@ -48,7 +45,7 @@ fn main() -> Result<(), Error> { let client = unity::Client::new().context("error creating XboxUnity client")?; let unity_title_info = client - .find_xbox_360_title_id(&title_id) + .find_xbox_360_title_id(title_id) .context("error querying XboxUnity")?; if let Some(unity_title_info) = &unity_title_info { diff --git a/examples/game_title/unity.rs b/examples/game_title/unity.rs index 3972727..1fcdfda 100644 --- a/examples/game_title/unity.rs +++ b/examples/game_title/unity.rs @@ -8,8 +8,6 @@ use anyhow::Error; use std::time::Duration; -use hex; - use std::fmt; #[derive(Deserialize)] @@ -131,8 +129,8 @@ impl Client { Ok(response) } - pub fn find_xbox_360_title_id(&self, title_id: &[u8; 4]) -> Result, Error> { - let title_id = hex::encode_upper(title_id); + pub fn find_xbox_360_title_id(&self, title_id: u32) -> Result, Error> { + let title_id = format!("{:08X}", title_id); let title_list = self.search(&title_id)?; diff --git a/src/bin/iso2god.rs b/src/bin/iso2god.rs index 6bc05ab..b195455 100644 --- a/src/bin/iso2god.rs +++ b/src/bin/iso2god.rs @@ -10,7 +10,9 @@ use anyhow::{Context, Error}; use clap::{arg, command, Parser}; -use iso2god::{game_list, god, iso, xex}; +use iso2god::executable::TitleInfo; +use iso2god::god::ContentType; +use iso2god::{game_list, god, iso}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -58,25 +60,22 @@ fn main() -> Result<(), Error> { let mut source_iso = iso::IsoReader::read(BufReader::new(source_iso_file)) .context("error reading source ISO")?; - let mut default_xex = source_iso - .get_entry(&"\\default.xex".into()) - .context("error reading source ISO")? - .context("default.xex file not found")?; + let title_info = + TitleInfo::from_image(&mut source_iso).context("error reading image executable")?; - let default_xex_header = - xex::XexHeader::read(&mut default_xex).context("error reading default.xex")?; - - let exe_info = default_xex_header - .fields - .execution_info - .context("no execution info in default.xex header")?; + let exe_info = title_info.execution_info; + let content_type = title_info.content_type; { - let title_id = hex::encode_upper(exe_info.title_id); + let title_id = format!("{:08X}", exe_info.title_id); let name = game_list::find_title_by_id(exe_info.title_id).unwrap_or("(unknown)".to_owned()); println!("Title ID: {title_id}"); println!(" Name: {name}"); + match content_type { + ContentType::GamesOnDemand => println!(" Type: Games on Demand"), + ContentType::XboxOriginal => println!(" Type: Xbox Original"), + } } if args.dry_run { @@ -93,9 +92,6 @@ fn main() -> Result<(), Error> { let block_count = div_ceil(data_size, god::BLOCK_SIZE as u64); let part_count = div_ceil(block_count, god::BLOCKS_PER_PART); - // the original code does not seem to support other types - let content_type = god::ContentType::GamesOnDemand; - let file_layout = god::FileLayout::new(&args.dest_dir, &exe_info, content_type); println!("clearing data directory"); @@ -150,7 +146,7 @@ fn main() -> Result<(), Error> { part_count as u32, last_part_size + (part_count - 1) * (god::BLOCK_SIZE as u64) * 0xa290, ) - .with_content_type(god::ContentType::GamesOnDemand) + .with_content_type(content_type) .with_mht_hash(&mht.digest()); let game_title = args diff --git a/src/executable/mod.rs b/src/executable/mod.rs new file mode 100644 index 0000000..0b4b123 --- /dev/null +++ b/src/executable/mod.rs @@ -0,0 +1,107 @@ +use crate::god::ContentType; +use crate::iso::IsoReader; +use anyhow::{anyhow, Context, Error}; +use byteorder::{ReadBytesExt, BE, LE}; +use std::io::{Read, Seek, SeekFrom}; + +pub mod xbe; +pub mod xex; + +#[derive(Clone, Debug)] +pub struct TitleExecutionInfo { + pub media_id: u32, + pub version: u32, + pub base_version: u32, + pub title_id: u32, + pub platform: u8, + pub executable_type: u8, + pub disc_number: u8, + pub disc_count: u8, +} + +pub struct TitleInfo { + pub content_type: ContentType, + pub execution_info: TitleExecutionInfo, +} + +impl TitleExecutionInfo { + pub fn from_xex(reader: &mut R) -> Result { + Ok(TitleExecutionInfo { + media_id: reader.read_u32::()?, + version: reader.read_u32::()?, + base_version: reader.read_u32::()?, + title_id: reader.read_u32::()?, + platform: reader.read_u8()?, + executable_type: reader.read_u8()?, + disc_number: reader.read_u8()?, + disc_count: reader.read_u8()?, + }) + } + + pub fn from_xbe(reader: &mut R) -> Result { + reader.seek(SeekFrom::Current(8))?; + let title_id = reader.read_u32::()?; + + reader.seek(SeekFrom::Current(164))?; + let version = reader.read_u32::()?; + + Ok(TitleExecutionInfo { + media_id: 0, + version, + base_version: 0, + title_id, + platform: 0, + executable_type: 0, + disc_number: 1, + disc_count: 1, + }) + } +} + +impl TitleInfo { + pub fn from_image(iso_image: &mut IsoReader) -> Result { + let content_type; + let mut executable; + + match iso_image.get_entry(&"\\default.xex".into())? { + Some(entry) => { + executable = entry; + content_type = ContentType::GamesOnDemand; + } + None => { + executable = iso_image + .get_entry(&"\\default.xbe".into())? + .ok_or_else(|| anyhow!("no executable found in this image"))?; + content_type = ContentType::XboxOriginal; + } + } + + let execution_info; + + match content_type { + ContentType::GamesOnDemand => { + let default_xex_header = + xex::XexHeader::read(&mut executable).context("error reading default.xex")?; + + execution_info = default_xex_header + .fields + .execution_info + .context("no execution info in default.xex header")?; + } + ContentType::XboxOriginal => { + let default_xbe_header = + xbe::XbeHeader::read(&mut executable).context("error reading default.xbe")?; + + execution_info = default_xbe_header + .fields + .execution_info + .context("no execution info in default.xbe header")?; + } + } + + Ok(TitleInfo { + content_type, + execution_info, + }) + } +} diff --git a/src/executable/xbe.rs b/src/executable/xbe.rs new file mode 100644 index 0000000..92bf13f --- /dev/null +++ b/src/executable/xbe.rs @@ -0,0 +1,54 @@ +use crate::executable::TitleExecutionInfo; +use anyhow::{bail, Error}; +use byteorder::{ReadBytesExt, LE}; +use std::io::{Read, Seek, SeekFrom}; + +pub struct XbeHeader { + // We only need these fields to get the cert address + pub dw_base_addr: u32, + pub dw_certificate_addr: u32, + pub fields: XbeHeaderFields, +} + +#[derive(Clone, Default, Debug)] +pub struct XbeHeaderFields { + pub execution_info: Option, +} + +impl XbeHeader { + pub fn read(reader: &mut R) -> Result { + Self::check_magic_bytes(reader)?; + + // Offset 0x0104 + reader.seek(SeekFrom::Current(256))?; + let dw_base_addr = reader.read_u32::()?; + + // Offset 0x0118 + reader.seek(SeekFrom::Current(16))?; + let dw_certificate_addr = reader.read_u32::()?; + + let offset = reader.stream_position()? - 284; + let cert_address = dw_certificate_addr - dw_base_addr; + reader.seek(SeekFrom::Start(offset + (cert_address as u64)))?; + + let mut fields: XbeHeaderFields = Default::default(); + fields.execution_info = Some(TitleExecutionInfo::from_xbe(reader)?); + + Ok(XbeHeader { + dw_base_addr, + dw_certificate_addr, + fields, + }) + } + + fn check_magic_bytes(reader: &mut R) -> Result<(), Error> { + let mut magic_bytes = [0u8; 4]; + reader.read_exact(&mut magic_bytes)?; + + if &magic_bytes != b"XBEH" { + bail!("missing 'XBEH' magic bytes in XBE header"); + } + + Ok(()) + } +} diff --git a/src/xex.rs b/src/executable/xex.rs similarity index 77% rename from src/xex.rs rename to src/executable/xex.rs index e3ec584..b5ff76d 100644 --- a/src/xex.rs +++ b/src/executable/xex.rs @@ -5,6 +5,7 @@ use byteorder::{ReadBytesExt, BE}; use bitflags::bitflags; use num_enum::TryFromPrimitive; +use crate::executable::TitleExecutionInfo; use anyhow::{bail, Error}; #[derive(Clone, Debug)] @@ -32,7 +33,7 @@ bitflags! { #[derive(Clone, Default, Debug)] pub struct XexHeaderFields { - pub execution_info: Option, + pub execution_info: Option, // other fields will be added if and when necessary } @@ -74,18 +75,6 @@ enum XexHeaderFieldId { ExportsByName = 0x_00_e1_04_02, } -#[derive(Clone, Debug)] -pub struct XexExecutionInfo { - pub media_id: [u8; 4], - pub version: u32, - pub base_version: u32, - pub title_id: [u8; 4], - pub platform: u8, - pub executable_type: u8, - pub disc_number: u8, - pub disc_count: u8, -} - impl XexHeader { pub fn read(reader: &mut R) -> Result { Self::check_magic_bytes(reader)?; @@ -133,7 +122,7 @@ impl XexHeader { Some(Key::ExecutionId) => { let offset = reader.stream_position()?; reader.seek(SeekFrom::Start(header_offset + (value as u64)))?; - fields.execution_info = Some(XexExecutionInfo::read(reader)?); + fields.execution_info = Some(TitleExecutionInfo::from_xex(reader)?); reader.seek(SeekFrom::Start(offset))?; } @@ -149,32 +138,3 @@ impl XexHeader { }) } } - -impl XexExecutionInfo { - fn read(reader: &mut R) -> Result { - let mut media_id = [0_u8; 4]; - reader.read_exact(&mut media_id)?; - - let version = reader.read_u32::()?; - let base_version = reader.read_u32::()?; - - let mut title_id = [0_u8; 4]; - reader.read_exact(&mut title_id)?; - - let platform = reader.read_u8()?; - let executable_type = reader.read_u8()?; - let disc_number = reader.read_u8()?; - let disc_count = reader.read_u8()?; - - Ok(XexExecutionInfo { - media_id, - version, - base_version, - title_id, - platform, - executable_type, - disc_number, - disc_count, - }) - } -} diff --git a/src/game_list/generate.bash b/src/game_list/generate.bash index c13b044..e25b568 100755 --- a/src/game_list/generate.bash +++ b/src/game_list/generate.bash @@ -8,9 +8,9 @@ cat <<'EOF' // Run `./generate.bash titles.jsonl mod.rs` to re-generate. -pub fn find_title_by_id(title_id: [u8; 4]) -> Option { +pub fn find_title_by_id(title_id: u32) -> Option { GAMES_BY_TITLE_ID - .binary_search_by_key(&u32::from_be_bytes(title_id), |x| x.0) + .binary_search_by_key(&title_id, |x| x.0) .ok() .map(|i| GAMES_BY_TITLE_ID[i].1.to_owned()) } diff --git a/src/game_list/mod.rs b/src/game_list/mod.rs index d2fe1ec..cd6d8dc 100644 --- a/src/game_list/mod.rs +++ b/src/game_list/mod.rs @@ -1,10 +1,9 @@ // DO NOT EDIT; this file is auto-generated. // Run `./generate.bash titles.jsonl mod.rs` to re-generate. - -pub fn find_title_by_id(title_id: [u8; 4]) -> Option { +pub fn find_title_by_id(title_id: u32) -> Option { GAMES_BY_TITLE_ID - .binary_search_by_key(&u32::from_be_bytes(title_id), |x| x.0) + .binary_search_by_key(&title_id, |x| x.0) .ok() .map(|i| GAMES_BY_TITLE_ID[i].1.to_owned()) } diff --git a/src/god/con_header.rs b/src/god/con_header.rs index e294d26..0eb4776 100644 --- a/src/god/con_header.rs +++ b/src/god/con_header.rs @@ -4,7 +4,7 @@ use byteorder::{WriteBytesExt, BE, LE}; use sha1::{Digest, Sha1}; -use crate::xex; +use crate::executable::TitleExecutionInfo; const EMPTY_LIVE: &[u8] = include_bytes!("empty_live.bin"); @@ -57,7 +57,7 @@ impl ConHeaderBuilder { self } - pub fn with_execution_info(mut self, exe_info: &xex::XexExecutionInfo) -> Self { + pub fn with_execution_info(mut self, exe_info: &TitleExecutionInfo) -> Self { let mut cursor = Cursor::new(&mut self.buffer); cursor.seek(SeekFrom::Start(0x0364)).unwrap(); @@ -68,10 +68,10 @@ impl ConHeaderBuilder { cursor.write_u8(exe_info.disc_count).unwrap(); cursor.seek(SeekFrom::Start(0x0360)).unwrap(); - cursor.write_all(&exe_info.title_id).unwrap(); + cursor.write_u32::(exe_info.title_id).unwrap(); cursor.seek(SeekFrom::Start(0x0354)).unwrap(); - cursor.write_all(&exe_info.media_id).unwrap(); + cursor.write_u32::(exe_info.media_id).unwrap(); self } diff --git a/src/god/file_layout.rs b/src/god/file_layout.rs index 15cefb4..2c5829d 100644 --- a/src/god/file_layout.rs +++ b/src/god/file_layout.rs @@ -1,21 +1,19 @@ use std::path::{Path, PathBuf}; -use hex; - -use crate::xex; +use crate::executable::TitleExecutionInfo; use super::*; pub struct FileLayout<'a> { base_path: &'a Path, - exe_info: &'a xex::XexExecutionInfo, + exe_info: &'a TitleExecutionInfo, content_type: ContentType, } impl<'a> FileLayout<'a> { pub fn new( base_path: &'a Path, - exe_info: &'a xex::XexExecutionInfo, + exe_info: &'a TitleExecutionInfo, content_type: ContentType, ) -> FileLayout<'a> { FileLayout { @@ -26,7 +24,7 @@ impl<'a> FileLayout<'a> { } fn title_id_string(&self) -> String { - hex::encode_upper(self.exe_info.title_id) + format!("{:08X}", self.exe_info.title_id) } fn content_type_string(&self) -> String { @@ -34,7 +32,14 @@ impl<'a> FileLayout<'a> { } fn media_id_string(&self) -> String { - hex::encode_upper(self.exe_info.media_id) + match self.content_type { + ContentType::GamesOnDemand => { + format!("{:08X}", self.exe_info.media_id) + } + ContentType::XboxOriginal => { + format!("{:08X}", self.exe_info.title_id) + } + } } pub fn data_dir_path(&self) -> PathBuf { diff --git a/src/lib.rs b/src/lib.rs index b9537cf..d9685e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ +pub mod executable; pub mod game_list; pub mod god; pub mod iso; -pub mod xex;