diff --git a/Cargo.lock b/Cargo.lock index 5e27a64..6d35f2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1537,6 +1537,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + [[package]] name = "rsa" version = "0.9.10" @@ -1682,6 +1691,7 @@ dependencies = [ "rand 0.10.0", "regex", "reqwest", + "roxmltree", "rsa", "rust-ini", "sha1", diff --git a/Cargo.toml b/Cargo.toml index d195bd8..d6f0ae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,3 +41,4 @@ walkdir = "2" tempfile = "3" rust-ini = "0" zip = "8" +roxmltree = "0" diff --git a/src/bin/rustwii/main.rs b/src/bin/rustwii/main.rs index 68476e8..d72be9d 100644 --- a/src/bin/rustwii/main.rs +++ b/src/bin/rustwii/main.rs @@ -128,13 +128,13 @@ fn main() -> Result<()> { title::ios::Commands::Cios { base, map, - output, cios_version, + output, modules, slot, version } => { - title::ios::build_cios(base, map, output, cios_version, modules, slot, version)? + title::ios::build_cios(base, map, cios_version, output, modules, slot, version)? }, title::ios::Commands::Patch { input, diff --git a/src/bin/rustwii/title/ios.rs b/src/bin/rustwii/title/ios.rs index 8ef2d3f..b3e60db 100644 --- a/src/bin/rustwii/title/ios.rs +++ b/src/bin/rustwii/title/ios.rs @@ -3,12 +3,13 @@ // // Code for the IOS patcher and cIOS build commands in the rustwii CLI. -use std::fs; +use std::{env, fs}; +use std::io::{Cursor, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; use clap::{Args, Subcommand}; use rustwii::title; -use rustwii::title::iospatcher; +use rustwii::title::{crypto, iospatcher}; use rustwii::title::tmd::ContentType; #[derive(Subcommand)] @@ -20,11 +21,10 @@ pub enum Commands { base: String, /// The cIOS map file map: String, + /// The cIOS version from the map to build + cios_version: String, /// Path for the finished cIOS WAD output: String, - /// The cIOS version from the map to build - #[arg(short, long)] - cios_version: Option, /// Path to the directory containing the cIOS modules (optional, defaults to the current /// directory) #[arg(short, long)] @@ -198,13 +198,191 @@ fn set_type_normal(ios: &mut title::Title, index: usize) -> Result<()> { pub fn build_cios( base: &str, map: &str, + cios_version: &str, output: &str, - cios_version: &Option, modules: &Option, slot: &Option, version: &Option ) -> Result<()> { - todo!(); + let base_path = Path::new(base); + if !base_path.exists() { + bail!("Source WAD \"{}\" does not exist.", base_path.display()); + } + + let map_path = Path::new(map); + if !map_path.exists() { + bail!("cIOS map file \"{}\" does not exist.", map_path.display()); + } + + let modules_path = if modules.is_some() { + PathBuf::from(modules.clone().unwrap()) + } else { + env::current_dir()? + }; + if !modules_path.exists() { + bail!("cIOS modules directory \"{}\" does not exist.", modules_path.display()); + } + + let out_path = Path::new(output); + + let mut ios = title::Title::from_bytes(&fs::read(base_path)?).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?; + + let map_string = fs::read_to_string(map_path).with_context(|| "Failed to read cIOS map file! The file may be invalid.")?; + let doc = roxmltree::Document::parse(&map_string).with_context(|| "Failed to parse cIOS map! The map may be invalid.")?; + let root = doc.root_element(); + + // Search the map for the specified cIOS version and bail if this map doesn't include it. + let target_option = root.children().into_iter() + .find(|x| x.attribute("name").unwrap_or("").eq(cios_version)); + let target_cios = if let Some(cios) = target_option { + cios + } else { + bail!("The target cIOS \"{}\" could not be found in the provided map.", cios_version); + }; + + // Search the target cIOS for the base provided and return the node matching it, if found. + let provided_base = format!("{}", ios.tmd().title_id().last().unwrap()); + let base_option = target_cios.children().into_iter() + .find(|x| x.attribute("ios").unwrap_or("").eq(&provided_base)); + let target_base = if let Some(base) = base_option { + base + } else { + bail!("The provided base (IOS{}) does not match any bases supported by the provided map.", provided_base); + }; + + // Check the IOS version required by the map against the version provided. + let req_base_version = target_base.attribute("version") + .unwrap_or("") + .parse::().with_context(|| "Failed to parse required base version from map! The map may be invalid.")?; + if ios.tmd().title_version() != req_base_version { + bail!("The provided base (IOS{} v{}) doesn't match the required version, v{}", + provided_base, + ios.tmd().title_version(), + req_base_version + ); + } + + println!("Building cIOS \"{cios_version}\" from base IOS{provided_base} v{req_base_version}..."); + + println!(" - Patching existing modules..."); + let content_with_patches: Vec = target_base.children() + .filter(|x| x.has_attribute("patchscount")) // yes, this typo is really in the maps + .collect(); + for content in content_with_patches { + let cid = u32::from_str_radix( + content.attribute("id").unwrap().trim_start_matches("0x"), + 16 + )?; + let target_content = ios.get_content_by_cid(cid)?; + let mut buf = Cursor::new(target_content); + + // Iterate over the patches. Another filter happens here just to be sure that this node's + // children are all actually patches. + for patch in content.children().filter(|x| x.tag_name().name().eq("patch")) { + // Now we need to do some "fun" parsing stuff to get the find and replace bytes from the map. + + // This block currently omitted because I don't really think it's necessary? The map + // contains the replacement bytes and the offset to write them at, so using the find + // bytes seems unnecessary. + // let find_strs: Vec<&str> = patch.attribute("originalbytes").unwrap().split(",").collect(); + // let find_seq: Vec = find_strs.iter() + // .map(|x| x.trim_start_matches("0x")) + // .map(|x| u8::from_str_radix(x, 16).unwrap()) + // .collect(); + + let replace_strs: Vec<&str> = patch.attribute("newbytes").unwrap().split(",").collect(); + let replace_seq: Vec = replace_strs.iter() + .map(|x| x.trim_start_matches("0x")) + .map(|x| u8::from_str_radix(x, 16).unwrap()) + .collect(); + + let offset = u64::from_str_radix( + patch.attribute("offset").unwrap().trim_start_matches("0x"), + 16 + )?; + buf.seek(SeekFrom::Start(offset))?; + buf.write_all(&replace_seq)?; + } + + // Done with patches for this content, so put it back into the title. + let idx = ios.tmd().get_index_from_cid(cid)?; + ios.set_content(buf.get_ref(), idx, None, Some(ContentType::Normal))?; + } + println!(" - Done."); + + println!(" - Adding required additional modules..."); + let content_new_modules: Vec = target_base.children() + .filter(|x| x.has_attribute("module")) + .collect(); + for content in content_new_modules { + let target_index = content.attribute("tmdmoduleid").unwrap().parse::()?; + let cid = u32::from_str_radix( + content.attribute("id").unwrap().trim_start_matches("0x"), + 16 + )?; + + let module_path = modules_path.join(content.attribute("module").unwrap_or("")) + .with_extension("app"); + if !module_path.exists() { + bail!("The required cIOS module \"{}\" could not be found.", module_path.file_name().unwrap().display()); + } + + let module = fs::read(module_path)?; + if target_index == -1 { + ios.add_content(&module, cid, ContentType::Normal)?; + } else { + let existing_module = ios.get_content_by_index(target_index as usize)?; + let existing_cid = ios.tmd().content_records()[target_index as usize].content_id; + let existing_type = ios.tmd().content_records()[target_index as usize].content_type; + ios.set_content(&module, target_index as usize, Some(cid), Some(ContentType::Normal))?; + ios.add_content(&existing_module, existing_cid, existing_type)?; + } + } + println!(" - Done."); + + println!(" - Setting cIOS' properties..."); + // Set the cIOS' slot and version to the specified values. + let slot = if let Some(slot) = slot && *slot >= 3 { + if *slot >= 3 { + *slot + } else { + println!("Warning: Ignoring invalid slot \"{slot}\", using default slot 249 instead."); + 249 + } + } else { + 249 + }; + + let version = if let Some(version) = version { + *version + } else { + 65535 + }; + + let tid = hex::decode(format!("00000001{slot:08X}"))?; + ios.set_title_id(tid.try_into().unwrap()).expect("Failed to set IOS slot!"); + println!(" - Set cIOS slot: {slot}"); + + ios.set_title_version(version); + println!(" - Set cIOS version: {version}"); + + println!(" - Done."); + + // If this is a vWii cIOS, then we need to re-encrypt it with the regular Wii common key so that + // it could be installed from within Wii mode with a normal WAD installer. + if ios.ticket().common_key_index() == 2 { + let title_key_dec = ios.ticket().title_key_dec(); + let title_key_common = crypto::encrypt_title_key(title_key_dec, 0, ios.tmd().title_id(), false); + let mut ticket = ios.ticket().clone(); + ticket.set_title_key(title_key_common); + ticket.set_common_key_index(0); + ios.set_ticket(ticket); + } + + ios.fakesign()?; + fs::write(out_path, ios.to_wad()?.to_bytes()?)?; + + println!("Successfully built cIOS \"{cios_version}\"!"); Ok(()) } diff --git a/src/title/tmd.rs b/src/title/tmd.rs index cb583b3..db438ad 100644 --- a/src/title/tmd.rs +++ b/src/title/tmd.rs @@ -55,7 +55,7 @@ impl fmt::Display for TitleType { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum ContentType { Normal = 1, Development = 2,