Made a bunch of fields that should be private private

This commit is contained in:
2026-02-22 22:21:37 -05:00
parent 94e0be0eef
commit 836d5e912a
40 changed files with 1499 additions and 929 deletions

View File

@@ -1,5 +1,5 @@
// archive/ash.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// archive/ash.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the decompression routines used for the Wii's ASH compression scheme.
// May someday even include the compression routines! If I ever get around to it.

View File

@@ -1,5 +1,5 @@
// archive/lz77.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// archive/lz77.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the compression and decompression routines used for the Wii's LZ77 compression scheme.

View File

@@ -1,5 +1,5 @@
// archive/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// archive/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Root for all archive-related modules.

View File

@@ -1,5 +1,5 @@
// archive/u8.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// archive/u8.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for parsing U8 archives.

View File

@@ -1,8 +1,8 @@
// Sample file for testing rustii library stuff.
use std::fs;
use rustii::title::{wad, cert};
use rustii::title;
use rustwii::title::{wad, cert};
use rustwii::title;
// use rustii::title::content;
fn main() {
@@ -12,19 +12,17 @@ fn main() {
let wad = wad::WAD::from_bytes(&data).unwrap();
println!("size of tmd: {:?}", wad.tmd().len());
println!("num content records: {:?}", title.tmd.content_records.borrow().len());
println!("first record data: {:?}", title.tmd.content_records.borrow().first().unwrap());
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.dec_title_key());
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());
println!("wad header: {:?}", wad.header);
let cert_chain = &title.cert_chain;
println!("cert chain OK");
let result = cert::verify_ca_cert(&cert_chain.ca_cert()).unwrap();

View File

@@ -1,6 +0,0 @@
// archive/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
pub mod ash;
pub mod lz77;
pub mod u8;

View File

@@ -1,5 +0,0 @@
// nand/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
pub mod emunand;
pub mod setting;

View File

@@ -1,6 +0,0 @@
// title/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
pub mod fakesign;
pub mod nus;
pub mod wad;

View File

@@ -1,5 +1,5 @@
// archive/ash.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// archive/ash.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for the ASH decompression command in the rustii CLI.
// Might even have the compression command someday if I ever write the compression code!
@@ -8,7 +8,7 @@ use std::{str, fs};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use clap::Subcommand;
use rustii::archive::ash;
use rustwii::archive::ash;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]

View File

@@ -1,5 +1,5 @@
// archive/lz77.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// archive/lz77.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for the LZ77 compression/decompression commands in the rustii CLI.
@@ -7,7 +7,7 @@ use std::{str, fs};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use clap::Subcommand;
use rustii::archive::lz77;
use rustwii::archive::lz77;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]

View File

@@ -0,0 +1,6 @@
// archive/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
pub mod ash;
pub mod lz77;
pub mod u8;

View File

@@ -1,5 +1,5 @@
// archive/u8.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// archive/u8.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for the U8 packing/unpacking commands in the rustii CLI.
@@ -10,7 +10,7 @@ use std::rc::Rc;
use anyhow::{bail, Context, Result};
use clap::Subcommand;
use glob::glob;
use rustii::archive::u8;
use rustwii::archive::u8;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]

View File

@@ -1,5 +1,5 @@
// filetypes.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// filetypes.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Common code for identifying Wii file types.

View File

