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; pub const RPF2_MAGIC: u32 = 0x32465052; pub const RPF3_MAGIC: u32 = 0x33465052; pub const RPF4_MAGIC: u32 = 0x34465052; pub const RPF6_MAGIC: u32 = 0x36465052; pub const RPF7_MAGIC: u32 = 0x52504637; pub const RPF8_MAGIC: u32 = 0x52504638; pub const RSC7_MAGIC: u32 = 0x37435352;
14pub const RSC8_MAGIC: u32 = 0x38435352;
15pub const IMG3_MAGIC: u32 = 0xA94E2A52; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RpfVersion {
21 V0, V2, V3, V4, V6, V7, V8, Img3, }
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum RpfEncryption {
35 None,
36 Open,
37 Aes,
38 Ng,
39 Tfit, }
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#[derive(Debug, Clone)]
71pub enum RpfEntryKind {
72 Directory {
73 entries_index: u32,
74 entries_count: u32,
75 },
76 BinaryFile {
77 file_offset : u32,
79 file_size : u32,
81 uncompressed_size: u32,
82 is_encrypted : bool,
83 },
84 ResourceFile {
85 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
111pub 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 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 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 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]); 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), }
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 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
439pub 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
484pub 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
507fn 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
594fn 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
646fn 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; 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; 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
738fn 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
848static 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 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 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 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
956fn parse_img3_toc(d: &[u8]) -> Result<(Vec<RpfEntry>, RpfEncryption)> {
959 if d.len() < 0x14 { bail!("IMG3 header too short"); }
960
961 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 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; 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 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; RpfEntryKind::BinaryFile {
1030 file_offset : raw_offset,
1031 file_size : 0, 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
1041fn 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
1049fn decompress_detect(data: &[u8], uncompressed_size: usize) -> Option<Vec<u8>> {
1051 if data.len() < 4 { return None; }
1052
1053 if (data[0] & 0xF0) == 0x20 && data[1] == 0xB5 && data[2] == 0x2F && data[3] == 0xFD {
1055 return decompress_zstd(data);
1056 }
1057
1058 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 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 let mut dec = Lzxd::new(WindowSize::KB256);
1081 dec.decompress_next(data, uncompressed_size)
1082 .ok()
1083 .map(|s| s.to_vec())
1084}
1085
1086fn 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
1098fn 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}