mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-07 16:01:01 -04:00
Compare commits
2 Commits
577d5a0efa
...
481594345d
Author | SHA1 | Date | |
---|---|---|---|
481594345d | |||
277c5d6439 |
42
Cargo.lock
generated
42
Cargo.lock
generated
@ -921,7 +921,7 @@ dependencies = [
|
|||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -1108,8 +1108,18 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1119,7 +1129,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1131,6 +1151,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.15",
|
"getrandom 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@ -1232,7 +1261,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"pkcs1",
|
"pkcs1",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"sha2",
|
"sha2",
|
||||||
"signature",
|
"signature",
|
||||||
"spki",
|
"spki",
|
||||||
@ -1257,6 +1286,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"glob",
|
"glob",
|
||||||
"hex",
|
"hex",
|
||||||
|
"rand 0.9.1",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rsa",
|
"rsa",
|
||||||
@ -1439,7 +1469,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -36,3 +36,4 @@ clap = { version = "4", features = ["derive"] }
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
reqwest = { version = "0", features = ["blocking"] }
|
reqwest = { version = "0", features = ["blocking"] }
|
||||||
|
rand = "0"
|
||||||
|
@ -11,8 +11,8 @@ 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.len());
|
println!("num content records: {:?}", title.tmd.content_records.borrow().len());
|
||||||
println!("first record data: {:?}", title.tmd.content_records.first().unwrap());
|
println!("first record data: {:?}", title.tmd.content_records.borrow().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);
|
||||||
|
@ -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.num_contents);
|
println!(" Total Contents: {}", tmd.content_records.borrow().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 {
|
for content in tmd.content_records.borrow().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);
|
||||||
|
@ -119,18 +119,24 @@ fn main() -> Result<()> {
|
|||||||
},
|
},
|
||||||
Some(Commands::Wad { command }) => {
|
Some(Commands::Wad { command }) => {
|
||||||
match command {
|
match command {
|
||||||
|
title::wad::Commands::Add { input, content, output, cid, r#type } => {
|
||||||
|
title::wad::add_wad(input, content, output, cid, r#type)?
|
||||||
|
},
|
||||||
title::wad::Commands::Convert { input, target, output } => {
|
title::wad::Commands::Convert { input, target, output } => {
|
||||||
title::wad::convert_wad(input, target, output)?
|
title::wad::convert_wad(input, target, output)?
|
||||||
},
|
},
|
||||||
title::wad::Commands::Pack { input, output} => {
|
title::wad::Commands::Pack { input, output} => {
|
||||||
title::wad::pack_wad(input, output)?
|
title::wad::pack_wad(input, output)?
|
||||||
},
|
},
|
||||||
title::wad::Commands::Unpack { input, output } => {
|
title::wad::Commands::Remove { input, output, identifier } => {
|
||||||
title::wad::unpack_wad(input, output)?
|
title::wad::remove_wad(input, output, identifier)?
|
||||||
},
|
},
|
||||||
title::wad::Commands::Set { input, content, output, identifier, r#type} => {
|
title::wad::Commands::Set { input, content, output, identifier, r#type} => {
|
||||||
title::wad::set_wad(input, content, output, identifier, r#type)?
|
title::wad::set_wad(input, content, output, identifier, r#type)?
|
||||||
}
|
},
|
||||||
|
title::wad::Commands::Unpack { input, output } => {
|
||||||
|
title::wad::unpack_wad(input, output)?
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => { /* Clap handles no passed command by itself */}
|
None => { /* Clap handles no passed command by itself */}
|
||||||
|
@ -109,7 +109,7 @@ pub fn download_content(tid: &str, cid: &str, version: &Option<String>, output:
|
|||||||
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.iter()
|
let (content_hash, content_size, content_index) = tmd.content_records.borrow().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.")?;
|
||||||
@ -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 {
|
for record in title.content.content_records.borrow().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 {
|
for record in content_region.content_records.borrow().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))?;
|
||||||
@ -241,9 +241,9 @@ pub fn download_title(tid: &str, version: &Option<String>, output: &TitleOutputT
|
|||||||
};
|
};
|
||||||
// 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 {
|
for record in tmd.content_records.borrow().iter() {
|
||||||
println!(" - Downloading content {} of {} (Content ID: {}, Size: {} bytes)...",
|
println!(" - Downloading content {} of {} (Content ID: {}, Size: {} bytes)...",
|
||||||
record.index + 1, &tmd.content_records.len(), record.content_id, record.content_size);
|
record.index + 1, &tmd.content_records.borrow().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!");
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,30 @@ use std::path::{Path, PathBuf};
|
|||||||
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 rand::prelude::*;
|
||||||
use rustii::title::{cert, crypto, tmd, ticket, content, wad};
|
use rustii::title::{cert, crypto, tmd, ticket, content, wad};
|
||||||
use rustii::title;
|
use rustii::title;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
#[command(arg_required_else_help = true)]
|
#[command(arg_required_else_help = true)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
|
/// Add new content to a WAD file
|
||||||
|
Add {
|
||||||
|
/// The path to the WAD file to modify
|
||||||
|
input: String,
|
||||||
|
/// The path to the new content to add
|
||||||
|
content: String,
|
||||||
|
/// An optional output path; defaults to overwriting input WAD file
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
/// An optional Content ID for the new content; defaults to being randomly assigned
|
||||||
|
#[arg(short, long)]
|
||||||
|
cid: Option<String>,
|
||||||
|
/// An optional type for the new content, can be "Normal", "Shared", or "DLC"; defaults to
|
||||||
|
/// "Normal"
|
||||||
|
#[arg(short, long)]
|
||||||
|
r#type: Option<String>,
|
||||||
|
},
|
||||||
/// Re-encrypt a WAD file with a different key
|
/// Re-encrypt a WAD file with a different key
|
||||||
Convert {
|
Convert {
|
||||||
/// The path to the WAD to convert
|
/// The path to the WAD to convert
|
||||||
@ -31,18 +49,21 @@ pub enum Commands {
|
|||||||
/// The name of the packed WAD file
|
/// The name of the packed WAD file
|
||||||
output: String
|
output: String
|
||||||
},
|
},
|
||||||
/// Unpack a WAD file into a directory
|
/// Remove content from a WAD file
|
||||||
Unpack {
|
Remove {
|
||||||
/// The path to the WAD to unpack
|
/// The path to the WAD file to modify
|
||||||
input: String,
|
input: String,
|
||||||
/// The directory to extract the WAD to
|
/// An optional output path; defaults to overwriting input WAD file
|
||||||
output: String
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
identifier: ContentIdentifier,
|
||||||
},
|
},
|
||||||
/// Replace existing content in a WAD file with new data
|
/// Replace existing content in a WAD file with new data
|
||||||
Set {
|
Set {
|
||||||
/// The path to the WAD file to modify
|
/// The path to the WAD file to modify
|
||||||
input: String,
|
input: String,
|
||||||
/// The new WAD content
|
/// The path to the new content to set
|
||||||
content: String,
|
content: String,
|
||||||
/// An optional output path; defaults to overwriting input WAD file
|
/// An optional output path; defaults to overwriting input WAD file
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
@ -52,7 +73,14 @@ pub enum Commands {
|
|||||||
r#type: Option<String>,
|
r#type: Option<String>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
identifier: ContentIdentifier,
|
identifier: ContentIdentifier,
|
||||||
}
|
},
|
||||||
|
/// Unpack a WAD file into a directory
|
||||||
|
Unpack {
|
||||||
|
/// The path to the WAD to unpack
|
||||||
|
input: String,
|
||||||
|
/// The directory to extract the WAD to
|
||||||
|
output: String
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
@ -74,10 +102,10 @@ pub struct ConvertTargets {
|
|||||||
#[clap(next_help_heading = "Content Identifier")]
|
#[clap(next_help_heading = "Content Identifier")]
|
||||||
#[group(multiple = false, required = true)]
|
#[group(multiple = false, required = true)]
|
||||||
pub struct ContentIdentifier {
|
pub struct ContentIdentifier {
|
||||||
/// The index of the content to replace
|
/// The index of the target content
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
index: Option<usize>,
|
index: Option<usize>,
|
||||||
/// The Content ID of the content to replace
|
/// The Content ID of the target content
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
cid: Option<String>,
|
cid: Option<String>,
|
||||||
}
|
}
|
||||||
@ -98,6 +126,60 @@ impl fmt::Display for Target {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_wad(input: &str, content: &str, output: &Option<String>, cid: &Option<String>, ctype: &Option<String>) -> Result<()> {
|
||||||
|
let in_path = Path::new(input);
|
||||||
|
if !in_path.exists() {
|
||||||
|
bail!("Source WAD \"{}\" could not be found.", in_path.display());
|
||||||
|
}
|
||||||
|
let content_path = Path::new(content);
|
||||||
|
if !content_path.exists() {
|
||||||
|
bail!("New content \"{}\" could not be found.", content_path.display());
|
||||||
|
}
|
||||||
|
let out_path = if output.is_some() {
|
||||||
|
PathBuf::from(output.clone().unwrap()).with_extension("wad")
|
||||||
|
} else {
|
||||||
|
in_path.to_path_buf()
|
||||||
|
};
|
||||||
|
// Load the WAD and parse the target type and Content ID.
|
||||||
|
let mut title = title::Title::from_bytes(&fs::read(in_path)?).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
|
||||||
|
let new_content = fs::read(content_path)?;
|
||||||
|
let target_type = if ctype.is_some() {
|
||||||
|
match ctype.clone().unwrap().to_ascii_lowercase().as_str() {
|
||||||
|
"normal" => tmd::ContentType::Normal,
|
||||||
|
"shared" => tmd::ContentType::Shared,
|
||||||
|
"dlc" => tmd::ContentType::DLC,
|
||||||
|
_ => bail!("The specified content type \"{}\" is invalid!", ctype.clone().unwrap()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Using default type \"Normal\" because no content type was specified.");
|
||||||
|
tmd::ContentType::Normal
|
||||||
|
};
|
||||||
|
let target_cid = if cid.is_some() {
|
||||||
|
let cid = u32::from_str_radix(cid.clone().unwrap().as_str(), 16).with_context(|| "The specified Content ID is invalid!")?;
|
||||||
|
if title.content.content_records.borrow().iter().any(|record| record.content_id == cid) {
|
||||||
|
bail!("The specified Content ID \"{:08X}\" is already being used in this WAD!", cid);
|
||||||
|
}
|
||||||
|
cid
|
||||||
|
} else {
|
||||||
|
// Generate a random CID if one wasn't specified, and ensure that it isn't already in use.
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let mut cid: u32;
|
||||||
|
loop {
|
||||||
|
cid = rng.random_range(0..=0xFF);
|
||||||
|
if !title.content.content_records.borrow().iter().any(|record| record.content_id == cid) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Generated new random Content ID \"{:08X}\" ({}) because no Content ID was specified.", cid, cid);
|
||||||
|
cid
|
||||||
|
};
|
||||||
|
title.add_content(&new_content, target_cid, target_type.clone()).with_context(|| "An unknown error occurred while setting the new content.")?;
|
||||||
|
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified WAD.")?;
|
||||||
|
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||||
|
println!("Successfully added new content with Content ID \"{:08X}\" ({}) and type \"{}\" to WAD file \"{}\"!", target_cid, target_cid, target_type, out_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>) -> Result<()> {
|
pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>) -> Result<()> {
|
||||||
let in_path = Path::new(input);
|
let in_path = Path::new(input);
|
||||||
if !in_path.exists() {
|
if !in_path.exists() {
|
||||||
@ -214,7 +296,7 @@ pub fn pack_wad(input: &str, output: &str) -> Result<()> {
|
|||||||
}
|
}
|
||||||
// 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(tmd.content_records.clone())?;
|
let mut content_region = content::ContentRegion::new(tmd.content_records.clone())?;
|
||||||
for content in tmd.content_records.clone() {
|
for content in tmd.content_records.borrow().iter() {
|
||||||
let data = fs::read(format!("{}/{:08X}.app", in_path.display(), content.index)).with_context(|| format!("Could not open content file \"{:08X}.app\" for reading.", content.index))?;
|
let data = fs::read(format!("{}/{:08X}.app", in_path.display(), content.index)).with_context(|| format!("Could not open content file \"{:08X}.app\" for reading.", content.index))?;
|
||||||
content_region.set_content(&data, content.index as usize, None, None, tik.dec_title_key())
|
content_region.set_content(&data, content.index as usize, None, None, tik.dec_title_key())
|
||||||
.with_context(|| "Failed to load content into the ContentRegion.")?;
|
.with_context(|| "Failed to load content into the ContentRegion.")?;
|
||||||
@ -239,35 +321,37 @@ pub fn pack_wad(input: &str, output: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unpack_wad(input: &str, output: &str) -> Result<()> {
|
pub fn remove_wad(input: &str, output: &Option<String>, identifier: &ContentIdentifier) -> Result<()> {
|
||||||
let in_path = Path::new(input);
|
let in_path = Path::new(input);
|
||||||
if !in_path.exists() {
|
if !in_path.exists() {
|
||||||
bail!("Source WAD \"{}\" could not be found.", input);
|
bail!("Source WAD \"{}\" could not be found.", in_path.display());
|
||||||
}
|
}
|
||||||
let wad_file = fs::read(in_path).with_context(|| format!("Failed to open WAD file \"{}\" for reading.", in_path.display()))?;
|
let out_path = if output.is_some() {
|
||||||
let title = title::Title::from_bytes(&wad_file).with_context(|| format!("The provided WAD file \"{}\" appears to be invalid.", in_path.display()))?;
|
PathBuf::from(output.clone().unwrap()).with_extension("wad")
|
||||||
let tid = hex::encode(title.tmd.title_id);
|
} else {
|
||||||
// Create output directory if it doesn't exist.
|
in_path.to_path_buf()
|
||||||
let out_path = Path::new(output);
|
};
|
||||||
if !out_path.exists() {
|
let mut title = title::Title::from_bytes(&fs::read(in_path)?).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
|
||||||
fs::create_dir(out_path).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?;
|
// Parse the identifier passed to choose how to find and remove the target.
|
||||||
|
// ...maybe don't take the above comment out of context
|
||||||
|
if identifier.index.is_some() {
|
||||||
|
title.content.remove_content(identifier.index.unwrap()).with_context(|| "The specified index does not exist in the provided WAD!")?;
|
||||||
|
println!("{:?}", title.tmd);
|
||||||
|
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified WAD.")?;
|
||||||
|
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||||
|
println!("Successfully removed content at index {} in WAD file \"{}\".", identifier.index.unwrap(), out_path.display());
|
||||||
|
} else if identifier.cid.is_some() {
|
||||||
|
let cid = u32::from_str_radix(identifier.cid.clone().unwrap().as_str(), 16).with_context(|| "The specified Content ID is invalid!")?;
|
||||||
|
let index = match title.content.get_index_from_cid(cid) {
|
||||||
|
Ok(index) => index,
|
||||||
|
Err(_) => bail!("The specified Content ID \"{}\" ({}) does not exist in this WAD!", identifier.cid.clone().unwrap(), cid),
|
||||||
|
};
|
||||||
|
title.content.remove_content(index).with_context(|| "An unknown error occurred while removing content from the WAD.")?;
|
||||||
|
println!("{:?}", title.tmd);
|
||||||
|
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified WAD.")?;
|
||||||
|
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||||
|
println!("Successfully removed content with Content ID \"{}\" ({}) in WAD file \"{}\".", identifier.cid.clone().unwrap(), cid, out_path.display());
|
||||||
}
|
}
|
||||||
// Write out all WAD components.
|
|
||||||
let tmd_file_name = format!("{}.tmd", tid);
|
|
||||||
fs::write(Path::join(out_path, tmd_file_name.clone()), title.tmd.to_bytes()?).with_context(|| format!("Failed to open TMD file \"{}\" for writing.", tmd_file_name))?;
|
|
||||||
let ticket_file_name = format!("{}.tik", tid);
|
|
||||||
fs::write(Path::join(out_path, ticket_file_name.clone()), title.ticket.to_bytes()?).with_context(|| format!("Failed to open Ticket file \"{}\" for writing.", ticket_file_name))?;
|
|
||||||
let cert_file_name = format!("{}.cert", tid);
|
|
||||||
fs::write(Path::join(out_path, cert_file_name.clone()), title.cert_chain.to_bytes()?).with_context(|| format!("Failed to open certificate chain file \"{}\" for writing.", cert_file_name))?;
|
|
||||||
let meta_file_name = format!("{}.footer", tid);
|
|
||||||
fs::write(Path::join(out_path, meta_file_name.clone()), title.meta()).with_context(|| format!("Failed to open footer file \"{}\" for writing.", meta_file_name))?;
|
|
||||||
// Iterate over contents, decrypt them, and write them out.
|
|
||||||
for i in 0..title.tmd.num_contents {
|
|
||||||
let content_file_name = format!("{:08X}.app", title.content.content_records[i as usize].index);
|
|
||||||
let dec_content = title.get_content_by_index(i as usize).with_context(|| format!("Failed to unpack content with Content ID {:08X}.", title.content.content_records[i as usize].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 as usize].content_id))?;
|
|
||||||
}
|
|
||||||
println!("WAD file unpacked!");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,7 +364,6 @@ pub fn set_wad(input: &str, content: &str, output: &Option<String>, identifier:
|
|||||||
if !content_path.exists() {
|
if !content_path.exists() {
|
||||||
bail!("New content \"{}\" could not be found.", content_path.display());
|
bail!("New content \"{}\" could not be found.", content_path.display());
|
||||||
}
|
}
|
||||||
// Get the output name now that we know the target, if one wasn't passed.
|
|
||||||
let out_path = if output.is_some() {
|
let out_path = if output.is_some() {
|
||||||
PathBuf::from(output.clone().unwrap()).with_extension("wad")
|
PathBuf::from(output.clone().unwrap()).with_extension("wad")
|
||||||
} else {
|
} else {
|
||||||
@ -323,3 +406,35 @@ pub fn set_wad(input: &str, content: &str, output: &Option<String>, identifier:
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unpack_wad(input: &str, output: &str) -> Result<()> {
|
||||||
|
let in_path = Path::new(input);
|
||||||
|
if !in_path.exists() {
|
||||||
|
bail!("Source WAD \"{}\" could not be found.", input);
|
||||||
|
}
|
||||||
|
let wad_file = fs::read(in_path).with_context(|| format!("Failed to open WAD file \"{}\" for reading.", in_path.display()))?;
|
||||||
|
let title = title::Title::from_bytes(&wad_file).with_context(|| format!("The provided WAD file \"{}\" appears to be invalid.", in_path.display()))?;
|
||||||
|
let tid = hex::encode(title.tmd.title_id);
|
||||||
|
// Create output directory if it doesn't exist.
|
||||||
|
let out_path = Path::new(output);
|
||||||
|
if !out_path.exists() {
|
||||||
|
fs::create_dir(out_path).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?;
|
||||||
|
}
|
||||||
|
// Write out all WAD components.
|
||||||
|
let tmd_file_name = format!("{}.tmd", tid);
|
||||||
|
fs::write(Path::join(out_path, tmd_file_name.clone()), title.tmd.to_bytes()?).with_context(|| format!("Failed to open TMD file \"{}\" for writing.", tmd_file_name))?;
|
||||||
|
let ticket_file_name = format!("{}.tik", tid);
|
||||||
|
fs::write(Path::join(out_path, ticket_file_name.clone()), title.ticket.to_bytes()?).with_context(|| format!("Failed to open Ticket file \"{}\" for writing.", ticket_file_name))?;
|
||||||
|
let cert_file_name = format!("{}.cert", tid);
|
||||||
|
fs::write(Path::join(out_path, cert_file_name.clone()), title.cert_chain.to_bytes()?).with_context(|| format!("Failed to open certificate chain file \"{}\" for writing.", cert_file_name))?;
|
||||||
|
let meta_file_name = format!("{}.footer", tid);
|
||||||
|
fs::write(Path::join(out_path, meta_file_name.clone()), title.meta()).with_context(|| format!("Failed to open footer file \"{}\" for writing.", meta_file_name))?;
|
||||||
|
// Iterate over contents, decrypt them, and write them out.
|
||||||
|
for i in 0..title.tmd.content_records.borrow().len() {
|
||||||
|
let content_file_name = format!("{:08X}.app", title.content.content_records.borrow()[i].index);
|
||||||
|
let dec_content = title.get_content_by_index(i).with_context(|| format!("Failed to unpack content with Content ID {:08X}.", title.content.content_records.borrow()[i].content_id))?;
|
||||||
|
fs::write(Path::join(out_path, content_file_name), dec_content).with_context(|| format!("Failed to open content file \"{:08X}.app\" for writing.", title.content.content_records.borrow()[i].content_id))?;
|
||||||
|
}
|
||||||
|
println!("WAD file unpacked!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -3,10 +3,11 @@
|
|||||||
//
|
//
|
||||||
// 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::content::ContentError::MissingContents;
|
|
||||||
use crate::title::tmd::{ContentRecord, ContentType};
|
use crate::title::tmd::{ContentRecord, ContentType};
|
||||||
use crate::title::crypto;
|
use crate::title::crypto;
|
||||||
use crate::title::crypto::encrypt_content;
|
use crate::title::crypto::encrypt_content;
|
||||||
@ -19,6 +20,10 @@ pub enum ContentError {
|
|||||||
MissingContents { required: usize, found: usize },
|
MissingContents { required: usize, found: usize },
|
||||||
#[error("content with requested Content ID {0} could not be found")]
|
#[error("content with requested Content ID {0} could not be found")]
|
||||||
CIDNotFound(u32),
|
CIDNotFound(u32),
|
||||||
|
#[error("the specified index {0} already exists in the content records")]
|
||||||
|
IndexAlreadyExists(u16),
|
||||||
|
#[error("the specified Content ID {0} already exists in the content records")]
|
||||||
|
CIDAlreadyExists(u32),
|
||||||
#[error("content's hash did not match the expected value (was {hash}, expected {expected})")]
|
#[error("content's hash did not match the expected value (was {hash}, expected {expected})")]
|
||||||
BadHash { hash: String, expected: String },
|
BadHash { hash: String, expected: String },
|
||||||
#[error("content data is not in a valid format")]
|
#[error("content data is not in a valid format")]
|
||||||
@ -28,9 +33,8 @@ 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: Vec<ContentRecord>,
|
pub content_records: Rc<RefCell<Vec<ContentRecord>>>,
|
||||||
pub content_region_size: u32,
|
pub content_region_size: u32,
|
||||||
pub num_contents: u16,
|
|
||||||
pub content_start_offsets: Vec<u64>,
|
pub content_start_offsets: Vec<u64>,
|
||||||
pub contents: Vec<Vec<u8>>,
|
pub contents: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
@ -38,32 +42,31 @@ pub struct ContentRegion {
|
|||||||
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: Vec<ContentRecord>) -> Result<Self, ContentError> {
|
pub fn from_bytes(data: &[u8], content_records: Rc<RefCell<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.len() as u16;
|
let num_contents = content_records.borrow().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.iter().scan(0, |offset, record| {
|
.chain(content_records.borrow().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.len()).collect(); // Trims the extra final entry.
|
})).take(content_records.borrow().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[i as usize].content_size + 15) & !15;
|
let size = (content_records.borrow()[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,
|
content_records: Rc::clone(&content_records),
|
||||||
content_region_size,
|
content_region_size,
|
||||||
num_contents,
|
|
||||||
content_start_offsets,
|
content_start_offsets,
|
||||||
contents,
|
contents,
|
||||||
})
|
})
|
||||||
@ -71,29 +74,29 @@ 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: Vec<ContentRecord>) -> Result<Self, ContentError> {
|
pub fn from_contents(contents: Vec<Vec<u8>>, content_records: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
|
||||||
if contents.len() != content_records.len() {
|
if contents.len() != content_records.borrow().len() {
|
||||||
return Err(MissingContents { required: content_records.len(), found: contents.len()});
|
return Err(ContentError::MissingContents { required: content_records.borrow().len(), found: contents.len()});
|
||||||
}
|
}
|
||||||
let mut content_region = Self::new(content_records)?;
|
let mut content_region = Self::new(Rc::clone(&content_records))?;
|
||||||
for i in 0..contents.len() {
|
for i in 0..contents.len() {
|
||||||
content_region.load_enc_content(&contents[i], content_region.content_records[i].index as usize)?;
|
let target_index = content_region.content_records.borrow()[i].index;
|
||||||
|
content_region.load_enc_content(&contents[i], target_index as usize)?;
|
||||||
}
|
}
|
||||||
Ok(content_region)
|
Ok(content_region)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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: Vec<ContentRecord>) -> Result<Self, ContentError> {
|
pub fn new(content_records: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
|
||||||
let content_region_size: u64 = content_records.iter().map(|x| (x.content_size + 63) & !63).sum();
|
let content_region_size: u64 = content_records.borrow().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.len() as u16;
|
let num_contents = content_records.borrow().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,
|
content_records: Rc::clone(&content_records),
|
||||||
content_region_size,
|
content_region_size,
|
||||||
num_contents,
|
|
||||||
content_start_offsets,
|
content_start_offsets,
|
||||||
contents,
|
contents,
|
||||||
})
|
})
|
||||||
@ -102,7 +105,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.num_contents {
|
for i in 0..self.content_records.borrow().len() {
|
||||||
let mut content = self.contents[i as usize].clone();
|
let mut content = self.contents[i as usize].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);
|
||||||
@ -115,7 +118,7 @@ impl ContentRegion {
|
|||||||
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.iter()
|
let content_index = self.content_records.borrow().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 {
|
||||||
@ -127,7 +130,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.len() - 1 })?;
|
let content = self.contents.get(index).ok_or(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 })?;
|
||||||
Ok(content.clone())
|
Ok(content.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,20 +138,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[index].index);
|
let mut content_dec = crypto::decrypt_content(&content, title_key, self.content_records.borrow()[index].index);
|
||||||
content_dec.resize(self.content_records[index].content_size as usize, 0);
|
content_dec.resize(self.content_records.borrow()[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[index].content_hash {
|
if result[..] != self.content_records.borrow()[index].content_hash {
|
||||||
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records[index].content_hash) });
|
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records.borrow()[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.iter().position(|x| x.content_id == cid);
|
let index = self.content_records.borrow().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)
|
||||||
@ -159,7 +162,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.iter().position(|x| x.content_id == cid);
|
let index = self.content_records.borrow().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)
|
||||||
@ -171,8 +174,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.len() {
|
if index >= self.content_records.borrow().len() {
|
||||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
|
||||||
}
|
}
|
||||||
self.contents[index] = content.to_vec();
|
self.contents[index] = content.to_vec();
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -183,16 +186,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.len() {
|
if index >= self.content_records.borrow().len() {
|
||||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
|
||||||
}
|
}
|
||||||
self.content_records[index].content_size = content_size;
|
self.content_records.borrow_mut()[index].content_size = content_size;
|
||||||
self.content_records[index].content_hash = content_hash;
|
self.content_records.borrow_mut()[index].content_hash = content_hash;
|
||||||
if cid.is_some() {
|
if cid.is_some() {
|
||||||
self.content_records[index].content_id = cid.unwrap();
|
// Make sure that the new CID isn't already in use.
|
||||||
|
if self.content_records.borrow().iter().any(|record| record.content_id == cid.unwrap()) {
|
||||||
|
return Err(ContentError::CIDAlreadyExists(cid.unwrap()));
|
||||||
|
}
|
||||||
|
self.content_records.borrow_mut()[index].content_id = cid.unwrap();
|
||||||
}
|
}
|
||||||
if content_type.is_some() {
|
if content_type.is_some() {
|
||||||
self.content_records[index].content_type = content_type.unwrap();
|
self.content_records.borrow_mut()[index].content_type = content_type.unwrap();
|
||||||
}
|
}
|
||||||
self.contents[index] = content.to_vec();
|
self.contents[index] = content.to_vec();
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -202,18 +209,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.len() {
|
if index >= self.content_records.borrow().len() {
|
||||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().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[index].content_hash {
|
if result[..] != self.content_records.borrow()[index].content_hash {
|
||||||
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records[index].content_hash) });
|
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records.borrow()[index].content_hash) });
|
||||||
}
|
}
|
||||||
let content_enc = encrypt_content(content, title_key, self.content_records[index].index, self.content_records[index].content_size);
|
let content_enc = encrypt_content(content, title_key, self.content_records.borrow()[index].index, self.content_records.borrow()[index].content_size);
|
||||||
self.contents[index] = content_enc;
|
self.contents[index] = content_enc;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -231,4 +238,50 @@ impl ContentRegion {
|
|||||||
self.set_enc_content(&content_enc, index, content_size, content_hash, cid, content_type)?;
|
self.set_enc_content(&content_enc, index, content_size, content_hash, cid, content_type)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes the content at the specified index from the content list and content records. This
|
||||||
|
/// may leave a gap in the indexes recorded in the content records, but this should not cause
|
||||||
|
/// issues on the Wii or with correctly implemented WAD parsers.
|
||||||
|
pub fn remove_content(&mut self, index: usize) -> Result<(), ContentError> {
|
||||||
|
if self.contents.get(index).is_none() || self.content_records.borrow().get(index).is_none() {
|
||||||
|
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
|
||||||
|
}
|
||||||
|
self.contents.remove(index);
|
||||||
|
self.content_records.borrow_mut().remove(index);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new encrypted content to the end of the content list and content records. The provided
|
||||||
|
/// Content ID, type, index, and decrypted hash will be added to the record.
|
||||||
|
pub fn add_enc_content(&mut self, content: &[u8], index: u16, cid: u32, content_type: ContentType, content_size: u64, content_hash: [u8; 20]) -> Result<(), ContentError> {
|
||||||
|
// Return an error if the specified index or CID already exist in the records.
|
||||||
|
if self.content_records.borrow().iter().any(|record| record.index == index) {
|
||||||
|
return Err(ContentError::IndexAlreadyExists(index));
|
||||||
|
}
|
||||||
|
if self.content_records.borrow().iter().any(|record| record.content_id == cid) {
|
||||||
|
return Err(ContentError::CIDAlreadyExists(cid));
|
||||||
|
}
|
||||||
|
self.contents.push(content.to_vec());
|
||||||
|
self.content_records.borrow_mut().push(ContentRecord { content_id: cid, index, content_type, content_size, content_hash });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new decrypted content to the end of the content list and content records. The provided
|
||||||
|
/// Content ID and type will be added to the record alongside a hash of the decrypted data. An
|
||||||
|
/// index will be automatically assigned based on the highest index currently recorded in the
|
||||||
|
/// content records.
|
||||||
|
pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: ContentType, title_key: [u8; 16]) -> Result<(), ContentError> {
|
||||||
|
let max_index = self.content_records.borrow().iter()
|
||||||
|
.max_by_key(|record| record.index)
|
||||||
|
.map(|record| record.index)
|
||||||
|
.unwrap_or(0); // This should be impossible, but I guess 0 is a safe value just in case?
|
||||||
|
let new_index = max_index + 1;
|
||||||
|
let content_size = content.len() as u64;
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
hasher.update(content);
|
||||||
|
let content_hash: [u8; 20] = hasher.finalize().into();
|
||||||
|
let content_enc = encrypt_content(content, title_key, new_index, content_size);
|
||||||
|
self.add_enc_content(&content_enc, new_index, cid, content_type, content_size, content_hash)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ 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)]
|
||||||
@ -52,7 +53,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(), tmd.content_records.clone()).map_err(TitleError::Content)?;
|
let content = content::ContentRegion::from_bytes(&wad.content(), Rc::clone(&tmd.content_records)).map_err(TitleError::Content)?;
|
||||||
Ok(Title {
|
Ok(Title {
|
||||||
cert_chain,
|
cert_chain,
|
||||||
crl: wad.crl(),
|
crl: wad.crl(),
|
||||||
@ -137,7 +138,15 @@ impl Title {
|
|||||||
/// 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.dec_title_key())?;
|
||||||
self.tmd.content_records = self.content.content_records.clone();
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new decrypted content to the end of the content list and content records. The provided
|
||||||
|
/// Content ID and type will be added to the record alongside a hash of the decrypted data. An
|
||||||
|
/// index will be automatically assigned based on the highest index currently recorded in the
|
||||||
|
/// content records.
|
||||||
|
pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: tmd::ContentType) -> Result<(), TitleError> {
|
||||||
|
self.content.add_content(content, cid, content_type, self.ticket.dec_title_key())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +158,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 {
|
for record in self.tmd.content_records.borrow().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;
|
||||||
|
@ -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.iter().map(|record| { record.content_id }).collect();
|
let content_ids: Vec<u32> = tmd.content_records.borrow().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)?);
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
//
|
//
|
||||||
// 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;
|
||||||
@ -110,7 +112,7 @@ pub struct TMD {
|
|||||||
pub num_contents: u16,
|
pub num_contents: u16,
|
||||||
pub boot_index: u16,
|
pub boot_index: u16,
|
||||||
pub minor_version: u16, // Normally unused, but good for fakesigning!
|
pub minor_version: u16, // Normally unused, but good for fakesigning!
|
||||||
pub content_records: Vec<ContentRecord>,
|
pub content_records: Rc<RefCell<Vec<ContentRecord>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TMD {
|
impl TMD {
|
||||||
@ -204,7 +206,7 @@ impl TMD {
|
|||||||
num_contents,
|
num_contents,
|
||||||
boot_index,
|
boot_index,
|
||||||
minor_version,
|
minor_version,
|
||||||
content_records,
|
content_records: Rc::new(RefCell::new(content_records)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,11 +233,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.num_contents)?;
|
buf.write_u16::<BigEndian>(self.content_records.borrow().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 {
|
for content in self.content_records.borrow().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 {
|
||||||
@ -317,11 +319,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 {
|
for record in self.content_records.borrow().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[*target_index as usize].content_type {
|
match self.content_records.borrow()[*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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user