add archive tests

This commit is contained in:
2023-05-08 23:41:55 +02:00
parent 06b679ba30
commit 4d048e77ee
19 changed files with 837 additions and 101 deletions

View File

@@ -18,6 +18,7 @@ pub struct Archive(HashMap<String, FilePointer>);
pub struct FilePointer {
pub position: usize,
pub length: usize,
pub path: String,
}
impl Deref for Archive {
@@ -114,7 +115,8 @@ mod tests {
archive["data\\config.txt"],
FilePointer {
position: 0x57b40,
length: 0xf4
length: 0xf4,
path: "data\\config.txt".to_string(),
}
);
assert_eq!(
@@ -122,6 +124,7 @@ mod tests {
FilePointer {
position: 0x57c40,
length: 0x7dfd8,
path: "data\\fonts\\dangerfont.bmp".to_string()
}
)
}
@@ -136,6 +139,7 @@ mod tests {
FilePointer {
position: 0x1200,
length: 0x8d9,
path: "data\\mhx.fnt".to_string()
}
);
assert_eq!(
@@ -143,6 +147,7 @@ mod tests {
FilePointer {
position: 0x1c00,
length: 0x427e,
path: "data\\text.txt".to_string(),
}
)
}
@@ -157,6 +162,7 @@ mod tests {
FilePointer {
position: 0x7000,
length: 0x40,
path: "data\\endbranding_xxl.txt".to_string()
}
);
assert_eq!(
@@ -164,6 +170,7 @@ mod tests {
FilePointer {
position: 0x7200,
length: 0x872,
path: "data\\settings_xxl.txt".to_string(),
}
)
}

View File

@@ -47,6 +47,7 @@ impl From<FileEntry> for FilePointer {
FilePointer {
position: value.pointer[0] as usize,
length: value.pointer[1] as usize,
path: value.name.to_string(),
}
}
}

View File

@@ -43,6 +43,7 @@ impl From<FileEntry> for FilePointer {
FilePointer {
position: value.pos as usize,
length: value.len as usize,
path: value.name.to_string(),
}
}
}

View File

@@ -0,0 +1,66 @@
use crate::media::txt::DecryptError;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum Error {
UnknownFormat(String),
InvalidExtension(Option<String>),
InvalidPath(String),
InvalidData {
info: Option<String>,
context: String,
},
Custom(Box<dyn std::error::Error>),
UnknownError,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::UnknownFormat(format) => write!(f, "Unknown format: {}", format),
Error::UnknownError => write!(f, "Unknown Error"),
Error::InvalidExtension(None) => write!(f, "Missing file extension"),
Error::InvalidExtension(Some(ext)) => write!(f, "Invalid extension {}", ext),
Error::InvalidPath(path) => write!(f, "Invalid Path {}", path),
Error::InvalidData { info, context } => write!(
f,
"Invalid data: {}; {}",
info.unwrap_or("[no info]".to_string()),
context
),
Error::Custom(error) => write!(f, "{}", error),
}
}
}
impl std::error::Error for Error {}
impl From<binrw::Error> for Error {
fn from(value: binrw::Error) -> Self {
Error::Custom(Box::new(value))
}
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Error::Custom(Box::new(value))
}
}
impl From<serde_xml_rs::Error> for Error {
fn from(value: serde_xml_rs::Error) -> Self {
Error::Custom(Box::new(value))
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(value: std::string::FromUtf8Error) -> Self {
Error::Custom(Box::new(value))
}
}
impl From<DecryptError> for Error {
fn from(value: DecryptError) -> Self {
Error::Custom(Box::new(value))
}
}

View File

@@ -1,2 +1,92 @@
use crate::archive::FilePointer;
use crate::error::Error;
use crate::media::level::LevelLayer;
use crate::media::rle::RleImage;
use crate::media::sprites::Sprites;
use crate::media::txt::{decrypt_exposed_txt, decrypt_txt};
use crate::media::ui::UiTag;
use binrw::prelude::BinRead;
use encoding_rs::WINDOWS_1252;
use itertools::Itertools;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::path::Path;
pub mod archive;
pub mod error;
pub mod media;
pub enum DatafileFile {
Txt(String),
Level(LevelLayer),
Sprites(Vec<Sprites>),
RleSprite(Box<RleImage>),
Bitmap(Vec<u8>),
Vorbis(Vec<u8>),
TileCollision(String),
Ui(UiTag),
Translations(HashMap<String, Vec<String>>),
}
impl FilePointer {
pub fn load_from<R>(&self, reader: &mut R) -> Result<DatafileFile, Error>
where
R: Read + Seek,
{
reader.seek(SeekFrom::Start(self.position as u64))?;
let mut data = vec![0u8; self.length as usize];
reader.read_exact(&mut data)?;
let path = Path::new(&self.path);
match path
.extension()
.and_then(OsStr::to_str)
.ok_or(Error::InvalidExtension(None))?
{
"dat" => Ok(DatafileFile::Level(LevelLayer::read(&mut Cursor::new(
data,
))?)),
"rle" => Ok(DatafileFile::RleSprite(Box::new(RleImage::read(
&mut Cursor::new(data),
)?))),
"bmp" => Ok(DatafileFile::Bitmap(data)),
"ogg" => Ok(DatafileFile::Vorbis(data)),
"xml" => Ok(DatafileFile::Ui(
serde_xml_rs::from_str::<UiTag>(String::from_utf8(data)?.as_str())?.post_process(),
)),
"txt" => {
let stem = path
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| Error::InvalidPath(path.to_string_lossy().to_string()))?;
let decr = decrypt_txt(data.into_iter())?;
if stem.starts_with("tile_collision") {
Ok(DatafileFile::TileCollision(decr))
} else if stem == "sprites" {
Ok(DatafileFile::Sprites(Sprites::parse(decr.as_str())?))
} else if stem.starts_with("profile") || stem.starts_with("highscores") {
Ok(DatafileFile::Txt(decrypt_exposed_txt(decr)?))
} else {
Ok(DatafileFile::Txt(decr))
}
}
"csv" => Ok(DatafileFile::Translations(
WINDOWS_1252
.decode(data.as_slice())
.0
.split('\n')
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.map(|l| {
l.splitn(2, ';')
.map(|s| s.to_string())
.collect_tuple::<(String, String)>()
.expect("Invalid csv")
})
.into_group_map(),
)),
ext => Err(Error::UnknownFormat(ext.to_string())),
}
}
}

