Compare commits

..

No commits in common. "405df67e49e076eff3053ff0b5e39a1207902b45" and "3fd701cac6f5aa291e577076f4b072364723be13" have entirely different histories.

5 changed files with 111 additions and 128 deletions

View File

@ -3,8 +3,6 @@
*Like rusty but it's rustii because the Wii? Get it?*
[![Build rustii](https://github.com/NinjaCheetah/rustii/actions/workflows/rust.yml/badge.svg)](https://github.com/NinjaCheetah/rustii/actions/workflows/rust.yml)
rustii is a library and command line tool written in Rust for handling the various files and formats found on the Wii. rustii is a port of my other library, [libWiiPy](https://github.com/NinjaCheetah/libWiiPy), which aims to accomplish the same goal in Python. Compared to libWiiPy, rustii is in its very early stages of development and is missing most of the features present in its Python counterpart. The goal is for rustii and libWiiPy to eventually have feature parity, with the rustii CLI acting as a drop-in replacement for the (comparatively much less efficient) [WiiPy](https://github.com/NinjaCheetah/WiiPy) CLI.
I'm still very new to Rust, so pardon any messy code or confusing API decisions you may find. libWiiPy started off like that, too.

View File

@ -5,44 +5,35 @@
use std::{str, fs};
use std::path::Path;
use anyhow::{bail, Context, Result};
use rustii::{title, title::cert, title::tmd, title::ticket, title::wad, title::versions};
use crate::filetypes::{WiiFileType, identify_file_type};
// Avoids duplicated code, since both TMD and Ticket info print the TID in the same way.
fn print_tid(title_id: [u8; 8]) -> Result<()> {
let ascii = String::from_utf8_lossy(&title_id[4..]).trim_end_matches('\0').trim_start_matches('\0').to_owned();
let ascii_tid = if ascii.len() == 4 {
Some(ascii)
fn tid_to_ascii(tid: [u8; 8]) -> Option<String> {
let tid = String::from_utf8_lossy(&tid[4..]).trim_end_matches('\0').trim_start_matches('\0').to_owned();
if tid.len() == 4 {
Some(tid)
} else {
None
};
if ascii_tid.is_some() {
println!(" Title ID: {} ({})", hex::encode(title_id).to_uppercase(), ascii_tid.unwrap());
} else {
println!(" Title ID: {}", hex::encode(title_id).to_uppercase());
}
Ok(())
}
// Same as above, both the TMD and Ticket info print the title version in the same way.
fn print_title_version(title_version: u16, title_id: [u8; 8], is_vwii: bool) -> Result<()> {
let converted_ver = versions::dec_to_standard(title_version, &hex::encode(title_id), Some(is_vwii));
if hex::encode(title_id).eq("0000000100000001") {
println!(" Title Version: {} (boot2v{})", title_version, title_version);
} else if hex::encode(title_id)[..8].eq("00000001") && converted_ver.is_some() {
println!(" Title Version: {} ({})", title_version, converted_ver.unwrap());
} else {
println!(" Title Version: {}", title_version);
}
Ok(())
}
fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()> {
fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) {
// Print all important keys from the TMD.
println!("Title Info");
print_tid(tmd.title_id)?;
print_title_version(tmd.title_version, tmd.title_id, tmd.is_vwii())?;
let ascii_tid = tid_to_ascii(tmd.title_id);
if ascii_tid.is_some() {
println!(" Title ID: {} ({})", hex::encode(tmd.title_id).to_uppercase(), ascii_tid.unwrap());
} else {
println!(" Title ID: {}", hex::encode(tmd.title_id).to_uppercase());
}
let converted_ver = versions::dec_to_standard(tmd.title_version, &hex::encode(tmd.title_id), Some(tmd.is_vwii != 0));
if hex::encode(tmd.title_id).eq("0000000100000001") {
println!(" Title Version: {} (boot2v{})", tmd.title_version, tmd.title_version);
} else if hex::encode(tmd.title_id)[..8].eq("00000001") && converted_ver.is_some() {
println!(" Title Version: {} ({})", tmd.title_version, converted_ver.unwrap());
} else {
println!(" Title Version: {}", tmd.title_version);
}
println!(" TMD Version: {}", tmd.tmd_version);
if hex::encode(tmd.ios_tid).eq("0000000000000000") {
println!(" Required IOS: N/A");
@ -124,14 +115,25 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()>
println!(" Content Size: {} bytes", content.content_size);
println!(" Content Hash: {}", hex::encode(content.content_hash));
}
Ok(())
}
fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) -> Result<()> {
fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) {
// Print all important keys from the Ticket.
println!("Ticket Info");
print_tid(ticket.title_id)?;
print_title_version(ticket.title_version, ticket.title_id, ticket.common_key_index == 2)?;
let ascii_tid = tid_to_ascii(ticket.title_id);
if ascii_tid.is_some() {
println!(" Title ID: {} ({})", hex::encode(ticket.title_id).to_uppercase(), ascii_tid.unwrap());
} else {
println!(" Title ID: {}", hex::encode(ticket.title_id).to_uppercase());
}
let converted_ver = versions::dec_to_standard(ticket.title_version, &hex::encode(ticket.title_id), None);
if hex::encode(ticket.title_id).eq("0000000100000001") {
println!(" Title Version: {} (boot2v{})", ticket.title_version, ticket.title_version);
} else if hex::encode(ticket.title_id)[..8].eq("00000001") && converted_ver.is_some() {
println!(" Title Version: {} ({})", ticket.title_version, converted_ver.unwrap());
} else {
println!(" Title Version: {}", ticket.title_version);
}
println!(" Ticket Version: {}", ticket.ticket_version);
let signature_issuer = String::from_utf8(Vec::from(ticket.signature_issuer)).unwrap_or_default();
if signature_issuer.contains("XS00000003") {
@ -182,26 +184,25 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) ->
} else {
println!(" Fakesigned: {}", ticket.is_fakesigned());
}
Ok(())
}
fn print_wad_info(wad: wad::WAD) -> Result<()> {
fn print_wad_info(wad: wad::WAD) {
println!("WAD Info");
match wad.header.wad_type {
wad::WADType::ImportBoot => { println!(" WAD Type: boot2") },
wad::WADType::Installable => { println!(" WAD Type: Standard Installable") },
}
// Create a Title for size info, signing info and TMD/Ticket info.
let title = title::Title::from_wad(&wad).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
let min_size_blocks = title.title_size_blocks(None)?;
let max_size_blocks = title.title_size_blocks(Some(true))?;
let title = title::Title::from_wad(&wad).unwrap();
let min_size_blocks = title.title_size_blocks(None).unwrap();
let max_size_blocks = title.title_size_blocks(Some(true)).unwrap();
if min_size_blocks == max_size_blocks {
println!(" Installed Size: {} blocks", min_size_blocks);
} else {
println!(" Installed Size: {}-{} blocks", min_size_blocks, max_size_blocks);
}
let min_size = title.title_size(None)? as f64 / 1048576.0;
let max_size = title.title_size(Some(true))? as f64 / 1048576.0;
let min_size = title.title_size(None).unwrap() as f64 / 1048576.0;
let max_size = title.title_size(Some(true)).unwrap() as f64 / 1048576.0;
if min_size == max_size {
println!(" Installed Size (MB): {:.2} MB", min_size);
} else {
@ -215,9 +216,9 @@ fn print_wad_info(wad: wad::WAD) -> Result<()> {
false => {
if title.is_fakesigned() {
"Fakesigned"
} else if cert::verify_tmd(&title.cert_chain.tmd_cert(), &title.tmd)? {
} else if cert::verify_tmd(&title.cert_chain.tmd_cert(), &title.tmd).unwrap() {
"Piratelegit (Unmodified TMD, Modified Ticket)"
} else if cert::verify_ticket(&title.cert_chain.ticket_cert(), &title.ticket)? {
} else if cert::verify_ticket(&title.cert_chain.ticket_cert(), &title.ticket).unwrap() {
"Edited (Modified TMD, Unmodified Ticket)"
} else {
"Illegitimate (Modified TMD + Ticket)"
@ -234,33 +235,31 @@ fn print_wad_info(wad: wad::WAD) -> Result<()> {
};
println!(" Signing Status: {}", signing_str);
println!();
print_ticket_info(title.ticket, Some(title.cert_chain.ticket_cert()))?;
print_ticket_info(title.ticket, Some(title.cert_chain.ticket_cert()));
println!();
print_tmd_info(title.tmd, Some(title.cert_chain.tmd_cert()))?;
Ok(())
print_tmd_info(title.tmd, Some(title.cert_chain.tmd_cert()));
}
pub fn info(input: &str) -> Result<()> {
pub fn info(input: &str) {
let in_path = Path::new(input);
if !in_path.exists() {
bail!("Input file \"{}\" does not exist.", in_path.display());
panic!("Error: Input file does not exist.");
}
match identify_file_type(input) {
Some(WiiFileType::Tmd) => {
let tmd = tmd::TMD::from_bytes(fs::read(in_path)?.as_slice()).with_context(|| "The provided TMD file could not be parsed, and is likely invalid.")?;
print_tmd_info(tmd, None)?;
let tmd = tmd::TMD::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
print_tmd_info(tmd, None);
},
Some(WiiFileType::Ticket) => {
let ticket = ticket::Ticket::from_bytes(fs::read(in_path)?.as_slice()).with_context(|| "The provided Ticket file could not be parsed, and is likely invalid.")?;
print_ticket_info(ticket, None)?;
let ticket = ticket::Ticket::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
print_ticket_info(ticket, None);
},
Some(WiiFileType::Wad) => {
let wad = wad::WAD::from_bytes(fs::read(in_path)?.as_slice()).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
print_wad_info(wad)?;
let wad = wad::WAD::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
print_wad_info(wad);
},
None => {
bail!("Information cannot be displayed for this file type.");
println!("Error: Information cannot be displayed for this file.");
}
}
Ok(())
}

View File

@ -51,19 +51,19 @@ fn main() -> Result<()> {
wad::convert_wad(input, target, output)?
},
Some(wad::Commands::Pack { input, output}) => {
wad::pack_wad(input, output)?
wad::pack_wad(input, output)
},
Some(wad::Commands::Unpack { input, output }) => {
wad::unpack_wad(input, output)?
wad::unpack_wad(input, output)
},
&None => { /* This is for me handled by clap */}
}
},
Some(Commands::Fakesign { input, output }) => {
fakesign::fakesign(input, output)?
fakesign::fakesign(input, output)
},
Some(Commands::Info { input }) => {
info::info(input)?
info::info(input)
}
None => {}
}

View File

@ -5,14 +5,13 @@
use std::{str, fs};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use rustii::{title, title::tmd, title::ticket};
use crate::filetypes::{WiiFileType, identify_file_type};
pub fn fakesign(input: &str, output: &Option<String>) -> Result<()> {
pub fn fakesign(input: &str, output: &Option<String>) {
let in_path = Path::new(input);
if !in_path.exists() {
bail!("Input file \"{}\" does not exist.", in_path.display());
panic!("Error: Input file does not exist.");
}
match identify_file_type(input) {
Some(WiiFileType::Wad) => {
@ -22,11 +21,10 @@ pub fn fakesign(input: &str, output: &Option<String>) -> Result<()> {
PathBuf::from(input)
};
// Load WAD into a Title instance, then fakesign it.
let mut title = title::Title::from_bytes(fs::read(in_path).with_context(|| "Could not open WAD file for reading.")?.as_slice())
.with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the provided WAD.")?;
let mut title = title::Title::from_bytes(fs::read(in_path).unwrap().as_slice()).expect("could not read WAD file");
title.fakesign().expect("could not fakesign WAD");
// Write output file.
fs::write(out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
fs::write(out_path, title.to_wad().unwrap().to_bytes().expect("could not create output WAD")).expect("could not write output WAD file");
println!("WAD fakesigned!");
},
Some(WiiFileType::Tmd) => {
@ -36,11 +34,10 @@ pub fn fakesign(input: &str, output: &Option<String>) -> Result<()> {
PathBuf::from(input)
};
// Load TMD into a TMD instance, then fakesign it.
let mut tmd = tmd::TMD::from_bytes(fs::read(in_path).with_context(|| "Could not open TMD file for reading.")?.as_slice())
.with_context(|| "The provided TMD file could not be parsed, and is likely invalid.")?;
tmd.fakesign().with_context(|| "An unknown error occurred while fakesigning the provided TMD.")?;
let mut tmd = tmd::TMD::from_bytes(fs::read(in_path).unwrap().as_slice()).expect("could not read TMD file");
tmd.fakesign().expect("could not fakesign TMD");
// Write output file.
fs::write(out_path, tmd.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
fs::write(out_path, tmd.to_bytes().expect("could not create output TMD")).expect("could not write output TMD file");
println!("TMD fakesigned!");
},
Some(WiiFileType::Ticket) => {
@ -50,16 +47,14 @@ pub fn fakesign(input: &str, output: &Option<String>) -> Result<()> {
PathBuf::from(input)
};
// Load Ticket into a Ticket instance, then fakesign it.
let mut ticket = ticket::Ticket::from_bytes(fs::read(in_path).with_context(|| "Could not open Ticket file for reading.")?.as_slice())
.with_context(|| "The provided Ticket file could not be parsed, and is likely invalid.")?;
ticket.fakesign().with_context(|| "An unknown error occurred while fakesigning the provided Ticket.")?;
let mut ticket = ticket::Ticket::from_bytes(fs::read(in_path).unwrap().as_slice()).expect("could not read Ticket file");
ticket.fakesign().expect("could not fakesign Ticket");
// Write output file.
fs::write(out_path, ticket.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
fs::write(out_path, ticket.to_bytes().expect("could not create output Ticket")).expect("could not write output Ticket file");
println!("Ticket fakesigned!");
},
None => {
bail!("You can only fakesign TMDs, Tickets, and WADs!");
panic!("Error: You can only fakesign TMDs, Tickets, and WADs!");
}
}
Ok(())
}

View File

@ -74,7 +74,7 @@ impl fmt::Display for Target {
pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>) -> Result<()> {
let in_path = Path::new(input);
if !in_path.exists() {
bail!("Source WAD \"{}\" could not be found.", in_path.display());
bail!("Source WAD \"{}\" could not be found.", input);
}
// Parse the target passed to identify the encryption target.
let target = if target.dev {
@ -89,12 +89,12 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
PathBuf::from(output.clone().unwrap()).with_extension("wad")
} else {
match target {
Target::Retail => PathBuf::from(format!("{}_retail.wad", in_path.file_stem().unwrap().to_str().unwrap())),
Target::Dev => PathBuf::from(format!("{}_dev.wad", in_path.file_stem().unwrap().to_str().unwrap())),
Target::Vwii => PathBuf::from(format!("{}_vWii.wad", in_path.file_stem().unwrap().to_str().unwrap())),
Target::Retail => PathBuf::from(format!("{}_retail", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
Target::Dev => PathBuf::from(format!("{}_dev", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
Target::Vwii => PathBuf::from(format!("{}_vWii", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
}
};
let mut title = title::Title::from_bytes(fs::read(in_path)?.as_slice()).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
let mut title = title::Title::from_bytes(fs::read(in_path)?.as_slice()).with_context(|| "The provided WAD file could not be loaded, and is likely invalid.")?;
// Bail if the WAD is already using the selected encryption.
if matches!(target, Target::Dev) && title.ticket.is_dev() {
bail!("This is already a development WAD!");
@ -119,21 +119,18 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
title.ticket.set_signature_issuer(String::from("Root-CA00000002-XS00000006"))?;
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id, Some(true));
title.ticket.common_key_index = 0;
title.tmd.is_vwii = 0;
},
Target::Retail => {
title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
title.ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?;
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id, Some(false));
title.ticket.common_key_index = 0;
title.tmd.is_vwii = 0;
},
Target::Vwii => {
title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
title.ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?;
title_key_new = crypto::encrypt_title_key(title_key, 2, title.ticket.title_id, Some(false));
title.ticket.common_key_index = 2;
title.tmd.is_vwii = 1;
}
}
title.ticket.title_key = title_key_new;
@ -143,56 +140,56 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
Ok(())
}
pub fn pack_wad(input: &str, output: &str) -> Result<()> {
pub fn pack_wad(input: &str, output: &str) {
let in_path = Path::new(input);
if !in_path.exists() {
bail!("Source directory \"{}\" does not exist.", in_path.display());
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()))?
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() {
bail!("No TMD file found in the source directory.");
panic!("Error: No TMD file found in the source directory.");
} else if tmd_files.len() > 1 {
bail!("More than one TMD file found in the source directory.");
panic!("Error: More than one TMD file found in the source directory.")
}
let tmd = tmd::TMD::from_bytes(&fs::read(&tmd_files[0]).with_context(|| "Could not open TMD file for reading.")?)
.with_context(|| "The provided TMD file appears to be invalid.")?;
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()))?
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() {
bail!("No Ticket file found in the source directory.");
panic!("Error: No Ticket file found in the source directory.");
} else if ticket_files.len() > 1 {
bail!("More than one Ticket file found in the source directory.");
panic!("Error: More than one Ticket file found in the source directory.")
}
let tik = ticket::Ticket::from_bytes(&fs::read(&ticket_files[0]).with_context(|| "Could not open Ticket file for reading.")?)
.with_context(|| "The provided Ticket file appears to be invalid.")?;
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()))?
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() {
bail!("No cert file found in the source directory.");
panic!("Error: No cert file found in the source directory.");
} else if cert_files.len() > 1 {
bail!("More than one Cert file found in the source directory.");
panic!("Error: More than one Cert file found in the source directory.")
}
let cert_chain = cert::CertificateChain::from_bytes(&fs::read(&cert_files[0]).with_context(|| "Could not open cert chain file for reading.")?)
.with_context(|| "The provided certificate chain appears to be invalid.")?;
let cert_chain = cert::CertificateChain::from_bytes(&fs::read(&cert_files[0]).expect("could not read cert chain file")).unwrap();
// Read footer, if one exists (only accept one file).
let footer_files: Vec<PathBuf> = glob(&format!("{}/*.footer", in_path.display()))?
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]).with_context(|| "Could not open footer file for reading.")?;
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())?;
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)).with_context(|| format!("Could not open content file \"{:08X}.app\" for reading.", content.index))?;
content_region.load_content(&data, content.index as usize, tik.dec_title_key())
.expect("failed to load content into ContentRegion, this is probably because content was modified which isn't supported yet");
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).with_context(|| "An unknown error occurred while building a WAD from the input files.")?;
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() {
@ -205,39 +202,33 @@ pub fn pack_wad(input: &str, output: &str) -> Result<()> {
out_path.set_extension("wad");
}
}
fs::write(out_path.clone(), wad.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
fs::write(out_path, wad.to_bytes().unwrap()).expect("could not write to wad file");
println!("WAD file packed!");
Ok(())
}
pub fn unpack_wad(input: &str, output: &str) -> Result<()> {
let in_path = Path::new(input);
if !in_path.exists() {
bail!("Source WAD \"{}\" could not be found.", input);
}
let wad_file = fs::read(in_path).with_context(|| format!("Failed to open WAD file \"{}\" for reading.", in_path.display()))?;
let title = title::Title::from_bytes(&wad_file).with_context(|| format!("The provided WAD file \"{}\" appears to be invalid.", in_path.display()))?;
pub fn unpack_wad(input: &str, output: &str) {
let wad_file = fs::read(input).expect("could not read WAD");
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.
let out_path = Path::new(output);
if !out_path.exists() {
fs::create_dir(out_path).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?;
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", tid);
fs::write(Path::join(out_path, tmd_file_name.clone()), title.tmd.to_bytes()?).with_context(|| format!("Failed to open TMD file \"{}\" for writing.", tmd_file_name))?;
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.clone()), title.ticket.to_bytes()?).with_context(|| format!("Failed to open Ticket file \"{}\" for writing.", ticket_file_name))?;
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.clone()), title.cert_chain.to_bytes()?).with_context(|| format!("Failed to open certificate chain file \"{}\" for writing.", cert_file_name))?;
fs::write(Path::join(out_path, cert_file_name), title.cert_chain.to_bytes().unwrap()).expect("could not write Cert file");
let meta_file_name = format!("{}.footer", tid);
fs::write(Path::join(out_path, meta_file_name.clone()), title.meta()).with_context(|| format!("Failed to open footer file \"{}\" for writing.", meta_file_name))?;
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.
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).with_context(|| format!("Failed to unpack content with Content ID {:08X}.", title.content.content_records[i as usize].content_id))?;
fs::write(Path::join(out_path, content_file_name), dec_content).with_context(|| format!("Failed to open content file \"{:08X}.app\" for writing.", title.content.content_records[i as usize].content_id))?;
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!");
Ok(())
}