Skip to main content

rage_rpf/
archive.rs

1use anyhow::{bail, Context, Result};
2use std::{fs, path::Path};
3
4use crate::crypto::{decrypt_aes, decrypt_ng, GtaKeys};
5
6pub const RPF0_MAGIC: u32 = 0x30465052; // Table Tennis
7pub const RPF2_MAGIC: u32 = 0x32465052; // GTA IV
8pub const RPF3_MAGIC: u32 = 0x33465052; // GTA IV Audio / MCLA (hashed names)
9pub const RPF4_MAGIC: u32 = 0x34465052; // Max Payne 3
10pub const RPF6_MAGIC: u32 = 0x36465052; // Red Dead Redemption
11pub const RPF7_MAGIC: u32 = 0x52504637; // GTA V
12pub const RPF8_MAGIC: u32 = 0x52504638; // Red Dead Redemption 2 (PC/y platform)
13pub const RSC7_MAGIC: u32 = 0x37435352;
14pub const RSC8_MAGIC: u32 = 0x38435352;
15pub const IMG3_MAGIC: u32 = 0xA94E2A52; // GTA SA / GTA IV (IMG v3)
16
17// ─── Version ─────────────────────────────────────────────────────────────────
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RpfVersion {
21    V0,   // Table Tennis — no encryption, deflate, TOC at 0x800
22    V2,   // GTA IV — optional AES, byte offsets, TOC at 0x800
23    V3,   // GTA IV Audio / MCLA — like V2 but hashed names
24    V4,   // Max Payne 3 — like V2 but offsets * 8
25    V6,   // Red Dead Redemption — big-endian 20-byte entries, offsets * 8
26    V7,   // GTA V / FiveM — AES or NG encryption, 512-byte block offsets
27    V8,   // Red Dead Redemption 2 — TFIT cipher, 24-byte entries, hash names
28    Img3, // GTA SA / GTA IV IMG v3 — flat archive, offsets * 2048
29}
30
31// ─── Encryption ──────────────────────────────────────────────────────────────
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum RpfEncryption {
35    None,
36    Open,
37    Aes,
38    Ng,
39    Tfit, // RPF8 TFIT cipher (keys not held)
40}
41
42impl RpfEncryption {
43    pub fn from_u32(v: u32) -> Self {
44        match v {
45            0x00000000 => Self::None,
46            0x4E45504F => Self::Open,
47            0x0FFFFFF9 => Self::Aes,
48            0x0FEFFFFF => Self::Ng,
49            _          => Self::Ng,
50        }
51    }
52
53    pub fn as_u32(self) -> u32 {
54        match self {
55            Self::None => 0x00000000,
56            Self::Open => 0x4E45504F,
57            Self::Aes  => 0x0FFFFFF9,
58            Self::Ng   => 0x0FEFFFFF,
59            Self::Tfit => 0x00000000,
60        }
61    }
62
63    pub fn is_encrypted(self) -> bool {
64        matches!(self, Self::Aes | Self::Ng | Self::Tfit)
65    }
66}
67
68// ─── Entry kinds ─────────────────────────────────────────────────────────────
69
70#[derive(Debug, Clone)]
71pub enum RpfEntryKind {
72    Directory {
73        entries_index: u32,
74        entries_count: u32,
75    },
76    BinaryFile {
77        /// V7: 512-byte block number.  All other versions: pre-computed byte offset.
78        file_offset      : u32,
79        /// Compressed on-disk size (0 = stored, use uncompressed_size for read length).
80        file_size        : u32,
81        uncompressed_size: u32,
82        is_encrypted     : bool,
83    },
84    ResourceFile {
85        /// V7: 512-byte block number.  All other versions: pre-computed byte offset.
86        file_offset   : u32,
87        file_size     : u32,
88        system_flags  : u32,
89        graphics_flags: u32,
90        is_encrypted  : bool,
91    },
92}
93
94#[derive(Debug, Clone)]
95pub struct RpfEntry {
96    pub name      : String,
97    pub name_lower: String,
98    pub kind      : RpfEntryKind,
99}
100
101impl RpfEntry {
102    pub fn is_directory(&self) -> bool {
103        matches!(self.kind, RpfEntryKind::Directory { .. })
104    }
105
106    pub fn is_file(&self) -> bool {
107        !self.is_directory()
108    }
109}
110
111// ─── RpfArchive — parsed metadata ────────────────────────────────────────────
112
113pub struct RpfArchive {
114    pub name        : String,
115    pub start_offset: usize,
116    pub encryption  : RpfEncryption,
117    pub entries     : Vec<RpfEntry>,
118    pub version     : RpfVersion,
119}
120
121impl RpfArchive {
122    pub fn parse(data: &[u8], name: &str, keys: Option<&GtaKeys>) -> Result<Self> {
123        Self::parse_at(data, 0, name, keys)
124    }
125
126    pub fn parse_at(data: &[u8], offset: usize, name: &str, keys: Option<&GtaKeys>) -> Result<Self> {
127        let d = data.get(offset..).context("offset out of bounds")?;
128        if d.len() < 12 { bail!("data too short"); }
129
130        let magic = u32::from_le_bytes(d[0..4].try_into().unwrap());
131        let version = match magic {
132            RPF0_MAGIC => RpfVersion::V0,
133            RPF2_MAGIC => RpfVersion::V2,
134            RPF3_MAGIC => RpfVersion::V3,
135            RPF4_MAGIC => RpfVersion::V4,
136            RPF6_MAGIC => RpfVersion::V6,
137            RPF7_MAGIC => RpfVersion::V7,
138            RPF8_MAGIC => RpfVersion::V8,
139            IMG3_MAGIC => RpfVersion::Img3,
140            _ => bail!("unknown archive magic: {:#010x}", magic),
141        };
142
143        let (entries, encryption) = match version {
144            RpfVersion::V7   => parse_rpf7_toc(d, name, keys)?,
145            RpfVersion::V0   => parse_rpf0_toc(d)?,
146            RpfVersion::V6   => parse_rpf6_toc(d)?,
147            RpfVersion::V8   => parse_rpf8_toc(d)?,
148            RpfVersion::Img3 => parse_img3_toc(d)?,
149            _                => parse_rpf2_toc(d, version)?,
150        };
151
152        let mut archive = Self { name: name.to_string(), start_offset: offset, encryption, entries, version };
153
154        // Resolve V7 resource entries with sentinel file_size 0xFFFFFF
155        if version == RpfVersion::V7 {
156            for entry in &mut archive.entries {
157                if let RpfEntryKind::ResourceFile { file_offset, file_size, .. } = &mut entry.kind {
158                    if *file_size == 0xFFFFFF {
159                        let body_off = offset + (*file_offset as usize * 512);
160                        if body_off + 16 <= data.len() {
161                            let b = &data[body_off..body_off + 16];
162                            *file_size = ((b[7]  as u32) <<  0)
163                                       | ((b[14] as u32) <<  8)
164                                       | ((b[5]  as u32) << 16)
165                                       | ((b[2]  as u32) << 24);
166                        }
167                    }
168                }
169            }
170        }
171
172        Ok(archive)
173    }
174
175    // ─── Extraction ──────────────────────────────────────────────────────────
176
177    pub fn extract_entry(
178        &self,
179        data: &[u8],
180        entry: &RpfEntry,
181        keys: Option<&GtaKeys>,
182    ) -> Result<Vec<u8>> {
183        match &entry.kind {
184            RpfEntryKind::Directory { .. } => bail!("cannot extract a directory entry"),
185
186            RpfEntryKind::BinaryFile {
187                file_offset, file_size, uncompressed_size, is_encrypted
188            } => {
189                let byte_off = self.offset_to_bytes(*file_offset);
190                let size = if *file_size > 0 { *file_size as usize } else { *uncompressed_size as usize };
191                if size == 0 { bail!("binary file has zero size"); }
192
193                let raw = data.get(byte_off..byte_off + size)
194                    .with_context(|| format!("{}: binary file out of bounds", entry.name_lower))?;
195                let mut buf = raw.to_vec();
196
197                if *is_encrypted {
198                    buf = self.decrypt(&buf, &entry.name, *uncompressed_size, keys)?;
199                }
200
201                if *file_size > 0 && *file_size < *uncompressed_size {
202                    buf = self.decompress(&buf, *uncompressed_size as usize).unwrap_or(buf);
203                }
204
205                Ok(buf)
206            }
207
208            RpfEntryKind::ResourceFile {
209                file_offset, file_size, system_flags, graphics_flags, is_encrypted
210            } => {
211                let total = *file_size as usize;
212                let rsc_hdr = self.resource_header_size();
213                if total < rsc_hdr { bail!("{}: resource too small ({} bytes)", entry.name_lower, total); }
214
215                let byte_off = self.offset_to_bytes(*file_offset);
216                let body_off = byte_off + rsc_hdr;
217                let body_len = total - rsc_hdr;
218
219                let raw = data.get(body_off..body_off + body_len)
220                    .with_context(|| format!("{}: resource out of bounds", entry.name_lower))?;
221                let mut body = raw.to_vec();
222
223                if *is_encrypted {
224                    body = self.decrypt(&body, &entry.name, *file_size, keys)?;
225                }
226
227                match self.version {
228                    RpfVersion::V7 => {
229                        let version = resource_version_from_flags(*system_flags, *graphics_flags);
230                        let mut out = Vec::with_capacity(body.len() + 16);
231                        out.extend_from_slice(&RSC7_MAGIC.to_le_bytes());
232                        out.extend_from_slice(&version.to_le_bytes());
233                        out.extend_from_slice(&system_flags.to_le_bytes());
234                        out.extend_from_slice(&graphics_flags.to_le_bytes());
235                        out.extend_from_slice(&body);
236                        Ok(out)
237                    }
238                    RpfVersion::V8 => {
239                        // Rebuild as RSC8 file
240                        let mut out = Vec::with_capacity(body.len() + 16);
241                        out.extend_from_slice(&RSC8_MAGIC.to_le_bytes());
242                        out.extend_from_slice(&[0u8; 4]); // flags placeholder
243                        out.extend_from_slice(&system_flags.to_le_bytes());
244                        out.extend_from_slice(&graphics_flags.to_le_bytes());
245                        out.extend_from_slice(&body);
246                        Ok(out)
247                    }
248                    _ => Ok(body), // V2/V6: return raw body
249                }
250            }
251        }
252    }
253
254    pub fn walk_files(
255        &self,
256        data: &[u8],
257        keys: Option<&GtaKeys>,
258        path_prefix: &str,
259        on_file: &mut dyn FnMut(&str, Vec<u8>),
260    ) -> Result<()> {
261        self.walk_inner(data, keys, path_prefix, on_file, 0)
262    }
263
264    fn walk_inner(
265        &self,
266        data: &[u8],
267        keys: Option<&GtaKeys>,
268        path_prefix: &str,
269        on_file: &mut dyn FnMut(&str, Vec<u8>),
270        depth: usize,
271    ) -> Result<()> {
272        const MAX_DEPTH: usize = 16;
273        if depth > MAX_DEPTH { return Ok(()); }
274
275        let is_aes = self.encryption == RpfEncryption::Aes;
276
277        for entry in &self.entries {
278            if entry.is_directory() { continue; }
279
280            let path = if path_prefix.is_empty() {
281                entry.name_lower.clone()
282            } else {
283                format!("{}/{}", path_prefix, entry.name_lower)
284            };
285
286            match &entry.kind {
287                RpfEntryKind::BinaryFile {
288                    file_offset, file_size, uncompressed_size, is_encrypted
289                } => {
290                    let byte_off = self.offset_to_bytes(*file_offset);
291                    let size = if *file_size > 0 { *file_size as usize } else { *uncompressed_size as usize };
292                    if size == 0 { continue; }
293                    if byte_off + size > data.len() {
294                        eprintln!("[RPF] {} out of bounds, skipping", path);
295                        continue;
296                    }
297
298                    let mut buf = data[byte_off..byte_off + size].to_vec();
299
300                    if *is_encrypted {
301                        if let Some(k) = keys {
302                            buf = if is_aes {
303                                decrypt_aes(&buf, &k.aes_key)
304                            } else {
305                                decrypt_ng(&buf, k, &entry.name, *uncompressed_size)
306                            };
307                        }
308                    }
309
310                    let out = if *file_size > 0 && *file_size < *uncompressed_size {
311                        self.decompress(&buf, *uncompressed_size as usize).unwrap_or(buf)
312                    } else {
313                        buf
314                    };
315
316                    if entry.name_lower.ends_with(".rpf") {
317                        match RpfArchive::parse(&out, &entry.name_lower, keys) {
318                            Ok(nested) => {
319                                let prefix = if path_prefix.is_empty() {
320                                    entry.name_lower.clone()
321                                } else {
322                                    format!("{}/{}", path_prefix, entry.name_lower)
323                                };
324                                if let Err(e) = nested.walk_inner(&out, keys, &prefix, on_file, depth + 1) {
325                                    eprintln!("[RPF] error in nested {}: {}", path, e);
326                                }
327                            }
328                            Err(e) => eprintln!("[RPF] failed to parse nested {}: {}", path, e),
329                        }
330                    } else {
331                        on_file(&path, out);
332                    }
333                }
334
335                RpfEntryKind::ResourceFile {
336                    file_offset, file_size, system_flags, graphics_flags, is_encrypted
337                } => {
338                    let total = *file_size as usize;
339                    let rsc_hdr = self.resource_header_size();
340                    if total < rsc_hdr { continue; }
341
342                    let byte_off = self.offset_to_bytes(*file_offset);
343                    let body_off = byte_off + rsc_hdr;
344                    let body_len = total - rsc_hdr;
345                    if body_off + body_len > data.len() {
346                        eprintln!("[RPF] {} out of bounds, skipping", path);
347                        continue;
348                    }
349
350                    let mut body = data[body_off..body_off + body_len].to_vec();
351
352                    if *is_encrypted {
353                        if let Some(k) = keys {
354                            body = if is_aes {
355                                decrypt_aes(&body, &k.aes_key)
356                            } else {
357                                decrypt_ng(&body, k, &entry.name, *file_size)
358                            };
359                        }
360                    }
361
362                    let out = match self.version {
363                        RpfVersion::V7 => {
364                            let version = resource_version_from_flags(*system_flags, *graphics_flags);
365                            let mut v = Vec::with_capacity(body.len() + 16);
366                            v.extend_from_slice(&RSC7_MAGIC.to_le_bytes());
367                            v.extend_from_slice(&version.to_le_bytes());
368                            v.extend_from_slice(&system_flags.to_le_bytes());
369                            v.extend_from_slice(&graphics_flags.to_le_bytes());
370                            v.extend_from_slice(&body);
371                            v
372                        }
373                        RpfVersion::V8 => {
374                            let mut v = Vec::with_capacity(body.len() + 16);
375                            v.extend_from_slice(&RSC8_MAGIC.to_le_bytes());
376                            v.extend_from_slice(&[0u8; 4]);
377                            v.extend_from_slice(&system_flags.to_le_bytes());
378                            v.extend_from_slice(&graphics_flags.to_le_bytes());
379                            v.extend_from_slice(&body);
380                            v
381                        }
382                        _ => body,
383                    };
384
385                    on_file(&path, out);
386                }
387
388                RpfEntryKind::Directory { .. } => {}
389            }
390        }
391
392        Ok(())
393    }
394
395    // ─── Internal helpers ─────────────────────────────────────────────────────
396
397    /// Convert a stored file_offset to an absolute byte position in `data`.
398    /// V7 stores 512-byte block numbers; all other versions store byte offsets.
399    fn offset_to_bytes(&self, raw_offset: u32) -> usize {
400        self.start_offset + match self.version {
401            RpfVersion::V7 => raw_offset as usize * 512,
402            _              => raw_offset as usize,
403        }
404    }
405
406    fn resource_header_size(&self) -> usize {
407        match self.version {
408            RpfVersion::V7 | RpfVersion::V8 => 16,
409            _ => 12,
410        }
411    }
412
413    fn decompress(&self, data: &[u8], uncompressed_size: usize) -> Option<Vec<u8>> {
414        match self.version {
415            RpfVersion::V6 => decompress_detect(data, uncompressed_size),
416            RpfVersion::V8 => inflate_raw(data),
417            _              => inflate(data),
418        }
419    }
420
421    fn decrypt(&self, data: &[u8], name: &str, length: u32, keys: Option<&GtaKeys>) -> Result<Vec<u8>> {
422        match self.encryption {
423            RpfEncryption::Aes => {
424                let k = keys.context("AES-encrypted entry requires --keys")?;
425                Ok(decrypt_aes(data, &k.aes_key))
426            }
427            RpfEncryption::Ng => {
428                let k = keys.context("NG-encrypted entry requires --keys")?;
429                Ok(decrypt_ng(data, k, name, length))
430            }
431            RpfEncryption::Tfit => {
432                bail!("TFIT decryption is not supported (RDR2 keys not held)")
433            }
434            _ => Ok(data.to_vec()),
435        }
436    }
437}
438
439// ─── RpfFile — owns the raw bytes ────────────────────────────────────────────
440
441pub struct RpfFile {
442    pub archive: RpfArchive,
443    data: Vec<u8>,
444}
445
446impl RpfFile {
447    pub fn open(path: &Path, keys: Option<&GtaKeys>) -> Result<Self> {
448        let data = fs::read(path)
449            .with_context(|| format!("cannot read {}", path.display()))?;
450
451        let name = path
452            .file_name()
453            .and_then(|n| n.to_str())
454            .unwrap_or_else(|| path.to_str().unwrap_or(""));
455
456        let archive = RpfArchive::parse(&data, name, keys)?;
457        Ok(Self { archive, data })
458    }
459
460    pub fn extract_by_name(&self, name: &str, keys: Option<&GtaKeys>) -> Result<Vec<u8>> {
461        let entry = self.archive.entries.iter()
462            .find(|e| e.name_lower == name.to_lowercase())
463            .with_context(|| format!("entry '{}' not found", name))?;
464        self.archive.extract_entry(&self.data, entry, keys)
465    }
466
467    pub fn extract(&self, entry: &RpfEntry, keys: Option<&GtaKeys>) -> Result<Vec<u8>> {
468        self.archive.extract_entry(&self.data, entry, keys)
469    }
470
471    pub fn walk(
472        &self,
473        keys: Option<&GtaKeys>,
474        on_file: &mut dyn FnMut(&str, Vec<u8>),
475    ) -> Result<()> {
476        self.archive.walk_files(&self.data, keys, "", on_file)
477    }
478
479    pub fn raw_data(&self) -> &[u8] {
480        &self.data
481    }
482}
483
484// ─── Resource page-flag helpers ───────────────────────────────────────────────
485
486pub fn resource_version_from_flags(sys_flags: u32, gfx_flags: u32) -> u32 {
487    let sv = (sys_flags  >> 28) & 0xF;
488    let gv = (gfx_flags  >> 28) & 0xF;
489    (sv << 4) | gv
490}
491
492pub fn resource_size_from_flags(flags: u32) -> usize {
493    let s0 = ((flags >> 27) & 0x1)  << 0;
494    let s1 = ((flags >> 26) & 0x1)  << 1;
495    let s2 = ((flags >> 25) & 0x1)  << 2;
496    let s3 = ((flags >> 24) & 0x1)  << 3;
497    let s4 = ((flags >> 17) & 0x7F) << 4;
498    let s5 = ((flags >> 11) & 0x3F) << 5;
499    let s6 = ((flags >> 7)  & 0xF)  << 6;
500    let s7 = ((flags >> 5)  & 0x3)  << 7;
501    let s8 = ((flags >> 4)  & 0x1)  << 8;
502    let ss = (flags & 0xF) as usize;
503    let base_size = 0x200usize << ss;
504    base_size * (s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8) as usize
505}
506
507// ─── RPF7 TOC ────────────────────────────────────────────────────────────────
508
509fn parse_rpf7_toc(d: &[u8], name: &str, keys: Option<&GtaKeys>) -> Result<(Vec<RpfEntry>, RpfEncryption)> {
510    if d.len() < 16 { bail!("RPF7 header too short"); }
511
512    let entry_count  = u32::from_le_bytes(d[4..8].try_into().unwrap()) as usize;
513    let names_length = u32::from_le_bytes(d[8..12].try_into().unwrap()) as usize;
514    let encryption   = RpfEncryption::from_u32(u32::from_le_bytes(d[12..16].try_into().unwrap()));
515
516    let entries_off  = 16;
517    let entries_size = entry_count * 16;
518    let names_off    = entries_off + entries_size;
519
520    if d.len() < names_off + names_length { bail!("RPF7 header truncated"); }
521
522    let mut entries_data = d[entries_off..entries_off + entries_size].to_vec();
523    let mut names_data   = d[names_off..names_off + names_length].to_vec();
524
525    match (encryption, keys) {
526        (RpfEncryption::Aes, Some(k)) => {
527            entries_data = decrypt_aes(&entries_data, &k.aes_key);
528            names_data   = decrypt_aes(&names_data,   &k.aes_key);
529        }
530        (RpfEncryption::Ng, Some(k)) => {
531            let file_size = d.len() as u32;
532            entries_data = decrypt_ng(&entries_data, k, name, file_size);
533            names_data   = decrypt_ng(&names_data,   k, name, file_size);
534        }
535        _ => {}
536    }
537
538    let entries = parse_rpf7_entries(&entries_data, &names_data, entry_count)?;
539    Ok((entries, encryption))
540}
541
542fn parse_rpf7_entries(entries_data: &[u8], names_data: &[u8], count: usize) -> Result<Vec<RpfEntry>> {
543    let mut entries = Vec::with_capacity(count);
544    for i in 0..count {
545        let off = i * 16;
546        if off + 16 > entries_data.len() { break; }
547        let chunk = &entries_data[off..off + 16];
548        let h2 = u32::from_le_bytes(chunk[4..8].try_into().unwrap());
549
550        let entry = if h2 == 0x7FFFFF00 {
551            parse_v7_directory(chunk, names_data, i)
552        } else if (h2 & 0x80000000) == 0 {
553            parse_v7_binary(chunk, names_data, i)
554        } else {
555            parse_v7_resource(chunk, names_data, i)
556        };
557        entries.push(entry);
558    }
559    Ok(entries)
560}
561
562fn parse_v7_directory(chunk: &[u8], names: &[u8], idx: usize) -> RpfEntry {
563    let name_offset   = u32::from_le_bytes(chunk[0..4].try_into().unwrap()) as usize;
564    let entries_index = u32::from_le_bytes(chunk[8..12].try_into().unwrap());
565    let entries_count = u32::from_le_bytes(chunk[12..16].try_into().unwrap());
566    let name = read_cstring(names, name_offset).unwrap_or_else(|| format!("dir_{}", idx));
567    let name_lower = name.to_lowercase();
568    RpfEntry { name, name_lower, kind: RpfEntryKind::Directory { entries_index, entries_count } }
569}
570
571fn parse_v7_binary(chunk: &[u8], names: &[u8], idx: usize) -> RpfEntry {
572    let name_offset       = u16::from_le_bytes(chunk[0..2].try_into().unwrap()) as usize;
573    let file_size         = (chunk[2] as u32) | ((chunk[3] as u32) << 8) | ((chunk[4] as u32) << 16);
574    let file_offset       = (chunk[5] as u32) | ((chunk[6] as u32) << 8) | ((chunk[7] as u32) << 16);
575    let uncompressed_size = u32::from_le_bytes(chunk[8..12].try_into().unwrap());
576    let is_encrypted      = u32::from_le_bytes(chunk[12..16].try_into().unwrap()) == 1;
577    let name = read_cstring(names, name_offset).unwrap_or_else(|| format!("binary_{}", idx));
578    let name_lower = name.to_lowercase();
579    RpfEntry { name, name_lower, kind: RpfEntryKind::BinaryFile { file_offset, file_size, uncompressed_size, is_encrypted } }
580}
581
582fn parse_v7_resource(chunk: &[u8], names: &[u8], idx: usize) -> RpfEntry {
583    let name_offset    = u16::from_le_bytes(chunk[0..2].try_into().unwrap()) as usize;
584    let file_size      = (chunk[2] as u32) | ((chunk[3] as u32) << 8) | ((chunk[4] as u32) << 16);
585    let file_offset    = ((chunk[5] as u32) | ((chunk[6] as u32) << 8) | ((chunk[7] as u32) << 16)) & 0x7FFFFF;
586    let system_flags   = u32::from_le_bytes(chunk[8..12].try_into().unwrap());
587    let graphics_flags = u32::from_le_bytes(chunk[12..16].try_into().unwrap());
588    let name = read_cstring(names, name_offset).unwrap_or_else(|| format!("resource_{}", idx));
589    let name_lower = name.to_lowercase();
590    let is_encrypted = name_lower.ends_with(".ysc");
591    RpfEntry { name, name_lower, kind: RpfEntryKind::ResourceFile { file_offset, file_size, system_flags, graphics_flags, is_encrypted } }
592}
593
594// ─── RPF0 TOC ────────────────────────────────────────────────────────────────
595
596fn parse_rpf0_toc(d: &[u8]) -> Result<(Vec<RpfEntry>, RpfEncryption)> {
597    if d.len() < 12 { bail!("RPF0 header too short"); }
598    let header_size = u32::from_le_bytes(d[4..8].try_into().unwrap()) as usize;
599    let entry_count = u32::from_le_bytes(d[8..12].try_into().unwrap()) as usize;
600
601    let toc_start    = 0x800;
602    let entries_size = entry_count * 16;
603    let names_size   = header_size.saturating_sub(entries_size);
604
605    if d.len() < toc_start + entries_size + names_size { bail!("RPF0 TOC truncated"); }
606
607    let entries_data = &d[toc_start..toc_start + entries_size];
608    let names_data   = &d[toc_start + entries_size..toc_start + entries_size + names_size];
609
610    let entries = parse_rpf0_entries(entries_data, names_data, entry_count)?;
611    Ok((entries, RpfEncryption::None))
612}
613
614fn parse_rpf0_entries(entries_data: &[u8], names_data: &[u8], count: usize) -> Result<Vec<RpfEntry>> {
615    let mut entries = Vec::with_capacity(count);
616    for i in 0..count {
617        let off = i * 16;
618        if off + 16 > entries_data.len() { break; }
619        let chunk = &entries_data[off..off + 16];
620
621        let dword0 = u32::from_le_bytes(chunk[0..4].try_into().unwrap());
622        let dword4 = u32::from_le_bytes(chunk[4..8].try_into().unwrap());
623        let dword8 = u32::from_le_bytes(chunk[8..12].try_into().unwrap());
624        let dwordc = u32::from_le_bytes(chunk[12..16].try_into().unwrap());
625
626        let is_dir      = dword0 & 0x80000000 != 0;
627        let name_offset = (dword0 & 0x7FFFFFFF) as usize;
628        let name = read_cstring(names_data, name_offset)
629            .unwrap_or_else(|| if is_dir { format!("dir_{}", i) } else { format!("file_{}", i) });
630        let name_lower = name.to_lowercase();
631
632        let kind = if is_dir {
633            RpfEntryKind::Directory { entries_index: dword4, entries_count: dword8 }
634        } else {
635            let file_offset       = dword4;
636            let disk_size         = dword8;
637            let uncompressed_size = dwordc;
638            let file_size = if disk_size != uncompressed_size { disk_size } else { 0 };
639            RpfEntryKind::BinaryFile { file_offset, file_size, uncompressed_size, is_encrypted: false }
640        };
641        entries.push(RpfEntry { name, name_lower, kind });
642    }
643    Ok(entries)
644}
645
646// ─── RPF2/3/4 TOC ────────────────────────────────────────────────────────────
647
648fn parse_rpf2_toc(d: &[u8], version: RpfVersion) -> Result<(Vec<RpfEntry>, RpfEncryption)> {
649    if d.len() < 24 { bail!("RPF2 header too short"); }
650    let header_size    = u32::from_le_bytes(d[4..8].try_into().unwrap()) as usize;
651    let entry_count    = u32::from_le_bytes(d[8..12].try_into().unwrap()) as usize;
652    let decryption_tag = u32::from_le_bytes(d[16..20].try_into().unwrap());
653
654    let toc_start    = 0x800;
655    let entries_size = entry_count * 16;
656    let names_size   = header_size.saturating_sub(entries_size);
657
658    if d.len() < toc_start + entries_size + names_size { bail!("RPF2 TOC truncated"); }
659
660    let entries_data = d[toc_start..toc_start + entries_size].to_vec();
661    let names_data   = d[toc_start + entries_size..toc_start + entries_size + names_size].to_vec();
662
663    let encryption = if decryption_tag != 0 {
664        eprintln!("[RPF2] encrypted TOC (tag={:#010x}): GTA IV key not supported", decryption_tag);
665        RpfEncryption::Aes
666    } else {
667        RpfEncryption::None
668    };
669
670    let entries = parse_rpf2_entries(&entries_data, &names_data, entry_count, version)?;
671    Ok((entries, encryption))
672}
673
674fn parse_rpf2_entries(
675    entries_data: &[u8],
676    names_data  : &[u8],
677    count       : usize,
678    version     : RpfVersion,
679) -> Result<Vec<RpfEntry>> {
680    let mut entries = Vec::with_capacity(count);
681    for i in 0..count {
682        let off = i * 16;
683        if off + 16 > entries_data.len() { break; }
684        let chunk = &entries_data[off..off + 16];
685
686        let dword0 = u32::from_le_bytes(chunk[0..4].try_into().unwrap());
687        let dword4 = u32::from_le_bytes(chunk[4..8].try_into().unwrap());
688        let dword8 = u32::from_le_bytes(chunk[8..12].try_into().unwrap());
689        let dwordc = u32::from_le_bytes(chunk[12..16].try_into().unwrap());
690
691        let is_dir        = dword8 & 0x80000000 != 0;
692        let is_resource   = dwordc & 0x80000000 != 0;
693        let is_compressed = dwordc & 0x40000000 != 0;
694
695        let name = if version == RpfVersion::V3 {
696            format!("{:08X}", dword0)
697        } else {
698            read_cstring(names_data, dword0 as usize)
699                .unwrap_or_else(|| if is_dir { format!("dir_{}", i) } else { format!("file_{}", i) })
700        };
701        let name_lower = name.to_lowercase();
702
703        let kind = if is_dir {
704            RpfEntryKind::Directory {
705                entries_index: dword8 & 0x7FFFFFFF,
706                entries_count: dwordc & 0x3FFFFFFF,
707            }
708        } else if is_resource {
709            let raw_offset     = dword8 & 0x7FFFFF00; // low byte is resource type, strip it
710            let byte_offset    = if version == RpfVersion::V4 { raw_offset * 8 } else { raw_offset };
711            let resource_flags = dwordc & 0x3FFFFFFF;
712            let virt_size = (resource_flags & 0x7FF) << (((resource_flags >> 11) & 0xF) + 8);
713            let phys_size = ((resource_flags >> 15) & 0x7FF) << (((resource_flags >> 26) & 0xF) + 8);
714            RpfEntryKind::ResourceFile {
715                file_offset  : byte_offset,
716                file_size    : dword4,
717                system_flags : virt_size,
718                graphics_flags: phys_size,
719                is_encrypted : false,
720            }
721        } else {
722            let raw_offset    = dword8 & 0x7FFFFFFF;
723            let file_offset   = if version == RpfVersion::V4 { raw_offset * 8 } else { raw_offset };
724            let disk_size     = dwordc & 0x00FFFFFF; // bits 24-29 unused, only 24 bits for size
725            let file_size     = if is_compressed { disk_size } else { 0 };
726            RpfEntryKind::BinaryFile {
727                file_offset,
728                file_size,
729                uncompressed_size: dword4,
730                is_encrypted: false,
731            }
732        };
733        entries.push(RpfEntry { name, name_lower, kind });
734    }
735    Ok(entries)
736}
737
738// ─── RPF6 TOC ────────────────────────────────────────────────────────────────
739
740fn parse_rpf6_toc(d: &[u8]) -> Result<(Vec<RpfEntry>, RpfEncryption)> {
741    if d.len() < 16 { bail!("RPF6 header too short"); }
742    let entry_count       = u32::from_be_bytes(d[4..8].try_into().unwrap()) as usize;
743    let debug_data_offset = u32::from_be_bytes(d[8..12].try_into().unwrap()) as u64 * 8;
744    let decryption_tag    = u32::from_be_bytes(d[12..16].try_into().unwrap());
745
746    let entries_start = 16;
747    let entries_size  = entry_count * 20;
748
749    if d.len() < entries_start + entries_size { bail!("RPF6 entries truncated"); }
750
751    let encryption = if decryption_tag != 0 {
752        eprintln!("[RPF6] encrypted TOC (tag={:#010x}): RDR1 key not supported", decryption_tag);
753        RpfEncryption::Aes
754    } else {
755        RpfEncryption::None
756    };
757
758    let debug: Option<(Vec<u8>, Vec<u8>)> = if debug_data_offset != 0 {
759        let start = debug_data_offset as usize;
760        if start < d.len() {
761            let debug_len         = d.len() - start;
762            let debug_entries_size = entry_count * 8;
763            if debug_len >= debug_entries_size {
764                Some((
765                    d[start..start + debug_entries_size].to_vec(),
766                    d[start + debug_entries_size..].to_vec(),
767                ))
768            } else { None }
769        } else { None }
770    } else { None };
771
772    let entries_data = &d[entries_start..entries_start + entries_size];
773    let entries = parse_rpf6_entries(entries_data, debug.as_ref(), entry_count)?;
774    Ok((entries, encryption))
775}
776
777fn parse_rpf6_entries(
778    entries_data: &[u8],
779    debug       : Option<&(Vec<u8>, Vec<u8>)>,
780    count       : usize,
781) -> Result<Vec<RpfEntry>> {
782    let mut entries = Vec::with_capacity(count);
783    for i in 0..count {
784        let off = i * 20;
785        if off + 20 > entries_data.len() { break; }
786        let chunk = &entries_data[off..off + 20];
787
788        let dword0  = u32::from_be_bytes(chunk[0..4].try_into().unwrap());
789        let dword4  = u32::from_be_bytes(chunk[4..8].try_into().unwrap());
790        let dword8  = u32::from_be_bytes(chunk[8..12].try_into().unwrap());
791        let dwordc  = u32::from_be_bytes(chunk[12..16].try_into().unwrap());
792        let dword10 = u32::from_be_bytes(chunk[16..20].try_into().unwrap());
793
794        let is_dir        = dword8 & 0x80000000 != 0;
795        let is_resource   = dwordc & 0x80000000 != 0;
796        let is_compressed = dwordc & 0x40000000 != 0;
797
798        let name = if let Some((offsets, names)) = debug {
799            let oi = i * 8;
800            if oi + 4 <= offsets.len() {
801                let name_off = u32::from_be_bytes(offsets[oi..oi+4].try_into().unwrap()) as usize;
802                read_cstring(names, name_off).unwrap_or_else(|| format!("{:08X}", dword0))
803            } else {
804                format!("{:08X}", dword0)
805            }
806        } else {
807            format!("{:08X}", dword0)
808        };
809        let name_lower = name.to_lowercase();
810
811        let kind = if is_dir {
812            RpfEntryKind::Directory {
813                entries_index: dword8 & 0x7FFFFFFF,
814                entries_count: dwordc & 0x3FFFFFFF,
815            }
816        } else if is_resource {
817            let byte_offset  = (((dword8 & 0x7FFFFF00) as u64) << 3) as u32;
818            let on_disk_size = dword4 & 0x7FFFFFFF;
819            let has_ext      = dword10 & 0x80000000 != 0;
820            let virt_size    = if has_ext { (dword10 & 0x3FFF) << 12 }
821                               else       { (dwordc & 0x7FF) << (((dwordc >> 11) & 0xF) + 8) };
822            let phys_size    = if has_ext { ((dword10 >> 14) & 0x3FFF) << 12 }
823                               else       { ((dwordc >> 15) & 0x7FF) << (((dwordc >> 26) & 0xF) + 8) };
824            RpfEntryKind::ResourceFile {
825                file_offset  : byte_offset,
826                file_size    : on_disk_size,
827                system_flags : virt_size,
828                graphics_flags: phys_size,
829                is_encrypted : false,
830            }
831        } else {
832            let byte_offset      = (((dword8 & 0x7FFFFFFF) as u64) << 3) as u32;
833            let on_disk_size     = dword4 & 0x7FFFFFFF;
834            let uncompressed_size = if is_compressed { dwordc & 0x3FFFFFFF } else { on_disk_size };
835            let file_size        = if is_compressed { on_disk_size } else { 0 };
836            RpfEntryKind::BinaryFile {
837                file_offset: byte_offset,
838                file_size,
839                uncompressed_size,
840                is_encrypted: false,
841            }
842        };
843        entries.push(RpfEntry { name, name_lower, kind });
844    }
845    Ok(entries)
846}
847
848// ─── RPF8 TOC ────────────────────────────────────────────────────────────────
849
850// File extension table matching Swage's GetFileExt (# replaced by 'y' for PC).
851static RPF8_BASE_EXTS: &[&str] = &[
852    "rpf", "ymf", "ydr", "yft", "ydd", "ytd", "ybn", "ybd", "ypd", "ybs",
853    "ysd", "ymt", "ysc", "ycs",
854];
855static RPF8_EXTRA_EXTS: &[&str] = &[
856    "mrf", "cut", "gfx", "ycd", "yld", "ypmd", "ypm", "yed", "ypt",
857    "ymap", "ytyp", "ych", "yldb", "yjd", "yad", "ynv", "yhn", "ypl",
858    "ynd", "yvr", "ywr", "ynh", "yfd", "yas",
859];
860
861fn rpf8_ext(id: u8) -> &'static str {
862    if (id as usize) < RPF8_BASE_EXTS.len() {
863        RPF8_BASE_EXTS[id as usize]
864    } else if id >= 64 {
865        let idx = (id - 64) as usize;
866        if idx < RPF8_EXTRA_EXTS.len() { RPF8_EXTRA_EXTS[idx] } else { "bin" }
867    } else {
868        "bin"
869    }
870}
871
872fn parse_rpf8_toc(d: &[u8]) -> Result<(Vec<RpfEntry>, RpfEncryption)> {
873    if d.len() < 16 { bail!("RPF8 header too short"); }
874
875    // Header: Magic(4) + EntryCount(4) + NamesLength(4) + DecryptionTag(2) + PlatformId(2)
876    let entry_count    = u32::from_le_bytes(d[4..8].try_into().unwrap()) as usize;
877    let _names_length  = u32::from_le_bytes(d[8..12].try_into().unwrap()) as usize;
878    let decryption_tag = u16::from_le_bytes(d[12..14].try_into().unwrap());
879
880    // RSA signature (256 bytes) immediately after header
881    let entries_start = 16 + 256;
882    let entries_size  = entry_count * 24;
883
884    if d.len() < entries_start + entries_size { bail!("RPF8 entries truncated"); }
885
886    let encryption = if decryption_tag != 0xFF {
887        eprintln!("[RPF8] TFIT-encrypted TOC (tag={:#06x}): RDR2 keys not supported", decryption_tag);
888        RpfEncryption::Tfit
889    } else {
890        RpfEncryption::None
891    };
892
893    let entries_data = &d[entries_start..entries_start + entries_size];
894    let entries = parse_rpf8_entries(entries_data, entry_count)?;
895
896    Ok((entries, encryption))
897}
898
899fn parse_rpf8_entries(entries_data: &[u8], count: usize) -> Result<Vec<RpfEntry>> {
900    let mut entries = Vec::with_capacity(count);
901    for i in 0..count {
902        let off = i * 24;
903        if off + 24 > entries_data.len() { break; }
904        let chunk = &entries_data[off..off + 24];
905
906        let qword0  = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
907        let qword8  = u64::from_le_bytes(chunk[8..16].try_into().unwrap());
908        let qword10 = u64::from_le_bytes(chunk[16..24].try_into().unwrap());
909
910        let hash         = (qword0 & 0xFFFFFFFF) as u32;
911        let _enc_config  = ((qword0 >> 32) & 0xFF) as u8;
912        let enc_key_id   = ((qword0 >> 40) & 0xFF) as u8;
913        let ext_id       = ((qword0 >> 48) & 0xFF) as u8;
914        let is_resource  = (qword0 >> 56) & 1 != 0;
915
916        let on_disk_size = ((qword8 & 0xFFFFFFF) << 4) as u32;
917        let byte_offset  = ((((qword8 >> 28) & 0x7FFFFFFF) << 4) & 0xFFFFFFFF) as u32;
918        let compressor   = ((qword8 >> 59) & 0x1F) as u8;
919
920        let is_encrypted = enc_key_id != 0xFF;
921        let is_dir       = ext_id == 0xFE;
922
923        let ext = if ext_id == 0xFF { "bin" } else { rpf8_ext(ext_id) };
924        let name = format!("{:08X}.{}", hash, ext);
925        let name_lower = name.to_lowercase();
926
927        let kind = if is_dir {
928            // RPF8 directories are currently unused per Swage comment
929            RpfEntryKind::Directory { entries_index: 0, entries_count: 0 }
930        } else if is_resource {
931            let virt_flags = (qword10 & 0xFFFFFFFF) as u32;
932            let phys_flags = (qword10 >> 32) as u32;
933            let file_size  = on_disk_size;
934            RpfEntryKind::ResourceFile {
935                file_offset  : byte_offset,
936                file_size,
937                system_flags : virt_flags,
938                graphics_flags: phys_flags,
939                is_encrypted,
940            }
941        } else {
942            let uncompressed_size = (qword10 & 0xFFFFFFFF) as u32;
943            let file_size = if compressor != 0 { on_disk_size } else { 0 };
944            RpfEntryKind::BinaryFile {
945                file_offset: byte_offset,
946                file_size,
947                uncompressed_size,
948                is_encrypted,
949            }
950        };
951        entries.push(RpfEntry { name, name_lower, kind });
952    }
953    Ok(entries)
954}
955
956// ─── IMG3 TOC ────────────────────────────────────────────────────────────────
957
958fn parse_img3_toc(d: &[u8]) -> Result<(Vec<RpfEntry>, RpfEncryption)> {
959    if d.len() < 0x14 { bail!("IMG3 header too short"); }
960
961    // Header (20 bytes): Magic(4) + Version(4) + EntryCount(4) + HeaderSize(4) + EntrySize(2) + pad(2)
962    let entry_count = u32::from_le_bytes(d[8..12].try_into().unwrap()) as usize;
963    let header_size = u32::from_le_bytes(d[12..16].try_into().unwrap()) as usize;
964    let entry_size  = u16::from_le_bytes(d[16..18].try_into().unwrap()) as usize;
965
966    let entry_size  = if entry_size == 0 { 16 } else { entry_size };
967    let entries_start = 0x14;
968    let entries_size  = entry_count * entry_size;
969    let names_start   = entries_start + entries_size;
970
971    if d.len() < entries_start + header_size { bail!("IMG3 TOC truncated"); }
972
973    let entries_data = &d[entries_start..entries_start + entries_size];
974    let names_data   = &d[names_start..entries_start + header_size];
975
976    let entries = parse_img3_entries(entries_data, names_data, entry_count, entry_size)?;
977    Ok((entries, RpfEncryption::None))
978}
979
980fn parse_img3_entries(
981    entries_data: &[u8],
982    names_data  : &[u8],
983    count       : usize,
984    entry_size  : usize,
985) -> Result<Vec<RpfEntry>> {
986    let mut entries  = Vec::with_capacity(count);
987    let mut name_pos = 0usize;
988
989    for i in 0..count {
990        let off = i * entry_size;
991        if off + 16 > entries_data.len() { break; }
992        let chunk = &entries_data[off..off + 16];
993
994        let dword0 = u32::from_le_bytes(chunk[0..4].try_into().unwrap());
995        let dword4 = u32::from_le_bytes(chunk[4..8].try_into().unwrap());
996        let dword8 = u32::from_le_bytes(chunk[8..12].try_into().unwrap());
997        let wordc  = u16::from_le_bytes(chunk[12..14].try_into().unwrap());
998        let worde  = u16::from_le_bytes(chunk[14..16].try_into().unwrap());
999
1000        // Name: sequential null-terminated strings in names_data
1001        let name_end = names_data[name_pos..].iter().position(|&b| b == 0)
1002            .map(|p| name_pos + p)
1003            .unwrap_or(names_data.len());
1004        let name = String::from_utf8_lossy(&names_data[name_pos..name_end]).into_owned();
1005        name_pos = name_end + 1;
1006        let name_lower = name.to_lowercase();
1007
1008        let is_resource     = worde & 0x2000 != 0;
1009        let _is_old_resource = worde & 0x4000 != 0;
1010
1011        let raw_offset = dword8 << 11; // * 2048
1012        let on_disk_size = ((wordc as u32) << 11).saturating_sub((worde & 0x7FF) as u32);
1013
1014        let kind = if is_resource {
1015            let virt_size = (dword0 & 0x7FF) << (((dword0 >> 11) & 0xF) + 8);
1016            let phys_size = ((dword0 >> 15) & 0x7FF) << (((dword0 >> 26) & 0xF) + 8);
1017            let total_size = virt_size.saturating_add(phys_size);
1018            // Store as BinaryFile: offset past 12-byte resource header, zlib-compressed body
1019            let body_offset = raw_offset.saturating_add(12);
1020            let body_size   = on_disk_size.saturating_sub(12);
1021            RpfEntryKind::BinaryFile {
1022                file_offset      : body_offset,
1023                file_size        : body_size,
1024                uncompressed_size: total_size,
1025                is_encrypted     : false,
1026            }
1027        } else {
1028            let _ = dword4; // resource_type, ignored for non-resource
1029            RpfEntryKind::BinaryFile {
1030                file_offset      : raw_offset,
1031                file_size        : 0, // stored
1032                uncompressed_size: on_disk_size,
1033                is_encrypted     : false,
1034            }
1035        };
1036        entries.push(RpfEntry { name, name_lower, kind });
1037    }
1038    Ok(entries)
1039}
1040
1041// ─── Common helpers ───────────────────────────────────────────────────────────
1042
1043fn read_cstring(data: &[u8], offset: usize) -> Option<String> {
1044    if offset >= data.len() { return None; }
1045    let end = data[offset..].iter().position(|&b| b == 0).map(|p| offset + p).unwrap_or(data.len());
1046    Some(String::from_utf8_lossy(&data[offset..end]).into_owned())
1047}
1048
1049/// Auto-detect and decompress RPF6 data (zstd, LZXD, zlib, raw deflate).
1050fn decompress_detect(data: &[u8], uncompressed_size: usize) -> Option<Vec<u8>> {
1051    if data.len() < 4 { return None; }
1052
1053    // zstd frame magic: first byte matches 0x2x, then 0xB5 0x2F 0xFD
1054    if (data[0] & 0xF0) == 0x20 && data[1] == 0xB5 && data[2] == 0x2F && data[3] == 0xFD {
1055        return decompress_zstd(data);
1056    }
1057
1058    // LZXD: magic 0x0F F5 12 F1, followed by 4-byte big-endian uncompressed size (8 bytes total header)
1059    if data.len() >= 8 && data[0] == 0x0F && data[1] == 0xF5 && data[2] == 0x12 && data[3] == 0xF1 {
1060        return decompress_lzxd(&data[8..], uncompressed_size);
1061    }
1062
1063    // Zlib / deflate fallback
1064    inflate(data)
1065}
1066
1067fn decompress_zstd(data: &[u8]) -> Option<Vec<u8>> {
1068    use ruzstd::decoding::StreamingDecoder;
1069    use ruzstd::io::Read;
1070    let cursor = std::io::Cursor::new(data);
1071    let mut dec = StreamingDecoder::new(cursor).ok()?;
1072    let mut out = Vec::new();
1073    dec.read_to_end(&mut out).ok()?;
1074    if out.is_empty() { None } else { Some(out) }
1075}
1076
1077fn decompress_lzxd(data: &[u8], uncompressed_size: usize) -> Option<Vec<u8>> {
1078    use lzxd::{Lzxd, WindowSize};
1079    // 256 KB window is a safe upper bound for RAGE game assets
1080    let mut dec = Lzxd::new(WindowSize::KB256);
1081    dec.decompress_next(data, uncompressed_size)
1082       .ok()
1083       .map(|s| s.to_vec())
1084}
1085
1086/// Raw deflate (no zlib header) — used by RPF8.
1087fn inflate_raw(data: &[u8]) -> Option<Vec<u8>> {
1088    use flate2::read::DeflateDecoder;
1089    use std::io::Read;
1090    let mut out = Vec::new();
1091    if DeflateDecoder::new(data).read_to_end(&mut out).is_ok() && !out.is_empty() {
1092        Some(out)
1093    } else {
1094        None
1095    }
1096}
1097
1098/// Try raw deflate then zlib — used by RPF0, RPF7, IMG3, RPF2.
1099fn inflate(data: &[u8]) -> Option<Vec<u8>> {
1100    use flate2::read::{DeflateDecoder, ZlibDecoder};
1101    use std::io::Read;
1102    let mut out = Vec::new();
1103    if DeflateDecoder::new(data).read_to_end(&mut out).is_ok() && !out.is_empty() {
1104        return Some(out);
1105    }
1106    out.clear();
1107    if ZlibDecoder::new(data).read_to_end(&mut out).is_ok() && !out.is_empty() {
1108        return Some(out);
1109    }
1110    None
1111}