Overhauled U8 archive handling

This commit is contained in:
2026-02-26 00:00:58 -05:00
parent 5cc6c1c8ff
commit 23699a518d
9 changed files with 324 additions and 248 deletions

87
Cargo.lock generated
View File

@@ -405,6 +405,22 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -884,9 +900,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.88" version = "0.3.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -919,6 +935,12 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.1" version = "0.8.1"
@@ -1280,9 +1302,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.9" version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
@@ -1366,10 +1388,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustls" name = "rustix"
version = "0.23.36" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"once_cell", "once_cell",
@@ -1462,6 +1497,7 @@ dependencies = [
"reqwest", "reqwest",
"rsa", "rsa",
"sha1", "sha1",
"tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",
"walkdir", "walkdir",
] ]
@@ -1702,6 +1738,19 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "tempfile"
version = "3.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [
"fastrand",
"getrandom 0.4.1",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@@ -1973,9 +2022,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.111" version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1986,9 +2035,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.61" version = "0.4.63"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -2000,9 +2049,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.111" version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -2010,9 +2059,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.111" version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -2023,9 +2072,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.111" version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -2066,9 +2115,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.88" version = "0.3.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustwii" name = "rustwii"
authors = ["NinjaCheetah <ninjacheetah@ncxprogramming.com>"] authors = ["NinjaCheetah <campbell@ninjacheetah.dev>"]
license = "MIT" license = "MIT"
description = "A Rust library and CLI for handling files and formats used by the Wii" description = "A Rust library and CLI for handling files and formats used by the Wii"
version = "0.1.0" version = "0.1.0"
@@ -29,7 +29,7 @@ cbc = "0"
aes = "0" aes = "0"
rsa = { version = "0", features = ["sha2"] } rsa = { version = "0", features = ["sha2"] }
hex = "0" hex = "0"
sha1 = { version = "0", features = ["oid"]} sha1 = { version = "0", features = ["oid"] }
glob = "0" glob = "0"
regex = "1" regex = "1"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
@@ -38,3 +38,4 @@ thiserror = "2"
reqwest = { version = "0", features = ["blocking"] } reqwest = { version = "0", features = ["blocking"] }
rand = "0" rand = "0"
walkdir = "2" walkdir = "2"
tempfile = "3"

View File

