diff --git a/README.md b/README.md index 902bdbc..7b43110 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ FLAGS: SUBCOMMANDS: bhd Extracts BHD/BDT contents bhds Extracts all BHD/BDT content (alphabetically) in a folder + bhf Extracts BHF/BDT contents bnd Extracts BND contents dcx Extracts and decompress DCX data hash Calculates hash for a string @@ -52,6 +53,7 @@ Features - BHD5 / BDT: extraction from disk to disk. - DCX: decompression from disk to disk. - BND (v3): extraction from disk/memory to disk/memory. +- BHF (v3): extraction from disk/memory to disk/memory. Repacking is not supported, maybe one day. It is not that useful when using [UDSFM][udsfm] and [Yabber][yabber], but if you really need it you can check out diff --git a/src/bin/ironring.rs b/src/bin/ironring.rs index 3993691..864aa16 100644 --- a/src/bin/ironring.rs +++ b/src/bin/ironring.rs @@ -73,6 +73,21 @@ fn main() { .long("force") .takes_value(false) .required(false))) + .subcommand(SubCommand::with_name("bhf") + .about("Extracts BHF/BDT contents") + .arg(Arg::with_name("file") + .takes_value(true) + .required(true)) + .arg(Arg::with_name("output") + .short("o") + .long("output") + .takes_value(true) + .required(false)) + .arg(Arg::with_name("overwrite") + .short("f") + .long("force") + .takes_value(false) + .required(false))) .get_matches(); process::exit(match matches.subcommand() { @@ -81,6 +96,7 @@ fn main() { ("hash", Some(s)) => { cmd_hash(s) } ("dcx", Some(s)) => { cmd_dcx(s) } ("bnd", Some(s)) => { cmd_bnd(s) } + ("bhf", Some(s)) => { cmd_bhf(s) } _ => { 0 } }) } @@ -215,6 +231,10 @@ fn cmd_bnd(args: &ArgMatches) -> i32 { fn cmd_bhf(args: &ArgMatches) -> i32 { let file_path: &str = args.value_of("file").unwrap(); - let output_path: &str = args.value_of("output").unwrap(); - 0 + let output_path: Option<&str> = args.value_of("output"); + let overwrite: bool = args.is_present("overwrite"); + match unpackers::bhf::extract_bhf_file(file_path, output_path, overwrite) { + Err(e) => { eprintln!("Failed to extract BHF: {:?}", e); return 1 } + _ => { 0 } + } } diff --git a/src/unpackers/bhd.rs b/src/unpackers/bhd.rs index 5be3be3..0987e2f 100644 --- a/src/unpackers/bhd.rs +++ b/src/unpackers/bhd.rs @@ -20,6 +20,7 @@ pub fn extract_bhd( names: &HashMap, output_path: &str ) -> Result<(), UnpackError> { + let bhd_path = path::Path::new(bhd_path); let bhd_data = utils_fs::open_file_to_vec(bhd_path)?; let bhd = match bhd::parse(&bhd_data) { Ok((_, bhd)) => bhd, @@ -27,7 +28,7 @@ pub fn extract_bhd( e => return Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))) }; - let bdt_path = path::PathBuf::from(bhd_path).with_extension("bdt"); + let bdt_path = bhd_path.to_path_buf().with_extension("bdt"); let mut bdt_file = fs::File::open(bdt_path.to_str().unwrap())?; extract_files(&bhd, &mut bdt_file, &names, &output_path)?; diff --git a/src/unpackers/bhf.rs b/src/unpackers/bhf.rs index b82dd1d..b33b4ee 100644 --- a/src/unpackers/bhf.rs +++ b/src/unpackers/bhf.rs @@ -1,5 +1,5 @@ use std::fs; -use std::io::Read; +use std::io::Write; use std::path; use nom::Err::{Error as NomError, Failure as NomFailure}; @@ -8,24 +8,49 @@ use crate::parsers::bhf; use crate::unpackers::errors::UnpackError; use crate::utils::fs as utils_fs; -pub fn extract_bhf(bhf_path: &str) -> Result<(), UnpackError> { - let bhf_data = utils_fs::open_file_to_vec(bhf_path)?; - let bhf = match bhf::parse(&bhf_data) { - Ok((_, bhf)) => { bhf } - Err(NomError(e)) | Err(NomFailure(e)) => return Err(UnpackError::parsing_err("BHF", e.1)), - e => return Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))), - }; +/// Extract BHF file and corresponding BDT contents to disk. +/// +/// Wraps around `extract_bhf` to load the BHF file from disk. +/// If output_dir is none, entries will be extracted relatively to the +/// BHF path. +pub fn extract_bhf_file( + bhf_path: &str, + output_dir: Option<&str>, + overwrite: bool +) -> Result<(), UnpackError> { + let bhf = load_bhf_file(bhf_path)?; - let bdt_path = get_bdt_for_bhf(bhf_path); - if bdt_path.is_none() { + let bdt_path: path::PathBuf = if let Some(path) = get_bdt_for_bhf(bhf_path) { + if !path.exists() { + return Err(UnpackError::Naming(format!("Can't find BDT: {:?}", path))) + } + path + } else { return Err(UnpackError::Naming(format!("Can't find BDT for BHF: {}", bhf_path))) - } - + }; + let bdt_data = utils_fs::open_file_to_vec(&bdt_path)?; + let output_dir: &str = if output_dir.is_none() { + let parent = path::Path::new(bhf_path).parent(); + if parent.is_none() { + return Err(UnpackError::Naming(format!("Can't find BHF parent dir: {:?}", bhf_path))) + } + parent.unwrap().to_str().unwrap() // Conversion should not fail as bhf_path is valid. + } else { + output_dir.unwrap() + }; + extract_bhf(&bhf, &bdt_data, output_dir, overwrite)?; Ok(()) } -fn get_bdt_for_bhf(bhf_path: &str) -> Option { +/// Return corresponding BDT path for a BHF path. +/// +/// Replaces the "bhd" suffix in extension wiuth "bdt". If the +/// extension does not ends with "bhd", a warning is printed but does +/// the extension is still appended with "bdt". If the path does not +/// have an extension or is overall invalid, it returns None. +/// It does not check if the file exists either. +pub fn get_bdt_for_bhf(bhf_path: &str) -> Option { let mut path = path::PathBuf::from(bhf_path); let ext = path.extension()?.to_str()?; if !ext.ends_with("bhd") { @@ -36,6 +61,77 @@ fn get_bdt_for_bhf(bhf_path: &str) -> Option { Some(path) } +/// Extract BHF+BDT contents to disk. +/// +/// Files are written in output_dir, creating it if needed, without +/// preserving directory structure. +pub fn extract_bhf( + bhf: &bhf::Bhf, + bdt_data: &Vec, + output_dir: &str, + overwrite: bool, +) -> Result<(), UnpackError> { + let output_dir = path::Path::new(output_dir); + utils_fs::ensure_dir_exists(output_dir)?; + for file_info in &bhf.file_infos { + // Extract all entries, print but ignore path errors. + match extract_bhf_entry(file_info, bdt_data, output_dir, overwrite) { + Err(UnpackError::Naming(e)) => { eprintln!("{}", e) } + _ => {} + } + } + Ok(()) +} + + +/// Extract a file contained in a BHF+BDT using its BhfFileInfo. +/// +/// The info struct must have a valid internal path. +pub fn extract_bhf_entry( + file_info: &bhf::BhfFileInfo, + bdt_data: &Vec, + output_dir: &path::Path, + overwrite: bool, +) -> Result<(), UnpackError> { + if file_info.path.is_none() { + return Err(UnpackError::Naming("No path for BHF entry.".to_owned())) + } + + let ofs_start = file_info.ofs_data as usize; + let ofs_end = ofs_start + file_info.size as usize; + let data = &bdt_data[ofs_start..ofs_end]; + + let internal_path = file_info.path.to_owned().unwrap(); + let file_name = internal_path.trim_start_matches('\\'); + let mut file_path = output_dir.to_path_buf(); + file_path.push(file_name); + if !overwrite && file_path.exists() { + let existing = file_path.to_string_lossy(); + return Err(UnpackError::Naming(format!("File already exists: {}", existing))) + } + + let mut output_file = fs::File::create(file_path)?; + output_file.write_all(&data)?; + Ok(()) +} + +/// Load a BHF file from disk. +/// +/// Wraps around `load_bhf` to load the BHF from disk. +pub fn load_bhf_file(bhf_path: &str) -> Result { + let bhf_data = utils_fs::open_file_to_vec(path::Path::new(bhf_path))?; + load_bhf(&bhf_data) +} + +/// Load a BHF file from a bytes slice. +pub fn load_bhf(bhf_data: &[u8]) -> Result { + match bhf::parse(&bhf_data) { + Ok((_, bhf)) => Ok(bhf), + Err(NomError(e)) | Err(NomFailure(e)) => Err(UnpackError::parsing_err("BHF", e.1)), + e => Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/unpackers/bnd.rs b/src/unpackers/bnd.rs index 085b17c..82e8948 100644 --- a/src/unpackers/bnd.rs +++ b/src/unpackers/bnd.rs @@ -46,18 +46,18 @@ pub fn extract_bnd( /// Extract a file contained in a BND using its BndFileInfo. /// /// The info struct must have a valid internal path. -fn extract_bnd_entry( +pub fn extract_bnd_entry( file_info: &bnd::BndFileInfo, bnd_data: &Vec, output_dir: &path::Path, overwrite: bool, ) -> Result<(), UnpackError> { if file_info.path.is_none() { - return Err(UnpackError::Naming("No path for BND entry.".to_owned())); + return Err(UnpackError::Naming("No path for BND entry.".to_owned())) } let ofs_start = file_info.ofs_data as usize; - let ofs_end = (file_info.ofs_data + file_info.size) as usize; + let ofs_end = ofs_start + file_info.size as usize; let data = &bnd_data[ofs_start..ofs_end]; let internal_path = file_info.path.to_owned().unwrap(); @@ -69,10 +69,11 @@ fn extract_bnd_entry( }; let mut file_path = output_dir.to_path_buf(); file_path.push(file_name); - if !overwrite && file_path.exists() { - return Err(UnpackError::Naming(format!("File already exists: {:?}", file_path))); + let existing = file_path.to_string_lossy(); + return Err(UnpackError::Naming(format!("File already exists: {}", existing))) } + let mut output_file = fs::File::create(file_path)?; output_file.write_all(&data)?; Ok(()) @@ -83,16 +84,15 @@ fn extract_bnd_entry( /// Wraps around `load_bnd` to load the BND from disk. It returns the /// parsed BND metadata and the whole file as a byte vector. pub fn load_bnd_file(bnd_path: &str) -> Result<(bnd::Bnd, Vec), UnpackError> { - let bnd_data = utils_fs::open_file_to_vec(bnd_path)?; + let bnd_data = utils_fs::open_file_to_vec(path::Path::new(bnd_path))?; Ok((load_bnd(&bnd_data)?, bnd_data)) } /// Load a BND file from a bytes slice. pub fn load_bnd(bnd_data: &[u8]) -> Result { - let (_, bnd) = match bnd::parse(bnd_data) { - Ok(result) => result, - Err(NomError(e)) | Err(NomFailure(e)) => return Err(UnpackError::parsing_err("BND", e.1)), - e => return Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))), - }; - Ok(bnd) + match bnd::parse(bnd_data) { + Ok((_, result)) => Ok(result), + Err(NomError(e)) | Err(NomFailure(e)) => Err(UnpackError::parsing_err("BND", e.1)), + e => Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))), + } } diff --git a/src/unpackers/dcx.rs b/src/unpackers/dcx.rs index 3a84d39..bc0e8f1 100644 --- a/src/unpackers/dcx.rs +++ b/src/unpackers/dcx.rs @@ -1,5 +1,6 @@ use std::fs; use std::io::{Read, Write}; +use std::path; use flate2::read::ZlibDecoder; use nom::Err::{Error as NomError, Failure as NomFailure}; @@ -18,6 +19,7 @@ pub fn extract_dcx(dcx_path: &str, output_path: &str) -> Result<(), UnpackError> /// Load a DCX file in memory along with its decompressed content. pub fn load_dcx(dcx_path: &str) -> Result<(dcx::Dcx, Vec), UnpackError> { + let dcx_path = path::Path::new(dcx_path); let dcx_data = utils_fs::open_file_to_vec(dcx_path)?; let (data, dcx) = match dcx::parse(&dcx_data) { Ok(result) => result, diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 5102756..e3930d8 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -25,7 +25,7 @@ pub fn strip_extension(path: &path::PathBuf) -> Option { } /// Open a binary file and read it to the end in a byte vector. -pub fn open_file_to_vec(path: &str) -> Result, io::Error> { +pub fn open_file_to_vec(path: &path::Path) -> Result, io::Error> { let mut file = fs::File::open(path)?; let file_len = file.metadata()?.len() as usize; let mut data = vec![0u8; file_len];