View File

@@ -0,0 +1 @@
ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜabcdefghijklmnopqrstuvwxyzäöüß0123456789,;.:!?+-*/=<>()[]{}\"$%&#~_^@|¡¿™©®º¹²³ªÀÁÂÃÅÆÇÈÉÊËÌÍÎÏIÐGÑÒÓÔÕŒØSŠÙÚÛÝÞŸŽàáâãåæçèéêëìíîïiðgñòóôõœøsšùúûýþÿž£¥ƒ¤¯¦¬¸¨·§×¢±÷µ«»

View File

@@ -0,0 +1 @@
ABCDEFGHIJKLMNOPQRSTUVWXYZ<EFBFBD><EFBFBD><EFBFBD>abcdefghijklmnopqrstuvwxyz<EFBFBD><EFBFBD><EFBFBD><EFBFBD>0123456789,;.:!?+-*/=<>()[]{}\"$%&#~_<>^@|<7C><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>G<EFBFBD><47><EFBFBD><EFBFBD>Ռ<EFBFBD>S<EFBFBD><53><EFBFBD><EFBFBD><EFBFBD>ޟ<EFBFBD><DE9F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD>g<EFBFBD><67><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>s<EFBFBD><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD><EFBFBD><EFBFBD>

View File

@@ -0,0 +1,2 @@
pub const CHARSET: &[u8] = include_bytes!("charset.txt");
pub const CHARSET_UTF8: &str = include_str!("charset-utf8.txt");

View File

@@ -0,0 +1,19 @@
use binrw::prelude::*;
#[binrw]
#[brw(little)]
pub struct LevelTile {
pub index: u8,
pub id: u8,
}
#[binrw]
#[brw(little)]
pub struct LevelLayer {
pub tile_count: u32,
pub width: u32,
pub height: u32,
pub unknown_2: u32,
#[br(count = width * height)]
pub tiles: Vec<LevelTile>,
}

View File

@@ -1 +1,6 @@
pub mod font;
pub mod level;
pub mod rle;
pub mod sprites;
pub mod txt;
pub mod ui;

View File

