Added WAD packing command to rustii CLI

Also added lots of required library magic to make WAD packing possible. This includes the high-level Title object from libWiiPy, the ability to set content in a WAD, the ability to generate a WADHeader from a WADBody, and more.
This commit is contained in:
2025-03-19 18:50:37 -04:00
parent 6ab9993dd9
commit 62f6e6c0ec
10 changed files with 358 additions and 29 deletions

View File

@@ -2,9 +2,13 @@
use std::fs;
use rustii::title::{tmd, ticket, content, crypto, wad};
use rustii::title;
fn main() {
let data = fs::read("sm.wad").unwrap();
let title = title::Title::from_bytes(&data).unwrap();
println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id));
let wad = wad::WAD::from_bytes(&data).unwrap();
println!("size of tmd: {:?}", wad.tmd().len());
let tmd = tmd::TMD::from_bytes(&wad.tmd()).unwrap();

View File

@@ -3,10 +3,12 @@
//
// Code for WAD-related commands in the rustii CLI.
use clap::Subcommand;
use std::{str, fs};
use std::path::Path;
use rustii::title::{tmd, ticket, wad, content};
use std::path::{Path, PathBuf};
use clap::Subcommand;
use glob::glob;
use rustii::title::{tmd, ticket, content, wad};
use rustii::title;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]
@@ -24,35 +26,93 @@ pub enum Commands {
}
pub fn pack_wad(input: &str, output: &str) {
print!("packing");
let in_path = Path::new(input);
if !in_path.exists() {
panic!("Error: Source directory does not exist.");
}
// Read TMD file (only accept one file).
let tmd_files: Vec<PathBuf> = glob(&format!("{}/*.tmd", in_path.display()))
.expect("failed to read glob pattern")
.filter_map(|f| f.ok()).collect();
if tmd_files.is_empty() {
panic!("Error: No TMD file found in the source directory.");
} else if tmd_files.len() > 1 {
panic!("Error: More than one TMD file found in the source directory.")
}
let tmd = tmd::TMD::from_bytes(&fs::read(&tmd_files[0]).expect("could not read TMD file")).unwrap();
// Read Ticket file (only accept one file).
let ticket_files: Vec<PathBuf> = glob(&format!("{}/*.tik", in_path.display()))
.expect("failed to read glob pattern")
.filter_map(|f| f.ok()).collect();
if ticket_files.is_empty() {
panic!("Error: No Ticket file found in the source directory.");
} else if ticket_files.len() > 1 {
panic!("Error: More than one Ticket file found in the source directory.")
}
let tik = ticket::Ticket::from_bytes(&fs::read(&ticket_files[0]).expect("could not read Ticket file")).unwrap();
// Read cert chain (only accept one file).
let cert_files: Vec<PathBuf> = glob(&format!("{}/*.cert", in_path.display()))
.expect("failed to read glob pattern")
.filter_map(|f| f.ok()).collect();
if cert_files.is_empty() {
panic!("Error: No cert file found in the source directory.");
} else if cert_files.len() > 1 {
panic!("Error: More than one Cert file found in the source directory.")
}
let cert_chain = fs::read(&cert_files[0]).expect("could not read cert chain file");
// Read footer, if one exists (only accept one file).
let footer_files: Vec<PathBuf> = glob(&format!("{}/*.footer", in_path.display()))
.expect("failed to read glob pattern")
.filter_map(|f| f.ok()).collect();
let mut footer: Vec<u8> = Vec::new();
if footer_files.len() == 1 {
footer = fs::read(&footer_files[0]).unwrap();
}
// Iterate over expected content and read it into a content region.
let mut content_region = content::ContentRegion::new(tmd.content_records.clone()).expect("could not create content region");
for content in tmd.content_records.clone() {
let data = fs::read(format!("{}/{:08X}.app", in_path.display(), content.index)).expect("could not read required content");
content_region.load_content(&data, content.index as usize, tik.dec_title_key()).expect("failed to load content into ContentRegion");
}
let wad = wad::WAD::from_parts(&cert_chain, &[], &tik, &tmd, &content_region, &footer).expect("failed to create WAD");
// Write out WAD file.
let mut out_path = PathBuf::from(output);
match out_path.extension() {
Some(ext) => {
if ext != "wad" {
out_path.set_extension("wad");
}
},
None => {
out_path.set_extension("wad");
}
}
fs::write(out_path, wad.to_bytes().unwrap()).expect("could not write to wad file");
println!("WAD file packed!");
}
pub fn unpack_wad(input: &str, output: &str) {
let wad_file = fs::read(input).expect("could not read WAD");
let wad = wad::WAD::from_bytes(&wad_file).expect("could not parse WAD");
let tmd = tmd::TMD::from_bytes(&wad.tmd()).expect("could not parse TMD");
let tik = ticket::Ticket::from_bytes(&wad.ticket()).expect("could not parse Ticket");
let cert_data = &wad.cert_chain();
let meta_data = &wad.meta();
let title = title::Title::from_bytes(&wad_file).unwrap();
let tid = hex::encode(title.tmd.title_id);
// Create output directory if it doesn't exist.
if !Path::new(output).exists() {
fs::create_dir(output).expect("could not create output directory");
}
let out_path = Path::new(output);
// Write out all WAD components.
let tmd_file_name = format!("{}.tmd", hex::encode(tmd.title_id));
fs::write(Path::join(out_path, tmd_file_name), tmd.to_bytes().unwrap()).expect("could not write TMD file");
let ticket_file_name = format!("{}.tik", hex::encode(tmd.title_id));
fs::write(Path::join(out_path, ticket_file_name), tik.to_bytes().unwrap()).expect("could not write Ticket file");
let cert_file_name = format!("{}.cert", hex::encode(tmd.title_id));
fs::write(Path::join(out_path, cert_file_name), cert_data).expect("could not write Cert file");
let meta_file_name = format!("{}.footer", hex::encode(tmd.title_id));
fs::write(Path::join(out_path, meta_file_name), meta_data).expect("could not write footer file");
let tmd_file_name = format!("{}.tmd", tid);
fs::write(Path::join(out_path, tmd_file_name), title.tmd.to_bytes().unwrap()).expect("could not write TMD file");
let ticket_file_name = format!("{}.tik", tid);
fs::write(Path::join(out_path, ticket_file_name), title.ticket.to_bytes().unwrap()).expect("could not write Ticket file");
let cert_file_name = format!("{}.cert", tid);
fs::write(Path::join(out_path, cert_file_name), title.cert_chain()).expect("could not write Cert file");
let meta_file_name = format!("{}.footer", tid);
fs::write(Path::join(out_path, meta_file_name), title.meta()).expect("could not write footer file");
// Iterate over contents, decrypt them, and write them out.
let content_region = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records).unwrap();
for i in 0..tmd.num_contents {
let content_file_name = format!("{:08X}.app", content_region.content_records[i as usize].index);
let dec_content = content_region.get_content_by_index(i as usize, tik.dec_title_key()).unwrap();
for i in 0..title.tmd.num_contents {
let content_file_name = format!("{:08X}.app", title.content.content_records[i as usize].index);
let dec_content = title.get_content_by_index(i as usize).unwrap();
fs::write(Path::join(out_path, content_file_name), dec_content).unwrap();
}
println!("WAD file unpacked!");