mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-06 15:31:02 -04:00
Ported all NUS download functions from libWiiPy and corresponding CLI commands
Also adds the basics of U8 archive packing/unpacking, however they are not in a usable state yet and there are no working CLI commands associated with them.
This commit is contained in:
parent
e55edc10fd
commit
be9148fcfa
1435
Cargo.lock
generated
1435
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -35,3 +35,4 @@ regex = "1"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
reqwest = { version = "0", features = ["blocking"] }
|
||||||
|
@ -3,3 +3,206 @@
|
|||||||
//
|
//
|
||||||
// Implements the structures and methods required for parsing U8 archives.
|
// Implements the structures and methods required for parsing U8 archives.
|
||||||
|
|
||||||
|
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum U8Error {
|
||||||
|
#[error("invalid file name at offset {0}")]
|
||||||
|
InvalidFileName(u64),
|
||||||
|
#[error("this does not appear to be a U8 archive (missing magic number)")]
|
||||||
|
NotU8Data,
|
||||||
|
#[error("U8 data is not in a valid format")]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct U8Node {
|
||||||
|
node_type: u8,
|
||||||
|
name_offset: u32, // This is really type u24, so the most significant byte will be ignored.
|
||||||
|
data_offset: u32,
|
||||||
|
size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct U8Archive {
|
||||||
|
u8_nodes: Vec<U8Node>,
|
||||||
|
file_names: Vec<String>,
|
||||||
|
file_data: Vec<Vec<u8>>,
|
||||||
|
root_node_offset: u32,
|
||||||
|
header_size: u32,
|
||||||
|
data_offset: u32,
|
||||||
|
padding: [u8; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl U8Archive {
|
||||||
|
/// Creates a new U8 instance from the binary data of a U8 file.
|
||||||
|
pub fn from_bytes(data: &[u8]) -> Result<Self, U8Error> {
|
||||||
|
let mut buf = Cursor::new(data);
|
||||||
|
let mut magic = [0u8; 4];
|
||||||
|
buf.read_exact(&mut magic)?;
|
||||||
|
// Check for an IMET header if the magic number isn't the correct value before throwing an
|
||||||
|
// error.
|
||||||
|
if &magic != b"\x55\xAA\x38\x2D" {
|
||||||
|
// Check for an IMET header immediately at the start of the file.
|
||||||
|
buf.seek(SeekFrom::Start(0x40))?;
|
||||||
|
buf.read_exact(&mut magic)?;
|
||||||
|
if &magic == b"\x49\x4D\x45\x54" {
|
||||||
|
// IMET with no build tag means the U8 archive should start at 0x600.
|
||||||
|
buf.seek(SeekFrom::Start(0x600))?;
|
||||||
|
buf.read_exact(&mut magic)?;
|
||||||
|
if &magic != b"\x55\xAA\x38\x2D" {
|
||||||
|
return Err(U8Error::NotU8Data);
|
||||||
|
}
|
||||||
|
println!("ignoring IMET header at 0x40");
|
||||||
|
}
|
||||||
|
// Check for an IMET header that comes after a built tag.
|
||||||
|
else {
|
||||||
|
buf.seek(SeekFrom::Start(0x80))?;
|
||||||
|
buf.read_exact(&mut magic)?;
|
||||||
|
if &magic == b"\x49\x4D\x45\x54" {
|
||||||
|
// IMET with a build tag means the U8 archive should start at 0x600.
|
||||||
|
buf.seek(SeekFrom::Start(0x640))?;
|
||||||
|
buf.read_exact(&mut magic)?;
|
||||||
|
if &magic != b"\x55\xAA\x38\x2D" {
|
||||||
|
return Err(U8Error::NotU8Data);
|
||||||
|
}
|
||||||
|
println!("ignoring IMET header at 0x80");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let root_node_offset = buf.read_u32::<BigEndian>()?;
|
||||||
|
let header_size = buf.read_u32::<BigEndian>()?;
|
||||||
|
let data_offset = buf.read_u32::<BigEndian>()?;
|
||||||
|
let mut padding = [0u8; 16];
|
||||||
|
buf.read_exact(&mut padding)?;
|
||||||
|
// Manually read the root node, since we need its size anyway to know how many nodes there
|
||||||
|
// are total.
|
||||||
|
let root_node_type = buf.read_u8()?;
|
||||||
|
let root_node_name_offset = buf.read_u24::<BigEndian>()?;
|
||||||
|
let root_node_data_offset = buf.read_u32::<BigEndian>()?;
|
||||||
|
let root_node_size = buf.read_u32::<BigEndian>()?;
|
||||||
|
let root_node = U8Node {
|
||||||
|
node_type: root_node_type,
|
||||||
|
name_offset: root_node_name_offset,
|
||||||
|
data_offset: root_node_data_offset,
|
||||||
|
size: root_node_size,
|
||||||
|
};
|
||||||
|
// Create a vec of nodes, push the root node, and then iterate over the remaining number
|
||||||
|
// of nodes in the file and push them to the vec.
|
||||||
|
let mut u8_nodes: Vec<U8Node> = Vec::new();
|
||||||
|
u8_nodes.push(root_node);
|
||||||
|
for _ in 1..root_node_size {
|
||||||
|
let node_type = buf.read_u8()?;
|
||||||
|
let name_offset = buf.read_u24::<BigEndian>()?;
|
||||||
|
let data_offset = buf.read_u32::<BigEndian>()?;
|
||||||
|
let size = buf.read_u32::<BigEndian>()?;
|
||||||
|
u8_nodes.push(U8Node { node_type, name_offset, data_offset, size })
|
||||||
|
}
|
||||||
|
// Iterate over the loaded nodes and load the file names and data associated with them.
|
||||||
|
let base_name_offset = buf.position();
|
||||||
|
let mut file_names = Vec::<String>::new();
|
||||||
|
let mut file_data = Vec::<Vec<u8>>::new();
|
||||||
|
for node in &u8_nodes {
|
||||||
|
buf.seek(SeekFrom::Start(base_name_offset + node.name_offset as u64))?;
|
||||||
|
let mut name_bin = Vec::<u8>::new();
|
||||||
|
// Read the file name one byte at a time until we find a null byte.
|
||||||
|
loop {
|
||||||
|
let byte = buf.read_u8()?;
|
||||||
|
if byte == b'\0' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
name_bin.push(byte);
|
||||||
|
}
|
||||||
|
file_names.push(String::from_utf8(name_bin).map_err(|_| U8Error::InvalidFileName(base_name_offset + node.name_offset as u64))?.to_owned());
|
||||||
|
// If this is a file node, read the data for the file.
|
||||||
|
if node.node_type == 0 {
|
||||||
|
buf.seek(SeekFrom::Start(node.data_offset as u64))?;
|
||||||
|
let mut data = vec![0u8; node.size as usize];
|
||||||
|
buf.read_exact(&mut data)?;
|
||||||
|
file_data.push(data);
|
||||||
|
} else {
|
||||||
|
file_data.push(Vec::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(U8Archive {
|
||||||
|
u8_nodes,
|
||||||
|
file_names,
|
||||||
|
file_data,
|
||||||
|
root_node_offset,
|
||||||
|
header_size,
|
||||||
|
data_offset,
|
||||||
|
padding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pack_dir() {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_dir(_input: &Path) -> Result<Self, U8Error> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dumps the data in a U8Archive instance back into binary data that can be written to a file.
|
||||||
|
pub fn to_bytes(&self) -> Result<Vec<u8>, U8Error> {
|
||||||
|
// Header size starts at 0 because the header size starts with the nodes and does not
|
||||||
|
// include the actual file header.
|
||||||
|
let mut header_size: u32 = 0;
|
||||||
|
// Add 12 bytes for each node, since that's how many bytes each one is made up of.
|
||||||
|
for _ in 0..self.u8_nodes.len() {
|
||||||
|
header_size += 12;
|
||||||
|
}
|
||||||
|
// Add the number of bytes used for each file/folder name in the string table.
|
||||||
|
for file_name in &self.file_names {
|
||||||
|
header_size += file_name.len() as u32 + 1
|
||||||
|
}
|
||||||
|
// The initial data offset is equal to the file header (32 bytes) + node data aligned to
|
||||||
|
// 64 bytes.
|
||||||
|
let data_offset: u32 = (header_size + 32 + 63) & !63;
|
||||||
|
// Adjust all nodes to place file data in the same order as the nodes. For some reason
|
||||||
|
// Nintendo-made U8 archives don't necessarily do this?
|
||||||
|
let mut current_data_offset = data_offset;
|
||||||
|
let mut current_name_offset: u32 = 0;
|
||||||
|
let mut u8_nodes = self.u8_nodes.clone();
|
||||||
|
for i in 0..u8_nodes.len() {
|
||||||
|
if u8_nodes[i].node_type == 0 {
|
||||||
|
u8_nodes[i].data_offset = (current_data_offset + 31) & !31;
|
||||||
|
current_data_offset += (u8_nodes[i].size + 31) & !31;
|
||||||
|
}
|
||||||
|
// Calculate the name offsets, including the extra 1 for the NULL byte.
|
||||||
|
u8_nodes[i].name_offset = current_name_offset;
|
||||||
|
current_name_offset += self.file_names[i].len() as u32 + 1
|
||||||
|
}
|
||||||
|
// Begin writing file data.
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
buf.write_all(b"\x55\xAA\x38\x2D")?;
|
||||||
|
buf.write_u32::<BigEndian>(0x20)?; // The root node offset is always 0x20.
|
||||||
|
buf.write_u32::<BigEndian>(header_size)?;
|
||||||
|
buf.write_u32::<BigEndian>(data_offset)?;
|
||||||
|
buf.write_all(&self.padding)?;
|
||||||
|
// Iterate over nodes and write them out.
|
||||||
|
for node in &u8_nodes {
|
||||||
|
buf.write_u8(node.node_type)?;
|
||||||
|
buf.write_u24::<BigEndian>(node.name_offset)?;
|
||||||
|
buf.write_u32::<BigEndian>(node.data_offset)?;
|
||||||
|
buf.write_u32::<BigEndian>(node.size)?;
|
||||||
|
}
|
||||||
|
// Iterate over file names with a null byte at the end.
|
||||||
|
for file_name in &self.file_names {
|
||||||
|
buf.write_all(file_name.as_bytes())?;
|
||||||
|
buf.write_u8(b'\0')?;
|
||||||
|
}
|
||||||
|
// Pad to the nearest multiple of 64 bytes.
|
||||||
|
buf.resize((buf.len() + 63) & !63, 0);
|
||||||
|
// Iterate over the file data and dump it. The file needs to be aligned to 32 bytes after
|
||||||
|
// each write.
|
||||||
|
for data in &self.file_data {
|
||||||
|
buf.write_all(data)?;
|
||||||
|
buf.resize((buf.len() + 31) & !31, 0);
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,3 +3,4 @@
|
|||||||
|
|
||||||
pub mod ash;
|
pub mod ash;
|
||||||
pub mod lz77;
|
pub mod lz77;
|
||||||
|
pub mod u8;
|
||||||
|
44
src/bin/rustii/archive/u8.rs
Normal file
44
src/bin/rustii/archive/u8.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// archive/u8.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii
|
||||||
|
//
|
||||||
|
// Code for the U8 packing/unpacking commands in the rustii CLI.
|
||||||
|
|
||||||
|
use std::{str, fs};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use clap::Subcommand;
|
||||||
|
use rustii::archive::u8;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Pack a directory into a U8 archive
|
||||||
|
Pack {
|
||||||
|
/// The directory to pack into a U8 archive
|
||||||
|
input: String,
|
||||||
|
/// The name of the packed U8 archive
|
||||||
|
output: String,
|
||||||
|
},
|
||||||
|
/// Unpack a U8 archive into a directory
|
||||||
|
Unpack {
|
||||||
|
/// The path to the U8 archive to unpack
|
||||||
|
input: String,
|
||||||
|
/// The directory to unpack the U8 archive to
|
||||||
|
output: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pack_u8_archive(_input: &str, _output: &str) -> Result<()> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unpack_u8_archive(input: &str, output: &str) -> Result<()> {
|
||||||
|
let in_path = Path::new(input);
|
||||||
|
if !in_path.exists() {
|
||||||
|
bail!("Source U8 archive \"{}\" could not be found.", input);
|
||||||
|
}
|
||||||
|
let u8_data = u8::U8Archive::from_bytes(&fs::read(in_path)?)?;
|
||||||
|
println!("{:?}", u8_data);
|
||||||
|
fs::write(Path::new(output), u8_data.to_bytes()?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -44,6 +44,14 @@ enum Commands {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: archive::lz77::Commands
|
command: archive::lz77::Commands
|
||||||
},
|
},
|
||||||
|
Nus {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: title::nus::Commands
|
||||||
|
},
|
||||||
|
U8 {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: archive::u8::Commands
|
||||||
|
},
|
||||||
/// Pack/unpack/edit a WAD file
|
/// Pack/unpack/edit a WAD file
|
||||||
Wad {
|
Wad {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@ -68,6 +76,9 @@ fn main() -> Result<()> {
|
|||||||
Some(Commands::Fakesign { input, output }) => {
|
Some(Commands::Fakesign { input, output }) => {
|
||||||
title::fakesign::fakesign(input, output)?
|
title::fakesign::fakesign(input, output)?
|
||||||
},
|
},
|
||||||
|
Some(Commands::Info { input }) => {
|
||||||
|
info::info(input)?
|
||||||
|
},
|
||||||
Some(Commands::Lz77 { command }) => {
|
Some(Commands::Lz77 { command }) => {
|
||||||
match command {
|
match command {
|
||||||
archive::lz77::Commands::Compress { input, output } => {
|
archive::lz77::Commands::Compress { input, output } => {
|
||||||
@ -78,8 +89,28 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(Commands::Info { input }) => {
|
Some(Commands::Nus { command }) => {
|
||||||
info::info(input)?
|
match command {
|
||||||
|
title::nus::Commands::Ticket { tid, output } => {
|
||||||
|
title::nus::download_ticket(tid, output)?
|
||||||
|
},
|
||||||
|
title::nus::Commands::Title { tid, version, output} => {
|
||||||
|
title::nus::download_title(tid, version, output)?
|
||||||
|
}
|
||||||
|
title::nus::Commands::Tmd { tid, version, output} => {
|
||||||
|
title::nus::download_tmd(tid, version, output)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Commands::U8 { command }) => {
|
||||||
|
match command {
|
||||||
|
archive::u8::Commands::Pack { input, output } => {
|
||||||
|
archive::u8::pack_u8_archive(input, output)?
|
||||||
|
},
|
||||||
|
archive::u8::Commands::Unpack { input, output } => {
|
||||||
|
archive::u8::unpack_u8_archive(input, output)?
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Some(Commands::Wad { command }) => {
|
Some(Commands::Wad { command }) => {
|
||||||
match command {
|
match command {
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
// https://github.com/NinjaCheetah/rustii
|
// https://github.com/NinjaCheetah/rustii
|
||||||
|
|
||||||
pub mod fakesign;
|
pub mod fakesign;
|
||||||
|
pub mod nus;
|
||||||
pub mod wad;
|
pub mod wad;
|
||||||
|
215
src/bin/rustii/title/nus.rs
Normal file
215
src/bin/rustii/title/nus.rs
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
// title/nus.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii
|
||||||
|
//
|
||||||
|
// Code for NUS-related commands in the rustii CLI.
|
||||||
|
|
||||||
|
use std::{str, fs};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use clap::{Subcommand, Args};
|
||||||
|
use rustii::title::{cert, content, nus, ticket, tmd};
|
||||||
|
use rustii::title;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Download a Ticket from the NUS
|
||||||
|
Ticket {
|
||||||
|
/// The Title ID that the Ticket is for
|
||||||
|
tid: String,
|
||||||
|
/// An optional Ticket name; defaults to <tid>.tik
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
},
|
||||||
|
/// Download a title from the NUS
|
||||||
|
Title {
|
||||||
|
/// The Title ID of the Title to download
|
||||||
|
tid: String,
|
||||||
|
/// The version of the Title to download
|
||||||
|
#[arg(short, long)]
|
||||||
|
version: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
output: TitleOutputType,
|
||||||
|
},
|
||||||
|
/// Download a TMD from the NUS
|
||||||
|
Tmd {
|
||||||
|
/// The Title ID that the TMD is for
|
||||||
|
tid: String,
|
||||||
|
/// The version of the TMD to download
|
||||||
|
#[arg(short, long)]
|
||||||
|
version: Option<String>,
|
||||||
|
/// An optional TMD name; defaults to <tid>.tmd
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[clap(next_help_heading = "Output Format")]
|
||||||
|
#[group(multiple = false, required = true)]
|
||||||
|
pub struct TitleOutputType {
|
||||||
|
/// Download the Title data to the specified output directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
/// Download the Title to a WAD file
|
||||||
|
#[arg(short, long)]
|
||||||
|
wad: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_ticket(tid: &str, output: &Option<String>) -> Result<()> {
|
||||||
|
println!("Downloading Ticket for title {tid}...");
|
||||||
|
if tid.len() != 16 {
|
||||||
|
bail!("The specified Title ID is invalid!");
|
||||||
|
}
|
||||||
|
let out_path = if output.is_some() {
|
||||||
|
PathBuf::from(output.clone().unwrap())
|
||||||
|
} else {
|
||||||
|
PathBuf::from(format!("{}.tik", tid))
|
||||||
|
};
|
||||||
|
let tid: [u8; 8] = hex::decode(tid)?.try_into().unwrap();
|
||||||
|
let tik_data = nus::download_ticket(tid, true).with_context(|| "Ticket data could not be downloaded.")?;
|
||||||
|
fs::write(&out_path, tik_data)?;
|
||||||
|
println!("Successfully downloaded Ticket to \"{}\"!", out_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_title_dir(title: title::Title, output: String) -> Result<()> {
|
||||||
|
println!(" - Saving downloaded data...");
|
||||||
|
let out_path = PathBuf::from(output);
|
||||||
|
if out_path.exists() {
|
||||||
|
if !out_path.is_dir() {
|
||||||
|
bail!("A file already exists with the specified directory name!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::create_dir(&out_path).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?;
|
||||||
|
}
|
||||||
|
let tid = hex::encode(title.tmd.title_id);
|
||||||
|
println!(" - Saving TMD...");
|
||||||
|
fs::write(out_path.join(format!("{}.tmd", &tid)), title.tmd.to_bytes()?).with_context(|| format!("Failed to open TMD file \"{}.tmd\" for writing.", tid))?;
|
||||||
|
println!(" - Saving Ticket...");
|
||||||
|
fs::write(out_path.join(format!("{}.tik", &tid)), title.ticket.to_bytes()?).with_context(|| format!("Failed to open Ticket file \"{}.tmd\" for writing.", tid))?;
|
||||||
|
println!(" - Saving certificate chain...");
|
||||||
|
fs::write(out_path.join(format!("{}.cert", &tid)), title.cert_chain.to_bytes()?).with_context(|| format!("Failed to open certificate chain file \"{}.cert\" for writing.", tid))?;
|
||||||
|
// Iterate over the content files and write them out in encrypted form.
|
||||||
|
for record in &title.content.content_records {
|
||||||
|
println!(" - Decrypting and saving content with Content ID {}...", record.content_id);
|
||||||
|
fs::write(out_path.join(format!("{:08X}.app", record.content_id)), title.get_content_by_cid(record.content_id)?)
|
||||||
|
.with_context(|| format!("Failed to open content file \"{:08X}.app\" for writing.", record.content_id))?;
|
||||||
|
}
|
||||||
|
println!("Successfully downloaded title with Title ID {} to directory \"{}\"!", tid, out_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_title_dir_enc(tmd: tmd::TMD, content_region: content::ContentRegion, cert_chain: cert::CertificateChain, output: String) -> Result<()> {
|
||||||
|
println!(" - Saving downloaded data...");
|
||||||
|
let out_path = PathBuf::from(output);
|
||||||
|
if out_path.exists() {
|
||||||
|
if !out_path.is_dir() {
|
||||||
|
bail!("A file already exists with the specified directory name!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::create_dir(&out_path).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?;
|
||||||
|
}
|
||||||
|
let tid = hex::encode(tmd.title_id);
|
||||||
|
println!(" - Saving TMD...");
|
||||||
|
fs::write(out_path.join(format!("{}.tmd", &tid)), tmd.to_bytes()?).with_context(|| format!("Failed to open TMD file \"{}.tmd\" for writing.", tid))?;
|
||||||
|
println!(" - Saving certificate chain...");
|
||||||
|
fs::write(out_path.join(format!("{}.cert", &tid)), cert_chain.to_bytes()?).with_context(|| format!("Failed to open certificate chain file \"{}.cert\" for writing.", tid))?;
|
||||||
|
// Iterate over the content files and write them out in encrypted form.
|
||||||
|
for record in &content_region.content_records {
|
||||||
|
println!(" - Saving content with Content ID {}...", record.content_id);
|
||||||
|
fs::write(out_path.join(format!("{:08X}", record.content_id)), content_region.get_enc_content_by_cid(record.content_id)?)
|
||||||
|
.with_context(|| format!("Failed to open content file \"{:08X}\" for writing.", record.content_id))?;
|
||||||
|
}
|
||||||
|
println!("Successfully downloaded title with Title ID {} to directory \"{}\"!", tid, out_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_title_wad(title: title::Title, output: String) -> Result<()> {
|
||||||
|
println!(" - Packing WAD...");
|
||||||
|
let out_path = PathBuf::from(output).with_extension("wad");
|
||||||
|
fs::write(&out_path, title.to_wad().with_context(|| "A WAD could not be packed.")?.to_bytes()?).with_context(|| format!("Could not open WAD file \"{}\" for writing.", out_path.display()))?;
|
||||||
|
println!("Successfully downloaded title with Title ID {} to WAD file \"{}\"!", hex::encode(title.tmd.title_id), out_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_title(tid: &str, version: &Option<String>, output: &TitleOutputType) -> Result<()> {
|
||||||
|
if tid.len() != 16 {
|
||||||
|
bail!("The specified Title ID is invalid!");
|
||||||
|
}
|
||||||
|
if version.is_some() {
|
||||||
|
println!("Downloading title {} v{}, please wait...", tid, version.clone().unwrap());
|
||||||
|
} else {
|
||||||
|
println!("Downloading title {} vLatest, please wait...", tid);
|
||||||
|
}
|
||||||
|
let version: Option<u16> = if version.is_some() {
|
||||||
|
Some(version.clone().unwrap().parse().with_context(|| "The specified Title version must be a valid integer!")?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let tid: [u8; 8] = hex::decode(tid)?.try_into().unwrap();
|
||||||
|
println!(" - Downloading and parsing TMD...");
|
||||||
|
let tmd = tmd::TMD::from_bytes(&nus::download_tmd(tid, version, true).with_context(|| "TMD data could not be downloaded.")?)?;
|
||||||
|
println!(" - Downloading and parsing Ticket...");
|
||||||
|
let tik_res = &nus::download_ticket(tid, true);
|
||||||
|
let tik = match tik_res {
|
||||||
|
Ok(tik) => Some(ticket::Ticket::from_bytes(tik)?),
|
||||||
|
Err(_) => {
|
||||||
|
if output.wad.is_some() {
|
||||||
|
bail!("--wad was specified, but this Title has no common Ticket and cannot be packed into a WAD!");
|
||||||
|
} else {
|
||||||
|
println!(" - No Ticket is available!");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Build a vec of contents by iterating over the content records and downloading each one.
|
||||||
|
let mut contents: Vec<Vec<u8>> = Vec::new();
|
||||||
|
for record in &tmd.content_records {
|
||||||
|
println!(" - Downloading content {} of {} (Content ID: {}, Size: {} bytes)...",
|
||||||
|
record.index + 1, &tmd.content_records.len(), record.content_id, record.content_size);
|
||||||
|
contents.push(nus::download_content(tid, record.content_id, true).with_context(|| format!("Content with Content ID {} could not be downloaded.", record.content_id))?);
|
||||||
|
println!(" - Done!");
|
||||||
|
}
|
||||||
|
let content_region = content::ContentRegion::from_contents(contents, tmd.content_records.clone())?;
|
||||||
|
println!(" - Building certificate chain...");
|
||||||
|
let cert_chain = cert::CertificateChain::from_bytes(&nus::download_cert_chain(true).with_context(|| "Certificate chain could not be built.")?)?;
|
||||||
|
if tik.is_some() {
|
||||||
|
// If we have a Ticket, then build a Title and jump to the output method.
|
||||||
|
let title = title::Title::from_parts(cert_chain, None, tik.unwrap(), tmd, content_region, None)?;
|
||||||
|
if output.wad.is_some() {
|
||||||
|
download_title_wad(title, output.wad.clone().unwrap())?;
|
||||||
|
} else {
|
||||||
|
download_title_dir(title, output.output.clone().unwrap())?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we're downloading to a directory and have no Ticket, save the TMD and encrypted
|
||||||
|
// contents to the directory only.
|
||||||
|
download_title_dir_enc(tmd, content_region, cert_chain, output.output.clone().unwrap())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_tmd(tid: &str, version: &Option<String>, output: &Option<String>) -> Result<()> {
|
||||||
|
let version: Option<u16> = if version.is_some() {
|
||||||
|
Some(version.clone().unwrap().parse().with_context(|| "The specified TMD version must be a valid integer!")?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
println!("Downloading TMD for title {tid}...");
|
||||||
|
if tid.len() != 16 {
|
||||||
|
bail!("The specified Title ID is invalid!");
|
||||||
|
}
|
||||||
|
let out_path = if output.is_some() {
|
||||||
|
PathBuf::from(output.clone().unwrap())
|
||||||
|
} else if version.is_some() {
|
||||||
|
PathBuf::from(format!("{}.tmd.{}", tid, version.unwrap()))
|
||||||
|
} else {
|
||||||
|
PathBuf::from(format!("{}.tmd", tid))
|
||||||
|
};
|
||||||
|
let tid: [u8; 8] = hex::decode(tid)?.try_into().unwrap();
|
||||||
|
let tmd_data = nus::download_tmd(tid, version, true).with_context(|| "TMD data could not be downloaded.")?;
|
||||||
|
fs::write(&out_path, tmd_data)?;
|
||||||
|
println!("Successfully downloaded TMD to \"{}\"!", out_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -138,7 +138,7 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
|
|||||||
}
|
}
|
||||||
title.ticket.title_key = title_key_new;
|
title.ticket.title_key = title_key_new;
|
||||||
title.fakesign()?;
|
title.fakesign()?;
|
||||||
fs::write(out_path.clone(), title.to_wad()?.to_bytes()?)?;
|
fs::write(&out_path, title.to_wad()?.to_bytes()?)?;
|
||||||
println!("Successfully converted {} WAD to {} WAD \"{}\"!", source, target, out_path.file_name().unwrap().to_str().unwrap());
|
println!("Successfully converted {} WAD to {} WAD \"{}\"!", source, target, out_path.file_name().unwrap().to_str().unwrap());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -205,7 +205,7 @@ pub fn pack_wad(input: &str, output: &str) -> Result<()> {
|
|||||||
out_path.set_extension("wad");
|
out_path.set_extension("wad");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs::write(out_path.clone(), wad.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
|
fs::write(&out_path, wad.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
|
||||||
println!("WAD file packed!");
|
println!("WAD file packed!");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||||
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;
|
use crate::title::tmd::ContentRecord;
|
||||||
use crate::title::crypto;
|
use crate::title::crypto;
|
||||||
|
|
||||||
@ -13,6 +14,8 @@ use crate::title::crypto;
|
|||||||
pub enum ContentError {
|
pub enum ContentError {
|
||||||
#[error("requested index {index} is out of range (must not exceed {max})")]
|
#[error("requested index {index} is out of range (must not exceed {max})")]
|
||||||
IndexOutOfRange { index: usize, max: usize },
|
IndexOutOfRange { index: usize, max: usize },
|
||||||
|
#[error("expected {required} contents based on content records but found {found}")]
|
||||||
|
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("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})")]
|
||||||
@ -71,15 +74,27 @@ impl ContentRegion {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a ContentRegion instance that can be used to parse and edit content stored in a
|
||||||
|
/// digital Wii title from a vector of contents and the ContentRecords from a TMD.
|
||||||
|
pub fn from_contents(contents: Vec<Vec<u8>>, content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
|
||||||
|
if contents.len() != content_records.len() {
|
||||||
|
return Err(MissingContents { required: content_records.len(), found: contents.len()});
|
||||||
|
}
|
||||||
|
let mut content_region = Self::new(content_records)?;
|
||||||
|
for i in 0..contents.len() {
|
||||||
|
content_region.load_enc_content(&contents[i], content_region.content_records[i].index as usize)?;
|
||||||
|
}
|
||||||
|
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: 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.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.len() as u16;
|
||||||
let content_start_offsets: Vec<u64> = Vec::new();
|
let content_start_offsets: Vec<u64> = vec![0; num_contents as usize];
|
||||||
let mut contents: Vec<Vec<u8>> = Vec::new();
|
let contents: Vec<Vec<u8>> = vec![Vec::new(); num_contents as usize];
|
||||||
contents.resize(num_contents as usize, Vec::new());
|
|
||||||
Ok(ContentRegion {
|
Ok(ContentRegion {
|
||||||
content_records,
|
content_records,
|
||||||
content_region_size,
|
content_region_size,
|
||||||
@ -144,6 +159,16 @@ impl ContentRegion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads existing content into the specified index of a ContentRegion instance. This content
|
||||||
|
/// must be encrypted.
|
||||||
|
pub fn load_enc_content(&mut self, content: &[u8], index: usize) -> Result<(), ContentError> {
|
||||||
|
if index >= self.content_records.len() {
|
||||||
|
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
||||||
|
}
|
||||||
|
self.contents[index] = Vec::from(content);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 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.
|
||||||
|
@ -7,6 +7,7 @@ pub mod cert;
|
|||||||
pub mod commonkeys;
|
pub mod commonkeys;
|
||||||
pub mod content;
|
pub mod content;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
|
pub mod nus;
|
||||||
pub mod ticket;
|
pub mod ticket;
|
||||||
pub mod tmd;
|
pub mod tmd;
|
||||||
pub mod versions;
|
pub mod versions;
|
||||||
@ -52,15 +53,37 @@ impl Title {
|
|||||||
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(), tmd.content_records.clone()).map_err(TitleError::Content)?;
|
||||||
let title = Title {
|
Ok(Title {
|
||||||
cert_chain,
|
cert_chain,
|
||||||
crl: wad.crl(),
|
crl: wad.crl(),
|
||||||
ticket,
|
ticket,
|
||||||
tmd,
|
tmd,
|
||||||
content,
|
content,
|
||||||
meta: wad.meta(),
|
meta: wad.meta(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Title instance from all of its individual components.
|
||||||
|
pub fn from_parts(cert_chain: cert::CertificateChain, crl: Option<&[u8]>, ticket: ticket::Ticket, tmd: tmd::TMD,
|
||||||
|
content: content::ContentRegion, meta: Option<&[u8]>) -> Result<Title, TitleError> {
|
||||||
|
// Create empty vecs for the CRL and meta areas if we weren't supplied with any, as they're
|
||||||
|
// optional components.
|
||||||
|
let crl = match crl {
|
||||||
|
Some(crl) => crl.to_vec(),
|
||||||
|
None => Vec::new()
|
||||||
};
|
};
|
||||||
Ok(title)
|
let meta = match meta {
|
||||||
|
Some(meta) => meta.to_vec(),
|
||||||
|
None => Vec::new()
|
||||||
|
};
|
||||||
|
Ok(Title {
|
||||||
|
cert_chain,
|
||||||
|
crl,
|
||||||
|
ticket,
|
||||||
|
tmd,
|
||||||
|
content,
|
||||||
|
meta
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a Title instance into a WAD, which can be used to export the Title back to a file.
|
/// Converts a Title instance into a WAD, which can be used to export the Title back to a file.
|
||||||
|
143
src/title/nus.rs
Normal file
143
src/title/nus.rs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// title/nus.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii
|
||||||
|
//
|
||||||
|
// Implements the functions required for downloading data from the NUS.
|
||||||
|
|
||||||
|
use std::str;
|
||||||
|
use std::io::Write;
|
||||||
|
use reqwest;
|
||||||
|
use thiserror::Error;
|
||||||
|
use crate::title::{cert, tmd, ticket, content};
|
||||||
|
use crate::title;
|
||||||
|
|
||||||
|
use sha1::{Sha1, Digest};
|
||||||
|
|
||||||
|
const WII_NUS_ENDPOINT: &str = "http://nus.cdn.shop.wii.com/ccs/download/";
|
||||||
|
const WII_U_NUS_ENDPOINT: &str = "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/";
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum NUSError {
|
||||||
|
#[error("the data returned by the NUS is not valid")]
|
||||||
|
InvalidData,
|
||||||
|
#[error("the requested Title ID or version could not be found on the NUS")]
|
||||||
|
NotFound,
|
||||||
|
#[error("Certificate processing error")]
|
||||||
|
Certificate(#[from] cert::CertificateError),
|
||||||
|
#[error("TMD processing error")]
|
||||||
|
TMD(#[from] tmd::TMDError),
|
||||||
|
#[error("Ticket processing error")]
|
||||||
|
Ticket(#[from] ticket::TicketError),
|
||||||
|
#[error("Content processing error")]
|
||||||
|
Content(#[from] content::ContentError),
|
||||||
|
#[error("an error occurred while assembling a Title from the downloaded data")]
|
||||||
|
Title(#[from] title::TitleError),
|
||||||
|
#[error("data could not be downloaded from the NUS")]
|
||||||
|
Request(#[from] reqwest::Error),
|
||||||
|
#[error("an error occurred writing NUS data")]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads the retail certificate chain from the NUS.
|
||||||
|
pub fn download_cert_chain(wiiu_endpoint: bool) -> Result<Vec<u8>, NUSError> {
|
||||||
|
// To build the certificate chain, we need to download both the TMD and Ticket of a title. For
|
||||||
|
// the sake of simplicity, we'll use the Wii Menu 4.3U because I already found the required TMD
|
||||||
|
// and Ticket offsets for it.
|
||||||
|
let endpoint_url = if wiiu_endpoint {
|
||||||
|
WII_U_NUS_ENDPOINT.to_owned()
|
||||||
|
} else {
|
||||||
|
WII_NUS_ENDPOINT.to_owned()
|
||||||
|
};
|
||||||
|
let tmd_url = format!("{}0000000100000002/tmd.513", endpoint_url);
|
||||||
|
let tik_url = format!("{}0000000100000002/cetk", endpoint_url);
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let tmd = client.get(tmd_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?.bytes()?;
|
||||||
|
let tik = client.get(tik_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?.bytes()?;
|
||||||
|
// Assemble the certificate chain.
|
||||||
|
let mut cert_chain: Vec<u8> = Vec::new();
|
||||||
|
// Certificate Authority data.
|
||||||
|
cert_chain.write_all(&tik[0x2A4 + 768..])?;
|
||||||
|
// Certificate Policy (TMD certificate) data.
|
||||||
|
cert_chain.write_all(&tmd[0x328..0x328 + 768])?;
|
||||||
|
// XS (Ticket certificate) data.
|
||||||
|
cert_chain.write_all(&tik[0x2A4..0x2A4 + 768])?;
|
||||||
|
Ok(cert_chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads a specified content file from the specified title from the NUS.
|
||||||
|
pub fn download_content(title_id: [u8; 8], content_id: u32, wiiu_endpoint: bool) -> Result<Vec<u8>, NUSError> {
|
||||||
|
// Build the download URL. The structure is download/<TID>/<CID>
|
||||||
|
let endpoint_url = if wiiu_endpoint {
|
||||||
|
WII_U_NUS_ENDPOINT.to_owned()
|
||||||
|
} else {
|
||||||
|
WII_NUS_ENDPOINT.to_owned()
|
||||||
|
};
|
||||||
|
let content_url = format!("{}{}/{:08X}", endpoint_url, &hex::encode(title_id), content_id);
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let response = client.get(content_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(NUSError::NotFound);
|
||||||
|
}
|
||||||
|
Ok(response.bytes()?.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads all contents from the specified title from the NUS.
|
||||||
|
pub fn download_contents(tmd: &tmd::TMD, wiiu_endpoint: bool) -> Result<Vec<Vec<u8>>, NUSError> {
|
||||||
|
let content_ids: Vec<u32> = tmd.content_records.iter().map(|record| { record.content_id }).collect();
|
||||||
|
let mut contents: Vec<Vec<u8>> = Vec::new();
|
||||||
|
for id in content_ids {
|
||||||
|
contents.push(download_content(tmd.title_id, id, wiiu_endpoint)?);
|
||||||
|
}
|
||||||
|
Ok(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads the Ticket for a specified Title ID from the NUS, if it's available.
|
||||||
|
pub fn download_ticket(title_id: [u8; 8], wiiu_endpoint: bool) -> Result<Vec<u8>, NUSError> {
|
||||||
|
// Build the download URL. The structure is download/<TID>/cetk.
|
||||||
|
let endpoint_url = if wiiu_endpoint {
|
||||||
|
WII_U_NUS_ENDPOINT.to_owned()
|
||||||
|
} else {
|
||||||
|
WII_NUS_ENDPOINT.to_owned()
|
||||||
|
};
|
||||||
|
let tik_url = format!("{}{}/cetk", endpoint_url, &hex::encode(title_id));
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let response = client.get(tik_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(NUSError::NotFound);
|
||||||
|
}
|
||||||
|
let tik = ticket::Ticket::from_bytes(&response.bytes()?).map_err(|_| NUSError::InvalidData)?;
|
||||||
|
tik.to_bytes().map_err(|_| NUSError::InvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads an entire title with all of its content from the NUS and returns a Title instance.
|
||||||
|
pub fn download_title(title_id: [u8; 8], title_version: Option<u16>, wiiu_endpoint: bool) -> Result<title::Title, NUSError> {
|
||||||
|
// Download the individual components of a title and then build a title from them.
|
||||||
|
let cert_chain = cert::CertificateChain::from_bytes(&download_cert_chain(wiiu_endpoint)?)?;
|
||||||
|
let tmd = tmd::TMD::from_bytes(&download_tmd(title_id, title_version, wiiu_endpoint)?)?;
|
||||||
|
let tik = ticket::Ticket::from_bytes(&download_ticket(title_id, wiiu_endpoint)?)?;
|
||||||
|
let content_region = content::ContentRegion::from_contents(download_contents(&tmd, wiiu_endpoint)?, tmd.content_records.clone())?;
|
||||||
|
let title = title::Title::from_parts(cert_chain, None, tik, tmd, content_region, None)?;
|
||||||
|
Ok(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads the TMD for a specified Title ID from the NUS.
|
||||||
|
pub fn download_tmd(title_id: [u8; 8], title_version: Option<u16>, wiiu_endpoint: bool) -> Result<Vec<u8>, NUSError> {
|
||||||
|
// Build the download URL. The structure is download/<TID>/tmd for latest and
|
||||||
|
// download/<TID>/tmd.<version> for when a specific version is requested.
|
||||||
|
let endpoint_url = if wiiu_endpoint {
|
||||||
|
WII_U_NUS_ENDPOINT.to_owned()
|
||||||
|
} else {
|
||||||
|
WII_NUS_ENDPOINT.to_owned()
|
||||||
|
};
|
||||||
|
let tmd_url = if title_version.is_some() {
|
||||||
|
format!("{}{}/tmd.{}", endpoint_url, &hex::encode(title_id), title_version.unwrap())
|
||||||
|
} else {
|
||||||
|
format!("{}{}/tmd", endpoint_url, &hex::encode(title_id))
|
||||||
|
};
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let response = client.get(tmd_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(NUSError::NotFound);
|
||||||
|
}
|
||||||
|
let tmd = tmd::TMD::from_bytes(&response.bytes()?).map_err(|_| NUSError::InvalidData)?;
|
||||||
|
tmd.to_bytes().map_err(|_| NUSError::InvalidData)
|
||||||
|
}
|
@ -50,11 +50,11 @@ impl fmt::Display for TitleType {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ContentType {
|
pub enum ContentType {
|
||||||
Normal,
|
Normal = 1,
|
||||||
Development,
|
Development = 2,
|
||||||
HashTree,
|
HashTree = 3,
|
||||||
DLC,
|
DLC = 16385,
|
||||||
Shared,
|
Shared = 32769,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ContentType {
|
impl fmt::Display for ContentType {
|
||||||
@ -70,8 +70,8 @@ impl fmt::Display for ContentType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum AccessRight {
|
pub enum AccessRight {
|
||||||
AHB,
|
AHB = 0,
|
||||||
DVDVideo,
|
DVDVideo = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -332,10 +332,7 @@ impl TMD {
|
|||||||
|
|
||||||
/// Gets whether a specified access right is enabled in a TMD.
|
/// Gets whether a specified access right is enabled in a TMD.
|
||||||
pub fn check_access_right(&self, right: AccessRight) -> bool {
|
pub fn check_access_right(&self, right: AccessRight) -> bool {
|
||||||
match right {
|
self.access_rights & (1 << right as u8) != 0
|
||||||
AccessRight::AHB => (self.access_rights & (1 << 0)) != 0,
|
|
||||||
AccessRight::DVDVideo => (self.access_rights & (1 << 1)) != 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the name of the certificate used to sign a TMD as a string.
|
/// Gets the name of the certificate used to sign a TMD as a string.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user