From c1d2a921c9566e1576ca9eff7ca9ec90e8e9d1ce Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 9 Feb 2023 13:04:39 -0500 Subject: [PATCH] Initial commit --- .gitignore | 3 + Cargo.lock | 415 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 17 ++ rustfmt.toml | 8 + src/argh_version.rs | 63 +++++++ src/cmd/mod.rs | 1 + src/cmd/pak.rs | 326 ++++++++++++++++++++++++++++++++++ src/main.rs | 35 ++++ src/util/file.rs | 57 ++++++ src/util/mod.rs | 1 + 10 files changed, 926 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rustfmt.toml create mode 100644 src/argh_version.rs create mode 100644 src/cmd/mod.rs create mode 100644 src/cmd/pak.rs create mode 100644 src/main.rs create mode 100644 src/util/file.rs create mode 100644 src/util/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c1a478 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +/out +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..29626e0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,415 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + +[[package]] +name = "argh" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab257697eb9496bf75526f0217b5ed64636a9cfafa78b8365c71bd283fcef93e" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b382dbd3288e053331f03399e1db106c9fb0d8562ad62cb04859ae926f324fa6" +dependencies = [ + "argh_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cb94155d965e3d37ffbbe7cc5b82c3dd79dd33bd48e536f73d2cfb8d85506f" + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "binrw" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272caaf6e0bfb7d508c0606e541e2c68f85c0d6352b62d0b299924eed59fe384" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4b28c1e534d96213c8966bb9240095757aa0909128985f97d16afd2e7257a8" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +dependencies = [ + "libc", +] + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "paktool-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "argh", + "binrw", + "binrw_derive", + "byteorder", + "env_logger", + "log", + "memmap2", + "uuid", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "uuid" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..06e4808 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "paktool-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.69" +argh = "0.1.10" +binrw = "0.11.1" +binrw_derive = "0.11.1" +byteorder = "1.4.3" +env_logger = "0.10.0" +log = "0.4.17" +memmap2 = "0.5.8" +uuid = "1.3.0" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..0a9eda5 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +fn_single_line = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +overflow_delimited_expr = true +reorder_impl_items = true +use_field_init_shorthand = true +use_small_heuristics = "Max" +where_single_line = true diff --git a/src/argh_version.rs b/src/argh_version.rs new file mode 100644 index 0000000..32705d7 --- /dev/null +++ b/src/argh_version.rs @@ -0,0 +1,63 @@ +// From https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c +//! Extend `argh` to be better integrated with the `cargo` ecosystem +//! +//! For now, this only adds a --version/-V option which causes early-exit. +use argh::{FromArgs, TopLevelCommand}; + +struct ArgsOrVersion(T); +impl TopLevelCommand for ArgsOrVersion where T: FromArgs {} +impl FromArgs for ArgsOrVersion +where T: FromArgs +{ + fn from_args(command_name: &[&str], args: &[&str]) -> Result { + /// Also use argh for catching `--version`-only invocations + #[derive(FromArgs)] + struct Version { + /// print version information and exit + #[argh(switch, short = 'V')] + pub version: bool, + } + match Version::from_args(command_name, args) { + Ok(v) => { + if v.version { + Err(argh::EarlyExit { + output: format!( + "{} {}", + command_name.first().unwrap_or(&""), + env!("CARGO_PKG_VERSION"), + ), + status: Ok(()), + }) + } else { + // seems args are empty + T::from_args(command_name, args).map(Self) + } + } + Err(exit) => match exit.status { + Ok(()) => { + // must have been --help + let help = match T::from_args(command_name, &["--help"]) { + Ok(_) => unreachable!(), + Err(exit) => exit.output, + }; + Err(argh::EarlyExit { + output: format!( + "{help} -V, --version print version information and exit" + ), + status: Ok(()), + }) + } + Err(()) => T::from_args(command_name, args).map(Self), + }, + } + } +} + +/// Create a `FromArgs` type from the current process’s `env::args`. +/// +/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested. +/// Error messages will be printed to stderr, and `--help` output to stdout. +pub fn from_env() -> T +where T: TopLevelCommand { + argh::from_env::>().0 +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs new file mode 100644 index 0000000..39f5341 --- /dev/null +++ b/src/cmd/mod.rs @@ -0,0 +1 @@ +pub mod pak; diff --git a/src/cmd/pak.rs b/src/cmd/pak.rs new file mode 100644 index 0000000..e532832 --- /dev/null +++ b/src/cmd/pak.rs @@ -0,0 +1,326 @@ +use std::{ + fmt::{Debug, Display, Formatter, Write}, + fs, + fs::File, + io::{Cursor, Read, Seek, SeekFrom}, + path::PathBuf, +}; + +use anyhow::{anyhow, bail, ensure, Result}; +use argh::FromArgs; +use binrw::{BinRead, BinReaderExt, BinResult, Endian, NullString}; +use binrw_derive::{binrw, BinWrite}; +use uuid::Uuid; + +use crate::util::file::map_file; + +#[derive(FromArgs, PartialEq, Debug)] +/// process PAK files +#[argh(subcommand, name = "pak")] +pub struct Args { + #[argh(subcommand)] + command: SubCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum SubCommand { + Extract(ExtractArgs), +} + +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// extract a PAK file +#[argh(subcommand, name = "extract")] +pub struct ExtractArgs { + #[argh(positional)] + /// input file + input: PathBuf, + #[argh(positional)] + /// output directory + output: PathBuf, +} + +#[binrw] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct FourCC(pub [u8; 4]); + +impl FourCC { + #[inline] + const fn swap(self) -> Self { Self([self.0[3], self.0[2], self.0[1], self.0[0]]) } +} + +impl Display for FourCC { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for c in self.0 { + f.write_char(c as char)?; + } + Ok(()) + } +} + +impl Debug for FourCC { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_char('"')?; + for c in self.0 { + f.write_char(c as char)?; + } + f.write_char('"')?; + Ok(()) + } +} + +impl PartialEq<[u8; 4]> for FourCC { + fn eq(&self, other: &[u8; 4]) -> bool { &self.0 == other } +} + +// PAK file +const K_FORM_PAK: FourCC = FourCC(*b"PACK"); +// Table of contents +const K_FORM_TOC: FourCC = FourCC(*b"TOCC"); + +#[binrw] +#[brw(magic = b"RFRM")] +#[derive(Clone, Debug)] +pub struct FormDescriptor { + pub size: u64, + pub unk: u64, + pub id: FourCC, + pub version: u32, + pub other_version: u32, // ? +} + +impl FormDescriptor { + #[inline] + fn read(reader: &mut R, e: Endian) -> BinResult { reader.read_type(e) } + + #[inline] + fn slice(data: &[u8], e: Endian) -> BinResult<(Self, &[u8], &[u8])> { + let mut reader = Cursor::new(data); + let header = Self::read(&mut reader, e)?; + let start = reader.position(); + let slice = &data[start as usize..(start + header.size) as usize]; + let remain = &data[(start + header.size) as usize..]; + Ok((header, slice, remain)) + } +} + +// Asset directory +const K_CHUNK_ADIR: FourCC = FourCC(*b"ADIR"); +// Metadata +const K_CHUNK_META: FourCC = FourCC(*b"META"); +// String table +const K_CHUNK_STRG: FourCC = FourCC(*b"STRG"); + +#[binrw] +#[derive(Clone, Debug)] +pub struct ChunkDescriptor { + pub id: FourCC, + pub size: u64, + pub unk: u32, + // game skips this amount of bytes before continuing + // but always 0? + pub skip: u64, +} + +impl ChunkDescriptor { + #[inline] + fn read(reader: &mut R, e: Endian) -> BinResult { + let desc: ChunkDescriptor = reader.read_type(e)?; + reader.seek(SeekFrom::Current(desc.skip as i64))?; + Ok(desc) + } + + #[inline] + fn slice(data: &[u8], e: Endian) -> BinResult<(Self, &[u8], &[u8])> { + let mut reader = Cursor::new(data); + let header = Self::read(&mut reader, e)?; + let start = reader.position(); + let slice = &data[start as usize..(start + header.size) as usize]; + let remain = &data[(start + header.size) as usize..]; + Ok((header, slice, remain)) + } +} + +#[derive(Clone, Debug)] +enum ChunkType { + AssetDirectory(AssetDirectory), + Metadata(Metadata), + StringTable(StringTable), +} + +impl ChunkType { + #[inline] + fn read(data: &[u8], kind: FourCC, e: Endian) -> Result { + let mut reader = Cursor::new(data); + match kind { + K_CHUNK_ADIR => Ok(Self::AssetDirectory(reader.read_type(e)?)), + K_CHUNK_META => Ok(Self::Metadata(reader.read_type(e)?)), + K_CHUNK_STRG => Ok(Self::StringTable(reader.read_type(e)?)), + _ => Err(anyhow!("Unknown chunk type {:?}", kind)), + } + } +} + +#[binrw] +#[derive(Clone, Debug)] +struct AssetDirectory { + #[bw(try_calc = entries.len().try_into())] + entry_count: u32, + #[br(count = entry_count)] + entries: Vec, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct AssetDirectoryEntry { + pub asset_type: FourCC, + #[br(map = Uuid::from_u128)] + #[bw(map = Uuid::as_u128)] + pub asset_id: Uuid, + pub unk1: u32, + pub unk2: u32, + pub offset: u64, + pub size: u64, + pub unk3: u64, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct Metadata { + #[bw(try_calc = entries.len().try_into())] + entry_count: u32, + #[br(count = entry_count)] + entries: Vec, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct MetadataEntry { + #[br(map = Uuid::from_u128)] + #[bw(map = Uuid::as_u128)] + pub asset_id: Uuid, + pub offset: u32, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct StringTable { + #[bw(try_calc = entries.len().try_into())] + entry_count: u32, + #[br(count = entry_count)] + entries: Vec, +} + +impl StringTable { + fn name_for_uuid(&self, id: Uuid) -> Option { + self.entries + .iter() + .find(|e| e.asset_id == id) + .map(|e| String::from_utf8(e.name.clone()).unwrap()) + } +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct StringTableEntry { + #[br(map = FourCC::swap)] + #[bw(map = |&f| f.swap())] + pub kind: FourCC, + #[br(map = Uuid::from_u128)] + #[bw(map = Uuid::as_u128)] + pub asset_id: Uuid, + #[bw(try_calc = name.len().try_into())] + pub name_length: u32, + #[br(count = name_length)] + pub name: Vec, +} + +pub fn run(args: Args) -> Result<()> { + match args.command { + SubCommand::Extract(c_args) => extract(c_args), + } +} + +/// Recursively dump an RFRM + contained chunks +fn dump_rfrm<'a, W: std::io::Write>(w: &mut W, data: &'a [u8], indent: usize) -> Result<&'a [u8]> { + let (rfrm, mut rfrm_data, remain) = FormDescriptor::slice(data, Endian::Little)?; + let indstr = " ".repeat(indent); + writeln!(w, "{indstr}{rfrm:?}")?; + while !rfrm_data.is_empty() { + if rfrm_data[0..4] == *b"RFRM" { + rfrm_data = dump_rfrm(w, rfrm_data, indent + 1)?; + } else { + let (desc, chunk_data, remain) = ChunkDescriptor::slice(rfrm_data, Endian::Little)?; + writeln!(w, "{indstr}- {desc:?}")?; + rfrm_data = remain; + } + } + Ok(remain) +} + +fn extract(args: ExtractArgs) -> Result<()> { + let data = map_file(args.input)?; + let (pack, pack_data, _) = FormDescriptor::slice(&data, Endian::Little)?; + ensure!(pack.id == K_FORM_PAK); + ensure!(pack.version == 1); + log::info!("PACK: {:?}", pack); + let (tocc, mut tocc_data, pack_remain) = FormDescriptor::slice(pack_data, Endian::Little)?; + ensure!(tocc.id == K_FORM_TOC); + ensure!(tocc.version == 3); + log::info!("TOCC: {:?}", tocc); + let mut adir: Option = None; + let mut meta: Option = None; + let mut strg: Option = None; + while !tocc_data.is_empty() { + let (desc, chunk_data, remain) = ChunkDescriptor::slice(tocc_data, Endian::Little)?; + log::info!("{:?} data size {}", desc, chunk_data.len()); + let header = ChunkType::read(chunk_data, desc.id, Endian::Little)?; + match header { + ChunkType::AssetDirectory(chunk) => { + for entry in &chunk.entries { + log::info!("- {:?}", entry); + } + adir = Some(chunk); + } + ChunkType::Metadata(chunk) => { + for entry in &chunk.entries { + log::info!("- {:?}", entry); + } + meta = Some(chunk); + } + ChunkType::StringTable(chunk) => { + for entry in &chunk.entries { + log::info!("- {:?}", entry); + } + strg = Some(chunk); + } + } + tocc_data = remain; + } + log::info!("Remaining PACK data: {:#X}", pack_remain.len()); + if let Some(adir) = adir { + for asset_entry in adir.entries { + let name = strg + .as_ref() + .and_then(|table| table.name_for_uuid(asset_entry.asset_id)) + .unwrap_or_else(|| format!("{}", asset_entry.asset_id)); + let filename = format!("{}.{}", name, asset_entry.asset_type); + let path = args.output.join(filename); + // log::info!("Extracting {}", path.display()); + let mut data = &data + [asset_entry.offset as usize..(asset_entry.offset + asset_entry.size) as usize]; + if asset_entry.asset_type == *b"FMV0" { + let (hdr, fmv_data, _) = FormDescriptor::slice(data, Endian::Little)?; + // log::info!("{:?}", hdr); + data = fmv_data; + } else if asset_entry.asset_type == *b"ROOM" { + let mut file = File::create("ROOM-format.txt")?; + dump_rfrm(&mut file, data, 0)?; + } + fs::write(path, data)?; + } + } else { + bail!("Failed to locate ADIR chunk"); + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..82d7846 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,35 @@ +mod argh_version; +mod cmd; +mod util; + +use argh::FromArgs; + +#[derive(FromArgs, PartialEq, Debug)] +/// GameCube/Wii decompilation project tools. +struct TopLevel { + #[argh(subcommand)] + command: SubCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum SubCommand { + Pak(cmd::pak::Args), +} + +fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .format_timestamp(None) + .format_target(false) + .format_level(false) + .init(); + + let args: TopLevel = argh_version::from_env(); + let result = match args.command { + SubCommand::Pak(args) => cmd::pak::run(args), + }; + if let Err(e) = result { + eprintln!("Failed: {e:?}"); + std::process::exit(1); + } +} diff --git a/src/util/file.rs b/src/util/file.rs new file mode 100644 index 0000000..642763b --- /dev/null +++ b/src/util/file.rs @@ -0,0 +1,57 @@ +use std::{ + fs::File, + io::{BufReader, Cursor, Read}, + path::Path, +}; + +use anyhow::{Context, Result}; +use byteorder::ReadBytesExt; +use memmap2::{Mmap, MmapOptions}; + +/// Opens a memory mapped file. +pub fn map_file>(path: P) -> Result { + let file = File::open(&path) + .with_context(|| format!("Failed to open file '{}'", path.as_ref().display()))?; + let map = unsafe { MmapOptions::new().map(&file) } + .with_context(|| format!("Failed to mmap file: '{}'", path.as_ref().display()))?; + Ok(map) +} + +pub type Reader<'a> = Cursor<&'a [u8]>; + +/// Creates a reader for the memory mapped file. +#[inline] +pub fn map_reader(mmap: &Mmap) -> Reader { Cursor::new(&*mmap) } + +/// Creates a buffered reader around a file (not memory mapped). +pub fn buf_reader>(path: P) -> Result> { + let file = File::open(&path) + .with_context(|| format!("Failed to open file '{}'", path.as_ref().display()))?; + Ok(BufReader::new(file)) +} + +/// Reads a string with known size at the specified offset. +pub fn read_string(reader: &mut Reader, off: u64, size: usize) -> Result { + let mut data = vec![0u8; size]; + let pos = reader.position(); + reader.set_position(off); + reader.read_exact(&mut data)?; + reader.set_position(pos); + Ok(String::from_utf8(data)?) +} + +/// Reads a zero-terminated string at the specified offset. +pub fn read_c_string(reader: &mut Reader, off: u64) -> Result { + let pos = reader.position(); + reader.set_position(off); + let mut s = String::new(); + loop { + let b = reader.read_u8()?; + if b == 0 { + break; + } + s.push(b as char); + } + reader.set_position(pos); + Ok(s) +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..2e172cd --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod file;