mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-02 22:01:02 -04:00
Added base for rustii CLI EmuNAND commands (mostly library-side)
The rustii CLI now offers setting decrypt/encrypt commands, as well as a WIP emunand install-title command. This command currently only supports installing single WADs and not a folder of WADs like WiiPy. To make EmuNANDs happen, library modules for parsing and editing setting.txt, uid.sys, and content.map have been added and have feature parity with libWiiPy. The basics of the library EmuNAND module also exist, offering the code for title installation and not much else yet.
This commit is contained in:
parent
15947ceff3
commit
26138c02be
@ -3,6 +3,7 @@
|
||||
use std::fs;
|
||||
use rustii::title::{wad, cert};
|
||||
use rustii::title;
|
||||
// use rustii::title::content;
|
||||
|
||||
fn main() {
|
||||
let data = fs::read("sm.wad").unwrap();
|
||||
@ -41,8 +42,17 @@ fn main() {
|
||||
|
||||
let result = title.verify().unwrap();
|
||||
println!("full title verified successfully: {}", result);
|
||||
|
||||
|
||||
|
||||
// let mut u8_archive = u8::U8Archive::from_bytes(&fs::read("00000001.app").unwrap()).unwrap();
|
||||
// println!("files and dirs counted: {}", u8_archive.node_tree.borrow().count());
|
||||
// fs::write("outfile.arc", u8_archive.to_bytes().unwrap()).unwrap();
|
||||
// println!("re-written");
|
||||
|
||||
|
||||
|
||||
// let mut content_map = content::SharedContentMap::from_bytes(&fs::read("content.map").unwrap()).unwrap();
|
||||
// content_map.add(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap();
|
||||
// fs::write("new.map", content_map.to_bytes().unwrap()).unwrap();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ mod archive;
|
||||
mod title;
|
||||
mod filetypes;
|
||||
mod info;
|
||||
mod nand;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, Parser};
|
||||
@ -26,6 +27,11 @@ enum Commands {
|
||||
#[command(subcommand)]
|
||||
command: archive::ash::Commands,
|
||||
},
|
||||
/// Manage Wii EmuNANDs
|
||||
EmuNAND {
|
||||
#[command(subcommand)]
|
||||
command: nand::emunand::Commands,
|
||||
},
|
||||
/// Fakesign a TMD, Ticket, or WAD (trucha bug)
|
||||
Fakesign {
|
||||
/// The path to a TMD, Ticket, or WAD
|
||||
@ -49,6 +55,11 @@ enum Commands {
|
||||
#[command(subcommand)]
|
||||
command: title::nus::Commands
|
||||
},
|
||||
/// Manage setting.txt
|
||||
Setting {
|
||||
#[command(subcommand)]
|
||||
command: nand::setting::Commands
|
||||
},
|
||||
/// Pack/unpack a U8 archive
|
||||
U8 {
|
||||
#[command(subcommand)]
|
||||
@ -74,6 +85,16 @@ fn main() -> Result<()> {
|
||||
archive::ash::decompress_ash(input, output)?
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(Commands::EmuNAND { command }) => {
|
||||
match command {
|
||||
nand::emunand::Commands::Debug { input } => {
|
||||
nand::emunand::debug(input)?
|
||||
},
|
||||
nand::emunand::Commands::InstallTitle { wad, emunand } => {
|
||||
nand::emunand::install_title(wad, emunand)?
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Fakesign { input, output }) => {
|
||||
title::fakesign::fakesign(input, output)?
|
||||
@ -106,7 +127,17 @@ fn main() -> Result<()> {
|
||||
title::nus::download_tmd(tid, version, output)?
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(Commands::Setting { command }) => {
|
||||
match command {
|
||||
nand::setting::Commands::Decrypt { input, output } => {
|
||||
nand::setting::decrypt_setting(input, output)?;
|
||||
},
|
||||
nand::setting::Commands::Encrypt { input, output } => {
|
||||
nand::setting::encrypt_setting(input, output)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(Commands::U8 { command }) => {
|
||||
match command {
|
||||
archive::u8::Commands::Pack { input, output } => {
|
||||
|
51
src/bin/rustii/nand/emunand.rs
Normal file
51
src/bin/rustii/nand/emunand.rs
Normal file
@ -0,0 +1,51 @@
|
||||
// nand/emunand.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Code for EmuNAND-related commands in the rustii CLI.
|
||||
|
||||
use std::{str, fs};
|
||||
use std::path::{Path, PathBuf};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::Subcommand;
|
||||
use rustii::nand::emunand;
|
||||
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,
|
||||
},
|
||||
InstallTitle {
|
||||
/// The path to the WAD file to install
|
||||
wad: String,
|
||||
/// The path to the target EmuNAND
|
||||
emunand: String,
|
||||
}
|
||||
}
|
||||
|
||||
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")?)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_title(wad: &str, emunand: &str) -> Result<()> {
|
||||
let wad_path = Path::new(wad);
|
||||
if !wad_path.exists() {
|
||||
bail!("Source WAD \"{}\" could not be found.", wad_path.display());
|
||||
}
|
||||
let emunand_path = Path::new(emunand);
|
||||
if !emunand_path.exists() {
|
||||
bail!("Target EmuNAND directory \"{}\" could not be found.", emunand_path.display());
|
||||
}
|
||||
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)?;
|
||||
println!("Successfully installed WAD \"{}\" to EmuNAND at \"{}\"!", wad_path.display(), emunand_path.display());
|
||||
Ok(())
|
||||
}
|
5
src/bin/rustii/nand/mod.rs
Normal file
5
src/bin/rustii/nand/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
// nand/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
|
||||
pub mod emunand;
|
||||
pub mod setting;
|
61
src/bin/rustii/nand/setting.rs
Normal file
61
src/bin/rustii/nand/setting.rs
Normal file
@ -0,0 +1,61 @@
|
||||
// nand/setting.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Code for setting.txt-related commands in the rustii CLI.
|
||||
|
||||
use std::{str, fs};
|
||||
use std::path::{Path, PathBuf};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::Subcommand;
|
||||
use rustii::nand::setting;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
pub enum Commands {
|
||||
/// Decrypt setting.txt
|
||||
Decrypt {
|
||||
/// The path to the setting.txt file to decrypt
|
||||
input: String,
|
||||
/// An optional output path; defaults to setting_dec.txt
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
},
|
||||
/// Encrypt setting.txt
|
||||
Encrypt {
|
||||
/// The path to the setting.txt to encrypt
|
||||
input: String,
|
||||
/// An optional output path; defaults to setting_enc.txt
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt_setting(input: &str, output: &Option<String>) -> Result<()> {
|
||||
let in_path = Path::new(input);
|
||||
if !in_path.exists() {
|
||||
bail!("Source file \"{}\" could not be found.", in_path.display());
|
||||
}
|
||||
let out_path = if output.is_some() {
|
||||
PathBuf::from(output.clone().unwrap()).with_extension("txt")
|
||||
} else {
|
||||
PathBuf::from("setting_dec.txt")
|
||||
};
|
||||
let setting = setting::SettingTxt::from_bytes(&fs::read(in_path)?).with_context(|| "The provided setting.txt could not be parsed, and is likely invalid.")?;
|
||||
fs::write(out_path, setting.to_string()?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn encrypt_setting(input: &str, output: &Option<String>) -> Result<()> {
|
||||
let in_path = Path::new(input);
|
||||
if !in_path.exists() {
|
||||
bail!("Source file \"{}\" could not be found.", in_path.display());
|
||||
}
|
||||
let out_path = if output.is_some() {
|
||||
PathBuf::from(output.clone().unwrap()).with_extension("txt")
|
||||
} else {
|
||||
PathBuf::from("setting_enc.txt")
|
||||
};
|
||||
let setting = setting::SettingTxt::from_string(String::from_utf8(fs::read(in_path)?).with_context(|| "Invalid characters found in input file!")?)?;
|
||||
fs::write(out_path, setting.to_bytes()?)?;
|
||||
Ok(())
|
||||
}
|
@ -4,4 +4,5 @@
|
||||
// Root level module that imports the feature modules.
|
||||
|
||||
pub mod archive;
|
||||
pub mod nand;
|
||||
pub mod title;
|
||||
|
150
src/nand/emunand.rs
Normal file
150
src/nand/emunand.rs
Normal file
@ -0,0 +1,150 @@
|
||||
// nand/emunand.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Implements the structures and methods required for handling Wii EmuNANDs.
|
||||
|
||||
use std::fs;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use crate::nand::sys;
|
||||
use crate::title;
|
||||
use crate::title::{cert, content, ticket, tmd};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EmuNANDError {
|
||||
#[error("EmuNAND requires the directory `{0}`, but a file with that name already exists")]
|
||||
DirectoryNameConflict(String),
|
||||
#[error("specified EmuNAND root does not exist")]
|
||||
RootNotFound,
|
||||
#[error("uid.sys processing error")]
|
||||
UidSys(#[from] sys::UidSysError),
|
||||
#[error("certificate processing error")]
|
||||
CertificateError(#[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("io error occurred during EmuNAND operation")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
fn safe_create_dir(dir: &PathBuf) -> Result<(), EmuNANDError> {
|
||||
if !dir.exists() {
|
||||
fs::create_dir(dir)?;
|
||||
} else if !dir.is_dir() {
|
||||
return Err(EmuNANDError::DirectoryNameConflict(dir.to_str().unwrap().to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An EmuNAND object that allows for creating and modifying Wii EmuNANDs.
|
||||
pub struct EmuNAND {
|
||||
emunand_root: PathBuf,
|
||||
emunand_dirs: HashMap<String, PathBuf>,
|
||||
}
|
||||
|
||||
impl EmuNAND {
|
||||
/// Open an existing EmuNAND in an EmuNAND instance that can be used to interact with it. This
|
||||
/// will initialize the basic directory structure if it doesn't already exist, but will not do
|
||||
/// anything beyond that.
|
||||
pub fn open(emunand_root: PathBuf) -> Result<Self, EmuNANDError> {
|
||||
if !emunand_root.exists() {
|
||||
return Err(EmuNANDError::RootNotFound);
|
||||
}
|
||||
let mut emunand_dirs: HashMap<String, PathBuf> = HashMap::new();
|
||||
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"));
|
||||
emunand_dirs.insert(String::from("shared2"), emunand_root.join("shared2"));
|
||||
emunand_dirs.insert(String::from("sys"), emunand_root.join("sys"));
|
||||
emunand_dirs.insert(String::from("ticket"), emunand_root.join("ticket"));
|
||||
emunand_dirs.insert(String::from("title"), emunand_root.join("title"));
|
||||
emunand_dirs.insert(String::from("tmp"), emunand_root.join("tmp"));
|
||||
emunand_dirs.insert(String::from("wfs"), emunand_root.join("wfs"));
|
||||
for dir in emunand_dirs.keys() {
|
||||
if !emunand_dirs[dir].exists() {
|
||||
fs::create_dir(&emunand_dirs[dir])?;
|
||||
} else if !emunand_dirs[dir].is_dir() {
|
||||
return Err(EmuNANDError::DirectoryNameConflict(emunand_dirs[dir].to_str().unwrap().to_string()));
|
||||
}
|
||||
}
|
||||
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> {
|
||||
// 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]);
|
||||
// Tickets are installed to /ticket/<tid_high>/<tid_low>.tik.
|
||||
let ticket_dir = self.emunand_dirs["ticket"].join(&tid_high);
|
||||
safe_create_dir(&ticket_dir)?;
|
||||
fs::write(ticket_dir.join(format!("{}.tik", &tid_low)), title.ticket.to_bytes()?)?;
|
||||
// TMDs and normal content (non-shared) are installed to
|
||||
// /title/<tid_high>/<tid_low>/content/, as title.tmd and <cid>.app.
|
||||
let mut title_dir = self.emunand_dirs["title"].join(&tid_high);
|
||||
safe_create_dir(&title_dir)?;
|
||||
title_dir = title_dir.join(&tid_low);
|
||||
safe_create_dir(&title_dir)?;
|
||||
// Create an empty "data" dir if it doesn't exist.
|
||||
safe_create_dir(&title_dir.join("data"))?;
|
||||
title_dir = title_dir.join("content");
|
||||
// Delete any existing installed content/the current TMD.
|
||||
if title_dir.exists() {
|
||||
fs::remove_dir_all(&title_dir)?;
|
||||
}
|
||||
fs::create_dir(&title_dir)?;
|
||||
fs::write(title_dir.join("title.tmd"), title.content.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());
|
||||
fs::write(content_path, title.get_content_by_index(i)?)?;
|
||||
}
|
||||
}
|
||||
// Shared content needs to be installed to /shared1/, with incremental names decided by
|
||||
// the records in /shared1/content.map.
|
||||
// Start by checking for a map and loading it if it exists, so that we know what shared
|
||||
// content is already installed.
|
||||
let content_map_path = self.emunand_dirs["shared1"].join("content.map");
|
||||
let mut content_map = if content_map_path.exists() {
|
||||
content::SharedContentMap::from_bytes(&fs::read(&content_map_path)?)?
|
||||
} else {
|
||||
content::SharedContentMap::new()
|
||||
};
|
||||
for i in 0..title.content.content_records.borrow().len() {
|
||||
if matches!(title.content.content_records.borrow()[i].content_type, tmd::ContentType::Shared) {
|
||||
if let Some(file_name) = content_map.add(&title.content.content_records.borrow()[i].content_hash)? {
|
||||
let content_path = self.emunand_dirs["shared1"].join(format!("{}.app", file_name.to_ascii_lowercase()));
|
||||
fs::write(content_path, title.get_content_by_index(i)?)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
if !meta_data.is_empty() {
|
||||
let mut meta_dir = self.emunand_dirs["meta"].join(&tid_high);
|
||||
safe_create_dir(&meta_dir)?;
|
||||
meta_dir = meta_dir.join(&tid_low);
|
||||
safe_create_dir(&meta_dir)?;
|
||||
fs::write(meta_dir.join("title.met"), meta_data)?;
|
||||
}
|
||||
// Finally, we need to update uid.sys (or create it if it doesn't exist) so that the newly
|
||||
// installed title will actually show up (at least for channels).
|
||||
let uid_sys_path = self.emunand_dirs["sys"].join("uid.sys");
|
||||
let mut uid_sys = if uid_sys_path.exists() {
|
||||
sys::UidSys::from_bytes(&fs::read(&uid_sys_path)?)?
|
||||
} else {
|
||||
sys::UidSys::new()
|
||||
};
|
||||
uid_sys.add(&title.tmd.title_id())?;
|
||||
fs::write(&uid_sys_path, &uid_sys.to_bytes()?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
8
src/nand/mod.rs
Normal file
8
src/nand/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
// nand/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Root for all NAND-related modules.
|
||||
|
||||
pub mod emunand;
|
||||
pub mod setting;
|
||||
pub mod sys;
|
102
src/nand/setting.rs
Normal file
102
src/nand/setting.rs
Normal file
@ -0,0 +1,102 @@
|
||||
// nand/setting.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Implements the structures and methods required for parsing and editing setting.txt in the Wii
|
||||
// Menu's data.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use byteorder::ReadBytesExt;
|
||||
|
||||
const SETTINGS_KEY: u32 = 0x73B5DBFA;
|
||||
|
||||
/// A structure that allows for encrypting, decrypting, parsing, and editing a setting.txt file.
|
||||
pub struct SettingTxt {
|
||||
pub area: String,
|
||||
pub model: String,
|
||||
pub dvd: u8,
|
||||
pub mpch: String,
|
||||
pub code: String,
|
||||
pub serial_number: String,
|
||||
pub video: String,
|
||||
pub game: String,
|
||||
}
|
||||
|
||||
impl SettingTxt {
|
||||
/// Creates a new SettingTxt instance from the binary data of an encrypted setting.txt file.
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, std::io::Error> {
|
||||
// Unlike most files we have to deal with, setting.txt is encrypted. This means we need to
|
||||
// decrypt it first, and *then* we can parse it.
|
||||
let mut buf = Cursor::new(data);
|
||||
let mut key: u32 = SETTINGS_KEY;
|
||||
let mut dec_data: Vec<u8> = Vec::new();
|
||||
for _ in 0..256 {
|
||||
dec_data.push(buf.read_u8()? ^ (key & 0xFF) as u8);
|
||||
key = key.rotate_left(1); // Automatic bit rotation!? Thanks for the tip clippy!
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
/// Creates a new SettingTxt instance from the decrypted text of a setting.txt file.
|
||||
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());
|
||||
}
|
||||
let area = setting_keys["AREA"].to_string();
|
||||
let model = setting_keys["MODEL"].to_string();
|
||||
let dvd = setting_keys["DVD"].as_str().parse::<u8>().unwrap();
|
||||
let mpch = setting_keys["MPCH"].to_string();
|
||||
let code = setting_keys["CODE"].to_string();
|
||||
let serial_number = setting_keys["SERNO"].to_string();
|
||||
let video = setting_keys["VIDEO"].to_string();
|
||||
let game = setting_keys["GAME"].to_string();
|
||||
Ok(SettingTxt {
|
||||
area,
|
||||
model,
|
||||
dvd,
|
||||
mpch,
|
||||
code,
|
||||
serial_number,
|
||||
video,
|
||||
game,
|
||||
})
|
||||
}
|
||||
|
||||
/// Encrypts and then dumps the data in a SettingTxt instance back into binary data that can be
|
||||
/// written to a file.
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, std::io::Error> {
|
||||
let setting_str = self.to_string()?;
|
||||
let setting_bytes = setting_str.as_bytes();
|
||||
let mut buf = Cursor::new(setting_bytes);
|
||||
let mut key: u32 = SETTINGS_KEY;
|
||||
let mut enc_data: Vec<u8> = Vec::new();
|
||||
for _ in 0..setting_str.len() {
|
||||
enc_data.push(buf.read_u8()? ^ (key & 0xFF) as u8);
|
||||
key = key.rotate_left(1);
|
||||
}
|
||||
enc_data.resize(256, 0);
|
||||
Ok(enc_data)
|
||||
}
|
||||
|
||||
/// Dumps the decrypted data in a SettingTxt instance into a string that can be written to a
|
||||
/// file.
|
||||
pub fn to_string(&self) -> Result<String, std::io::Error> {
|
||||
let mut setting_str = String::new();
|
||||
setting_str += &format!("AREA={}\r\n", self.area);
|
||||
setting_str += &format!("MODEL={}\r\n", self.model);
|
||||
setting_str += &format!("DVD={}\r\n", self.dvd);
|
||||
setting_str += &format!("MPCH={}\r\n", self.mpch);
|
||||
setting_str += &format!("CODE={}\r\n", self.code);
|
||||
setting_str += &format!("SERNO={}\r\n", self.serial_number);
|
||||
setting_str += &format!("VIDEO={}\r\n", self.video);
|
||||
setting_str += &format!("GAME={}\r\n", self.game);
|
||||
Ok(setting_str)
|
||||
}
|
||||
}
|
87
src/nand/sys.rs
Normal file
87
src/nand/sys.rs
Normal file
@ -0,0 +1,87 @@
|
||||
// nand/sys.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Implements the structures and methods required for parsing and editing files in /sys/ on the
|
||||
// Wii's NAND.
|
||||
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UidSysError {
|
||||
#[error("uid.sys is an invalid length and cannot be parsed")]
|
||||
InvalidUidSysLength,
|
||||
#[error("uid.sys data is not in a valid format")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// A structure that represents a Title ID/UID pairing in a uid.sys file.
|
||||
pub struct UidSysEntry {
|
||||
pub title_id: [u8; 8],
|
||||
pub uid: u32,
|
||||
}
|
||||
|
||||
/// A structure that allows for creating, parsing, and editing a /sys/uid.sys file.
|
||||
pub struct UidSys {
|
||||
entries: Vec<UidSysEntry>,
|
||||
}
|
||||
|
||||
impl UidSys {
|
||||
/// Creates a new UidSys instance from the binary data of a uid.sys file.
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, UidSysError> {
|
||||
// The uid.sys file must be divisible by a multiple of 12, or something is wrong, since each
|
||||
// entry is 12 bytes long.
|
||||
if (data.len() % 12) != 0 {
|
||||
return Err(UidSysError::InvalidUidSysLength);
|
||||
}
|
||||
let entry_count = data.len() / 12;
|
||||
let mut buf = Cursor::new(data);
|
||||
let mut entries: Vec<UidSysEntry> = Vec::new();
|
||||
for _ in 0..entry_count {
|
||||
let mut title_id = [0u8; 8];
|
||||
buf.read_exact(&mut title_id)?;
|
||||
let uid = buf.read_u32::<BigEndian>()?;
|
||||
entries.push(UidSysEntry { title_id, uid });
|
||||
}
|
||||
Ok(UidSys { entries })
|
||||
}
|
||||
|
||||
/// Creates a new UidSys instance and initializes it with the default entry of the Wii Menu
|
||||
/// (0000000100000002) with UID 0x1000.
|
||||
pub fn new() -> Self {
|
||||
let mut uid_sys = UidSys { entries: Vec::new() };
|
||||
uid_sys.add(&[0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2]).unwrap();
|
||||
uid_sys
|
||||
}
|
||||
|
||||
/// Dumps the data in a UidSys back into binary data that can be written to a file.
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, UidSysError> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
for entry in self.entries.iter() {
|
||||
buf.write_all(&entry.title_id)?;
|
||||
buf.write_u32::<BigEndian>(entry.uid)?;
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Adds a new Title ID to uid.sys, and assigns it a new UID. The new Title ID will only be
|
||||
/// added if it is not already present in the file. Returns None if the Title ID was already
|
||||
/// present, or the newly assigned UID if the Title ID was just added.
|
||||
pub fn add(&mut self, title_id: &[u8; 8]) -> Result<Option<u32>, UidSysError> {
|
||||
// Return None if the Title ID is already accounted for.
|
||||
if self.entries.iter().any(|entry| entry.title_id == *title_id) {
|
||||
return Ok(None);
|
||||
}
|
||||
// Find the highest UID and increment it to choose the UID for the new Title ID.
|
||||
let max_uid = self.entries.iter()
|
||||
.max_by_key(|entry| entry.uid)
|
||||
.map(|entry| entry.uid)
|
||||
.unwrap_or(4095);
|
||||
self.entries.push(UidSysEntry {
|
||||
title_id: *title_id,
|
||||
uid: max_uid + 1,
|
||||
});
|
||||
Ok(Some(max_uid + 1))
|
||||
}
|
||||
}
|
@ -26,6 +26,10 @@ pub enum ContentError {
|
||||
CIDAlreadyExists(u32),
|
||||
#[error("content's hash did not match the expected value (was {hash}, expected {expected})")]
|
||||
BadHash { hash: String, expected: String },
|
||||
#[error("content.map is an invalid length and cannot be parsed")]
|
||||
InvalidSharedContentMapLength,
|
||||
#[error("found invalid shared content name `{0}`")]
|
||||
InvalidSharedContentName(String),
|
||||
#[error("content data is not in a valid format")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
@ -106,7 +110,7 @@ impl ContentRegion {
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, std::io::Error> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
for i in 0..self.content_records.borrow().len() {
|
||||
let mut content = self.contents[i as usize].clone();
|
||||
let mut content = self.contents[i].clone();
|
||||
// Round up size to nearest 64 to add appropriate padding.
|
||||
content.resize((content.len() + 63) & !63, 0);
|
||||
buf.write_all(&content)?;
|
||||
@ -285,3 +289,83 @@ impl ContentRegion {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A structure that represents a shared Content ID/content hash pairing in a content.map file.
|
||||
pub struct ContentMapEntry {
|
||||
pub shared_id: u32,
|
||||
pub hash: [u8; 20],
|
||||
}
|
||||
|
||||
/// A structure that allows for parsing and editing a /shared1/content.map file.
|
||||
pub struct SharedContentMap {
|
||||
pub records: Vec<ContentMapEntry>,
|
||||
}
|
||||
|
||||
impl SharedContentMap {
|
||||
/// Creates a new SharedContentMap instance from the binary data of a content.map file.
|
||||
pub fn from_bytes(data: &[u8]) -> Result<SharedContentMap, ContentError> {
|
||||
// The uid.sys file must be divisible by a multiple of 28, or something is wrong, since each
|
||||
// entry is 28 bytes long.
|
||||
if (data.len() % 28) != 0 {
|
||||
return Err(ContentError::InvalidSharedContentMapLength);
|
||||
}
|
||||
let record_count = data.len() / 28;
|
||||
let mut buf = Cursor::new(data);
|
||||
let mut records: Vec<ContentMapEntry> = Vec::new();
|
||||
for _ in 0..record_count {
|
||||
// This requires some convoluted parsing, because Nintendo represents the file names as
|
||||
// actual chars and not numbers, despite the fact that the names are always numbers and
|
||||
// using numbers would make incrementing easier. Read the names in as a string, and then
|
||||
// parse that hex string into a u32.
|
||||
let mut shared_id_bytes = [0u8; 8];
|
||||
buf.read_exact(&mut shared_id_bytes)?;
|
||||
let shared_id_str = String::from_utf8_lossy(&shared_id_bytes);
|
||||
let shared_id = match u32::from_str_radix(&shared_id_str, 16) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Err(ContentError::InvalidSharedContentName(shared_id_str.to_string())),
|
||||
};
|
||||
let mut hash = [0u8; 20];
|
||||
buf.read_exact(&mut hash)?;
|
||||
records.push(ContentMapEntry { shared_id, hash });
|
||||
}
|
||||
Ok(SharedContentMap { records })
|
||||
}
|
||||
|
||||
/// Creates a new, empty SharedContentMap instance that can then be populated.
|
||||
pub fn new() -> Self {
|
||||
SharedContentMap { records: Vec::new() }
|
||||
}
|
||||
|
||||
/// Dumps the data in a SharedContentMap back into binary data that can be written to a file.
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, std::io::Error> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
for record in self.records.iter() {
|
||||
let shared_id = format!("{:08X}", record.shared_id).to_ascii_lowercase();
|
||||
buf.write_all(shared_id.as_bytes())?;
|
||||
buf.write_all(&record.hash)?;
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Adds new shared content to content.map, and assigns it a new file name. The new content
|
||||
/// will only be added if its hash is not already present in the file. Returns None if the
|
||||
/// content hash was already present, or the assigned file name if the hash was just added.
|
||||
pub fn add(&mut self, hash: &[u8; 20]) -> Result<Option<String>, ContentError> {
|
||||
// Return None if the hash is already accounted for.
|
||||
if self.records.iter().any(|entry| entry.hash == *hash) {
|
||||
return Ok(None);
|
||||
}
|
||||
// Find the highest index (represented by the file name) and increment it to choose the
|
||||
// name for the new shared content.
|
||||
let max_index = self.records.iter()
|
||||
.max_by_key(|record| record.shared_id)
|
||||
.map(|record| record.shared_id + 1)
|
||||
.unwrap_or(0);
|
||||
self.records.push(ContentMapEntry {
|
||||
shared_id: max_index,
|
||||
hash: *hash,
|
||||
});
|
||||
Ok(Some(format!("{:08X}", max_index)))
|
||||
}
|
||||
}
|
||||
|
@ -124,50 +124,50 @@ impl TMD {
|
||||
/// Creates a new TMD instance from the binary data of a TMD file.
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, TMDError> {
|
||||
let mut buf = Cursor::new(data);
|
||||
let signature_type = buf.read_u32::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let signature_type = buf.read_u32::<BigEndian>()?;
|
||||
let mut signature = [0u8; 256];
|
||||
buf.read_exact(&mut signature).map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut signature)?;
|
||||
// Maybe this can be read differently?
|
||||
let mut padding1 = [0u8; 60];
|
||||
buf.read_exact(&mut padding1).map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut padding1)?;
|
||||
let mut signature_issuer = [0u8; 64];
|
||||
buf.read_exact(&mut signature_issuer).map_err(TMDError::IO)?;
|
||||
let tmd_version = buf.read_u8().map_err(TMDError::IO)?;
|
||||
let ca_crl_version = buf.read_u8().map_err(TMDError::IO)?;
|
||||
let signer_crl_version = buf.read_u8().map_err(TMDError::IO)?;
|
||||
let is_vwii = buf.read_u8().map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut signature_issuer)?;
|
||||
let tmd_version = buf.read_u8()?;
|
||||
let ca_crl_version = buf.read_u8()?;
|
||||
let signer_crl_version = buf.read_u8()?;
|
||||
let is_vwii = buf.read_u8()?;
|
||||
let mut ios_tid = [0u8; 8];
|
||||
buf.read_exact(&mut ios_tid).map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut ios_tid)?;
|
||||
let mut title_id = [0u8; 8];
|
||||
buf.read_exact(&mut title_id).map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut title_id)?;
|
||||
let mut title_type = [0u8; 4];
|
||||
buf.read_exact(&mut title_type).map_err(TMDError::IO)?;
|
||||
let group_id = buf.read_u16::<BigEndian>().map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut title_type)?;
|
||||
let group_id = buf.read_u16::<BigEndian>()?;
|
||||
// Same here...
|
||||
let mut padding2 = [0u8; 2];
|
||||
buf.read_exact(&mut padding2).map_err(TMDError::IO)?;
|
||||
let region = buf.read_u16::<BigEndian>().map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut padding2)?;
|
||||
let region = buf.read_u16::<BigEndian>()?;
|
||||
let mut ratings = [0u8; 16];
|
||||
buf.read_exact(&mut ratings).map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut ratings)?;
|
||||
// ...and here...
|
||||
let mut reserved1 = [0u8; 12];
|
||||
buf.read_exact(&mut reserved1).map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut reserved1)?;
|
||||
let mut ipc_mask = [0u8; 12];
|
||||
buf.read_exact(&mut ipc_mask).map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut ipc_mask)?;
|
||||
// ...and here.
|
||||
let mut reserved2 = [0u8; 18];
|
||||
buf.read_exact(&mut reserved2).map_err(TMDError::IO)?;
|
||||
let access_rights = buf.read_u32::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let title_version = buf.read_u16::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let num_contents = buf.read_u16::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let boot_index = buf.read_u16::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let minor_version = buf.read_u16::<BigEndian>().map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut reserved2)?;
|
||||
let access_rights = buf.read_u32::<BigEndian>()?;
|
||||
let title_version = buf.read_u16::<BigEndian>()?;
|
||||
let num_contents = buf.read_u16::<BigEndian>()?;
|
||||
let boot_index = buf.read_u16::<BigEndian>()?;
|
||||
let minor_version = buf.read_u16::<BigEndian>()?;
|
||||
// Build content records by iterating over the rest of the data num_contents times.
|
||||
let mut content_records = Vec::with_capacity(num_contents as usize);
|
||||
for _ in 0..num_contents {
|
||||
let content_id = buf.read_u32::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let index = buf.read_u16::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let type_int = buf.read_u16::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let content_id = buf.read_u32::<BigEndian>()?;
|
||||
let index = buf.read_u16::<BigEndian>()?;
|
||||
let type_int = buf.read_u16::<BigEndian>()?;
|
||||
let content_type = match type_int {
|
||||
1 => ContentType::Normal,
|
||||
2 => ContentType::Development,
|
||||
@ -176,9 +176,9 @@ impl TMD {
|
||||
32769 => ContentType::Shared,
|
||||
_ => return Err(TMDError::InvalidContentType(type_int))
|
||||
};
|
||||
let content_size = buf.read_u64::<BigEndian>().map_err(TMDError::IO)?;
|
||||
let content_size = buf.read_u64::<BigEndian>()?;
|
||||
let mut content_hash = [0u8; 20];
|
||||
buf.read_exact(&mut content_hash).map_err(TMDError::IO)?;
|
||||
buf.read_exact(&mut content_hash)?;
|
||||
content_records.push(ContentRecord {
|
||||
content_id,
|
||||
index,
|
||||
|
Loading…
x
Reference in New Issue
Block a user