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

1642
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,18 @@
[package] [package]
name = "rustii" name = "rustwii"
authors = ["NinjaCheetah <ninjacheetah@ncxprogramming.com>"] authors = ["NinjaCheetah <ninjacheetah@ncxprogramming.com>"]
license = "MIT" license = "MIT"
description = "A Rust library and CLI for handling files and formats used by the Wii" description = "A Rust library and CLI for handling files and formats used by the Wii"
version = "0.1.0" version = "0.1.0"
readme = "README.md" readme = "README.md"
homepage = "https://github.com/NinjaCheetah/rustii" homepage = "https://github.com/NinjaCheetah/rustwii"
repository = "https://github.com/NinjaCheetah/rustii" repository = "https://github.com/NinjaCheetah/rustwii"
edition = "2024" edition = "2024"
default-run = "rustii" default-run = "rustwii"
[[bin]] [[bin]]
name = "rustii" name = "rustwii"
path = "src/bin/rustii/main.rs" path = "src/bin/rustwii/main.rs"
[[bin]] [[bin]]
name = "playground" name = "playground"

View File

@@ -1,13 +1,13 @@
![rustii-banner](https://github.com/user-attachments/assets/08a7eea1-837e-4bce-939e-13c720b35226) ![rustwii-banner](https://github.com/user-attachments/assets/08a7eea1-837e-4bce-939e-13c720b35226)
# rustii # rustwii
*Like rusty but it's rustii because the Wii? Get it?* *Like rusty but it's rustwii because the Wii? Get it?*
[![Build rustii](https://github.com/NinjaCheetah/rustii/actions/workflows/rust.yml/badge.svg)](https://github.com/NinjaCheetah/rustii/actions/workflows/rust.yml) [![Build rustwii](https://github.com/NinjaCheetah/rustwii/actions/workflows/rust.yml/badge.svg)](https://github.com/NinjaCheetah/rustwii/actions/workflows/rust.yml)
rustii is a library and command line tool written in Rust for handling the various files and formats found on the Wii. rustii is a port of my other library, [libWiiPy](https://github.com/NinjaCheetah/libWiiPy), which aims to accomplish the same goal in Python. At this point, rustii should not be considered stable, however it offers most of the same core functionality as libWiiPy, and the rustii CLI offers most of the same features as WiiPy. You can check which features are available and ready for use in both the library and the CLI below. The goal is for rustii and libWiiPy to eventually have feature parity, with the rustii CLI acting as a drop-in replacement for the (comparatively much less efficient) [WiiPy](https://github.com/NinjaCheetah/WiiPy) CLI. rustwii is a library and command line tool written in Rust for handling the various files and formats found on the Wii. rustwii is a port of my other library, [libWiiPy](https://github.com/NinjaCheetah/libWiiPy), which aims to accomplish the same goal in Python. At this point, rustwii should not be considered stable, however it offers most of the same core functionality as libWiiPy, and the rustwii CLI offers most of the same features as WiiPy. You can check which features are available and ready for use in both the library and the CLI below. The goal is for rustwii and libWiiPy to eventually have feature parity, with the rustwii CLI acting as a drop-in replacement for the (comparatively much less efficient) [WiiPy](https://github.com/NinjaCheetah/WiiPy) CLI.
There is currently no public documentation for rustii, as I'm putting that off until I reach feature parity with libWiiPy so that the APIs are an equal level of stable. You can, however, reference the doc strings present on many of the structs and functions, and build them into basic documentation yourself (using `cargo doc --no-deps`). The [libWiiPy API docs](https://docs.ninjacheetah.dev) may also be helpful in some cases. There is currently no public documentation for rustwii, as I'm putting that off until I reach feature parity with libWiiPy so that the APIs are an equal level of stable. You can, however, reference the doc strings present on many of the structs and functions, and build them into basic documentation yourself (using `cargo doc --no-deps`). The [libWiiPy API docs](https://docs.ninjacheetah.dev) may also be helpful in some cases.
I'm still very new to Rust, so pardon any messy code or confusing API decisions you may find. libWiiPy started off like that, too. I'm still very new to Rust, so pardon any messy code or confusing API decisions you may find. libWiiPy started off like that, too.
@@ -31,13 +31,13 @@ I'm still very new to Rust, so pardon any messy code or confusing API decisions
- Info command for WADs/TMDs/Tickets/U8 archives - Info command for WADs/TMDs/Tickets/U8 archives
- U8 archive packing/unpacking - U8 archive packing/unpacking
To see specific usage information, check `rustii --help` and `rustii <command> --help`. To see specific usage information, check `rustwii --help` and `rustwii <command> --help`.
## Building ## Building
rustii is a standard Rust crate. You'll need to have [Rust installed](https://www.rust-lang.org/learn/get-started), and then you can simply run: rustwii is a standard Rust crate. You'll need to have [Rust installed](https://www.rust-lang.org/learn/get-started), and then you can simply run:
``` ```
cargo build --release cargo build --release
``` ```
to compile the rustii library and CLI. The CLI can then be found at `target/release/rustii(.exe)`. to compile the rustwii library and CLI. The CLI can then be found at `target/release/rustwii(.exe)`.
You can also download the latest nightly build from [GitHub Actions](https://github.com/NinjaCheetah/rustii/actions). You can also download the latest nightly build from [GitHub Actions](https://github.com/NinjaCheetah/rustwii/actions).

View File

@@ -1,5 +1,5 @@
// archive/ash.rs from rustii (c) 2025 NinjaCheetah & Contributors // archive/ash.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Implements the decompression routines used for the Wii's ASH compression scheme. // 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. // 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 // archive/lz77.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Implements the compression and decompression routines used for the Wii's LZ77 compression scheme. // 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 // archive/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Root for all archive-related modules. // Root for all archive-related modules.

View File

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

View File

@@ -1,8 +1,8 @@
// Sample file for testing rustii library stuff. // Sample file for testing rustii library stuff.
use std::fs; use std::fs;
use rustii::title::{wad, cert}; use rustwii::title::{wad, cert};
use rustii::title; use rustwii::title;
// use rustii::title::content; // use rustii::title::content;
fn main() { fn main() {
@@ -12,19 +12,17 @@ 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.borrow().len()); println!("num content records: {:?}", title.tmd.content_records().len());
println!("first record data: {:?}", title.tmd.content_records.borrow().first().unwrap()); println!("first record data: {:?}", title.tmd.content_records().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());
println!("title key (enc): {:?}", title.ticket.title_key); println!("title key (enc): {:?}", title.ticket.title_key());
println!("title key (dec): {:?}", title.ticket.dec_title_key()); println!("title key (dec): {:?}", title.ticket.title_key_dec());
println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned()); println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned());
println!("title is fakesigned: {:?}", title.is_fakesigned()); println!("title is fakesigned: {:?}", title.is_fakesigned());
println!("wad header: {:?}", wad.header);
let cert_chain = &title.cert_chain; let cert_chain = &title.cert_chain;
println!("cert chain OK"); println!("cert chain OK");
let result = cert::verify_ca_cert(&cert_chain.ca_cert()).unwrap(); 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 // archive/ash.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Code for the ASH decompression command in the rustii CLI. // Code for the ASH decompression command in the rustii CLI.
// Might even have the compression command someday if I ever write the compression code! // 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 std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use clap::Subcommand; use clap::Subcommand;
use rustii::archive::ash; use rustwii::archive::ash;
#[derive(Subcommand)] #[derive(Subcommand)]
#[command(arg_required_else_help = true)] #[command(arg_required_else_help = true)]

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
// title/fakesign.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/fakesign.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Code for the fakesign command in the rustii CLI. // Code for the fakesign command in the rustii CLI.
use std::{str, fs}; use std::{str, fs};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result}; 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}; use crate::filetypes::{WiiFileType, identify_file_type};
pub fn fakesign(input: &str, output: &Option<String>) -> Result<()> { 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 // title/nus.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Code for NUS-related commands in the rustii CLI. // Code for NUS-related commands in the rustii CLI.
@@ -8,8 +8,8 @@ use std::path::PathBuf;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use clap::{Subcommand, Args}; use clap::{Subcommand, Args};
use sha1::{Sha1, Digest}; use sha1::{Sha1, Digest};
use rustii::title::{cert, content, crypto, nus, ticket, tmd}; use rustwii::title::{cert, content, crypto, nus, ticket, tmd};
use rustii::title; use rustwii::title;
#[derive(Subcommand)] #[derive(Subcommand)]
#[command(arg_required_else_help = true)] #[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.") 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.borrow().iter() let (content_hash, content_size, content_index) = tmd.content_records().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.")?;
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); content_dec.resize(content_size as usize, 0);
// Verify the content's hash before saving it. // Verify the content's hash before saving it.
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
@@ -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.borrow().iter() { for record in title.content.content_records().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.borrow().iter() { for record in content_region.content_records().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))?;
@@ -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. // 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.borrow().iter() { for record in tmd.content_records().iter() {
println!(" - Downloading content {} of {} (Content ID: {}, Size: {} bytes)...", 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))?); 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!");
} }
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..."); println!(" - Building certificate chain...");
let cert_chain = cert::CertificateChain::from_bytes(&nus::download_cert_chain(true).with_context(|| "Certificate chain could not be built.")?)?; let cert_chain = cert::CertificateChain::from_bytes(&nus::download_cert_chain(true).with_context(|| "Certificate chain could not be built.")?)?;
if tik.is_some() { if tik.is_some() {

View File

@@ -1,19 +1,18 @@
// title/wad.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/wad.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Code for WAD-related commands in the rustii CLI. // Code for WAD-related commands in the rustii CLI.
use std::{str, fs, fmt}; use std::{str, fs, fmt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::rc::Rc;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use clap::{Subcommand, Args}; use clap::{Subcommand, Args};
use glob::glob; use glob::glob;
use hex::FromHex; use hex::FromHex;
use rand::prelude::*; use rand::prelude::*;
use regex::RegexBuilder; use regex::RegexBuilder;
use rustii::title::{cert, crypto, tmd, ticket, content, wad}; use rustwii::title::{cert, crypto, tmd, ticket, content, wad};
use rustii::title; use rustwii::title;
#[derive(Subcommand)] #[derive(Subcommand)]
#[command(arg_required_else_help = true)] #[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 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.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); bail!("The specified Content ID \"{:08X}\" is already being used in this WAD!", cid);
} }
cid cid
@@ -196,7 +195,7 @@ 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.borrow().iter().any(|record| record.content_id == cid) { if !title.content.content_records().iter().any(|record| record.content_id == cid) {
break; break;
} }
} }
@@ -250,32 +249,32 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
} else { } else {
"retail" "retail"
}; };
let title_key = title.ticket.dec_title_key(); let title_key = title.ticket.title_key_dec();
let title_key_new: [u8; 16]; let title_key_new: [u8; 16];
match target { match target {
Target::Dev => { Target::Dev => {
title.tmd.set_signature_issuer(String::from("Root-CA00000002-CP00000007"))?; title.tmd.set_signature_issuer(String::from("Root-CA00000002-CP00000007"))?;
title.ticket.set_signature_issuer(String::from("Root-CA00000002-XS00000006"))?; 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_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id(), true);
title.ticket.common_key_index = 0; title.ticket.set_common_key_index(0);
title.tmd.is_vwii = 0; title.tmd.set_is_vwii(false);
}, },
Target::Retail => { Target::Retail => {
title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?; title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
title.ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?; 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_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id(), false);
title.ticket.common_key_index = 0; title.ticket.set_common_key_index(0);
title.tmd.is_vwii = 0; title.tmd.set_is_vwii(false);
}, },
Target::Vwii => { Target::Vwii => {
title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?; title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
title.ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?; 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_key_new = crypto::encrypt_title_key(title_key, 2, title.ticket.title_id(), false);
title.ticket.common_key_index = 2; title.ticket.set_common_key_index(2);
title.tmd.is_vwii = 1; title.tmd.set_is_vwii(true);
} }
} }
title.ticket.title_key = title_key_new; title.ticket.set_title_key(title_key_new);
title.fakesign()?; title.fakesign()?;
fs::write(&out_path, title.to_wad()?.to_bytes()?)?; 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()); 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.")?; 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. // Iterate over expected content and read it into a content region.
let mut content_region = content::ContentRegion::new(Rc::clone(&tmd.content_records))?; let mut content_region = content::ContentRegion::new(tmd.content_records().clone())?;
let content_indexes: Vec<u16> = tmd.content_records.borrow().iter().map(|record| record.index).collect(); let content_indexes: Vec<u16> = tmd.content_records().iter().map(|record| record.index).collect();
for index in content_indexes { 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))?; 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.")?; .with_context(|| "Failed to load content into the ContentRegion.")?;
} }
// Ensure that the TMD is modified with our potentially updated content records. // 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.")?; 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. // Write out WAD file.
let mut out_path = PathBuf::from(output); 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); 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.content_records.borrow().len() { for i in 0..title.tmd.content_records().len() {
let content_file_name = format!("{:08X}.app", title.content.content_records.borrow()[i].index); 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.borrow()[i].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()[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))?; 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()); println!("Successfully unpacked WAD file to \"{}\"!", out_path.display());
Ok(()) Ok(())

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
// title/cert.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/cert.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Implements the structures and methods required for validating the signatures of Wii titles. // Implements the structures and methods required for validating the signatures of Wii titles.
@@ -37,8 +37,8 @@ pub enum CertificateKeyType {
ECC ECC
} }
#[derive(Debug, Clone)]
/// A structure that represents the components of a Wii signing certificate. /// A structure that represents the components of a Wii signing certificate.
#[derive(Debug, Clone)]
pub struct Certificate { pub struct Certificate {
signer_key_type: CertificateKeyType, signer_key_type: CertificateKeyType,
signature: Vec<u8>, signature: Vec<u8>,
@@ -165,8 +165,8 @@ impl Certificate {
} }
} }
#[derive(Debug)]
/// A structure that represents the components of the Wii's signing certificate chain. /// A structure that represents the components of the Wii's signing certificate chain.
#[derive(Debug)]
pub struct CertificateChain { pub struct CertificateChain {
ca_cert: Certificate, ca_cert: Certificate,
tmd_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_modulus = BigUint::from_bytes_be(&tmd_cert.pub_key_modulus());
let public_key_exponent = BigUint::from(tmd_cert.pub_key_exponent()); let public_key_exponent = BigUint::from(tmd_cert.pub_key_exponent());
let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap(); 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), Ok(_) => Ok(true),
Err(_) => Ok(false), 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_modulus = BigUint::from_bytes_be(&ticket_cert.pub_key_modulus());
let public_key_exponent = BigUint::from(ticket_cert.pub_key_exponent()); let public_key_exponent = BigUint::from(ticket_cert.pub_key_exponent());
let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap(); 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), Ok(_) => Ok(true),
Err(_) => Ok(false), Err(_) => Ok(false),
} }

