Skip to content

Commit 6523483

Browse files
committed
Initial commit
0 parents  commit 6523483

File tree

5 files changed

+318
-0
lines changed

5 files changed

+318
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
/Cargo.lock

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "rpgmad-lib"
3+
version = "1.0.1"
4+
edition = "2021"
5+
6+
[features]
7+
rayon = ["dep:rayon"]
8+
9+
[dependencies]
10+
rayon = { version = "1.10.0", optional = true }

LICENSE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2+
Version 2, December 2004
3+
4+
Copyright (C) 2024 Pavlov Danil <[email protected]>
5+
6+
Everyone is permitted to copy and distribute verbatim or modified
7+
copies of this license document, and changing it is allowed as long
8+
as the name is changed.
9+
10+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11+
12+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
13+
14+
0. You just DO WHAT THE FUCK YOU WANT TO.

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# rpgm-archive-decrypter-lib
2+
3+
A decrypter implementation for [rpgm-archive-decrypter](https://github.com/savannstm/rpgm-archive-decrypter).
4+
Not intended for use in other applications; but can be.
5+
6+
## Quick example
7+
8+
```rust
9+
let archive_bytes = std::fs::read("C:/Documents/Game/Game.rgssad");
10+
let mut decrypter = rpgmad_lib::Decrypter::new();
11+
12+
// Writes decrypted game files to "C:/Documents/Game"
13+
decrypter.decrypt("C:/Documents/Game", false).unwrap()
14+
```
15+
16+
## License
17+
18+
Project is licensed under WTFPL.

src/lib.rs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
//!A decrypter implementation for rpgm-archive-decrypter. Not intended for use in other applications; but can be.
2+
3+
#[cfg(feature = "rayon")]
4+
use rayon::prelude::*;
5+
#[cfg(feature = "rayon")]
6+
use std::sync::{Arc, Mutex};
7+
use std::{
8+
cell::UnsafeCell,
9+
fs::{create_dir_all, write},
10+
path::{Path, PathBuf},
11+
};
12+
13+
#[derive(PartialEq)]
14+
enum Engine {
15+
Older,
16+
VXAce,
17+
}
18+
19+
enum SeekFrom {
20+
Start,
21+
Current,
22+
}
23+
24+
impl std::fmt::Display for Engine {
25+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26+
let variant_name: &str = match self {
27+
Engine::Older => "XP/VX",
28+
Engine::VXAce => "VXAce",
29+
};
30+
31+
write!(f, "{}", variant_name)
32+
}
33+
}
34+
35+
struct VecWalker {
36+
data: Vec<u8>,
37+
pos: usize,
38+
len: usize,
39+
}
40+
41+
impl VecWalker {
42+
pub fn new(data: Vec<u8>) -> Self {
43+
let len: usize = data.len();
44+
VecWalker { data, pos: 0, len }
45+
}
46+
47+
pub fn advance(&mut self, bytes: usize) -> &[u8] {
48+
self.pos += bytes;
49+
&self.data[self.pos - bytes..self.pos]
50+
}
51+
52+
pub fn read_chunk(&mut self) -> [u8; 4] {
53+
let chunk: &[u8] = self.advance(4);
54+
unsafe { *(chunk.as_ptr() as *const [u8; 4]) }
55+
}
56+
57+
pub fn read_byte(&mut self) -> u8 {
58+
self.pos += 1;
59+
self.data[self.pos - 1]
60+
}
61+
62+
pub fn seek(&mut self, offset: usize, seek_from: SeekFrom) {
63+
self.pos = match seek_from {
64+
SeekFrom::Start => offset,
65+
SeekFrom::Current => self.pos + offset,
66+
};
67+
}
68+
}
69+
70+
struct Archive {
71+
filename: String,
72+
size: i32,
73+
offset: usize,
74+
key: u32,
75+
}
76+
77+
pub struct Decrypter {
78+
walker: UnsafeCell<VecWalker>,
79+
key: u32,
80+
engine: Engine,
81+
}
82+
83+
impl Decrypter {
84+
/// Creates a new decrypter for specified archive binary data.
85+
pub fn new(bytes: Vec<u8>) -> Self {
86+
Self {
87+
walker: UnsafeCell::new(VecWalker::new(bytes)),
88+
key: 0xDEADCAFE,
89+
engine: Engine::Older,
90+
}
91+
}
92+
93+
/// Extracts archive to `output_path`. Does nothing if extracted files already exist and `force` is set to `false`.
94+
pub fn extract<P: AsRef<Path>>(&mut self, output_path: P, force: bool) -> Result<(), &str> {
95+
let walker: &mut VecWalker = unsafe { &mut *self.walker.get() };
96+
97+
let version: u8 = {
98+
let header: &[u8] = walker.advance(6);
99+
100+
if header != b"RGSSAD" {
101+
return Err("Unknown archive header. Expected: RGSSAD.");
102+
}
103+
104+
walker.seek(1, SeekFrom::Current);
105+
walker.read_byte()
106+
};
107+
108+
self.engine = if version == 1 {
109+
Engine::Older
110+
} else if version == 3 {
111+
Engine::VXAce
112+
} else {
113+
return Err("Unknown archive game engine. Archive is possibly corrupted.");
114+
};
115+
116+
let archives: Vec<Archive> = self.read_archive();
117+
118+
#[cfg(feature = "rayon")]
119+
let arc: Arc<Mutex<&mut VecWalker>> = Arc::new(Mutex::new(walker));
120+
121+
#[cfg(feature = "rayon")]
122+
let archives = archives.into_par_iter();
123+
124+
#[cfg(not(feature = "rayon"))]
125+
let archives = archives.into_iter();
126+
127+
let output_path: &Path = output_path.as_ref();
128+
129+
archives.for_each(|archive: Archive| {
130+
let output_path: PathBuf = output_path.join(archive.filename);
131+
132+
if output_path.exists() && !force {
133+
println!("Output files already exist. Use --force to forcefully overwrite them.");
134+
return;
135+
}
136+
137+
#[cfg(feature = "rayon")]
138+
let mut walker = arc.lock().unwrap();
139+
140+
walker.seek(archive.offset, SeekFrom::Start);
141+
let data: Vec<u8> = Vec::from(walker.advance(archive.size as usize));
142+
143+
#[cfg(feature = "rayon")]
144+
drop(walker);
145+
146+
let parent_directory: &Path = unsafe { output_path.parent().unwrap_unchecked() };
147+
148+
if !parent_directory.exists() {
149+
create_dir_all(parent_directory).unwrap();
150+
}
151+
152+
let decrypted: Vec<u8> = Self::decrypt_archive(&data, archive.key);
153+
write(output_path, decrypted).unwrap();
154+
});
155+
156+
Ok(())
157+
}
158+
159+
fn decrypt_archive(data: &[u8], mut key: u32) -> Vec<u8> {
160+
let mut decrypted: Vec<u8> = Vec::with_capacity(data.len());
161+
162+
let mut key_bytes: [u8; 4] = key.to_le_bytes();
163+
let mut j: usize = 0;
164+
165+
for item in data {
166+
if j == 4 {
167+
j = 0;
168+
key = key.wrapping_mul(7).wrapping_add(3);
169+
key_bytes = key.to_le_bytes();
170+
}
171+
172+
decrypted.push(item ^ key_bytes[j]);
173+
j += 1;
174+
}
175+
176+
decrypted
177+
}
178+
179+
fn decrypt_integer(&mut self, value: i32) -> i32 {
180+
let result: i32 = value ^ self.key as i32;
181+
182+
if self.engine == Engine::Older {
183+
self.key = self.key.wrapping_mul(7).wrapping_add(3);
184+
}
185+
186+
result
187+
}
188+
189+
fn decrypt_filename(&mut self, filename: &[u8]) -> String {
190+
let mut decrypted: Vec<u8> = Vec::with_capacity(filename.len());
191+
192+
if self.engine == Engine::VXAce {
193+
let key_bytes: [u8; 4] = self.key.to_le_bytes();
194+
let mut j: usize = 0;
195+
196+
for item in filename {
197+
if j == 4 {
198+
j = 0;
199+
}
200+
201+
decrypted.push(item ^ key_bytes[j]);
202+
j += 1;
203+
}
204+
} else {
205+
for item in filename {
206+
decrypted.push(item ^ (self.key & 0xff) as u8);
207+
self.key = self.key.wrapping_mul(7).wrapping_add(3);
208+
}
209+
}
210+
211+
String::from_utf8(decrypted).unwrap()
212+
}
213+
214+
fn read_archive(&mut self) -> Vec<Archive> {
215+
let walker: &mut VecWalker = unsafe { &mut *self.walker.get() };
216+
217+
if self.engine == Engine::VXAce {
218+
// 0xDEADCAFE key is not ever used and overwritten.
219+
self.key = u32::from_le_bytes(walker.read_chunk())
220+
.wrapping_mul(9)
221+
.wrapping_add(3);
222+
}
223+
224+
let mut archives: Vec<Archive> = Vec::with_capacity(1024);
225+
226+
loop {
227+
let (filename, size, offset, key) = if self.engine == Engine::VXAce {
228+
let offset: usize =
229+
self.decrypt_integer(i32::from_le_bytes(walker.read_chunk())) as usize;
230+
231+
let size: i32 = self.decrypt_integer(i32::from_le_bytes(walker.read_chunk()));
232+
233+
let key: u32 = self.decrypt_integer(i32::from_le_bytes(walker.read_chunk())) as u32;
234+
235+
let length: i32 = self.decrypt_integer(i32::from_le_bytes(walker.read_chunk()));
236+
237+
if offset == 0 {
238+
break;
239+
}
240+
241+
let filename: String = self.decrypt_filename(walker.advance(length as usize));
242+
243+
(filename, size, offset, key)
244+
} else {
245+
let length: i32 = self.decrypt_integer(i32::from_le_bytes(walker.read_chunk()));
246+
247+
let filename: String = self.decrypt_filename(walker.advance(length as usize));
248+
249+
let size: i32 = self.decrypt_integer(i32::from_le_bytes(walker.read_chunk()));
250+
251+
let offset: usize = walker.pos;
252+
253+
let key: u32 = self.key;
254+
255+
walker.seek(size as usize, SeekFrom::Current);
256+
257+
if walker.pos == walker.len {
258+
break;
259+
}
260+
261+
(filename, size, offset, key)
262+
};
263+
264+
archives.push(Archive {
265+
filename,
266+
size,
267+
offset,
268+
key,
269+
});
270+
}
271+
272+
archives
273+
}
274+
}

0 commit comments

Comments
 (0)