From 2ceca5e673343d9ad64758e2cfea51fa48c3fe43 Mon Sep 17 00:00:00 2001 From: gered Date: Sun, 16 May 2021 19:10:06 -0400 Subject: [PATCH] some re-working of quest bin/dat reading/writing methods get rid of the generalized traits that the read/write methods were located in. now those methods are just located directly in the struct impl itself also cleaned up quest bin/dat errors added validation checks in quest bin/dat reading --- psoutils/src/bytes.rs | 29 -------- psoutils/src/quest/bin.rs | 147 +++++++++++++++++++++----------------- psoutils/src/quest/dat.rs | 139 ++++++++++++++++++----------------- 3 files changed, 157 insertions(+), 158 deletions(-) diff --git a/psoutils/src/bytes.rs b/psoutils/src/bytes.rs index 041e136..5cab010 100644 --- a/psoutils/src/bytes.rs +++ b/psoutils/src/bytes.rs @@ -1,32 +1,3 @@ -use byteorder::{ReadBytesExt, WriteBytesExt}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ReadBytesError { - #[error("Unexpected error while reading bytes: {0}")] - UnexpectedError(String), - - #[error("I/O error reading bytes")] - IoError(#[from] std::io::Error), -} - -pub trait ReadFromBytes: Sized { - fn read_from_bytes(reader: &mut T) -> Result; -} - -#[derive(Error, Debug)] -pub enum WriteBytesError { - #[error("Unexpected error while writing bytes: {0}")] - UnexpectedError(String), - - #[error("I/O error writing bytes")] - IoError(#[from] std::io::Error), -} - -pub trait WriteAsBytes { - fn write_as_bytes(&self, writer: &mut T) -> Result<(), WriteBytesError>; -} - pub trait FixedLengthByteArrays { fn as_unpadded_slice(&self) -> &[u8]; fn to_fixed_length(&self, length: usize) -> Vec; diff --git a/psoutils/src/quest/bin.rs b/psoutils/src/quest/bin.rs index 5e546f4..565a2de 100644 --- a/psoutils/src/quest/bin.rs +++ b/psoutils/src/quest/bin.rs @@ -7,7 +7,7 @@ use thiserror::Error; use crate::bytes::*; use crate::compression::{prs_compress, prs_decompress}; -use crate::text::{Language, LanguageError}; +use crate::text::Language; pub const QUEST_BIN_NAME_LENGTH: usize = 32; pub const QUEST_BIN_SHORT_DESCRIPTION_LENGTH: usize = 128; @@ -23,14 +23,8 @@ pub enum QuestBinError { #[error("I/O error while processing quest bin")] IoError(#[from] std::io::Error), - #[error("String encoding error during processing of quest bin string field")] - StringEncodingError(#[from] LanguageError), - - #[error("Error reading quest bin from bytes")] - ReadFromBytesError(#[from] ReadBytesError), - - #[error("Error writing quest bin as bytes")] - WriteAsBytesError(#[from] WriteBytesError), + #[error("Bad quest bin data format: {0}")] + DataFormatError(String), } #[derive(Copy, Clone)] @@ -83,58 +77,30 @@ impl QuestBin { pub fn from_compressed_bytes(bytes: &[u8]) -> Result { let decompressed = prs_decompress(&bytes); let mut reader = Cursor::new(decompressed); - Ok(QuestBin::read_from_bytes(&mut reader)?) + Ok(QuestBin::from_uncompressed_bytes(&mut reader)?) } - pub fn from_compressed_file(path: &Path) -> Result { - let mut file = File::open(path)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - QuestBin::from_compressed_bytes(&buffer) - } - - pub fn from_uncompressed_file(path: &Path) -> Result { - let file = File::open(path)?; - let mut reader = BufReader::new(file); - Ok(QuestBin::read_from_bytes(&mut reader)?) - } - - pub fn to_compressed_bytes(&self) -> Result, WriteBytesError> { - let bytes = self.to_uncompressed_bytes()?; - Ok(prs_compress(bytes.as_ref())) - } - - pub fn to_uncompressed_bytes(&self) -> Result, WriteBytesError> { - let mut bytes = Cursor::new(Vec::new()); - self.write_as_bytes(&mut bytes)?; - Ok(bytes.into_inner().into_boxed_slice()) - } - - pub fn calculate_size(&self) -> usize { - QUEST_BIN_HEADER_SIZE - + self.object_code.as_ref().len() - + self.function_offset_table.as_ref().len() - } -} - -impl ReadFromBytes for QuestBin { - fn read_from_bytes(reader: &mut T) -> Result { + pub fn from_uncompressed_bytes( + reader: &mut T, + ) -> Result { let object_code_offset = reader.read_u32::()?; + if object_code_offset != QUEST_BIN_HEADER_SIZE as u32 { + return Err(QuestBinError::DataFormatError(format!( + "Invalid object_code_offset found: {}", + object_code_offset + ))); + } + let function_offset_table_offset = reader.read_u32::()?; let bin_size = reader.read_u32::()?; - let _xfffffff = reader.read_u32::()?; // always 0xffffffff + let _xfffffff = reader.read_u32::()?; // always expected to be 0xffffffff let is_download = reader.read_u8()?; + let is_download = is_download != 0; + let language = reader.read_u8()?; - let quest_number_and_episode = reader.read_u16::()?; - - let is_download = if is_download == 0 { false } else { true }; - let quest_number = QuestNumber { - number: quest_number_and_episode, - }; - let language = match Language::from_number(language) { Err(e) => { - return Err(ReadBytesError::UnexpectedError(format!( + return Err(QuestBinError::DataFormatError(format!( "Unsupported language value found in quest header: {}", e ))) @@ -142,11 +108,16 @@ impl ReadFromBytes for QuestBin { Ok(encoding) => encoding, }; + let quest_number_and_episode = reader.read_u16::()?; + let quest_number = QuestNumber { + number: quest_number_and_episode, + }; + let mut name_bytes = [0u8; QUEST_BIN_NAME_LENGTH]; reader.read_exact(&mut name_bytes)?; let name = match language.decode_text(name_bytes.as_unpadded_slice()) { Err(e) => { - return Err(ReadBytesError::UnexpectedError(format!( + return Err(QuestBinError::DataFormatError(format!( "Error decoding string in quest 'name' field: {}", e ))) @@ -159,7 +130,7 @@ impl ReadFromBytes for QuestBin { let short_description = match language.decode_text(short_description_bytes.as_unpadded_slice()) { Err(e) => { - return Err(ReadBytesError::UnexpectedError(format!( + return Err(QuestBinError::DataFormatError(format!( "Error decoding string in quest 'short_description' field: {}", e ))) @@ -172,7 +143,7 @@ impl ReadFromBytes for QuestBin { let long_description = match language.decode_text(long_description_bytes.as_unpadded_slice()) { Err(e) => { - return Err(ReadBytesError::UnexpectedError(format!( + return Err(QuestBinError::DataFormatError(format!( "Error decoding string in quest 'long_description' field: {}", e ))) @@ -186,12 +157,17 @@ impl ReadFromBytes for QuestBin { let function_offset_table_size = bin_size - function_offset_table_offset; if function_offset_table_size % 4 != 0 { - return Err(ReadBytesError::UnexpectedError(String::from("Non-dword-sized bytes found in quest bin where function offset table is expected (probably a PRS decompression issue?)"))); + return Err(QuestBinError::DataFormatError( + format!( + "Non-dword-sized data segment found in quest bin where function offset table is expected. Function offset table data size: {}", + function_offset_table_size + ) + )); } let mut function_offset_table = vec![0u8; function_offset_table_size as usize]; reader.read_exact(&mut function_offset_table)?; - Ok(QuestBin { + let bin = QuestBin { header: QuestBinHeader { is_download, language, @@ -202,12 +178,36 @@ impl ReadFromBytes for QuestBin { }, object_code: object_code.into_boxed_slice(), function_offset_table: function_offset_table.into_boxed_slice(), - }) - } -} + }; -impl WriteAsBytes for QuestBin { - fn write_as_bytes(&self, writer: &mut T) -> Result<(), WriteBytesError> { + let our_bin_size = bin.calculate_size(); + if our_bin_size != bin_size as usize { + return Err(QuestBinError::DataFormatError(format!( + "bin_size value {} found in header does not match size of data actually read {}", + bin_size, our_bin_size + ))); + } + + Ok(bin) + } + + pub fn from_compressed_file(path: &Path) -> Result { + let mut file = File::open(path)?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + QuestBin::from_compressed_bytes(&buffer) + } + + pub fn from_uncompressed_file(path: &Path) -> Result { + let file = File::open(path)?; + let mut reader = BufReader::new(file); + Ok(QuestBin::from_uncompressed_bytes(&mut reader)?) + } + + pub fn to_uncompressed_bytes( + &self, + writer: &mut T, + ) -> Result<(), QuestBinError> { let bin_size = self.calculate_size(); let object_code_offset = QUEST_BIN_HEADER_SIZE; let function_offset_table_offset = QUEST_BIN_HEADER_SIZE + self.object_code.len(); @@ -224,7 +224,7 @@ impl WriteAsBytes for QuestBin { let name_bytes = match language.encode_text(&self.header.name) { Err(e) => { - return Err(WriteBytesError::UnexpectedError(format!( + return Err(QuestBinError::DataFormatError(format!( "Error encoding string for quest 'name' field: {}", e ))) @@ -235,7 +235,7 @@ impl WriteAsBytes for QuestBin { let short_description_bytes = match language.encode_text(&self.header.short_description) { Err(e) => { - return Err(WriteBytesError::UnexpectedError(format!( + return Err(QuestBinError::DataFormatError(format!( "Error encoding string for quest 'short_description_bytes' field: {}", e ))) @@ -248,7 +248,7 @@ impl WriteAsBytes for QuestBin { let long_description_bytes = match language.encode_text(&self.header.long_description) { Err(e) => { - return Err(WriteBytesError::UnexpectedError(format!( + return Err(QuestBinError::DataFormatError(format!( "Error encoding string for quest 'long_description_bytes' field: {}", e ))) @@ -264,6 +264,23 @@ impl WriteAsBytes for QuestBin { Ok(()) } + + pub fn as_uncompressed_bytes(&self) -> Result, QuestBinError> { + let mut buffer = Cursor::new(Vec::::new()); + self.to_uncompressed_bytes(&mut buffer)?; + Ok(buffer.into_inner().into_boxed_slice()) + } + + pub fn as_compressed_bytes(&self) -> Result, QuestBinError> { + let uncompressed = self.as_uncompressed_bytes()?; + Ok(prs_compress(uncompressed.as_ref())) + } + + pub fn calculate_size(&self) -> usize { + QUEST_BIN_HEADER_SIZE + + self.object_code.as_ref().len() + + self.function_offset_table.as_ref().len() + } } #[cfg(test)] diff --git a/psoutils/src/quest/dat.rs b/psoutils/src/quest/dat.rs index 3a0b196..1cb331e 100644 --- a/psoutils/src/quest/dat.rs +++ b/psoutils/src/quest/dat.rs @@ -6,9 +6,7 @@ use std::path::Path; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use thiserror::Error; -use crate::bytes::*; use crate::compression::{prs_compress, prs_decompress}; -use crate::text::LanguageError; pub const QUEST_DAT_TABLE_HEADER_SIZE: usize = 16; @@ -60,14 +58,8 @@ pub enum QuestDatError { #[error("I/O error while processing quest dat")] IoError(#[from] std::io::Error), - #[error("String encoding error during processing of quest dat string field")] - StringEncodingError(#[from] LanguageError), - - #[error("Error reading quest dat from bytes")] - ReadFromBytesError(#[from] ReadBytesError), - - #[error("Error writing quest dat as bytes")] - WriteAsBytesError(#[from] WriteBytesError), + #[error("Bad quest dat data format: {0}")] + DataFormatError(String), } #[derive(Debug, Eq, PartialEq, Copy, Clone)] @@ -173,7 +165,56 @@ impl QuestDat { pub fn from_compressed_bytes(bytes: &[u8]) -> Result { let decompressed = prs_decompress(&bytes); let mut reader = Cursor::new(decompressed); - Ok(QuestDat::read_from_bytes(&mut reader)?) + Ok(QuestDat::from_uncompressed_bytes(&mut reader)?) + } + + pub fn from_uncompressed_bytes( + reader: &mut T, + ) -> Result { + let mut tables = Vec::new(); + let mut index = 0; + loop { + let table_type = reader.read_u32::()?; + let table_size = reader.read_u32::()?; + let area = reader.read_u32::()?; + let table_body_size = reader.read_u32::()?; + + // quest .dat files appear to always use a "zero-table" to mark the end of the file + if table_type == 0 && table_size == 0 && area == 0 && table_body_size == 0 { + break; + } + + if table_size != table_body_size + QUEST_DAT_TABLE_HEADER_SIZE as u32 { + return Err(QuestDatError::DataFormatError(format!( + "Malformed table at index {}. table_size != table_body_size + 16", + index + ))); + } + + let table_type: QuestDatTableType = match table_type.into() { + QuestDatTableType::Unknown(n) => { + return Err(QuestDatError::DataFormatError(format!( + "Invalid table_type {} for table at index {}", + n, index + ))) + } + otherwise => otherwise, + }; + + let mut body_bytes = vec![0u8; table_body_size as usize]; + reader.read_exact(&mut body_bytes)?; + + tables.push(QuestDatTable { + header: QuestDatTableHeader { table_type, area }, + bytes: body_bytes.into_boxed_slice(), + }); + + index += 1; + } + + Ok(QuestDat { + tables: tables.into_boxed_slice(), + }) } pub fn from_compressed_file(path: &Path) -> Result { @@ -186,61 +227,13 @@ impl QuestDat { pub fn from_uncompressed_file(path: &Path) -> Result { let file = File::open(path)?; let mut reader = BufReader::new(file); - Ok(QuestDat::read_from_bytes(&mut reader)?) + Ok(QuestDat::from_uncompressed_bytes(&mut reader)?) } - pub fn to_compressed_bytes(&self) -> Result, WriteBytesError> { - let bytes = self.to_uncompressed_bytes()?; - Ok(prs_compress(bytes.as_ref())) - } - - pub fn to_uncompressed_bytes(&self) -> Result, WriteBytesError> { - let mut bytes = Cursor::new(Vec::new()); - self.write_as_bytes(&mut bytes)?; - Ok(bytes.into_inner().into_boxed_slice()) - } - - pub fn calculate_size(&self) -> usize { - self.tables - .iter() - .map(|table| QUEST_DAT_TABLE_HEADER_SIZE + table.body_size() as usize) - .sum() - } -} - -impl ReadFromBytes for QuestDat { - fn read_from_bytes(reader: &mut T) -> Result { - let mut tables = Vec::new(); - loop { - let table_type = reader.read_u32::()?; - let table_size = reader.read_u32::()?; - let area = reader.read_u32::()?; - let table_body_size = reader.read_u32::()?; - - // quest .dat files appear to always use a "zero-table" to mark the end of the file - if table_type == 0 && table_size == 0 && area == 0 && table_body_size == 0 { - break; - } - - let mut body_bytes = vec![0u8; table_body_size as usize]; - reader.read_exact(&mut body_bytes)?; - - let table_type: QuestDatTableType = table_type.into(); - - tables.push(QuestDatTable { - header: QuestDatTableHeader { table_type, area }, - bytes: body_bytes.into_boxed_slice(), - }); - } - - Ok(QuestDat { - tables: tables.into_boxed_slice(), - }) - } -} - -impl WriteAsBytes for QuestDat { - fn write_as_bytes(&self, writer: &mut T) -> Result<(), WriteBytesError> { + pub fn to_uncompressed_bytes( + &self, + writer: &mut T, + ) -> Result<(), QuestDatError> { for table in self.tables.iter() { let table_size = table.calculate_size() as u32; let table_body_size = table.body_size() as u32; @@ -261,6 +254,24 @@ impl WriteAsBytes for QuestDat { Ok(()) } + + pub fn as_uncompressed_bytes(&self) -> Result, QuestDatError> { + let mut buffer = Cursor::new(Vec::::new()); + self.to_uncompressed_bytes(&mut buffer)?; + Ok(buffer.into_inner().into_boxed_slice()) + } + + pub fn as_compressed_bytes(&self) -> Result, QuestDatError> { + let uncompressed = self.as_uncompressed_bytes()?; + Ok(prs_compress(uncompressed.as_ref())) + } + + pub fn calculate_size(&self) -> usize { + self.tables + .iter() + .map(|table| QUEST_DAT_TABLE_HEADER_SIZE + table.body_size() as usize) + .sum() + } } #[cfg(test)]