@@ -3,9 +3,8 @@
// //
// Implements the structures and methods required for parsing U8 archives. // Implements the structures and methods required for parsing U8 archives.
use std::cell::RefCell; use std::cmp::max;
use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::rc::{Rc, Weak};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use thiserror::Error; use thiserror::Error;
@@ -26,83 +25,18 @@ pub enum U8Error {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct U8Directory { pub struct U8Directory {
pub name: String, pub name: String,
pub parent: Option<Weak<RefCell<U8Directory>>>, pub dirs: Vec<U8Directory>,
pub dirs: Vec<Rc<RefCell<U8Directory>>>, pub files: Vec<U8File>,
pub files: Vec<Rc<RefCell<U8File>>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct U8File { pub struct U8File {
pub name: String, pub name: String,
pub data: Vec<u8>, pub data: Vec<u8>,
pub parent: Option<Weak<RefCell<U8Directory>>>,
}
impl U8Directory {
pub fn new(name: String) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
name,
parent: None,
dirs: Vec::new(),
files: Vec::new(),
}))
}
pub fn add_dir(parent: &Rc<RefCell<Self>>, child: Rc<RefCell<Self>>) {
child.borrow_mut().parent = Some(Rc::downgrade(parent));
parent.borrow_mut().dirs.push(child);
}
pub fn add_file(parent: &Rc<RefCell<Self>>, file: Rc<RefCell<U8File>>) {
file.borrow_mut().parent = Some(Rc::downgrade(parent));
parent.borrow_mut().files.push(file);
}
pub fn get_parent(&self) -> Option<Rc<RefCell<U8Directory>>> {
self.parent.as_ref()?.upgrade()
}
pub fn get_child_dir(parent: &Rc<RefCell<U8Directory>>, name: &str) -> Option<Rc<RefCell<U8Directory>>> {
parent.borrow().dirs.iter()
.find(|dir| dir.borrow().name == name)
.map(Rc::clone)
}
fn count_recursive(dir: &Rc<RefCell<U8Directory>>, count: &mut usize) {
*count += dir.borrow().files.len();
for dir in dir.borrow().dirs.iter() {
*count += 1;
Self::count_recursive(dir, count);
}
}
pub fn count(&self) -> usize {
let mut count: usize = 1;
count += self.files.len();
for dir in &self.dirs {
count += 1;
Self::count_recursive(dir, &mut count);
}
count
}
}
impl U8File {
pub fn new(name: String, data: Vec<u8>) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
name,
data,
parent: None,
}))
}
pub fn get_parent(&self) -> Option<Rc<RefCell<U8Directory>>> {
self.parent.as_ref()?.upgrade()
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct U8Node { struct U8Node {
pub node_type: u8, pub node_type: u8,
pub name_offset: u32, // This is really type u24, so the most significant byte will be ignored. pub name_offset: u32, // This is really type u24, so the most significant byte will be ignored.
pub data_offset: u32, pub data_offset: u32,
@@ -110,13 +44,15 @@ pub struct U8Node {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct U8Archive { struct U8Reader {
pub node_tree: Rc<RefCell<U8Directory>>, buf: Cursor<Box<[u8]>>,
u8_nodes: Vec<U8Node>,
index: usize,
base_name_offset: u64
} }
impl U8Archive { impl U8Reader {
/// Creates a new U8 instance from the binary data of a U8 file. fn new(data: Box<[u8]>) -> Result<Self, U8Error> {
pub fn from_bytes(data: &[u8]) -> Result<Self, U8Error> {
let mut buf = Cursor::new(data); let mut buf = Cursor::new(data);
let mut magic = [0u8; 4]; let mut magic = [0u8; 4];
buf.read_exact(&mut magic)?; buf.read_exact(&mut magic)?;
@@ -135,7 +71,7 @@ impl U8Archive {
} }
println!("ignoring IMET header at 0x40"); println!("ignoring IMET header at 0x40");
} }
// Check for an IMET header that comes after a built tag. // Check for an IMET header that comes after a build tag.
else { else {
buf.seek(SeekFrom::Start(0x80))?; buf.seek(SeekFrom::Start(0x80))?;
buf.read_exact(&mut magic)?; buf.read_exact(&mut magic)?;
@@ -150,6 +86,7 @@ impl U8Archive {
} }
} }
} }
// We're skipping the following values: // We're skipping the following values:
// root_node_offset (u32): constant value, always 0x20 // root_node_offset (u32): constant value, always 0x20
// header_size (u32): we don't need this because we already know how long the string table is // header_size (u32): we don't need this because we already know how long the string table is
@@ -168,6 +105,7 @@ impl U8Archive {
data_offset: root_node_data_offset, data_offset: root_node_data_offset,
size: root_node_size, size: root_node_size,
}; };
// Create a vec of nodes, push the root node, and then iterate over the remaining number // Create a vec of nodes, push the root node, and then iterate over the remaining number
// of nodes in the file and push them to the vec. // of nodes in the file and push them to the vec.
let mut u8_nodes: Vec<U8Node> = Vec::new(); let mut u8_nodes: Vec<U8Node> = Vec::new();
@@ -179,103 +117,131 @@ impl U8Archive {
let size = buf.read_u32::<BigEndian>()?; let size = buf.read_u32::<BigEndian>()?;
u8_nodes.push(U8Node { node_type, name_offset, data_offset, size }) u8_nodes.push(U8Node { node_type, name_offset, data_offset, size })
} }
// Iterate over the loaded nodes and load the file names and data associated with them. // Save the base name offset for later.
let base_name_offset = buf.position(); let base_name_offset = buf.position();
let mut file_names = Vec::<String>::new();
let mut file_data = Vec::<Vec<u8>>::new(); Ok(Self {
for node in &u8_nodes { buf,
buf.seek(SeekFrom::Start(base_name_offset + node.name_offset as u64))?; u8_nodes,
let mut name_bin = Vec::<u8>::new(); index: 0,
// Read the file name one byte at a time until we find a null byte. base_name_offset
loop { })
let byte = buf.read_u8()?; }
if byte == b'\0' {
break; fn file_name(&mut self, name_offset: u64) -> Result<String, U8Error> {
} self.buf.seek(SeekFrom::Start(self.base_name_offset + name_offset))?;
name_bin.push(byte); let mut name_bin = Vec::<u8>::new();
} loop {
file_names.push(String::from_utf8(name_bin).map_err(|_| U8Error::InvalidFileName(base_name_offset + node.name_offset as u64))?.to_owned()); let byte = self.buf.read_u8()?;
// If this is a file node, read the data for the file. if byte == b'\0' {
if node.node_type == 0 { break;
buf.seek(SeekFrom::Start(node.data_offset as u64))?;
let mut data = vec![0u8; node.size as usize];
buf.read_exact(&mut data)?;
file_data.push(data);
} else {
file_data.push(Vec::new());
} }
name_bin.push(byte);
} }
// Now that we have all the data loaded out of the file, assemble the tree of U8Items that Ok(String::from_utf8(name_bin)
// provides an actual map of the archive's data. .map_err(|_| U8Error::InvalidFileName(self.base_name_offset + name_offset))?.to_owned()
let node_tree = U8Directory::new(String::new()); )
let mut focused_node = Rc::clone(&node_tree); }
// This is the order of directory nodes we've traversed down.
let mut parent_dirs: Vec<u32> = Vec::from([0]); fn file_data(&mut self, data_offset: u64, size: usize) -> Result<Vec<u8>, U8Error> {
for i in 0..u8_nodes.len() { self.buf.seek(SeekFrom::Start(data_offset))?;
match u8_nodes[i].node_type { let mut data = vec![0u8; size];
self.buf.read_exact(&mut data)?;
Ok(data)
}
fn read_dir_recursive(&mut self) -> Result<U8Directory, U8Error> {
let mut current_dir = U8Directory::new(self.file_name(self.u8_nodes[self.index].name_offset as u64)?);
let current_dir_end = self.u8_nodes[self.index].size as usize;
self.index += 1;
while self.index < current_dir_end {
match self.u8_nodes[self.index].node_type {
1 => { 1 => {
// Code for a directory node. // Directory node, recursive over the child dir and then add it to the
if u8_nodes[i].name_offset != 0 { // current one.
// If we're already at the correct level, push a new empty dir item to the let child_dir = self.read_dir_recursive()?;
// item we're currently working on. current_dir.add_dir(child_dir);
if u8_nodes[i].data_offset == *parent_dirs.last().unwrap() {
parent_dirs.push(i as u32);
U8Directory::add_dir(&focused_node, U8Directory::new(file_names[i].clone()));
focused_node = U8Directory::get_child_dir(&focused_node, &file_names[i]).unwrap();
}
// Otherwise, go back up the path until we're at the correct level.
else {
while u8_nodes[i].data_offset != *parent_dirs.last().unwrap() {
parent_dirs.pop();
let parent = focused_node.as_ref().borrow().get_parent().unwrap();
focused_node = parent;
}
parent_dirs.push(i as u32);
// Rebuild current working directory, and make sure all directories in the
// path exist.
U8Directory::add_dir(&focused_node, U8Directory::new(file_names[i].clone()));
focused_node = U8Directory::get_child_dir(&focused_node, &file_names[i]).unwrap()
}
}
}, },
0 => { 0 => {
// Code for a file node. // File node, add
U8Directory::add_file(&focused_node, U8File::new(file_names[i].clone(), file_data[i].clone())); current_dir.add_file(
U8File::new(
self.file_name(self.u8_nodes[self.index].name_offset as u64)?,
self.file_data(self.u8_nodes[self.index].data_offset as u64, self.u8_nodes[self.index].size as usize)?
)
);
self.index += 1;
}, },
x => return Err(U8Error::InvalidNodeType(x, i)) x => return Err(U8Error::InvalidNodeType(x, self.index))
} }
} }
Ok(U8Archive {
node_tree, Ok(current_dir)
})
} }
}
pub fn from_tree(node_tree: &Rc<RefCell<U8Directory>>) -> Result<Self, U8Error> {
Ok(U8Archive { impl U8Directory {
node_tree: node_tree.clone(), pub fn new(name: String) -> Self {
}) Self {
name,
dirs: vec![],
files: vec![]
}
} }
fn pack_dir_recursive(file_names: &mut Vec<String>, file_data: &mut Vec<Vec<u8>>, u8_nodes: &mut Vec<U8Node>, current_node: &Rc<RefCell<U8Directory>>) { pub fn dirs(&self) -> &Vec<U8Directory> {
// For files, read their data into the file data list, add their name into the file name &self.dirs
// list, then calculate the offset for their file name and create a new U8Node() for them. }
pub fn set_dirs(&mut self, dirs: Vec<U8Directory>) {
self.dirs = dirs
}
pub fn files(&self) -> &Vec<U8File> {
&self.files
}
pub fn set_files(&mut self, files: Vec<U8File>) {
self.files = files
}
pub fn add_dir(&mut self, child: Self) -> &mut U8Directory {
self.dirs.push(child);
self.dirs.last_mut().unwrap()
}
pub fn add_file(&mut self, file: U8File) {
self.files.push(file);
}
/// Creates a new U8 instance from the binary data of a U8 file.
pub fn from_bytes(data: Box<[u8]>) -> Result<Self, U8Error> {
let mut u8_reader = U8Reader::new(data)?;
u8_reader.read_dir_recursive()
}
fn pack_dir_recursive(&self, file_names: &mut Vec<String>, file_data: &mut Vec<Vec<u8>>, u8_nodes: &mut Vec<U8Node>) {
// For files, read their data into the file data list, add their name into the file name
// list, then calculate the offset for their file name and create a new U8Node() for them.
// 0 values for name/data offsets are temporary and are set later. // 0 values for name/data offsets are temporary and are set later.
let parent_node = u8_nodes.len() - 1; let parent_node = u8_nodes.len() - 1;
for file in &current_node.borrow().files { for file in &self.files {
file_names.push(file.borrow().name.clone()); file_names.push(file.name.clone());
file_data.push(file.borrow().data.clone()); file_data.push(file.data.clone());
u8_nodes.push(U8Node { node_type: 0, name_offset: 0, data_offset: 0, size: file_data[u8_nodes.len()].len() as u32}); u8_nodes.push(U8Node { node_type: 0, name_offset: 0, data_offset: 0, size: file_data[u8_nodes.len()].len() as u32});
} }
// For directories, add their name to the file name list, add empty data to the file data
// list, find the total number of files and directories inside the directory to calculate // For directories, add their name to the file name list, add empty data to the file data
// the final node included in it, then recursively call this function again on that // list, find the total number of files and directories inside the directory to calculate
// the final node included in it, then recursively call this function again on that
// directory to process it. // directory to process it.
for dir in &current_node.borrow().dirs { for dir in &self.dirs {
file_names.push(dir.borrow().name.clone()); file_names.push(dir.name.clone());
file_data.push(Vec::new()); file_data.push(Vec::new());
let max_node = u8_nodes.len() + current_node.borrow().count() + 1; let max_node = u8_nodes.len() + dir.count();
u8_nodes.push(U8Node { node_type: 1, name_offset: 0, data_offset: parent_node as u32, size: max_node as u32}); u8_nodes.push(U8Node { node_type: 1, name_offset: 0, data_offset: parent_node as u32, size: max_node as u32});
U8Archive::pack_dir_recursive(file_names, file_data, u8_nodes, dir) dir.pack_dir_recursive(file_names, file_data, u8_nodes);
} }
} }
@@ -285,9 +251,9 @@ impl U8Archive {
let mut file_names: Vec<String> = vec![String::new()]; let mut file_names: Vec<String> = vec![String::new()];
let mut file_data: Vec<Vec<u8>> = vec![Vec::new()]; let mut file_data: Vec<Vec<u8>> = vec![Vec::new()];
let mut u8_nodes: Vec<U8Node> = Vec::new(); let mut u8_nodes: Vec<U8Node> = Vec::new();
u8_nodes.push(U8Node { node_type: 1, name_offset: 0, data_offset: 0, size: self.node_tree.borrow().count() as u32 }); u8_nodes.push(U8Node { node_type: 1, name_offset: 0, data_offset: 0, size: self.count() as u32 });
let root_node = Rc::clone(&self.node_tree); self.pack_dir_recursive(&mut file_names, &mut file_data, &mut u8_nodes);
U8Archive::pack_dir_recursive(&mut file_names, &mut file_data, &mut u8_nodes, &root_node);
// Header size starts at 0 because the header size starts with the nodes and does not // Header size starts at 0 because the header size starts with the nodes and does not
// include the actual file header. // include the actual file header.
let mut header_size: u32 = 0; let mut header_size: u32 = 0;
@@ -315,6 +281,7 @@ impl U8Archive {
u8_nodes[i].name_offset = current_name_offset; u8_nodes[i].name_offset = current_name_offset;
current_name_offset += file_names[i].len() as u32 + 1 current_name_offset += file_names[i].len() as u32 + 1
} }
// Begin writing file data. // Begin writing file data.
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
buf.write_all(b"\x55\xAA\x38\x2D")?; buf.write_all(b"\x55\xAA\x38\x2D")?;
@@ -344,4 +311,26 @@ impl U8Archive {
} }
Ok(buf) Ok(buf)
} }
fn count_recursive(&self) -> usize {
let mut count = self.files.len() + self.dirs.len();
for dir in self.dirs.iter() {
count += dir.count_recursive();
}
count
}
pub fn count(&self) -> usize {
1 + self.count_recursive()
}
}
impl U8File {
pub fn new(name: String, data: Vec<u8>) -> Self {
Self {
name,
data
}
}
} }

