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:
Campbell 2025-03-19 18:50:37 -04:00
parent 6ab9993dd9
commit 62f6e6c0ec
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
10 changed files with 358 additions and 29 deletions

2
.gitignore vendored
View File

@ -21,3 +21,5 @@ target/
*.tmd
*.tik
*.cert
*.footer
*.app

7
Cargo.lock generated
View File

@ -197,6 +197,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "heck"
version = "0.5.0"
@ -263,6 +269,7 @@ dependencies = [
"byteorder",
"cbc",
"clap",
"glob",
"hex",
"sha1",
]

View File

@ -29,4 +29,5 @@ cbc = "0"
aes = "0"
hex = "0"
sha1 = "0"
glob = "0"
clap = { version = "4", features = ["derive"] }

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!");

View File

@ -8,7 +8,7 @@ use std::fmt;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use sha1::{Sha1, Digest};
use crate::title::tmd::ContentRecord;
use crate::title::crypto::decrypt_content;
use crate::title::crypto;
#[derive(Debug)]
pub enum ContentError {
@ -40,6 +40,8 @@ pub struct ContentRegion {
}
impl ContentRegion {
/// Creates a ContentRegion instance that can be used to parse and edit content stored in a
/// digital Wii title from the content area of a WAD and the ContentRecords from a TMD.
pub fn from_bytes(data: &[u8], content_records: Vec<ContentRecord>) -> Result<Self, std::io::Error> {
let content_region_size = data.len() as u32;
let num_contents = content_records.len() as u16;
@ -76,6 +78,24 @@ impl ContentRegion {
})
}
/// Creates a ContentRegion instance from the ContentRecords of a TMD that contains no actual
/// content. This can be used to load existing content from files.
pub fn new(content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
let content_region_size: u64 = content_records.iter().map(|x| (x.content_size + 63) & !63).sum();
let content_region_size = content_region_size as u32;
let num_contents = content_records.len() as u16;
let content_start_offsets: Vec<u64> = Vec::new();
let mut contents: Vec<Vec<u8>> = Vec::new();
contents.resize(num_contents as usize, Vec::new());
Ok(ContentRegion {
content_records,
content_region_size,
num_contents,
content_start_offsets,
contents,
})
}
pub fn to_bytes(&self) -> Result<Vec<u8>, std::io::Error> {
let mut buf: Vec<u8> = Vec::new();
for i in 0..self.num_contents {
@ -95,7 +115,7 @@ impl ContentRegion {
pub fn get_content_by_index(&self, index: usize, title_key: [u8; 16]) -> Result<Vec<u8>, ContentError> {
let content = self.get_enc_content_by_index(index)?;
// Verify the hash of the decrypted content against its record.
let mut content_dec = decrypt_content(&content, title_key, self.content_records[index].index);
let mut content_dec = crypto::decrypt_content(&content, title_key, self.content_records[index].index);
content_dec.resize(self.content_records[index].content_size as usize, 0);
let mut hasher = Sha1::new();
hasher.update(content_dec.clone());
@ -125,4 +145,21 @@ impl ContentRegion {
Err(ContentError::CIDNotFound)
}
}
pub fn load_content(&mut self, content: &[u8], index: usize, title_key: [u8; 16]) -> Result<(), ContentError> {
if index >= self.content_records.len() {
return Err(ContentError::IndexNotFound);
}
// Hash the content we're trying to load to ensure it matches the hash expected in the
// matching record.
let mut hasher = Sha1::new();
hasher.update(content);
let result = hasher.finalize();
if result[..] != self.content_records[index].content_hash {
return Err(ContentError::BadHash);
}
let content_enc = crypto::encrypt_content(content, title_key, self.content_records[index].index, self.content_records[index].content_size);
self.contents[index] = content_enc;
Ok(())
}
}

View File

@ -52,6 +52,9 @@ pub fn encrypt_content(data: &[u8], title_key: [u8; 16], index: u16, size: u64)
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
let encryptor = Aes128CbcEnc::new(&title_key.into(), iv.as_slice().into());
let mut buf = data.to_owned();
let size = (size + 15) & !15;
buf.resize(size as usize, 0);
encryptor.encrypt_padded_mut::<ZeroPadding>(&mut buf, size as usize).unwrap();
buf.resize(size as usize, 0);
buf
}

View File

@ -1,5 +1,7 @@
// title/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
//
// Root for all title-related modules and implementation of the high-level Title object.
pub mod commonkeys;
pub mod content;
@ -7,3 +9,124 @@ pub mod crypto;
pub mod ticket;
pub mod tmd;
pub mod wad;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub enum TitleError {
BadTicket,
BadTMD,
BadContent,
InvalidWAD,
WADError(wad::WADError),
IOError(std::io::Error),
}
impl fmt::Display for TitleError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let description = match *self {
TitleError::BadTicket => "The provided Ticket data was invalid.",
TitleError::BadTMD => "The provided TMD data was invalid.",
TitleError::BadContent => "The provided content data was invalid.",
TitleError::InvalidWAD => "The provided WAD data was invalid.",
TitleError::WADError(_) => "A WAD could not be built from the provided data.",
TitleError::IOError(_) => "The provided Title data was invalid.",
};
f.write_str(description)
}
}
impl Error for TitleError {}
#[derive(Debug)]
pub struct Title {
cert_chain: Vec<u8>,
crl: Vec<u8>,
pub ticket: ticket::Ticket,
pub tmd: tmd::TMD,
pub content: content::ContentRegion,
meta: Vec<u8>
}
impl Title {
pub fn from_wad(wad: &wad::WAD) -> Result<Title, TitleError> {
let ticket = ticket::Ticket::from_bytes(&wad.ticket()).map_err(|_| TitleError::BadTicket)?;
let tmd = tmd::TMD::from_bytes(&wad.tmd()).map_err(|_| TitleError::BadTMD)?;
let content = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records.clone()).map_err(|_| TitleError::BadContent)?;
let title = Title {
cert_chain: wad.cert_chain(),
crl: wad.crl(),
ticket,
tmd,
content,
meta: wad.meta(),
};
Ok(title)
}
pub fn to_wad(&self) -> Result<wad::WAD, TitleError> {
// Create a new WAD from the data in the Title.
let wad = wad::WAD::from_parts(
&self.cert_chain,
&self.crl,
&self.ticket,
&self.tmd,
&self.content,
&self.meta
).map_err(TitleError::WADError)?;
Ok(wad)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Title, TitleError> {
let wad = wad::WAD::from_bytes(bytes).map_err(|_| TitleError::InvalidWAD)?;
let title = Title::from_wad(&wad)?;
Ok(title)
}
pub fn get_content_by_index(&self, index: usize) -> Result<Vec<u8>, content::ContentError> {
let content = self.content.get_content_by_index(index, self.ticket.dec_title_key())?;
Ok(content)
}
pub fn get_content_by_cid(&self, cid: u32) -> Result<Vec<u8>, content::ContentError> {
let content = self.content.get_content_by_cid(cid, self.ticket.dec_title_key())?;
Ok(content)
}
pub fn cert_chain(&self) -> Vec<u8> {
self.cert_chain.clone()
}
pub fn set_cert_chain(&mut self, cert_chain: &[u8]) {
self.cert_chain = cert_chain.to_vec();
}
pub fn crl(&self) -> Vec<u8> {
self.crl.clone()
}
pub fn set_crl(&mut self, crl: &[u8]) {
self.crl = crl.to_vec();
}
pub fn set_ticket(&mut self, ticket: ticket::Ticket) {
self.ticket = ticket;
}
pub fn set_tmd(&mut self, tmd: tmd::TMD) {
self.tmd = tmd;
}
pub fn set_content(&mut self, content: content::ContentRegion) {
self.content = content;
}
pub fn meta(&self) -> Vec<u8> {
self.meta.clone()
}
pub fn set_meta(&mut self, meta: &[u8]) {
self.meta = meta.to_vec();
}
}