@@ -1,5 +1,5 @@
// info.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// info.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for the info command in the rustii CLI.
@@ -8,8 +8,8 @@ use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use anyhow::{bail, Context, Result};
use rustii::archive::u8;
use rustii::{title, title::cert, title::tmd, title::ticket, title::wad, title::versions};
use rustwii::archive::u8;
use rustwii::{title, title::cert, title::tmd, title::ticket, title::wad, title::versions};
use crate::filetypes::{WiiFileType, identify_file_type};
// Avoids duplicated code, since both TMD and Ticket info print the TID in the same way.
@@ -45,15 +45,15 @@ 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())?;
print_title_version(tmd.title_version, tmd.title_id(), tmd.is_vwii())?;
println!(" TMD Version: {}", tmd.tmd_version);
print_title_version(tmd.title_version(), tmd.title_id(), tmd.is_vwii())?;
println!(" TMD Version: {}", tmd.tmd_version());
if hex::encode(tmd.ios_tid()).eq("0000000000000000") {
println!(" Required IOS: N/A");
}
else if hex::encode(tmd.ios_tid()).ne(&format!("{:016X}", tmd.title_version)) {
else if hex::encode(tmd.ios_tid()).ne(&format!("{:016X}", tmd.title_version())) {
println!(" Required IOS: IOS{} ({})", tmd.ios_tid().last().unwrap(), hex::encode(tmd.ios_tid()).to_uppercase());
}
let signature_issuer = String::from_utf8(Vec::from(tmd.signature_issuer)).unwrap_or_default();
let signature_issuer = String::from_utf8(Vec::from(tmd.signature_issuer())).unwrap_or_default();
if signature_issuer.contains("CP00000004") {
println!(" Certificate: CP00000004 (Retail)");
println!(" Certificate Issuer: Root-CA00000001 (Retail)");
@@ -74,7 +74,7 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()>
println!(" Certificate Info: {} (Unknown)", signature_issuer);
}
let region = if hex::encode(tmd.title_id()).eq("0000000100000002") {
match versions::dec_to_standard(tmd.title_version, &hex::encode(tmd.title_id()), Some(tmd.is_vwii != 0))
match versions::dec_to_standard(tmd.title_version(), &hex::encode(tmd.title_id()), Some(tmd.is_vwii() != false))
.unwrap_or_default().chars().last() {
Some('U') => "USA",
Some('E') => "EUR",
@@ -89,7 +89,7 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()>
};
println!(" Region: {}", region);
println!(" Title Type: {}", tmd.title_type()?);
println!(" vWii Title: {}", tmd.is_vwii != 0);
println!(" vWii Title: {}", tmd.is_vwii() != false);
println!(" DVD Video Access: {}", tmd.check_access_right(tmd::AccessRight::DVDVideo));
println!(" AHB Access: {}", tmd.check_access_right(tmd::AccessRight::AHB));
if cert.is_some() {
@@ -117,10 +117,10 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) -> Result<()>
println!(" Fakesigned: {}", tmd.is_fakesigned());
}
println!("\nContent Info");
println!(" Total Contents: {}", tmd.content_records.borrow().len());
println!(" Boot Content Index: {}", tmd.boot_index);
println!(" Total Contents: {}", tmd.content_records().len());
println!(" Boot Content Index: {}", tmd.boot_index());
println!(" Content Records:");
for content in tmd.content_records.borrow().iter() {
for content in tmd.content_records().iter() {
println!(" Content Index: {}", content.index);
println!(" Content ID: {:08X}", content.content_id);
println!(" Content Type: {}", content.content_type);
@@ -134,9 +134,9 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) ->
// Print all important keys from the Ticket.
println!("Ticket Info");
print_tid(ticket.title_id())?;
print_title_version(ticket.title_version, ticket.title_id(), ticket.common_key_index == 2)?;
println!(" Ticket Version: {}", ticket.ticket_version);
let signature_issuer = String::from_utf8(Vec::from(ticket.signature_issuer)).unwrap_or_default();
print_title_version(ticket.title_version(), ticket.title_id(), ticket.common_key_index() == 2)?;
println!(" Ticket Version: {}", ticket.ticket_version());
let signature_issuer = String::from_utf8(Vec::from(ticket.signature_issuer())).unwrap_or_default();
if signature_issuer.contains("XS00000003") {
println!(" Certificate: XS00000003 (Retail)");
println!(" Certificate Issuer: Root-CA00000001 (Retail)");
@@ -149,7 +149,7 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) ->
} else {
println!(" Certificate Info: {} (Unknown)", signature_issuer);
}
let key = match ticket.common_key_index {
let key = match ticket.common_key_index() {
0 => {
if ticket.is_dev() { "Common (Development)" }
else { "Common (Retail)" }
@@ -159,8 +159,8 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) ->
_ => "Unknown (Likely Common)"
};
println!(" Decryption Key: {}", key);
println!(" Title Key (Encrypted): {}", hex::encode(ticket.title_key));
println!(" Title Key (Decrypted): {}", hex::encode(ticket.dec_title_key()));
println!(" Title Key (Encrypted): {}", hex::encode(ticket.title_key()));
println!(" Title Key (Decrypted): {}", hex::encode(ticket.title_key_dec()));
if cert.is_some() {
let signing_str = match cert::verify_ticket(&cert.unwrap(), &ticket) {
Ok(result) => match result {
@@ -190,7 +190,7 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) ->
fn print_wad_info(wad: wad::WAD) -> Result<()> {
println!("WAD Info");
match wad.header.wad_type {
match wad.wad_type() {
wad::WADType::ImportBoot => { println!(" WAD Type: boot2") },
wad::WADType::Installable => { println!(" WAD Type: Standard Installable") },
}

View File

@@ -1,5 +1,5 @@
// main.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// main.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Base for the rustii CLI that handles argument parsing and directs execution to the proper module.

View File

@@ -1,5 +1,5 @@
// nand/emunand.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// nand/emunand.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for EmuNAND-related commands in the rustii CLI.
@@ -8,9 +8,9 @@ use std::path::{absolute, Path};
use anyhow::{bail, Context, Result};
use clap::Subcommand;
use walkdir::WalkDir;
use rustii::nand::{emunand, setting};
use rustii::title::{nus, tmd};
use rustii::title;
use rustwii::nand::{emunand, setting};
use rustwii::title::{nus, tmd};
use rustwii::title;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]
@@ -64,7 +64,7 @@ pub fn info(emunand: &str) -> Result<()> {
match emunand.get_title_tmd([0, 0, 0, 1, 0, 0, 0, 2]) {
Some(tmd) => {
is_vwii = tmd.is_vwii();
println!(" System Menu Version: {}", title::versions::dec_to_standard(tmd.title_version, "0000000100000002", Some(is_vwii)).unwrap());
println!(" System Menu Version: {}", title::versions::dec_to_standard(tmd.title_version(), "0000000100000002", Some(is_vwii)).unwrap());
},
None => {
println!(" System Menu Version: None");
@@ -142,12 +142,12 @@ pub fn info(emunand: &str) -> Result<()> {
println!(" BC-WFS ({})", ios.to_ascii_uppercase());
}
let tmd = emunand.get_title_tmd(hex::decode(ios)?.try_into().unwrap()).unwrap();
println!(" Version: {}", tmd.title_version);
println!(" Version: {}", tmd.title_version());
}
else {
println!(" IOS{} ({})", u32::from_str_radix(&ios[8..16], 16)?, ios.to_ascii_uppercase());
let tmd = emunand.get_title_tmd(hex::decode(ios)?.try_into().unwrap()).unwrap();
println!(" Version: {} ({})", tmd.title_version, title::versions::dec_to_standard(tmd.title_version, ios, None).unwrap());
println!(" Version: {} ({})", tmd.title_version(), title::versions::dec_to_standard(tmd.title_version(), ios, None).unwrap());
}
}
println!();
@@ -168,7 +168,7 @@ pub fn info(emunand: &str) -> Result<()> {
println!(" {}", title.to_uppercase());
}
let tmd = emunand.get_title_tmd(hex::decode(&title)?.try_into().unwrap()).unwrap();
println!(" Version: {}", tmd.title_version);
println!(" Version: {}", tmd.title_version());
let ios_tid = &hex::encode(tmd.ios_tid()).to_ascii_uppercase();
print!(" Required IOS: IOS{} ({})", u32::from_str_radix(&hex::encode(&tmd.ios_tid()[4..8]), 16)?, ios_tid);
if !installed_ioses.contains(ios_tid) {
@@ -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);

View File

@@ -0,0 +1,5 @@
// nand/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
pub mod emunand;
pub mod setting;

View File

@@ -1,5 +1,5 @@
// nand/setting.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// nand/setting.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for setting.txt-related commands in the rustii CLI.
@@ -7,7 +7,7 @@ use std::{str, fs};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use clap::Subcommand;
use rustii::nand::setting;
use rustwii::nand::setting;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]

View File

@@ -1,12 +1,12 @@
// title/fakesign.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/fakesign.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for the fakesign command in the rustii CLI.
use std::{str, fs};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use rustii::{title, title::tmd, title::ticket};
use rustwii::{title, title::tmd, title::ticket};
use crate::filetypes::{WiiFileType, identify_file_type};
pub fn fakesign(input: &str, output: &Option<String>) -> Result<()> {

View File

@@ -0,0 +1,6 @@
// title/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
pub mod fakesign;
pub mod nus;
pub mod wad;

View File

@@ -1,5 +1,5 @@
// title/nus.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/nus.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for NUS-related commands in the rustii CLI.
@@ -8,8 +8,8 @@ use std::path::PathBuf;
use anyhow::{bail, Context, Result};
use clap::{Subcommand, Args};
use sha1::{Sha1, Digest};
use rustii::title::{cert, content, crypto, nus, ticket, tmd};
use rustii::title;
use rustwii::title::{cert, content, crypto, nus, ticket, tmd};
use rustwii::title;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]
@@ -109,11 +109,11 @@ pub fn download_content(tid: &str, cid: &str, version: &Option<u16>, output: &Op
Err(_) => bail!("No Ticket is available for this title! The content cannot be decrypted.")
};
println!(" - Decrypting content...");
let (content_hash, content_size, content_index) = tmd.content_records.borrow().iter()
let (content_hash, content_size, content_index) = tmd.content_records().iter()
.find(|record| record.content_id == cid)
.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.")?;
let mut content_dec = crypto::decrypt_content(&content, tik.dec_title_key(), content_index);
let mut content_dec = crypto::decrypt_content(&content, tik.title_key_dec(), content_index);
content_dec.resize(content_size as usize, 0);
// Verify the content's hash before saving it.
let mut hasher = Sha1::new();
@@ -167,7 +167,7 @@ fn download_title_dir(title: title::Title, output: String) -> Result<()> {
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))?;
// Iterate over the content files and write them out in encrypted form.
for record in title.content.content_records.borrow().iter() {
for record in title.content.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))?;
@@ -192,7 +192,7 @@ 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.borrow().iter() {
for record in content_region.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)?)
.with_context(|| format!("Failed to open content file \"{:08X}\" for writing.", record.content_id))?;
@@ -236,13 +236,13 @@ pub fn download_title(tid: &str, version: &Option<u16>, output: &TitleOutputType
};
// Build a vec of contents by iterating over the content records and downloading each one.
let mut contents: Vec<Vec<u8>> = Vec::new();
for record in tmd.content_records.borrow().iter() {
for record in tmd.content_records().iter() {
println!(" - Downloading content {} of {} (Content ID: {}, Size: {} bytes)...",
record.index + 1, &tmd.content_records.borrow().len(), record.content_id, record.content_size);
record.index + 1, &tmd.content_records().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))?);
println!(" - Done!");
}
let content_region = content::ContentRegion::from_contents(contents, tmd.content_records.clone())?;
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 tik.is_some() {

View File

@@ -1,19 +1,18 @@
// title/wad.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/wad.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for WAD-related commands in the rustii CLI.
use std::{str, fs, fmt};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use anyhow::{bail, Context, Result};
use clap::{Subcommand, Args};
use glob::glob;
use hex::FromHex;
use rand::prelude::*;
use regex::RegexBuilder;
use rustii::title::{cert, crypto, tmd, ticket, content, wad};
use rustii::title;
use rustwii::title::{cert, crypto, tmd, ticket, content, wad};
use rustwii::title;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]
@@ -186,7 +185,7 @@ pub fn add_wad(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.borrow().iter().any(|record| record.content_id == cid) {
if title.content.content_records().iter().any(|record| record.content_id == cid) {
bail!("The specified Content ID \"{:08X}\" is already being used in this WAD!", cid);
}
cid
@@ -196,7 +195,7 @@ pub fn add_wad(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.borrow().iter().any(|record| record.content_id == cid) {
if !title.content.content_records().iter().any(|record| record.content_id == cid) {
break;
}
}
@@ -250,32 +249,32 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
} else {
"retail"
};
let title_key = title.ticket.dec_title_key();
let title_key = title.ticket.title_key_dec();
let title_key_new: [u8; 16];
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.common_key_index = 0;
title.tmd.is_vwii = 0;
title.ticket.set_common_key_index(0);
title.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.common_key_index = 0;
title.tmd.is_vwii = 0;
title.ticket.set_common_key_index(0);
title.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.common_key_index = 2;
title.tmd.is_vwii = 1;
title.ticket.set_common_key_index(2);
title.tmd.set_is_vwii(true);
}
}
title.ticket.title_key = title_key_new;
title.ticket.set_title_key(title_key_new);
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());
@@ -389,15 +388,15 @@ pub fn pack_wad(input: &str, output: &str) -> Result<()> {
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(Rc::clone(&tmd.content_records))?;
let content_indexes: Vec<u16> = tmd.content_records.borrow().iter().map(|record| record.index).collect();
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();
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.dec_title_key())
content_region.set_content(&data, index as usize, None, None, tik.title_key_dec())
.with_context(|| "Failed to load content into the ContentRegion.")?;
}
// Ensure that the TMD is modified with our potentially updated content records.
tmd.content_records = content_region.content_records.clone();
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.")?;
// Write out WAD file.
let mut out_path = PathBuf::from(output);
@@ -525,10 +524,10 @@ pub fn unpack_wad(input: &str, output: &str) -> Result<()> {
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.borrow().len() {
let content_file_name = format!("{:08X}.app", title.content.content_records.borrow()[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.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.borrow()[i].content_id))?;
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))?;
}
println!("Successfully unpacked WAD file to \"{}\"!", out_path.display());
Ok(())

