rpgmad_lib/
lib.rs

1//! # rpgm-archive-decrypter-lib
2//!
3//! Library for decrypting RPG Maker `rgss` archives.
4//!
5//! Used in [rpgm-archive-decrypter](https://github.com/savannstm/rpgm-archive-decrypter).
6//!
7//! ## Quick example
8//!
9//! ```no_run
10//! use rpgmad_lib::{Decrypter, extract_archive};
11//!
12//! // Using Decrypter struct
13//! let archive_content: Vec<u8> = std::fs::read("C:/Game/Game.rgss3a").unwrap();
14//! let mut decrypter = Decrypter::new();
15//!
16//! // You can optionally set force
17//! // decrypter.set_force(true)
18//!
19//! decrypter.extract(&archive_content, "C:/Game").unwrap();
20//!
21//! // Using function
22//! let force = false; // When `true`, it will overwrite existing files in the game directory.
23//! extract_archive(&archive_content, "C:/Game", force).unwrap();
24//! ```
25//!
26//! ## License
27//!
28//! Project is licensed under WTFPL.
29
30use std::{
31    fs::{create_dir_all, write},
32    path::{Path, PathBuf},
33};
34use thiserror::Error;
35
36const ARCHIVE_HEADER: &[u8; 6] = b"RGSSAD";
37const OLDER_DEFAULT_KEY: u32 = 0xDEADCAFE;
38
39#[derive(Error, Debug)]
40pub enum ExtractError {
41    #[error("Invalid archive file header: {found:?}. Expected: RGSSAD ([82, 71, 83, 83, 65, 68])")]
42    InvalidHeader { found: [u8; 6] },
43    #[error("Invalid game engine byte: {found}. Expected `1` for XP/VX or `3` for VX Ace.")]
44    InvalidEngine { found: u8 },
45}
46
47pub enum ExtractOutcome {
48    Extracted,
49    FilesExist,
50}
51
52#[derive(PartialEq)]
53enum EngineType {
54    Older,
55    VXAce,
56}
57
58enum SeekFrom {
59    Start,
60    Current,
61}
62
63impl std::fmt::Display for EngineType {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            EngineType::Older => write!(f, "XP/VX"),
67            EngineType::VXAce => write!(f, "VXAce"),
68        }
69    }
70}
71
72#[derive(Default)]
73struct ArchiveEntry {
74    filename_bytes: Vec<u8>,
75    size: i32,
76    offset: usize,
77    key: u32,
78}
79
80/// A struct responsible for decrypting and extracting files from encrypted game archives.
81pub struct Decrypter<'a> {
82    data: &'a [u8],
83    pos: usize,
84    len: usize,
85    force: bool,
86    engine_type: EngineType,
87    key: u32,
88    key_bytes: [u8; 4],
89}
90
91impl<'a> Decrypter<'a> {
92    /// Creates a new `Decrypter` with empty buffer, and `force` set to `false`.
93    pub fn new() -> Self {
94        Self {
95            data: &[],
96            pos: 0,
97            len: 0,
98
99            force: false,
100
101            engine_type: EngineType::Older,
102            key: OLDER_DEFAULT_KEY,
103            key_bytes: OLDER_DEFAULT_KEY.to_le_bytes(),
104        }
105    }
106
107    /// Enables or disables forced overwrite during extraction.
108    ///
109    /// When `enabled` is `true`, extracted files will overwrite existing files on disk.
110    ///
111    /// Returns self.
112    #[inline]
113    pub fn force(mut self, enabled: bool) -> Self {
114        self.force = enabled;
115        self
116    }
117
118    /// Enables or disables forced overwrite during extraction.
119    ///
120    /// When `enabled` is `true`, extracted files will overwrite existing files on disk.
121    #[inline]
122    pub fn set_force(&mut self, enabled: bool) {
123        self.force = enabled;
124    }
125
126    #[inline]
127    fn update_key(&mut self, new_key: u32) {
128        self.key = new_key;
129        self.key_bytes = new_key.to_le_bytes();
130    }
131
132    #[inline]
133    fn read_bytes(&mut self, bytes: usize) -> &[u8] {
134        self.pos += bytes;
135        &self.data[self.pos - bytes..self.pos]
136    }
137
138    #[inline]
139    fn read_int(&mut self) -> i32 {
140        let chunk: &[u8] = self.read_bytes(4);
141        i32::from_le_bytes(unsafe { *(chunk.as_ptr() as *const [u8; 4]) })
142    }
143
144    #[inline]
145    fn read_byte(&mut self) -> u8 {
146        self.pos += 1;
147        self.data[self.pos - 1]
148    }
149
150    #[inline]
151    fn seek_byte(&mut self, offset: usize, seek_from: SeekFrom) {
152        self.pos = match seek_from {
153            SeekFrom::Start => offset,
154            SeekFrom::Current => self.pos + offset,
155        };
156    }
157
158    #[inline]
159    fn decrypt_entry(&mut self, entry: &ArchiveEntry) -> Vec<u8> {
160        let mut key = entry.key;
161        let mut key_bytes: [u8; 4] = key.to_le_bytes();
162        let mut key_byte_pos: usize = 0;
163
164        self.seek_byte(entry.offset, SeekFrom::Start);
165
166        let content = self.read_bytes(entry.size as usize);
167        let mut decrypted: Vec<u8> = Vec::with_capacity(content.len());
168
169        for item in content {
170            if key_byte_pos == 4 {
171                key_byte_pos = 0;
172                key = key.wrapping_mul(7).wrapping_add(3);
173                key_bytes = key.to_le_bytes();
174            }
175
176            decrypted.push(item ^ key_bytes[key_byte_pos]);
177            key_byte_pos += 1;
178        }
179
180        decrypted
181    }
182
183    #[inline]
184    fn decrypt_int(&mut self) -> i32 {
185        let int: i32 = self.read_int();
186        let result: i32 = int ^ self.key as i32;
187
188        if self.engine_type == EngineType::Older {
189            self.update_key(self.key.wrapping_mul(7).wrapping_add(3));
190        }
191
192        result
193    }
194
195    #[inline]
196    fn decrypt_filename(&mut self, entry: &mut ArchiveEntry) {
197        let filename_bytes =
198            unsafe { &*(self.read_bytes(entry.filename_bytes.capacity()) as *const [u8]) };
199
200        if self.engine_type == EngineType::VXAce {
201            let mut key_byte_pos: usize = 0;
202
203            for byte in filename_bytes {
204                if key_byte_pos == 4 {
205                    key_byte_pos = 0;
206                }
207
208                entry
209                    .filename_bytes
210                    .push(byte ^ self.key_bytes[key_byte_pos]);
211                key_byte_pos += 1;
212            }
213        } else {
214            for byte in filename_bytes {
215                entry.filename_bytes.push(byte ^ self.key as u8);
216                self.update_key(self.key.wrapping_mul(7).wrapping_add(3));
217            }
218        }
219    }
220
221    #[inline]
222    fn parse_header(&mut self) -> Result<(), ExtractError> {
223        let header: &[u8] = self.read_bytes(6);
224
225        if header != ARCHIVE_HEADER {
226            return Err(ExtractError::InvalidHeader {
227                found: unsafe { *(header.as_ptr() as *const [u8; 6]) },
228            });
229        }
230
231        self.seek_byte(1, SeekFrom::Current);
232        let engine_type: u8 = self.read_byte();
233
234        self.engine_type = match engine_type {
235            1 => EngineType::Older,
236            3 => EngineType::VXAce,
237            _ => return Err(ExtractError::InvalidEngine { found: engine_type }),
238        };
239
240        Ok(())
241    }
242
243    #[inline]
244    fn extract_entries(&mut self) -> Vec<ArchiveEntry> {
245        if self.engine_type == EngineType::VXAce {
246            // Default key is not ever used and overwritten.
247            let key = self.read_int() as u32;
248            self.update_key(key.wrapping_mul(9).wrapping_add(3));
249        }
250
251        let mut entries = Vec::with_capacity(16384);
252
253        loop {
254            let mut entry: ArchiveEntry = ArchiveEntry::default();
255
256            match self.engine_type {
257                EngineType::VXAce => {
258                    entry.offset = self.decrypt_int() as usize;
259
260                    if entry.offset == 0 {
261                        break;
262                    }
263
264                    entry.size = self.decrypt_int();
265                    entry.key = self.decrypt_int() as u32;
266
267                    entry
268                        .filename_bytes
269                        .reserve_exact(self.decrypt_int() as usize);
270                    self.decrypt_filename(&mut entry);
271                }
272                EngineType::Older => {
273                    entry
274                        .filename_bytes
275                        .reserve_exact(self.decrypt_int() as usize);
276                    self.decrypt_filename(&mut entry);
277
278                    entry.size = self.decrypt_int();
279                    entry.offset = self.pos;
280                    entry.key = self.key;
281
282                    self.seek_byte(entry.size as usize, SeekFrom::Current);
283
284                    if self.pos == self.len {
285                        break;
286                    }
287                }
288            }
289
290            entries.push(entry);
291        }
292
293        entries
294    }
295
296    fn reset(&mut self, data: &[u8]) {
297        self.data = unsafe { &*(data as *const [u8]) };
298        self.len = data.len();
299        self.pos = 0;
300        self.engine_type = EngineType::Older;
301        self.key = OLDER_DEFAULT_KEY;
302        self.key_bytes = OLDER_DEFAULT_KEY.to_le_bytes();
303    }
304
305    /// Extracts files from the archive data into the specified output path.
306    ///
307    /// # Parameters
308    /// - `data`: The content of the archive file.
309    /// - `output_path`: The output path for extracted files.
310    ///
311    /// # Returns
312    /// - `Ok(ExtractOutcome::Extracted)` if files were successfully extracted.
313    /// - `Ok(ExtractOutcome::FilesExist)` if files already exist and `force` is `false`.
314    /// - `Err(ExtractError::InvalidHeader)` for invalid header.
315    /// - `Err(ExtractError::InvalidEngine)` for invalid header engine type byte.
316    /// # Example
317    /// ```no_run
318    /// use rpgmad_lib::Decrypter;
319    ///
320    /// let archive_data: Vec<u8> = std::fs::read("Game.rgss3a").unwrap();
321    /// let mut decrypter = Decrypter::new();
322    /// decrypter.extract(&archive_data, "output").unwrap();
323    /// ```
324    #[inline]
325    pub fn extract<P: AsRef<Path>>(
326        &mut self,
327        data: &[u8],
328        output_path: P,
329    ) -> Result<ExtractOutcome, ExtractError> {
330        self.reset(data);
331        self.parse_header()?;
332
333        let entries: Vec<ArchiveEntry> = self.extract_entries();
334
335        for entry in entries {
336            let filename = String::from_utf8_lossy(&entry.filename_bytes);
337            let file_output_path: PathBuf = output_path.as_ref().join(&*filename);
338
339            if file_output_path.exists() && !self.force {
340                return Ok(ExtractOutcome::FilesExist);
341            }
342
343            if let Some(dir) = file_output_path.parent() {
344                create_dir_all(dir).unwrap();
345            }
346
347            let decrypted = self.decrypt_entry(&entry);
348            write(file_output_path, decrypted).unwrap();
349        }
350
351        Ok(ExtractOutcome::Extracted)
352    }
353}
354
355impl<'a> Default for Decrypter<'a> {
356    /// Returns a new `Decrypter` with default parameters.
357    ///
358    /// Equivalent to calling `Decrypter::new()`.
359    fn default() -> Self {
360        Self::new()
361    }
362}
363
364/// A convenience function to extract an archive in a single call.
365///
366/// This is a wrapper around `Decrypter::extract` with automatic initialization.
367///
368/// # Parameters
369/// - `data`: The content of the archive file.
370/// - `output_path`: The output path for extracted files.
371/// - `force`: If `true`, existing files will be overwritten.
372///
373/// # Returns
374/// - `Ok(ExtractOutcome::Extracted)` if files were successfully extracted.
375/// - `Ok(ExtractOutcome::FilesExist)` if files already exist and `force` is `false`.
376/// - `Err(ExtractError::InvalidHeader)` for invalid header.
377/// - `Err(ExtractError::InvalidEngine)` for invalid header engine type byte.
378///
379/// # Example
380/// ```no_run
381/// use rpgmad_lib::extract_archive;
382///
383/// let data: Vec<u8> = std::fs::read("Game.rgssad").unwrap();
384/// extract_archive(&data, "output", true).unwrap();
385/// ```
386pub fn extract_archive<P: AsRef<Path>>(
387    data: &[u8],
388    output_path: P,
389    force: bool,
390) -> Result<ExtractOutcome, ExtractError> {
391    Decrypter::new().force(force).extract(data, output_path)
392}