@@ -0,0 +1,29 @@
use crate::media::rle::{bgra_to_rgba, RleImage};
use image::error::{LimitError, LimitErrorKind};
use image::{AnimationDecoder, Delay, Frame, Frames, ImageBuffer, ImageError};
use std::time::Duration;
impl<'a> AnimationDecoder<'a> for RleImage {
fn into_frames(self) -> Frames<'a> {
Frames::new(Box::new(self.frames.into_iter().map(move |frame| {
let buffer = ImageBuffer::from_raw(
frame.width,
frame.height,
frame
.data
.into_iter()
.flat_map(|it| bgra_to_rgba(self.color_table[it as usize]))
.collect(),
)
.ok_or(ImageError::Limits(LimitError::from_kind(
LimitErrorKind::InsufficientMemory,
)))?;
Ok(Frame::from_parts(
buffer,
frame.left,
frame.top,
Delay::from_saturating_duration(Duration::from_millis(80)),
))
})))
}
}

View File

@@ -0,0 +1,98 @@
use binrw::prelude::*;
use binrw::Endian;
use std::io::{Read, Seek};
#[cfg(all(feature = "rle_gif"))]
pub mod gif;
#[binread]
#[br(little, magic = 0x67u32)]
pub struct RleImage {
pub hash: u64,
pub color_table: [[u8; 4]; 512],
pub width: u32,
pub height: u32,
pub numerator: u32,
pub denominator: u32,
#[br(temp)]
pub frame_count: u32,
#[br(count = frame_count)]
pub frames: Vec<RleLayer>,
}
#[binread]
#[br(little)]
pub struct RleLayer {
pub width: u32,
pub height: u32,
pub left: u32,
pub top: u32,
pub numerator: u32,
pub denominator: u32,
pub data_size: u32,
pub unknown3: u32,
#[br(args(width * height), parse_with = parse_rle)]
pub data: Vec<u8>,
}
pub fn parse_rle<R: Read + Seek>(
reader: &mut R,
endian: Endian,
(size,): (u32,),
) -> BinResult<Vec<u8>> {
let mut data = Vec::with_capacity(size as usize);
while data.len() != size as usize {
let count: i8 = reader.read_type(endian)?;
if count > 0 {
let value: u8 = reader.read_type(endian)?;
for _ in 0..count {
data.push(value);
}
} else {
for _ in 0..-count {
data.push(reader.read_type(endian)?);
}
}
}
Ok(data)
}
impl RleImage {
pub fn get_image_data(&self, layer: &RleLayer) -> Vec<u8> {
let mut data = Vec::<u8>::with_capacity(self.width as usize * self.height as usize * 4);
let mut i = 0;
for y in 0..self.height {
for x in 0..self.width {
if y < layer.top
|| y >= layer.top + layer.height
|| x < layer.left
|| x >= layer.left + layer.width
{
data.push(0);
data.push(0);
data.push(0);
data.push(0);
} else {
let color = self.color_table[layer.data[i] as usize];
i += 1;
data.push(color[2]);
data.push(color[1]);
data.push(color[0]);
data.push(if color[2] == 0 && color[1] == 0 && color[0] == 0 {
0
} else {
255
});
}
}
}
data
}
}
pub fn bgra_to_rgba(pixel: [u8; 4]) -> [u8; 4] {
[pixel[2], pixel[1], pixel[0], pixel[3]]
}

View File

@@ -0,0 +1,76 @@
use crate::error::Error;
#[derive(Debug)]
pub struct Sprites {
pub name: String,
pub sprite_type: SpriteType,
pub file_name: String,
pub render_mode: RenderMode,
pub frames: Option<CropMode>,
}
impl Sprites {
pub fn parse(string: &str) -> Result<Vec<Self>, Error> {
string
.split('\n')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(Sprites::parse_single)
.collect()
}
pub fn parse_single(string: &str) -> Result<Self, Error> {
let mut components = string.split_whitespace();
let invalid_data = |info| Error::InvalidData {
info,
context: string.to_string(),
};
let eof = || invalid_data(Some("eof".to_string()));
Ok(Sprites {
file_name: components.next().ok_or_else(eof)?.to_string(),
sprite_type: match components.next().ok_or_else(eof)? {
"anim_rle" => SpriteType::AnimRle,
"anim" => SpriteType::Anim,
"static" => SpriteType::Static,
e => return Err(invalid_data(Some(e.to_string()))),
},
name: components.next().ok_or_else(eof)?.to_string(),
render_mode: match components.next().ok_or_else(eof)? {
"normx" => RenderMode::NormX,
"flipx" => RenderMode::FlipX,
e => return Err(invalid_data(Some(e.to_string()))),
},
frames: if let Some(c) = components.next() {
Some(match c {
"nocrop" => CropMode::NoCrop,
x => x
.parse::<i32>()
.map(CropMode::FrameCount)
.map_err(|err| Error::Custom(Box::new(err)))?,
})
} else {
None
},
})
}
}
#[derive(Debug)]
pub enum CropMode {
FrameCount(i32),
NoCrop,
}
#[derive(Debug)]
pub enum RenderMode {
NormX,
FlipX,
}
#[derive(Debug)]
pub enum SpriteType {
Static,
Anim,
AnimRle,
}

