use std::collections::BTreeMap;
use std::error::Error;
use std::fs;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::io::BufReader;
use std::io::SeekFrom;
use std::path::Path;
use encoding::all::ISO_8859_1;
use encoding::{EncoderTrap, Encoding};
use flate2::bufread::ZlibDecoder;
use serde::Deserialize;
pub type IntLen = u64;
pub type RpaIdxColl = BTreeMap<String, Vec<RpaEntry>>;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum RpaVersion {
V3,
V3_2,
V2,
}
#[derive(Debug)]
pub struct RenpyArchive<'a> {
reader: BufReader<File>,
path: &'a Path,
version: RpaVersion,
indices: RpaIdx,
pad_len: u16,
key: ObfuscationKey,
}
#[derive(Debug, Deserialize, Clone)]
pub struct RpaIdx(RpaIdxColl);
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum RpaEntry {
V2(RpaEntryv2),
V3(RpaEntryv3),
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
pub struct RpaEntryv3 {
offset: IntLen,
len: IntLen,
prefix: String,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
pub struct RpaEntryv2 {
offset: IntLen,
len: IntLen,
}
#[derive(Debug, Clone, Copy)]
pub struct ObfuscationKey(IntLen);
impl<'a> RenpyArchive<'a> {
pub fn from_file(path: &'a Path) -> Result<Self, Box<dyn Error>> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut first_line = String::new();
reader.read_line(&mut first_line)?;
let literal: Vec<_> = first_line.split_ascii_whitespace().collect();
let rpa_version = Self::get_rpa_version(literal[0]);
let (mut indices, obfuscation_key) = Self::extract_metadata(&rpa_version, &mut reader)?;
if let RpaVersion::V3 | RpaVersion::V3_2 = rpa_version {
Self::deobfuscate_indices(&mut indices, &obfuscation_key);
}
Ok(Self {
reader,
path,
version: rpa_version,
indices,
pad_len: 0,
key: obfuscation_key,
})
}
pub fn indices_map(&self) -> &RpaIdxColl {
&self.indices.0
}
pub fn list_indices(&self) -> Vec<String> {
self.indices.0.keys().cloned().collect()
}
fn get_rpa_version<S: AsRef<str>>(magic_literal: S) -> RpaVersion {
match magic_literal.as_ref() {
"RPA-3.2" => RpaVersion::V3_2,
"RPA-3.0" => RpaVersion::V3,
"RPA-2.0" => RpaVersion::V2,
unsupported => panic!("Unsupported RPA version '{}'", unsupported),
}
}
fn extract_metadata(
rpa_version: &RpaVersion,
reader: &mut BufReader<File>,
) -> Result<(RpaIdx, ObfuscationKey), Box<dyn Error>> {
let mut contents = String::new();
reader.seek(SeekFrom::Start(0))?;
reader.read_line(&mut contents)?;
let metadata: Vec<&str> = contents.split_ascii_whitespace().collect();
let offset = IntLen::from_str_radix(metadata[1], 16)?;
let key = Self::construct_obfuscation_key(rpa_version, &metadata);
reader.seek(SeekFrom::Start(offset))?;
let mut bytes: Vec<u8> = Vec::new();
let bytes_read = reader.read_to_end(&mut bytes)?;
let mut decoded_bytes: Vec<u8> = Vec::with_capacity(2 * bytes_read);
ZlibDecoder::new(&bytes[..]).read_to_end(&mut decoded_bytes)?;
let deserialized_indices: RpaIdx = serde_pickle::from_slice(&decoded_bytes)?;
Ok((deserialized_indices, ObfuscationKey(key)))
}
fn construct_obfuscation_key<S: AsRef<str>>(
rpa_version: &RpaVersion,
metadata: &[S],
) -> IntLen {
let key: IntLen = match *rpa_version {
RpaVersion::V3 => metadata.as_ref()[2..]
.iter()
.fold(0, |acc: IntLen, sub_key| {
acc ^ IntLen::from_str_radix(sub_key.as_ref(), 16).unwrap()
}),
RpaVersion::V3_2 => metadata.as_ref()[3..]
.iter()
.fold(0, |acc: IntLen, sub_key| {
acc ^ IntLen::from_str_radix(sub_key.as_ref(), 16).unwrap()
}),
RpaVersion::V2 => 0,
};
key
}
pub fn read_file_from_archive<S: AsRef<str>>(
&mut self,
filename: S,
) -> Result<Vec<u8>, Box<dyn Error>> {
let rpa_idx = match self.indices.0.get(filename.as_ref()) {
Some(idx) => Ok(idx),
None => Err(format!("No entry for key '{}'", filename.as_ref())),
}?;
let rpa_idx = &rpa_idx[0];
let (offset, len, prefix) = match rpa_idx {
RpaEntry::V3(rpa_v3) => (rpa_v3.offset, rpa_v3.len, Some(rpa_v3.prefix.as_str())),
RpaEntry::V2(rpa_v2) => (rpa_v2.offset, rpa_v2.len, None),
};
println!(
"Reading file '{}' from archive '{}' (offset: {}, len: {} bytes).",
filename.as_ref(),
self.path.display(),
offset,
len
);
self.reader.seek(SeekFrom::Start(offset))?;
let mut encoded_prefix = ISO_8859_1.encode(prefix.unwrap_or(""), EncoderTrap::Strict)?;
let desired_capacity = len as usize - prefix.unwrap_or("").len();
let mut buf = vec![0u8; desired_capacity];
self.reader.read_exact(&mut buf)?;
assert_eq!(desired_capacity, buf.len());
println!("Successfully read {} bytes.", buf.len());
if self.version == RpaVersion::V3 && !encoded_prefix.is_empty() {
encoded_prefix.append(&mut buf);
Ok(encoded_prefix)
} else {
Ok(buf)
}
}
pub fn write_file<S: AsRef<str>>(&self, filepath: S, file_buf: &[u8]) -> io::Result<()> {
let path = Path::new(filepath.as_ref());
if let Some(parent_dirs) = path.parent() {
if !parent_dirs.exists() {
println!(
"Creating path '{}' before extracting.",
parent_dirs.display()
);
fs::create_dir_all(parent_dirs)?;
}
}
print!("Writing file '{}' to disk...", filepath.as_ref());
let mut file = File::create(path)?;
file.write_all(&file_buf)?;
println!("Done.");
Ok(())
}
fn deobfuscate_indices(rpa_idx: &mut RpaIdx, key: &ObfuscationKey) {
let key = key.0;
let deobfuscate = |rpa_entry: &mut RpaEntry| match rpa_entry {
RpaEntry::V2(rpa_entry) => {
rpa_entry.offset ^= key;
rpa_entry.len ^= key;
}
RpaEntry::V3(rpa_entry) => {
rpa_entry.offset ^= key;
rpa_entry.len ^= key;
}
};
for rpa_list in rpa_idx.0.values_mut() {
for rpa_entry in rpa_list.iter_mut() {
deobfuscate(rpa_entry);
}
}
}
}
impl RpaEntry {
pub fn with_prefix<S: AsRef<str>>(offset: IntLen, len: IntLen, prefix: S) -> Self {
RpaEntry::V3(RpaEntryv3 {
offset,
len,
prefix: String::from(prefix.as_ref()),
})
}
}