Implement decompression of .rpgsave files
This commit is contained in:
parent
17672ce114
commit
30d4a861f2
|
@ -62,12 +62,18 @@ version = "0.2.71"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
|
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lz-string"
|
||||||
|
version = "0.0.1"
|
||||||
|
source = "git+https://github.com/adumbidiot/lz-string-rs#f968dd54b0b7d0a560235f50f204557d84d1f0c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpgmv_decryptor"
|
name = "rpgmv_decryptor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
"lz-string",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -9,3 +9,4 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.31"
|
anyhow = "1.0.31"
|
||||||
clap = "2.33.1"
|
clap = "2.33.1"
|
||||||
|
lz-string = { git = "https://github.com/adumbidiot/lz-string-rs" }
|
256
src/main.rs
256
src/main.rs
|
@ -1,11 +1,10 @@
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use clap::{App, Arg};
|
use clap::{App, Arg};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write};
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// # RPGMV Decryptor
|
/// # RPGMV Decryptor
|
||||||
///
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let matches = App::new("RPGMV Decoder")
|
let matches = App::new("RPGMV Decoder")
|
||||||
.version("0.1.0")
|
.version("0.1.0")
|
||||||
|
@ -25,122 +24,195 @@ fn main() -> Result<()> {
|
||||||
.long("key")
|
.long("key")
|
||||||
.value_name("KEY")
|
.value_name("KEY")
|
||||||
.help("The Encryption Key (look in www/data/System.json or www/js/rpg_core.js")
|
.help("The Encryption Key (look in www/data/System.json or www/js/rpg_core.js")
|
||||||
.takes_value(true)
|
.takes_value(true),
|
||||||
.required(true),
|
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let path_string = matches.value_of("path").context("No file path provided")?;
|
let path_string = matches.value_of("path").context("No file path provided")?;
|
||||||
let key_string = matches.value_of("key").context("No key provided")?;
|
|
||||||
|
|
||||||
let path = Path::new(path_string);
|
let path = Path::new(path_string);
|
||||||
let key = get_key_bytes(key_string)?;
|
|
||||||
|
|
||||||
// let default_rpgmv_header: [u8; 16] = [
|
|
||||||
// 0x52, 0x50, 0x47, 0x4d, 0x56, 0x00, 0x00, 0x00, 0x00, 0x3, 0x1, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 0x00,
|
|
||||||
// ];
|
|
||||||
|
|
||||||
let file_buf = get_file_data(path)?;
|
|
||||||
let decrypted = decrypt(file_buf, key);
|
|
||||||
|
|
||||||
let mut file;
|
|
||||||
|
|
||||||
let filename = path
|
let filename = path
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.context("Unable to determine file stem")?
|
.context("Unable to determine file stem")?
|
||||||
.to_str()
|
.to_str()
|
||||||
.context("Unable to convert file stem to UTF-8 string")?;
|
.context("file stem is not valid UTF-8")?;
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
|
||||||
match detect_original_extension(path)? {
|
match path.extension() {
|
||||||
RPGFile::Photo => file = File::create(format!("{}.png", filename))?, // .rpgmvps are .pngs
|
Some(ext) => match ext.to_str() {
|
||||||
RPGFile::Video => file = File::create(format!("{}.m4a", filename))?, // .rpgmvms are .m4as
|
Some("rpgsave") => {
|
||||||
RPGFile::Audio => file = File::create(format!("{}.ogg", filename))?, // .rpgmvos are .oggs
|
use rpgmv_save::*;
|
||||||
}
|
|
||||||
|
|
||||||
file.write_all(&decrypted)?;
|
let decompressed = decompress_save(path)?;
|
||||||
|
write_decompressed_save(format!("{}.json", filename), &decompressed)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
|
|
||||||
/// Determines the original file type for the encrypted .rpgmv* file
|
|
||||||
fn detect_original_extension<P: AsRef<Path>>(path: P) -> Result<RPGFile> {
|
|
||||||
match path.as_ref().extension() {
|
|
||||||
Some(ext) => {
|
|
||||||
let ext = ext
|
|
||||||
.to_str()
|
|
||||||
.context("File extension was not UTF-8 Compatible")?;
|
|
||||||
|
|
||||||
match ext {
|
|
||||||
"rpgmvp" => Ok(RPGFile::Photo),
|
|
||||||
"rpgmvm" => Ok(RPGFile::Video),
|
|
||||||
"rpgmvo" => Ok(RPGFile::Audio),
|
|
||||||
_ => Err(anyhow!("File was lacking a supported file extension")),
|
|
||||||
}
|
}
|
||||||
|
Some("rpgmvp") | Some("rpgmvm") | Some("rpgmvo") => {
|
||||||
|
use rpgmv_media::*;
|
||||||
|
|
||||||
|
let key_string = matches.value_of("key").context("No key provided")?;
|
||||||
|
let key = get_key_bytes(key_string)?;
|
||||||
|
|
||||||
|
// let default_rpgmv_header: [u8; 16] = [
|
||||||
|
// 0x52, 0x50, 0x47, 0x4d, 0x56, 0x00, 0x00, 0x00, 0x00, 0x3, 0x1, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 0x00,
|
||||||
|
// ];
|
||||||
|
|
||||||
|
let file_buf = get_file_data(path)?;
|
||||||
|
let decrypted = decrypt(file_buf, key);
|
||||||
|
|
||||||
|
let mut file;
|
||||||
|
|
||||||
|
match detect_original_extension(path)? {
|
||||||
|
RPGFile::Photo => file = File::create(format!("{}.png", filename))?, // .rpgmvps are .pngs
|
||||||
|
RPGFile::Video => file = File::create(format!("{}.m4a", filename))?, // .rpgmvms are .m4as
|
||||||
|
RPGFile::Audio => file = File::create(format!("{}.ogg", filename))?, // .rpgmvos are .oggs
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(file.write_all(&decrypted)?)
|
||||||
|
}
|
||||||
|
Some(ext_str) => Err(anyhow!("{} is not a supported .ext", ext_str)),
|
||||||
|
None => Err(anyhow!("{}'s .ext is not valid UTF-8", path_str)),
|
||||||
|
},
|
||||||
|
None => Err(anyhow!("Unable to determine the extension of {}", path_str)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod rpgmv_save {
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use std::char::decode_utf16;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn decompress_save<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||||
|
let mut save_file = File::open(path.as_ref())?;
|
||||||
|
let mut base64_string = String::new();
|
||||||
|
save_file.read_to_string(&mut base64_string)?;
|
||||||
|
|
||||||
|
let maybe_compressed = convert_base64_string(&base64_string);
|
||||||
|
|
||||||
|
match maybe_compressed {
|
||||||
|
Some(compressed) => Ok(lz_string::decompress(&compressed, 32)
|
||||||
|
.ok_or_else(|| anyhow!("Failed to decompress .rpgsave"))?),
|
||||||
|
None => Err(anyhow!("Failed to convert the .rpgsave from base64")),
|
||||||
}
|
}
|
||||||
None => Err(anyhow!("File is lacking a file extension")),
|
}
|
||||||
|
|
||||||
|
pub fn write_decompressed_save<P: AsRef<Path>>(path: P, decompressed: &str) -> Result<()> {
|
||||||
|
let mut out = File::create(path.as_ref())?;
|
||||||
|
out.write_all(decompressed.as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_base64_string(base64: &str) -> Option<String> {
|
||||||
|
let codes = convert_to_utf16_char_codes(base64)?;
|
||||||
|
|
||||||
|
let res = decode_utf16(codes.iter().cloned())
|
||||||
|
.map(|res| res.expect("UTF-16 Decoding failed"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_to_utf16_char_codes(base64: &str) -> Option<Vec<u16>> {
|
||||||
|
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||||
|
let mut codes: Vec<u16> = Vec::with_capacity(base64.chars().count());
|
||||||
|
|
||||||
|
for c in base64.chars() {
|
||||||
|
codes.push(alphabet.find(c)? as u16);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(codes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get_key_bytes takes a 32-character hexadecimal string and interperets
|
mod rpgmv_media {
|
||||||
/// every two characters as an unsined 8-bit integer.
|
use anyhow::{anyhow, Context, Result};
|
||||||
///
|
use std::fs::File;
|
||||||
/// # Examples
|
use std::io::Read;
|
||||||
/// `71e7bdd9b28010980c2a4e8a1386e100` will be split up as:
|
use std::path::Path;
|
||||||
///
|
|
||||||
/// `0x71`, `0xe7`, `0xbd`, `0xd9`, `0xb2`, `0x80`, `0x10`, `0x98`, `0x0c`, `0x2a`, `0x4e`, `0x8a`, `0x13`, `0x86`, `0xe1`, and `0x00`
|
|
||||||
fn get_key_bytes(key: &str) -> Result<Vec<u8>> {
|
|
||||||
let sub_len = 2;
|
|
||||||
|
|
||||||
let mut key_buf = Vec::with_capacity(key.len() / 2);
|
/// get_key_bytes takes a 32-character hexadecimal string and interperets
|
||||||
|
/// every two characters as an unsined 8-bit integer.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// `71e7bdd9b28010980c2a4e8a1386e100` will be split up as:
|
||||||
|
///
|
||||||
|
/// `0x71`, `0xe7`, `0xbd`, `0xd9`, `0xb2`, `0x80`, `0x10`, `0x98`, `0x0c`, `0x2a`, `0x4e`, `0x8a`, `0x13`, `0x86`, `0xe1`, and `0x00`
|
||||||
|
pub fn get_key_bytes(key: &str) -> Result<Vec<u8>> {
|
||||||
|
let sub_len = 2;
|
||||||
|
|
||||||
let mut chars = key.chars();
|
let mut key_buf = Vec::with_capacity(key.len() / 2);
|
||||||
let sub_strings = (0..)
|
|
||||||
.map(|_| chars.by_ref().take(sub_len).collect::<String>())
|
|
||||||
.take_while(|s| !s.is_empty())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
for string in sub_strings {
|
let mut chars = key.chars();
|
||||||
key_buf.push(u8::from_str_radix(&string, 16)?);
|
let sub_strings = (0..)
|
||||||
|
.map(|_| chars.by_ref().take(sub_len).collect::<String>())
|
||||||
|
.take_while(|s| !s.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for string in sub_strings {
|
||||||
|
key_buf.push(u8::from_str_radix(&string, 16)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(key_buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(key_buf)
|
/// get_file_data returns a buffer containing a data of a file which is on disk.
|
||||||
}
|
pub fn get_file_data<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
/// get_file_data returns a buffer containing a data of a file which is on disk.
|
let mut file = File::open(path.as_ref())?;
|
||||||
fn get_file_data<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
|
file.read_to_end(&mut buf)?;
|
||||||
let mut buf = Vec::new();
|
|
||||||
|
|
||||||
let mut file = File::open(path.as_ref())?;
|
Ok(buf)
|
||||||
file.read_to_end(&mut buf)?;
|
|
||||||
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _encrypt(_bytes: Vec<u8>, _key: Vec<u8>) -> Vec<u8> {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypts a RPGMV media file
|
|
||||||
///
|
|
||||||
/// The only part of the data that is actually encrypted (read: has a cipher applied to it)
|
|
||||||
/// is the header of the original file, which is bytes 17 -> 33 (16 bytes). Our key provides us with
|
|
||||||
/// 16 values which we can use to perform a bitwise XOR. The bitwise XOR will undo the cipher, returning the header to
|
|
||||||
/// it's original position. At this point, byte #17 -> end of file will now be recognized as their respected unencrypted
|
|
||||||
// file formats.
|
|
||||||
fn decrypt(bytes: Vec<u8>, key: Vec<u8>) -> Vec<u8> {
|
|
||||||
let mut decrypted = bytes[16..].to_owned(); // Throw out the first 16 bytes (the RPGMV Header)
|
|
||||||
|
|
||||||
for i in 0..16 as usize {
|
|
||||||
decrypted[i as usize] = decrypted[i] ^ key[i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypted
|
fn _encrypt(_bytes: Vec<u8>, _key: Vec<u8>) -> Vec<u8> {
|
||||||
}
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
/// An enum that represents the types of supported RPGMV media files.
|
/// Decrypts a RPGMV media file
|
||||||
enum RPGFile {
|
///
|
||||||
Photo,
|
/// The only part of the data that is actually encrypted (read: has a cipher applied to it)
|
||||||
Video,
|
/// is the header of the original file, which is bytes 17 -> 33 (16 bytes). Our key provides us with
|
||||||
Audio,
|
/// 16 values which we can use to perform a bitwise XOR. The bitwise XOR will undo the cipher, returning the header to
|
||||||
|
/// it's original position. At this point, byte #17 -> end of file will now be recognized as their respected unencrypted
|
||||||
|
// file formats.
|
||||||
|
pub fn decrypt(bytes: Vec<u8>, key: Vec<u8>) -> Vec<u8> {
|
||||||
|
let mut decrypted = bytes[16..].to_owned(); // Throw out the first 16 bytes (the RPGMV Header)
|
||||||
|
|
||||||
|
for i in 0..16 as usize {
|
||||||
|
decrypted[i as usize] = decrypted[i] ^ key[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the original file type for the encrypted .rpgmv* file
|
||||||
|
pub fn detect_original_extension<P: AsRef<Path>>(path: P) -> Result<RPGFile> {
|
||||||
|
match path.as_ref().extension() {
|
||||||
|
Some(ext) => {
|
||||||
|
let ext = ext
|
||||||
|
.to_str()
|
||||||
|
.context("File extension was not UTF-8 Compatible")?;
|
||||||
|
|
||||||
|
match ext {
|
||||||
|
"rpgmvp" => Ok(RPGFile::Photo),
|
||||||
|
"rpgmvm" => Ok(RPGFile::Video),
|
||||||
|
"rpgmvo" => Ok(RPGFile::Audio),
|
||||||
|
_ => Err(anyhow!("File was lacking a supported file extension")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(anyhow!("File is lacking a file extension")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An enum that represents the types of supported RPGMV media files.
|
||||||
|
pub enum RPGFile {
|
||||||
|
Photo,
|
||||||
|
Video,
|
||||||
|
Audio,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue