Use Rc<RefCell<T>>> for content_records so that they sync in a Title

When using a Title, the content_records value stored in the TMD and ContentRegion instances will now point to the same data, meaning that they stay in sync. Previously, you had to manually sync the content records between them as they were modified, and not doing so would cause problems when editing a WAD.
This commit is contained in:
Campbell 2025-04-27 21:30:31 -04:00
parent 277c5d6439
commit 481594345d
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
8 changed files with 80 additions and 89 deletions

View File

@ -11,8 +11,8 @@ fn main() {
let wad = wad::WAD::from_bytes(&data).unwrap(); let wad = wad::WAD::from_bytes(&data).unwrap();
println!("size of tmd: {:?}", wad.tmd().len()); println!("size of tmd: {:?}", wad.tmd().len());
println!("num content records: {:?}", title.tmd.content_records.len()); println!("num content records: {:?}", title.tmd.content_records.borrow().len());
println!("first record data: {:?}", title.tmd.content_records.first().unwrap()); println!("first record data: {:?}", title.tmd.content_records.borrow().first().unwrap());
println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned()); println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned());
println!("title version from ticket is: {:?}", title.ticket.title_version); println!("title version from ticket is: {:?}", title.ticket.title_version);

View File

@ -117,10 +117,10 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()>
println!(" Fakesigned: {}", tmd.is_fakesigned()); println!(" Fakesigned: {}", tmd.is_fakesigned());
} }
println!("\nContent Info"); println!("\nContent Info");
println!(" Total Contents: {}", tmd.num_contents); println!(" Total Contents: {}", tmd.content_records.borrow().len());
println!(" Boot Content Index: {}", tmd.boot_index); println!(" Boot Content Index: {}", tmd.boot_index);
println!(" Content Records:"); println!(" Content Records:");
for content in tmd.content_records { for content in tmd.content_records.borrow().iter() {
println!(" Content Index: {}", content.index); println!(" Content Index: {}", content.index);
println!(" Content ID: {:08X}", content.content_id); println!(" Content ID: {:08X}", content.content_id);
println!(" Content Type: {}", content.content_type); println!(" Content Type: {}", content.content_type);

View File

@ -109,7 +109,7 @@ pub fn download_content(tid: &str, cid: &str, version: &Option<String>, output:
Err(_) => bail!("No Ticket is available for this title! The content cannot be decrypted.") Err(_) => bail!("No Ticket is available for this title! The content cannot be decrypted.")
}; };
println!(" - Decrypting content..."); println!(" - Decrypting content...");
let (content_hash, content_size, content_index) = tmd.content_records.iter() let (content_hash, content_size, content_index) = tmd.content_records.borrow().iter()
.find(|record| record.content_id == cid) .find(|record| record.content_id == cid)
.map(|record| (record.content_hash, record.content_size, record.index)) .map(|record| (record.content_hash, record.content_size, record.index))
.with_context(|| "No matching content record could be found. Please make sure the requested content is from the specified title version.")?; .with_context(|| "No matching content record could be found. Please make sure the requested content is from the specified title version.")?;
@ -167,7 +167,7 @@ fn download_title_dir(title: title::Title, output: String) -> Result<()> {
println!(" - Saving certificate chain..."); 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. // Iterate over the content files and write them out in encrypted form.
for record in &title.content.content_records { for record in title.content.content_records.borrow().iter() {
println!(" - Decrypting and saving content with Content ID {}...", record.content_id); 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)?) 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))?; .with_context(|| format!("Failed to open content file \"{:08X}.app\" for writing.", record.content_id))?;
@ -192,7 +192,7 @@ fn download_title_dir_enc(tmd: tmd::TMD, content_region: content::ContentRegion,
println!(" - Saving certificate chain..."); 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))?; 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. // Iterate over the content files and write them out in encrypted form.
for record in &content_region.content_records { for record in content_region.content_records.borrow().iter() {
println!(" - Saving content with Content ID {}...", record.content_id); 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)?) fs::write(out_path.join(format!("{:08X}", record.content_id)), content_region.get_enc_content_by_cid(record.content_id)?)
.with_context(|| format!("Failed to open content file \"{:08X}\" for writing.", record.content_id))?; .with_context(|| format!("Failed to open content file \"{:08X}\" for writing.", record.content_id))?;
@ -241,9 +241,9 @@ pub fn download_title(tid: &str, version: &Option<String>, output: &TitleOutputT
}; };
// Build a vec of contents by iterating over the content records and downloading each one. // Build a vec of contents by iterating over the content records and downloading each one.
let mut contents: Vec<Vec<u8>> = Vec::new(); let mut contents: Vec<Vec<u8>> = Vec::new();
for record in &tmd.content_records { for record in tmd.content_records.borrow().iter() {
println!(" - Downloading content {} of {} (Content ID: {}, Size: {} bytes)...", println!(" - Downloading content {} of {} (Content ID: {}, Size: {} bytes)...",
record.index + 1, &tmd.content_records.len(), record.content_id, record.content_size); record.index + 1, &tmd.content_records.borrow().len(), record.content_id, record.content_size);
contents.push(nus::download_content(tid, record.content_id, true).with_context(|| format!("Content with Content ID {} could not be downloaded.", record.content_id))?); 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!"); println!(" - Done!");
} }

