From 94e0be0eeffa7e1d4e996c5015040f15b5570645 Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Sat, 3 May 2025 23:43:02 -0400 Subject: [PATCH] Added emunand info and install-missing commands to rustii CLI These were grueling to port. There's just so much printing and format converting to deal with. Ugh. At least it's done now. --- Cargo.lock | 29 ++++ Cargo.toml | 1 + src/bin/rustii/main.rs | 18 +- src/bin/rustii/nand/emunand.rs | 309 +++++++++++++++++++++++++++++++-- src/nand/emunand.rs | 126 +++++++++++++- src/nand/setting.rs | 2 - src/title/versions.rs | 4 +- 7 files changed, 459 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 058c2bf..674ee7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1292,6 +1292,7 @@ dependencies = [ "rsa", "sha1", "thiserror", + "walkdir", ] [[package]] @@ -1358,6 +1359,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.27" @@ -1785,6 +1795,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -1890,6 +1910,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "windows-link" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index aea40e7..34f2a90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,4 @@ anyhow = "1" thiserror = "2" reqwest = { version = "0", features = ["blocking"] } rand = "0" +walkdir = "2" diff --git a/src/bin/rustii/main.rs b/src/bin/rustii/main.rs index 725edd5..be5d291 100644 --- a/src/bin/rustii/main.rs +++ b/src/bin/rustii/main.rs @@ -28,7 +28,7 @@ enum Commands { command: archive::ash::Commands, }, /// Manage Wii EmuNANDs - EmuNAND { + Emunand { #[command(subcommand)] command: nand::emunand::Commands, }, @@ -86,13 +86,19 @@ fn main() -> Result<()> { } } }, - Some(Commands::EmuNAND { command }) => { + Some(Commands::Emunand { command }) => { match command { - nand::emunand::Commands::Debug { input } => { - nand::emunand::debug(input)? + nand::emunand::Commands::Info { emunand } => { + nand::emunand::info(emunand)? }, - nand::emunand::Commands::InstallTitle { wad, emunand } => { - nand::emunand::install_title(wad, emunand)? + nand::emunand::Commands::InstallMissing { emunand, vwii } => { + nand::emunand::install_missing(emunand, vwii)? + }, + nand::emunand::Commands::InstallTitle { wad, emunand, override_meta} => { + nand::emunand::install_title(wad, emunand, override_meta)? + }, + nand::emunand::Commands::UninstallTitle { tid, emunand, remove_ticket } => { + nand::emunand::uninstall_title(tid, emunand, remove_ticket)? } } } diff --git a/src/bin/rustii/nand/emunand.rs b/src/bin/rustii/nand/emunand.rs index 2852c68..26b2166 100644 --- a/src/bin/rustii/nand/emunand.rs +++ b/src/bin/rustii/nand/emunand.rs @@ -4,36 +4,302 @@ // Code for EmuNAND-related commands in the rustii CLI. use std::{str, fs}; -use std::path::{Path, PathBuf}; +use std::path::{absolute, Path}; use anyhow::{bail, Context, Result}; use clap::Subcommand; -use rustii::nand::emunand; +use walkdir::WalkDir; +use rustii::nand::{emunand, setting}; +use rustii::title::{nus, tmd}; use rustii::title; #[derive(Subcommand)] #[command(arg_required_else_help = true)] pub enum Commands { - /// Placeholder command for debugging EmuNAND module - Debug { - /// The path to the test EmuNAND - input: String, + /// Display information about an EmuNAND + Info { + emunand: String, }, + /// Automatically install missing IOSes to an EmuNAND + InstallMissing { + /// The path to the target EmuNAND + emunand: String, + /// Explicitly install vWii IOSes instead of detecting the EmuNAND type automatically + #[clap(long)] + vwii: bool + }, + /// Install a WAD file to an EmuNAND InstallTitle { /// The path to the WAD file to install wad: String, /// The path to the target EmuNAND emunand: String, + /// Install the content at index 0 as title.met; this will override any meta/footer data + /// included in the WAD + #[clap(long)] + override_meta: bool, + }, + /// Uninstall a title from an EmuNAND + UninstallTitle { + /// The Title ID of the title to uninstall, or the path to a WAD file to read the Title ID + /// from + tid: String, + /// The path to the target EmuNAND + emunand: String, + /// Remove the Ticket file; default behavior is to leave it intact + #[clap(long)] + remove_ticket: bool, } } -pub fn debug(input: &str) -> Result<()> { - let emunand_root = Path::new(input); - let emunand = emunand::EmuNAND::open(emunand_root.to_path_buf())?; - emunand.install_title(title::Title::from_bytes(&fs::read("channel_retail.wad")?)?)?; +pub fn info(emunand: &str) -> Result<()> { + let emunand_path = Path::new(emunand); + if !emunand_path.exists() { + bail!("Target EmuNAND directory \"{}\" could not be found.", emunand_path.display()); + } + let emunand = emunand::EmuNAND::open(emunand_path.to_path_buf())?; + // Summarize all the details of an EmuNAND. + println!("EmuNAND Info"); + println!(" Path: {}", absolute(emunand_path)?.display()); + let mut is_vwii = false; + match emunand.get_title_tmd([0, 0, 0, 1, 0, 0, 0, 2]) { + Some(tmd) => { + is_vwii = tmd.is_vwii(); + println!(" System Menu Version: {}", title::versions::dec_to_standard(tmd.title_version, "0000000100000002", Some(is_vwii)).unwrap()); + }, + None => { + println!(" System Menu Version: None"); + } + } + let setting_path = emunand.get_emunand_dir("title").unwrap() + .join("00000001") + .join("00000002") + .join("data") + .join("setting.txt"); + if setting_path.exists() { + let setting_txt = setting::SettingTxt::from_bytes(&fs::read(setting_path)?)?; + println!(" System Region: {}", setting_txt.area); + } else { + println!(" System Region: N/A"); + } + if is_vwii { + println!(" Type: vWii"); + } else { + println!(" Type: Wii"); + } + let categories = emunand.get_installed_titles(); + let mut installed_count = 0; + for category in &categories { + if category.title_type != "00010000" { + for _ in &category.titles { + installed_count += 1; + } + } + } + println!(" Installed Titles: {}", installed_count); + let total_size: u64 = WalkDir::new(emunand.get_emunand_dir("root").unwrap()) + .into_iter() + .filter_map(Result::ok) + .filter(|entry| entry.file_type().is_file()) + .map(|entry| fs::metadata(entry.path()).map(|m| m.len()).unwrap_or(0)) + .sum(); + println!(" Space Used: {} blocks ({:.2} MB)", title::bytes_to_blocks(total_size as usize), total_size as f64 / 1048576.0); + println!(); + // Build a catalog of all installed titles so that we can display them. + let mut installed_ioses: Vec = Vec::new(); + let mut installed_titles: Vec = Vec::new(); + let mut disc_titles: Vec = Vec::new(); + for category in categories { + if category.title_type == "00000001" { + let mut ioses: Vec = Vec::new(); + for title in category.titles { + if title != "00000002" { + ioses.push(u32::from_str_radix(&title, 16)?); + } + } + ioses.sort(); + ioses.iter().for_each(|x| installed_ioses.push(format!("00000001{:08X}", x))); + } else if category.title_type != "00010000" { + category.titles.iter().for_each(|x| installed_titles.push(format!("{}{}", category.title_type, x).to_ascii_uppercase())); + } else if category.title_type == "00000000" { + category.titles.iter().filter(|x| x.as_str() != "48415A41") + .for_each(|x| disc_titles.push(format!("{}{}", category.title_type, x).to_ascii_uppercase())); + } + } + // Print the titles that are installed to the EmuNAND. + if !installed_ioses.is_empty() { + println!("System Titles:"); + for ios in &installed_ioses { + if ["00000001", "00000100", "00000101", "00000200", "00000201"].contains(&&ios[8..16]) { + if ios[8..16].eq("00000001") { + println!(" boot2 ({})", ios.to_ascii_uppercase()); + } else if ios[8..16].eq("00000100") { + println!(" BC ({})", ios.to_ascii_uppercase()); + } else if ios[8..16].eq("00000101") { + println!(" MIOS ({})", ios.to_ascii_uppercase()); + } else if ios[8..16].eq("00000200") { + println!(" BC-NAND ({})", ios.to_ascii_uppercase()); + } else if ios[8..16].eq("00000201") { + println!(" BC-WFS ({})", ios.to_ascii_uppercase()); + } + let tmd = emunand.get_title_tmd(hex::decode(ios)?.try_into().unwrap()).unwrap(); + println!(" Version: {}", tmd.title_version); + } + else { + println!(" IOS{} ({})", u32::from_str_radix(&ios[8..16], 16)?, ios.to_ascii_uppercase()); + let tmd = emunand.get_title_tmd(hex::decode(ios)?.try_into().unwrap()).unwrap(); + println!(" Version: {} ({})", tmd.title_version, title::versions::dec_to_standard(tmd.title_version, ios, None).unwrap()); + } + } + println!(); + } + let mut missing_ioses: Vec = Vec::new(); + if !installed_titles.is_empty() { + println!("Installed Titles:"); + for title in installed_titles { + let ascii = String::from_utf8_lossy(&hex::decode(&title[8..16])?).to_string(); + let ascii_tid = if ascii.len() == 4 { + Some(ascii) + } else { + None + }; + if ascii_tid.is_some() { + println!(" {} ({})", title.to_uppercase(), ascii_tid.unwrap()); + } else { + println!(" {}", title.to_uppercase()); + } + let tmd = emunand.get_title_tmd(hex::decode(&title)?.try_into().unwrap()).unwrap(); + println!(" Version: {}", tmd.title_version); + let ios_tid = &hex::encode(tmd.ios_tid()).to_ascii_uppercase(); + print!(" Required IOS: IOS{} ({})", u32::from_str_radix(&hex::encode(&tmd.ios_tid()[4..8]), 16)?, ios_tid); + if !installed_ioses.contains(ios_tid) { + println!(" *"); + if !missing_ioses.contains(ios_tid) { + missing_ioses.push(String::from(ios_tid)); + } + } + else { + println!(); + } + } + println!(); + } + if !disc_titles.is_empty() { + println!("Save data was found for the following disc titles:"); + for title in disc_titles { + let ascii = String::from_utf8_lossy(&hex::decode(&title[8..16])?).to_string(); + let ascii_tid = if ascii.len() == 4 { + Some(ascii) + } else { + None + }; + if ascii_tid.is_some() { + println!(" {} ({})", title.to_uppercase(), ascii_tid.unwrap()); + } else { + println!(" {}", title.to_uppercase()); + } + } + println!(); + } + // Finally, list IOSes that are required by an installed title but are not currently installed. + // This message is sponsored by `rustii emunand install-missing`. + if !missing_ioses.is_empty() { + println!("Some titles installed are missing their required IOS. These missing IOSes are \ + marked with \"*\" in the title list above. If these IOSes are not installed, the titles \ + requiring them will not launch. The IOSes required but not installed are:"); + for missing in missing_ioses { + println!(" IOS{} ({})", u32::from_str_radix(&missing[8..16], 16)?, missing); + } + println!("Missing IOSes can be automatically installed using the install-missing command."); + } Ok(()) } -pub fn install_title(wad: &str, emunand: &str) -> Result<()> { +pub fn install_missing(emunand: &str, vwii: &bool) -> Result<()> { + let emunand_path = Path::new(emunand); + if !emunand_path.exists() { + bail!("Target EmuNAND directory \"{}\" could not be found.", emunand_path.display()); + } + let emunand = emunand::EmuNAND::open(emunand_path.to_path_buf())?; + // Determine Wii vs vWii EmuNAND. + let vwii = if *vwii { + true + } else { + match emunand.get_title_tmd([0, 0, 0, 1, 0, 0, 0, 2]) { + Some(tmd) => { + tmd.is_vwii() + }, + None => { + false + } + } + }; + // Build a list of IOSes that are required by at least one installed title but are not + // installed themselves. Then from there we can call the NUS download_title() function to + // download and trigger an EmuNAND install for each of them. + let categories = emunand.get_installed_titles(); + let mut installed_ioses: Vec = Vec::new(); + let mut installed_titles: Vec = Vec::new(); + for category in categories { + if category.title_type == "00000001" { + let mut ioses: Vec = Vec::new(); + for title in category.titles { + if title == "00000002" { + installed_titles.push(format!("{}{}", category.title_type, title)); + } else if title != "00000001" { + ioses.push(u32::from_str_radix(&title, 16)?); + } + } + ioses.sort(); + ioses.iter().for_each(|x| installed_ioses.push(format!("00000001{:08X}", x))); + } else if category.title_type != "00010000" { + category.titles.iter().for_each(|x| installed_titles.push(format!("{}{}", category.title_type, x))); + } + } + let title_tmds: Vec = installed_titles.iter().map(|x| emunand.get_title_tmd(hex::decode(x).unwrap().try_into().unwrap()).unwrap()).collect(); + let mut missing_ioses: Vec = title_tmds.iter() + .filter(|x| !installed_ioses.contains(&hex::encode(x.ios_tid()).to_ascii_uppercase())) + .map(|x| u32::from_str_radix(&hex::encode(&x.ios_tid()[4..8]), 16).unwrap()).collect(); + if missing_ioses.is_empty() { + bail!("All required IOSes are already installed!"); + } + missing_ioses.sort(); + // Because we don't need to install the same IOS for every single title that requires it. + missing_ioses.dedup(); + let missing_tids: Vec<[u8; 8]> = { + if vwii { + missing_ioses.iter().map(|x| { + let mut tid = [0u8; 8]; + tid[3] = 7; + tid[4..8].copy_from_slice(&x.to_be_bytes()); + tid + }).collect() + } else { + missing_ioses.iter().map(|x| { + let mut tid = [0u8; 8]; + tid[3] = 1; + tid[4..8].copy_from_slice(&x.to_be_bytes()); + tid + }).collect() + } + }; + println!("Missing IOSes:"); + for ios in &missing_tids { + println!(" IOS{} ({})", u32::from_str_radix(&hex::encode(&ios[4..8]), 16)?, hex::encode(ios).to_ascii_uppercase()); + } + println!(); + for ios in missing_tids { + println!("Downloading IOS{} ({})...", u32::from_str_radix(&hex::encode(&ios[4..8]), 16)?, hex::encode(ios).to_ascii_uppercase()); + let title = nus::download_title(ios, None, true)?; + let version = title.tmd.title_version; + println!(" Installing IOS{} ({}) v{}...", u32::from_str_radix(&hex::encode(&ios[4..8]), 16)?, hex::encode(ios).to_ascii_uppercase(), version); + emunand.install_title(title, false)?; + println!(" Installed IOS{} ({}) v{}!", u32::from_str_radix(&hex::encode(&ios[4..8]), 16)?, hex::encode(ios).to_ascii_uppercase(), version); + } + println!("\nAll missing IOSes have been installed!"); + Ok(()) +} + +pub fn install_title(wad: &str, emunand: &str, override_meta: &bool) -> Result<()> { let wad_path = Path::new(wad); if !wad_path.exists() { bail!("Source WAD \"{}\" could not be found.", wad_path.display()); @@ -45,7 +311,26 @@ pub fn install_title(wad: &str, emunand: &str) -> Result<()> { let wad_file = fs::read(wad_path).with_context(|| format!("Failed to open WAD file \"{}\" for reading.", wad_path.display()))?; let title = title::Title::from_bytes(&wad_file).with_context(|| format!("The provided WAD file \"{}\" appears to be invalid.", wad_path.display()))?; let emunand = emunand::EmuNAND::open(emunand_path.to_path_buf())?; - emunand.install_title(title)?; + emunand.install_title(title, *override_meta)?; println!("Successfully installed WAD \"{}\" to EmuNAND at \"{}\"!", wad_path.display(), emunand_path.display()); Ok(()) } + +pub fn uninstall_title(tid: &str, emunand: &str, remove_ticket: &bool) -> Result<()> { + let emunand_path = Path::new(emunand); + if !emunand_path.exists() { + bail!("Target EmuNAND directory \"{}\" could not be found.", emunand_path.display()); + } + let tid_as_path = Path::new(&tid); + let tid_bin: [u8; 8] = if tid_as_path.exists() { + let wad_file = fs::read(tid_as_path).with_context(|| format!("Failed to open WAD file \"{}\" for reading.", tid_as_path.display()))?; + let title = title::Title::from_bytes(&wad_file).with_context(|| format!("The provided WAD file \"{}\" appears to be invalid.", tid_as_path.display()))?; + title.tmd.title_id() + } else { + hex::decode(tid).with_context(|| "The specified Title ID is not valid! The Title ID must be in hex format.")?.try_into().unwrap() + }; + let emunand = emunand::EmuNAND::open(emunand_path.to_path_buf())?; + emunand.uninstall_title(tid_bin, *remove_ticket)?; + println!("Successfully uninstalled title with Title ID \"{}\" from EmuNAND at \"{}\"!", hex::encode(tid_bin).to_ascii_uppercase(), emunand_path.display()); + Ok(()) +} diff --git a/src/nand/emunand.rs b/src/nand/emunand.rs index 675814c..caa9a66 100644 --- a/src/nand/emunand.rs +++ b/src/nand/emunand.rs @@ -5,7 +5,8 @@ use std::fs; use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; +use glob::glob; use thiserror::Error; use crate::nand::sys; use crate::title; @@ -13,6 +14,8 @@ use crate::title::{cert, content, ticket, tmd}; #[derive(Debug, Error)] pub enum EmuNANDError { + #[error("the specified title is not installed to the EmuNAND")] + TitleNotInstalled, #[error("EmuNAND requires the directory `{0}`, but a file with that name already exists")] DirectoryNameConflict(String), #[error("specified EmuNAND root does not exist")] @@ -31,6 +34,15 @@ pub enum EmuNANDError { IO(#[from] std::io::Error), } +#[derive(Debug)] +/// A structure that represents titles installed to an EmuNAND. The title_type is the Title ID high, +/// which is the type of the titles the structure represents, and titles contains a Vec of Title ID +/// lows that represent each title installed in the given type. +pub struct InstalledTitles { + pub title_type: String, + pub titles: Vec, +} + fn safe_create_dir(dir: &PathBuf) -> Result<(), EmuNANDError> { if !dir.exists() { fs::create_dir(dir)?; @@ -42,7 +54,6 @@ fn safe_create_dir(dir: &PathBuf) -> Result<(), EmuNANDError> { /// An EmuNAND object that allows for creating and modifying Wii EmuNANDs. pub struct EmuNAND { - emunand_root: PathBuf, emunand_dirs: HashMap, } @@ -55,6 +66,7 @@ impl EmuNAND { return Err(EmuNANDError::RootNotFound); } let mut emunand_dirs: HashMap = HashMap::new(); + emunand_dirs.insert(String::from("root"), emunand_root.clone()); emunand_dirs.insert(String::from("import"), emunand_root.join("import")); emunand_dirs.insert(String::from("meta"), emunand_root.join("meta")); emunand_dirs.insert(String::from("shared1"), emunand_root.join("shared1")); @@ -72,13 +84,84 @@ impl EmuNAND { } } Ok(EmuNAND { - emunand_root, emunand_dirs, }) } - /// Install the provided title to the EmuNAND, mimicking a WAD installation performed by ES. - pub fn install_title(&self, title: title::Title) -> Result<(), EmuNANDError> { + /// Gets the path to a directory in the root of an EmuNAND, if it's a valid directory. + pub fn get_emunand_dir(&self, dir: &str) -> Option<&PathBuf> { + self.emunand_dirs.get(dir) + } + + /// Scans titles installed to an EmuNAND and returns a Vec of InstalledTitles instances. + pub fn get_installed_titles(&self) -> Vec { + // Scan TID highs in /title/ first. + let tid_highs: Vec = glob(&format!("{}/*", self.emunand_dirs["title"].display())) + .unwrap().filter_map(|f| f.ok()).collect(); + // Iterate over the TID lows in each TID high, and save every title where + // /title///title.tmd exists. + let mut installed_titles: Vec = Vec::new(); + for high in tid_highs { + if high.is_dir() { + let tid_lows: Vec = glob(&format!("{}/*", high.display())) + .unwrap().filter_map(|f| f.ok()).collect(); + let mut valid_lows: Vec = Vec::new(); + for low in tid_lows { + if low.join("content").join("title.tmd").exists() { + valid_lows.push(low.file_name().unwrap().to_str().unwrap().to_string().to_ascii_uppercase()); + } + } + installed_titles.push(InstalledTitles { + title_type: high.file_name().unwrap().to_str().unwrap().to_string().to_ascii_uppercase(), + titles: valid_lows, + }) + } + } + installed_titles + } + + /// Get the Ticket for a title installed to an EmuNAND. Returns a Ticket instance if a Ticket + /// with the specified Title ID can be found, or None if not. + pub fn get_title_ticket(&self, tid: [u8; 8]) -> Option { + let ticket_path = self.emunand_dirs["title"] + .join(hex::encode(&tid[0..4])) + .join(format!("{}.tik", hex::encode(&tid[4..8]))); + if ticket_path.exists() { + match fs::read(&ticket_path) { + Ok(content) => { + ticket::Ticket::from_bytes(&content).ok() + }, + Err(_) => None, + } + } else { + None + } + } + + /// Get the TMD for a title installed to an EmuNAND. Returns a Ticket instance if a TMD with the + /// specified Title ID can be found, or None if not. + pub fn get_title_tmd(&self, tid: [u8; 8]) -> Option { + let tmd_path = self.emunand_dirs["title"] + .join(hex::encode(&tid[0..4])) + .join(hex::encode(&tid[4..8]).to_ascii_lowercase()) + .join("content") + .join("title.tmd"); + if tmd_path.exists() { + match fs::read(&tmd_path) { + Ok(content) => { + tmd::TMD::from_bytes(&content).ok() + }, + Err(_) => None, + } + } else { + None + } + } + + /// Install the provided title to an EmuNAND, mimicking a WAD installation performed by ES. The + /// "override meta" option will install the content at index 0 as title.met, instead of any + /// actual meta/footer data contained in the title. + pub fn install_title(&self, title: title::Title, override_meta: bool) -> Result<(), EmuNANDError> { // Save the two halves of the TID, since those are part of the installation path. let tid_high = hex::encode(&title.tmd.title_id()[0..4]); let tid_low = hex::encode(&title.tmd.title_id()[4..8]); @@ -100,7 +183,7 @@ impl EmuNAND { fs::remove_dir_all(&title_dir)?; } fs::create_dir(&title_dir)?; - fs::write(title_dir.join("title.tmd"), title.content.to_bytes()?)?; + fs::write(title_dir.join("title.tmd"), title.tmd.to_bytes()?)?; for i in 0..title.content.content_records.borrow().len() { if matches!(title.content.content_records.borrow()[i].content_type, tmd::ContentType::Normal) { let content_path = title_dir.join(format!("{:08X}.app", title.content.content_records.borrow()[i].content_id).to_ascii_lowercase()); @@ -127,7 +210,13 @@ impl EmuNAND { } fs::write(&content_map_path, content_map.to_bytes()?)?; // The "footer" (officially "meta") is installed to /meta///title.met. - let meta_data = title.meta(); + // The "override meta" option installs the content at index 0 to title.met instead, as that + // content contains the banner, and that's what title.met is meant to hold. + let meta_data = if override_meta { + title.get_content_by_index(0)? + } else { + title.meta() + }; if !meta_data.is_empty() { let mut meta_dir = self.emunand_dirs["meta"].join(&tid_high); safe_create_dir(&meta_dir)?; @@ -147,4 +236,27 @@ impl EmuNAND { fs::write(&uid_sys_path, &uid_sys.to_bytes()?)?; Ok(()) } + + /// Uninstall a title with the provided Title ID from an EmuNAND. By default, the Ticket will be + /// left intact unlesss "remove ticket" is set to true. + pub fn uninstall_title(&self, tid: [u8; 8], remove_ticket: bool) -> Result<(), EmuNANDError> { + // Save the two halves of the TID, since those are part of the installation path. + let tid_high = hex::encode(&tid[0..4]); + let tid_low = hex::encode(&tid[4..8]); + // Ensure that a title directory actually exists for the specified title. If it does, then + // delete it. + let title_dir = self.emunand_dirs["title"].join(&tid_high).join(&tid_low); + if !title_dir.exists() { + return Err(EmuNANDError::TitleNotInstalled); + } + fs::remove_dir_all(&title_dir)?; + // If we've been told to delete the Ticket, check if it exists and then do so. + if remove_ticket { + let ticket_path = self.emunand_dirs["ticket"].join(&tid_high).join(format!("{}.tik", &tid_low)); + if ticket_path.exists() { + fs::remove_file(&ticket_path)?; + } + } + Ok(()) + } } diff --git a/src/nand/setting.rs b/src/nand/setting.rs index e8e3220..1e57abf 100644 --- a/src/nand/setting.rs +++ b/src/nand/setting.rs @@ -36,7 +36,6 @@ impl SettingTxt { } let setting_str = String::from_utf8_lossy(&dec_data); let setting_str = setting_str[0..setting_str.clone().rfind('\n').unwrap_or(setting_str.len() - 2) + 1].to_string(); - println!("{:?}", setting_str); let setting_txt = SettingTxt::from_string(setting_str)?; Ok(setting_txt) } @@ -45,7 +44,6 @@ impl SettingTxt { pub fn from_string(data: String) -> Result { let mut setting_keys: HashMap = HashMap::new(); for line in data.lines() { - println!("{}", line); let (key, value) = line.split_once("=").unwrap(); setting_keys.insert(key.to_owned(), value.to_owned()); } diff --git a/src/title/versions.rs b/src/title/versions.rs index 5ebef61..890827f 100644 --- a/src/title/versions.rs +++ b/src/title/versions.rs @@ -78,8 +78,6 @@ pub fn dec_to_standard(version: u16, title_id: &str, vwii: Option) -> Opti let map = wii_menu_versions_map(vwii); map.get(&version).cloned() } else { - let version_upper = (version as f64 / 256.0).floor() as u16; - let version_lower = version % 256; - Some(format!("{}.{}", version_upper, version_lower)) + Some(format!("{}.{}", version >> 8, version & 0xF)) } }