update prs compression functions to catch and return errors

this is probably not an exhaustive set of potential errors, but it
accounts for all the possible ones i've seen as i've been working on
this, that are basically always a result of "bad data".

still a bit worried about potential panics also resulting from bad data
related to overflows and such things ...but have not seen any so far
This commit is contained in:
Gered 2021-05-20 13:49:01 -04:00
parent 5e6dd30fd4
commit f055daa73d
3 changed files with 88 additions and 24 deletions

View file

@ -1,5 +1,13 @@
use std::ffi::c_void; use std::ffi::c_void;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PrsCompressionError {
#[error("Error due to bad input data: {0}")]
BadData(String),
}
struct Context { struct Context {
bitpos: u8, bitpos: u8,
forward_log: Vec<u8>, forward_log: Vec<u8>,
@ -128,7 +136,7 @@ fn is_mem_equal(base: &[u8], offset1: isize, offset2: isize, length: usize) -> b
} }
} }
pub fn prs_compress(source: &[u8]) -> Box<[u8]> { pub fn prs_compress(source: &[u8]) -> Result<Box<[u8]>, PrsCompressionError> {
let mut pc = Context::new(); let mut pc = Context::new();
let mut x: isize = 0; let mut x: isize = 0;
@ -160,7 +168,15 @@ pub fn prs_compress(source: &[u8]) -> Box<[u8]> {
} }
if lssize == 0 { if lssize == 0 {
pc.raw_byte(source[x as usize]); pc.raw_byte(match source.get(x as usize) {
Some(value) => *value,
None => {
return Err(PrsCompressionError::BadData(format!(
"tried to add raw byte from source at out-of-bounds index {}",
x
)))
}
});
} else { } else {
pc.copy(lsoffset, lssize as u8); pc.copy(lsoffset, lssize as u8);
x += lssize - 1; x += lssize - 1;
@ -169,7 +185,7 @@ pub fn prs_compress(source: &[u8]) -> Box<[u8]> {
x += 1; x += 1;
} }
pc.finish() Ok(pc.finish())
} }
enum Next { enum Next {
@ -198,7 +214,7 @@ impl<'a> ByteReader<'a> {
} }
} }
pub fn prs_decompress(source: &[u8]) -> Box<[u8]> { pub fn prs_decompress(source: &[u8]) -> Result<Box<[u8]>, PrsCompressionError> {
let mut output = Vec::new(); let mut output = Vec::new();
let mut reader = ByteReader::new(source); let mut reader = ByteReader::new(source);
let mut r3: i32; let mut r3: i32;
@ -208,9 +224,19 @@ pub fn prs_decompress(source: &[u8]) -> Box<[u8]> {
let mut flag: bool; let mut flag: bool;
let mut offset: i32; let mut offset: i32;
// if you prs_compress a zero-length buffer, you get a 3-byte "compressed" result.
// therefore, 3 byte minimum input buffer is required to get any kind of "meaningful"
// decompression result back out
if source.len() < 3 {
return Err(PrsCompressionError::BadData(format!(
"Input data is too short: {} bytes",
source.len()
)));
}
current_byte = match reader.next() { current_byte = match reader.next() {
Next::Byte(byte) => byte, Next::Byte(byte) => byte,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}; };
loop { loop {
@ -218,7 +244,7 @@ pub fn prs_decompress(source: &[u8]) -> Box<[u8]> {
if bitpos == 0 { if bitpos == 0 {
current_byte = match reader.next() { current_byte = match reader.next() {
Next::Byte(byte) => byte, Next::Byte(byte) => byte,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}; };
bitpos = 8; bitpos = 8;
} }
@ -228,7 +254,7 @@ pub fn prs_decompress(source: &[u8]) -> Box<[u8]> {
if flag { if flag {
output.push(match reader.next() { output.push(match reader.next() {
Next::Byte(byte) => byte, Next::Byte(byte) => byte,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}); });
continue; continue;
} }
@ -237,7 +263,7 @@ pub fn prs_decompress(source: &[u8]) -> Box<[u8]> {
if bitpos == 0 { if bitpos == 0 {
current_byte = match reader.next() { current_byte = match reader.next() {
Next::Byte(byte) => byte, Next::Byte(byte) => byte,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}; };
bitpos = 8; bitpos = 8;
} }
@ -247,22 +273,22 @@ pub fn prs_decompress(source: &[u8]) -> Box<[u8]> {
if flag { if flag {
r3 = match reader.next() { r3 = match reader.next() {
Next::Byte(byte) => byte as i32, Next::Byte(byte) => byte as i32,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}; };
let high_byte = match reader.next() { let high_byte = match reader.next() {
Next::Byte(byte) => byte as i32, Next::Byte(byte) => byte as i32,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}; };
offset = ((high_byte & 0xff) << 8) | (r3 & 0xff); offset = ((high_byte & 0xff) << 8) | (r3 & 0xff);
if offset == 0 { if offset == 0 {
return output.into_boxed_slice(); return Ok(output.into_boxed_slice());
} }
r3 &= 0x00000007; r3 &= 0x00000007;
r5 = (offset >> 3) | -8192i32; // 0xffffe000 r5 = (offset >> 3) | -8192i32; // 0xffffe000
if r3 == 0 { if r3 == 0 {
r3 = match reader.next() { r3 = match reader.next() {
Next::Byte(byte) => byte as i32, Next::Byte(byte) => byte as i32,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}; };
r3 = (r3 & 0xff) + 1; r3 = (r3 & 0xff) + 1;
} else { } else {
@ -275,7 +301,7 @@ pub fn prs_decompress(source: &[u8]) -> Box<[u8]> {
if bitpos == 0 { if bitpos == 0 {
current_byte = match reader.next() { current_byte = match reader.next() {
Next::Byte(byte) => byte, Next::Byte(byte) => byte,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}; };
bitpos = 8; bitpos = 8;
} }
@ -286,7 +312,7 @@ pub fn prs_decompress(source: &[u8]) -> Box<[u8]> {
} }
offset = match reader.next() { offset = match reader.next() {
Next::Byte(byte) => byte as i32, Next::Byte(byte) => byte as i32,
Next::Eof() => return output.into_boxed_slice(), Next::Eof() => return Ok(output.into_boxed_slice()),
}; };
r3 += 2; r3 += 2;
r5 = offset | -256i32; // 0xffffff00 r5 = offset | -256i32; // 0xffffff00
@ -296,13 +322,25 @@ pub fn prs_decompress(source: &[u8]) -> Box<[u8]> {
} }
for _ in 0..r3 { for _ in 0..r3 {
let index = output.len() as i32 + r5; let index = output.len() as i32 + r5;
output.push(output[index as usize]); output.push(match output.get(index as usize) {
Some(value) => *value,
None => {
return Err(PrsCompressionError::BadData(format!(
"tried to push copy of byte at out-of-bounds index {}",
index
)))
}
});
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use claim::*;
use rand::rngs::StdRng;
use rand::{Fill, SeedableRng};
use super::*; use super::*;
struct TestData<'a> { struct TestData<'a> {
@ -652,13 +690,33 @@ I do not like green eggs and ham."
]; ];
#[test] #[test]
pub fn compresses_things() { pub fn compresses_things() -> Result<(), PrsCompressionError> {
for (index, test) in TEST_DATA.iter().enumerate() { for (index, test) in TEST_DATA.iter().enumerate() {
println!("\ntest #{}", index); println!("\ntest #{}", index);
println!(" prs_compress({:02x?})", test.uncompressed); println!(" prs_compress({:02x?})", test.uncompressed);
assert_eq!(*test.compressed, *prs_compress(&test.uncompressed)); assert_eq!(*test.compressed, *prs_compress(&test.uncompressed)?);
println!(" prs_decompress({:02x?})", test.compressed); println!(" prs_decompress({:02x?})", test.compressed);
assert_eq!(*test.uncompressed, *prs_decompress(&test.compressed)); assert_eq!(*test.uncompressed, *prs_decompress(&test.compressed)?);
} }
Ok(())
}
#[test]
pub fn decompress_bad_data_error_result() -> Result<(), PrsCompressionError> {
let data: &[u8] = &[];
assert_matches!(prs_decompress(data), Err(PrsCompressionError::BadData(..)));
let data: &[u8] = &[1, 2];
assert_matches!(prs_decompress(data), Err(PrsCompressionError::BadData(..)));
let data: &[u8] = &[1, 2, 3];
assert_matches!(prs_decompress(data), Err(PrsCompressionError::BadData(..)));
let mut data = [0u8; 1024];
let mut rng = StdRng::seed_from_u64(42);
data.try_fill(&mut rng).unwrap();
assert_matches!(prs_decompress(&data), Err(PrsCompressionError::BadData(..)));
Ok(())
} }
} }

View file

@ -6,7 +6,7 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use thiserror::Error; use thiserror::Error;
use crate::bytes::*; use crate::bytes::*;
use crate::compression::{prs_compress, prs_decompress}; use crate::compression::{prs_compress, prs_decompress, PrsCompressionError};
use crate::text::Language; use crate::text::Language;
pub const QUEST_BIN_NAME_LENGTH: usize = 32; pub const QUEST_BIN_NAME_LENGTH: usize = 32;
@ -23,6 +23,9 @@ pub enum QuestBinError {
#[error("I/O error while processing quest bin")] #[error("I/O error while processing quest bin")]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("PRS compression failed")]
PrsCompressionError(#[from] PrsCompressionError),
#[error("Bad quest bin data format: {0}")] #[error("Bad quest bin data format: {0}")]
DataFormatError(String), DataFormatError(String),
} }
@ -75,7 +78,7 @@ pub struct QuestBin {
impl QuestBin { impl QuestBin {
pub fn from_compressed_bytes(bytes: &[u8]) -> Result<QuestBin, QuestBinError> { pub fn from_compressed_bytes(bytes: &[u8]) -> Result<QuestBin, QuestBinError> {
let decompressed = prs_decompress(&bytes); let decompressed = prs_decompress(&bytes)?;
let mut reader = Cursor::new(decompressed); let mut reader = Cursor::new(decompressed);
Ok(QuestBin::from_uncompressed_bytes(&mut reader)?) Ok(QuestBin::from_uncompressed_bytes(&mut reader)?)
} }
@ -283,7 +286,7 @@ impl QuestBin {
pub fn to_compressed_bytes(&self) -> Result<Box<[u8]>, QuestBinError> { pub fn to_compressed_bytes(&self) -> Result<Box<[u8]>, QuestBinError> {
let uncompressed = self.to_uncompressed_bytes()?; let uncompressed = self.to_uncompressed_bytes()?;
Ok(prs_compress(uncompressed.as_ref())) Ok(prs_compress(uncompressed.as_ref())?)
} }
pub fn calculate_size(&self) -> usize { pub fn calculate_size(&self) -> usize {

View file

@ -6,7 +6,7 @@ use std::path::Path;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use thiserror::Error; use thiserror::Error;
use crate::compression::{prs_compress, prs_decompress}; use crate::compression::{prs_compress, prs_decompress, PrsCompressionError};
pub const QUEST_DAT_TABLE_HEADER_SIZE: usize = 16; pub const QUEST_DAT_TABLE_HEADER_SIZE: usize = 16;
@ -58,6 +58,9 @@ pub enum QuestDatError {
#[error("I/O error while processing quest dat")] #[error("I/O error while processing quest dat")]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("PRS compression failed")]
PrsCompressionError(#[from] PrsCompressionError),
#[error("Bad quest dat data format: {0}")] #[error("Bad quest dat data format: {0}")]
DataFormatError(String), DataFormatError(String),
} }
@ -163,7 +166,7 @@ pub struct QuestDat {
impl QuestDat { impl QuestDat {
pub fn from_compressed_bytes(bytes: &[u8]) -> Result<QuestDat, QuestDatError> { pub fn from_compressed_bytes(bytes: &[u8]) -> Result<QuestDat, QuestDatError> {
let decompressed = prs_decompress(&bytes); let decompressed = prs_decompress(&bytes)?;
let mut reader = Cursor::new(decompressed); let mut reader = Cursor::new(decompressed);
Ok(QuestDat::from_uncompressed_bytes(&mut reader)?) Ok(QuestDat::from_uncompressed_bytes(&mut reader)?)
} }
@ -276,7 +279,7 @@ impl QuestDat {
pub fn to_compressed_bytes(&self) -> Result<Box<[u8]>, QuestDatError> { pub fn to_compressed_bytes(&self) -> Result<Box<[u8]>, QuestDatError> {
let uncompressed = self.to_uncompressed_bytes()?; let uncompressed = self.to_uncompressed_bytes()?;
Ok(prs_compress(uncompressed.as_ref())) Ok(prs_compress(uncompressed.as_ref())?)
} }
pub fn calculate_size(&self) -> usize { pub fn calculate_size(&self) -> usize {