mirror of
https://github.com/Theaninova/mhlib.git
synced 2026-01-09 19:42:53 +00:00
add archive tests
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
rust/springylib/src/error.rs
Normal file
66
rust/springylib/src/error.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
rust/springylib/src/media/font/charset-utf8.txt
Normal file
1
rust/springylib/src/media/font/charset-utf8.txt
Normal file
@@ -0,0 +1 @@
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜabcdefghijklmnopqrstuvwxyzäöüß0123456789,;.:!?+-*/=<>()[]{}\"$%&#~_’^@|¡¿™©®º¹²³ªÀÁÂÃÅÆÇÈÉÊËÌÍÎÏIÐGÑÒÓÔÕŒØSŠÙÚÛÝÞŸŽàáâãåæçèéêëìíîïiðgñòóôõœøsšùúûýþÿž£¥ƒ¤¯¦¬¸¨·§×¢±÷µ«»
|
||||
1
rust/springylib/src/media/font/charset.txt
Normal file
1
rust/springylib/src/media/font/charset.txt
Normal 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>
|
||||
2
rust/springylib/src/media/font/mod.rs
Normal file
2
rust/springylib/src/media/font/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub const CHARSET: &[u8] = include_bytes!("charset.txt");
|
||||
pub const CHARSET_UTF8: &str = include_str!("charset-utf8.txt");
|
||||
19
rust/springylib/src/media/level.rs
Normal file
19
rust/springylib/src/media/level.rs
Normal 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>,
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
pub mod font;
|
||||
pub mod level;
|
||||
pub mod rle;
|
||||
pub mod sprites;
|
||||
pub mod txt;
|
||||
pub mod ui;
|
||||
|
||||
29
rust/springylib/src/media/rle/gif.rs
Normal file
29
rust/springylib/src/media/rle/gif.rs
Normal 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)),
|
||||
))
|
||||
})))
|
||||
}
|
||||
}
|
||||
98
rust/springylib/src/media/rle/mod.rs
Normal file
98
rust/springylib/src/media/rle/mod.rs
Normal 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]]
|
||||
}
|
||||
76
rust/springylib/src/media/sprites.rs
Normal file
76
rust/springylib/src/media/sprites.rs
Normal 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,
|
||||
}
|
||||
101
rust/springylib/src/media/txt.rs
Normal file
101
rust/springylib/src/media/txt.rs
Normal 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user