View File

@@ -1,5 +1,5 @@
// lib.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// lib.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Root level module that imports the feature modules.

View File

@@ -1,5 +1,5 @@
// nand/emunand.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// nand/emunand.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for handling Wii EmuNANDs.
@@ -184,9 +184,9 @@ impl EmuNAND {
}
fs::create_dir(&title_dir)?;
fs::write(title_dir.join("title.tmd"), title.tmd.to_bytes()?)?;
for i in 0..title.content.content_records.borrow().len() {
if matches!(title.content.content_records.borrow()[i].content_type, tmd::ContentType::Normal) {
let content_path = title_dir.join(format!("{:08X}.app", title.content.content_records.borrow()[i].content_id).to_ascii_lowercase());
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(content_path, title.get_content_by_index(i)?)?;
}
}
@@ -200,9 +200,9 @@ impl EmuNAND {
} else {
content::SharedContentMap::new()
};
for i in 0..title.content.content_records.borrow().len() {
if matches!(title.content.content_records.borrow()[i].content_type, tmd::ContentType::Shared) {
if let Some(file_name) = content_map.add(&title.content.content_records.borrow()[i].content_hash)? {
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)? {
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)?)?;
}

View File

@@ -1,5 +1,5 @@
// nand/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// nand/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Root for all NAND-related modules.