View File

@ -151,11 +151,12 @@ pub fn add_wad(input: &str, content: &str, output: &Option<String>, cid: &Option
_ => bail!("The specified content type \"{}\" is invalid!", ctype.clone().unwrap()), _ => bail!("The specified content type \"{}\" is invalid!", ctype.clone().unwrap()),
} }
} else { } else {
println!("Using default type \"Normal\" because no content type was specified.");
tmd::ContentType::Normal tmd::ContentType::Normal
}; };
let target_cid = if cid.is_some() { 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!")?; 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.content.content_records.borrow().iter().any(|record| record.content_id == cid) {
bail!("The specified Content ID \"{:08X}\" is already being used in this WAD!", cid); bail!("The specified Content ID \"{:08X}\" is already being used in this WAD!", cid);
} }
cid cid
@ -165,18 +166,17 @@ pub fn add_wad(input: &str, content: &str, output: &Option<String>, cid: &Option
let mut cid: u32; let mut cid: u32;
loop { loop {
cid = rng.random_range(0..=0xFF); cid = rng.random_range(0..=0xFF);
if !title.content.content_records.iter().any(|record| record.content_id == cid) { if !title.content.content_records.borrow().iter().any(|record| record.content_id == cid) {
break; break;
} }
} }
println!("Generated new random Content ID \"{:08X}\" ({}) because no Content ID was specified.", cid, cid);
cid cid
}; };
title.add_content(&new_content, target_cid, target_type.clone()).with_context(|| "An unknown error occurred while setting the new content.")?; title.add_content(&new_content, target_cid, target_type.clone()).with_context(|| "An unknown error occurred while setting the new content.")?;
title.tmd.content_records = title.content.content_records.clone();
title.tmd.num_contents = title.content.num_contents;
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified 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.")?; fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
println!("Successfully added new content with Content ID \"{:08X}\" ({}) and type \"{}\" to WAD file \"{}\".", target_cid, target_cid, target_type, out_path.display()); println!("Successfully added new content with Content ID \"{:08X}\" ({}) and type \"{}\" to WAD file \"{}\"!", target_cid, target_cid, target_type, out_path.display());
Ok(()) Ok(())
} }
@ -296,7 +296,7 @@ pub fn pack_wad(input: &str, output: &str) -> Result<()> {
} }
// Iterate over expected content and read it into a content region. // Iterate over expected content and read it into a content region.
let mut content_region = content::ContentRegion::new(tmd.content_records.clone())?; let mut content_region = content::ContentRegion::new(tmd.content_records.clone())?;
for content in tmd.content_records.clone() { for content in tmd.content_records.borrow().iter() {
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))?; 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.set_content(&data, content.index as usize, None, None, tik.dec_title_key()) content_region.set_content(&data, content.index as usize, None, None, tik.dec_title_key())
.with_context(|| "Failed to load content into the ContentRegion.")?; .with_context(|| "Failed to load content into the ContentRegion.")?;
@ -336,11 +336,6 @@ pub fn remove_wad(input: &str, output: &Option<String>, identifier: &ContentIden
// ...maybe don't take the above comment out of context // ...maybe don't take the above comment out of context
if identifier.index.is_some() { if identifier.index.is_some() {
title.content.remove_content(identifier.index.unwrap()).with_context(|| "The specified index does not exist in the provided WAD!")?; title.content.remove_content(identifier.index.unwrap()).with_context(|| "The specified index does not exist in the provided WAD!")?;
// Sync the content records in the TMD with the modified ones in the ContentRegion. The fact
// that this is required is probably bad and should be addressed on the library side at some
// point.
title.tmd.content_records = title.content.content_records.clone();
title.tmd.num_contents = title.content.num_contents;
println!("{:?}", title.tmd); println!("{:?}", title.tmd);
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified 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.")?; fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
@ -352,9 +347,6 @@ pub fn remove_wad(input: &str, output: &Option<String>, identifier: &ContentIden
Err(_) => bail!("The specified Content ID \"{}\" ({}) does not exist in this WAD!", identifier.cid.clone().unwrap(), cid), 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.")?; title.content.remove_content(index).with_context(|| "An unknown error occurred while removing content from the WAD.")?;
// Ditto.
title.tmd.content_records = title.content.content_records.clone();
title.tmd.num_contents = title.content.num_contents;
println!("{:?}", title.tmd); println!("{:?}", title.tmd);
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified 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.")?; fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
@ -438,10 +430,10 @@ pub fn unpack_wad(input: &str, output: &str) -> Result<()> {
let meta_file_name = format!("{}.footer", tid); let meta_file_name = format!("{}.footer", tid);
fs::write(Path::join(out_path, meta_file_name.clone()), title.meta()).with_context(|| format!("Failed to open footer file \"{}\" for writing.", meta_file_name))?; fs::write(Path::join(out_path, meta_file_name.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. // Iterate over contents, decrypt them, and write them out.
for i in 0..title.tmd.num_contents { for i in 0..title.tmd.content_records.borrow().len() {
let content_file_name = format!("{:08X}.app", title.content.content_records[i as usize].index); let content_file_name = format!("{:08X}.app", title.content.content_records.borrow()[i].index);
let dec_content = title.get_content_by_index(i as usize).with_context(|| format!("Failed to unpack content with Content ID {:08X}.", title.content.content_records[i as usize].content_id))?; let dec_content = title.get_content_by_index(i).with_context(|| format!("Failed to unpack content with Content ID {:08X}.", title.content.content_records.borrow()[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 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.borrow()[i].content_id))?;
} }
println!("WAD file unpacked!"); println!("WAD file unpacked!");
Ok(()) Ok(())

View File

@ -3,7 +3,9 @@
// //
// Implements content parsing and editing. // Implements content parsing and editing.
use std::cell::RefCell;
use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::rc::Rc;
use sha1::{Sha1, Digest}; use sha1::{Sha1, Digest};
use thiserror::Error; use thiserror::Error;
use crate::title::tmd::{ContentRecord, ContentType}; use crate::title::tmd::{ContentRecord, ContentType};
@ -31,9 +33,8 @@ pub enum ContentError {
#[derive(Debug)] #[derive(Debug)]
/// A structure that represents the block of data containing the content of a digital Wii title. /// A structure that represents the block of data containing the content of a digital Wii title.
pub struct ContentRegion { pub struct ContentRegion {
pub content_records: Vec<ContentRecord>, pub content_records: Rc<RefCell<Vec<ContentRecord>>>,
pub content_region_size: u32, pub content_region_size: u32,
pub num_contents: u16,
pub content_start_offsets: Vec<u64>, pub content_start_offsets: Vec<u64>,
pub contents: Vec<Vec<u8>>, pub contents: Vec<Vec<u8>>,
} }
@ -41,32 +42,31 @@ pub struct ContentRegion {
impl ContentRegion { impl ContentRegion {
/// Creates a ContentRegion instance that can be used to parse and edit content stored in a /// 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. /// 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> { pub fn from_bytes(data: &[u8], content_records: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
let content_region_size = data.len() as u32; let content_region_size = data.len() as u32;
let num_contents = content_records.len() as u16; let num_contents = content_records.borrow().len() as u16;
// Calculate the starting offsets of each content. // Calculate the starting offsets of each content.
let content_start_offsets: Vec<u64> = std::iter::once(0) let content_start_offsets: Vec<u64> = std::iter::once(0)
.chain(content_records.iter().scan(0, |offset, record| { .chain(content_records.borrow().iter().scan(0, |offset, record| {
*offset += record.content_size; *offset += record.content_size;
if record.content_size % 64 != 0 { if record.content_size % 64 != 0 {
*offset += 64 - (record.content_size % 64); *offset += 64 - (record.content_size % 64);
} }
Some(*offset) Some(*offset)
})).take(content_records.len()).collect(); // Trims the extra final entry. })).take(content_records.borrow().len()).collect(); // Trims the extra final entry.
// Parse the content blob and create a vector of vectors from it. // 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 contents: Vec<Vec<u8>> = Vec::with_capacity(num_contents as usize);
let mut buf = Cursor::new(data); let mut buf = Cursor::new(data);
for i in 0..num_contents { for i in 0..num_contents {
buf.seek(SeekFrom::Start(content_start_offsets[i as usize]))?; buf.seek(SeekFrom::Start(content_start_offsets[i as usize]))?;
let size = (content_records[i as usize].content_size + 15) & !15; let size = (content_records.borrow()[i as usize].content_size + 15) & !15;
let mut content = vec![0u8; size as usize]; let mut content = vec![0u8; size as usize];
buf.read_exact(&mut content)?; buf.read_exact(&mut content)?;
contents.push(content); contents.push(content);
} }
Ok(ContentRegion { Ok(ContentRegion {
content_records, content_records: Rc::clone(&content_records),
content_region_size, content_region_size,
num_contents,
content_start_offsets, content_start_offsets,
contents, contents,
}) })
@ -74,29 +74,29 @@ impl ContentRegion {
/// Creates a ContentRegion instance that can be used to parse and edit content stored in a /// 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. /// 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> { pub fn from_contents(contents: Vec<Vec<u8>>, content_records: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
if contents.len() != content_records.len() { if contents.len() != content_records.borrow().len() {
return Err(ContentError::MissingContents { required: content_records.len(), found: contents.len()}); return Err(ContentError::MissingContents { required: content_records.borrow().len(), found: contents.len()});
} }
let mut content_region = Self::new(content_records)?; let mut content_region = Self::new(Rc::clone(&content_records))?;
for i in 0..contents.len() { for i in 0..contents.len() {
content_region.load_enc_content(&contents[i], content_region.content_records[i].index as usize)?; let target_index = content_region.content_records.borrow()[i].index;
content_region.load_enc_content(&contents[i], target_index as usize)?;
} }
Ok(content_region) Ok(content_region)
} }
/// Creates a ContentRegion instance from the ContentRecords of a TMD that contains no actual /// 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. /// content. This can be used to load existing content from files.
pub fn new(content_records: Vec<ContentRecord>) -> Result<Self, ContentError> { pub fn new(content_records: Rc<RefCell<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: u64 = content_records.borrow().iter().map(|x| (x.content_size + 63) & !63).sum();
let content_region_size = content_region_size as u32; let content_region_size = content_region_size as u32;
let num_contents = content_records.len() as u16; let num_contents = content_records.borrow().len() as u16;
let content_start_offsets: Vec<u64> = vec![0; num_contents as usize]; let content_start_offsets: Vec<u64> = vec![0; num_contents as usize];
let contents: Vec<Vec<u8>> = vec![Vec::new(); num_contents as usize]; let contents: Vec<Vec<u8>> = vec![Vec::new(); num_contents as usize];
Ok(ContentRegion { Ok(ContentRegion {
content_records, content_records: Rc::clone(&content_records),
content_region_size, content_region_size,
num_contents,
content_start_offsets, content_start_offsets,
contents, contents,
}) })
@ -105,7 +105,7 @@ impl ContentRegion {
/// Dumps the entire ContentRegion back into binary data that can be written to a file. /// 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> { pub fn to_bytes(&self) -> Result<Vec<u8>, std::io::Error> {
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
for i in 0..self.num_contents { for i in 0..self.content_records.borrow().len() {
let mut content = self.contents[i as usize].clone(); let mut content = self.contents[i as usize].clone();
// Round up size to nearest 64 to add appropriate padding. // Round up size to nearest 64 to add appropriate padding.
content.resize((content.len() + 63) & !63, 0); content.resize((content.len() + 63) & !63, 0);
@ -118,7 +118,7 @@ impl ContentRegion {
pub fn get_index_from_cid(&self, cid: u32) -> Result<usize, ContentError> { 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 // Use fancy Rust find and map methods to find the index matching the provided CID. Take
// that libWiiPy! // that libWiiPy!
let content_index = self.content_records.iter() let content_index = self.content_records.borrow().iter()
.find(|record| record.content_id == cid) .find(|record| record.content_id == cid)
.map(|record| record.index); .map(|record| record.index);
if let Some(index) = content_index { if let Some(index) = content_index {
@ -130,7 +130,7 @@ impl ContentRegion {
/// Gets the encrypted content file from the ContentRegion at the specified index. /// 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> { 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 })?; let content = self.contents.get(index).ok_or(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 })?;
Ok(content.clone()) Ok(content.clone())
} }
@ -138,20 +138,20 @@ impl ContentRegion {
pub fn get_content_by_index(&self, index: usize, title_key: [u8; 16]) -> Result<Vec<u8>, ContentError> { 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)?; let content = self.get_enc_content_by_index(index)?;
// Verify the hash of the decrypted content against its record. // 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); let mut content_dec = crypto::decrypt_content(&content, title_key, self.content_records.borrow()[index].index);
content_dec.resize(self.content_records[index].content_size as usize, 0); content_dec.resize(self.content_records.borrow()[index].content_size as usize, 0);
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
hasher.update(content_dec.clone()); hasher.update(content_dec.clone());
let result = hasher.finalize(); let result = hasher.finalize();
if result[..] != self.content_records[index].content_hash { if result[..] != self.content_records.borrow()[index].content_hash {
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records[index].content_hash) }); return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records.borrow()[index].content_hash) });
} }
Ok(content_dec) Ok(content_dec)
} }
/// Gets the encrypted content file from the ContentRegion with the specified Content ID. /// 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> { 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); let index = self.content_records.borrow().iter().position(|x| x.content_id == cid);
if let Some(index) = index { if let Some(index) = index {
let content = self.get_enc_content_by_index(index).map_err(|_| ContentError::CIDNotFound(cid))?; let content = self.get_enc_content_by_index(index).map_err(|_| ContentError::CIDNotFound(cid))?;
Ok(content) Ok(content)
@ -162,7 +162,7 @@ impl ContentRegion {
/// Gets the decrypted content file from the ContentRegion with the specified Content ID. /// 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> { 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); let index = self.content_records.borrow().iter().position(|x| x.content_id == cid);
if let Some(index) = index { if let Some(index) = index {
let content_dec = self.get_content_by_index(index, title_key)?; let content_dec = self.get_content_by_index(index, title_key)?;
Ok(content_dec) Ok(content_dec)
@ -174,8 +174,8 @@ impl ContentRegion {
/// Loads existing content into the specified index of a ContentRegion instance. This content /// Loads existing content into the specified index of a ContentRegion instance. This content
/// must be encrypted. /// must be encrypted.
pub fn load_enc_content(&mut self, content: &[u8], index: usize) -> Result<(), ContentError> { pub fn load_enc_content(&mut self, content: &[u8], index: usize) -> Result<(), ContentError> {
if index >= self.content_records.len() { if index >= self.content_records.borrow().len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 }); return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
} }
self.contents[index] = content.to_vec(); self.contents[index] = content.to_vec();
Ok(()) Ok(())
@ -186,20 +186,20 @@ impl ContentRegion {
/// values can be set in the corresponding content record. Optionally, a new Content ID or /// 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. /// 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> { 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() { if index >= self.content_records.borrow().len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 }); return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
} }
self.content_records[index].content_size = content_size; self.content_records.borrow_mut()[index].content_size = content_size;
self.content_records[index].content_hash = content_hash; self.content_records.borrow_mut()[index].content_hash = content_hash;
if cid.is_some() { if cid.is_some() {
// Make sure that the new CID isn't already in use. // Make sure that the new CID isn't already in use.
if self.content_records.iter().any(|record| record.content_id == cid.unwrap()) { if self.content_records.borrow().iter().any(|record| record.content_id == cid.unwrap()) {
return Err(ContentError::CIDAlreadyExists(cid.unwrap())); return Err(ContentError::CIDAlreadyExists(cid.unwrap()));
} }
self.content_records[index].content_id = cid.unwrap(); self.content_records.borrow_mut()[index].content_id = cid.unwrap();
} }
if content_type.is_some() { if content_type.is_some() {
self.content_records[index].content_type = content_type.unwrap(); self.content_records.borrow_mut()[index].content_type = content_type.unwrap();
} }
self.contents[index] = content.to_vec(); self.contents[index] = content.to_vec();
Ok(()) Ok(())
@ -209,18 +209,18 @@ impl ContentRegion {
/// must be decrypted and needs to match the size and hash listed in the content record at that /// must be decrypted and needs to match the size and hash listed in the content record at that
/// index. /// index.
pub fn load_content(&mut self, content: &[u8], index: usize, title_key: [u8; 16]) -> Result<(), ContentError> { pub fn load_content(&mut self, content: &[u8], index: usize, title_key: [u8; 16]) -> Result<(), ContentError> {
if index >= self.content_records.len() { if index >= self.content_records.borrow().len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 }); return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
} }
// Hash the content we're trying to load to ensure it matches the hash expected in the // Hash the content we're trying to load to ensure it matches the hash expected in the
// matching record. // matching record.
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
hasher.update(content); hasher.update(content);
let result = hasher.finalize(); let result = hasher.finalize();
if result[..] != self.content_records[index].content_hash { if result[..] != self.content_records.borrow()[index].content_hash {
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records[index].content_hash) }); return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records.borrow()[index].content_hash) });
} }
let content_enc = encrypt_content(content, title_key, self.content_records[index].index, self.content_records[index].content_size); let content_enc = encrypt_content(content, title_key, self.content_records.borrow()[index].index, self.content_records.borrow()[index].content_size);
self.contents[index] = content_enc; self.contents[index] = content_enc;
Ok(()) Ok(())
} }
@ -243,12 +243,11 @@ impl ContentRegion {
/// may leave a gap in the indexes recorded in the content records, but this should not cause /// 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. /// issues on the Wii or with correctly implemented WAD parsers.
pub fn remove_content(&mut self, index: usize) -> Result<(), ContentError> { pub fn remove_content(&mut self, index: usize) -> Result<(), ContentError> {
if self.contents.get(index).is_none() || self.content_records.get(index).is_none() { if self.contents.get(index).is_none() || self.content_records.borrow().get(index).is_none() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 }); return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
} }
self.contents.remove(index); self.contents.remove(index);
self.content_records.remove(index); self.content_records.borrow_mut().remove(index);
self.num_contents -= 1;
Ok(()) Ok(())
} }
@ -256,15 +255,14 @@ impl ContentRegion {
/// Content ID, type, index, and decrypted hash will be added to the record. /// 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> { 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. // Return an error if the specified index or CID already exist in the records.
if self.content_records.iter().any(|record| record.index == index) { if self.content_records.borrow().iter().any(|record| record.index == index) {
return Err(ContentError::IndexAlreadyExists(index)); return Err(ContentError::IndexAlreadyExists(index));
} }
if self.content_records.iter().any(|record| record.content_id == cid) { if self.content_records.borrow().iter().any(|record| record.content_id == cid) {
return Err(ContentError::CIDAlreadyExists(cid)); return Err(ContentError::CIDAlreadyExists(cid));
} }
self.contents.push(content.to_vec()); self.contents.push(content.to_vec());
self.content_records.push(ContentRecord { content_id: cid, index, content_type, content_size, content_hash }); self.content_records.borrow_mut().push(ContentRecord { content_id: cid, index, content_type, content_size, content_hash });
self.num_contents += 1;
Ok(()) Ok(())
} }
@ -273,7 +271,7 @@ impl ContentRegion {
/// index will be automatically assigned based on the highest index currently recorded in the /// index will be automatically assigned based on the highest index currently recorded in the
/// content records. /// content records.
pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: ContentType, title_key: [u8; 16]) -> Result<(), ContentError> { 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() let max_index = self.content_records.borrow().iter()
.max_by_key(|record| record.index) .max_by_key(|record| record.index)
.map(|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? .unwrap_or(0); // This should be impossible, but I guess 0 is a safe value just in case?

View File

@ -13,6 +13,7 @@ pub mod tmd;
pub mod versions; pub mod versions;
pub mod wad; pub mod wad;
use std::rc::Rc;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -52,7 +53,7 @@ impl Title {
let cert_chain = cert::CertificateChain::from_bytes(&wad.cert_chain()).map_err(TitleError::CertificateError)?; 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 ticket = ticket::Ticket::from_bytes(&wad.ticket()).map_err(TitleError::Ticket)?;
let tmd = tmd::TMD::from_bytes(&wad.tmd()).map_err(TitleError::TMD)?; 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 = content::ContentRegion::from_bytes(&wad.content(), Rc::clone(&tmd.content_records)).map_err(TitleError::Content)?;
Ok(Title { Ok(Title {
cert_chain, cert_chain,
crl: wad.crl(), crl: wad.crl(),
@ -137,7 +138,6 @@ impl Title {
/// content type can be provided, with the existing values being preserved by default. /// 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> { 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.dec_title_key())?; self.content.set_content(content, index, cid, content_type, self.ticket.dec_title_key())?;
self.tmd.content_records = self.content.content_records.clone();
Ok(()) Ok(())
} }
@ -147,7 +147,6 @@ impl Title {
/// content records. /// content records.
pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: tmd::ContentType) -> Result<(), TitleError> { 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.dec_title_key())?; self.content.add_content(content, cid, content_type, self.ticket.dec_title_key())?;
self.tmd.content_records = self.content.content_records.clone();
Ok(()) Ok(())
} }
@ -159,7 +158,7 @@ impl Title {
// accurate results. // accurate results.
title_size += self.tmd.to_bytes().map_err(|x| TitleError::TMD(tmd::TMDError::IO(x)))?.len(); title_size += self.tmd.to_bytes().map_err(|x| TitleError::TMD(tmd::TMDError::IO(x)))?.len();
title_size += self.ticket.to_bytes().map_err(|x| TitleError::Ticket(ticket::TicketError::IO(x)))?.len(); title_size += self.ticket.to_bytes().map_err(|x| TitleError::Ticket(ticket::TicketError::IO(x)))?.len();
for record in &self.tmd.content_records { for record in self.tmd.content_records.borrow().iter() {
if matches!(record.content_type, tmd::ContentType::Shared) { if matches!(record.content_type, tmd::ContentType::Shared) {
if absolute == Some(true) { if absolute == Some(true) {
title_size += record.content_size as usize; title_size += record.content_size as usize;

View File

@ -80,7 +80,7 @@ pub fn download_content(title_id: [u8; 8], content_id: u32, wiiu_endpoint: bool)
/// Downloads all contents from the specified title from the NUS. /// Downloads all contents from the specified title from the NUS.
pub fn download_contents(tmd: &tmd::TMD, wiiu_endpoint: bool) -> Result<Vec<Vec<u8>>, NUSError> { pub fn download_contents(tmd: &tmd::TMD, wiiu_endpoint: bool) -> Result<Vec<Vec<u8>>, NUSError> {
let content_ids: Vec<u32> = tmd.content_records.iter().map(|record| { record.content_id }).collect(); let content_ids: Vec<u32> = tmd.content_records.borrow().iter().map(|record| { record.content_id }).collect();
let mut contents: Vec<Vec<u8>> = Vec::new(); let mut contents: Vec<Vec<u8>> = Vec::new();
for id in content_ids { for id in content_ids {
contents.push(download_content(tmd.title_id, id, wiiu_endpoint)?); contents.push(download_content(tmd.title_id, id, wiiu_endpoint)?);

View File

@ -3,9 +3,11 @@
// //
// Implements the structures and methods required for TMD parsing and editing. // Implements the structures and methods required for TMD parsing and editing.
use std::cell::RefCell;
use std::fmt; use std::fmt;
use std::io::{Cursor, Read, Write}; use std::io::{Cursor, Read, Write};
use std::ops::Index; use std::ops::Index;
use std::rc::Rc;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use sha1::{Sha1, Digest}; use sha1::{Sha1, Digest};
use thiserror::Error; use thiserror::Error;
@ -110,7 +112,7 @@ pub struct TMD {
pub num_contents: u16, pub num_contents: u16,
pub boot_index: u16, pub boot_index: u16,
pub minor_version: u16, // Normally unused, but good for fakesigning! pub minor_version: u16, // Normally unused, but good for fakesigning!
pub content_records: Vec<ContentRecord>, pub content_records: Rc<RefCell<Vec<ContentRecord>>>,
} }
impl TMD { impl TMD {
@ -204,7 +206,7 @@ impl TMD {
num_contents, num_contents,
boot_index, boot_index,
minor_version, minor_version,
content_records, content_records: Rc::new(RefCell::new(content_records)),
}) })
} }
@ -231,11 +233,11 @@ impl TMD {
buf.write_all(&self.reserved2)?; buf.write_all(&self.reserved2)?;
buf.write_u32::<BigEndian>(self.access_rights)?; buf.write_u32::<BigEndian>(self.access_rights)?;
buf.write_u16::<BigEndian>(self.title_version)?; buf.write_u16::<BigEndian>(self.title_version)?;
buf.write_u16::<BigEndian>(self.num_contents)?; buf.write_u16::<BigEndian>(self.content_records.borrow().len() as u16)?;
buf.write_u16::<BigEndian>(self.boot_index)?; buf.write_u16::<BigEndian>(self.boot_index)?;
buf.write_u16::<BigEndian>(self.minor_version)?; buf.write_u16::<BigEndian>(self.minor_version)?;
// Iterate over content records and write out content record data. // Iterate over content records and write out content record data.
for content in &self.content_records { for content in self.content_records.borrow().iter() {
buf.write_u32::<BigEndian>(content.content_id)?; buf.write_u32::<BigEndian>(content.content_id)?;
buf.write_u16::<BigEndian>(content.index)?; buf.write_u16::<BigEndian>(content.index)?;
match content.content_type { match content.content_type {
@ -317,11 +319,11 @@ impl TMD {
// Find possible content indices, because the provided one could exist while the indices // Find possible content indices, because the provided one could exist while the indices
// are out of order, which could cause problems finding the content. // are out of order, which could cause problems finding the content.
let mut content_indices = Vec::new(); let mut content_indices = Vec::new();
for record in &self.content_records { for record in self.content_records.borrow().iter() {
content_indices.push(record.index); content_indices.push(record.index);
} }
let target_index = content_indices.index(index); let target_index = content_indices.index(index);
match self.content_records[*target_index as usize].content_type { match self.content_records.borrow()[*target_index as usize].content_type {
ContentType::Normal => ContentType::Normal, ContentType::Normal => ContentType::Normal,
ContentType::Development => ContentType::Development, ContentType::Development => ContentType::Development,
ContentType::HashTree => ContentType::HashTree, ContentType::HashTree => ContentType::HashTree,