View File

@@ -0,0 +1,101 @@
use std::fmt::{Display, Formatter};
use std::num::ParseIntError;
use std::string::FromUtf8Error;
#[derive(Debug)]
pub enum DecryptError {
FromUtf8Error(FromUtf8Error),
ParseIntError(ParseIntError),
}
impl Display for DecryptError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
DecryptError::FromUtf8Error(error) => write!(f, "{}", error),
DecryptError::ParseIntError(error) => write!(f, "{}", error),
}
}
}
impl std::error::Error for DecryptError {}
impl From<FromUtf8Error> for DecryptError {
fn from(e: FromUtf8Error) -> DecryptError {
DecryptError::FromUtf8Error(e)
}
}
impl From<ParseIntError> for DecryptError {
fn from(e: ParseIntError) -> DecryptError {
DecryptError::ParseIntError(e)
}
}
/// Decrypts txt files contained inside the dat file
pub fn decrypt_txt<I>(buffer: I) -> Result<String, DecryptError>
where
I: Iterator<Item = u8>,
{
let mut key = 0x1234u16;
String::from_utf8(
buffer
.map(|char| {
let decr = char ^ key as u8;
key = key.wrapping_mul(3).wrapping_add(2);
decr
})
.map(|char| (((char >> 1) ^ (char << 1)) & 0x55) ^ (char << 1))
.collect(),
)
.map_err(DecryptError::from)
}
/// Parses a hex string to a Vec<u8>
fn from_hex(line: &str) -> Result<Vec<u8>, ParseIntError> {
(0..line.len())
.step_by(2)
.map(|i| u8::from_str_radix(line.get(i..=i + 1).unwrap_or(""), 16))
.collect()
}
/// This function is applied to *exposed* txt files,
/// such as the player profile or high scores
///
/// If the file is contained in the datafile, it has
/// to first be decrypted normally and then again
/// with this function.
pub fn decrypt_exposed_txt(contents: String) -> Result<String, DecryptError> {
contents
.split_terminator("\r\n")
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.map(from_hex)
.map(|line| decrypt_txt(line.map_err(DecryptError::from)?.into_iter()))
.collect::<Result<Vec<String>, _>>()
.map(|l| l.join("\r\n"))
}
#[cfg(test)]
mod tests {
use crate::media::txt::{decrypt_exposed_txt, decrypt_txt, from_hex};
#[test]
fn it_should_parse_hex() {
assert_eq!(from_hex("abcdef").unwrap(), vec![0xab, 0xcd, 0xef]);
}
#[test]
fn it_should_decrypt() {
let data: Vec<u8> = vec![0x3a, 0x9b, 0x6f, 0x09, 0x7e, 0xd3, 0x74, 0xd6];
assert_eq!(decrypt_txt(data.into_iter()).unwrap(), "\r\nsound ",)
}
#[test]
fn it_should_decrypt_exposed() {
assert_eq!(
decrypt_exposed_txt("83\r\n248ecc86d5d85f6fc6626a6ef5be3e".to_string()).unwrap(),
"{\r\n \"isValid\" 1"
)
}
}

View File

@@ -54,8 +54,8 @@ impl Default for FadeMode {
}
impl UiTag {
pub fn post_process(&mut self) {
if let UiTag::Menu(menu) = self {
pub fn post_process(mut self) -> Self {
if let UiTag::Menu(mut menu) = &self {
let children: Vec<UiTag> = menu.children.drain(..).collect();
let mut area_stack: Vec<Vec<UiTag>> = vec![vec![]];
@@ -84,6 +84,8 @@ impl UiTag {
menu.children = area_stack.pop().unwrap();
debug_assert!(area_stack.is_empty());
}
self
}
}
@@ -102,8 +104,7 @@ mod tests {
#[test]
fn it_should_post_process() {
let mut xml: UiTag = serde_xml_rs::from_str(XML).unwrap();
xml.post_process();
let mut xml = serde_xml_rs::from_str::<UiTag>(XML).unwrap().post_process();
if let UiTag::Menu(UiMenu { children, .. }) = xml {
if let &[UiTag::TextArea(UiTextArea { children, .. })] = &children.as_slice() {