mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2026-03-03 03:15:28 -05:00
Refactored entire way that title content is handled
The ContentRegion has been entirely dissolved. Its fields were not particularly useful, as the content records were just a duplicate from the TMD, the file data itself, and then two integers that were assigned during construction and then literally never referenced. Instead, the only copy of the content records now lives in the TMD, and the content is stored within the title directly since that was the only meaningful field. All the content related methods were moved from the ContentRegion struct over to the Title struct, since the content just lives there now. This should hopefully make things much easier to deal with as you no longer need to worry about keeping two separate copies of the content records in sync. This also might all change again in the future idk
This commit is contained in:
@@ -16,34 +16,34 @@ fn main() {
|
||||
let patch_count = title::iospatcher::ios_patch_sigchecks(&mut title, index).unwrap();
|
||||
println!("patches applied: {}", patch_count);
|
||||
|
||||
println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id()));
|
||||
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());
|
||||
println!("num content records: {:?}", title.tmd.content_records().len());
|
||||
println!("first record data: {:?}", title.tmd.content_records().first().unwrap());
|
||||
println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned());
|
||||
println!("num content records: {:?}", title.tmd().content_records().len());
|
||||
println!("first record data: {:?}", title.tmd().content_records().first().unwrap());
|
||||
println!("TMD is fakesigned: {:?}",title.tmd().is_fakesigned());
|
||||
|
||||
println!("title version from ticket is: {:?}", title.ticket.title_version());
|
||||
println!("title key (enc): {:?}", title.ticket.title_key());
|
||||
println!("title key (dec): {:?}", title.ticket.title_key_dec());
|
||||
println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned());
|
||||
println!("title version from ticket is: {:?}", title.ticket().title_version());
|
||||
println!("title key (enc): {:?}", title.ticket().title_key());
|
||||
println!("title key (dec): {:?}", title.ticket().title_key_dec());
|
||||
println!("ticket is fakesigned: {:?}", title.ticket().is_fakesigned());
|
||||
|
||||
println!("title is fakesigned: {:?}", title.is_fakesigned());
|
||||
|
||||
let cert_chain = &title.cert_chain;
|
||||
let cert_chain = &title.cert_chain();
|
||||
println!("cert chain OK");
|
||||
let result = cert::verify_ca_cert(&cert_chain.ca_cert()).unwrap();
|
||||
println!("CA cert {} verified successfully: {}", cert_chain.ca_cert().child_cert_identity(), result);
|
||||
|
||||
let result = cert::verify_child_cert(&cert_chain.ca_cert(), &cert_chain.tmd_cert()).unwrap();
|
||||
println!("TMD cert {} verified successfully: {}", cert_chain.tmd_cert().child_cert_identity(), result);
|
||||
let result = cert::verify_tmd(&cert_chain.tmd_cert(), &title.tmd).unwrap();
|
||||
let result = cert::verify_tmd(&cert_chain.tmd_cert(), title.tmd()).unwrap();
|
||||
println!("TMD verified successfully: {}", result);
|
||||
|
||||
let result = cert::verify_child_cert(&cert_chain.ca_cert(), &cert_chain.ticket_cert()).unwrap();
|
||||
println!("Ticket cert {} verified successfully: {}", cert_chain.ticket_cert().child_cert_identity(), result);
|
||||
let result = cert::verify_ticket(&cert_chain.ticket_cert(), &title.ticket).unwrap();
|
||||
let result = cert::verify_ticket(&cert_chain.ticket_cert(), title.ticket()).unwrap();
|
||||
println!("Ticket verified successfully: {}", result);
|
||||
|
||||
let result = title.verify().unwrap();
|
||||
|
||||
@@ -39,7 +39,7 @@ fn print_title_version(title_version: u16, title_id: [u8; 8], is_vwii: bool) ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()> {
|
||||
fn print_tmd_info(tmd: &tmd::TMD, cert: Option<cert::Certificate>) -> Result<()> {
|
||||
// Print all important keys from the TMD.
|
||||
println!("Title Info");
|
||||
print_tid(tmd.title_id())?;
|
||||
@@ -91,7 +91,7 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()>
|
||||
println!(" DVD Video Access: {}", tmd.check_access_right(tmd::AccessRight::DVDVideo));
|
||||
println!(" AHB Access: {}", tmd.check_access_right(tmd::AccessRight::AHB));
|
||||
if let Some(cert) = cert {
|
||||
let signing_str = match cert::verify_tmd(&cert, &tmd) {
|
||||
let signing_str = match cert::verify_tmd(&cert, tmd) {
|
||||
Ok(result) => match result {
|
||||
true => "Valid (Unmodified TMD)",
|
||||
false => {
|
||||
@@ -128,7 +128,7 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) -> Result<()> {
|
||||
fn print_ticket_info(ticket: &ticket::Ticket, cert: Option<cert::Certificate>) -> Result<()> {
|
||||
// Print all important keys from the Ticket.
|
||||
println!("Ticket Info");
|
||||
print_tid(ticket.title_id())?;
|
||||
@@ -160,7 +160,7 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) ->
|
||||
println!(" Title Key (Encrypted): {}", hex::encode(ticket.title_key()));
|
||||
println!(" Title Key (Decrypted): {}", hex::encode(ticket.title_key_dec()));
|
||||
if let Some(cert) = cert {
|
||||
let signing_str = match cert::verify_ticket(&cert, &ticket) {
|
||||
let signing_str = match cert::verify_ticket(&cert, ticket) {
|
||||
Ok(result) => match result {
|
||||
true => "Valid (Unmodified Ticket)",
|
||||
false => {
|
||||
@@ -216,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())? {
|
||||
"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())? {
|
||||
"Edited (Modified TMD, Unmodified Ticket)"
|
||||
} else {
|
||||
"Illegitimate (Modified TMD + Ticket)"
|
||||
@@ -235,9 +235,9 @@ 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()))?;
|
||||
print_tmd_info(title.tmd(), Some(title.cert_chain().tmd_cert()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -278,11 +278,11 @@ pub fn info(input: &str) -> Result<()> {
|
||||
match identify_file_type(input) {
|
||||
Some(WiiFileType::Tmd) => {
|
||||
let tmd = tmd::TMD::from_bytes(&fs::read(in_path)?).with_context(|| "The provided TMD file could not be parsed, and is likely invalid.")?;
|
||||
print_tmd_info(tmd, None)?;
|
||||
print_tmd_info(&tmd, None)?;
|
||||
},
|
||||
Some(WiiFileType::Ticket) => {
|
||||
let ticket = ticket::Ticket::from_bytes(&fs::read(in_path)?).with_context(|| "The provided Ticket file could not be parsed, and is likely invalid.")?;
|
||||
print_ticket_info(ticket, None)?;
|
||||
print_ticket_info(&ticket, None)?;
|
||||
},
|
||||
Some(WiiFileType::Wad) => {
|
||||
let wad = wad::WAD::from_bytes(&fs::read(in_path)?).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
|
||||
|
||||
@@ -290,7 +290,7 @@ pub fn install_missing(emunand: &str, vwii: &bool) -> Result<()> {
|
||||
for ios in missing_tids {
|
||||
println!("Downloading IOS{} ({})...", u32::from_str_radix(&hex::encode(&ios[4..8]), 16)?, hex::encode(ios).to_ascii_uppercase());
|
||||
let title = nus::download_title(ios, None, true)?;
|
||||
let version = title.tmd.title_version();
|
||||
let version = title.tmd().title_version();
|
||||
println!(" Installing IOS{} ({}) v{}...", u32::from_str_radix(&hex::encode(&ios[4..8]), 16)?, hex::encode(ios).to_ascii_uppercase(), version);
|
||||
emunand.install_title(title, false)?;
|
||||
println!(" Installed IOS{} ({}) v{}!", u32::from_str_radix(&hex::encode(&ios[4..8]), 16)?, hex::encode(ios).to_ascii_uppercase(), version);
|
||||
@@ -325,7 +325,7 @@ pub fn uninstall_title(tid: &str, emunand: &str, remove_ticket: &bool) -> Result
|
||||
let tid_bin: [u8; 8] = if tid_as_path.exists() {
|
||||
let wad_file = fs::read(tid_as_path).with_context(|| format!("Failed to open WAD file \"{}\" for reading.", tid_as_path.display()))?;
|
||||
let title = title::Title::from_bytes(&wad_file).with_context(|| format!("The provided WAD file \"{}\" appears to be invalid.", tid_as_path.display()))?;
|
||||
title.tmd.title_id()
|
||||
title.tmd().title_id()
|
||||
} else {
|
||||
hex::decode(tid).with_context(|| "The specified Title ID is not valid! The Title ID must be in hex format.")?.try_into().unwrap()
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ use anyhow::{bail, Context, Result};
|
||||
use clap::Args;
|
||||
use rustwii::title;
|
||||
use rustwii::title::iospatcher;
|
||||
use rustwii::title::tmd::ContentType;
|
||||
|
||||
#[derive(Args)]
|
||||
#[clap(next_help_heading = "Patches")]
|
||||
@@ -51,7 +52,7 @@ pub fn patch_ios(
|
||||
};
|
||||
|
||||
let mut ios = title::Title::from_bytes(&fs::read(in_path)?).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
|
||||
let tid = hex::encode(ios.tmd.title_id());
|
||||
let tid = hex::encode(ios.tmd().title_id());
|
||||
|
||||
// If the TID is not a valid IOS TID, then bail.
|
||||
if !tid[..8].eq("00000001") || tid[8..].eq("00000001") || tid[8..].eq("00000002") {
|
||||
@@ -102,6 +103,12 @@ pub fn patch_ios(
|
||||
println!("{} patch(es) applied", count);
|
||||
patches_applied += count;
|
||||
}
|
||||
|
||||
// Set the type of the content containing ES to "Normal" to avoid it getting installed to
|
||||
// /shared1 on NAND.
|
||||
if *no_shared {
|
||||
set_type_normal(&mut ios, es_index)?;
|
||||
}
|
||||
}
|
||||
|
||||
if enabled_patches.drive_inquiry {
|
||||
@@ -112,6 +119,10 @@ pub fn patch_ios(
|
||||
let count = iospatcher::ios_patch_drive_inquiry(&mut ios, dip_index)?;
|
||||
println!("{} patch(es) applied", count);
|
||||
patches_applied += count;
|
||||
|
||||
if *no_shared {
|
||||
set_type_normal(&mut ios, dip_index)?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nTotal patches applied: {patches_applied}");
|
||||
@@ -127,3 +138,13 @@ pub fn patch_ios(
|
||||
println!("IOS successfully patched!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_type_normal(ios: &mut title::Title, index: usize) -> Result<()> {
|
||||
let mut content_records = ios.tmd().content_records().clone();
|
||||
content_records[index].content_type = ContentType::Normal;
|
||||
let mut tmd = ios.tmd().clone();
|
||||
tmd.set_content_records(content_records);
|
||||
ios.set_tmd(tmd);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::path::PathBuf;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Subcommand, Args};
|
||||
use sha1::{Sha1, Digest};
|
||||
use rustwii::title::{cert, content, crypto, nus, ticket, tmd};
|
||||
use rustwii::title::{cert, crypto, nus, ticket, tmd};
|
||||
use rustwii::title;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -159,15 +159,15 @@ fn download_title_dir(title: title::Title, output: String) -> Result<()> {
|
||||
} else {
|
||||
fs::create_dir(&out_path).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?;
|
||||
}
|
||||
let tid = hex::encode(title.tmd.title_id());
|
||||
let tid = hex::encode(title.tmd().title_id());
|
||||
println!(" - Saving TMD...");
|
||||
fs::write(out_path.join(format!("{}.tmd", &tid)), title.tmd.to_bytes()?).with_context(|| format!("Failed to open TMD file \"{}.tmd\" for writing.", tid))?;
|
||||
fs::write(out_path.join(format!("{}.tmd", &tid)), title.tmd().to_bytes()?).with_context(|| format!("Failed to open TMD file \"{}.tmd\" for writing.", tid))?;
|
||||
println!(" - Saving Ticket...");
|
||||
fs::write(out_path.join(format!("{}.tik", &tid)), title.ticket.to_bytes()?).with_context(|| format!("Failed to open Ticket file \"{}.tmd\" for writing.", tid))?;
|
||||
fs::write(out_path.join(format!("{}.tik", &tid)), title.ticket().to_bytes()?).with_context(|| format!("Failed to open Ticket file \"{}.tmd\" for writing.", tid))?;
|
||||
println!(" - Saving certificate chain...");
|
||||
fs::write(out_path.join(format!("{}.cert", &tid)), title.cert_chain.to_bytes()?).with_context(|| format!("Failed to open certificate chain file \"{}.cert\" for writing.", tid))?;
|
||||
fs::write(out_path.join(format!("{}.cert", &tid)), title.cert_chain().to_bytes()?).with_context(|| format!("Failed to open certificate chain file \"{}.cert\" for writing.", tid))?;
|
||||
// Iterate over the content files and write them out in encrypted form.
|
||||
for record in title.content.content_records().iter() {
|
||||
for record in title.tmd().content_records().iter() {
|
||||
println!(" - Decrypting and saving content with Content ID {}...", record.content_id);
|
||||
fs::write(out_path.join(format!("{:08X}.app", record.content_id)), title.get_content_by_cid(record.content_id)?)
|
||||
.with_context(|| format!("Failed to open content file \"{:08X}.app\" for writing.", record.content_id))?;
|
||||
@@ -176,7 +176,7 @@ fn download_title_dir(title: title::Title, output: String) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_title_dir_enc(tmd: tmd::TMD, content_region: content::ContentRegion, cert_chain: cert::CertificateChain, output: String) -> Result<()> {
|
||||
fn download_title_dir_enc(tmd: tmd::TMD, contents: Vec<Vec<u8>>, cert_chain: cert::CertificateChain, output: String) -> Result<()> {
|
||||
println!(" - Saving downloaded data...");
|
||||
let out_path = PathBuf::from(output);
|
||||
if out_path.exists() {
|
||||
@@ -192,9 +192,10 @@ fn download_title_dir_enc(tmd: tmd::TMD, content_region: content::ContentRegion,
|
||||
println!(" - Saving certificate chain...");
|
||||
fs::write(out_path.join(format!("{}.cert", &tid)), cert_chain.to_bytes()?).with_context(|| format!("Failed to open certificate chain file \"{}.cert\" for writing.", tid))?;
|
||||
// Iterate over the content files and write them out in encrypted form.
|
||||
for record in content_region.content_records().iter() {
|
||||
for record in tmd.content_records().iter() {
|
||||
println!(" - Saving content with Content ID {}...", record.content_id);
|
||||
fs::write(out_path.join(format!("{:08X}", record.content_id)), content_region.get_enc_content_by_cid(record.content_id)?)
|
||||
let idx = tmd.get_index_from_cid(record.content_id)?;
|
||||
fs::write(out_path.join(format!("{:08X}", record.content_id)), &contents[idx])
|
||||
.with_context(|| format!("Failed to open content file \"{:08X}\" for writing.", record.content_id))?;
|
||||
}
|
||||
println!("Successfully downloaded title with Title ID {} to directory \"{}\"!", tid, out_path.display());
|
||||
@@ -205,7 +206,7 @@ fn download_title_wad(title: title::Title, output: String) -> Result<()> {
|
||||
println!(" - Packing WAD...");
|
||||
let out_path = PathBuf::from(output).with_extension("wad");
|
||||
fs::write(&out_path, title.to_wad().with_context(|| "A WAD could not be packed.")?.to_bytes()?).with_context(|| format!("Could not open WAD file \"{}\" for writing.", out_path.display()))?;
|
||||
println!("Successfully downloaded title with Title ID {} to WAD file \"{}\"!", hex::encode(title.tmd.title_id()), out_path.display());
|
||||
println!("Successfully downloaded title with Title ID {} to WAD file \"{}\"!", hex::encode(title.tmd().title_id()), out_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -242,12 +243,11 @@ pub fn download_title(tid: &str, version: &Option<u16>, output: &TitleOutputType
|
||||
contents.push(nus::download_content(tid, record.content_id, true).with_context(|| format!("Content with Content ID {} could not be downloaded.", record.content_id))?);
|
||||
println!(" - Done!");
|
||||
}
|
||||
let content_region = content::ContentRegion::from_contents(contents, tmd.content_records().clone())?;
|
||||
println!(" - Building certificate chain...");
|
||||
let cert_chain = cert::CertificateChain::from_bytes(&nus::download_cert_chain(true).with_context(|| "Certificate chain could not be built.")?)?;
|
||||
if let Some(tik) = tik {
|
||||
// If we have a Ticket, then build a Title and jump to the output method.
|
||||
let title = title::Title::from_parts(cert_chain, None, tik, tmd, content_region, None)?;
|
||||
let title = title::Title::from_parts_with_content(cert_chain, None, tik, tmd, contents, None)?;
|
||||
if output.wad.is_some() {
|
||||
download_title_wad(title, output.wad.clone().unwrap())?;
|
||||
} else {
|
||||
@@ -256,7 +256,7 @@ pub fn download_title(tid: &str, version: &Option<u16>, output: &TitleOutputType
|
||||
} else {
|
||||
// If we're downloading to a directory and have no Ticket, save the TMD and encrypted
|
||||
// contents to the directory only.
|
||||
download_title_dir_enc(tmd, content_region, cert_chain, output.output.clone().unwrap())?;
|
||||
download_title_dir_enc(tmd, contents, cert_chain, output.output.clone().unwrap())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ pub fn tmd_remove(input: &str, output: &Option<String>, identifier: &ContentIden
|
||||
if let Some(index) = identifier.index {
|
||||
let mut content_records = tmd.content_records().clone();
|
||||
content_records.remove(index);
|
||||
tmd.set_content_records(&content_records);
|
||||
tmd.set_content_records(content_records);
|
||||
tmd.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified TMD.")?;
|
||||
fs::write(&out_path, tmd.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||
println!("Successfully removed content at index {} in TMD file \"{}\".", index, out_path.display());
|
||||
@@ -121,7 +121,7 @@ pub fn tmd_remove(input: &str, output: &Option<String>, identifier: &ContentIden
|
||||
};
|
||||
let mut content_records = tmd.content_records().clone();
|
||||
content_records.remove(index as usize);
|
||||
tmd.set_content_records(&content_records);
|
||||
tmd.set_content_records(content_records);
|
||||
tmd.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified TMD.")?;
|
||||
fs::write(&out_path, tmd.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||
println!("Successfully removed content with Content ID \"{}\" ({}) in WAD file \"{}\".", identifier.cid.clone().unwrap(), cid, out_path.display());
|
||||
|
||||
@@ -10,7 +10,7 @@ use clap::{Subcommand, Args};
|
||||
use glob::glob;
|
||||
use hex::FromHex;
|
||||
use rand::prelude::*;
|
||||
use rustwii::title::{cert, crypto, tmd, ticket, content, wad};
|
||||
use rustwii::title::{cert, crypto, tmd, ticket};
|
||||
use rustwii::title;
|
||||
use crate::title::shared::{validate_target_ios, validate_target_tid, validate_target_type, ContentIdentifier, TitleModifications};
|
||||
|
||||
@@ -156,7 +156,7 @@ pub fn wad_add(input: &str, content: &str, output: &Option<String>, cid: &Option
|
||||
};
|
||||
let target_cid = if cid.is_some() {
|
||||
let cid = u32::from_str_radix(cid.clone().unwrap().as_str(), 16).with_context(|| "The specified Content ID is invalid!")?;
|
||||
if title.content.content_records().iter().any(|record| record.content_id == cid) {
|
||||
if title.tmd().content_records().iter().any(|record| record.content_id == cid) {
|
||||
bail!("The specified Content ID \"{:08X}\" is already being used in this WAD!", cid);
|
||||
}
|
||||
cid
|
||||
@@ -166,7 +166,7 @@ pub fn wad_add(input: &str, content: &str, output: &Option<String>, cid: &Option
|
||||
let mut cid: u32;
|
||||
loop {
|
||||
cid = rng.random_range(0..=0xFF);
|
||||
if !title.content.content_records().iter().any(|record| record.content_id == cid) {
|
||||
if !title.tmd().content_records().iter().any(|record| record.content_id == cid) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -205,47 +205,51 @@ pub fn wad_convert(input: &str, target: &ConvertTargets, output: &Option<String>
|
||||
};
|
||||
let mut title = title::Title::from_bytes(&fs::read(in_path)?).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() {
|
||||
if matches!(target, Target::Dev) && title.ticket().is_dev() {
|
||||
bail!("This is already a development WAD!");
|
||||
} else if matches!(target, Target::Retail) && !title.ticket.is_dev() && !title.tmd.is_vwii() {
|
||||
} else if matches!(target, Target::Retail) && !title.ticket().is_dev() && !title.tmd().is_vwii() {
|
||||
bail!("This is already a retail WAD!");
|
||||
} else if matches!(target, Target::Vwii) && !title.ticket.is_dev() && title.tmd.is_vwii() {
|
||||
} else if matches!(target, Target::Vwii) && !title.ticket().is_dev() && title.tmd().is_vwii() {
|
||||
bail!("This is already a vWii WAD!");
|
||||
}
|
||||
// Save the current encryption to display at the end.
|
||||
let source = if title.ticket.is_dev() {
|
||||
let source = if title.ticket().is_dev() {
|
||||
"development"
|
||||
} else if title.tmd.is_vwii() {
|
||||
} else if title.tmd().is_vwii() {
|
||||
"vWii"
|
||||
} else {
|
||||
"retail"
|
||||
};
|
||||
let title_key = title.ticket.title_key_dec();
|
||||
let title_key = title.ticket().title_key_dec();
|
||||
let title_key_new: [u8; 16];
|
||||
let mut tmd = title.tmd().clone();
|
||||
let mut ticket = title.ticket().clone();
|
||||
match target {
|
||||
Target::Dev => {
|
||||
title.tmd.set_signature_issuer(String::from("Root-CA00000002-CP00000007"))?;
|
||||
title.ticket.set_signature_issuer(String::from("Root-CA00000002-XS00000006"))?;
|
||||
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id(), true);
|
||||
title.ticket.set_common_key_index(0);
|
||||
title.tmd.set_is_vwii(false);
|
||||
tmd.set_signature_issuer(String::from("Root-CA00000002-CP00000007"))?;
|
||||
ticket.set_signature_issuer(String::from("Root-CA00000002-XS00000006"))?;
|
||||
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket().title_id(), true);
|
||||
ticket.set_common_key_index(0);
|
||||
tmd.set_is_vwii(false);
|
||||
},
|
||||
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(), false);
|
||||
title.ticket.set_common_key_index(0);
|
||||
title.tmd.set_is_vwii(false);
|
||||
tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
|
||||
ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?;
|
||||
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket().title_id(), false);
|
||||
ticket.set_common_key_index(0);
|
||||
tmd.set_is_vwii(false);
|
||||
},
|
||||
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(), false);
|
||||
title.ticket.set_common_key_index(2);
|
||||
title.tmd.set_is_vwii(true);
|
||||
tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
|
||||
ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?;
|
||||
title_key_new = crypto::encrypt_title_key(title_key, 2, title.ticket().title_id(), false);
|
||||
ticket.set_common_key_index(2);
|
||||
tmd.set_is_vwii(true);
|
||||
}
|
||||
}
|
||||
title.ticket.set_title_key(title_key_new);
|
||||
ticket.set_title_key(title_key_new);
|
||||
title.set_tmd(tmd);
|
||||
title.set_ticket(ticket);
|
||||
title.fakesign()?;
|
||||
fs::write(&out_path, title.to_wad()?.to_bytes()?)?;
|
||||
println!("Successfully converted {} WAD to {} WAD \"{}\"!", source, target, out_path.file_name().unwrap().to_str().unwrap());
|
||||
@@ -271,18 +275,18 @@ pub fn wad_edit(input: &str, output: &Option<String>, edits: &TitleModifications
|
||||
if edits.tid.is_some() || edits.r#type.is_some() {
|
||||
let tid_high = if let Some(new_type) = &edits.r#type {
|
||||
let new_type = validate_target_type(&new_type.to_ascii_lowercase())?;
|
||||
changes_summary.push(format!("Changed title type from \"{}\" to \"{}\"", title.tmd.title_type()?, new_type));
|
||||
changes_summary.push(format!("Changed title type from \"{}\" to \"{}\"", title.tmd().title_type()?, new_type));
|
||||
Vec::from_hex(format!("{:08X}", new_type as u32))?
|
||||
} else {
|
||||
title.tmd.title_id()[0..4].to_vec()
|
||||
title.tmd().title_id()[0..4].to_vec()
|
||||
};
|
||||
|
||||
let tid_low = if let Some(new_tid) = &edits.tid {
|
||||
let new_tid = validate_target_tid(&new_tid.to_ascii_uppercase())?;
|
||||
changes_summary.push(format!("Changed Title ID from \"{}\" to \"{}\"", hex::encode(&title.tmd.title_id()[4..8]).to_ascii_uppercase(), hex::encode(&new_tid).to_ascii_uppercase()));
|
||||
changes_summary.push(format!("Changed Title ID from \"{}\" to \"{}\"", hex::encode(&title.tmd().title_id()[4..8]).to_ascii_uppercase(), hex::encode(&new_tid).to_ascii_uppercase()));
|
||||
new_tid
|
||||
} else {
|
||||
title.tmd.title_id()[4..8].to_vec()
|
||||
title.tmd().title_id()[4..8].to_vec()
|
||||
};
|
||||
|
||||
let new_tid: Vec<u8> = tid_high.iter().chain(&tid_low).copied().collect();
|
||||
@@ -291,8 +295,10 @@ pub fn wad_edit(input: &str, output: &Option<String>, edits: &TitleModifications
|
||||
|
||||
if let Some(new_ios) = edits.ios {
|
||||
let new_ios_tid = validate_target_ios(new_ios)?;
|
||||
title.tmd.set_ios_tid(new_ios_tid)?;
|
||||
changes_summary.push(format!("Changed required IOS from IOS{} to IOS{}", title.tmd.ios_tid().last().unwrap(), new_ios));
|
||||
changes_summary.push(format!("Changed required IOS from IOS{} to IOS{}", title.tmd().ios_tid().last().unwrap(), new_ios));
|
||||
let mut tmd = title.tmd().clone();
|
||||
tmd.set_ios_tid(new_ios_tid)?;
|
||||
title.set_tmd(tmd);
|
||||
}
|
||||
|
||||
title.fakesign()?;
|
||||
@@ -317,7 +323,7 @@ pub fn wad_pack(input: &str, output: &str) -> Result<()> {
|
||||
} else if tmd_files.len() > 1 {
|
||||
bail!("More than one TMD file found in the source directory.");
|
||||
}
|
||||
let mut tmd = tmd::TMD::from_bytes(&fs::read(&tmd_files[0]).with_context(|| "Could not open TMD file for reading.")?)
|
||||
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()))?
|
||||
@@ -346,17 +352,25 @@ pub fn wad_pack(input: &str, output: &str) -> Result<()> {
|
||||
if footer_files.len() == 1 {
|
||||
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())?;
|
||||
let content_indexes: Vec<u16> = tmd.content_records().iter().map(|record| record.index).collect();
|
||||
|
||||
// Create a title to use for content loading.
|
||||
let mut title = title::Title::from_parts(
|
||||
cert_chain,
|
||||
None,
|
||||
tik,
|
||||
tmd,
|
||||
Some(&footer)
|
||||
)?;
|
||||
|
||||
// Iterate over expected content and load the content into the title.
|
||||
let content_indexes: Vec<u16> = title.tmd().content_records().iter().map(|record| record.index).collect();
|
||||
for index in content_indexes {
|
||||
let data = fs::read(format!("{}/{:08X}.app", in_path.display(), index)).with_context(|| format!("Could not open content file \"{:08X}.app\" for reading.", index))?;
|
||||
content_region.set_content(&data, index as usize, None, None, tik.title_key_dec())
|
||||
let data = fs::read(format!("{}/{:08X}.app", in_path.display(), index))
|
||||
.with_context(|| format!("Could not open content file \"{:08X}.app\" for reading.", index))?;
|
||||
title.set_content(&data, index as usize, None, None)
|
||||
.with_context(|| "Failed to load content into the ContentRegion.")?;
|
||||
}
|
||||
// Ensure that the TMD is modified with our potentially updated content records.
|
||||
tmd.set_content_records(content_region.content_records());
|
||||
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 = title.to_wad()?;
|
||||
// Write out WAD file.
|
||||
let mut out_path = PathBuf::from(output);
|
||||
match out_path.extension() {
|
||||
@@ -388,19 +402,17 @@ pub fn wad_remove(input: &str, output: &Option<String>, identifier: &ContentIden
|
||||
// Parse the identifier passed to choose how to find and remove the target.
|
||||
// ...maybe don't take the above comment out of context
|
||||
if let Some(index) = identifier.index {
|
||||
title.content.remove_content(index).with_context(|| "The specified index does not exist in the provided WAD!")?;
|
||||
println!("{:?}", title.tmd);
|
||||
title.remove_content(index).with_context(|| "The specified index does not exist in the provided WAD!")?;
|
||||
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified WAD.")?;
|
||||
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||
println!("Successfully removed content at index {} in WAD file \"{}\".", index, out_path.display());
|
||||
} else if identifier.cid.is_some() {
|
||||
let cid = u32::from_str_radix(identifier.cid.clone().unwrap().as_str(), 16).with_context(|| "The specified Content ID is invalid!")?;
|
||||
let index = match title.content.get_index_from_cid(cid) {
|
||||
let index = match title.tmd().get_index_from_cid(cid) {
|
||||
Ok(index) => index,
|
||||
Err(_) => bail!("The specified Content ID \"{}\" ({}) does not exist in this WAD!", identifier.cid.clone().unwrap(), cid),
|
||||
};
|
||||
title.content.remove_content(index).with_context(|| "An unknown error occurred while removing content from the WAD.")?;
|
||||
println!("{:?}", title.tmd);
|
||||
title.remove_content(index).with_context(|| "An unknown error occurred while removing content from the WAD.")?;
|
||||
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified WAD.")?;
|
||||
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||
println!("Successfully removed content with Content ID \"{}\" ({}) in WAD file \"{}\".", identifier.cid.clone().unwrap(), cid, out_path.display());
|
||||
@@ -437,7 +449,7 @@ pub fn wad_set(input: &str, content: &str, output: &Option<String>, identifier:
|
||||
// Parse the identifier passed to choose how to do the find and replace.
|
||||
if let Some(index) = identifier.index {
|
||||
match title.set_content(&new_content, index, None, target_type) {
|
||||
Err(title::TitleError::Content(content::ContentError::IndexOutOfRange { index, max })) => {
|
||||
Err(title::TitleError::IndexOutOfRange { index, max }) => {
|
||||
bail!("The specified index {} does not exist in this WAD! The maximum index is {}.", index, max)
|
||||
},
|
||||
Err(e) => bail!("An unknown error occurred while setting the new content: {e}"),
|
||||
@@ -448,7 +460,7 @@ pub fn wad_set(input: &str, content: &str, output: &Option<String>, identifier:
|
||||
println!("Successfully replaced content at index {} in WAD file \"{}\".", identifier.index.unwrap(), out_path.display());
|
||||
} else if identifier.cid.is_some() {
|
||||
let cid = u32::from_str_radix(identifier.cid.clone().unwrap().as_str(), 16).with_context(|| "The specified Content ID is invalid!")?;
|
||||
let index = match title.content.get_index_from_cid(cid) {
|
||||
let index = match title.tmd().get_index_from_cid(cid) {
|
||||
Ok(index) => index,
|
||||
Err(_) => bail!("The specified Content ID \"{}\" ({}) does not exist in this WAD!", identifier.cid.clone().unwrap(), cid),
|
||||
};
|
||||
@@ -467,7 +479,7 @@ pub fn wad_unpack(input: &str, output: &str) -> Result<()> {
|
||||
}
|
||||
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());
|
||||
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() {
|
||||
@@ -475,18 +487,18 @@ pub fn wad_unpack(input: &str, output: &str) -> Result<()> {
|
||||
}
|
||||
// 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.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.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.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.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.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.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.content_records().len() {
|
||||
let content_file_name = format!("{:08X}.app", title.content.content_records()[i].index);
|
||||
let dec_content = title.get_content_by_index(i).with_context(|| format!("Failed to unpack content with Content ID {:08X}.", title.content.content_records()[i].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].content_id))?;
|
||||
for i in 0..title.tmd().content_records().len() {
|
||||
let content_file_name = format!("{:08X}.app", title.tmd().content_records()[i].index);
|
||||
let dec_content = title.get_content_by_index(i).with_context(|| format!("Failed to unpack content with Content ID {:08X}.", title.tmd().content_records()[i].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.tmd().content_records()[i].content_id))?;
|
||||
}
|
||||
println!("Successfully unpacked WAD file to \"{}\"!", out_path.display());
|
||||
Ok(())
|
||||
|
||||
@@ -8,9 +8,9 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use glob::glob;
|
||||
use thiserror::Error;
|
||||
use crate::nand::sys;
|
||||
use crate::nand::{sharedcontentmap, sys};
|
||||
use crate::title;
|
||||
use crate::title::{cert, content, ticket, tmd};
|
||||
use crate::title::{cert, ticket, tmd};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EmuNANDError {
|
||||
@@ -28,8 +28,10 @@ pub enum EmuNANDError {
|
||||
TMD(#[from] tmd::TMDError),
|
||||
#[error("Ticket processing error")]
|
||||
Ticket(#[from] ticket::TicketError),
|
||||
#[error("content processing error")]
|
||||
Content(#[from] content::ContentError),
|
||||
#[error("Title content processing error")]
|
||||
TitleContent(#[from] title::TitleError),
|
||||
#[error("content.map processing error")]
|
||||
SharedContent(#[from] sharedcontentmap::SharedContentError),
|
||||
#[error("io error occurred during EmuNAND operation")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
@@ -163,12 +165,12 @@ impl EmuNAND {
|
||||
/// actual meta/footer data contained in the title.
|
||||
pub fn install_title(&self, title: title::Title, override_meta: bool) -> Result<(), EmuNANDError> {
|
||||
// Save the two halves of the TID, since those are part of the installation path.
|
||||
let tid_high = hex::encode(&title.tmd.title_id()[0..4]);
|
||||
let tid_low = hex::encode(&title.tmd.title_id()[4..8]);
|
||||
let tid_high = hex::encode(&title.tmd().title_id()[0..4]);
|
||||
let tid_low = hex::encode(&title.tmd().title_id()[4..8]);
|
||||
// Tickets are installed to /ticket/<tid_high>/<tid_low>.tik.
|
||||
let ticket_dir = self.emunand_dirs["ticket"].join(&tid_high);
|
||||
safe_create_dir(&ticket_dir)?;
|
||||
fs::write(ticket_dir.join(format!("{}.tik", &tid_low)), title.ticket.to_bytes()?)?;
|
||||
fs::write(ticket_dir.join(format!("{}.tik", &tid_low)), title.ticket().to_bytes()?)?;
|
||||
// TMDs and normal content (non-shared) are installed to
|
||||
// /title/<tid_high>/<tid_low>/content/, as title.tmd and <cid>.app.
|
||||
let mut title_dir = self.emunand_dirs["title"].join(&tid_high);
|
||||
@@ -183,10 +185,10 @@ impl EmuNAND {
|
||||
fs::remove_dir_all(&title_dir)?;
|
||||
}
|
||||
fs::create_dir(&title_dir)?;
|
||||
fs::write(title_dir.join("title.tmd"), title.tmd.to_bytes()?)?;
|
||||
for i in 0..title.content.content_records().len() {
|
||||
if matches!(title.content.content_records()[i].content_type, tmd::ContentType::Normal) {
|
||||
let content_path = title_dir.join(format!("{:08X}.app", title.content.content_records()[i].content_id).to_ascii_lowercase());
|
||||
fs::write(title_dir.join("title.tmd"), title.tmd().to_bytes()?)?;
|
||||
for i in 0..title.tmd().content_records().len() {
|
||||
if matches!(title.tmd().content_records()[i].content_type, tmd::ContentType::Normal) {
|
||||
let content_path = title_dir.join(format!("{:08X}.app", title.tmd().content_records()[i].content_id).to_ascii_lowercase());
|
||||
fs::write(content_path, title.get_content_by_index(i)?)?;
|
||||
}
|
||||
}
|
||||
@@ -196,13 +198,13 @@ impl EmuNAND {
|
||||
// content is already installed.
|
||||
let content_map_path = self.emunand_dirs["shared1"].join("content.map");
|
||||
let mut content_map = if content_map_path.exists() {
|
||||
content::SharedContentMap::from_bytes(&fs::read(&content_map_path)?)?
|
||||
sharedcontentmap::SharedContentMap::from_bytes(&fs::read(&content_map_path)?)?
|
||||
} else {
|
||||
content::SharedContentMap::new()
|
||||
sharedcontentmap::SharedContentMap::new()
|
||||
};
|
||||
for i in 0..title.content.content_records().len() {
|
||||
if matches!(title.content.content_records()[i].content_type, tmd::ContentType::Shared) {
|
||||
if let Some(file_name) = content_map.add(&title.content.content_records()[i].content_hash)? {
|
||||
for i in 0..title.tmd().content_records().len() {
|
||||
if matches!(title.tmd().content_records()[i].content_type, tmd::ContentType::Shared) {
|
||||
if let Some(file_name) = content_map.add(&title.tmd().content_records()[i].content_hash)? {
|
||||
let content_path = self.emunand_dirs["shared1"].join(format!("{}.app", file_name.to_ascii_lowercase()));
|
||||
fs::write(content_path, title.get_content_by_index(i)?)?;
|
||||
}
|
||||
@@ -232,7 +234,7 @@ impl EmuNAND {
|
||||
} else {
|
||||
sys::UidSys::new()
|
||||
};
|
||||
uid_sys.add(&title.tmd.title_id())?;
|
||||
uid_sys.add(&title.tmd().title_id())?;
|
||||
fs::write(&uid_sys_path, &uid_sys.to_bytes()?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
pub mod emunand;
|
||||
pub mod setting;
|
||||
pub mod sys;
|
||||
pub mod sharedcontentmap;
|
||||
|
||||
104
src/nand/sharedcontentmap.rs
Normal file
104
src/nand/sharedcontentmap.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
// nand/sharedcontentmap.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustwii
|
||||
//
|
||||
// Implements shared content map parsing and editing to update the records of what content is
|
||||
// installed at /shared1/ on NAND.
|
||||
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SharedContentError {
|
||||
#[error("content.map is an invalid length and cannot be parsed")]
|
||||
InvalidSharedContentMapLength,
|
||||
#[error("found invalid shared content name `{0}`")]
|
||||
InvalidSharedContentName(String),
|
||||
#[error("shared content map is not in a valid format")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A structure that represents a shared Content ID/content hash pairing in a content.map file.
|
||||
pub struct ContentMapEntry {
|
||||
pub shared_id: u32,
|
||||
pub hash: [u8; 20],
|
||||
}
|
||||
|
||||
/// A structure that allows for parsing and editing a /shared1/content.map file.
|
||||
pub struct SharedContentMap {
|
||||
pub records: Vec<ContentMapEntry>,
|
||||
}
|
||||
|
||||
impl Default for SharedContentMap {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedContentMap {
|
||||
/// Creates a new SharedContentMap instance from the binary data of a content.map file.
|
||||
pub fn from_bytes(data: &[u8]) -> Result<SharedContentMap, SharedContentError> {
|
||||
// The uid.sys file must be divisible by a multiple of 28, or something is wrong, since each
|
||||
// entry is 28 bytes long.
|
||||
if !data.len().is_multiple_of(28) {
|
||||
return Err(SharedContentError::InvalidSharedContentMapLength);
|
||||
}
|
||||
let record_count = data.len() / 28;
|
||||
let mut buf = Cursor::new(data);
|
||||
let mut records: Vec<ContentMapEntry> = Vec::new();
|
||||
for _ in 0..record_count {
|
||||
// This requires some convoluted parsing, because Nintendo represents the file names as
|
||||
// actual chars and not numbers, despite the fact that the names are always numbers and
|
||||
// using numbers would make incrementing easier. Read the names in as a string, and then
|
||||
// parse that hex string into a u32.
|
||||
let mut shared_id_bytes = [0u8; 8];
|
||||
buf.read_exact(&mut shared_id_bytes)?;
|
||||
let shared_id_str = String::from_utf8_lossy(&shared_id_bytes);
|
||||
let shared_id = match u32::from_str_radix(&shared_id_str, 16) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Err(SharedContentError::InvalidSharedContentName(shared_id_str.to_string())),
|
||||
};
|
||||
let mut hash = [0u8; 20];
|
||||
buf.read_exact(&mut hash)?;
|
||||
records.push(ContentMapEntry { shared_id, hash });
|
||||
}
|
||||
Ok(SharedContentMap { records })
|
||||
}
|
||||
|
||||
/// Creates a new, empty SharedContentMap instance that can then be populated.
|
||||
pub fn new() -> Self {
|
||||
SharedContentMap { records: Vec::new() }
|
||||
}
|
||||
|
||||
/// Dumps the data in a SharedContentMap 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();
|
||||
for record in self.records.iter() {
|
||||
let shared_id = format!("{:08X}", record.shared_id).to_ascii_lowercase();
|
||||
buf.write_all(shared_id.as_bytes())?;
|
||||
buf.write_all(&record.hash)?;
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Adds new shared content to content.map, and assigns it a new file name. The new content
|
||||
/// will only be added if its hash is not already present in the file. Returns None if the
|
||||
/// content hash was already present, or the assigned file name if the hash was just added.
|
||||
pub fn add(&mut self, hash: &[u8; 20]) -> Result<Option<String>, SharedContentError> {
|
||||
// Return None if the hash is already accounted for.
|
||||
if self.records.iter().any(|entry| entry.hash == *hash) {
|
||||
return Ok(None);
|
||||
}
|
||||
// Find the highest index (represented by the file name) and increment it to choose the
|
||||
// name for the new shared content.
|
||||
let max_index = self.records.iter()
|
||||
.max_by_key(|record| record.shared_id)
|
||||
.map(|record| record.shared_id + 1)
|
||||
.unwrap_or(0);
|
||||
self.records.push(ContentMapEntry {
|
||||
shared_id: max_index,
|
||||
hash: *hash,
|
||||
});
|
||||
Ok(Some(format!("{:08X}", max_index)))
|
||||
}
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
// title/content.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustwii
|
||||
//
|
||||
// Implements content parsing and editing.
|
||||
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use sha1::{Sha1, Digest};
|
||||
use thiserror::Error;
|
||||
use crate::title::tmd::{ContentRecord, ContentType};
|
||||
use crate::title::crypto;
|
||||
use crate::title::crypto::encrypt_content;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ContentError {
|
||||
#[error("requested index {index} is out of range (must not exceed {max})")]
|
||||
IndexOutOfRange { index: usize, max: usize },
|
||||
#[error("expected {required} contents based on content records but found {found}")]
|
||||
MissingContents { required: usize, found: usize },
|
||||
#[error("content with requested Content ID {0} could not be found")]
|
||||
CIDNotFound(u32),
|
||||
#[error("the specified index {0} already exists in the content records")]
|
||||
IndexAlreadyExists(u16),
|
||||
#[error("the specified Content ID {0} already exists in the content records")]
|
||||
CIDAlreadyExists(u32),
|
||||
#[error("content's hash did not match the expected value (was {hash}, expected {expected})")]
|
||||
BadHash { hash: String, expected: String },
|
||||
#[error("content.map is an invalid length and cannot be parsed")]
|
||||
InvalidSharedContentMapLength,
|
||||
#[error("found invalid shared content name `{0}`")]
|
||||
InvalidSharedContentName(String),
|
||||
#[error("content data is not in a valid format")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A structure that represents the block of data containing the content of a digital Wii title.
|
||||
pub struct ContentRegion {
|
||||
content_records: Vec<ContentRecord>,
|
||||
content_region_size: u32,
|
||||
content_start_offsets: Vec<u64>,
|
||||
contents: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
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, ContentError> {
|
||||
let content_region_size = data.len() as u32;
|
||||
let num_contents = content_records.len() as u16;
|
||||
// Calculate the starting offsets of each content.
|
||||
let content_start_offsets: Vec<u64> = std::iter::once(0)
|
||||
.chain(content_records.iter().scan(0, |offset, record| {
|
||||
*offset += record.content_size;
|
||||
if record.content_size % 64 != 0 {
|
||||
*offset += 64 - (record.content_size % 64);
|
||||
}
|
||||
Some(*offset)
|
||||
})).take(content_records.len()).collect(); // Trims the extra final entry.
|
||||
// Parse the content blob and create a vector of vectors from it.
|
||||
let mut contents: Vec<Vec<u8>> = Vec::with_capacity(num_contents as usize);
|
||||
let mut buf = Cursor::new(data);
|
||||
for i in 0..num_contents {
|
||||
buf.seek(SeekFrom::Start(content_start_offsets[i as usize]))?;
|
||||
let size = (content_records[i as usize].content_size + 15) & !15;
|
||||
let mut content = vec![0u8; size as usize];
|
||||
buf.read_exact(&mut content)?;
|
||||
contents.push(content);
|
||||
}
|
||||
Ok(ContentRegion {
|
||||
content_records,
|
||||
content_region_size,
|
||||
content_start_offsets,
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a ContentRegion instance that can be used to parse and edit content stored in a
|
||||
/// digital Wii title from a vector of contents and the ContentRecords from a TMD.
|
||||
pub fn from_contents(contents: Vec<Vec<u8>>, content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
|
||||
if contents.len() != content_records.len() {
|
||||
return Err(ContentError::MissingContents { required: content_records.len(), found: contents.len()});
|
||||
}
|
||||
let mut content_region = Self::new(content_records)?;
|
||||
for (index, content) in contents.iter().enumerate() {
|
||||
let target_index = content_region.content_records[index].index;
|
||||
content_region.load_enc_content(content, target_index as usize)?;
|
||||
}
|
||||
Ok(content_region)
|
||||
}
|
||||
|
||||
/// 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![0; num_contents as usize];
|
||||
let contents: Vec<Vec<u8>> = vec![Vec::new(); num_contents as usize];
|
||||
Ok(ContentRegion {
|
||||
content_records,
|
||||
content_region_size,
|
||||
content_start_offsets,
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
/// Dumps the entire ContentRegion 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();
|
||||
for i in 0..self.content_records.len() {
|
||||
let mut content = self.contents[i].clone();
|
||||
// Round up size to nearest 64 to add appropriate padding.
|
||||
content.resize((content.len() + 63) & !63, 0);
|
||||
buf.write_all(&content)?;
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Gets the content records in the ContentRegion.
|
||||
pub fn content_records(&self) -> &Vec<ContentRecord> {
|
||||
&self.content_records
|
||||
}
|
||||
|
||||
/// Gets the size of the ContentRegion.
|
||||
pub fn content_region_size(&self) -> u32 {
|
||||
self.content_region_size
|
||||
}
|
||||
|
||||
/// Gets the start offsets of the content in the ContentRegion.
|
||||
pub fn content_start_offsets(&self) -> &Vec<u64> {
|
||||
&self.content_start_offsets
|
||||
}
|
||||
|
||||
/// Gets the actual data of the content in the ContentRegion.
|
||||
pub fn contents(&self) -> &Vec<Vec<u8>> {
|
||||
&self.contents
|
||||
}
|
||||
|
||||
/// Gets the index of content using its Content ID.
|
||||
pub fn get_index_from_cid(&self, cid: u32) -> Result<usize, ContentError> {
|
||||
// Use fancy Rust find and map methods to find the index matching the provided CID. Take
|
||||
// that libWiiPy!
|
||||
let content_index = self.content_records.iter()
|
||||
.find(|record| record.content_id == cid)
|
||||
.map(|record| record.index);
|
||||
if let Some(index) = content_index {
|
||||
Ok(index as usize)
|
||||
} else {
|
||||
Err(ContentError::CIDNotFound(cid))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the encrypted content file from the ContentRegion at the specified index.
|
||||
pub fn get_enc_content_by_index(&self, index: usize) -> Result<Vec<u8>, ContentError> {
|
||||
let content = self.contents.get(index).ok_or(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 })?;
|
||||
Ok(content.clone())
|
||||
}
|
||||
|
||||
/// Gets the decrypted content file from the ContentRegion at the specified index.
|
||||
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 = 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());
|
||||
let result = hasher.finalize();
|
||||
if result[..] != self.content_records[index].content_hash {
|
||||
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records[index].content_hash) });
|
||||
}
|
||||
Ok(content_dec)
|
||||
}
|
||||
|
||||
/// Gets the encrypted content file from the ContentRegion with the specified Content ID.
|
||||
pub fn get_enc_content_by_cid(&self, cid: u32) -> Result<Vec<u8>, ContentError> {
|
||||
let index = self.content_records.iter().position(|x| x.content_id == cid);
|
||||
if let Some(index) = index {
|
||||
let content = self.get_enc_content_by_index(index).map_err(|_| ContentError::CIDNotFound(cid))?;
|
||||
Ok(content)
|
||||
} else {
|
||||
Err(ContentError::CIDNotFound(cid))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the decrypted content file from the ContentRegion with the specified Content ID.
|
||||
pub fn get_content_by_cid(&self, cid: u32, title_key: [u8; 16]) -> Result<Vec<u8>, ContentError> {
|
||||
let index = self.content_records.iter().position(|x| x.content_id == cid);
|
||||
if let Some(index) = index {
|
||||
let content_dec = self.get_content_by_index(index, title_key)?;
|
||||
Ok(content_dec)
|
||||
} else {
|
||||
Err(ContentError::CIDNotFound(cid))
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads existing content into the specified index of a ContentRegion instance. This content
|
||||
/// must be encrypted.
|
||||
pub fn load_enc_content(&mut self, content: &[u8], index: usize) -> Result<(), ContentError> {
|
||||
if index >= self.content_records.len() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
||||
}
|
||||
self.contents[index] = content.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the content at the specified index to the provided encrypted content. This requires
|
||||
/// the size and hash of the original decrypted content to be known so that the appropriate
|
||||
/// values can be set in the corresponding content record. Optionally, a new Content ID or
|
||||
/// content type can be provided, with the existing values being preserved by default.
|
||||
pub fn set_enc_content(&mut self, content: &[u8], index: usize, content_size: u64, content_hash: [u8; 20], cid: Option<u32>, content_type: Option<ContentType>) -> Result<(), ContentError> {
|
||||
if index >= self.content_records.len() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
||||
}
|
||||
self.content_records[index].content_size = content_size;
|
||||
self.content_records[index].content_hash = content_hash;
|
||||
if let Some(cid) = cid {
|
||||
// Make sure that the new CID isn't already in use.
|
||||
if self.content_records.iter().any(|record| record.content_id == cid) {
|
||||
return Err(ContentError::CIDAlreadyExists(cid));
|
||||
}
|
||||
self.content_records[index].content_id = cid;
|
||||
}
|
||||
if let Some(content_type) = content_type {
|
||||
self.content_records[index].content_type = content_type;
|
||||
}
|
||||
self.contents[index] = content.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads existing content into the specified index of a ContentRegion instance. This content
|
||||
/// must be decrypted and needs to match the size and hash listed in the content record at that
|
||||
/// index.
|
||||
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::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
||||
}
|
||||
// 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 { hash: hex::encode(result), expected: hex::encode(self.content_records[index].content_hash) });
|
||||
}
|
||||
let content_enc = encrypt_content(content, title_key, self.content_records[index].index, self.content_records[index].content_size);
|
||||
self.contents[index] = content_enc;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the content at the specified index to the provided decrypted content. This content will
|
||||
/// have its size and hash saved into the matching record. Optionally, a new Content ID or
|
||||
/// content type can be provided, with the existing values being preserved by default. The
|
||||
/// Title Key will be used to encrypt this content before it is stored.
|
||||
pub fn set_content(&mut self, content: &[u8], index: usize, cid: Option<u32>, content_type: Option<ContentType>, title_key: [u8; 16]) -> Result<(), ContentError> {
|
||||
let content_size = content.len() as u64;
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(content);
|
||||
let content_hash: [u8; 20] = hasher.finalize().into();
|
||||
let content_enc = encrypt_content(content, title_key, index as u16, content_size);
|
||||
self.set_enc_content(&content_enc, index, content_size, content_hash, cid, content_type)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the content at the specified index from the content list and content records. This
|
||||
/// may leave a gap in the indexes recorded in the content records, but this should not cause
|
||||
/// issues on the Wii or with correctly implemented WAD parsers.
|
||||
pub fn remove_content(&mut self, index: usize) -> Result<(), ContentError> {
|
||||
if self.contents.get(index).is_none() || self.content_records.get(index).is_none() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
||||
}
|
||||
self.contents.remove(index);
|
||||
self.content_records.remove(index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds new encrypted content to the end of the content list and content records. The provided
|
||||
/// Content ID, type, index, and decrypted hash will be added to the record.
|
||||
pub fn add_enc_content(&mut self, content: &[u8], index: u16, cid: u32, content_type: ContentType, content_size: u64, content_hash: [u8; 20]) -> Result<(), ContentError> {
|
||||
// Return an error if the specified index or CID already exist in the records.
|
||||
if self.content_records.iter().any(|record| record.index == index) {
|
||||
return Err(ContentError::IndexAlreadyExists(index));
|
||||
}
|
||||
if self.content_records.iter().any(|record| record.content_id == cid) {
|
||||
return Err(ContentError::CIDAlreadyExists(cid));
|
||||
}
|
||||
self.contents.push(content.to_vec());
|
||||
self.content_records.push(ContentRecord { content_id: cid, index, content_type, content_size, content_hash });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds new decrypted content to the end of the content list and content records. The provided
|
||||
/// Content ID and type will be added to the record alongside a hash of the decrypted data. An
|
||||
/// index will be automatically assigned based on the highest index currently recorded in the
|
||||
/// content records.
|
||||
pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: ContentType, title_key: [u8; 16]) -> Result<(), ContentError> {
|
||||
let max_index = self.content_records.iter()
|
||||
.max_by_key(|record| record.index)
|
||||
.map(|record| record.index)
|
||||
.unwrap_or(0); // This should be impossible, but I guess 0 is a safe value just in case?
|
||||
let new_index = max_index + 1;
|
||||
let content_size = content.len() as u64;
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(content);
|
||||
let content_hash: [u8; 20] = hasher.finalize().into();
|
||||
let content_enc = encrypt_content(content, title_key, new_index, content_size);
|
||||
self.add_enc_content(&content_enc, new_index, cid, content_type, content_size, content_hash)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A structure that represents a shared Content ID/content hash pairing in a content.map file.
|
||||
pub struct ContentMapEntry {
|
||||
pub shared_id: u32,
|
||||
pub hash: [u8; 20],
|
||||
}
|
||||
|
||||
/// A structure that allows for parsing and editing a /shared1/content.map file.
|
||||
pub struct SharedContentMap {
|
||||
pub records: Vec<ContentMapEntry>,
|
||||
}
|
||||
|
||||
impl Default for SharedContentMap {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedContentMap {
|
||||
/// Creates a new SharedContentMap instance from the binary data of a content.map file.
|
||||
pub fn from_bytes(data: &[u8]) -> Result<SharedContentMap, ContentError> {
|
||||
// The uid.sys file must be divisible by a multiple of 28, or something is wrong, since each
|
||||
// entry is 28 bytes long.
|
||||
if !data.len().is_multiple_of(28) {
|
||||
return Err(ContentError::InvalidSharedContentMapLength);
|
||||
}
|
||||
let record_count = data.len() / 28;
|
||||
let mut buf = Cursor::new(data);
|
||||
let mut records: Vec<ContentMapEntry> = Vec::new();
|
||||
for _ in 0..record_count {
|
||||
// This requires some convoluted parsing, because Nintendo represents the file names as
|
||||
// actual chars and not numbers, despite the fact that the names are always numbers and
|
||||
// using numbers would make incrementing easier. Read the names in as a string, and then
|
||||
// parse that hex string into a u32.
|
||||
let mut shared_id_bytes = [0u8; 8];
|
||||
buf.read_exact(&mut shared_id_bytes)?;
|
||||
let shared_id_str = String::from_utf8_lossy(&shared_id_bytes);
|
||||
let shared_id = match u32::from_str_radix(&shared_id_str, 16) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Err(ContentError::InvalidSharedContentName(shared_id_str.to_string())),
|
||||
};
|
||||
let mut hash = [0u8; 20];
|
||||
buf.read_exact(&mut hash)?;
|
||||
records.push(ContentMapEntry { shared_id, hash });
|
||||
}
|
||||
Ok(SharedContentMap { records })
|
||||
}
|
||||
|
||||
/// Creates a new, empty SharedContentMap instance that can then be populated.
|
||||
pub fn new() -> Self {
|
||||
SharedContentMap { records: Vec::new() }
|
||||
}
|
||||
|
||||
/// Dumps the data in a SharedContentMap 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();
|
||||
for record in self.records.iter() {
|
||||
let shared_id = format!("{:08X}", record.shared_id).to_ascii_lowercase();
|
||||
buf.write_all(shared_id.as_bytes())?;
|
||||
buf.write_all(&record.hash)?;
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Adds new shared content to content.map, and assigns it a new file name. The new content
|
||||
/// will only be added if its hash is not already present in the file. Returns None if the
|
||||
/// content hash was already present, or the assigned file name if the hash was just added.
|
||||
pub fn add(&mut self, hash: &[u8; 20]) -> Result<Option<String>, ContentError> {
|
||||
// Return None if the hash is already accounted for.
|
||||
if self.records.iter().any(|entry| entry.hash == *hash) {
|
||||
return Ok(None);
|
||||
}
|
||||
// Find the highest index (represented by the file name) and increment it to choose the
|
||||
// name for the new shared content.
|
||||
let max_index = self.records.iter()
|
||||
.max_by_key(|record| record.shared_id)
|
||||
.map(|record| record.shared_id + 1)
|
||||
.unwrap_or(0);
|
||||
self.records.push(ContentMapEntry {
|
||||
shared_id: max_index,
|
||||
hash: *hash,
|
||||
});
|
||||
Ok(Some(format!("{:08X}", max_index)))
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
use std::io::{Cursor, Seek, SeekFrom, Write};
|
||||
use thiserror::Error;
|
||||
use crate::title;
|
||||
use crate::title::content;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IOSPatcherError {
|
||||
@@ -14,8 +13,6 @@ pub enum IOSPatcherError {
|
||||
NotIOS,
|
||||
#[error("the required module \"{0}\" could not be found, this may not be a valid IOS")]
|
||||
ModuleNotFound(String),
|
||||
#[error("failed to get IOS content")]
|
||||
Content(#[from] content::ContentError),
|
||||
#[error("failed to set content in Title")]
|
||||
Title(#[from] title::TitleError),
|
||||
#[error("IOS content is invalid")]
|
||||
|
||||
288
src/title/mod.rs
288
src/title/mod.rs
@@ -5,7 +5,6 @@
|
||||
|
||||
pub mod cert;
|
||||
pub mod commonkeys;
|
||||
pub mod content;
|
||||
pub mod crypto;
|
||||
pub mod iospatcher;
|
||||
pub mod nus;
|
||||
@@ -14,6 +13,8 @@ pub mod tmd;
|
||||
pub mod versions;
|
||||
pub mod wad;
|
||||
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use sha1::{Sha1, Digest};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -28,22 +29,33 @@ pub enum TitleError {
|
||||
TMD(#[from] tmd::TMDError),
|
||||
#[error("Ticket processing error")]
|
||||
Ticket(#[from] ticket::TicketError),
|
||||
#[error("content processing error")]
|
||||
Content(#[from] content::ContentError),
|
||||
#[error("WAD processing error")]
|
||||
WAD(#[from] wad::WADError),
|
||||
#[error("WAD data is not in a valid format")]
|
||||
IO(#[from] std::io::Error),
|
||||
// Content-specific (not generic or inherited from another struct's errors).
|
||||
#[error("requested index {index} is out of range (must not exceed {max})")]
|
||||
IndexOutOfRange { index: usize, max: usize },
|
||||
#[error("expected {required} contents based on content records but found {found}")]
|
||||
MissingContents { required: usize, found: usize },
|
||||
#[error("content with requested Content ID {0} could not be found")]
|
||||
CIDNotFound(u32),
|
||||
#[error("the specified index {0} already exists in the content records")]
|
||||
IndexAlreadyExists(u16),
|
||||
#[error("the specified Content ID {0} already exists in the content records")]
|
||||
CIDAlreadyExists(u32),
|
||||
#[error("content's hash did not match the expected value (was {hash}, expected {expected})")]
|
||||
BadHash { hash: String, expected: String },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A structure that represents the components of a digital Wii title.
|
||||
pub struct Title {
|
||||
pub cert_chain: cert::CertificateChain,
|
||||
cert_chain: cert::CertificateChain,
|
||||
crl: Vec<u8>,
|
||||
pub ticket: ticket::Ticket,
|
||||
pub tmd: tmd::TMD,
|
||||
pub content: content::ContentRegion,
|
||||
ticket: ticket::Ticket,
|
||||
tmd: tmd::TMD,
|
||||
content: Vec<Vec<u8>>,
|
||||
meta: Vec<u8>
|
||||
}
|
||||
|
||||
@@ -53,7 +65,7 @@ impl Title {
|
||||
let cert_chain = cert::CertificateChain::from_bytes(&wad.cert_chain()).map_err(TitleError::CertificateError)?;
|
||||
let ticket = ticket::Ticket::from_bytes(&wad.ticket()).map_err(TitleError::Ticket)?;
|
||||
let tmd = tmd::TMD::from_bytes(&wad.tmd()).map_err(TitleError::TMD)?;
|
||||
let content = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records().clone()).map_err(TitleError::Content)?;
|
||||
let content = Self::parse_content_region(wad.content(), tmd.content_records())?;
|
||||
Ok(Title {
|
||||
cert_chain,
|
||||
crl: wad.crl(),
|
||||
@@ -65,8 +77,18 @@ impl Title {
|
||||
}
|
||||
|
||||
/// Creates a new Title instance from all of its individual components.
|
||||
pub fn from_parts(cert_chain: cert::CertificateChain, crl: Option<&[u8]>, ticket: ticket::Ticket, tmd: tmd::TMD,
|
||||
content: content::ContentRegion, meta: Option<&[u8]>) -> Result<Title, TitleError> {
|
||||
pub fn from_parts_with_content(
|
||||
cert_chain: cert::CertificateChain,
|
||||
crl: Option<&[u8]>,
|
||||
ticket: ticket::Ticket,
|
||||
tmd: tmd::TMD,
|
||||
content: Vec<Vec<u8>>,
|
||||
meta: Option<&[u8]>
|
||||
) -> Result<Title, TitleError> {
|
||||
// Validate the provided content.
|
||||
if content.len() != tmd.content_records().len() {
|
||||
return Err(TitleError::MissingContents { required: tmd.content_records().len(), found: content.len()});
|
||||
}
|
||||
// Create empty vecs for the CRL and meta areas if we weren't supplied with any, as they're
|
||||
// optional components.
|
||||
let crl = match crl {
|
||||
@@ -87,15 +109,67 @@ impl Title {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new Title instance from all of its individual components. Content is expected to
|
||||
/// be added to the title once created.
|
||||
pub fn from_parts(
|
||||
cert_chain: cert::CertificateChain,
|
||||
crl: Option<&[u8]>,
|
||||
ticket: ticket::Ticket,
|
||||
tmd: tmd::TMD,
|
||||
meta: Option<&[u8]>
|
||||
) -> Result<Title, TitleError> {
|
||||
let content: Vec<Vec<u8>> = vec![vec![]; tmd.content_records().len()];
|
||||
Self::from_parts_with_content(
|
||||
cert_chain,
|
||||
crl,
|
||||
ticket,
|
||||
tmd,
|
||||
content,
|
||||
meta
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_content_region(content_data: Vec<u8>, content_records: &[tmd::ContentRecord]) -> Result<Vec<Vec<u8>>, TitleError> {
|
||||
let num_contents = content_records.len();
|
||||
// Calculate the starting offsets of each content.
|
||||
let content_start_offsets: Vec<u64> = std::iter::once(0)
|
||||
.chain(content_records.iter().scan(0, |offset, record| {
|
||||
*offset += record.content_size;
|
||||
if record.content_size % 64 != 0 {
|
||||
*offset += 64 - (record.content_size % 64);
|
||||
}
|
||||
Some(*offset)
|
||||
})).take(content_records.len()).collect(); // Trims the extra final entry.
|
||||
// Parse the content blob and create a vector of vectors from it.
|
||||
let mut contents: Vec<Vec<u8>> = Vec::with_capacity(num_contents);
|
||||
let mut buf = Cursor::new(content_data);
|
||||
for i in 0..num_contents {
|
||||
buf.seek(SeekFrom::Start(content_start_offsets[i]))?;
|
||||
let size = (content_records[i].content_size + 15) & !15;
|
||||
let mut content = vec![0u8; size as usize];
|
||||
buf.read_exact(&mut content)?;
|
||||
contents.push(content);
|
||||
}
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
/// Converts a Title instance into a WAD, which can be used to export the Title back to a file.
|
||||
pub fn to_wad(&self) -> Result<wad::WAD, TitleError> {
|
||||
let mut content: Vec<u8> = Vec::new();
|
||||
for i in 0..self.tmd.content_records().len() {
|
||||
let mut content_cur = self.content[i].clone();
|
||||
// Round up size to nearest 64 to add appropriate padding.
|
||||
content_cur.resize((content_cur.len() + 63) & !63, 0);
|
||||
content.write_all(&content_cur)?;
|
||||
}
|
||||
// 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,
|
||||
&content,
|
||||
&self.meta
|
||||
).map_err(TitleError::WAD)?;
|
||||
Ok(wad)
|
||||
@@ -108,6 +182,18 @@ impl Title {
|
||||
Ok(title)
|
||||
}
|
||||
|
||||
pub fn cert_chain(&self) -> &cert::CertificateChain {
|
||||
&self.cert_chain
|
||||
}
|
||||
|
||||
pub fn ticket(&self) -> &ticket::Ticket {
|
||||
&self.ticket
|
||||
}
|
||||
|
||||
pub fn tmd(&self) -> &tmd::TMD {
|
||||
&self.tmd
|
||||
}
|
||||
|
||||
/// Gets whether the TMD and Ticket of a Title are both fakesigned.
|
||||
pub fn is_fakesigned(&self) -> bool {
|
||||
self.tmd.is_fakesigned() && self.ticket.is_fakesigned()
|
||||
@@ -121,24 +207,175 @@ impl Title {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the encrypted content file from the ContentRegion at the specified index.
|
||||
pub fn get_enc_content_by_index(&self, index: usize) -> Result<Vec<u8>, TitleError> {
|
||||
let content = self.content.get(index).ok_or(
|
||||
TitleError::IndexOutOfRange { index, max: self.tmd.content_records().len() - 1 }
|
||||
)?;
|
||||
Ok(content.clone())
|
||||
}
|
||||
|
||||
/// Gets the decrypted content file from the Title at the specified index.
|
||||
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.title_key_dec())?;
|
||||
pub fn get_content_by_index(&self, index: usize) -> Result<Vec<u8>, TitleError> {
|
||||
let content = self.get_enc_content_by_index(index)?;
|
||||
// Verify the hash of the decrypted content against its record.
|
||||
let mut content_dec = crypto::decrypt_content(&content, self.ticket.title_key_dec(), self.tmd.content_records()[index].index);
|
||||
content_dec.resize(self.tmd.content_records()[index].content_size as usize, 0);
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(content_dec.clone());
|
||||
let result = hasher.finalize();
|
||||
if result[..] != self.tmd.content_records()[index].content_hash {
|
||||
return Err(TitleError::BadHash {
|
||||
hash: hex::encode(result), expected: hex::encode(self.tmd.content_records()[index].content_hash)
|
||||
});
|
||||
}
|
||||
Ok(content_dec)
|
||||
}
|
||||
|
||||
/// Gets the encrypted content file from the ContentRegion with the specified Content ID.
|
||||
pub fn get_enc_content_by_cid(&self, cid: u32) -> Result<Vec<u8>, TitleError> {
|
||||
let index = self.tmd.content_records().iter().position(|x| x.content_id == cid);
|
||||
if let Some(index) = index {
|
||||
let content = self.get_enc_content_by_index(index).map_err(|_| TitleError::CIDNotFound(cid))?;
|
||||
Ok(content)
|
||||
} else {
|
||||
Err(TitleError::CIDNotFound(cid))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the decrypted content file from the Title with the specified Content ID.
|
||||
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.title_key_dec())?;
|
||||
Ok(content)
|
||||
pub fn get_content_by_cid(&self, cid: u32) -> Result<Vec<u8>, TitleError> {
|
||||
let index = self.tmd.content_records().iter().position(|x| x.content_id == cid);
|
||||
if let Some(index) = index {
|
||||
let content_dec = self.get_content_by_index(index)?;
|
||||
Ok(content_dec)
|
||||
} else {
|
||||
Err(TitleError::CIDNotFound(cid))
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads existing content into the specified index of a ContentRegion instance. This content
|
||||
/// must be encrypted.
|
||||
pub fn load_enc_content(&mut self, content: &[u8], index: usize) -> Result<(), TitleError> {
|
||||
if index >= self.tmd.content_records().len() {
|
||||
return Err(TitleError::IndexOutOfRange { index, max: self.tmd.content_records().len() - 1 });
|
||||
}
|
||||
self.content[index] = content.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the content at the specified index to the provided encrypted content. This requires
|
||||
/// the size and hash of the original decrypted content to be known so that the appropriate
|
||||
/// values can be set in the corresponding content record. Optionally, a new Content ID or
|
||||
/// content type can be provided, with the existing values being preserved by default.
|
||||
pub fn set_enc_content(
|
||||
&mut self, content: &[u8],
|
||||
index: usize, content_size: u64,
|
||||
content_hash: [u8; 20],
|
||||
cid: Option<u32>,
|
||||
content_type: Option<tmd::ContentType>
|
||||
) -> Result<(), TitleError> {
|
||||
if index >= self.tmd.content_records().len() {
|
||||
return Err(TitleError::IndexOutOfRange { index, max: self.tmd.content_records().len() - 1 });
|
||||
}
|
||||
let mut content_records = self.tmd.content_records().clone();
|
||||
content_records[index].content_size = content_size;
|
||||
content_records[index].content_hash = content_hash;
|
||||
if let Some(cid) = cid {
|
||||
// Make sure that the new CID isn't already in use.
|
||||
if content_records.iter().any(|record| record.content_id == cid) {
|
||||
return Err(TitleError::CIDAlreadyExists(cid));
|
||||
}
|
||||
content_records[index].content_id = cid;
|
||||
}
|
||||
if let Some(content_type) = content_type {
|
||||
content_records[index].content_type = content_type;
|
||||
}
|
||||
self.tmd.set_content_records(content_records);
|
||||
self.content[index] = content.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads existing content into the specified index of a ContentRegion instance. This content
|
||||
/// must be decrypted and needs to match the size and hash listed in the content record at that
|
||||
/// index.
|
||||
pub fn load_content(&mut self, content: &[u8], index: usize) -> Result<(), TitleError> {
|
||||
if index >= self.tmd.content_records().len() {
|
||||
return Err(TitleError::IndexOutOfRange { index, max: self.tmd.content_records().len() - 1 });
|
||||
}
|
||||
// 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.tmd.content_records()[index].content_hash {
|
||||
return Err(TitleError::BadHash {
|
||||
hash: hex::encode(result), expected: hex::encode(self.tmd.content_records()[index].content_hash)
|
||||
});
|
||||
}
|
||||
let content_enc = crypto::encrypt_content(
|
||||
content,
|
||||
self.ticket.title_key_dec(),
|
||||
self.tmd.content_records()[index].index,
|
||||
self.tmd.content_records()[index].content_size
|
||||
);
|
||||
self.content[index] = content_enc;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the content at the specified index to the provided decrypted content. This content will
|
||||
/// have its size and hash saved into the matching record. Optionally, a new Content ID or
|
||||
/// content type can be provided, with the existing values being preserved by default.
|
||||
pub fn set_content(&mut self, content: &[u8], index: usize, cid: Option<u32>, content_type: Option<tmd::ContentType>) -> Result<(), TitleError> {
|
||||
self.content.set_content(content, index, cid, content_type, self.ticket.title_key_dec())?;
|
||||
self.tmd.set_content_records(self.content.content_records());
|
||||
let content_size = content.len() as u64;
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(content);
|
||||
let content_hash: [u8; 20] = hasher.finalize().into();
|
||||
let content_enc = crypto::encrypt_content(
|
||||
content,
|
||||
self.ticket.title_key_dec(),
|
||||
index as u16,
|
||||
content_size
|
||||
);
|
||||
self.set_enc_content(&content_enc, index, content_size, content_hash, cid, content_type)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the content at the specified index from the content list and content records. This
|
||||
/// may leave a gap in the indexes recorded in the content records, but this should not cause
|
||||
/// issues on the Wii or with correctly implemented WAD parsers.
|
||||
pub fn remove_content(&mut self, index: usize) -> Result<(), TitleError> {
|
||||
if self.content.get(index).is_none() || self.tmd.content_records().get(index).is_none() {
|
||||
return Err(TitleError::IndexOutOfRange { index, max: self.tmd.content_records().len() - 1 });
|
||||
}
|
||||
self.content.remove(index);
|
||||
let mut content_records = self.tmd.content_records().clone();
|
||||
content_records.remove(index);
|
||||
self.tmd.set_content_records(content_records);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds new encrypted content to the end of the content list and content records. The provided
|
||||
/// Content ID, type, index, and decrypted hash will be added to the record.
|
||||
pub fn add_enc_content(
|
||||
&mut self, content:
|
||||
&[u8], index: u16,
|
||||
cid: u32,
|
||||
content_type: tmd::ContentType,
|
||||
content_size: u64,
|
||||
content_hash: [u8; 20]
|
||||
) -> Result<(), TitleError> {
|
||||
// Return an error if the specified index or CID already exist in the records.
|
||||
if self.tmd.content_records().iter().any(|record| record.index == index) {
|
||||
return Err(TitleError::IndexAlreadyExists(index));
|
||||
}
|
||||
if self.tmd.content_records().iter().any(|record| record.content_id == cid) {
|
||||
return Err(TitleError::CIDAlreadyExists(cid));
|
||||
}
|
||||
self.content.push(content.to_vec());
|
||||
let mut content_records = self.tmd.content_records().clone();
|
||||
content_records.push(tmd::ContentRecord { content_id: cid, index, content_type, content_size, content_hash });
|
||||
self.tmd.set_content_records(content_records);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -147,8 +384,17 @@ impl Title {
|
||||
/// index will be automatically assigned based on the highest index currently recorded in the
|
||||
/// content records.
|
||||
pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: tmd::ContentType) -> Result<(), TitleError> {
|
||||
self.content.add_content(content, cid, content_type, self.ticket.title_key_dec())?;
|
||||
self.tmd.set_content_records(self.content.content_records());
|
||||
let max_index = self.tmd.content_records().iter()
|
||||
.max_by_key(|record| record.index)
|
||||
.map(|record| record.index)
|
||||
.unwrap_or(0); // This should be impossible, but I guess 0 is a safe value just in case?
|
||||
let new_index = max_index + 1;
|
||||
let content_size = content.len() as u64;
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(content);
|
||||
let content_hash: [u8; 20] = hasher.finalize().into();
|
||||
let content_enc = crypto::encrypt_content(content, self.ticket.title_key_dec(), new_index, content_size);
|
||||
self.add_enc_content(&content_enc, new_index, cid, content_type, content_size, content_hash)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -223,7 +469,7 @@ impl Title {
|
||||
self.tmd = tmd;
|
||||
}
|
||||
|
||||
pub fn set_content_region(&mut self, content: content::ContentRegion) {
|
||||
pub fn set_contents(&mut self, content: Vec<Vec<u8>>) {
|
||||
self.content = content;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::str;
|
||||
use std::io::Write;
|
||||
use reqwest;
|
||||
use thiserror::Error;
|
||||
use crate::title::{cert, tmd, ticket, content};
|
||||
use crate::title::{cert, tmd, ticket};
|
||||
use crate::title;
|
||||
|
||||
const WII_NUS_ENDPOINT: &str = "http://nus.cdn.shop.wii.com/ccs/download/";
|
||||
@@ -25,8 +25,6 @@ pub enum NUSError {
|
||||
TMD(#[from] tmd::TMDError),
|
||||
#[error("Ticket processing error")]
|
||||
Ticket(#[from] ticket::TicketError),
|
||||
#[error("Content processing error")]
|
||||
Content(#[from] content::ContentError),
|
||||
#[error("an error occurred while assembling a Title from the downloaded data")]
|
||||
Title(#[from] title::TitleError),
|
||||
#[error("data could not be downloaded from the NUS")]
|
||||
@@ -112,8 +110,8 @@ pub fn download_title(title_id: [u8; 8], title_version: Option<u16>, wiiu_endpoi
|
||||
let cert_chain = cert::CertificateChain::from_bytes(&download_cert_chain(wiiu_endpoint)?)?;
|
||||
let tmd = tmd::TMD::from_bytes(&download_tmd(title_id, title_version, wiiu_endpoint)?)?;
|
||||
let tik = ticket::Ticket::from_bytes(&download_ticket(title_id, wiiu_endpoint)?)?;
|
||||
let content_region = content::ContentRegion::from_contents(download_contents(&tmd, wiiu_endpoint)?, tmd.content_records().clone())?;
|
||||
let title = title::Title::from_parts(cert_chain, None, tik, tmd, content_region, None)?;
|
||||
let contents = download_contents(&tmd, wiiu_endpoint)?;
|
||||
let title = title::Title::from_parts_with_content(cert_chain, None, tik, tmd, contents, None)?;
|
||||
Ok(title)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct TitleLimit {
|
||||
pub limit_max: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
/// A structure that represents a Wii Ticket file.
|
||||
pub struct Ticket {
|
||||
signature_type: u32,
|
||||
|
||||
@@ -24,6 +24,8 @@ pub enum TMDError {
|
||||
InvalidContentType(u16),
|
||||
#[error("encountered unknown title type `{0}`")]
|
||||
InvalidTitleType(String),
|
||||
#[error("content with requested Content ID {0} could not be found")]
|
||||
CIDNotFound(u32),
|
||||
#[error("TMD data is not in a valid format")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
@@ -90,7 +92,7 @@ pub struct ContentRecord {
|
||||
}
|
||||
|
||||
/// A structure that represents a Wii TMD (Title Metadata) file.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TMD {
|
||||
signature_type: u32,
|
||||
signature: [u8; 256],
|
||||
@@ -322,8 +324,8 @@ impl TMD {
|
||||
}
|
||||
|
||||
/// Sets the content records in the TMD.
|
||||
pub fn set_content_records(&mut self, content_records: &[ContentRecord]) {
|
||||
self.content_records = content_records.to_vec();
|
||||
pub fn set_content_records(&mut self, content_records: Vec<ContentRecord>) {
|
||||
self.content_records = content_records;
|
||||
}
|
||||
|
||||
/// Gets whether a TMD is fakesigned using the strncmp (trucha) bug or not.
|
||||
@@ -476,4 +478,18 @@ impl TMD {
|
||||
self.ios_tid = ios_tid;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the index of content using its Content ID.
|
||||
pub fn get_index_from_cid(&self, cid: u32) -> Result<usize, TMDError> {
|
||||
// Use fancy Rust find and map methods to find the index matching the provided CID. Take
|
||||
// that libWiiPy!
|
||||
let content_index = self.content_records().iter()
|
||||
.find(|record| record.content_id == cid)
|
||||
.map(|record| record.index);
|
||||
if let Some(index) = content_index {
|
||||
Ok(index as usize)
|
||||
} else {
|
||||
Err(TMDError::CIDNotFound(cid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::str;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
use thiserror::Error;
|
||||
use crate::title::{cert, tmd, ticket, content};
|
||||
use crate::title::{cert, tmd, ticket};
|
||||
use crate::title::ticket::TicketError;
|
||||
use crate::title::tmd::TMDError;
|
||||
|
||||
@@ -143,13 +143,13 @@ impl WADHeader {
|
||||
impl WADBody {
|
||||
/// Creates a new WADBody instance from instances of the components stored in a WAD file.
|
||||
pub fn from_parts(cert_chain: &cert::CertificateChain, crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD,
|
||||
content: &content::ContentRegion, meta: &[u8]) -> Result<WADBody, WADError> {
|
||||
content: &[u8], meta: &[u8]) -> Result<WADBody, WADError> {
|
||||
let body = WADBody {
|
||||
cert_chain: cert_chain.to_bytes().map_err(WADError::IO)?,
|
||||
crl: crl.to_vec(),
|
||||
ticket: ticket.to_bytes().map_err(WADError::IO)?,
|
||||
tmd: tmd.to_bytes().map_err(WADError::IO)?,
|
||||
content: content.to_bytes().map_err(WADError::IO)?,
|
||||
content: content.to_vec(),
|
||||
meta: meta.to_vec(),
|
||||
};
|
||||
Ok(body)
|
||||
@@ -239,7 +239,7 @@ impl WAD {
|
||||
/// Creates a new WAD instance from instances of the components stored in a WAD file. This
|
||||
/// first creates a WADBody from the components, then generates a new WADHeader from them.
|
||||
pub fn from_parts(cert_chain: &cert::CertificateChain, crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD,
|
||||
content: &content::ContentRegion, meta: &[u8]) -> Result<WAD, WADError> {
|
||||
content: &[u8], 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 {
|
||||
|
||||
Reference in New Issue
Block a user