#![allow(dead_code)]
use std::collections::BTreeMap;
use nom::bytes::streaming::take;
use nom::number::streaming::{le_u16, le_u32};
use chrono::prelude::*;
use crate::parsing::*;
mod resource_type;
pub use resource_type::*;
#[derive(Debug, Copy, Clone)]
pub enum FileType {
Erf,
Hak,
Mod,
Pwc,
}
impl FileType {
pub fn to_fixed_string(&self) -> FixedSizeString<4> {
FixedSizeString::<4>::from_str(match self {
FileType::Erf => "ERF ",
FileType::Hak => "HAK ",
FileType::Mod => "MOD ",
FileType::Pwc => "PWC ",
})
.unwrap()
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(ftype: &str) -> Option<Self> {
match ftype {
"ERF " => Some(FileType::Erf),
"HAK " => Some(FileType::Hak),
"MOD " => Some(FileType::Mod),
"PWC " => Some(FileType::Pwc),
_ => None,
}
}
pub fn guess(file_name: &std::path::Path) -> Option<Self> {
match file_name.extension()?.to_str().unwrap() {
"erf" => Some(FileType::Erf),
"hak" => Some(FileType::Hak),
"mod" => Some(FileType::Mod),
"pwc" => Some(FileType::Pwc),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Header {
pub file_type: FixedSizeString<4>,
pub file_version: FixedSizeString<4>,
pub build_date: NaiveDate,
pub description_strref: u32,
}
struct HeaderParsingInfo {
localizedstrings_count: u32,
localizedstrings_size: u32,
keys_count: u32,
localizedstrings_offset: u32,
keys_offset: u32,
resources_offset: u32,
}
impl Header {
fn from_bytes(input: &[u8]) -> NWNParseResult<(Self, HeaderParsingInfo)> {
let (input, file_type) = FixedSizeString::<4>::from_bytes(input)?;
let (input, file_version) = FixedSizeString::<4>::from_bytes(input)?;
let (input, localizedstrings_count) = le_u32(input)?;
let (input, localizedstrings_size) = le_u32(input)?;
let (input, keys_count) = le_u32(input)?;
let (input, localizedstrings_offset) = le_u32(input)?;
let (input, keys_offset) = le_u32(input)?;
let (input, resources_offset) = le_u32(input)?;
let (input, build_year) = le_u32(input)?;
let build_year = build_year + 1900;
let (input, build_day) = le_u32(input)?;
let build_day = build_day + 1;
let (input, description_strref) = le_u32(input)?;
let (input, _) = take(116u8)(input)?;
Ok((
input,
(
Self {
file_type,
file_version,
build_date: NaiveDate::from_yo_opt(build_year as i32, build_day)
.unwrap_or(NaiveDate::from_yo_opt(1900, 1).unwrap()),
description_strref,
},
HeaderParsingInfo {
localizedstrings_count,
localizedstrings_size,
keys_count,
localizedstrings_offset,
keys_offset,
resources_offset,
},
),
))
}
fn to_bytes(&self, pi: &HeaderParsingInfo) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut output = vec![];
output.extend(self.file_type.as_bytes());
output.extend(self.file_version.as_bytes());
output.extend(pi.localizedstrings_count.to_le_bytes());
output.extend(pi.localizedstrings_size.to_le_bytes());
output.extend(pi.keys_count.to_le_bytes());
output.extend(pi.localizedstrings_offset.to_le_bytes());
output.extend(pi.keys_offset.to_le_bytes());
output.extend(pi.resources_offset.to_le_bytes());
let build_year: u32 = (self.build_date.year() as u32)
.checked_sub(1900)
.ok_or("Build year cannot be lower than 1900".to_string())?;
let build_day: u32 = self
.build_date
.ordinal()
.checked_sub(1)
.ok_or("Build day cannot be lower than 1".to_string())?;
output.extend(build_year.to_le_bytes());
output.extend(build_day.to_le_bytes());
output.extend(self.description_strref.to_le_bytes());
output.extend(&[0; 116]);
assert_eq!(output.len(), 160);
Ok(output)
}
}
#[derive(Debug)]
pub struct Resource {
pub file_name: FixedSizeString<32>,
pub resource_type: u16,
pub data: ResourceData,
}
impl Resource {
pub fn get_filename(&self) -> String {
self.file_name.to_string() + "." + ResourceType::from(self.resource_type).ext()
}
pub fn from_file(
file_path: &std::path::Path,
resource_type: Option<u16>,
) -> Result<Self, Box<dyn std::error::Error>> {
let resource_type = resource_type.unwrap_or(ResourceType::from(
file_path
.extension()
.ok_or(format!("failed to find file extension for {:?}", file_path))?
.to_str()
.unwrap(),
) as u16);
Ok(Self {
file_name: FixedSizeString::<32>::from_str(
file_path
.file_stem()
.unwrap_or(
file_path
.file_name()
.ok_or(format!("failed to find file name for {:?}", file_path))?,
)
.to_str()
.unwrap(),
)?,
resource_type,
data: ResourceData::File(file_path.to_path_buf()),
})
}
}
#[derive(Debug)]
pub enum ResourceData {
File(std::path::PathBuf),
Buffer(Vec<u8>),
ArchiveOffset { offset: u32, size: u32 },
}
impl ResourceData {
pub fn len(&self) -> Option<usize> {
match self {
ResourceData::File(path) => Some(std::fs::metadata(path).ok()?.len() as usize),
ResourceData::Buffer(buf) => Some(buf.len()),
ResourceData::ArchiveOffset { offset: _, size } => Some(*size as usize),
}
}
pub fn is_empty(&self) -> Option<bool> {
self.len().map(|e| e == 0)
}
pub fn file_read(&self) -> Result<Vec<u8>, std::io::Error> {
if let ResourceData::File(path) = self {
std::fs::read(path)
} else {
panic!("not a ResourceData::File");
}
}
pub fn file_load(&mut self) -> Result<(), std::io::Error> {
*self = ResourceData::Buffer(self.file_read()?);
Ok(())
}
pub fn archiveoffset_seek(
&self,
stream: &mut (impl std::io::Seek + std::io::Read),
) -> Result<Vec<u8>, std::io::Error> {
if let ResourceData::ArchiveOffset { offset, size } = self {
stream.seek(std::io::SeekFrom::Start(*offset as u64))?;
let mut buf = vec![0u8; *size as usize];
stream.read_exact(&mut buf)?;
Ok(buf)
} else {
panic!("not a ResourceData::ArchiveOffset");
}
}
pub fn archiveoffset_load_stream(
&mut self,
stream: &mut (impl std::io::Seek + std::io::Read),
) -> Result<(), std::io::Error> {
*self = ResourceData::Buffer(self.archiveoffset_seek(stream)?);
Ok(())
}
pub fn archiveoffset_load(&mut self, buffer: &[u8], buffer_offset: u32) -> Result<(), String> {
if let ResourceData::ArchiveOffset { offset, size } = &self {
let start = (offset - buffer_offset) as usize;
let end = (offset + size - buffer_offset) as usize;
let data = buffer
.get(start..end)
.ok_or(format!(
"resourceData is outside of buffer: start={}, end={} buffer.len()={}",
start,
end,
buffer.len(),
))?
.to_vec();
*self = ResourceData::Buffer(data);
}
Ok(())
}
}
#[derive(Debug)]
pub struct Erf {
pub header: Header,
pub description: BTreeMap<u32, String>,
pub resources: Vec<Resource>,
}
impl Erf {
pub fn new(
file_type: FixedSizeString<4>,
nwn1: bool,
build_date: chrono::NaiveDate,
description_strref: Option<u32>,
) -> Self {
Self {
header: Header {
file_type,
file_version: if nwn1 {
FixedSizeString::<4>::from_str("V1.0").unwrap()
} else {
FixedSizeString::<4>::from_str("V1.1").unwrap()
},
build_date,
description_strref: description_strref.unwrap_or(0),
},
resources: vec![],
description: BTreeMap::new(),
}
}
pub fn from_stream_head(
input: &mut dyn std::io::Read,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut buf = vec![0; 160];
input.read_exact(&mut buf)?;
let (_, (_header, header_parse_info)) =
nom_parse_context("while parsing header", Header::from_bytes(&buf))
.map_err(|e| e.to_string())?;
let head_end = header_parse_info.resources_offset + header_parse_info.keys_count * 8;
buf.resize(head_end as usize, 0);
input.read_exact(&mut buf[160..])?;
let (_, erf) = Self::from_bytes_head(&buf).map_err(|e| e.to_string())?;
Ok(erf)
}
pub fn from_bytes_head(input: &[u8]) -> NWNParseResult<Self> {
let (_, (header, header_parse_info)) = Header::from_bytes(input)?;
let has_long_resnames = header.file_version.as_str() >= "V1.1";
let mut description = BTreeMap::new();
{
let mut locstr_input = input
.get(header_parse_info.localizedstrings_offset as usize..)
.ok_or(nom::Err::Error(NWNParseError::from_string(format!(
"localized strings offset {} is out of bounds",
header_parse_info.localizedstrings_offset
))))?;
for _ in 0..header_parse_info.localizedstrings_count {
let language_id;
(locstr_input, language_id) = le_u32(locstr_input)?;
let string_size;
(locstr_input, string_size) = le_u32(locstr_input)?;
let str_data = locstr_input
.get(..string_size as usize)
.ok_or(nom::Err::Error(NWNParseError::from_string(format!(
"erf description string is too long ({}) and hits out of bounds",
string_size
))))?;
let text = std::str::from_utf8(str_data)
.map_err(|e| nom::Err::Error(NWNParseError::from(e)))?;
locstr_input = &locstr_input[string_size as usize..];
description.insert(language_id, text.to_string());
}
}
let mut resources = Vec::with_capacity(header_parse_info.keys_count as usize);
{
let mut keys_input =
input
.get(header_parse_info.keys_offset as usize..)
.ok_or(nom::Err::Error(NWNParseError::from_string(format!(
"keys offset {} is out of bounds",
header_parse_info.keys_offset
))))?;
for _ in 0..header_parse_info.keys_count {
let file_name;
(keys_input, file_name) = if has_long_resnames {
FixedSizeString::<32>::from_bytes(keys_input)?
} else {
let (keys_input, file_name) = FixedSizeString::<16>::from_bytes(keys_input)?;
(
keys_input,
FixedSizeString::<32>::from_str(file_name.as_str()).unwrap(), )
};
let _resource_id;
(keys_input, _resource_id) = le_u32(keys_input)?;
let resource_type;
(keys_input, resource_type) = le_u16(keys_input)?;
(keys_input, _) = take(2u8)(keys_input)?;
resources.push(Resource {
file_name,
resource_type,
data: ResourceData::ArchiveOffset { offset: 0, size: 0 }, })
}
}
{
let mut resources_input = input
.get(header_parse_info.resources_offset as usize..)
.ok_or(nom::Err::Error(NWNParseError::from_string(format!(
"resources offset {} is out of bounds",
header_parse_info.resources_offset
))))?;
for i in 0..header_parse_info.keys_count {
let offset;
(resources_input, offset) = le_u32(resources_input)?;
let size;
(resources_input, size) = le_u32(resources_input)?;
resources[i as usize].data = ResourceData::ArchiveOffset { offset, size };
}
}
Ok((
input,
Self {
header,
resources,
description,
},
))
}
pub fn from_bytes(input: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
let (_, mut erf) =
Self::from_bytes_head(input).map_err(|e| format!("failed to parse ERF head: {}", e))?; erf.load_archiveoffsets(input, 0)?;
Ok(erf)
}
pub fn load_archiveoffsets_stream(
&mut self,
stream: &mut (impl std::io::Seek + std::io::Read),
) -> Result<(), std::io::Error> {
for resource in self.resources.iter_mut() {
if let ResourceData::ArchiveOffset { offset: _, size: _ } = &resource.data {
resource.data.archiveoffset_load_stream(stream)?;
}
}
Ok(())
}
pub fn load_archiveoffsets(&mut self, buffer: &[u8], buffer_offset: u32) -> Result<(), String> {
for resource in self.resources.iter_mut() {
if let ResourceData::ArchiveOffset { offset: _, size: _ } = &resource.data {
resource.data.archiveoffset_load(buffer, buffer_offset)?;
}
}
Ok(())
}
pub fn load_files(&mut self) -> std::io::Result<()> {
for resource in self.resources.iter_mut() {
if let ResourceData::File(_) = &resource.data {
resource.data.file_load()?;
}
}
Ok(())
}
pub fn to_bytes_head(&self) -> Result<(Vec<u8>, usize), Box<dyn std::error::Error>> {
let has_long_resnames = self.header.file_version.as_str() >= "V1.1";
let mut output = vec![0; 160];
let localizedstrings_offset = output.len() as u32;
let localizedstrings_count = self.description.len() as u32;
for (lang_id, text) in self.description.iter() {
output.extend(lang_id.to_le_bytes());
output.extend((text.len() as u32).to_le_bytes());
output.extend(text.as_bytes());
}
let localizedstrings_size = output.len() as u32 - localizedstrings_offset;
output.reserve((24 + 8) * self.resources.len());
let keys_count = self.resources.len() as u32;
let keys_offset = output.len() as u32;
for (i, res) in self.resources.iter().enumerate() {
if has_long_resnames {
output.extend(res.file_name.as_bytes());
} else {
if res.file_name.len() > 16 {
return Err(format!(
"resource {:?} as a name longer than 16 characters",
res.file_name.as_str()
)
.into());
}
output.extend(&res.file_name.as_bytes()[..16]);
}
output.extend((i as u32).to_le_bytes());
output.extend(res.resource_type.to_le_bytes());
output.extend(&[0u8; 2]);
}
let resources_offset = output.len() as u32;
let mut resource_data_offset: u32 = resources_offset + 8 * self.resources.len() as u32;
for res in self.resources.iter() {
let len = match &res.data {
ResourceData::File(f) => std::fs::metadata(f)?.len() as u32,
ResourceData::Buffer(buf) => buf.len() as u32,
ResourceData::ArchiveOffset { offset: _, size } => *size,
};
output.extend(resource_data_offset.to_le_bytes());
output.extend(len.to_le_bytes());
resource_data_offset += len;
}
let pi = HeaderParsingInfo {
localizedstrings_count,
localizedstrings_size,
keys_count,
localizedstrings_offset,
keys_offset,
resources_offset,
};
output[..160].copy_from_slice(&self.header.to_bytes(&pi)?);
Ok((output, resource_data_offset as usize))
}
pub fn to_bytes(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buf = std::io::Cursor::new(vec![]);
self.write_stream(&mut buf)?;
Ok(buf.into_inner())
}
pub fn write_stream<T>(&self, output: &mut T) -> Result<(), Box<dyn std::error::Error>>
where
T: std::io::Write + std::io::Seek,
{
let (head_data, complete_size) = self.to_bytes_head()?;
output.seek(std::io::SeekFrom::Start(complete_size as u64))?;
output.write_all(&[])?;
output.seek(std::io::SeekFrom::Start(0))?;
output.write_all(&head_data)?;
for res in self.resources.iter() {
match &res.data {
ResourceData::File(f) => output.write_all(&std::fs::read(f)?),
ResourceData::Buffer(buf) => output.write_all(buf),
ResourceData::ArchiveOffset { offset: _, size: _ } => {
return Err(format!(
"resource {} contains ResourceData::ArchiveOffset data",
res.file_name
)
.into())
}
}?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hak() {
let erf_bytes = include_bytes!("../unittest/test.hak");
let erf = Erf::from_bytes(erf_bytes).unwrap();
assert_eq!(erf.header.file_type.as_str(), "HAK ");
assert_eq!(erf.header.file_version.as_str(), "V1.1");
assert_eq!(erf.header.build_date.year(), 2016);
assert_eq!(erf.header.build_date.month(), 06);
assert_eq!(erf.header.build_date.day(), 8);
assert_eq!(erf.header.description_strref, 0);
assert_eq!(erf.resources.len(), 2);
assert_eq!(erf.resources[0].file_name.as_str(), "eye");
assert_eq!(erf.resources[0].resource_type, ResourceType::Tga as u16);
assert_eq!(erf.resources[1].file_name.as_str(), "test");
assert_eq!(erf.resources[1].resource_type, ResourceType::Txt as u16);
assert_eq!(erf.description.len(), 0);
let data = erf.to_bytes().unwrap();
Erf::from_bytes(&data).unwrap();
assert_eq!(data, erf_bytes);
}
#[test]
fn test_nwn2_mod() {
let erf_bytes = include_bytes!("../unittest/nwn2_module.mod");
let erf = Erf::from_bytes(erf_bytes).unwrap();
assert_eq!(erf.header.file_type.as_str(), "MOD ");
assert_eq!(erf.header.file_version.as_str(), "V1.1");
assert_eq!(erf.header.build_date.year(), 2016);
assert_eq!(erf.header.build_date.month(), 06);
assert_eq!(erf.header.build_date.day(), 9);
assert_eq!(erf.header.description_strref, 0);
assert_eq!(erf.resources.len(), 8);
assert_eq!(erf.resources[0].file_name.as_str(), "conversation1");
assert_eq!(erf.resources[0].resource_type, ResourceType::Dlg as u16);
assert_eq!(erf.resources[1].file_name.as_str(), "gr_dm1");
assert_eq!(erf.resources[1].resource_type, ResourceType::Utc as u16);
assert_eq!(erf.resources[2].file_name.as_str(), "module");
assert_eq!(erf.resources[2].resource_type, ResourceType::Ifo as u16);
assert_eq!(erf.resources[3].file_name.as_str(), "module");
assert_eq!(erf.resources[3].resource_type, ResourceType::Jrl as u16);
assert_eq!(erf.description.len(), 11);
assert_eq!(erf.description[&0], "module description");
assert_eq!(erf.description[&6], "");
assert_eq!(erf.description[&130], "");
let data = erf.to_bytes().unwrap();
Erf::from_bytes(&data).unwrap();
assert_eq!(data, erf_bytes);
}
fn test_nwn1_mod() {
let erf_bytes = include_bytes!("../unittest/nwn1_module.mod");
let erf = Erf::from_bytes(erf_bytes).unwrap();
assert_eq!(erf.header.file_type.as_str(), "MOD ");
assert_eq!(erf.header.file_version.as_str(), "V1.0");
assert_eq!(erf.header.description_strref, u32::max_value());
assert_eq!(erf.resources.len(), 16);
assert_eq!(erf.resources[0].file_name.as_str(), "area001");
assert_eq!(erf.resources[0].resource_type, ResourceType::Are as u16);
assert_eq!(erf.resources[1].file_name.as_str(), "area001");
assert_eq!(erf.resources[1].resource_type, ResourceType::Git as u16);
assert_eq!(erf.resources[2].file_name.as_str(), "area001");
assert_eq!(erf.resources[2].resource_type, ResourceType::Gic as u16);
assert_eq!(erf.resources[3].file_name.as_str(), "beast001");
assert_eq!(erf.resources[3].resource_type, ResourceType::Utc as u16);
assert_eq!(erf.resources[4].file_name.as_str(), "creaturepalcus");
assert_eq!(erf.resources[4].resource_type, ResourceType::Itp as u16);
assert_eq!(erf.resources[9].file_name.as_str(), "module");
assert_eq!(erf.resources[9].resource_type, ResourceType::Itp as u16);
assert_eq!(erf.description.len(), 0);
let data = erf.to_bytes().unwrap();
Erf::from_bytes(&data).unwrap();
assert_eq!(data, erf_bytes);
}
}