mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2026-03-03 03:15:28 -05:00
Made a bunch of fields that should be private private
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
// title/content.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
// title/content.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustwii
|
||||
//
|
||||
// Implements content parsing and editing.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::rc::Rc;
|
||||
use sha1::{Sha1, Digest};
|
||||
use thiserror::Error;
|
||||
use crate::title::tmd::{ContentRecord, ContentType};
|
||||
@@ -37,39 +35,39 @@ pub enum ContentError {
|
||||
#[derive(Debug)]
|
||||
/// A structure that represents the block of data containing the content of a digital Wii title.
|
||||
pub struct ContentRegion {
|
||||
pub content_records: Rc<RefCell<Vec<ContentRecord>>>,
|
||||
pub content_region_size: u32,
|
||||
pub content_start_offsets: Vec<u64>,
|
||||
pub contents: Vec<Vec<u8>>,
|
||||
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: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
|
||||
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.borrow().len() as u16;
|
||||
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.borrow().iter().scan(0, |offset, record| {
|
||||
.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.borrow().len()).collect(); // Trims the extra final entry.
|
||||
})).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.borrow()[i as usize].content_size + 15) & !15;
|
||||
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: Rc::clone(&content_records),
|
||||
content_records,
|
||||
content_region_size,
|
||||
content_start_offsets,
|
||||
contents,
|
||||
@@ -78,13 +76,13 @@ impl ContentRegion {
|
||||
|
||||
/// 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: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
|
||||
if contents.len() != content_records.borrow().len() {
|
||||
return Err(ContentError::MissingContents { required: content_records.borrow().len(), found: contents.len()});
|
||||
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(Rc::clone(&content_records))?;
|
||||
let mut content_region = Self::new(content_records)?;
|
||||
for i in 0..contents.len() {
|
||||
let target_index = content_region.content_records.borrow()[i].index;
|
||||
let target_index = content_region.content_records[i].index;
|
||||
content_region.load_enc_content(&contents[i], target_index as usize)?;
|
||||
}
|
||||
Ok(content_region)
|
||||
@@ -92,14 +90,14 @@ impl ContentRegion {
|
||||
|
||||
/// Creates a ContentRegion instance from the ContentRecords of a TMD that contains no actual
|
||||
/// content. This can be used to load existing content from files.
|
||||
pub fn new(content_records: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
|
||||
let content_region_size: u64 = content_records.borrow().iter().map(|x| (x.content_size + 63) & !63).sum();
|
||||
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.borrow().len() as u16;
|
||||
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: Rc::clone(&content_records),
|
||||
content_records,
|
||||
content_region_size,
|
||||
content_start_offsets,
|
||||
contents,
|
||||
@@ -109,7 +107,7 @@ impl ContentRegion {
|
||||
/// 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.borrow().len() {
|
||||
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);
|
||||
@@ -117,12 +115,32 @@ impl ContentRegion {
|
||||
}
|
||||
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.borrow().iter()
|
||||
let content_index = self.content_records.iter()
|
||||
.find(|record| record.content_id == cid)
|
||||
.map(|record| record.index);
|
||||
if let Some(index) = content_index {
|
||||
@@ -134,7 +152,7 @@ impl ContentRegion {
|
||||
|
||||
/// 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.borrow().len() - 1 })?;
|
||||
let content = self.contents.get(index).ok_or(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 })?;
|
||||
Ok(content.clone())
|
||||
}
|
||||
|
||||
@@ -142,20 +160,20 @@ impl ContentRegion {
|
||||
pub fn get_content_by_index(&self, index: usize, title_key: [u8; 16]) -> Result<Vec<u8>, ContentError> {
|
||||
let content = self.get_enc_content_by_index(index)?;
|
||||
// Verify the hash of the decrypted content against its record.
|
||||
let mut content_dec = crypto::decrypt_content(&content, title_key, self.content_records.borrow()[index].index);
|
||||
content_dec.resize(self.content_records.borrow()[index].content_size as usize, 0);
|
||||
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.borrow()[index].content_hash {
|
||||
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records.borrow()[index].content_hash) });
|
||||
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.borrow().iter().position(|x| x.content_id == cid);
|
||||
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)
|
||||
@@ -166,7 +184,7 @@ impl ContentRegion {
|
||||
|
||||
/// 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.borrow().iter().position(|x| x.content_id == cid);
|
||||
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)
|
||||
@@ -178,8 +196,8 @@ impl ContentRegion {
|
||||
/// 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.borrow().len() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
|
||||
if index >= self.content_records.len() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
||||
}
|
||||
self.contents[index] = content.to_vec();
|
||||
Ok(())
|
||||
@@ -190,20 +208,20 @@ impl ContentRegion {
|
||||
/// 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.borrow().len() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
|
||||
if index >= self.content_records.len() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
||||
}
|
||||
self.content_records.borrow_mut()[index].content_size = content_size;
|
||||
self.content_records.borrow_mut()[index].content_hash = content_hash;
|
||||
self.content_records[index].content_size = content_size;
|
||||
self.content_records[index].content_hash = content_hash;
|
||||
if cid.is_some() {
|
||||
// Make sure that the new CID isn't already in use.
|
||||
if self.content_records.borrow().iter().any(|record| record.content_id == cid.unwrap()) {
|
||||
if self.content_records.iter().any(|record| record.content_id == cid.unwrap()) {
|
||||
return Err(ContentError::CIDAlreadyExists(cid.unwrap()));
|
||||
}
|
||||
self.content_records.borrow_mut()[index].content_id = cid.unwrap();
|
||||
self.content_records[index].content_id = cid.unwrap();
|
||||
}
|
||||
if content_type.is_some() {
|
||||
self.content_records.borrow_mut()[index].content_type = content_type.unwrap();
|
||||
self.content_records[index].content_type = content_type.unwrap();
|
||||
}
|
||||
self.contents[index] = content.to_vec();
|
||||
Ok(())
|
||||
@@ -213,18 +231,18 @@ impl ContentRegion {
|
||||
/// 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.borrow().len() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
|
||||
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.borrow()[index].content_hash {
|
||||
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records.borrow()[index].content_hash) });
|
||||
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.borrow()[index].index, self.content_records.borrow()[index].content_size);
|
||||
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(())
|
||||
}
|
||||
@@ -247,11 +265,11 @@ impl ContentRegion {
|
||||
/// 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.borrow().get(index).is_none() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
|
||||
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.borrow_mut().remove(index);
|
||||
self.content_records.remove(index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -259,14 +277,14 @@ impl ContentRegion {
|
||||
/// 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.borrow().iter().any(|record| record.index == index) {
|
||||
if self.content_records.iter().any(|record| record.index == index) {
|
||||
return Err(ContentError::IndexAlreadyExists(index));
|
||||
}
|
||||
if self.content_records.borrow().iter().any(|record| record.content_id == cid) {
|
||||
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.borrow_mut().push(ContentRecord { content_id: cid, index, content_type, content_size, content_hash });
|
||||
self.content_records.push(ContentRecord { content_id: cid, index, content_type, content_size, content_hash });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -275,7 +293,7 @@ impl ContentRegion {
|
||||
/// 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.borrow().iter()
|
||||
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?
|
||||
|
||||
Reference in New Issue
Block a user