View File

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

View File

@@ -1,11 +1,9 @@
// title/content.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/content.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// 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};
@@ -37,39 +35,39 @@ 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: Rc<RefCell<Vec<ContentRecord>>>, content_records: Vec<ContentRecord>,
pub content_region_size: u32, content_region_size: u32,
pub content_start_offsets: Vec<u64>, content_start_offsets: Vec<u64>,
pub contents: Vec<Vec<u8>>, contents: Vec<Vec<u8>>,
} }
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: 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 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. // 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.borrow().iter().scan(0, |offset, record| { .chain(content_records.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.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. // 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.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]; 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: Rc::clone(&content_records), content_records,
content_region_size, content_region_size,
content_start_offsets, content_start_offsets,
contents, contents,
@@ -78,13 +76,13 @@ 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: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> { pub fn from_contents(contents: Vec<Vec<u8>>, content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
if contents.len() != content_records.borrow().len() { if contents.len() != content_records.len() {
return Err(ContentError::MissingContents { required: content_records.borrow().len(), found: contents.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() { 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)?; content_region.load_enc_content(&contents[i], target_index as usize)?;
} }
Ok(content_region) Ok(content_region)
@@ -92,14 +90,14 @@ impl ContentRegion {
/// 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: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> { pub fn new(content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
let content_region_size: u64 = content_records.borrow().iter().map(|x| (x.content_size + 63) & !63).sum(); 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 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 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: Rc::clone(&content_records), content_records,
content_region_size, content_region_size,
content_start_offsets, content_start_offsets,
contents, contents,
@@ -109,7 +107,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.content_records.borrow().len() { for i in 0..self.content_records.len() {
let mut content = self.contents[i].clone(); let mut content = self.contents[i].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);
@@ -117,12 +115,32 @@ impl ContentRegion {
} }
Ok(buf) 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. /// Gets the index of content using its Content ID.
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.borrow().iter() let content_index = self.content_records.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 {
@@ -134,7 +152,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.borrow().len() - 1 })?; let content = self.contents.get(index).ok_or(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 })?;
Ok(content.clone()) 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> { 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.borrow()[index].index); let mut content_dec = crypto::decrypt_content(&content, title_key, self.content_records[index].index);
content_dec.resize(self.content_records.borrow()[index].content_size as usize, 0); content_dec.resize(self.content_records[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.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.borrow()[index].content_hash) }); return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records[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.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 { 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)
@@ -166,7 +184,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.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 { 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)
@@ -178,8 +196,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.borrow().len() { if index >= self.content_records.len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 }); return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
} }
self.contents[index] = content.to_vec(); self.contents[index] = content.to_vec();
Ok(()) Ok(())
@@ -190,20 +208,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.borrow().len() { if index >= self.content_records.len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 }); return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
} }
self.content_records.borrow_mut()[index].content_size = content_size; self.content_records[index].content_size = content_size;
self.content_records.borrow_mut()[index].content_hash = content_hash; self.content_records[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.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())); 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() { 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(); self.contents[index] = content.to_vec();
Ok(()) 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 /// 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.borrow().len() { if index >= self.content_records.len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 }); 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 // 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.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.borrow()[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; self.contents[index] = content_enc;
Ok(()) Ok(())
} }
@@ -247,11 +265,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.borrow().get(index).is_none() { if self.contents.get(index).is_none() || self.content_records.get(index).is_none() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 }); return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
} }
self.contents.remove(index); self.contents.remove(index);
self.content_records.borrow_mut().remove(index); self.content_records.remove(index);
Ok(()) Ok(())
} }
@@ -259,14 +277,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.borrow().iter().any(|record| record.index == index) { if self.content_records.iter().any(|record| record.index == index) {
return Err(ContentError::IndexAlreadyExists(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)); return Err(ContentError::CIDAlreadyExists(cid));
} }
self.contents.push(content.to_vec()); 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(()) Ok(())
} }
@@ -275,7 +293,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.borrow().iter() let max_index = self.content_records.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

