// title/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustii // // Root for all title-related modules and implementation of the high-level Title object. pub mod cert; pub mod commonkeys; pub mod content; pub mod crypto; pub mod nus; pub mod ticket; pub mod tmd; pub mod versions; pub mod wad; use thiserror::Error; #[derive(Debug, Error)] pub enum TitleError { #[error("the data for required Title component `{0}` was invalid")] InvalidData(String), #[error("WAD data is not in a valid format")] InvalidWAD, #[error("certificate processing error")] CertificateError(#[from] cert::CertificateError), #[error("TMD processing error")] 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), } #[derive(Debug)] /// A structure that represents the components of a digital Wii title. pub struct Title { pub cert_chain: cert::CertificateChain, crl: Vec, pub ticket: ticket::Ticket, pub tmd: tmd::TMD, pub content: content::ContentRegion, meta: Vec } impl Title { /// Creates a new Title instance from an existing WAD instance. pub fn from_wad(wad: &wad::WAD) -> Result { 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)?; Ok(Title { cert_chain, crl: wad.crl(), ticket, tmd, content, meta: wad.meta(), }) } /// 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 { // 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 { Some(crl) => crl.to_vec(), None => Vec::new() }; let meta = match meta { Some(meta) => meta.to_vec(), None => Vec::new() }; Ok(Title { cert_chain, crl, ticket, tmd, content, meta }) } /// 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 { // 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, &self.meta ).map_err(TitleError::WAD)?; Ok(wad) } /// Creates a new Title instance from the binary data of a WAD file. pub fn from_bytes(bytes: &[u8]) -> Result { let wad = wad::WAD::from_bytes(bytes).map_err(|_| TitleError::InvalidWAD)?; let title = Title::from_wad(&wad)?; Ok(title) } /// 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() } /// Fakesigns the TMD and Ticket of a Title. pub fn fakesign(&mut self) -> Result<(), TitleError> { // Run the fakesign methods on the TMD and Ticket. self.tmd.fakesign().map_err(TitleError::TMD)?; self.ticket.fakesign().map_err(TitleError::Ticket)?; Ok(()) } /// Gets the decrypted content file from the Title at the specified index. pub fn get_content_by_index(&self, index: usize) -> Result, content::ContentError> { let content = self.content.get_content_by_index(index, self.ticket.dec_title_key())?; Ok(content) } /// Gets the decrypted content file from the Title with the specified Content ID. pub fn get_content_by_cid(&self, cid: u32) -> Result, content::ContentError> { let content = self.content.get_content_by_cid(cid, self.ticket.dec_title_key())?; Ok(content) } /// Gets the installed size of the title, in bytes. Use the optional parameter "absolute" to set /// whether shared content should be included in this total or not. pub fn title_size(&self, absolute: Option) -> Result { let mut title_size: usize = 0; // Get the TMD and Ticket size by dumping them and measuring their length for the most // accurate results. 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(); for record in &self.tmd.content_records { if matches!(record.content_type, tmd::ContentType::Shared) { if absolute == Some(true) { title_size += record.content_size as usize; } } else { title_size += record.content_size as usize; } } Ok(title_size) } /// Gets the installed size of the title, in blocks. Use the optional parameter "absolute" to /// set whether shared content should be included in this total or not. pub fn title_size_blocks(&self, absolute: Option) -> Result { let title_size_bytes = self.title_size(absolute)?; Ok((title_size_bytes as f64 / 131072.0).ceil() as usize) } /// Verifies entire certificate chain, and then the TMD and Ticket. Returns true if the title /// is entirely valid, or false if any component of the verification fails. pub fn verify(&self) -> Result { if !cert::verify_ca_cert(&self.cert_chain.ca_cert()).map_err(TitleError::CertificateError)? { return Ok(false) } if !cert::verify_child_cert(&self.cert_chain.ca_cert(), &self.cert_chain.tmd_cert()).map_err(TitleError::CertificateError)? || !cert::verify_child_cert(&self.cert_chain.ca_cert(), &self.cert_chain.ticket_cert()).map_err(TitleError::CertificateError)? { return Ok(false) } if !cert::verify_tmd(&self.cert_chain.tmd_cert(), &self.tmd).map_err(TitleError::CertificateError)? || !cert::verify_ticket(&self.cert_chain.ticket_cert(), &self.ticket).map_err(TitleError::CertificateError)? { return Ok(false) } Ok(true) } pub fn set_cert_chain(&mut self, cert_chain: cert::CertificateChain) { self.cert_chain = cert_chain; } pub fn crl(&self) -> Vec { self.crl.clone() } pub fn set_crl(&mut self, crl: &[u8]) { self.crl = crl.to_vec(); } pub fn set_ticket(&mut self, ticket: ticket::Ticket) { self.ticket = ticket; } pub fn set_tmd(&mut self, tmd: tmd::TMD) { self.tmd = tmd; } pub fn set_content(&mut self, content: content::ContentRegion) { self.content = content; } pub fn meta(&self) -> Vec { self.meta.clone() } pub fn set_meta(&mut self, meta: &[u8]) { self.meta = meta.to_vec(); } }