mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2026-03-11 04:27:49 -04:00
Finished cIOS building
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -41,3 +41,4 @@ walkdir = "2"
|
||||
tempfile = "3"
|
||||
rust-ini = "0"
|
||||
zip = "8"
|
||||
roxmltree = "0"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<u16>,
|
||||
/// 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<u16>,
|
||||
modules: &Option<String>,
|
||||
slot: &Option<u8>,
|
||||
version: &Option<u16>
|
||||
) -> 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(())
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ impl fmt::Display for TitleType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ContentType {
|
||||
Normal = 1,
|
||||
Development = 2,
|
||||
|
||||
Reference in New Issue
Block a user