@@ -1,5 +1,5 @@
// title/crypto.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/crypto.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Implements the common crypto functions required to handle Wii content encryption. // 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 // title/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Root for all title-related modules and implementation of the high-level Title object. // 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 versions;
pub mod wad; pub mod wad;
use std::rc::Rc;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, 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 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(), 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 { Ok(Title {
cert_chain, cert_chain,
crl: wad.crl(), crl: wad.crl(),
@@ -123,13 +122,13 @@ impl Title {
/// Gets the decrypted content file from the Title at the specified index. /// 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> { 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) Ok(content)
} }
/// Gets the decrypted content file from the Title with the specified Content ID. /// 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> { 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) Ok(content)
} }
@@ -137,7 +136,7 @@ impl Title {
/// have its size and hash saved into the matching record. Optionally, a new Content ID or /// 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. /// 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.title_key_dec())?;
Ok(()) Ok(())
} }
@@ -146,7 +145,7 @@ impl Title {
/// 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: 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.title_key_dec())?;
Ok(()) Ok(())
} }
@@ -158,7 +157,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.borrow().iter() { for record in self.tmd.content_records().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

@@ -1,5 +1,5 @@
// title/nus.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/nus.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Implements the functions required for downloading data from the NUS. // 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. /// 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.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(); 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)?);
@@ -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 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 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 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)?; let title = title::Title::from_parts(cert_chain, None, tik, tmd, content_region, None)?;
Ok(title) Ok(title)
} }

