From 1c509413bfbd94733163a80b8337a2ba0cc73f8c Mon Sep 17 00:00:00 2001 From: dece Date: Sat, 23 May 2020 19:49:43 +0200 Subject: [PATCH] dat: extract archive contents (King's Field IV) --- README.md | 1 + src/bin/rir.rs | 18 ++++++++++++ src/lib.rs | 2 ++ src/parsers/dat.rs | 47 ++++++++++++++++++++++++++++++ src/unpackers/bnd.rs | 2 +- src/unpackers/dat.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/parsers/dat.rs create mode 100644 src/unpackers/dat.rs diff --git a/README.md b/README.md index 07a1daa..e89e731 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ SUBCOMMANDS: bhds Extracts all BHD/BDT content (alphabetically) in a folder bhf Extracts BHF/BDT contents bnd Extracts BND contents + dat Extracts King's Field IV DAT contents dcx Extracts and decompress DCX data hash Calculates hash for a string help Prints this message or the help of the given subcommand(s) diff --git a/src/bin/rir.rs b/src/bin/rir.rs index fde2ff2..3dee414 100644 --- a/src/bin/rir.rs +++ b/src/bin/rir.rs @@ -86,6 +86,14 @@ fn main() { .arg(Arg::with_name("paramdef") .help("PARAMDEF file path") .short("d").long("def").takes_value(true).required(false))) + .subcommand(SubCommand::with_name("dat") + .about("Extracts King's Field IV DAT contents") + .arg(Arg::with_name("file") + .help("DAT file path") + .takes_value(true).required(true)) + .arg(Arg::with_name("output") + .help("Output directory") + .short("o").long("output").takes_value(true).required(true))) .get_matches(); process::exit(match matches.subcommand() { @@ -97,6 +105,7 @@ fn main() { ("bhf", Some(s)) => { cmd_bhf(s) } ("paramdef", Some(s)) => { cmd_paramdef(s) } ("param", Some(s)) => { cmd_param(s) } + ("dat", Some(s)) => { cmd_dat(s) } _ => { 0 } }) } @@ -239,3 +248,12 @@ fn cmd_param(args: &ArgMatches) -> i32 { }; 0 } + +fn cmd_dat(args: &ArgMatches) -> i32 { + let file_path: &str = args.value_of("file").unwrap(); + let output_path: &str = args.value_of("output").unwrap(); + match unpackers::dat::extract_dat_file(file_path, output_path) { + Err(e) => { eprintln!("Failed to extract DAT: {:?}", e); 1 } + _ => 0 + } +} diff --git a/src/lib.rs b/src/lib.rs index a05af1e..d379805 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod parsers { pub mod bnd; pub mod common; pub mod dcx; + pub mod dat; pub mod param; pub mod paramdef; } @@ -16,6 +17,7 @@ pub mod unpackers { pub mod bnd; pub mod dcx; pub mod errors; + pub mod dat; pub mod param; pub mod paramdef; } diff --git a/src/parsers/dat.rs b/src/parsers/dat.rs new file mode 100644 index 0000000..9ad9112 --- /dev/null +++ b/src/parsers/dat.rs @@ -0,0 +1,47 @@ +use nom::IResult; +use nom::multi::count; +use nom::number::complete::*; +use nom::sequence::tuple; + +use crate::parsers::common::take_cstring_from; + +#[derive(Debug)] +pub struct DatHeader { + pub unk00: u32, + pub num_files: u32, +} + +fn parse_header(i: &[u8]) -> IResult<&[u8], DatHeader> { + let (i, (unk00, num_files)) = tuple((le_u32, le_u32))(i)?; + Ok((i, DatHeader { unk00, num_files })) +} + +#[derive(Debug)] +pub struct DatFileEntry { + pub name: String, + pub size: u32, + pub padded_size: u32, + pub ofs_data: u32, +} + +fn parse_file_entry(i: &[u8]) -> IResult<&[u8], DatFileEntry> { + let (i, name) = take_cstring_from(i, 0x34)?; + let name = String::from_utf8_lossy(name).to_string(); + let (i, (size, padded_size, ofs_data)) = tuple((le_u32, le_u32, le_u32))(i)?; + Ok((i, DatFileEntry { name, size, padded_size, ofs_data })) +} + +#[derive(Debug)] +pub struct Dat { + pub header: DatHeader, + pub files: Vec, +} + +/// Parse a DAT archive, returning it with its full file data. +pub fn parse(i: &[u8]) -> IResult<&[u8], Dat> { + let full_file = i; + let (_, header) = parse_header(i)?; + let i = &full_file[0x40..]; + let (_, files) = count(parse_file_entry, header.num_files as usize)(i)?; + Ok((full_file, Dat { header, files })) +} diff --git a/src/unpackers/bnd.rs b/src/unpackers/bnd.rs index 8965935..831a53d 100644 --- a/src/unpackers/bnd.rs +++ b/src/unpackers/bnd.rs @@ -43,7 +43,7 @@ pub fn extract_bnd( for file_info in &bnd.file_infos { // Extract all entries, print but ignore path errors. match extract_bnd_entry(file_info, bnd_data, output_dir, overwrite) { - Err(UnpackError::Naming(e)) => { eprintln!("{}", e) } + Err(UnpackError::Naming(e)) => eprintln!("{}", e), _ => {} } } diff --git a/src/unpackers/dat.rs b/src/unpackers/dat.rs new file mode 100644 index 0000000..53d2d89 --- /dev/null +++ b/src/unpackers/dat.rs @@ -0,0 +1,68 @@ +use std::fs; +use std::io::Write; +use std::path; + +use nom::Err::{Error as NomError, Failure as NomFailure}; + +use crate::parsers::dat; +use crate::unpackers::errors::UnpackError; +use crate::utils::fs as utils_fs; + +pub fn extract_dat_file(dat_path: &str, output_path: &str) -> Result<(), UnpackError> { + let (dat, dat_data) = load_dat_file(dat_path)?; + extract_dat(&dat, dat_data, output_path) +} + +pub fn extract_dat( + dat: &dat::Dat, + dat_data: Vec, + output_path: &str +) -> Result<(), UnpackError> { + let output_dir = path::Path::new(output_path); + utils_fs::ensure_dir_exists(output_dir)?; + for file_entry in &dat.files { + match extract_file(file_entry, &dat_data, output_dir) { + Err(UnpackError::Io(e)) => eprintln!("Can't extract {}: {}", file_entry.name, e), + _ => {} + } + } + Ok(()) +} + +fn extract_file( + file_entry: &dat::DatFileEntry, + data: &Vec, + output_dir: &path::Path +) -> Result<(), UnpackError> { + let ofs_start = file_entry.ofs_data as usize; + let ofs_end = ofs_start + file_entry.size as usize; + let data = &data[ofs_start..ofs_end]; + let internal_path = &file_entry.name; + // If the path contains dirs, they have to be created. + if internal_path.contains('/') { + let internal_pb = path::PathBuf::from(internal_path); + let internal_parent_dir = internal_pb.parent() + .ok_or(UnpackError::Naming(format!("Bad path: {:?}", internal_pb)))?; + let mut parent_dir = output_dir.to_path_buf(); + parent_dir.push(internal_parent_dir); + utils_fs::ensure_dir_exists(&parent_dir)?; + } + let mut file_path = output_dir.to_path_buf(); + file_path.push(internal_path); + let mut output_file = fs::File::create(file_path)?; + output_file.write_all(&data)?; + Ok(()) +} + +pub fn load_dat_file(dat_path: &str) -> Result<(dat::Dat, Vec), UnpackError> { + let dat_data = utils_fs::open_file_to_vec(path::Path::new(dat_path))?; + Ok((load_dat(&dat_data)?, dat_data)) +} + +pub fn load_dat(dat_data: &[u8]) -> Result { + match dat::parse(&dat_data) { + Ok((_, dat)) => Ok(dat), + Err(NomError(e)) | Err(NomFailure(e)) => Err(UnpackError::parsing_err("DAT", e.1)), + e => Err(UnpackError::Unknown(format!("Unknown error: {:?}", e))), + } +}