mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-07 16:01:01 -04:00
Compare commits
4 Commits
1bcc004af7
...
ac1368053b
Author | SHA1 | Date | |
---|---|---|---|
ac1368053b | |||
3178063a07 | |||
8c7cd48dff | |||
839e33b911 |
45
Cargo.lock
generated
45
Cargo.lock
generated
@ -13,6 +13,15 @@ dependencies = [
|
|||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
@ -237,6 +246,12 @@ version = "0.2.171"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.1"
|
version = "1.21.1"
|
||||||
@ -261,6 +276,35 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustii"
|
name = "rustii"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -271,6 +315,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"glob",
|
"glob",
|
||||||
"hex",
|
"hex",
|
||||||
|
"regex",
|
||||||
"sha1",
|
"sha1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -30,4 +30,5 @@ aes = "0"
|
|||||||
hex = "0"
|
hex = "0"
|
||||||
sha1 = "0"
|
sha1 = "0"
|
||||||
glob = "0"
|
glob = "0"
|
||||||
|
regex = "1"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
@ -1,41 +1,43 @@
|
|||||||
// Sample file for testing rustii library stuff.
|
// Sample file for testing rustii library stuff.
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use rustii::title::{tmd, ticket, content, crypto, wad};
|
use rustii::title::{content, crypto, wad};
|
||||||
use rustii::title;
|
use rustii::title;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let data = fs::read("sm.wad").unwrap();
|
let data = fs::read("sm.wad").unwrap();
|
||||||
let title = title::Title::from_bytes(&data).unwrap();
|
let mut title = title::Title::from_bytes(&data).unwrap();
|
||||||
println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id));
|
println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id));
|
||||||
|
|
||||||
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());
|
||||||
let tmd = tmd::TMD::from_bytes(&wad.tmd()).unwrap();
|
println!("num content records: {:?}", title.tmd.content_records.len());
|
||||||
println!("num content records: {:?}", tmd.content_records.len());
|
println!("first record data: {:?}", title.tmd.content_records.first().unwrap());
|
||||||
println!("first record data: {:?}", tmd.content_records.first().unwrap());
|
if !title.tmd.is_fakesigned() {
|
||||||
assert_eq!(wad.tmd(), tmd.to_bytes().unwrap());
|
title.tmd.fakesign().unwrap();
|
||||||
|
}
|
||||||
|
println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned());
|
||||||
|
|
||||||
let tik = ticket::Ticket::from_bytes(&wad.ticket()).unwrap();
|
println!("title version from ticket is: {:?}", title.ticket.title_version);
|
||||||
println!("title version from ticket is: {:?}", tik.title_version);
|
println!("title key (enc): {:?}", title.ticket.title_key);
|
||||||
println!("title key (enc): {:?}", tik.title_key);
|
println!("title key (dec): {:?}", title.ticket.dec_title_key());
|
||||||
println!("title key (dec): {:?}", tik.dec_title_key());
|
if !title.ticket.is_fakesigned() {
|
||||||
assert_eq!(wad.ticket(), tik.to_bytes().unwrap());
|
title.ticket.fakesign().unwrap();
|
||||||
|
}
|
||||||
|
println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned());
|
||||||
|
|
||||||
let content_region = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records).unwrap();
|
println!("title is fakesigned: {:?}", title.is_fakesigned());
|
||||||
|
|
||||||
|
let content_region = content::ContentRegion::from_bytes(&wad.content(), title.tmd.content_records).unwrap();
|
||||||
assert_eq!(wad.content(), content_region.to_bytes().unwrap());
|
assert_eq!(wad.content(), content_region.to_bytes().unwrap());
|
||||||
println!("content OK");
|
println!("content OK");
|
||||||
|
|
||||||
let content_dec = content_region.get_content_by_index(0, tik.dec_title_key()).unwrap();
|
let content_dec = content_region.get_content_by_index(0, title.ticket.dec_title_key()).unwrap();
|
||||||
println!("content dec from index: {:?}", content_dec);
|
println!("content dec from index: {:?}", content_dec);
|
||||||
|
|
||||||
let content = content_region.get_enc_content_by_index(0).unwrap();
|
let content = content_region.get_enc_content_by_index(0).unwrap();
|
||||||
assert_eq!(content, crypto::encrypt_content(&content_dec, tik.dec_title_key(), 0, content_region.content_records[0].content_size));
|
assert_eq!(content, crypto::encrypt_content(&content_dec, title.ticket.dec_title_key(), 0, content_region.content_records[0].content_size));
|
||||||
println!("content re-encrypted OK");
|
println!("content re-encrypted OK");
|
||||||
|
|
||||||
println!("wad header: {:?}", wad.header);
|
println!("wad header: {:?}", wad.header);
|
||||||
|
|
||||||
let repacked = wad.to_bytes().unwrap();
|
|
||||||
assert_eq!(repacked, data);
|
|
||||||
println!("wad packed OK");
|
|
||||||
}
|
}
|
||||||
|
87
src/bin/rustii/filetypes.rs
Normal file
87
src/bin/rustii/filetypes.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// filetypes.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii
|
||||||
|
//
|
||||||
|
// Common code for identifying Wii file types.
|
||||||
|
|
||||||
|
use std::{str, fs::File};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
use regex::RegexBuilder;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum WiiFileType {
|
||||||
|
Wad,
|
||||||
|
Tmd,
|
||||||
|
Ticket
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identify_file_type(input: &str) -> Option<WiiFileType> {
|
||||||
|
let input = Path::new(input);
|
||||||
|
let re = RegexBuilder::new(r"tmd\.?[0-9]*").case_insensitive(true).build().unwrap();
|
||||||
|
// == TMD ==
|
||||||
|
if re.is_match(input.to_str()?) ||
|
||||||
|
input.file_name().is_some_and(|f| f.eq_ignore_ascii_case("tmd.bin")) ||
|
||||||
|
input.extension().is_some_and(|f| f.eq_ignore_ascii_case("tmd")) {
|
||||||
|
return Some(WiiFileType::Tmd);
|
||||||
|
}
|
||||||
|
// == Ticket ==
|
||||||
|
if input.extension().is_some_and(|f| f.eq_ignore_ascii_case("tik")) ||
|
||||||
|
input.file_name().is_some_and(|f| f.eq_ignore_ascii_case("ticket.bin")) ||
|
||||||
|
input.file_name().is_some_and(|f| f.eq_ignore_ascii_case("cetk")) {
|
||||||
|
return Some(WiiFileType::Ticket);
|
||||||
|
}
|
||||||
|
// == WAD ==
|
||||||
|
if input.extension().is_some_and(|f| f.eq_ignore_ascii_case("wad")) {
|
||||||
|
return Some(WiiFileType::Wad);
|
||||||
|
}
|
||||||
|
// Advanced WAD detection, where we read and compare the first 8 bytes (only if the path exists.)
|
||||||
|
if input.exists() {
|
||||||
|
let mut f = File::open(input).unwrap();
|
||||||
|
let mut magic_number = vec![0u8; 8];
|
||||||
|
f.read_exact(&mut magic_number).unwrap();
|
||||||
|
if magic_number == b"\x00\x00\x00\x20\x49\x73\x00\x00" || magic_number == b"\x00\x00\x00\x20\x69\x62\x00\x00" {
|
||||||
|
return Some(WiiFileType::Wad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == No match found! ==
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_tmd() {
|
||||||
|
assert_eq!(identify_file_type("tmd"), Some(WiiFileType::Tmd));
|
||||||
|
assert_eq!(identify_file_type("TMD"), Some(WiiFileType::Tmd));
|
||||||
|
assert_eq!(identify_file_type("tmd.bin"), Some(WiiFileType::Tmd));
|
||||||
|
assert_eq!(identify_file_type("TMD.BIN"), Some(WiiFileType::Tmd));
|
||||||
|
assert_eq!(identify_file_type("tmd.513"), Some(WiiFileType::Tmd));
|
||||||
|
assert_eq!(identify_file_type("0000000100000002.tmd"), Some(WiiFileType::Tmd));
|
||||||
|
assert_eq!(identify_file_type("0000000100000002.TMD"), Some(WiiFileType::Tmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_tik() {
|
||||||
|
assert_eq!(identify_file_type("ticket.bin"), Some(WiiFileType::Ticket));
|
||||||
|
assert_eq!(identify_file_type("TICKET.BIN"), Some(WiiFileType::Ticket));
|
||||||
|
assert_eq!(identify_file_type("cetk"), Some(WiiFileType::Ticket));
|
||||||
|
assert_eq!(identify_file_type("CETK"), Some(WiiFileType::Ticket));
|
||||||
|
assert_eq!(identify_file_type("0000000100000002.tik"), Some(WiiFileType::Ticket));
|
||||||
|
assert_eq!(identify_file_type("0000000100000002.TIK"), Some(WiiFileType::Ticket));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_wad() {
|
||||||
|
assert_eq!(identify_file_type("0000000100000002.wad"), Some(WiiFileType::Wad));
|
||||||
|
assert_eq!(identify_file_type("0000000100000002.WAD"), Some(WiiFileType::Wad));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_no_match() {
|
||||||
|
assert_eq!(identify_file_type("somefile.txt"), None);
|
||||||
|
}
|
||||||
|
}
|
144
src/bin/rustii/info.rs
Normal file
144
src/bin/rustii/info.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// info.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii
|
||||||
|
//
|
||||||
|
// Code for the info command in the rustii CLI.
|
||||||
|
|
||||||
|
use std::{str, fs};
|
||||||
|
use std::path::Path;
|
||||||
|
use rustii::{title, title::tmd, title::ticket, title::wad};
|
||||||
|
use crate::filetypes::{WiiFileType, identify_file_type};
|
||||||
|
|
||||||
|
fn print_tmd_info(tmd: tmd::TMD) {
|
||||||
|
// Print all important keys from the TMD.
|
||||||
|
println!("Title Info");
|
||||||
|
println!(" Title ID: {}", hex::encode(tmd.title_id).to_uppercase());
|
||||||
|
println!(" Title Version: {}", tmd.title_version);
|
||||||
|
println!(" TMD Version: {}", tmd.tmd_version);
|
||||||
|
if hex::encode(tmd.ios_tid) == "0000000000000000" {
|
||||||
|
println!(" Required IOS: N/A");
|
||||||
|
}
|
||||||
|
else if hex::encode(tmd.ios_tid) != "0000000100000001" {
|
||||||
|
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();
|
||||||
|
if signature_issuer.contains("CP00000004") {
|
||||||
|
println!(" Certificate: CP00000004 (Retail)");
|
||||||
|
println!(" Certificate Issuer: Root-CA00000001 (Retail)");
|
||||||
|
}
|
||||||
|
else if signature_issuer.contains("CP00000007") {
|
||||||
|
println!(" Certificate: CP00000007 (Development)");
|
||||||
|
println!(" Certificate Issuer: Root-CA00000002 (Development)");
|
||||||
|
}
|
||||||
|
else if signature_issuer.contains("CP00000005") {
|
||||||
|
println!(" Certificate: CP00000005 (Development/Unknown)");
|
||||||
|
println!(" Certificate Issuer: Root-CA00000002 (Development)");
|
||||||
|
}
|
||||||
|
else if signature_issuer.contains("CP10000000") {
|
||||||
|
println!(" Certificate: CP10000000 (Arcade)");
|
||||||
|
println!(" Certificate Issuer: Root-CA10000000 (Arcade)");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
println!(" Certificate Info: {} (Unknown)", signature_issuer);
|
||||||
|
}
|
||||||
|
println!(" Region: {}", tmd.region());
|
||||||
|
println!(" Title Type: {}", tmd.title_type());
|
||||||
|
println!(" vWii Title: {}", tmd.is_vwii != 0);
|
||||||
|
println!(" DVD Video Access: {}", tmd.check_access_right(tmd::AccessRight::DVDVideo));
|
||||||
|
println!(" AHB Access: {}", tmd.check_access_right(tmd::AccessRight::AHB));
|
||||||
|
println!(" Fakesigned: {}", tmd.is_fakesigned());
|
||||||
|
println!("\nContent Info");
|
||||||
|
println!(" Total Contents: {}", tmd.num_contents);
|
||||||
|
println!(" Boot Content Index: {}", tmd.boot_index);
|
||||||
|
println!(" Content Records:");
|
||||||
|
for content in tmd.content_records {
|
||||||
|
println!(" Content Index: {}", content.index);
|
||||||
|
println!(" Content ID: {:08X}", content.content_id);
|
||||||
|
println!(" Content Type: {}", content.content_type);
|
||||||
|
println!(" Content Size: {} bytes", content.content_size);
|
||||||
|
println!(" Content Hash: {}", hex::encode(content.content_hash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_ticket_info(ticket: ticket::Ticket) {
|
||||||
|
// Print all important keys from the Ticket.
|
||||||
|
println!("Ticket Info");
|
||||||
|
println!(" Title ID: {}", hex::encode(ticket.title_id).to_uppercase());
|
||||||
|
println!(" Title Version: {}", ticket.title_version);
|
||||||
|
println!(" Ticket Version: {}", ticket.ticket_version);
|
||||||
|
let signature_issuer = String::from_utf8(Vec::from(ticket.signature_issuer)).unwrap_or_default();
|
||||||
|
if signature_issuer.contains("XS00000003") {
|
||||||
|
println!(" Certificate: XS00000003 (Retail)");
|
||||||
|
println!(" Certificate Issuer: Root-CA00000001 (Retail)");
|
||||||
|
}
|
||||||
|
else if signature_issuer.contains("XS00000006") {
|
||||||
|
println!(" Certificate: XS00000006 (Development)");
|
||||||
|
println!(" Certificate Issuer: Root-CA00000002 (Development)");
|
||||||
|
}
|
||||||
|
else if signature_issuer.contains("XS00000004") {
|
||||||
|
println!(" Certificate: XS00000004 (Development/Unknown)");
|
||||||
|
println!(" Certificate Issuer: Root-CA00000002 (Development)");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
println!(" Certificate Info: {} (Unknown)", signature_issuer);
|
||||||
|
}
|
||||||
|
let key = match ticket.common_key_index {
|
||||||
|
0 => {
|
||||||
|
if ticket.is_dev() { "Common (Development)" }
|
||||||
|
else { "Common (Retail)" }
|
||||||
|
}
|
||||||
|
1 => "Korean",
|
||||||
|
2 => "vWii",
|
||||||
|
_ => "Unknown (Likely Common)"
|
||||||
|
};
|
||||||
|
println!(" Decryption Key: {}", key);
|
||||||
|
println!(" Title Key (Encrypted): {}", hex::encode(ticket.title_key));
|
||||||
|
println!(" Title Key (Decrypted): {}", hex::encode(ticket.dec_title_key()));
|
||||||
|
println!(" Fakesigned: {}", ticket.is_fakesigned());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_wad_info(wad: wad::WAD) {
|
||||||
|
println!("WAD Info");
|
||||||
|
match wad.header.wad_type {
|
||||||
|
wad::WADType::ImportBoot => { println!(" WAD Type: boot2") },
|
||||||
|
wad::WADType::Installable => { println!(" WAD Type: Standard Installable") },
|
||||||
|
}
|
||||||
|
// Create a Title for size info, signing info and TMD/Ticket info.
|
||||||
|
let title = title::Title::from_wad(&wad).unwrap();
|
||||||
|
let min_size_blocks = title.title_size_blocks(None).unwrap();
|
||||||
|
let max_size_blocks = title.title_size_blocks(Some(true)).unwrap();
|
||||||
|
println!(" Installed Size: {}-{} blocks", min_size_blocks, max_size_blocks);
|
||||||
|
let min_size = title.title_size(None).unwrap() as f64 / 1048576.0;
|
||||||
|
let max_size = title.title_size(Some(true)).unwrap() as f64 / 1048576.0;
|
||||||
|
println!(" Installed Size (MB): {:.2}-{:.2} MB", min_size, max_size);
|
||||||
|
println!(" Has Meta/Footer: {}", wad.meta_size() != 0);
|
||||||
|
println!(" Has CRL: {}", wad.crl_size() != 0);
|
||||||
|
println!(" Fakesigned: {}", title.is_fakesigned());
|
||||||
|
println!();
|
||||||
|
print_ticket_info(title.ticket);
|
||||||
|
println!();
|
||||||
|
print_tmd_info(title.tmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(input: &str) {
|
||||||
|
let in_path = Path::new(input);
|
||||||
|
if !in_path.exists() {
|
||||||
|
panic!("Error: Input file does not exist.");
|
||||||
|
}
|
||||||
|
match identify_file_type(input) {
|
||||||
|
Some(WiiFileType::Tmd) => {
|
||||||
|
let tmd = tmd::TMD::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
|
||||||
|
print_tmd_info(tmd);
|
||||||
|
},
|
||||||
|
Some(WiiFileType::Ticket) => {
|
||||||
|
let ticket = ticket::Ticket::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
|
||||||
|
print_ticket_info(ticket);
|
||||||
|
},
|
||||||
|
Some(WiiFileType::Wad) => {
|
||||||
|
let wad = wad::WAD::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap();
|
||||||
|
print_wad_info(wad);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!("Error: Information cannot be displayed for this file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,9 +4,11 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
mod title;
|
mod title;
|
||||||
use clap::{Subcommand, Parser};
|
mod filetypes;
|
||||||
use title::wad;
|
mod info;
|
||||||
|
|
||||||
|
use clap::{Subcommand, Parser};
|
||||||
|
use title::{wad, fakesign};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
@ -23,6 +25,19 @@ enum Commands {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<wad::Commands>,
|
command: Option<wad::Commands>,
|
||||||
},
|
},
|
||||||
|
/// Fakesign a TMD, Ticket, or WAD (trucha bug)
|
||||||
|
Fakesign {
|
||||||
|
/// The path to a TMD, Ticket, or WAD
|
||||||
|
input: String,
|
||||||
|
/// An (optional) output name; defaults to overwriting input file if not provided
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
},
|
||||||
|
/// Get information about a TMD, Ticket, or WAD
|
||||||
|
Info {
|
||||||
|
/// The path to a TMD, Ticket, or WAD
|
||||||
|
input: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -37,8 +52,14 @@ fn main() {
|
|||||||
Some(wad::Commands::Unpack { input, output }) => {
|
Some(wad::Commands::Unpack { input, output }) => {
|
||||||
wad::unpack_wad(input, output)
|
wad::unpack_wad(input, output)
|
||||||
},
|
},
|
||||||
&None => { /* This is handled by clap */}
|
&None => { /* This is for me handled by clap */}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Some(Commands::Fakesign { input, output }) => {
|
||||||
|
fakesign::fakesign(input, output)
|
||||||
|
},
|
||||||
|
Some(Commands::Info { input }) => {
|
||||||
|
info::info(input)
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
60
src/bin/rustii/title/fakesign.rs
Normal file
60
src/bin/rustii/title/fakesign.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// title/fakesign.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii
|
||||||
|
//
|
||||||
|
// Code for the fakesign command in the rustii CLI.
|
||||||
|
|
||||||
|
use std::{str, fs};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use rustii::{title, title::tmd, title::ticket};
|
||||||
|
use crate::filetypes::{WiiFileType, identify_file_type};
|
||||||
|
|
||||||
|
pub fn fakesign(input: &str, output: &Option<String>) {
|
||||||
|
let in_path = Path::new(input);
|
||||||
|
if !in_path.exists() {
|
||||||
|
panic!("Error: Input file does not exist.");
|
||||||
|
}
|
||||||
|
match identify_file_type(input) {
|
||||||
|
Some(WiiFileType::Wad) => {
|
||||||
|
let out_path = if output.is_some() {
|
||||||
|
PathBuf::from(output.clone().unwrap().as_str()).with_extension("wad")
|
||||||
|
} else {
|
||||||
|
PathBuf::from(input)
|
||||||
|
};
|
||||||
|
// Load WAD into a Title instance, then fakesign it.
|
||||||
|
let mut title = title::Title::from_bytes(fs::read(in_path).unwrap().as_slice()).expect("could not read WAD file");
|
||||||
|
title.fakesign().expect("could not fakesign WAD");
|
||||||
|
// Write output file.
|
||||||
|
fs::write(out_path, title.to_wad().unwrap().to_bytes().expect("could not create output WAD")).expect("could not write output WAD file");
|
||||||
|
println!("WAD fakesigned!");
|
||||||
|
},
|
||||||
|
Some(WiiFileType::Tmd) => {
|
||||||
|
let out_path = if output.is_some() {
|
||||||
|
PathBuf::from(output.clone().unwrap().as_str()).with_extension("tmd")
|
||||||
|
} else {
|
||||||
|
PathBuf::from(input)
|
||||||
|
};
|
||||||
|
// Load TMD into a TMD instance, then fakesign it.
|
||||||
|
let mut tmd = tmd::TMD::from_bytes(fs::read(in_path).unwrap().as_slice()).expect("could not read TMD file");
|
||||||
|
tmd.fakesign().expect("could not fakesign TMD");
|
||||||
|
// Write output file.
|
||||||
|
fs::write(out_path, tmd.to_bytes().expect("could not create output TMD")).expect("could not write output TMD file");
|
||||||
|
println!("TMD fakesigned!");
|
||||||
|
},
|
||||||
|
Some(WiiFileType::Ticket) => {
|
||||||
|
let out_path = if output.is_some() {
|
||||||
|
PathBuf::from(output.clone().unwrap().as_str()).with_extension("tik")
|
||||||
|
} else {
|
||||||
|
PathBuf::from(input)
|
||||||
|
};
|
||||||
|
// Load Ticket into a Ticket instance, then fakesign it.
|
||||||
|
let mut ticket = ticket::Ticket::from_bytes(fs::read(in_path).unwrap().as_slice()).expect("could not read Ticket file");
|
||||||
|
ticket.fakesign().expect("could not fakesign Ticket");
|
||||||
|
// Write output file.
|
||||||
|
fs::write(out_path, ticket.to_bytes().expect("could not create output Ticket")).expect("could not write output Ticket file");
|
||||||
|
println!("Ticket fakesigned!");
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
panic!("Error: You can only fakesign TMDs, Tickets, and WADs!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
// title/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
// title/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||||
// https://github.com/NinjaCheetah/rustii
|
// https://github.com/NinjaCheetah/rustii
|
||||||
|
|
||||||
|
pub mod fakesign;
|
||||||
pub mod wad;
|
pub mod wad;
|
||||||
|
@ -20,6 +20,8 @@ pub enum TitleError {
|
|||||||
BadTMD,
|
BadTMD,
|
||||||
BadContent,
|
BadContent,
|
||||||
InvalidWAD,
|
InvalidWAD,
|
||||||
|
TMDError(tmd::TMDError),
|
||||||
|
TicketError(ticket::TicketError),
|
||||||
WADError(wad::WADError),
|
WADError(wad::WADError),
|
||||||
IOError(std::io::Error),
|
IOError(std::io::Error),
|
||||||
}
|
}
|
||||||
@ -31,6 +33,8 @@ impl fmt::Display for TitleError {
|
|||||||
TitleError::BadTMD => "The provided TMD data was invalid.",
|
TitleError::BadTMD => "The provided TMD data was invalid.",
|
||||||
TitleError::BadContent => "The provided content data was invalid.",
|
TitleError::BadContent => "The provided content data was invalid.",
|
||||||
TitleError::InvalidWAD => "The provided WAD data was invalid.",
|
TitleError::InvalidWAD => "The provided WAD data was invalid.",
|
||||||
|
TitleError::TMDError(_) => "An error occurred while processing TMD data.",
|
||||||
|
TitleError::TicketError(_) => "An error occurred while processing ticket data.",
|
||||||
TitleError::WADError(_) => "A WAD could not be built from the provided data.",
|
TitleError::WADError(_) => "A WAD could not be built from the provided data.",
|
||||||
TitleError::IOError(_) => "The provided Title data was invalid.",
|
TitleError::IOError(_) => "The provided Title data was invalid.",
|
||||||
};
|
};
|
||||||
@ -85,6 +89,17 @@ impl Title {
|
|||||||
Ok(title)
|
Ok(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_fakesigned(&self) -> bool {
|
||||||
|
self.tmd.is_fakesigned() && self.ticket.is_fakesigned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fakesign(&mut self) -> Result<(), TitleError> {
|
||||||
|
// Run the fakesign methods on the TMD and Ticket.
|
||||||
|
self.tmd.fakesign().map_err(TitleError::TMDError)?;
|
||||||
|
self.ticket.fakesign().map_err(TitleError::TicketError)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
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.dec_title_key())?;
|
||||||
Ok(content)
|
Ok(content)
|
||||||
@ -95,6 +110,34 @@ impl Title {
|
|||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the installed size of the title, in bytes. Use the optional parameter "absolute" to set
|
||||||
|
/// whether shared content should be included in this total or not.
|
||||||
|
pub fn title_size(&self, absolute: Option<bool>) -> Result<usize, TitleError> {
|
||||||
|
let mut title_size: usize = 0;
|
||||||
|
// Get the TMD and Ticket size by dumping them and measuring their length for the most
|
||||||
|
// accurate results.
|
||||||
|
title_size += self.tmd.to_bytes().map_err(|x| TitleError::TMDError(tmd::TMDError::IOError(x)))?.len();
|
||||||
|
title_size += self.ticket.to_bytes().map_err(|x| TitleError::TicketError(ticket::TicketError::IOError(x)))?.len();
|
||||||
|
for record in &self.tmd.content_records {
|
||||||
|
if matches!(record.content_type, tmd::ContentType::Shared) {
|
||||||
|
if absolute == Some(true) {
|
||||||
|
title_size += record.content_size as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
title_size += record.content_size as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(title_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the installed size of the title, in blocks. Use the optional parameter "absolute" to
|
||||||
|
/// set whether shared content should be included in this total or not.
|
||||||
|
pub fn title_size_blocks(&self, absolute: Option<bool>) -> Result<usize, TitleError> {
|
||||||
|
let title_size_bytes = self.title_size(absolute)?;
|
||||||
|
Ok((title_size_bytes as f64 / 131072.0).ceil() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cert_chain(&self) -> Vec<u8> {
|
pub fn cert_chain(&self) -> Vec<u8> {
|
||||||
self.cert_chain.clone()
|
self.cert_chain.clone()
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,13 @@ use std::error::Error;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::{Cursor, Read, Write};
|
use std::io::{Cursor, Read, Write};
|
||||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use sha1::{Sha1, Digest};
|
||||||
use crate::title::crypto::decrypt_title_key;
|
use crate::title::crypto::decrypt_title_key;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum TicketError {
|
pub enum TicketError {
|
||||||
UnsupportedVersion,
|
UnsupportedVersion,
|
||||||
|
CannotFakesign,
|
||||||
IOError(std::io::Error),
|
IOError(std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,6 +21,7 @@ impl fmt::Display for TicketError {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let description = match *self {
|
let description = match *self {
|
||||||
TicketError::UnsupportedVersion => "The provided Ticket is not a supported version (only v0 is supported).",
|
TicketError::UnsupportedVersion => "The provided Ticket is not a supported version (only v0 is supported).",
|
||||||
|
TicketError::CannotFakesign => "The Ticket data could not be fakesigned.",
|
||||||
TicketError::IOError(_) => "The provided Ticket data was invalid.",
|
TicketError::IOError(_) => "The provided Ticket data was invalid.",
|
||||||
};
|
};
|
||||||
f.write_str(description)
|
f.write_str(description)
|
||||||
@ -184,4 +187,37 @@ impl Ticket {
|
|||||||
let issuer_str = String::from_utf8(Vec::from(&self.signature_issuer)).unwrap_or_default();
|
let issuer_str = String::from_utf8(Vec::from(&self.signature_issuer)).unwrap_or_default();
|
||||||
issuer_str.contains("Root-CA00000002-XS00000004") || issuer_str.contains("Root-CA00000002-XS00000006")
|
issuer_str.contains("Root-CA00000002-XS00000004") || issuer_str.contains("Root-CA00000002-XS00000006")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_fakesigned(&self) -> bool {
|
||||||
|
// Can't be fakesigned without a null signature.
|
||||||
|
if self.signature != [0; 256] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Test the hash of the Ticket body to make sure it starts with 00.
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
let ticket_body = self.to_bytes().unwrap();
|
||||||
|
hasher.update(&ticket_body[320..]);
|
||||||
|
let result = hasher.finalize();
|
||||||
|
if result[0] != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fakesign(&mut self) -> Result<(), TicketError> {
|
||||||
|
// Erase the signature.
|
||||||
|
self.signature = [0; 256];
|
||||||
|
let mut current_int: u16 = 0;
|
||||||
|
let mut test_hash: [u8; 20] = [255; 20];
|
||||||
|
while test_hash[0] != 0 {
|
||||||
|
if current_int == 65535 { return Err(TicketError::CannotFakesign); }
|
||||||
|
current_int += 1;
|
||||||
|
self.unknown2 = current_int.to_be_bytes();
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
let ticket_body = self.to_bytes().unwrap();
|
||||||
|
hasher.update(&ticket_body[320..]);
|
||||||
|
test_hash = <[u8; 20]>::from(hasher.finalize());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
236
src/title/tmd.rs
236
src/title/tmd.rs
@ -3,15 +3,90 @@
|
|||||||
//
|
//
|
||||||
// Implements the structures and methods required for TMD parsing and editing.
|
// Implements the structures and methods required for TMD parsing and editing.
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
use std::io::{Cursor, Read, Write};
|
use std::io::{Cursor, Read, Write};
|
||||||
|
use std::ops::Index;
|
||||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use sha1::{Sha1, Digest};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[derive(Clone)]
|
pub enum TMDError {
|
||||||
|
CannotFakesign,
|
||||||
|
InvalidContentType(u16),
|
||||||
|
IOError(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TMDError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let description = match *self {
|
||||||
|
TMDError::CannotFakesign => "The TMD data could not be fakesigned.",
|
||||||
|
TMDError::InvalidContentType(_) => "The TMD contains content with an invalid type.",
|
||||||
|
TMDError::IOError(_) => "The provided TMD data was invalid.",
|
||||||
|
};
|
||||||
|
f.write_str(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for TMDError {}
|
||||||
|
|
||||||
|
pub enum TitleType {
|
||||||
|
System,
|
||||||
|
Game,
|
||||||
|
Channel,
|
||||||
|
SystemChannel,
|
||||||
|
GameChannel,
|
||||||
|
DLC,
|
||||||
|
HiddenChannel,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TitleType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
TitleType::System => write!(f, "System"),
|
||||||
|
TitleType::Game => write!(f, "Game"),
|
||||||
|
TitleType::Channel => write!(f, "Channel"),
|
||||||
|
TitleType::SystemChannel => write!(f, "SystemChannel"),
|
||||||
|
TitleType::GameChannel => write!(f, "GameChannel"),
|
||||||
|
TitleType::DLC => write!(f, "DLC"),
|
||||||
|
TitleType::HiddenChannel => write!(f, "HiddenChannel"),
|
||||||
|
TitleType::Unknown => write!(f, "Unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ContentType {
|
||||||
|
Normal,
|
||||||
|
Development,
|
||||||
|
HashTree,
|
||||||
|
DLC,
|
||||||
|
Shared,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ContentType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ContentType::Normal => write!(f, "Normal"),
|
||||||
|
ContentType::Development => write!(f, "Development/Unknown"),
|
||||||
|
ContentType::HashTree => write!(f, "Hash Tree"),
|
||||||
|
ContentType::DLC => write!(f, "DLC"),
|
||||||
|
ContentType::Shared => write!(f, "Shared"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AccessRight {
|
||||||
|
AHB,
|
||||||
|
DVDVideo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct ContentRecord {
|
pub struct ContentRecord {
|
||||||
pub content_id: u32,
|
pub content_id: u32,
|
||||||
pub index: u16,
|
pub index: u16,
|
||||||
pub content_type: u16,
|
pub content_type: ContentType,
|
||||||
pub content_size: u64,
|
pub content_size: u64,
|
||||||
pub content_hash: [u8; 20],
|
pub content_hash: [u8; 20],
|
||||||
}
|
}
|
||||||
@ -31,7 +106,7 @@ pub struct TMD {
|
|||||||
pub title_type: [u8; 4],
|
pub title_type: [u8; 4],
|
||||||
pub group_id: u16,
|
pub group_id: u16,
|
||||||
padding2: [u8; 2],
|
padding2: [u8; 2],
|
||||||
pub region: u16,
|
region: u16,
|
||||||
pub ratings: [u8; 16],
|
pub ratings: [u8; 16],
|
||||||
reserved1: [u8; 12],
|
reserved1: [u8; 12],
|
||||||
pub ipc_mask: [u8; 12],
|
pub ipc_mask: [u8; 12],
|
||||||
@ -46,55 +121,63 @@ pub struct TMD {
|
|||||||
|
|
||||||
impl TMD {
|
impl TMD {
|
||||||
/// Creates a new TMD instance from the binary data of a TMD file.
|
/// Creates a new TMD instance from the binary data of a TMD file.
|
||||||
pub fn from_bytes(data: &[u8]) -> Result<Self, std::io::Error> {
|
pub fn from_bytes(data: &[u8]) -> Result<Self, TMDError> {
|
||||||
let mut buf = Cursor::new(data);
|
let mut buf = Cursor::new(data);
|
||||||
let signature_type = buf.read_u32::<BigEndian>()?;
|
let signature_type = buf.read_u32::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let mut signature = [0u8; 256];
|
let mut signature = [0u8; 256];
|
||||||
buf.read_exact(&mut signature)?;
|
buf.read_exact(&mut signature).map_err(TMDError::IOError)?;
|
||||||
// Maybe this can be read differently?
|
// Maybe this can be read differently?
|
||||||
let mut padding1 = [0u8; 60];
|
let mut padding1 = [0u8; 60];
|
||||||
buf.read_exact(&mut padding1)?;
|
buf.read_exact(&mut padding1).map_err(TMDError::IOError)?;
|
||||||
let mut signature_issuer = [0u8; 64];
|
let mut signature_issuer = [0u8; 64];
|
||||||
buf.read_exact(&mut signature_issuer)?;
|
buf.read_exact(&mut signature_issuer).map_err(TMDError::IOError)?;
|
||||||
let tmd_version = buf.read_u8()?;
|
let tmd_version = buf.read_u8().map_err(TMDError::IOError)?;
|
||||||
let ca_crl_version = buf.read_u8()?;
|
let ca_crl_version = buf.read_u8().map_err(TMDError::IOError)?;
|
||||||
let signer_crl_version = buf.read_u8()?;
|
let signer_crl_version = buf.read_u8().map_err(TMDError::IOError)?;
|
||||||
let is_vwii = buf.read_u8()?;
|
let is_vwii = buf.read_u8().map_err(TMDError::IOError)?;
|
||||||
let mut ios_tid = [0u8; 8];
|
let mut ios_tid = [0u8; 8];
|
||||||
buf.read_exact(&mut ios_tid)?;
|
buf.read_exact(&mut ios_tid).map_err(TMDError::IOError)?;
|
||||||
let mut title_id = [0u8; 8];
|
let mut title_id = [0u8; 8];
|
||||||
buf.read_exact(&mut title_id)?;
|
buf.read_exact(&mut title_id).map_err(TMDError::IOError)?;
|
||||||
let mut title_type = [0u8; 4];
|
let mut title_type = [0u8; 4];
|
||||||
buf.read_exact(&mut title_type)?;
|
buf.read_exact(&mut title_type).map_err(TMDError::IOError)?;
|
||||||
let group_id = buf.read_u16::<BigEndian>()?;
|
let group_id = buf.read_u16::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
// Same here...
|
// Same here...
|
||||||
let mut padding2 = [0u8; 2];
|
let mut padding2 = [0u8; 2];
|
||||||
buf.read_exact(&mut padding2)?;
|
buf.read_exact(&mut padding2).map_err(TMDError::IOError)?;
|
||||||
let region = buf.read_u16::<BigEndian>()?;
|
let region = buf.read_u16::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let mut ratings = [0u8; 16];
|
let mut ratings = [0u8; 16];
|
||||||
buf.read_exact(&mut ratings)?;
|
buf.read_exact(&mut ratings).map_err(TMDError::IOError)?;
|
||||||
// ...and here...
|
// ...and here...
|
||||||
let mut reserved1 = [0u8; 12];
|
let mut reserved1 = [0u8; 12];
|
||||||
buf.read_exact(&mut reserved1)?;
|
buf.read_exact(&mut reserved1).map_err(TMDError::IOError)?;
|
||||||
let mut ipc_mask = [0u8; 12];
|
let mut ipc_mask = [0u8; 12];
|
||||||
buf.read_exact(&mut ipc_mask)?;
|
buf.read_exact(&mut ipc_mask).map_err(TMDError::IOError)?;
|
||||||
// ...and here.
|
// ...and here.
|
||||||
let mut reserved2 = [0u8; 18];
|
let mut reserved2 = [0u8; 18];
|
||||||
buf.read_exact(&mut reserved2)?;
|
buf.read_exact(&mut reserved2).map_err(TMDError::IOError)?;
|
||||||
let access_rights = buf.read_u32::<BigEndian>()?;
|
let access_rights = buf.read_u32::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let title_version = buf.read_u16::<BigEndian>()?;
|
let title_version = buf.read_u16::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let num_contents = buf.read_u16::<BigEndian>()?;
|
let num_contents = buf.read_u16::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let boot_index = buf.read_u16::<BigEndian>()?;
|
let boot_index = buf.read_u16::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let minor_version = buf.read_u16::<BigEndian>()?;
|
let minor_version = buf.read_u16::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
// Build content records by iterating over the rest of the data num_contents times.
|
// Build content records by iterating over the rest of the data num_contents times.
|
||||||
let mut content_records = Vec::with_capacity(num_contents as usize);
|
let mut content_records = Vec::with_capacity(num_contents as usize);
|
||||||
for _ in 0..num_contents {
|
for _ in 0..num_contents {
|
||||||
let content_id = buf.read_u32::<BigEndian>()?;
|
let content_id = buf.read_u32::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let index = buf.read_u16::<BigEndian>()?;
|
let index = buf.read_u16::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let content_type = buf.read_u16::<BigEndian>()?;
|
let type_int = buf.read_u16::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let content_size = buf.read_u64::<BigEndian>()?;
|
let content_type = match type_int {
|
||||||
|
1 => ContentType::Normal,
|
||||||
|
2 => ContentType::Development,
|
||||||
|
3 => ContentType::HashTree,
|
||||||
|
16385 => ContentType::DLC,
|
||||||
|
32769 => ContentType::Shared,
|
||||||
|
_ => return Err(TMDError::InvalidContentType(type_int))
|
||||||
|
};
|
||||||
|
let content_size = buf.read_u64::<BigEndian>().map_err(TMDError::IOError)?;
|
||||||
let mut content_hash = [0u8; 20];
|
let mut content_hash = [0u8; 20];
|
||||||
buf.read_exact(&mut content_hash)?;
|
buf.read_exact(&mut content_hash).map_err(TMDError::IOError)?;
|
||||||
content_records.push(ContentRecord {
|
content_records.push(ContentRecord {
|
||||||
content_id,
|
content_id,
|
||||||
index,
|
index,
|
||||||
@ -161,10 +244,97 @@ impl TMD {
|
|||||||
for content in &self.content_records {
|
for content in &self.content_records {
|
||||||
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)?;
|
||||||
buf.write_u16::<BigEndian>(content.content_type)?;
|
match content.content_type {
|
||||||
|
ContentType::Normal => { buf.write_u16::<BigEndian>(1)?; },
|
||||||
|
ContentType::Development => { buf.write_u16::<BigEndian>(2)?; },
|
||||||
|
ContentType::HashTree => { buf.write_u16::<BigEndian>(3)?; },
|
||||||
|
ContentType::DLC => { buf.write_u16::<BigEndian>(16385)?; },
|
||||||
|
ContentType::Shared => { buf.write_u16::<BigEndian>(32769)?; }
|
||||||
|
}
|
||||||
buf.write_u64::<BigEndian>(content.content_size)?;
|
buf.write_u64::<BigEndian>(content.content_size)?;
|
||||||
buf.write_all(&content.content_hash)?;
|
buf.write_all(&content.content_hash)?;
|
||||||
}
|
}
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_fakesigned(&self) -> bool {
|
||||||
|
// Can't be fakesigned without a null signature.
|
||||||
|
if self.signature != [0; 256] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Test the hash of the TMD body to make sure it starts with 00.
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
let tmd_body = self.to_bytes().unwrap();
|
||||||
|
hasher.update(&tmd_body[320..]);
|
||||||
|
let result = hasher.finalize();
|
||||||
|
if result[0] != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fakesign(&mut self) -> Result<(), TMDError> {
|
||||||
|
// Erase the signature.
|
||||||
|
self.signature = [0; 256];
|
||||||
|
let mut current_int: u16 = 0;
|
||||||
|
let mut test_hash: [u8; 20] = [255; 20];
|
||||||
|
while test_hash[0] != 0 {
|
||||||
|
if current_int == 65535 { return Err(TMDError::CannotFakesign); }
|
||||||
|
current_int += 1;
|
||||||
|
self.minor_version = current_int;
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
let ticket_body = self.to_bytes().unwrap();
|
||||||
|
hasher.update(&ticket_body[320..]);
|
||||||
|
test_hash = <[u8; 20]>::from(hasher.finalize());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn region(&self) -> &str {
|
||||||
|
match self.region {
|
||||||
|
0 => "JPN",
|
||||||
|
1 => "USA",
|
||||||
|
2 => "EUR",
|
||||||
|
3 => "None",
|
||||||
|
4 => "KOR",
|
||||||
|
_ => "Unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title_type(&self) -> TitleType {
|
||||||
|
match hex::encode(self.title_id)[..8].to_string().as_str() {
|
||||||
|
"00000001" => TitleType::System,
|
||||||
|
"00010000" => TitleType::Game,
|
||||||
|
"00010001" => TitleType::Channel,
|
||||||
|
"00010002" => TitleType::SystemChannel,
|
||||||
|
"00010004" => TitleType::GameChannel,
|
||||||
|
"00010005" => TitleType::DLC,
|
||||||
|
"00010008" => TitleType::HiddenChannel,
|
||||||
|
_ => TitleType::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content_type(&self, index: usize) -> ContentType {
|
||||||
|
// Find possible content indices, because the provided one could exist while the indices
|
||||||
|
// are out of order, which could cause problems finding the content.
|
||||||
|
let mut content_indices = Vec::new();
|
||||||
|
for record in &self.content_records {
|
||||||
|
content_indices.push(record.index);
|
||||||
|
}
|
||||||
|
let target_index = content_indices.index(index);
|
||||||
|
match self.content_records[*target_index as usize].content_type {
|
||||||
|
ContentType::Normal => ContentType::Normal,
|
||||||
|
ContentType::Development => ContentType::Development,
|
||||||
|
ContentType::HashTree => ContentType::HashTree,
|
||||||
|
ContentType::DLC => ContentType::DLC,
|
||||||
|
ContentType::Shared => ContentType::Shared,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_access_right(&self, right: AccessRight) -> bool {
|
||||||
|
match right {
|
||||||
|
AccessRight::AHB => (self.access_rights & (1 << 0)) != 0,
|
||||||
|
AccessRight::DVDVideo => (self.access_rights & (1 << 1)) != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,14 @@ use std::str;
|
|||||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use crate::title::{tmd, ticket, content};
|
use crate::title::{tmd, ticket, content};
|
||||||
|
use crate::title::ticket::TicketError;
|
||||||
|
use crate::title::tmd::TMDError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum WADError {
|
pub enum WADError {
|
||||||
BadType,
|
BadType,
|
||||||
|
TMDError(TMDError),
|
||||||
|
TicketError(TicketError),
|
||||||
IOError(std::io::Error),
|
IOError(std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +24,8 @@ impl fmt::Display for WADError {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let description = match *self {
|
let description = match *self {
|
||||||
WADError::BadType => "An invalid WAD type was specified.",
|
WADError::BadType => "An invalid WAD type was specified.",
|
||||||
|
WADError::TMDError(_) => "An error occurred while loading TMD data.",
|
||||||
|
WADError::TicketError(_) => "An error occurred while loading Ticket data.",
|
||||||
WADError::IOError(_) => "The provided WAD data was invalid.",
|
WADError::IOError(_) => "The provided WAD data was invalid.",
|
||||||
};
|
};
|
||||||
f.write_str(description)
|
f.write_str(description)
|
||||||
@ -68,7 +74,7 @@ impl WADHeader {
|
|||||||
pub fn from_body(body: &WADBody) -> Result<WADHeader, WADError> {
|
pub fn from_body(body: &WADBody) -> Result<WADHeader, WADError> {
|
||||||
// Generates a new WADHeader from a populated WADBody object.
|
// Generates a new WADHeader from a populated WADBody object.
|
||||||
// Parse the TMD and use that to determine if this is a standard WAD or a boot2 WAD.
|
// Parse the TMD and use that to determine if this is a standard WAD or a boot2 WAD.
|
||||||
let tmd = tmd::TMD::from_bytes(&body.tmd).map_err(WADError::IOError)?;
|
let tmd = tmd::TMD::from_bytes(&body.tmd).map_err(WADError::TMDError)?;
|
||||||
let wad_type = match hex::encode(tmd.title_id).as_str() {
|
let wad_type = match hex::encode(tmd.title_id).as_str() {
|
||||||
"0000000100000001" => WADType::ImportBoot,
|
"0000000100000001" => WADType::ImportBoot,
|
||||||
_ => WADType::Installable,
|
_ => WADType::Installable,
|
||||||
@ -233,6 +239,8 @@ impl WAD {
|
|||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cert_chain_size(&self) -> u32 { self.header.cert_chain_size }
|
||||||
|
|
||||||
pub fn cert_chain(&self) -> Vec<u8> {
|
pub fn cert_chain(&self) -> Vec<u8> {
|
||||||
self.body.cert_chain.clone()
|
self.body.cert_chain.clone()
|
||||||
}
|
}
|
||||||
@ -242,6 +250,8 @@ impl WAD {
|
|||||||
self.header.cert_chain_size = cert_chain.len() as u32;
|
self.header.cert_chain_size = cert_chain.len() as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn crl_size(&self) -> u32 { self.header.crl_size }
|
||||||
|
|
||||||
pub fn crl(&self) -> Vec<u8> {
|
pub fn crl(&self) -> Vec<u8> {
|
||||||
self.body.crl.clone()
|
self.body.crl.clone()
|
||||||
}
|
}
|
||||||
@ -251,6 +261,8 @@ impl WAD {
|
|||||||
self.header.crl_size = crl.len() as u32;
|
self.header.crl_size = crl.len() as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ticket_size(&self) -> u32 { self.header.ticket_size }
|
||||||
|
|
||||||
pub fn ticket(&self) -> Vec<u8> {
|
pub fn ticket(&self) -> Vec<u8> {
|
||||||
self.body.ticket.clone()
|
self.body.ticket.clone()
|
||||||
}
|
}
|
||||||
@ -260,6 +272,8 @@ impl WAD {
|
|||||||
self.header.ticket_size = ticket.len() as u32;
|
self.header.ticket_size = ticket.len() as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tmd_size(&self) -> u32 { self.header.tmd_size }
|
||||||
|
|
||||||
pub fn tmd(&self) -> Vec<u8> {
|
pub fn tmd(&self) -> Vec<u8> {
|
||||||
self.body.tmd.clone()
|
self.body.tmd.clone()
|
||||||
}
|
}
|
||||||
@ -269,6 +283,8 @@ impl WAD {
|
|||||||
self.header.tmd_size = tmd.len() as u32;
|
self.header.tmd_size = tmd.len() as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn content_size(&self) -> u32 { self.header.content_size }
|
||||||
|
|
||||||
pub fn content(&self) -> Vec<u8> {
|
pub fn content(&self) -> Vec<u8> {
|
||||||
self.body.content.clone()
|
self.body.content.clone()
|
||||||
}
|
}
|
||||||
@ -278,6 +294,8 @@ impl WAD {
|
|||||||
self.header.content_size = content.len() as u32;
|
self.header.content_size = content.len() as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn meta_size(&self) -> u32 { self.header.meta_size }
|
||||||
|
|
||||||
pub fn meta(&self) -> Vec<u8> {
|
pub fn meta(&self) -> Vec<u8> {
|
||||||
self.body.meta.clone()
|
self.body.meta.clone()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user