mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2026-03-10 03:57:48 -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:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user