View File

@@ -1,5 +1,5 @@
// nand/setting.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// nand/setting.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for parsing and editing setting.txt in the Wii
// Menu's data.

View File

@@ -1,5 +1,5 @@
// nand/sys.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// nand/sys.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for parsing and editing files in /sys/ on the
// Wii's NAND.
@@ -27,12 +27,18 @@ pub struct UidSys {
entries: Vec<UidSysEntry>,
}
impl Default for UidSys {
fn default() -> Self {
Self::new()
}
}
impl UidSys {
/// Creates a new UidSys instance from the binary data of a uid.sys file.
pub fn from_bytes(data: &[u8]) -> Result<Self, UidSysError> {
// The uid.sys file must be divisible by a multiple of 12, or something is wrong, since each
// entry is 12 bytes long.
if (data.len() % 12) != 0 {
if !data.len().is_multiple_of(12) {
return Err(UidSysError::InvalidUidSysLength);
}
let entry_count = data.len() / 12;

View File

@@ -1,5 +1,5 @@
// title/cert.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/cert.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for validating the signatures of Wii titles.
@@ -37,8 +37,8 @@ pub enum CertificateKeyType {
ECC
}
#[derive(Debug, Clone)]
/// A structure that represents the components of a Wii signing certificate.
#[derive(Debug, Clone)]
pub struct Certificate {
signer_key_type: CertificateKeyType,
signature: Vec<u8>,
@@ -165,8 +165,8 @@ impl Certificate {
}
}
#[derive(Debug)]
/// A structure that represents the components of the Wii's signing certificate chain.
#[derive(Debug)]
pub struct CertificateChain {
ca_cert: Certificate,
tmd_cert: Certificate,
@@ -346,7 +346,7 @@ pub fn verify_tmd(tmd_cert: &Certificate, tmd: &tmd::TMD) -> Result<bool, Certif
let public_key_modulus = BigUint::from_bytes_be(&tmd_cert.pub_key_modulus());
let public_key_exponent = BigUint::from(tmd_cert.pub_key_exponent());
let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap();
match root_key.verify(Pkcs1v15Sign::new::<Sha1>(), &tmd_hash, tmd.signature.as_slice()) {
match root_key.verify(Pkcs1v15Sign::new::<Sha1>(), &tmd_hash, tmd.signature().as_slice()) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
@@ -368,7 +368,7 @@ pub fn verify_ticket(ticket_cert: &Certificate, ticket: &ticket::Ticket) -> Resu
let public_key_modulus = BigUint::from_bytes_be(&ticket_cert.pub_key_modulus());
let public_key_exponent = BigUint::from(ticket_cert.pub_key_exponent());
let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap();
match root_key.verify(Pkcs1v15Sign::new::<Sha1>(), &ticket_hash, ticket.signature.as_slice()) {
match root_key.verify(Pkcs1v15Sign::new::<Sha1>(), &ticket_hash, ticket.signature().as_slice()) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}

View File

@@ -1,5 +1,5 @@
// title/commonkeys.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/commonkeys.rs from rustwii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
const COMMON_KEY: &str = "ebe42a225e8593e448d9c5457381aaf7";
const KOREAN_KEY: &str = "63b82bb4f4614e2e13f2fefbba4c9b7e";

View File

@@ -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?

View File

@@ -1,5 +1,5 @@
// title/crypto.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/crypto.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the common crypto functions required to handle Wii content encryption.

View File

@@ -1,5 +1,5 @@
// title/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Root for all title-related modules and implementation of the high-level Title object.
@@ -13,7 +13,6 @@ pub mod tmd;
pub mod versions;
pub mod wad;
use std::rc::Rc;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -53,7 +52,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(), Rc::clone(&tmd.content_records)).map_err(TitleError::Content)?;
let content = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records().clone()).map_err(TitleError::Content)?;
Ok(Title {
cert_chain,
crl: wad.crl(),
@@ -123,13 +122,13 @@ impl Title {
/// 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.dec_title_key())?;
let content = self.content.get_content_by_index(index, self.ticket.title_key_dec())?;
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<Vec<u8>, content::ContentError> {
let content = self.content.get_content_by_cid(cid, self.ticket.dec_title_key())?;
let content = self.content.get_content_by_cid(cid, self.ticket.title_key_dec())?;
Ok(content)
}
@@ -137,7 +136,7 @@ impl Title {
/// 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.dec_title_key())?;
self.content.set_content(content, index, cid, content_type, self.ticket.title_key_dec())?;
Ok(())
}
@@ -146,7 +145,7 @@ 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.dec_title_key())?;
self.content.add_content(content, cid, content_type, self.ticket.title_key_dec())?;
Ok(())
}
@@ -158,7 +157,7 @@ impl Title {
// 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.borrow().iter() {
for record in self.tmd.content_records().iter() {
if matches!(record.content_type, tmd::ContentType::Shared) {
if absolute == Some(true) {
title_size += record.content_size as usize;

View File

@@ -1,5 +1,5 @@
// title/nus.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/nus.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the functions required for downloading data from the NUS.
@@ -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.
pub fn download_contents(tmd: &tmd::TMD, wiiu_endpoint: bool) -> Result<Vec<Vec<u8>>, NUSError> {
let content_ids: Vec<u32> = tmd.content_records.borrow().iter().map(|record| { record.content_id }).collect();
let content_ids: Vec<u32> = tmd.content_records().iter().map(|record| { record.content_id }).collect();
let mut contents: Vec<Vec<u8>> = Vec::new();
for id in content_ids {
contents.push(download_content(tmd.title_id(), id, wiiu_endpoint)?);
@@ -112,7 +112,7 @@ 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 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)?;
Ok(title)
}

View File

@@ -1,5 +1,5 @@
// title/tik.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/tik.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for Ticket parsing and editing.
@@ -33,28 +33,28 @@ pub struct TitleLimit {
#[derive(Debug)]
/// A structure that represents a Wii Ticket file.
pub struct Ticket {
pub signature_type: u32,
pub signature: [u8; 256],
signature_type: u32,
signature: [u8; 256],
padding1: [u8; 60],
pub signature_issuer: [u8; 64],
pub ecdh_data: [u8; 60],
pub ticket_version: u8,
signature_issuer: [u8; 64],
ecdh_data: [u8; 60],
ticket_version: u8,
reserved1: [u8; 2],
pub title_key: [u8; 16],
title_key: [u8; 16],
unknown1: [u8; 1],
pub ticket_id: [u8; 8],
pub console_id: [u8; 4],
ticket_id: [u8; 8],
console_id: [u8; 4],
title_id: [u8; 8],
unknown2: [u8; 2],
pub title_version: u16,
pub permitted_titles_mask: [u8; 4],
pub permit_mask: [u8; 4],
pub title_export_allowed: u8,
pub common_key_index: u8,
title_version: u16,
permitted_titles_mask: [u8; 4],
permit_mask: [u8; 4],
title_export_allowed: u8,
common_key_index: u8,
unknown3: [u8; 48],
pub content_access_permission: [u8; 64],
content_access_permission: [u8; 64],
padding2: [u8; 2],
pub title_limits: [TitleLimit; 8],
title_limits: [TitleLimit; 8],
}
impl Ticket {
@@ -169,8 +169,87 @@ impl Ticket {
Ok(buf)
}
/// Gets the type of the signature on the Ticket.
pub fn signature_type(&self) -> u32 {
self.signature_type
}
/// Gets the signature of the Ticket.
pub fn signature(&self) -> [u8; 256] {
self.signature
}
/// Gets the ECDH data listed in the Ticket.
pub fn ecdh_data(&self) -> [u8; 60] {
self.ecdh_data
}
/// Gets the version of the Ticket file.
pub fn ticket_version(&self) -> u8 {
self.ticket_version
}
/// Gets the raw encrypted Title Key from the Ticket.
pub fn title_key(&self) -> [u8; 16] {
self.title_key
}
pub fn set_title_key(&mut self, title_key: [u8; 16]) {
self.title_key = title_key;
}
/// Gets the Ticket ID listed in the Ticket.
pub fn ticket_id(&self) -> [u8; 8] {
self.ticket_id
}
/// Gets the console ID listed in the Ticket.
pub fn console_id(&self) -> [u8; 4] {
self.console_id
}
/// Gets the version of the title listed in the Ticket.
pub fn title_version(&self) -> u16 {
self.title_version
}
/// Gets the permitted titles mask listed in the Ticket.
pub fn permitted_titles_mask(&self) -> [u8; 4] {
self.permitted_titles_mask
}
/// Gets the permit mask listed in the Ticket.
pub fn permit_mask(&self) -> [u8; 4] {
self.permit_mask
}
/// Gets whether title export is allowed by the Ticket.
pub fn title_export_allowed(&self) -> bool {
self.title_export_allowed == 1
}
/// Gets the index of the common key used by the Ticket.
pub fn common_key_index(&self) -> u8 {
self.common_key_index
}
/// Sets the index of the common key used by the Ticket.
pub fn set_common_key_index(&mut self, index: u8) {
self.common_key_index = index;
}
/// Gets the content access permissions listed in the Ticket.
pub fn content_access_permission(&self) -> [u8; 64] {
self.content_access_permission
}
/// Gets the title usage limits listed in the Ticket.
pub fn title_limits(&self) -> [TitleLimit; 8] {
self.title_limits
}
/// Gets the decrypted version of the Title Key stored in a Ticket.
pub fn dec_title_key(&self) -> [u8; 16] {
pub fn title_key_dec(&self) -> [u8; 16] {
// Get the dev status of this Ticket so decrypt_title_key knows the right common key.
let is_dev = self.is_dev();
decrypt_title_key(self.title_key, self.common_key_index, self.title_id, is_dev)
@@ -242,7 +321,7 @@ impl Ticket {
/// Sets a new Title ID for the Ticket. This will re-encrypt the Title Key, since the Title ID
/// is used as the IV for decrypting the Title Key.
pub fn set_title_id(&mut self, title_id: [u8; 8]) -> Result<(), TicketError> {
let new_enc_title_key = crypto::encrypt_title_key(self.dec_title_key(), self.common_key_index, title_id, self.is_dev());
let new_enc_title_key = crypto::encrypt_title_key(self.title_key_dec(), self.common_key_index, title_id, self.is_dev());
self.title_key = new_enc_title_key;
self.title_id = title_id;
Ok(())

View File

@@ -1,13 +1,11 @@
// title/tmd.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/tmd.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for TMD parsing and editing.
use std::cell::RefCell;
use std::fmt;
use std::io::{Cursor, Read, Write};
use std::ops::Index;
use std::rc::Rc;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use sha1::{Sha1, Digest};
use thiserror::Error;
@@ -81,8 +79,8 @@ pub enum AccessRight {
DVDVideo = 1,
}
#[derive(Debug, Clone)]
/// A structure that represents the metadata of a content file in a digital Wii title.
#[derive(Debug, Clone)]
pub struct ContentRecord {
pub content_id: u32,
pub index: u16,
@@ -91,33 +89,33 @@ pub struct ContentRecord {
pub content_hash: [u8; 20],
}
#[derive(Debug)]
/// A structure that represents a Wii TMD (Title Metadata) file.
#[derive(Debug)]
pub struct TMD {
pub signature_type: u32,
pub signature: [u8; 256],
signature_type: u32,
signature: [u8; 256],
padding1: [u8; 60],
pub signature_issuer: [u8; 64],
pub tmd_version: u8,
pub ca_crl_version: u8,
pub signer_crl_version: u8,
pub is_vwii: u8,
signature_issuer: [u8; 64],
tmd_version: u8,
ca_crl_version: u8,
signer_crl_version: u8,
is_vwii: u8,
ios_tid: [u8; 8],
title_id: [u8; 8],
title_type: [u8; 4],
pub group_id: u16,
group_id: u16,
padding2: [u8; 2],
region: u16,
pub ratings: [u8; 16],
ratings: [u8; 16],
reserved1: [u8; 12],
pub ipc_mask: [u8; 12],
ipc_mask: [u8; 12],
reserved2: [u8; 18],
pub access_rights: u32,
pub title_version: u16,
pub num_contents: u16,
pub boot_index: u16,
pub minor_version: u16, // Normally unused, but good for fakesigning!
pub content_records: Rc<RefCell<Vec<ContentRecord>>>,
access_rights: u32,
title_version: u16,
num_contents: u16,
boot_index: u16,
minor_version: u16, // Normally unused, but useful when fakesigning.
content_records: Vec<ContentRecord>,
}
impl TMD {
@@ -211,7 +209,7 @@ impl TMD {
num_contents,
boot_index,
minor_version,
content_records: Rc::new(RefCell::new(content_records)),
content_records,
})
}
@@ -238,11 +236,11 @@ impl TMD {
buf.write_all(&self.reserved2)?;
buf.write_u32::<BigEndian>(self.access_rights)?;
buf.write_u16::<BigEndian>(self.title_version)?;
buf.write_u16::<BigEndian>(self.content_records.borrow().len() as u16)?;
buf.write_u16::<BigEndian>(self.content_records.len() as u16)?;
buf.write_u16::<BigEndian>(self.boot_index)?;
buf.write_u16::<BigEndian>(self.minor_version)?;
// Iterate over content records and write out content record data.
for content in self.content_records.borrow().iter() {
for content in self.content_records.iter() {
buf.write_u32::<BigEndian>(content.content_id)?;
buf.write_u16::<BigEndian>(content.index)?;
match content.content_type {
@@ -258,6 +256,76 @@ impl TMD {
Ok(buf)
}
/// Gets the type of the signature on the TMD.
pub fn signature_type(&self) -> u32 {
self.signature_type
}
/// Gets the signature of the TMD.
pub fn signature(&self) -> [u8; 256] {
self.signature
}
/// Gets the version of the TMD file.
pub fn tmd_version(&self) -> u8 {
self.tmd_version
}
/// Gets the version of CA CRL listed in the TMD.
pub fn ca_crl_version(&self) -> u8 {
self.ca_crl_version
}
/// Gets the version of the signer CRL listed in the TMD.
pub fn signer_crl_version(&self) -> u8 {
self.signer_crl_version
}
/// Gets the group ID listed in the TMD.
pub fn group_id(&self) -> u16 {
self.group_id
}
/// Gets the age ratings listed in the TMD.
pub fn ratings(&self) -> [u8; 16] {
self.ratings
}
/// Gets the ipc mask listed in the TMD.
pub fn ipc_mask(&self) -> [u8; 12] {
self.ipc_mask
}
/// Gets the version of title listed in the TMD.
pub fn title_version(&self) -> u16 {
self.title_version
}
/// Gets the number of contents listed in the TMD.
pub fn num_contents(&self) -> u16 {
self.num_contents
}
/// Gets the index of the title's boot content.
pub fn boot_index(&self) -> u16 {
self.boot_index
}
/// Gets the minor version listed in the TMD. This field is typically unused.
pub fn minor_version(&self) -> u16 {
self.minor_version
}
/// Gets a reference to the content records from the TMD.
pub fn content_records(&self) -> &Vec<ContentRecord> {
&self.content_records
}
/// Sets the content records in the TMD.
pub fn set_content_records(&mut self, content_records: &Vec<ContentRecord>) {
self.content_records = content_records.clone()
}
/// Gets whether a TMD is fakesigned using the strncmp (trucha) bug or not.
pub fn is_fakesigned(&self) -> bool {
// Can't be fakesigned without a null signature.
@@ -331,11 +399,11 @@ impl TMD {
// Find possible content indices, because the provided one could exist while the indices
// are out of order, which could cause problems finding the content.
let mut content_indices = Vec::new();
for record in self.content_records.borrow().iter() {
for record in self.content_records.iter() {
content_indices.push(record.index);
}
let target_index = content_indices.index(index);
match self.content_records.borrow()[*target_index as usize].content_type {
match self.content_records[*target_index as usize].content_type {
ContentType::Normal => ContentType::Normal,
ContentType::Development => ContentType::Development,
ContentType::HashTree => ContentType::HashTree,
@@ -365,10 +433,15 @@ impl TMD {
Ok(())
}
/// Gets whether a TMD describes a vWii title or not.
/// Gets whether a TMD describes a vWii title.
pub fn is_vwii(&self) -> bool {
self.is_vwii == 1
}
/// Sets whether a TMD describes a vWii title.
pub fn set_is_vwii(&mut self, value: bool) {
self.is_vwii = value as u8;
}
/// Gets the Title ID of a TMD.
pub fn title_id(&self) -> [u8; 8] {

View File

@@ -1,5 +1,5 @@
// title/versions.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/versions.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Handles converting Title version formats, and provides Wii Menu version constants.

View File

@@ -1,5 +1,5 @@
// title/wad.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/wad.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for WAD parsing and editing.
@@ -32,16 +32,16 @@ pub enum WADType {
#[derive(Debug)]
/// A structure that represents an entire WAD file as a separate header and body.
pub struct WAD {
pub header: WADHeader,
pub body: WADBody,
header: WADHeader,
body: WADBody,
}
#[derive(Debug)]
/// A structure that represents the header of a WAD file.
pub struct WADHeader {
pub header_size: u32,
pub wad_type: WADType,
pub wad_version: u16,
header_size: u32,
wad_type: WADType,
wad_version: u16,
cert_chain_size: u32,
crl_size: u32,
ticket_size: u32,
@@ -93,6 +93,51 @@ impl WADHeader {
};
Ok(header)
}
/// Gets the size of the header data.
pub fn header_size(&self) -> u32 {
self.header_size
}
/// Gets the type of WAD described by the header.
pub fn wad_type(&self) -> &WADType {
&self.wad_type
}
/// Gets the version of the WAD described by the header.
pub fn wad_version(&self) -> u16 {
self.wad_version
}
/// Gets the size of the certificate chain defined in the header.
pub fn cert_chain_size(&self) -> u32 {
self.cert_chain_size
}
/// Gets the size of the CRL defined in the header.
pub fn crl_size(&self) -> u32 {
self.crl_size
}
/// Gets the size of the Ticket defined in the header.
pub fn ticket_size(&self) -> u32 {
self.ticket_size
}
/// Gets the size of the TMD defined in the header.
pub fn tmd_size(&self) -> u32 {
self.tmd_size
}
/// Gets the size of the content defined in the header.
pub fn content_size(&self) -> u32 {
self.content_size
}
/// Gets the size of the metadata defined in the header.
pub fn meta_size(&self) -> u32 {
self.meta_size
}
}
impl WADBody {
@@ -236,6 +281,11 @@ impl WAD {
buf.resize((buf.len() + 63) & !63, 0);
Ok(buf)
}
/// Gets the type of the WAD.
pub fn wad_type(&self) -> &WADType {
self.header.wad_type()
}
pub fn cert_chain_size(&self) -> u32 { self.header.cert_chain_size }