mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-07 16:01:01 -04:00
Compare commits
4 Commits
3fd701cac6
...
405df67e49
Author | SHA1 | Date | |
---|---|---|---|
405df67e49 | |||
74584b1ffd | |||
d9e8465f0c | |||
85f3f028d4 |
@ -3,6 +3,8 @@
|
||||
|
||||
*Like rusty but it's rustii because the Wii? Get it?*
|
||||
|
||||
[](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.
|
||||
|
@ -5,35 +5,44 @@
|
||||
|
||||
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};
|
||||
|
||||
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)
|
||||
// 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)
|
||||
} 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(())
|
||||
}
|
||||
|
||||
fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) {
|
||||
// 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<()> {
|
||||
// Print all important keys from the TMD.
|
||||
println!("Title Info");
|
||||
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);
|
||||
}
|
||||
print_tid(tmd.title_id)?;
|
||||
print_title_version(tmd.title_version, tmd.title_id, tmd.is_vwii())?;
|
||||
println!(" TMD Version: {}", tmd.tmd_version);
|
||||
if hex::encode(tmd.ios_tid).eq("0000000000000000") {
|
||||
println!(" Required IOS: N/A");
|
||||
@ -115,25 +124,14 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) {
|
||||
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>) {
|
||||
fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) -> Result<()> {
|
||||
// Print all important keys from the Ticket.
|
||||
println!("Ticket Info");
|
||||
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);
|
||||
}
|
||||
print_tid(ticket.title_id)?;
|
||||
print_title_version(ticket.title_version, ticket.title_id, ticket.common_key_index == 2)?;
|
||||
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") {
|
||||
@ -184,25 +182,26 @@ 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) {
|
||||
fn print_wad_info(wad: wad::WAD) -> Result<()> {
|
||||
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).unwrap();
|
||||
let min_size_blocks = title.title_size_blocks(None).unwrap();
|
||||
let max_size_blocks = title.title_size_blocks(Some(true)).unwrap();
|
||||
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))?;
|
||||
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).unwrap() as f64 / 1048576.0;
|
||||
let max_size = title.title_size(Some(true)).unwrap() as f64 / 1048576.0;
|
||||
let min_size = title.title_size(None)? as f64 / 1048576.0;
|
||||
let max_size = title.title_size(Some(true))? as f64 / 1048576.0;
|
||||
if min_size == max_size {
|
||||
println!(" Installed Size (MB): {:.2} MB", min_size);
|
||||
} else {
|
||||
@ -216,9 +215,9 @@ fn print_wad_info(wad: wad::WAD) {
|
||||
false => {
|
||||
if title.is_fakesigned() {
|
||||
"Fakesigned"
|
||||
} else if cert::verify_tmd(&title.cert_chain.tmd_cert(), &title.tmd).unwrap() {
|
||||
} else if cert::verify_tmd(&title.cert_chain.tmd_cert(), &title.tmd)? {
|
||||
"Piratelegit (Unmodified TMD, Modified Ticket)"
|
||||
} else if cert::verify_ticket(&title.cert_chain.ticket_cert(), &title.ticket).unwrap() {
|
||||
} else if cert::verify_ticket(&title.cert_chain.ticket_cert(), &title.ticket)? {
|
||||
"Edited (Modified TMD, Unmodified Ticket)"
|
||||
} else {
|
||||
"Illegitimate (Modified TMD + Ticket)"
|
||||
@ -235,31 +234,33 @@ fn print_wad_info(wad: wad::WAD) {
|
||||
};
|
||||
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()));
|
||||
print_tmd_info(title.tmd, Some(title.cert_chain.tmd_cert()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn info(input: &str) {
|
||||
pub fn info(input: &str) -> Result<()> {
|
||||
let in_path = Path::new(input);
|
||||
if !in_path.exists() {
|
||||
panic!("Error: Input file does not exist.");
|
||||
bail!("Input file \"{}\" does not exist.", in_path.display());
|
||||
}
|
||||
match identify_file_type(input) {
|
||||
Some(WiiFileType::Tmd) => {
|
||||
let tmd = tmd::TMD::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
|
||||
print_tmd_info(tmd, None);
|
||||
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)?;
|
||||
},
|
||||
Some(WiiFileType::Ticket) => {
|
||||
let ticket = ticket::Ticket::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
|
||||
print_ticket_info(ticket, None);
|
||||
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)?;
|
||||
},
|
||||
Some(WiiFileType::Wad) => {
|
||||
let wad = wad::WAD::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
|
||||
print_wad_info(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)?;
|
||||
},
|
||||
None => {
|
||||
println!("Error: Information cannot be displayed for this file.");
|
||||
bail!("Information cannot be displayed for this file type.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -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 => {}
|
||||
}
|
||||
|
@ -5,13 +5,14 @@
|
||||
|
||||
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>) {
|
||||
pub fn fakesign(input: &str, output: &Option<String>) -> Result<()> {
|
||||
let in_path = Path::new(input);
|
||||
if !in_path.exists() {
|
||||
panic!("Error: Input file does not exist.");
|
||||
bail!("Input file \"{}\" does not exist.", in_path.display());
|
||||
}
|
||||
match identify_file_type(input) {
|
||||
Some(WiiFileType::Wad) => {
|
||||
@ -21,10 +22,11 @@ pub fn fakesign(input: &str, output: &Option<String>) {
|
||||
PathBuf::from(input)
|
||||
};
|
||||
// Load WAD into a Title instance, then fakesign it.
|
||||
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");
|
||||
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.")?;
|
||||
// Write output file.
|
||||
fs::write(out_path, title.to_wad().unwrap().to_bytes().expect("could not create output WAD")).expect("could not write output WAD file");
|
||||
fs::write(out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||
println!("WAD fakesigned!");
|
||||
},
|
||||
Some(WiiFileType::Tmd) => {
|
||||
@ -34,10 +36,11 @@ pub fn fakesign(input: &str, output: &Option<String>) {
|
||||
PathBuf::from(input)
|
||||
};
|
||||
// Load TMD into a TMD instance, then fakesign it.
|
||||
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");
|
||||
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.")?;
|
||||
// Write output file.
|
||||
fs::write(out_path, tmd.to_bytes().expect("could not create output TMD")).expect("could not write output TMD file");
|
||||
fs::write(out_path, tmd.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||
println!("TMD fakesigned!");
|
||||
},
|
||||
Some(WiiFileType::Ticket) => {
|
||||
@ -47,14 +50,16 @@ pub fn fakesign(input: &str, output: &Option<String>) {
|
||||
PathBuf::from(input)
|
||||
};
|
||||
// Load Ticket into a Ticket instance, then fakesign it.
|
||||
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");
|
||||
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.")?;
|
||||
// Write output file.
|
||||
fs::write(out_path, ticket.to_bytes().expect("could not create output Ticket")).expect("could not write output Ticket file");
|
||||
fs::write(out_path, ticket.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||
println!("Ticket fakesigned!");
|
||||
},
|
||||
None => {
|
||||
panic!("Error: You can only fakesign TMDs, Tickets, and WADs!");
|
||||
bail!("You can only fakesign TMDs, Tickets, and WADs!");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -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.", input);
|
||||
bail!("Source WAD \"{}\" could not be found.", in_path.display());
|
||||
}
|
||||
// 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", 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"),
|
||||
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())),
|
||||
}
|
||||
};
|
||||
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.")?;
|
||||
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.")?;
|
||||
// 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,18 +119,21 @@ 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;
|
||||
@ -140,56 +143,56 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pack_wad(input: &str, output: &str) {
|
||||
pub fn pack_wad(input: &str, output: &str) -> Result<()> {
|
||||
let in_path = Path::new(input);
|
||||
if !in_path.exists() {
|
||||
panic!("Error: Source directory does not exist.");
|
||||
bail!("Source directory \"{}\" does not exist.", in_path.display());
|
||||
}
|
||||
// Read TMD file (only accept one file).
|
||||
let tmd_files: Vec<PathBuf> = glob(&format!("{}/*.tmd", in_path.display()))
|
||||
.expect("failed to read glob pattern")
|
||||
let tmd_files: Vec<PathBuf> = glob(&format!("{}/*.tmd", in_path.display()))?
|
||||
.filter_map(|f| f.ok()).collect();
|
||||
if tmd_files.is_empty() {
|
||||
panic!("Error: No TMD file found in the source directory.");
|
||||
bail!("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.")
|
||||
bail!("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();
|
||||
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.")?;
|
||||
// Read Ticket file (only accept one file).
|
||||
let ticket_files: Vec<PathBuf> = glob(&format!("{}/*.tik", in_path.display()))
|
||||
.expect("failed to read glob pattern")
|
||||
let ticket_files: Vec<PathBuf> = glob(&format!("{}/*.tik", in_path.display()))?
|
||||
.filter_map(|f| f.ok()).collect();
|
||||
if ticket_files.is_empty() {
|
||||
panic!("Error: No Ticket file found in the source directory.");
|
||||
bail!("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.")
|
||||
bail!("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();
|
||||
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.")?;
|
||||
// Read cert chain (only accept one file).
|
||||
let cert_files: Vec<PathBuf> = glob(&format!("{}/*.cert", in_path.display()))
|
||||
.expect("failed to read glob pattern")
|
||||
let cert_files: Vec<PathBuf> = glob(&format!("{}/*.cert", in_path.display()))?
|
||||
.filter_map(|f| f.ok()).collect();
|
||||
if cert_files.is_empty() {
|
||||
panic!("Error: No cert file found in the source directory.");
|
||||
bail!("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.")
|
||||
bail!("More than one Cert file found in the source directory.");
|
||||
}
|
||||
let cert_chain = cert::CertificateChain::from_bytes(&fs::read(&cert_files[0]).expect("could not read cert chain file")).unwrap();
|
||||
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.")?;
|
||||
// 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")
|
||||
let footer_files: Vec<PathBuf> = glob(&format!("{}/*.footer", in_path.display()))?
|
||||
.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();
|
||||
footer = fs::read(&footer_files[0]).with_context(|| "Could not open footer file for reading.")?;
|
||||
}
|
||||
// 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");
|
||||
let mut content_region = content::ContentRegion::new(tmd.content_records.clone())?;
|
||||
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 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 wad = wad::WAD::from_parts(&cert_chain, &[], &tik, &tmd, &content_region, &footer).expect("failed to create WAD");
|
||||
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.")?;
|
||||
// Write out WAD file.
|
||||
let mut out_path = PathBuf::from(output);
|
||||
match out_path.extension() {
|
||||
@ -202,33 +205,39 @@ pub fn pack_wad(input: &str, output: &str) {
|
||||
out_path.set_extension("wad");
|
||||
}
|
||||
}
|
||||
fs::write(out_path, wad.to_bytes().unwrap()).expect("could not write to wad file");
|
||||
fs::write(out_path.clone(), wad.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
|
||||
println!("WAD file packed!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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();
|
||||
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()))?;
|
||||
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);
|
||||
if !out_path.exists() {
|
||||
fs::create_dir(out_path).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?;
|
||||
}
|
||||
// Write out all WAD components.
|
||||
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");
|
||||
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))?;
|
||||
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");
|
||||
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))?;
|
||||
let cert_file_name = format!("{}.cert", tid);
|
||||
fs::write(Path::join(out_path, cert_file_name), title.cert_chain.to_bytes().unwrap()).expect("could not write Cert file");
|
||||
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))?;
|
||||
let meta_file_name = format!("{}.footer", tid);
|
||||
fs::write(Path::join(out_path, meta_file_name), title.meta()).expect("could not write footer file");
|
||||
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))?;
|
||||
// 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).unwrap();
|
||||
fs::write(Path::join(out_path, content_file_name), dec_content).unwrap();
|
||||
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))?;
|
||||
}
|
||||
println!("WAD file unpacked!");
|
||||
Ok(())
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user