diff --git a/Cargo.lock b/Cargo.lock index e4a2aaa..5e27a64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aes" version = "0.8.4" @@ -166,6 +172,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + [[package]] name = "cbc" version = "0.1.2" @@ -297,6 +312,32 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "core-foundation" version = "0.9.4" @@ -341,6 +382,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -351,6 +407,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + [[package]] name = "der" version = "0.7.10" @@ -362,6 +424,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -371,6 +442,7 @@ dependencies = [ "block-buffer", "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -384,6 +456,15 @@ dependencies = [ "syn", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dunce" version = "1.0.5" @@ -427,6 +508,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "fnv" version = "1.0.7" @@ -547,11 +638,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "rand_core 0.10.0", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -579,6 +672,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -606,6 +705,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.4.0" @@ -923,6 +1031,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.182" @@ -959,6 +1073,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rust2" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +dependencies = [ + "sha2", +] + [[package]] name = "memchr" version = "2.8.0" @@ -971,6 +1094,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -998,6 +1131,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "num-integer" version = "0.1.46" @@ -1046,6 +1185,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1094,6 +1253,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1103,6 +1268,18 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1381,6 +1558,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1496,10 +1683,12 @@ dependencies = [ "regex", "reqwest", "rsa", + "rust-ini", "sha1", "tempfile", "thiserror 2.0.18", "walkdir", + "zip", ] [[package]] @@ -1630,6 +1819,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" version = "0.4.12" @@ -1791,6 +1986,35 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -1923,6 +2147,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.19.0" @@ -2571,6 +2801,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -2605,8 +2849,81 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e499faf5c6b97a0d086f4a8733de6d47aee2252b8127962439d8d4311a73f72" +dependencies = [ + "aes", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.4.1", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "typed-path", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index af7b45b..d195bd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,5 @@ reqwest = { version = "0", features = ["blocking"] } rand = "0" walkdir = "2" tempfile = "3" +rust-ini = "0" +zip = "8" diff --git a/LICENSE b/LICENSE index e8a0fd1..ed60952 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 NinjaCheetah +Copyright (c) 2025-2026 NinjaCheetah Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/archive/u8.rs b/src/archive/u8.rs index 96029e3..bd7855d 100644 --- a/src/archive/u8.rs +++ b/src/archive/u8.rs @@ -3,7 +3,6 @@ // // Implements the structures and methods required for parsing U8 archives. -use std::cmp::max; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use thiserror::Error; diff --git a/src/bin/playground/main.rs b/src/bin/playground/main.rs index 32024d0..ce504ac 100644 --- a/src/bin/playground/main.rs +++ b/src/bin/playground/main.rs @@ -7,40 +7,40 @@ use rustwii::archive::u8; // use rustii::title::content; fn main() { - // let data = fs::read("sm.wad").unwrap(); - // let title = title::Title::from_bytes(&data).unwrap(); - // println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id())); - // - // let wad = wad::WAD::from_bytes(&data).unwrap(); - // println!("size of tmd: {:?}", wad.tmd().len()); - // println!("num content records: {:?}", title.tmd.content_records().len()); - // println!("first record data: {:?}", title.tmd.content_records().first().unwrap()); - // println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned()); - // - // println!("title version from ticket is: {:?}", title.ticket.title_version()); - // println!("title key (enc): {:?}", title.ticket.title_key()); - // println!("title key (dec): {:?}", title.ticket.title_key_dec()); - // println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned()); - // - // println!("title is fakesigned: {:?}", title.is_fakesigned()); - // - // let cert_chain = &title.cert_chain; - // println!("cert chain OK"); - // let result = cert::verify_ca_cert(&cert_chain.ca_cert()).unwrap(); - // 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(); - // 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(); - // println!("TMD verified successfully: {}", result); - // - // 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); - // let result = cert::verify_ticket(&cert_chain.ticket_cert(), &title.ticket).unwrap(); - // println!("Ticket verified successfully: {}", result); - // - // let result = title.verify().unwrap(); - // println!("full title verified successfully: {}", result); + let data = fs::read("sm.wad").unwrap(); + let title = title::Title::from_bytes(&data).unwrap(); + println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id())); + + let wad = wad::WAD::from_bytes(&data).unwrap(); + println!("size of tmd: {:?}", wad.tmd().len()); + println!("num content records: {:?}", title.tmd.content_records().len()); + println!("first record data: {:?}", title.tmd.content_records().first().unwrap()); + println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned()); + + println!("title version from ticket is: {:?}", title.ticket.title_version()); + println!("title key (enc): {:?}", title.ticket.title_key()); + println!("title key (dec): {:?}", title.ticket.title_key_dec()); + println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned()); + + println!("title is fakesigned: {:?}", title.is_fakesigned()); + + let cert_chain = &title.cert_chain; + println!("cert chain OK"); + let result = cert::verify_ca_cert(&cert_chain.ca_cert()).unwrap(); + 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(); + 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(); + println!("TMD verified successfully: {}", result); + + 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); + let result = cert::verify_ticket(&cert_chain.ticket_cert(), &title.ticket).unwrap(); + println!("Ticket verified successfully: {}", result); + + let result = title.verify().unwrap(); + println!("full title verified successfully: {}", result); diff --git a/src/bin/rustwii/archive/theme.rs b/src/bin/rustwii/archive/theme.rs index c241806..2c3c32a 100644 --- a/src/bin/rustwii/archive/theme.rs +++ b/src/bin/rustwii/archive/theme.rs @@ -3,9 +3,17 @@ // // Code for the theme building commands in the rustii CLI. +use std::collections::HashMap; +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; use clap::Subcommand; -use tempfile::tempdir; +use ini::{Ini, ParseOption}; +use tempfile::Builder; +use zip::ZipArchive; +use rustwii::archive::{ash, lz77, u8}; +use crate::archive::u8::{pack_dir_recursive, unpack_dir_recursive}; #[derive(Subcommand)] #[command(arg_required_else_help = true)] @@ -13,16 +21,153 @@ pub enum Commands { /// Apply an MYM theme to the Wii Menu ApplyMym { /// The path to the source MYM file to apply - mym_path: String, + mym: String, /// The path to the base Wii Menu asset archive (000000xx.app) - base_path: String, + base: String, /// The file to output the finished theme to (.csm) output: String, } } -pub fn theme_apply_mym(mym_path: &str, base_path: &str, output: &str) -> Result<()> { - todo!(); +pub fn theme_apply_mym(mym: &str, base: &str, output: &str) -> Result<()> { + let mym_path = Path::new(mym); + if !mym_path.exists() { + bail!("Theme file \"{}\" could not be found.", mym); + } + + let base_path = Path::new(base); + if !base_path.exists() { + bail!("Base asset file \"{}\" could not be found.", base); + } + + let out_path = PathBuf::from(output); + + // Create the temporary work directory and extract the mym file to it. + let work_dir = Builder::new().prefix("mym_apply_").tempdir()?; + let mym_dir = work_dir.path().join("mym_work"); + let mym_buf = fs::read(mym_path).with_context(|| format!("Failed to open theme file \"{}\" for reading.", mym_path.display()))?; + ZipArchive::extract(&mut ZipArchive::new(Cursor::new(mym_buf))?, &mym_dir)?; + + // Load the mym ini file. Escapes have to be disabled so that Windows-formatted paths are + // loaded correct. + let mym_ini = Ini::load_from_file_opt( + mym_dir.join("mym.ini"), + ParseOption { enabled_escape: false, ..Default::default() } + ).with_context(|| "Failed to load theme config file. This theme may be invalid!")?; + + // Extract the base asset archive to the temporary dir. + let base_dir = work_dir.path().join("base_work"); + fs::create_dir(&base_dir)?; + let assets_u8 = u8::U8Directory::from_bytes(fs::read(base_path).with_context(|| format!("Base asset file \"{}\" could not be read.", base_path.display()))?.into_boxed_slice())?; + unpack_dir_recursive(&assets_u8, base_dir.clone()).expect("Failed to extract base assets, they may be invalid!"); + + // Store any nested containers that we extract so that they can be re-packed later. + let mut extracted_containers: HashMap = HashMap::new(); + + // Iterate through the ini file and apply modifications as necessary. + for (sec, prop) in mym_ini.iter() { + if let Some(sec) = sec { + if sec.contains("sdta") { + // Validate that the file and source keys exist, and then build a path to the + // source file. + if !prop.contains_key("file") || !prop.contains_key("source") { + bail!("Theme config entry \"{}\" is invalid and cannot be applied.", sec) + } + let source_parts: Vec<&str> = prop.get("source").unwrap().split("\\").collect(); + let mut source_path = mym_dir.clone(); + source_path.extend(source_parts); + + if !source_path.exists() { + bail!("Required source file \"{}\" could not be found! This theme may be invalid.", prop.get("source").unwrap()) + } + + println!("Applying static data file \"{}\" from theme...", source_path.file_name().unwrap().to_str().unwrap()); + let target_parts: Vec<&str> = prop.get("file").unwrap().split("\\").collect(); + let mut target_path = base_dir.clone(); + target_path.extend(target_parts); + fs::copy(source_path, target_path).expect("Failed to copy asset from theme."); + } else if sec.contains("cont") { + // Validate that the file key exists and that container specified exists. + if !prop.contains_key("file") { + bail!("Theme config entry \"{}\" is invalid and cannot be applied.", sec) + } + let container_parts: Vec<&str> = prop.get("file").unwrap().split("\\").collect(); + let mut container_path = base_dir.clone(); + container_path.extend(container_parts); + + if !container_path.exists() { + bail!("Required base container \"{}\" could not be found! The base assets or theme may be invalid.", prop.get("file").unwrap()) + } + + // Buffer in the container file, check its magic number, and decompress it if + // necessary. + println!("Unpacking base container \"{}\" for modification...", container_path.file_name().unwrap().to_str().unwrap()); + let container_data = fs::read(&container_path)?; + let decompressed_container = if &container_data[0..4] == b"LZ77" { + println!(" - Decompressing LZ77 data..."); + lz77::decompress_lz77(&container_data)? + } else if &container_data[0..4] == b"ASH0" { + println!(" - Decompressing ASH data..."); + ash::decompress_ash(&container_data, None, None)? + } else { + container_data + }; + + // Load the unpacked archive, bailing if it still isn't a U8 archive. + if &decompressed_container[0..4] != b"\x55\xAA\x38\x2D" { + bail!("Required base container \"{}\" is not a U8 archive. The base assets may be invalid.", container_path.file_name().unwrap().display()) + } + + // Extracted container name should follow the format: + // __out + let extracted_container_name = container_path + .file_name().unwrap() + .to_str().unwrap().replace(".", "_") + + "_out"; + let extracted_container_path = container_path.parent().unwrap().join(extracted_container_name); + fs::create_dir(&extracted_container_path)?; + let u8_root = u8::U8Directory::from_bytes(decompressed_container.into_boxed_slice()).with_context(|| "Failed to extract base container! The base assets may be invalid.")?; + + // Finally, unpack the specified container to the created path and register it as + // an extracted container so that we can repack it later. + unpack_dir_recursive(&u8_root, extracted_container_path.clone())?; + extracted_containers.insert( + container_path.file_name().unwrap().to_str().unwrap().to_owned(), + extracted_container_path + ); + println!(" - Done."); + } else { + bail!("Theme config file contains unknown or unsupported key \"{}\"!", sec) + } + } + } + + // Iterate over any containers we unpacked so we can repack them and clean up the unpacked + // folder. + println!("Repacking extracted containers..."); + for container in extracted_containers { + // Add the original file name to the parent of the extracted dir, and that's where the + // repacked container should go. + println!(" - Repacking container \"{}\"...", container.0); + let repacked_container_path = container.1.parent().unwrap().join(container.0.clone()); + let mut u8_root = u8::U8Directory::new(String::new()); + pack_dir_recursive(&mut u8_root, container.1.clone()).with_context(|| format!("Failed to repack extracted base container \"{}\". An unknown error occurred.", container.0))?; + + // Always compress the repacked archive with LZ77 compression. + let compressed_container = lz77::compress_lz77(&u8_root.to_bytes()?)?; + fs::write(repacked_container_path, compressed_container)?; + + // Erase the extracted container directory so it doesn't get packed into the final themed + // archive. + fs::remove_dir_all(container.1)?; + println!(" - Done."); + } + + // Theme applied, re-pack the base dir and write it out to the specified path. + let mut finished_u8 = u8::U8Directory::new(String::new()); + pack_dir_recursive(&mut finished_u8, base_dir).expect("Failed to pack finalized theme!"); + fs::write(&out_path, &finished_u8.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?; + println!("\nSuccessfully applied theme \"{}\" to output file \"{}\"!", mym, output); Ok(()) } diff --git a/src/bin/rustwii/archive/u8.rs b/src/bin/rustwii/archive/u8.rs index 3eeceeb..a3b3fd8 100644 --- a/src/bin/rustwii/archive/u8.rs +++ b/src/bin/rustwii/archive/u8.rs @@ -29,7 +29,7 @@ pub enum Commands { } } -fn pack_dir_recursive(dir: &mut u8::U8Directory, in_path: PathBuf) -> Result<()> { +pub fn pack_dir_recursive(dir: &mut u8::U8Directory, in_path: PathBuf) -> Result<()> { let mut files = Vec::new(); let mut dirs = Vec::new(); for entry in glob(&format!("{}/*", in_path.display()))?.flatten() { @@ -64,7 +64,7 @@ pub fn pack_u8_archive(input: &str, output: &str) -> Result<()> { Ok(()) } -fn unpack_dir_recursive(dir: &u8::U8Directory, out_path: PathBuf) -> Result<()> { +pub fn unpack_dir_recursive(dir: &u8::U8Directory, out_path: PathBuf) -> Result<()> { let out_path = out_path.join(&dir.name); for file in &dir.files { fs::write(out_path.join(&file.name), &file.data).with_context(|| format!("Failed to write output file \"{}\".", &file.name))?; diff --git a/src/bin/rustwii/main.rs b/src/bin/rustwii/main.rs index e6325b9..66ac050 100644 --- a/src/bin/rustwii/main.rs +++ b/src/bin/rustwii/main.rs @@ -156,8 +156,8 @@ fn main() -> Result<()> { }, 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)? + archive::theme::Commands::ApplyMym { mym, base, output } => { + archive::theme::theme_apply_mym(mym, base, output)? } } },