View File

@@ -1,5 +1,5 @@
// title/tik.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/tik.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Implements the structures and methods required for Ticket parsing and editing. // Implements the structures and methods required for Ticket parsing and editing.
@@ -33,28 +33,28 @@ pub struct TitleLimit {
#[derive(Debug)] #[derive(Debug)]
/// A structure that represents a Wii Ticket file. /// A structure that represents a Wii Ticket file.
pub struct Ticket { pub struct Ticket {
pub signature_type: u32, signature_type: u32,
pub signature: [u8; 256], signature: [u8; 256],
padding1: [u8; 60], padding1: [u8; 60],
pub signature_issuer: [u8; 64], signature_issuer: [u8; 64],
pub ecdh_data: [u8; 60], ecdh_data: [u8; 60],
pub ticket_version: u8, ticket_version: u8,
reserved1: [u8; 2], reserved1: [u8; 2],
pub title_key: [u8; 16], title_key: [u8; 16],
unknown1: [u8; 1], unknown1: [u8; 1],
pub ticket_id: [u8; 8], ticket_id: [u8; 8],
pub console_id: [u8; 4], console_id: [u8; 4],
title_id: [u8; 8], title_id: [u8; 8],
unknown2: [u8; 2], unknown2: [u8; 2],
pub title_version: u16, title_version: u16,
pub permitted_titles_mask: [u8; 4], permitted_titles_mask: [u8; 4],
pub permit_mask: [u8; 4], permit_mask: [u8; 4],
pub title_export_allowed: u8, title_export_allowed: u8,
pub common_key_index: u8, common_key_index: u8,
unknown3: [u8; 48], unknown3: [u8; 48],
pub content_access_permission: [u8; 64], content_access_permission: [u8; 64],
padding2: [u8; 2], padding2: [u8; 2],
pub title_limits: [TitleLimit; 8], title_limits: [TitleLimit; 8],
} }
impl Ticket { impl Ticket {
@@ -169,8 +169,87 @@ impl Ticket {
Ok(buf) 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. /// 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. // Get the dev status of this Ticket so decrypt_title_key knows the right common key.
let is_dev = self.is_dev(); let is_dev = self.is_dev();
decrypt_title_key(self.title_key, self.common_key_index, self.title_id, 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 /// 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. /// is used as the IV for decrypting the Title Key.
pub fn set_title_id(&mut self, title_id: [u8; 8]) -> Result<(), TicketError> { 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_key = new_enc_title_key;
self.title_id = title_id; self.title_id = title_id;
Ok(()) Ok(())

View File

@@ -1,13 +1,11 @@
// title/tmd.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/tmd.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// 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;
@@ -81,8 +79,8 @@ pub enum AccessRight {
DVDVideo = 1, DVDVideo = 1,
} }
#[derive(Debug, Clone)]
/// A structure that represents the metadata of a content file in a digital Wii title. /// A structure that represents the metadata of a content file in a digital Wii title.
#[derive(Debug, Clone)]
pub struct ContentRecord { pub struct ContentRecord {
pub content_id: u32, pub content_id: u32,
pub index: u16, pub index: u16,
@@ -91,33 +89,33 @@ pub struct ContentRecord {
pub content_hash: [u8; 20], pub content_hash: [u8; 20],
} }
#[derive(Debug)]
/// A structure that represents a Wii TMD (Title Metadata) file. /// A structure that represents a Wii TMD (Title Metadata) file.
#[derive(Debug)]
pub struct TMD { pub struct TMD {
pub signature_type: u32, signature_type: u32,
pub signature: [u8; 256], signature: [u8; 256],
padding1: [u8; 60], padding1: [u8; 60],
pub signature_issuer: [u8; 64], signature_issuer: [u8; 64],
pub tmd_version: u8, tmd_version: u8,
pub ca_crl_version: u8, ca_crl_version: u8,
pub signer_crl_version: u8, signer_crl_version: u8,
pub is_vwii: u8, is_vwii: u8,
ios_tid: [u8; 8], ios_tid: [u8; 8],
title_id: [u8; 8], title_id: [u8; 8],
title_type: [u8; 4], title_type: [u8; 4],
pub group_id: u16, group_id: u16,
padding2: [u8; 2], padding2: [u8; 2],
region: u16, region: u16,
pub ratings: [u8; 16], ratings: [u8; 16],
reserved1: [u8; 12], reserved1: [u8; 12],
pub ipc_mask: [u8; 12], ipc_mask: [u8; 12],
reserved2: [u8; 18], reserved2: [u8; 18],
pub access_rights: u32, access_rights: u32,
pub title_version: u16, title_version: u16,
pub num_contents: u16, num_contents: u16,
pub boot_index: u16, boot_index: u16,
pub minor_version: u16, // Normally unused, but good for fakesigning! minor_version: u16, // Normally unused, but useful when fakesigning.
pub content_records: Rc<RefCell<Vec<ContentRecord>>>, content_records: Vec<ContentRecord>,
} }
impl TMD { impl TMD {
@@ -211,7 +209,7 @@ impl TMD {
num_contents, num_contents,
boot_index, boot_index,
minor_version, 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_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.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.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.borrow().iter() { for content in self.content_records.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 {
@@ -258,6 +256,76 @@ impl TMD {
Ok(buf) 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. /// Gets whether a TMD is fakesigned using the strncmp (trucha) bug or not.
pub fn is_fakesigned(&self) -> bool { pub fn is_fakesigned(&self) -> bool {
// Can't be fakesigned without a null signature. // 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 // 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.borrow().iter() { for record in self.content_records.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.borrow()[*target_index as usize].content_type { match self.content_records[*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,
@@ -365,10 +433,15 @@ impl TMD {
Ok(()) 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 { pub fn is_vwii(&self) -> bool {
self.is_vwii == 1 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. /// Gets the Title ID of a TMD.
pub fn title_id(&self) -> [u8; 8] { pub fn title_id(&self) -> [u8; 8] {

View File

@@ -1,5 +1,5 @@
// title/versions.rs from rustii (c) 2025 NinjaCheetah & Contributors // title/versions.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Handles converting Title version formats, and provides Wii Menu version constants. // 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 // title/wad.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii // https://github.com/NinjaCheetah/rustwii
// //
// Implements the structures and methods required for WAD parsing and editing. // Implements the structures and methods required for WAD parsing and editing.
@@ -32,16 +32,16 @@ pub enum WADType {
#[derive(Debug)] #[derive(Debug)]
/// A structure that represents an entire WAD file as a separate header and body. /// A structure that represents an entire WAD file as a separate header and body.
pub struct WAD { pub struct WAD {
pub header: WADHeader, header: WADHeader,
pub body: WADBody, body: WADBody,
} }
#[derive(Debug)] #[derive(Debug)]
/// A structure that represents the header of a WAD file. /// A structure that represents the header of a WAD file.
pub struct WADHeader { pub struct WADHeader {
pub header_size: u32, header_size: u32,
pub wad_type: WADType, wad_type: WADType,
pub wad_version: u16, wad_version: u16,
cert_chain_size: u32, cert_chain_size: u32,
crl_size: u32, crl_size: u32,
ticket_size: u32, ticket_size: u32,
@@ -93,6 +93,51 @@ impl WADHeader {
}; };
Ok(header) 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 { impl WADBody {
@@ -236,6 +281,11 @@ impl WAD {
buf.resize((buf.len() + 63) & !63, 0); buf.resize((buf.len() + 63) & !63, 0);
Ok(buf) 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 } pub fn cert_chain_size(&self) -> u32 { self.header.cert_chain_size }