mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-01 05:31:00 -04:00
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.
This commit is contained in:
parent
26138c02be
commit
94e0be0eef
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -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"
|
||||
|
@ -37,3 +37,4 @@ anyhow = "1"
|
||||
thiserror = "2"
|
||||
reqwest = { version = "0", features = ["blocking"] }
|
||||
rand = "0"
|
||||
walkdir = "2"
|
||||
|
@ -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)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String> = Vec::new();
|
||||
let mut installed_titles: Vec<String> = Vec::new();
|
||||
let mut disc_titles: Vec<String> = Vec::new();
|
||||
for category in categories {
|
||||
if category.title_type == "00000001" {
|
||||
let mut ioses: Vec<u32> = 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<String> = 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<String> = Vec::new();
|
||||
let mut installed_titles: Vec<String> = Vec::new();
|
||||
for category in categories {
|
||||
if category.title_type == "00000001" {
|
||||
let mut ioses: Vec<u32> = 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<tmd::TMD> = installed_titles.iter().map(|x| emunand.get_title_tmd(hex::decode(x).unwrap().try_into().unwrap()).unwrap()).collect();
|
||||
let mut missing_ioses: Vec<u32> = 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(())
|
||||
}
|
||||
|
@ -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<String>,
|
||||
}
|
||||
|
||||
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<String, PathBuf>,
|
||||
}
|
||||
|
||||
@ -55,6 +66,7 @@ impl EmuNAND {
|
||||
return Err(EmuNANDError::RootNotFound);
|
||||
}
|
||||
let mut emunand_dirs: HashMap<String, PathBuf> = 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<InstalledTitles> {
|
||||
// Scan TID highs in /title/ first.
|
||||
let tid_highs: Vec<PathBuf> = 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/<tid_high>/<tid_low>/title.tmd exists.
|
||||
let mut installed_titles: Vec<InstalledTitles> = Vec::new();
|
||||
for high in tid_highs {
|
||||
if high.is_dir() {
|
||||
let tid_lows: Vec<PathBuf> = glob(&format!("{}/*", high.display()))
|
||||
.unwrap().filter_map(|f| f.ok()).collect();
|
||||
let mut valid_lows: Vec<String> = 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<ticket::Ticket> {
|
||||
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<tmd::TMD> {
|
||||
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/<tid_high>/<tid_low>/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(())
|
||||
}
|
||||
}
|
||||
|
@ -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<Self, std::io::Error> {
|
||||
let mut setting_keys: HashMap<String, String> = 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());
|
||||
}
|
||||
|
@ -78,8 +78,6 @@ pub fn dec_to_standard(version: u16, title_id: &str, vwii: Option<bool>) -> 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))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user