Finished cIOS building

This commit is contained in:
2026-03-10 22:10:32 -04:00
parent 3f112856e5
commit 9472c049ee
5 changed files with 199 additions and 10 deletions

10
Cargo.lock generated
View File

@@ -1537,6 +1537,15 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.10" version = "0.9.10"
@@ -1682,6 +1691,7 @@ dependencies = [
"rand 0.10.0", "rand 0.10.0",
"regex", "regex",
"reqwest", "reqwest",
"roxmltree",
"rsa", "rsa",
"rust-ini", "rust-ini",
"sha1", "sha1",

View File

@@ -41,3 +41,4 @@ walkdir = "2"
tempfile = "3" tempfile = "3"
rust-ini = "0" rust-ini = "0"
zip = "8" zip = "8"
roxmltree = "0"

View File

@@ -128,13 +128,13 @@ fn main() -> Result<()> {
title::ios::Commands::Cios { title::ios::Commands::Cios {
base, base,
map, map,
output,
cios_version, cios_version,
output,
modules, modules,
slot, slot,
version 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 { title::ios::Commands::Patch {
input, input,

View File

@@ -3,12 +3,13 @@
// //
// Code for the IOS patcher and cIOS build commands in the rustwii CLI. // 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 std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use clap::{Args, Subcommand}; use clap::{Args, Subcommand};
use rustwii::title; use rustwii::title;
use rustwii::title::iospatcher; use rustwii::title::{crypto, iospatcher};
use rustwii::title::tmd::ContentType; use rustwii::title::tmd::ContentType;
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -20,11 +21,10 @@ pub enum Commands {
base: String, base: String,
/// The cIOS map file /// The cIOS map file
map: String, map: String,
/// The cIOS version from the map to build
cios_version: String,
/// Path for the finished cIOS WAD /// Path for the finished cIOS WAD
output: String, output: String,
/// The cIOS version from the map to build
#[arg(short, long)]
cios_version: Option<u16>,
/// Path to the directory containing the cIOS modules (optional, defaults to the current /// Path to the directory containing the cIOS modules (optional, defaults to the current
/// directory) /// directory)
#[arg(short, long)] #[arg(short, long)]
@@ -198,13 +198,191 @@ fn set_type_normal(ios: &mut title::Title, index: usize) -> Result<()> {
pub fn build_cios( pub fn build_cios(
base: &str, base: &str,
map: &str, map: &str,
cios_version: &str,
output: &str, output: &str,
cios_version: &Option<u16>,
modules: &Option<String>, modules: &Option<String>,
slot: &Option<u8>, slot: &Option<u8>,
version: &Option<u16> version: &Option<u16>
) -> Result<()> { ) -> 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::<u16>().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<roxmltree::Node> = 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<u8> = 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<u8> = 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<roxmltree::Node> = target_base.children()
.filter(|x| x.has_attribute("module"))
.collect();
for content in content_new_modules {
let target_index = content.attribute("tmdmoduleid").unwrap().parse::<i32>()?;
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(()) Ok(())
} }

View File

@@ -55,7 +55,7 @@ impl fmt::Display for TitleType {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Copy)]
pub enum ContentType { pub enum ContentType {
Normal = 1, Normal = 1,
Development = 2, Development = 2,