View File

@@ -3,47 +3,49 @@
use std::fs; use std::fs;
use rustwii::title::{wad, cert}; use rustwii::title::{wad, cert};
use rustwii::title; use rustwii::title;
use rustwii::archive::u8;
// use rustii::title::content; // use rustii::title::content;
fn main() { fn main() {
let data = fs::read("sm.wad").unwrap(); // let data = fs::read("sm.wad").unwrap();
let title = title::Title::from_bytes(&data).unwrap(); // let title = title::Title::from_bytes(&data).unwrap();
println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id())); // println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id()));
//
let wad = wad::WAD::from_bytes(&data).unwrap(); // let wad = wad::WAD::from_bytes(&data).unwrap();
println!("size of tmd: {:?}", wad.tmd().len()); // println!("size of tmd: {:?}", wad.tmd().len());
println!("num content records: {:?}", title.tmd.content_records().len()); // println!("num content records: {:?}", title.tmd.content_records().len());
println!("first record data: {:?}", title.tmd.content_records().first().unwrap()); // println!("first record data: {:?}", title.tmd.content_records().first().unwrap());
println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned()); // println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned());
//
println!("title version from ticket is: {:?}", title.ticket.title_version()); // println!("title version from ticket is: {:?}", title.ticket.title_version());
println!("title key (enc): {:?}", title.ticket.title_key()); // println!("title key (enc): {:?}", title.ticket.title_key());
println!("title key (dec): {:?}", title.ticket.title_key_dec()); // println!("title key (dec): {:?}", title.ticket.title_key_dec());
println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned()); // println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned());
//
println!("title is fakesigned: {:?}", title.is_fakesigned()); // println!("title is fakesigned: {:?}", title.is_fakesigned());
//
let cert_chain = &title.cert_chain; // let cert_chain = &title.cert_chain;
println!("cert chain OK"); // println!("cert chain OK");
let result = cert::verify_ca_cert(&cert_chain.ca_cert()).unwrap(); // let result = cert::verify_ca_cert(&cert_chain.ca_cert()).unwrap();
println!("CA cert {} verified successfully: {}", cert_chain.ca_cert().child_cert_identity(), result); // println!("CA cert {} verified successfully: {}", cert_chain.ca_cert().child_cert_identity(), result);
//
let result = cert::verify_child_cert(&cert_chain.ca_cert(), &cert_chain.tmd_cert()).unwrap(); // let result = cert::verify_child_cert(&cert_chain.ca_cert(), &cert_chain.tmd_cert()).unwrap();
println!("TMD cert {} verified successfully: {}", cert_chain.tmd_cert().child_cert_identity(), result); // println!("TMD cert {} verified successfully: {}", cert_chain.tmd_cert().child_cert_identity(), result);
let result = cert::verify_tmd(&cert_chain.tmd_cert(), &title.tmd).unwrap(); // let result = cert::verify_tmd(&cert_chain.tmd_cert(), &title.tmd).unwrap();
println!("TMD verified successfully: {}", result); // println!("TMD verified successfully: {}", result);
//
let result = cert::verify_child_cert(&cert_chain.ca_cert(), &cert_chain.ticket_cert()).unwrap(); // let result = cert::verify_child_cert(&cert_chain.ca_cert(), &cert_chain.ticket_cert()).unwrap();
println!("Ticket cert {} verified successfully: {}", cert_chain.ticket_cert().child_cert_identity(), result); // println!("Ticket cert {} verified successfully: {}", cert_chain.ticket_cert().child_cert_identity(), result);
let result = cert::verify_ticket(&cert_chain.ticket_cert(), &title.ticket).unwrap(); // let result = cert::verify_ticket(&cert_chain.ticket_cert(), &title.ticket).unwrap();
println!("Ticket verified successfully: {}", result); // println!("Ticket verified successfully: {}", result);
//
let result = title.verify().unwrap(); // let result = title.verify().unwrap();
println!("full title verified successfully: {}", result); // println!("full title verified successfully: {}", result);
// let mut u8_archive = u8::U8Archive::from_bytes(&fs::read("00000001.app").unwrap()).unwrap(); let u8_archive = u8::U8Directory::from_bytes(fs::read("testu8.arc").unwrap().into_boxed_slice()).unwrap();
println!("{:#?}", u8_archive);
// println!("files and dirs counted: {}", u8_archive.node_tree.borrow().count()); // println!("files and dirs counted: {}", u8_archive.node_tree.borrow().count());
// fs::write("outfile.arc", u8_archive.to_bytes().unwrap()).unwrap(); // fs::write("outfile.arc", u8_archive.to_bytes().unwrap()).unwrap();
// println!("re-written"); // println!("re-written");

