diff --git a/bin/src/commands/utils.rs b/bin/src/commands/utils.rs index fe152331..f3f37200 100644 --- a/bin/src/commands/utils.rs +++ b/bin/src/commands/utils.rs @@ -9,6 +9,7 @@ pub fn cli() -> Command { .subcommand_required(false) .arg_required_else_help(true) .subcommand(utils::inspect::cli()) + .subcommand(utils::pbo::cli()) .subcommand(utils::verify::cli()) } @@ -19,6 +20,7 @@ pub fn cli() -> Command { pub fn execute(matches: &ArgMatches) -> Result<(), Error> { match matches.subcommand() { Some(("inspect", matches)) => utils::inspect::execute(matches), + Some(("pbo", matches)) => utils::pbo::execute(matches), Some(("verify", matches)) => utils::verify::execute(matches), _ => unreachable!(), } diff --git a/bin/src/utils/inspect.rs b/bin/src/utils/inspect.rs index 34d9f67b..afe2ca90 100644 --- a/bin/src/utils/inspect.rs +++ b/bin/src/utils/inspect.rs @@ -115,16 +115,23 @@ pub fn bisign(mut file: File, path: &PathBuf) -> Result { } /// Prints information about a [`ReadablePbo`] to stdout -fn pbo(file: File) -> Result<(), Error> { +/// +/// # Errors +/// [`hemtt_pbo::Error`] if the file is not a valid [`ReadablePbo`] +/// +/// # Panics +/// If the file is not a valid [`ReadablePbo`] +pub fn pbo(file: File) -> Result<(), Error> { let mut pbo = ReadablePbo::from(file)?; println!("Properties"); for (key, value) in pbo.properties() { println!(" - {key}: {value}"); } + println!("Checksum (SHA1)"); let stored = *pbo.checksum(); - println!(" - Stored Hash: {stored:?}"); + println!(" - Stored: {}", stored.hex()); let actual = pbo.gen_checksum().unwrap(); - println!(" - Actual Hash: {actual:?}"); + println!(" - Actual: {}", actual.hex()); let files = pbo.files(); println!("Files"); diff --git a/bin/src/utils/mod.rs b/bin/src/utils/mod.rs index 2d634c68..93366a81 100644 --- a/bin/src/utils/mod.rs +++ b/bin/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod inspect; +pub mod pbo; pub mod verify; diff --git a/bin/src/utils/pbo/extract.rs b/bin/src/utils/pbo/extract.rs new file mode 100644 index 00000000..0749ec8e --- /dev/null +++ b/bin/src/utils/pbo/extract.rs @@ -0,0 +1,44 @@ +use std::{fs::File, path::PathBuf}; + +use clap::{ArgMatches, Command}; +use hemtt_pbo::ReadablePbo; + +use crate::Error; + +#[must_use] +pub fn cli() -> Command { + Command::new("extract") + .about("Extract a file from a PBO") + .arg( + clap::Arg::new("pbo") + .help("PBO file to extract from") + .required(true), + ) + .arg( + clap::Arg::new("file") + .help("File to extract") + .required(true), + ) + .arg(clap::Arg::new("output").help("Where to save the extracted file")) +} + +/// Execute the extract command +/// +/// # Errors +/// [`Error`] depending on the modules +pub fn execute(matches: &ArgMatches) -> Result<(), Error> { + let path = PathBuf::from(matches.get_one::("pbo").expect("required")); + let mut pbo = ReadablePbo::from(File::open(path)?)?; + let file = matches.get_one::("file").expect("required"); + let Some(mut file) = pbo.file(file)? else { + error!("File `{file}` not found in PBO"); + return Ok(()); + }; + let output = matches.get_one::("output").map(PathBuf::from); + if let Some(output) = output { + std::io::copy(&mut file, &mut File::create(output)?)?; + } else { + std::io::copy(&mut file, &mut std::io::stdout())?; + } + Ok(()) +} diff --git a/bin/src/utils/pbo/mod.rs b/bin/src/utils/pbo/mod.rs new file mode 100644 index 00000000..b8ce31d8 --- /dev/null +++ b/bin/src/utils/pbo/mod.rs @@ -0,0 +1,44 @@ +use std::{fs::File, path::PathBuf}; + +use clap::{ArgMatches, Command}; + +use crate::Error; + +use super::inspect::pbo; + +mod extract; +mod unpack; + +#[must_use] +pub fn cli() -> Command { + Command::new("pbo") + .about("Commands for PBO files") + .arg_required_else_help(true) + .subcommand(extract::cli()) + .subcommand(unpack::cli()) + .subcommand( + Command::new("inspect") + .about("Inspect a PBO") + .arg(clap::Arg::new("pbo").help("PBO to inspect").required(true)), + ) +} + +/// Execute the pbo command +/// +/// # Errors +/// [`Error`] depending on the modules +/// +/// # Panics +/// If the args are not present from clap +pub fn execute(matches: &ArgMatches) -> Result<(), Error> { + match matches.subcommand() { + Some(("extract", matches)) => extract::execute(matches), + Some(("unpack", matches)) => unpack::execute(matches), + + Some(("inspect", matches)) => pbo(File::open(PathBuf::from( + matches.get_one::("pbo").expect("required"), + ))?), + + _ => unreachable!(), + } +} diff --git a/bin/src/utils/pbo/unpack.rs b/bin/src/utils/pbo/unpack.rs new file mode 100644 index 00000000..d3de0f70 --- /dev/null +++ b/bin/src/utils/pbo/unpack.rs @@ -0,0 +1,65 @@ +use std::{ + fs::{File, OpenOptions}, + io::Write, + path::PathBuf, +}; + +use clap::{ArgMatches, Command}; +use hemtt_pbo::ReadablePbo; + +use crate::Error; + +#[must_use] +pub fn cli() -> Command { + Command::new("unpack") + .about("Unpack a PBO") + .arg( + clap::Arg::new("pbo") + .help("PBO file to unpack") + .required(true), + ) + .arg( + clap::Arg::new("output") + .help("Directory to unpack to") + .required(true), + ) +} + +/// Execute the unpack command +/// +/// # Errors +/// [`Error`] depending on the modules +pub fn execute(matches: &ArgMatches) -> Result<(), Error> { + let path = PathBuf::from(matches.get_one::("pbo").expect("required")); + let mut pbo = ReadablePbo::from(File::open(path)?)?; + let output = PathBuf::from(matches.get_one::("output").expect("required")); + if output.exists() { + error!("Output directory already exists"); + return Ok(()); + } + std::fs::create_dir_all(&output)?; + for (key, value) in pbo.properties() { + debug!("{}: {}", key, value); + if key == "prefix" { + let mut file = File::create(output.join("$PBOPREFIX$"))?; + file.write_all(value.as_bytes())?; + } else { + let mut file = OpenOptions::new() + .write(true) + .create(true) + .append(true) + .open(output.join("properties.txt"))?; + file.write_all(format!("{key}={value}\n").as_bytes())?; + } + } + for header in pbo.files() { + let path = output.join(header.filename().replace('\\', "/")); + std::fs::create_dir_all(path.parent().unwrap())?; + let mut out = File::create(path)?; + let mut file = pbo + .file(header.filename())? + .expect("file must exist if header exists"); + std::io::copy(&mut file, &mut out)?; + } + Ok(()) +} diff --git a/bin/src/utils/verify.rs b/bin/src/utils/verify.rs index e79c3961..66b6141a 100644 --- a/bin/src/utils/verify.rs +++ b/bin/src/utils/verify.rs @@ -49,9 +49,9 @@ pub fn execute(matches: &ArgMatches) -> Result<(), Error> { println!(); println!("PBO: {pbo_path:?}"); let stored = *pbo.checksum(); - println!(" - Stored Hash: {stored:?}"); + println!(" - Stored SHA1 Hash: {}", stored.hex()); let actual = pbo.gen_checksum().unwrap(); - println!(" - Actual Hash: {actual:?}"); + println!(" - Actual SHA1 Hash: {}", actual.hex()); println!(" - Properties"); for ext in pbo.properties() { println!(" - {}: {}", ext.0, ext.1); diff --git a/libs/pbo/src/model/checksum.rs b/libs/pbo/src/model/checksum.rs index 96670a6a..07f1e54b 100644 --- a/libs/pbo/src/model/checksum.rs +++ b/libs/pbo/src/model/checksum.rs @@ -24,6 +24,15 @@ impl Checksum { pub const fn as_bytes(&self) -> &[u8; 20] { &self.0 } + + #[must_use] + pub fn hex(&self) -> String { + let mut out = String::new(); + for byte in &self.0 { + out.push_str(&format!("{byte:02x}")); + } + out + } } impl From> for Checksum {