bhf: complete extraction
This commit is contained in:
parent
9febe111a4
commit
987f188903
|
@ -38,6 +38,7 @@ FLAGS:
|
||||||
SUBCOMMANDS:
|
SUBCOMMANDS:
|
||||||
bhd Extracts BHD/BDT contents
|
bhd Extracts BHD/BDT contents
|
||||||
bhds Extracts all BHD/BDT content (alphabetically) in a folder
|
bhds Extracts all BHD/BDT content (alphabetically) in a folder
|
||||||
|
bhf Extracts BHF/BDT contents
|
||||||
bnd Extracts BND contents
|
bnd Extracts BND contents
|
||||||
dcx Extracts and decompress DCX data
|
dcx Extracts and decompress DCX data
|
||||||
hash Calculates hash for a string
|
hash Calculates hash for a string
|
||||||
|
@ -52,6 +53,7 @@ Features
|
||||||
- BHD5 / BDT: extraction from disk to disk.
|
- BHD5 / BDT: extraction from disk to disk.
|
||||||
- DCX: decompression from disk to disk.
|
- DCX: decompression from disk to disk.
|
||||||
- BND (v3): extraction from disk/memory to disk/memory.
|
- 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
|
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
|
[UDSFM][udsfm] and [Yabber][yabber], but if you really need it you can check out
|
||||||
|
|
|
@ -73,6 +73,21 @@ fn main() {
|
||||||
.long("force")
|
.long("force")
|
||||||
.takes_value(false)
|
.takes_value(false)
|
||||||
.required(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();
|
.get_matches();
|
||||||
|
|
||||||
process::exit(match matches.subcommand() {
|
process::exit(match matches.subcommand() {
|
||||||
|
@ -81,6 +96,7 @@ fn main() {
|
||||||
("hash", Some(s)) => { cmd_hash(s) }
|
("hash", Some(s)) => { cmd_hash(s) }
|
||||||
("dcx", Some(s)) => { cmd_dcx(s) }
|
("dcx", Some(s)) => { cmd_dcx(s) }
|
||||||
("bnd", Some(s)) => { cmd_bnd(s) }
|
("bnd", Some(s)) => { cmd_bnd(s) }
|
||||||
|
("bhf", Some(s)) => { cmd_bhf(s) }
|
||||||
_ => { 0 }
|
_ => { 0 }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -215,6 +231,10 @@ fn cmd_bnd(args: &ArgMatches) -> i32 {
|
||||||
|
|
||||||
fn cmd_bhf(args: &ArgMatches) -> i32 {
|
fn cmd_bhf(args: &ArgMatches) -> i32 {
|
||||||
let file_path: &str = args.value_of("file").unwrap();
|
let file_path: &str = args.value_of("file").unwrap();
|
||||||
let output_path: &str = args.value_of("output").unwrap();
|
let output_path: Option<&str> = args.value_of("output");
|
||||||
0
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub fn extract_bhd(
|
||||||
names: &HashMap<String, String>,
|
names: &HashMap<String, String>,
|
||||||
output_path: &str
|
output_path: &str
|
||||||
) -> Result<(), UnpackError> {
|
) -> Result<(), UnpackError> {
|
||||||
|
let bhd_path = path::Path::new(bhd_path);
|
||||||
let bhd_data = utils_fs::open_file_to_vec(bhd_path)?;
|
let bhd_data = utils_fs::open_file_to_vec(bhd_path)?;
|
||||||
let bhd = match bhd::parse(&bhd_data) {
|
let bhd = match bhd::parse(&bhd_data) {
|
||||||
Ok((_, bhd)) => bhd,
|
Ok((_, bhd)) => bhd,
|
||||||
|
@ -27,7 +28,7 @@ pub fn extract_bhd(
|
||||||
e => return Err(UnpackError::Unknown(format!("Unknown error: {:?}", e)))
|
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())?;
|
let mut bdt_file = fs::File::open(bdt_path.to_str().unwrap())?;
|
||||||
|
|
||||||
extract_files(&bhd, &mut bdt_file, &names, &output_path)?;
|
extract_files(&bhd, &mut bdt_file, &names, &output_path)?;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Write;
|
||||||
use std::path;
|
use std::path;
|
||||||
|
|
||||||
use nom::Err::{Error as NomError, Failure as NomFailure};
|
use nom::Err::{Error as NomError, Failure as NomFailure};
|
||||||
|
@ -8,24 +8,49 @@ use crate::parsers::bhf;
|
||||||
use crate::unpackers::errors::UnpackError;
|
use crate::unpackers::errors::UnpackError;
|
||||||
use crate::utils::fs as utils_fs;
|
use crate::utils::fs as utils_fs;
|
||||||
|
|
||||||
pub fn extract_bhf(bhf_path: &str) -> Result<(), UnpackError> {
|
/// Extract BHF file and corresponding BDT contents to disk.
|
||||||
let bhf_data = utils_fs::open_file_to_vec(bhf_path)?;
|
///
|
||||||
let bhf = match bhf::parse(&bhf_data) {
|
/// Wraps around `extract_bhf` to load the BHF file from disk.
|
||||||
Ok((_, bhf)) => { bhf }
|
/// If output_dir is none, entries will be extracted relatively to the
|
||||||
Err(NomError(e)) | Err(NomFailure(e)) => return Err(UnpackError::parsing_err("BHF", e.1)),
|
/// BHF path.
|
||||||
e => return Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))),
|
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);
|
let bdt_path: path::PathBuf = if let Some(path) = get_bdt_for_bhf(bhf_path) {
|
||||||
if bdt_path.is_none() {
|
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)))
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bdt_for_bhf(bhf_path: &str) -> Option<path::PathBuf> {
|
/// 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<path::PathBuf> {
|
||||||
let mut path = path::PathBuf::from(bhf_path);
|
let mut path = path::PathBuf::from(bhf_path);
|
||||||
let ext = path.extension()?.to_str()?;
|
let ext = path.extension()?.to_str()?;
|
||||||
if !ext.ends_with("bhd") {
|
if !ext.ends_with("bhd") {
|
||||||
|
@ -36,6 +61,77 @@ fn get_bdt_for_bhf(bhf_path: &str) -> Option<path::PathBuf> {
|
||||||
Some(path)
|
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<u8>,
|
||||||
|
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<u8>,
|
||||||
|
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<bhf::Bhf, UnpackError> {
|
||||||
|
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<bhf::Bhf, UnpackError> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -46,18 +46,18 @@ pub fn extract_bnd(
|
||||||
/// Extract a file contained in a BND using its BndFileInfo.
|
/// Extract a file contained in a BND using its BndFileInfo.
|
||||||
///
|
///
|
||||||
/// The info struct must have a valid internal path.
|
/// The info struct must have a valid internal path.
|
||||||
fn extract_bnd_entry(
|
pub fn extract_bnd_entry(
|
||||||
file_info: &bnd::BndFileInfo,
|
file_info: &bnd::BndFileInfo,
|
||||||
bnd_data: &Vec<u8>,
|
bnd_data: &Vec<u8>,
|
||||||
output_dir: &path::Path,
|
output_dir: &path::Path,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), UnpackError> {
|
) -> Result<(), UnpackError> {
|
||||||
if file_info.path.is_none() {
|
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_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 data = &bnd_data[ofs_start..ofs_end];
|
||||||
|
|
||||||
let internal_path = file_info.path.to_owned().unwrap();
|
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();
|
let mut file_path = output_dir.to_path_buf();
|
||||||
file_path.push(file_name);
|
file_path.push(file_name);
|
||||||
|
|
||||||
if !overwrite && file_path.exists() {
|
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)?;
|
let mut output_file = fs::File::create(file_path)?;
|
||||||
output_file.write_all(&data)?;
|
output_file.write_all(&data)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -83,16 +84,15 @@ fn extract_bnd_entry(
|
||||||
/// Wraps around `load_bnd` to load the BND from disk. It returns the
|
/// Wraps around `load_bnd` to load the BND from disk. It returns the
|
||||||
/// parsed BND metadata and the whole file as a byte vector.
|
/// parsed BND metadata and the whole file as a byte vector.
|
||||||
pub fn load_bnd_file(bnd_path: &str) -> Result<(bnd::Bnd, Vec<u8>), UnpackError> {
|
pub fn load_bnd_file(bnd_path: &str) -> Result<(bnd::Bnd, Vec<u8>), 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))
|
Ok((load_bnd(&bnd_data)?, bnd_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a BND file from a bytes slice.
|
/// Load a BND file from a bytes slice.
|
||||||
pub fn load_bnd(bnd_data: &[u8]) -> Result<bnd::Bnd, UnpackError> {
|
pub fn load_bnd(bnd_data: &[u8]) -> Result<bnd::Bnd, UnpackError> {
|
||||||
let (_, bnd) = match bnd::parse(bnd_data) {
|
match bnd::parse(bnd_data) {
|
||||||
Ok(result) => result,
|
Ok((_, result)) => Ok(result),
|
||||||
Err(NomError(e)) | Err(NomFailure(e)) => return Err(UnpackError::parsing_err("BND", e.1)),
|
Err(NomError(e)) | Err(NomFailure(e)) => Err(UnpackError::parsing_err("BND", e.1)),
|
||||||
e => return Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))),
|
e => Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))),
|
||||||
};
|
}
|
||||||
Ok(bnd)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
use std::path;
|
||||||
|
|
||||||
use flate2::read::ZlibDecoder;
|
use flate2::read::ZlibDecoder;
|
||||||
use nom::Err::{Error as NomError, Failure as NomFailure};
|
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.
|
/// Load a DCX file in memory along with its decompressed content.
|
||||||
pub fn load_dcx(dcx_path: &str) -> Result<(dcx::Dcx, Vec<u8>), UnpackError> {
|
pub fn load_dcx(dcx_path: &str) -> Result<(dcx::Dcx, Vec<u8>), UnpackError> {
|
||||||
|
let dcx_path = path::Path::new(dcx_path);
|
||||||
let dcx_data = utils_fs::open_file_to_vec(dcx_path)?;
|
let dcx_data = utils_fs::open_file_to_vec(dcx_path)?;
|
||||||
let (data, dcx) = match dcx::parse(&dcx_data) {
|
let (data, dcx) = match dcx::parse(&dcx_data) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub fn strip_extension(path: &path::PathBuf) -> Option<path::PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a binary file and read it to the end in a byte vector.
|
/// Open a binary file and read it to the end in a byte vector.
|
||||||
pub fn open_file_to_vec(path: &str) -> Result<Vec<u8>, io::Error> {
|
pub fn open_file_to_vec(path: &path::Path) -> Result<Vec<u8>, io::Error> {
|
||||||
let mut file = fs::File::open(path)?;
|
let mut file = fs::File::open(path)?;
|
||||||
let file_len = file.metadata()?.len() as usize;
|
let file_len = file.metadata()?.len() as usize;
|
||||||
let mut data = vec![0u8; file_len];
|
let mut data = vec![0u8; file_len];
|
||||||
|
|
Reference in a new issue