mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2026-03-03 11:25:29 -05:00
Added TMD command to CLI
This commit is contained in:
@@ -60,6 +60,11 @@ enum Commands {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: nand::setting::Commands
|
command: nand::setting::Commands
|
||||||
},
|
},
|
||||||
|
/// Edit a TMD file
|
||||||
|
Tmd {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: title::tmd::Commands
|
||||||
|
},
|
||||||
/// Pack/unpack a U8 archive
|
/// Pack/unpack a U8 archive
|
||||||
U8 {
|
U8 {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@@ -144,6 +149,16 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Some(Commands::Tmd { command}) => {
|
||||||
|
match command {
|
||||||
|
title::tmd::Commands::Edit { input, output, edits} => {
|
||||||
|
title::tmd::tmd_edit(input, output, edits)?
|
||||||
|
},
|
||||||
|
title::tmd::Commands::Remove { input, output, identifier } => {
|
||||||
|
title::tmd::tmd_remove(input, output, identifier)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
Some(Commands::U8 { command }) => {
|
Some(Commands::U8 { command }) => {
|
||||||
match command {
|
match command {
|
||||||
archive::u8::Commands::Pack { input, output } => {
|
archive::u8::Commands::Pack { input, output } => {
|
||||||
@@ -157,25 +172,25 @@ 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::Commands::Add { input, content, output, cid, r#type } => {
|
||||||
title::wad::add_wad(input, content, output, cid, r#type)?
|
title::wad::wad_add(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::wad_convert(input, target, output)?
|
||||||
},
|
},
|
||||||
title::wad::Commands::Edit { input, output, edits } => {
|
title::wad::Commands::Edit { input, output, edits } => {
|
||||||
title::wad::edit_wad(input, output, edits)?
|
title::wad::wad_edit(input, output, edits)?
|
||||||
},
|
},
|
||||||
title::wad::Commands::Pack { input, output} => {
|
title::wad::Commands::Pack { input, output} => {
|
||||||
title::wad::pack_wad(input, output)?
|
title::wad::wad_pack(input, output)?
|
||||||
},
|
},
|
||||||
title::wad::Commands::Remove { input, output, identifier } => {
|
title::wad::Commands::Remove { input, output, identifier } => {
|
||||||
title::wad::remove_wad(input, output, identifier)?
|
title::wad::wad_remove(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::wad_set(input, content, output, identifier, r#type)?
|
||||||
},
|
},
|
||||||
title::wad::Commands::Unpack { input, output } => {
|
title::wad::Commands::Unpack { input, output } => {
|
||||||
title::wad::unpack_wad(input, output)?
|
title::wad::wad_unpack(input, output)?
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,3 +4,5 @@
|
|||||||
pub mod fakesign;
|
pub mod fakesign;
|
||||||
pub mod nus;
|
pub mod nus;
|
||||||
pub mod wad;
|
pub mod wad;
|
||||||
|
pub mod tmd;
|
||||||
|
mod shared;
|
||||||
|
|||||||
73
src/bin/rustwii/title/shared.rs
Normal file
73
src/bin/rustwii/title/shared.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// title/shared.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustwii
|
||||||
|
//
|
||||||
|
// Code shared between title commands in the rustii CLI.
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use clap::Args;
|
||||||
|
use hex::FromHex;
|
||||||
|
use regex::RegexBuilder;
|
||||||
|
use rustwii::title::tmd;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[clap(next_help_heading = "Content Identifier")]
|
||||||
|
#[group(multiple = false, required = true)]
|
||||||
|
/// Method of identifying individual content in a title, shared between the WAD and TMD commands.
|
||||||
|
pub struct ContentIdentifier {
|
||||||
|
/// The index of the target content
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub index: Option<usize>,
|
||||||
|
/// The Content ID of the target content
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub cid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[clap(next_help_heading = "Possible Modifications")]
|
||||||
|
#[group(multiple = true, required = true)]
|
||||||
|
/// Modifications that can be made to a title, shared between the WAD and TMD commands.
|
||||||
|
pub struct TitleModifications {
|
||||||
|
/// A new IOS version for this title (formatted as the decimal IOS version, e.g. 58, with a
|
||||||
|
/// valid range of 3-255)
|
||||||
|
#[arg(long)]
|
||||||
|
pub ios: Option<u8>,
|
||||||
|
/// A new Title ID for this title (formatted as 4 ASCII characters, e.g. HADE)
|
||||||
|
#[arg(long)]
|
||||||
|
pub tid: Option<String>,
|
||||||
|
/// A new type for this title (valid options are "System", "Channel", "SystemChannel",
|
||||||
|
/// "GameChannel", "DLC", "HiddenChannel")
|
||||||
|
#[arg(long)]
|
||||||
|
pub r#type: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a target IOS number and returns its TID.
|
||||||
|
pub fn validate_target_ios(new_ios: u8) -> Result<[u8; 8], anyhow::Error> {
|
||||||
|
if new_ios < 3 {
|
||||||
|
bail!("The specified IOS version is not valid! The new IOS version must be between 3 and 255.")
|
||||||
|
}
|
||||||
|
let new_ios_tid = <[u8; 8]>::from_hex(format!("00000001{:08X}", new_ios))?;
|
||||||
|
Ok(new_ios_tid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a target Title ID and returns it as a vector.
|
||||||
|
pub fn validate_target_tid(new_tid_low: &str) -> Result<Vec<u8>, anyhow::Error> {
|
||||||
|
let re = RegexBuilder::new(r"^[a-z0-9!@#$%^&*]{4}$").case_insensitive(true).build()?;
|
||||||
|
if !re.is_match(new_tid_low) {
|
||||||
|
bail!("The specified Title ID is not valid! The new Title ID must be 4 characters and include only letters, numbers, and the special characters \"!@#$%&*\".");
|
||||||
|
}
|
||||||
|
Ok(Vec::from_hex(hex::encode(new_tid_low))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a target title type and returns it.
|
||||||
|
pub fn validate_target_type(new_type: &str) -> Result<tmd::TitleType, anyhow::Error> {
|
||||||
|
let new_type = match new_type {
|
||||||
|
"system" => tmd::TitleType::System,
|
||||||
|
"channel" => tmd::TitleType::Channel,
|
||||||
|
"systemchannel" => tmd::TitleType::SystemChannel,
|
||||||
|
"gamechannel" => tmd::TitleType::GameChannel,
|
||||||
|
"dlc" => tmd::TitleType::DLC,
|
||||||
|
"hiddenchannel" => tmd::TitleType::HiddenChannel,
|
||||||
|
_ => bail!("The specified title type \"{}\" is invalid! Try --help to see valid types.", new_type),
|
||||||
|
};
|
||||||
|
Ok(new_type)
|
||||||
|
}
|
||||||
130
src/bin/rustwii/title/tmd.rs
Normal file
130
src/bin/rustwii/title/tmd.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// title/tmd.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustwii
|
||||||
|
//
|
||||||
|
// Code for TMD-related commands in the rustii CLI.
|
||||||
|
|
||||||
|
use std::{str, fs};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use clap::Subcommand;
|
||||||
|
use hex::FromHex;
|
||||||
|
use rustwii::title::tmd;
|
||||||
|
use crate::title::shared::{validate_target_ios, validate_target_tid, validate_target_type, ContentIdentifier, TitleModifications};
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Edit the properties of a TMD file
|
||||||
|
Edit {
|
||||||
|
/// The path to the TMD to modify
|
||||||
|
input: String,
|
||||||
|
/// An optional output path; defaults to overwriting input TMD file
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
edits: TitleModifications
|
||||||
|
},
|
||||||
|
/// Remove content from a TMD file
|
||||||
|
Remove {
|
||||||
|
/// The path to the WAD file to modify
|
||||||
|
input: String,
|
||||||
|
/// An optional output path; defaults to overwriting input TMD file
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
identifier: ContentIdentifier,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tmd_edit(input: &str, output: &Option<String>, edits: &TitleModifications) -> Result<()> {
|
||||||
|
let in_path = Path::new(input);
|
||||||
|
if !in_path.exists() {
|
||||||
|
bail!("Source TMD \"{}\" does not exist.", in_path.display());
|
||||||
|
}
|
||||||
|
let out_path = if output.is_some() {
|
||||||
|
PathBuf::from(output.clone().unwrap())
|
||||||
|
} else {
|
||||||
|
in_path.to_path_buf()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tmd = tmd::TMD::from_bytes(&fs::read(in_path)?).with_context(|| "The provided TMD file could not be parsed, and is likely invalid.")?;
|
||||||
|
// Parse possible edits and perform each one provided.
|
||||||
|
let mut changes_summary: Vec<String> = Vec::new();
|
||||||
|
// These are joined, because that way if both are selected we only need to set the TID a
|
||||||
|
// single time.
|
||||||
|
if edits.tid.is_some() || edits.r#type.is_some() {
|
||||||
|
let tid_high = if let Some(new_type) = &edits.r#type {
|
||||||
|
let new_type = validate_target_type(&new_type.to_ascii_lowercase())?;
|
||||||
|
changes_summary.push(format!("Changed title type from \"{}\" to \"{}\"", tmd.title_type()?, new_type));
|
||||||
|
Vec::from_hex(format!("{:08X}", new_type as u32))?
|
||||||
|
} else {
|
||||||
|
tmd.title_id()[0..4].to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tid_low = if let Some(new_tid) = &edits.tid {
|
||||||
|
let new_tid = validate_target_tid(&new_tid.to_ascii_uppercase())?;
|
||||||
|
changes_summary.push(format!("Changed Title ID from \"{}\" to \"{}\"", hex::encode(&tmd.title_id()[4..8]).to_ascii_uppercase(), hex::encode(&new_tid).to_ascii_uppercase()));
|
||||||
|
new_tid
|
||||||
|
} else {
|
||||||
|
tmd.title_id()[4..8].to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_tid: Vec<u8> = tid_high.iter().chain(&tid_low).copied().collect();
|
||||||
|
tmd.set_title_id(new_tid.try_into().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply IOS edits.
|
||||||
|
if let Some(new_ios) = edits.ios {
|
||||||
|
let new_ios_tid = validate_target_ios(new_ios)?;
|
||||||
|
changes_summary.push(format!("Changed required IOS from IOS{} to IOS{}", tmd.ios_tid().last().unwrap(), new_ios));
|
||||||
|
tmd.set_ios_tid(new_ios_tid)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tmd.fakesign()?;
|
||||||
|
fs::write(&out_path, tmd.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
|
||||||
|
println!("Successfully edited TMD file \"{}\"!\nSummary of changes:", out_path.display());
|
||||||
|
for change in &changes_summary {
|
||||||
|
println!(" - {}", change);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tmd_remove(input: &str, output: &Option<String>, identifier: &ContentIdentifier) -> Result<()> {
|
||||||
|
let in_path = Path::new(input);
|
||||||
|
if !in_path.exists() {
|
||||||
|
bail!("Source TMD \"{}\" could not be found.", in_path.display());
|
||||||
|
}
|
||||||
|
let out_path = if output.is_some() {
|
||||||
|
PathBuf::from(output.clone().unwrap())
|
||||||
|
} else {
|
||||||
|
in_path.to_path_buf()
|
||||||
|
};
|
||||||
|
let mut tmd = tmd::TMD::from_bytes(&fs::read(in_path)?).with_context(|| "The provided TMD file could not be parsed, and is likely invalid.")?;
|
||||||
|
// Parse the identifier passed to choose how to find and remove the target.
|
||||||
|
// ...maybe don't take the above comment out of context
|
||||||
|
if let Some(index) = identifier.index {
|
||||||
|
let mut content_records = tmd.content_records().clone();
|
||||||
|
content_records.remove(index);
|
||||||
|
tmd.set_content_records(&content_records);
|
||||||
|
tmd.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified TMD.")?;
|
||||||
|
fs::write(&out_path, tmd.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
|
||||||
|
println!("Successfully removed content at index {} in TMD file \"{}\".", index, 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 tmd.content_records().iter()
|
||||||
|
.find(|record| record.content_id == cid)
|
||||||
|
.map(|record| record.index)
|
||||||
|
{
|
||||||
|
Some(index) => index,
|
||||||
|
None => bail!("The specified Content ID \"{}\" ({}) does not exist in this WAD!", identifier.cid.clone().unwrap(), cid),
|
||||||
|
};
|
||||||
|
let mut content_records = tmd.content_records().clone();
|
||||||
|
content_records.remove(index as usize);
|
||||||
|
tmd.set_content_records(&content_records);
|
||||||
|
tmd.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified TMD.")?;
|
||||||
|
fs::write(&out_path, tmd.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());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@ use clap::{Subcommand, Args};
|
|||||||
use glob::glob;
|
use glob::glob;
|
||||||
use hex::FromHex;
|
use hex::FromHex;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use regex::RegexBuilder;
|
|
||||||
use rustwii::title::{cert, crypto, tmd, ticket, content, wad};
|
use rustwii::title::{cert, crypto, tmd, ticket, content, wad};
|
||||||
use rustwii::title;
|
use rustwii::title;
|
||||||
|
use crate::title::shared::{validate_target_ios, validate_target_tid, validate_target_type, ContentIdentifier, TitleModifications};
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
#[command(arg_required_else_help = true)]
|
#[command(arg_required_else_help = true)]
|
||||||
@@ -52,7 +52,7 @@ pub enum Commands {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
output: Option<String>,
|
output: Option<String>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
edits: WadModifications
|
edits: TitleModifications
|
||||||
},
|
},
|
||||||
/// Pack a directory into a WAD file
|
/// Pack a directory into a WAD file
|
||||||
Pack {
|
Pack {
|
||||||
@@ -110,35 +110,6 @@ pub struct ConvertTargets {
|
|||||||
vwii: bool,
|
vwii: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
#[clap(next_help_heading = "Content Identifier")]
|
|
||||||
#[group(multiple = false, required = true)]
|
|
||||||
pub struct ContentIdentifier {
|
|
||||||
/// The index of the target content
|
|
||||||
#[arg(short, long)]
|
|
||||||
index: Option<usize>,
|
|
||||||
/// The Content ID of the target content
|
|
||||||
#[arg(short, long)]
|
|
||||||
cid: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
#[clap(next_help_heading = "Possible Modifications")]
|
|
||||||
#[group(multiple = true, required = true)]
|
|
||||||
pub struct WadModifications {
|
|
||||||
/// A new IOS version for this WAD (formatted as the decimal IOS version, e.g. 58, with a valid
|
|
||||||
/// range of 3-255)
|
|
||||||
#[arg(long)]
|
|
||||||
ios: Option<u8>,
|
|
||||||
/// A new Title ID for this WAD (formatted as 4 ASCII characters, e.g. HADE)
|
|
||||||
#[arg(long)]
|
|
||||||
tid: Option<String>,
|
|
||||||
/// A new type for this WAD (valid options are "System", "Channel", "SystemChannel",
|
|
||||||
/// "GameChannel", "DLC", "HiddenChannel")
|
|
||||||
#[arg(long)]
|
|
||||||
r#type: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Target {
|
enum Target {
|
||||||
Retail,
|
Retail,
|
||||||
Dev,
|
Dev,
|
||||||
@@ -155,7 +126,7 @@ impl fmt::Display for Target {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_wad(input: &str, content: &str, output: &Option<String>, cid: &Option<String>, ctype: &Option<String>) -> Result<()> {
|
pub fn wad_add(input: &str, content: &str, output: &Option<String>, cid: &Option<String>, ctype: &Option<String>) -> 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.", in_path.display());
|
bail!("Source WAD \"{}\" could not be found.", in_path.display());
|
||||||
@@ -209,7 +180,7 @@ pub fn add_wad(input: &str, content: &str, output: &Option<String>, cid: &Option
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>) -> Result<()> {
|
pub fn wad_convert(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() {
|
||||||
bail!("Source WAD \"{}\" could not be found.", in_path.display());
|
bail!("Source WAD \"{}\" could not be found.", in_path.display());
|
||||||
@@ -281,10 +252,10 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edit_wad(input: &str, output: &Option<String>, edits: &WadModifications) -> Result<()> {
|
pub fn wad_edit(input: &str, output: &Option<String>, edits: &TitleModifications) -> Result<()> {
|
||||||
let in_path = Path::new(input);
|
let in_path = Path::new(input);
|
||||||
if !in_path.exists() {
|
if !in_path.exists() {
|
||||||
bail!("Source directory \"{}\" does not exist.", in_path.display());
|
bail!("Source WAD \"{}\" does not exist.", in_path.display());
|
||||||
}
|
}
|
||||||
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")
|
||||||
@@ -298,44 +269,32 @@ pub fn edit_wad(input: &str, output: &Option<String>, edits: &WadModifications)
|
|||||||
// These are joined, because that way if both are selected we only need to set the TID (and by
|
// These are joined, because that way if both are selected we only need to set the TID (and by
|
||||||
// extension, re-encrypt the Title Key) a single time.
|
// extension, re-encrypt the Title Key) a single time.
|
||||||
if edits.tid.is_some() || edits.r#type.is_some() {
|
if edits.tid.is_some() || edits.r#type.is_some() {
|
||||||
let tid_high = if edits.r#type.is_some() {
|
let tid_high = if let Some(new_type) = &edits.r#type {
|
||||||
let new_type = match edits.r#type.clone().unwrap().to_ascii_lowercase().as_str() {
|
let new_type = validate_target_type(&new_type.to_ascii_lowercase())?;
|
||||||
"system" => tmd::TitleType::System,
|
|
||||||
"channel" => tmd::TitleType::Channel,
|
|
||||||
"systemchannel" => tmd::TitleType::SystemChannel,
|
|
||||||
"gamechannel" => tmd::TitleType::GameChannel,
|
|
||||||
"dlc" => tmd::TitleType::DLC,
|
|
||||||
"hiddenchannel" => tmd::TitleType::HiddenChannel,
|
|
||||||
_ => bail!("The specified title type \"{}\" is invalid! Try --help to see valid types.", edits.r#type.clone().unwrap()),
|
|
||||||
};
|
|
||||||
changes_summary.push(format!("Changed title type from \"{}\" to \"{}\"", title.tmd.title_type()?, new_type));
|
changes_summary.push(format!("Changed title type from \"{}\" to \"{}\"", title.tmd.title_type()?, new_type));
|
||||||
Vec::from_hex(format!("{:08X}", new_type as u32))?
|
Vec::from_hex(format!("{:08X}", new_type as u32))?
|
||||||
} else {
|
} else {
|
||||||
title.tmd.title_id()[0..4].to_vec()
|
title.tmd.title_id()[0..4].to_vec()
|
||||||
};
|
};
|
||||||
let tid_low = if edits.tid.is_some() {
|
|
||||||
let re = RegexBuilder::new(r"^[a-z0-9!@#$%^&*]{4}$").case_insensitive(true).build()?;
|
let tid_low = if let Some(new_tid) = &edits.tid {
|
||||||
let new_tid_low = edits.tid.clone().unwrap().to_ascii_uppercase();
|
let new_tid = validate_target_tid(&new_tid.to_ascii_uppercase())?;
|
||||||
if !re.is_match(&new_tid_low) {
|
changes_summary.push(format!("Changed Title ID from \"{}\" to \"{}\"", hex::encode(&title.tmd.title_id()[4..8]).to_ascii_uppercase(), hex::encode(&new_tid).to_ascii_uppercase()));
|
||||||
bail!("The specified Title ID is not valid! The new Title ID must be 4 characters and include only letters, numbers, and the special characters \"!@#$%&*\".");
|
new_tid
|
||||||
}
|
|
||||||
changes_summary.push(format!("Changed Title ID from \"{}\" to \"{}\"", hex::encode(&title.tmd.title_id()[4..8]).to_ascii_uppercase(), hex::encode(&new_tid_low).to_ascii_uppercase()));
|
|
||||||
Vec::from_hex(hex::encode(new_tid_low))?
|
|
||||||
} else {
|
} else {
|
||||||
title.tmd.title_id()[4..8].to_vec()
|
title.tmd.title_id()[4..8].to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_tid: Vec<u8> = tid_high.iter().chain(&tid_low).copied().collect();
|
let new_tid: Vec<u8> = tid_high.iter().chain(&tid_low).copied().collect();
|
||||||
title.set_title_id(new_tid.try_into().unwrap())?;
|
title.set_title_id(new_tid.try_into().unwrap())?;
|
||||||
}
|
}
|
||||||
if let Some(ios) = edits.ios {
|
|
||||||
let new_ios = ios;
|
if let Some(new_ios) = edits.ios {
|
||||||
if new_ios < 3 {
|
let new_ios_tid = validate_target_ios(new_ios)?;
|
||||||
bail!("The specified IOS version is not valid! The new IOS version must be between 3 and 255.")
|
|
||||||
}
|
|
||||||
let new_ios_tid = <[u8; 8]>::from_hex(format!("00000001{:08X}", new_ios))?;
|
|
||||||
changes_summary.push(format!("Changed required IOS from IOS{} to IOS{}", title.tmd.ios_tid().last().unwrap(), new_ios));
|
|
||||||
title.tmd.set_ios_tid(new_ios_tid)?;
|
title.tmd.set_ios_tid(new_ios_tid)?;
|
||||||
|
changes_summary.push(format!("Changed required IOS from IOS{} to IOS{}", title.tmd.ios_tid().last().unwrap(), new_ios));
|
||||||
}
|
}
|
||||||
|
|
||||||
title.fakesign()?;
|
title.fakesign()?;
|
||||||
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
|
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
|
||||||
println!("Successfully edited WAD file \"{}\"!\nSummary of changes:", out_path.display());
|
println!("Successfully edited WAD file \"{}\"!\nSummary of changes:", out_path.display());
|
||||||
@@ -345,7 +304,7 @@ pub fn edit_wad(input: &str, output: &Option<String>, edits: &WadModifications)
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pack_wad(input: &str, output: &str) -> Result<()> {
|
pub fn wad_pack(input: &str, output: &str) -> Result<()> {
|
||||||
let in_path = Path::new(input);
|
let in_path = Path::new(input);
|
||||||
if !in_path.exists() {
|
if !in_path.exists() {
|
||||||
bail!("Source directory \"{}\" does not exist.", in_path.display());
|
bail!("Source directory \"{}\" does not exist.", in_path.display());
|
||||||
@@ -415,7 +374,7 @@ pub fn pack_wad(input: &str, output: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_wad(input: &str, output: &Option<String>, identifier: &ContentIdentifier) -> Result<()> {
|
pub fn wad_remove(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.", in_path.display());
|
bail!("Source WAD \"{}\" could not be found.", in_path.display());
|
||||||
@@ -449,7 +408,7 @@ pub fn remove_wad(input: &str, output: &Option<String>, identifier: &ContentIden
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_wad(input: &str, content: &str, output: &Option<String>, identifier: &ContentIdentifier, ctype: &Option<String>) -> Result<()> {
|
pub fn wad_set(input: &str, content: &str, output: &Option<String>, identifier: &ContentIdentifier, ctype: &Option<String>) -> 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.", in_path.display());
|
bail!("Source WAD \"{}\" could not be found.", in_path.display());
|
||||||
@@ -501,7 +460,7 @@ pub fn set_wad(input: &str, content: &str, output: &Option<String>, identifier:
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unpack_wad(input: &str, output: &str) -> Result<()> {
|
pub fn wad_unpack(input: &str, output: &str) -> 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.", input);
|
||||||
|
|||||||
Reference in New Issue
Block a user