View File

@ -7,6 +7,7 @@ use std::io::{Cursor, Read, Write};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
#[derive(Debug)]
#[derive(Clone)]
pub struct ContentRecord {
pub content_id: u32,
pub index: u16,
@ -44,6 +45,7 @@ pub struct TMD {
}
impl TMD {
/// Creates a new TMD instance from the binary data of a TMD file.
pub fn from_bytes(data: &[u8]) -> Result<Self, std::io::Error> {
let mut buf = Cursor::new(data);
let signature_type = buf.read_u32::<BigEndian>()?;
@ -129,6 +131,7 @@ impl TMD {
})
}
/// Dumps the data in a TMD back into binary data that can be written to a file.
pub fn to_bytes(&self) -> Result<Vec<u8>, std::io::Error> {
let mut buf: Vec<u8> = Vec::new();
buf.write_u32::<BigEndian>(self.signature_type)?;

View File

@ -8,6 +8,7 @@ use std::fmt;
use std::str;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use crate::title::{tmd, ticket, content};
#[derive(Debug)]
pub enum WADError {
@ -28,7 +29,7 @@ impl fmt::Display for WADError {
impl Error for WADError {}
#[derive(Debug)]
pub enum WADTypes {
pub enum WADType {
Installable,
ImportBoot
}
@ -42,7 +43,7 @@ pub struct WAD {
#[derive(Debug)]
pub struct WADHeader {
pub header_size: u32,
pub wad_type: WADTypes,
pub wad_type: WADType,
pub wad_version: u16,
cert_chain_size: u32,
crl_size: u32,
@ -63,6 +64,53 @@ pub struct WADBody {
meta: Vec<u8>,
}
impl WADHeader {
pub fn from_body(body: &WADBody) -> Result<WADHeader, WADError> {
// Generates a new WADHeader from a populated WADBody object.
// Parse the TMD and use that to determine if this is a standard WAD or a boot2 WAD.
let tmd = tmd::TMD::from_bytes(&body.tmd).map_err(WADError::IOError)?;
let wad_type = match hex::encode(tmd.title_id).as_str() {
"0000000100000001" => WADType::ImportBoot,
_ => WADType::Installable,
};
// Find the sizes of all components of the Title.
let cert_chain_size = body.cert_chain.len() as u32;
let crl_size = body.crl.len() as u32;
let ticket_size = body.ticket.len() as u32;
let tmd_size = body.tmd.len() as u32;
let content_size = body.content.len() as u32;
let meta_size = body.meta.len() as u32;
let header = WADHeader {
header_size: 32,
wad_type,
wad_version: 0, // This is always officially a zero.
cert_chain_size,
crl_size,
ticket_size,
tmd_size,
content_size,
meta_size,
padding: [0; 32],
};
Ok(header)
}
}
impl WADBody {
pub fn from_parts(cert_chain: &[u8], crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD,
content: &content::ContentRegion, meta: &[u8]) -> Result<WADBody, WADError> {
let body = WADBody {
cert_chain: cert_chain.to_vec(),
crl: crl.to_vec(),
ticket: ticket.to_bytes().map_err(WADError::IOError)?,
tmd: tmd.to_bytes().map_err(WADError::IOError)?,
content: content.to_bytes().map_err(WADError::IOError)?,
meta: meta.to_vec(),
};
Ok(body)
}
}
impl WAD {
pub fn from_bytes(data: &[u8]) -> Result<WAD, WADError> {
let mut buf = Cursor::new(data);
@ -71,8 +119,8 @@ impl WAD {
buf.read_exact(&mut wad_type).map_err(WADError::IOError)?;
let wad_type = match str::from_utf8(&wad_type) {
Ok(wad_type) => match wad_type {
"Is" => WADTypes::Installable,
"ib" => WADTypes::ImportBoot,
"Is" => WADType::Installable,
"ib" => WADType::ImportBoot,
_ => return Err(WADError::BadType),
},
Err(_) => return Err(WADError::BadType),
@ -142,12 +190,23 @@ impl WAD {
Ok(wad)
}
pub fn from_parts(cert_chain: &[u8], crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD,
content: &content::ContentRegion, meta: &[u8]) -> Result<WAD, WADError> {
let body = WADBody::from_parts(cert_chain, crl, ticket, tmd, content, meta)?;
let header = WADHeader::from_body(&body)?;
let wad = WAD {
header,
body,
};
Ok(wad)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, WADError> {
let mut buf = Vec::new();
buf.write_u32::<BigEndian>(self.header.header_size).map_err(WADError::IOError)?;
match self.header.wad_type {
WADTypes::Installable => { buf.write("Is".as_bytes()).map_err(WADError::IOError)?; },
WADTypes::ImportBoot => { buf.write("ib".as_bytes()).map_err(WADError::IOError)?; },
WADType::Installable => { buf.write("Is".as_bytes()).map_err(WADError::IOError)?; },
WADType::ImportBoot => { buf.write("ib".as_bytes()).map_err(WADError::IOError)?; },
}
buf.write_u16::<BigEndian>(self.header.wad_version).map_err(WADError::IOError)?;
buf.write_u32::<BigEndian>(self.header.cert_chain_size).map_err(WADError::IOError)?;
@ -178,23 +237,53 @@ impl WAD {
self.body.cert_chain.clone()
}
pub fn set_cert_chain(&mut self, cert_chain: &[u8]) {
self.body.cert_chain = cert_chain.to_vec();
self.header.cert_chain_size = cert_chain.len() as u32;
}
pub fn crl(&self) -> Vec<u8> {
self.body.crl.clone()
}
pub fn set_crl(&mut self, crl: &[u8]) {
self.body.crl = crl.to_vec();
self.header.crl_size = crl.len() as u32;
}
pub fn ticket(&self) -> Vec<u8> {
self.body.ticket.clone()
}
pub fn set_ticket(&mut self, ticket: &[u8]) {
self.body.ticket = ticket.to_vec();
self.header.ticket_size = ticket.len() as u32;
}
pub fn tmd(&self) -> Vec<u8> {
self.body.tmd.clone()
}
pub fn set_tmd(&mut self, tmd: &[u8]) {
self.body.tmd = tmd.to_vec();
self.header.tmd_size = tmd.len() as u32;
}
pub fn content(&self) -> Vec<u8> {
self.body.content.clone()
}
pub fn set_content(&mut self, content: &[u8]) {
self.body.content = content.to_vec();
self.header.content_size = content.len() as u32;
}
pub fn meta(&self) -> Vec<u8> {
self.body.meta.clone()
}
pub fn set_meta(&mut self, meta: &[u8]) {
self.body.meta = meta.to_vec();
self.header.meta_size = meta.len() as u32;
}
}