View File

@@ -4,3 +4,4 @@
pub mod ash; pub mod ash;
pub mod lz77; pub mod lz77;
pub mod u8; pub mod u8;
pub mod theme;

View File

@@ -0,0 +1,28 @@
// archive/theme.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Code for the theme building commands in the rustii CLI.
use anyhow::{bail, Context, Result};
use clap::Subcommand;
use tempfile::tempdir;
#[derive(Subcommand)]
#[command(arg_required_else_help = true)]
pub enum Commands {
/// Apply an MYM theme to the Wii Menu
ApplyMym {
/// The path to the source MYM file to apply
mym_path: String,
/// The path to the base Wii Menu asset archive (000000xx.app)
base_path: String,
/// The file to output the finished theme to (<filename>.csm)
output: String,
}
}
pub fn theme_apply_mym(mym_path: &str, base_path: &str, output: &str) -> Result<()> {
todo!();
Ok(())
}

View File

@@ -4,9 +4,7 @@
// Code for the U8 packing/unpacking commands in the rustii CLI. // Code for the U8 packing/unpacking commands in the rustii CLI.
use std::{str, fs}; use std::{str, fs};
use std::cell::RefCell;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::rc::Rc;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use clap::Subcommand; use clap::Subcommand;
use glob::glob; use glob::glob;
@@ -31,7 +29,7 @@ pub enum Commands {
} }
} }
fn pack_dir_recursive(dir: &Rc<RefCell<u8::U8Directory>>, in_path: PathBuf) -> Result<()> { fn pack_dir_recursive(dir: &mut u8::U8Directory, in_path: PathBuf) -> Result<()> {
let mut files = Vec::new(); let mut files = Vec::new();
let mut dirs = Vec::new(); let mut dirs = Vec::new();
for entry in glob(&format!("{}/*", in_path.display()))?.flatten() { for entry in glob(&format!("{}/*", in_path.display()))?.flatten() {
@@ -43,13 +41,12 @@ fn pack_dir_recursive(dir: &Rc<RefCell<u8::U8Directory>>, in_path: PathBuf) -> R
} }
for file in files { for file in files {
let node = u8::U8File::new(file.file_name().unwrap().to_str().unwrap().to_owned(), fs::read(file)?); let node = u8::U8File::new(file.file_name().unwrap().to_str().unwrap().to_owned(), fs::read(file)?);
u8::U8Directory::add_file(dir, node); dir.add_file(node);
} }
for child_dir in dirs { for child_dir in dirs {
let node = u8::U8Directory::new(child_dir.file_name().unwrap().to_str().unwrap().to_owned()); let node = u8::U8Directory::new(child_dir.file_name().unwrap().to_str().unwrap().to_owned());
u8::U8Directory::add_dir(dir, node); let dir = dir.add_dir(node);
let dir = u8::U8Directory::get_child_dir(dir, child_dir.file_name().unwrap().to_str().unwrap()).unwrap(); pack_dir_recursive(dir, child_dir)?;
pack_dir_recursive(&dir, child_dir)?;
} }
Ok(()) Ok(())
} }
@@ -60,22 +57,21 @@ pub fn pack_u8_archive(input: &str, output: &str) -> Result<()> {
bail!("Source directory \"{}\" could not be found.", in_path.display()); bail!("Source directory \"{}\" could not be found.", in_path.display());
} }
let out_path = PathBuf::from(output); let out_path = PathBuf::from(output);
let node_tree = u8::U8Directory::new(String::new()); let mut root_dir = u8::U8Directory::new(String::new());
pack_dir_recursive(&node_tree, in_path.to_path_buf()).with_context(|| "A U8 archive could not be packed.")?; pack_dir_recursive(&mut root_dir, in_path.to_path_buf()).with_context(|| "A U8 archive could not be packed.")?;
let u8_archive = u8::U8Archive::from_tree(&node_tree).with_context(|| "An unknown error occurred while creating a U8 archive from the data.")?; fs::write(&out_path, &root_dir.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
fs::write(&out_path, &u8_archive.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
println!("Successfully packed directory \"{}\" into U8 archive \"{}\"!", in_path.display(), out_path.display()); println!("Successfully packed directory \"{}\" into U8 archive \"{}\"!", in_path.display(), out_path.display());
Ok(()) Ok(())
} }
fn unpack_dir_recursive(dir: &Rc<RefCell<u8::U8Directory>>, out_path: PathBuf) -> Result<()> { fn unpack_dir_recursive(dir: &u8::U8Directory, out_path: PathBuf) -> Result<()> {
let out_path = out_path.join(&dir.borrow().name); let out_path = out_path.join(&dir.name);
for file in &dir.borrow().files { for file in &dir.files {
fs::write(out_path.join(&file.borrow().name), &file.borrow().data).with_context(|| format!("Failed to write output file \"{}\".", &file.borrow().name))?; fs::write(out_path.join(&file.name), &file.data).with_context(|| format!("Failed to write output file \"{}\".", &file.name))?;
} }
for dir in &dir.borrow().dirs { for dir in &dir.dirs {
if !out_path.join(&dir.borrow().name).exists() { if !out_path.join(&dir.name).exists() {
fs::create_dir(out_path.join(&dir.borrow().name)).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?; fs::create_dir(out_path.join(&dir.name)).with_context(|| format!("The output directory \"{}\" could not be created.", out_path.display()))?;
} }
unpack_dir_recursive(dir, out_path.clone())?; unpack_dir_recursive(dir, out_path.clone())?;
} }
@@ -97,8 +93,8 @@ pub fn unpack_u8_archive(input: &str, output: &str) -> Result<()> {
} }
// Extract the files and directories in the root, and then recurse over each directory to // Extract the files and directories in the root, and then recurse over each directory to
// extract the files and directories they contain. // extract the files and directories they contain.
let u8_archive = u8::U8Archive::from_bytes(&fs::read(in_path).with_context(|| format!("Input file \"{}\" could not be read.", in_path.display()))?)?; let root_dir = u8::U8Directory::from_bytes(fs::read(in_path).with_context(|| format!("Input file \"{}\" could not be read.", in_path.display()))?.into_boxed_slice())?;
unpack_dir_recursive(&u8_archive.node_tree, out_path.clone())?; unpack_dir_recursive(&root_dir, out_path.clone())?;
println!("Successfully unpacked U8 archive to directory \"{}\"!", out_path.display()); println!("Successfully unpacked U8 archive to directory \"{}\"!", out_path.display());
Ok(()) Ok(())
} }

View File

@@ -4,9 +4,7 @@
// Code for the info command in the rustii CLI. // Code for the info command in the rustii CLI.
use std::{str, fs}; use std::{str, fs};
use std::cell::RefCell;
use std::path::Path; use std::path::Path;
use std::rc::Rc;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use rustwii::archive::u8; use rustwii::archive::u8;
use rustwii::{title, title::cert, title::tmd, title::ticket, title::wad, title::versions}; use rustwii::{title, title::cert, title::tmd, title::ticket, title::wad, title::versions};
@@ -243,32 +241,32 @@ fn print_wad_info(wad: wad::WAD) -> Result<()> {
Ok(()) Ok(())
} }
fn print_full_tree(dir: &Rc<RefCell<u8::U8Directory>>, indent: usize) { fn print_full_tree(dir: &u8::U8Directory, indent: usize) {
let prefix = " ".repeat(indent); let prefix = " ".repeat(indent);
let dir_name = if !dir.borrow().name.is_empty() { let dir_name = if !dir.name.is_empty() {
&dir.borrow().name &dir.name
} else { } else {
&String::from("root") &String::from("root")
}; };
println!("{}D {}", prefix, dir_name); println!("{}D {}", prefix, dir_name);
// Print subdirectories // Print subdirectories
for subdir in &dir.borrow().dirs { for subdir in &dir.dirs {
print_full_tree(subdir, indent + 1); print_full_tree(subdir, indent + 1);
} }
// Print files // Print files
for file in &dir.borrow().files { for file in &dir.files {
let file_name = &file.borrow().name; let file_name = &file.name;
println!("{} F {}", prefix, file_name); println!("{} F {}", prefix, file_name);
} }
} }
fn print_u8_info(u8_archive: u8::U8Archive) -> Result<()> { fn print_u8_info(root_dir: u8::U8Directory) -> Result<()> {
println!("U8 Archive Info"); println!("U8 Archive Info");
println!(" Node Count: {}", u8_archive.node_tree.borrow().count()); println!(" Node Count: {}", root_dir.count());
println!(" Archive Data:"); println!(" Archive Data:");
print_full_tree(&u8_archive.node_tree, 2); print_full_tree(&root_dir, 2);
Ok(()) Ok(())
} }
@@ -291,7 +289,7 @@ pub fn info(input: &str) -> Result<()> {
print_wad_info(wad)?; print_wad_info(wad)?;
}, },
Some(WiiFileType::U8) => { Some(WiiFileType::U8) => {
let u8_archive = u8::U8Archive::from_bytes(&fs::read(in_path)?).with_context(|| "The provided U8 archive could not be parsed, and is likely invalid.")?; let u8_archive = u8::U8Directory::from_bytes(fs::read(in_path)?.into_boxed_slice()).with_context(|| "The provided U8 archive could not be parsed, and is likely invalid.")?;
print_u8_info(u8_archive)?; print_u8_info(u8_archive)?;
} }
None => { None => {

View File

@@ -60,6 +60,11 @@ enum Commands {
#[command(subcommand)] #[command(subcommand)]
command: nand::setting::Commands command: nand::setting::Commands
}, },
/// Apply custom themes to the Wii Menu
Theme {
#[command(subcommand)]
command: archive::theme::Commands
},
/// Edit a TMD file /// Edit a TMD file
Tmd { Tmd {
#[command(subcommand)] #[command(subcommand)]
@@ -149,7 +154,14 @@ fn main() -> Result<()> {
} }
} }
}, },
Some(Commands::Tmd { command}) => { Some(Commands::Theme { command }) => {
match command {
archive::theme::Commands::ApplyMym { mym_path, base_path, output } => {
archive::theme::theme_apply_mym(mym_path, base_path, output)?
}
}
},
Some(Commands::Tmd { command }) => {
match command { match command {
title::tmd::Commands::Edit { input, output, edits} => { title::tmd::Commands::Edit { input, output, edits} => {
title::tmd::tmd_edit(input, output, edits)? title::tmd::tmd_edit(input, output, edits)?