1use crate::GameKind;
2use anyhow::bail;
3use anyhow::ensure;
4use anyhow::Context;
5use camino::Utf8Path;
6use camino::Utf8PathBuf;
7use object::LittleEndian as LE;
8use object::U16;
9use object::U32;
10use std::ffi::OsStr;
11use std::fs::File;
12use std::io::Read;
13use std::path::Path;
14use std::path::PathBuf;
15use walkdir::WalkDir;
16
17#[derive(serde::Deserialize, Debug)]
18pub struct Assembly {
19 #[serde(rename = "assemblyIdentity")]
20 pub assembly_identity: AssemblyIdentity,
21
22 pub description: Option<Description>,
23}
24
25#[derive(serde::Deserialize, Debug)]
26#[expect(dead_code)]
27pub struct AssemblyIdentity {
28 #[serde(rename = "@version")]
29 pub version: String,
30
31 #[serde(rename = "@processorArchitecture")]
32 pub processor_architecture: Option<String>,
33
34 #[serde(rename = "@name")]
35 pub name: String,
36
37 #[serde(rename = "@type")]
38 pub type_: String,
39}
40
41#[derive(serde::Deserialize, Debug)]
42pub struct Description {
43 #[serde(rename = "$value")]
44 pub value: String,
45}
46
47#[derive(Debug)]
48#[expect(dead_code)]
49struct VersionInfo {
50 pub fixed_file_info: Option<FixedFileInfo>,
51 pub string_file_info: Option<StringFileInfo>,
52}
53
54impl VersionInfo {
55 fn parse<'data, R>(reader: R, offset: &mut u64, expected_size: u64) -> anyhow::Result<Self>
57 where
58 R: object::read::ReadRef<'data>,
59 {
60 let start_offset = *offset;
61
62 let _length: U16<LE> = *reader.read(offset).ok().context("failed to read length")?;
63
64 let value_length: U16<LE> = *reader
65 .read(offset)
66 .ok()
67 .context("failed to read value length")?;
68
69 let type_: U16<LE> = *reader.read(offset).ok().context("failed to read type")?;
70 ensure!(type_.get(LE) == 0, "text version data is not supported");
71
72 let expected_key = "VS_VERSION_INFO\0";
73 let key: &[u16] = reader
74 .read_slice(offset, expected_key.len())
75 .ok()
76 .context("failed to read key")?;
77 let key = String::from_utf16(key)?;
78 ensure!(expected_key == key);
79
80 read_padding(reader, offset)?;
81
82 let value_length_u64 = u64::from(value_length.get(LE));
83 let fixed_file_info = if value_length_u64 != 0 {
84 ensure!(value_length.get(LE) == 52);
85 Some(FixedFileInfo::parse(reader, offset)?)
86 } else {
87 None
88 };
89
90 let read_size = *offset - start_offset;
91 ensure!(read_size <= expected_size);
92 if read_size == expected_size {
93 return Ok(Self {
94 fixed_file_info,
95 string_file_info: None,
96 });
97 }
98
99 let mut maybe_string_file_info: Option<Option<StringFileInfo>> = None;
100 let string_file_info_key = "StringFileInfo\0";
101 let var_file_info_key = "VarFileInfo\0";
102 let key_peek_len = std::cmp::min(string_file_info_key.len(), var_file_info_key.len());
103 loop {
104 read_padding(reader, offset)?;
105
106 let start_offset = *offset;
107
108 let length: U16<LE> = *reader.read(offset).ok().context("failed to read length")?;
109 let length = length.get(LE);
110
111 let value_length: U16<LE> = *reader
112 .read(offset)
113 .ok()
114 .context("failed to read value length")?;
115 ensure!(value_length.get(LE) == 0);
116
117 let type_: U16<LE> = *reader.read(offset).ok().context("failed to read type")?;
118 ensure!(type_.get(LE) == 1);
119
120 let key_bytes: &[u16] = reader
121 .read_slice(offset, key_peek_len)
122 .ok()
123 .context("failed to read key bytes")?;
124 let key = String::from_utf16(key_bytes)?;
125 if key == string_file_info_key[..key_peek_len] {
126 ensure!(maybe_string_file_info.is_none());
127
128 let remaining_key_bytes: &[u16] = reader
129 .read_slice(offset, string_file_info_key.len() - key_peek_len)
130 .ok()
131 .context("failed to read remaining key bytes")?;
132 let remaining_key_bytes = String::from_utf16(remaining_key_bytes)?;
133 ensure!(string_file_info_key[key_peek_len..] == remaining_key_bytes);
134
135 read_padding(reader, offset)?;
136
137 let mut children = Vec::with_capacity(1);
138 loop {
139 let table = StringTable::parse(reader, offset)?;
140 children.push(table);
141
142 let current_length = *offset - start_offset;
143 ensure!(current_length <= u64::from(length));
144 if current_length == u64::from(length) {
145 break;
146 }
147 }
148
149 let string_file_info = StringFileInfo { children };
150
151 maybe_string_file_info = Some(Some(string_file_info));
152 } else if key == var_file_info_key[..key_peek_len] {
153 break;
155 } else {
156 bail!("unknown key \"{key}\"");
157 }
158 }
159 let string_file_info = maybe_string_file_info.unwrap();
160
161 Ok(Self {
162 fixed_file_info,
163 string_file_info,
164 })
165 }
166}
167
168#[derive(Debug)]
169struct StringFileInfo {
170 pub children: Vec<StringTable>,
171}
172
173fn read_padding<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<()>
174where
175 R: object::read::ReadRef<'data>,
176{
177 let padding_size = 4 - (*offset % 4);
178 if padding_size != 4 {
179 let padding = reader
180 .read_bytes(offset, padding_size)
181 .ok()
182 .context("failed to read padding")?;
183 ensure!(padding.iter().all(|b| *b == 0));
184 }
185
186 Ok(())
187}
188
189fn read_utf16_nul_string<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<String>
190where
191 R: object::read::ReadRef<'data>,
192{
193 let mut raw = Vec::new();
194 while raw.is_empty() || *raw.last().unwrap() != 0 {
195 let value: U16<LE> = *reader
196 .read(offset)
197 .ok()
198 .context("failed to read wide char")?;
199 raw.push(value.get(LE));
200 }
201
202 let value = String::from_utf16(&raw)?;
203
204 Ok(value)
205}
206
207#[derive(Debug)]
208#[expect(dead_code)]
209struct FixedFileInfo {
210 struct_version: u32,
211 file_version: u64,
212 product_version: u64,
213 file_flags_mask: u32,
214 file_flags: u32,
215 file_os: u32,
216 file_type: u32,
217 file_subtype: u32,
218 file_date: u64,
219}
220
221impl FixedFileInfo {
222 fn parse<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<Self>
223 where
224 R: object::read::ReadRef<'data>,
225 {
226 let signature: U32<LE> = *reader
227 .read(offset)
228 .ok()
229 .context("failed to read signature")?;
230 ensure!(signature.get(LE) == 0xFEEF04BD);
231
232 let struct_version: U32<LE> = *reader
233 .read(offset)
234 .ok()
235 .context("failed to read struct version")?;
236 let struct_version = struct_version.get(LE);
237
238 let file_version_ms: U32<LE> = *reader
239 .read(offset)
240 .ok()
241 .context("failed to read file version ms")?;
242 let file_version_ls: U32<LE> = *reader
243 .read(offset)
244 .ok()
245 .context("failed to read file version ls")?;
246 let file_version =
247 (u64::from(file_version_ms.get(LE)) << 32) | u64::from(file_version_ls.get(LE));
248
249 let product_version_ms: U32<LE> = *reader
250 .read(offset)
251 .ok()
252 .context("failed to read product version ms")?;
253 let product_version_ls: U32<LE> = *reader
254 .read(offset)
255 .ok()
256 .context("failed to read product version ls")?;
257 let product_version =
258 (u64::from(product_version_ms.get(LE)) << 32) | u64::from(product_version_ls.get(LE));
259
260 let file_flags_mask: U32<LE> = *reader
261 .read(offset)
262 .ok()
263 .context("failed to read file flags mask")?;
264 let file_flags_mask = file_flags_mask.get(LE);
265
266 let file_flags: U32<LE> = *reader
267 .read(offset)
268 .ok()
269 .context("failed to read file flags")?;
270 let file_flags = file_flags.get(LE);
271
272 let file_os: U32<LE> = *reader.read(offset).ok().context("failed to read file os")?;
273 let file_os = file_os.get(LE);
274
275 let file_type: U32<LE> = *reader
276 .read(offset)
277 .ok()
278 .context("failed to read file type")?;
279 let file_type = file_type.get(LE);
280
281 let file_subtype: U32<LE> = *reader
282 .read(offset)
283 .ok()
284 .context("failed to read file subtype")?;
285 let file_subtype = file_subtype.get(LE);
286
287 let file_date_ms: U32<LE> = *reader
288 .read(offset)
289 .ok()
290 .context("failed to read file date ms")?;
291
292 let file_date_ls: U32<LE> = *reader
293 .read(offset)
294 .ok()
295 .context("failed to read file date ls")?;
296 let file_date = (u64::from(file_date_ms.get(LE)) << 32) | u64::from(file_date_ls.get(LE));
297
298 Ok(Self {
299 struct_version,
300 file_version,
301 product_version,
302 file_flags_mask,
303 file_flags,
304 file_os,
305 file_type,
306 file_subtype,
307 file_date,
308 })
309 }
310}
311
312#[derive(Debug)]
313#[allow(dead_code)]
314struct StringTable {
315 pub key: String,
316 pub children: Vec<StringStruct>,
317}
318
319impl StringTable {
320 fn parse<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<Self>
321 where
322 R: object::read::ReadRef<'data>,
323 {
324 let start_offset = *offset;
325
326 let length: U16<LE> = *reader.read(offset).ok().context("failed to read length")?;
327 let length = length.get(LE);
328
329 let value_length: U16<LE> = *reader
330 .read(offset)
331 .ok()
332 .context("failed to read value length")?;
333 ensure!(value_length.get(LE) == 0);
334
335 let type_: U16<LE> = *reader.read(offset).ok().context("failed to read type")?;
336 ensure!(type_.get(LE) == 1);
337
338 let key: &[u16] = reader
339 .read_slice(offset, 8)
340 .ok()
341 .context("failed to read key")?;
342 let key = String::from_utf16(key)?;
343 ensure!(key.bytes().all(|b| b.is_ascii_hexdigit()));
344 ensure!(key.len() == 8);
345
346 read_padding(reader, offset)?;
347
348 let mut children = Vec::new();
349 loop {
350 let string = StringStruct::parse(reader, offset)?;
351 children.push(string);
352
353 let current_length = *offset - start_offset;
354 ensure!(current_length <= u64::from(length));
355 if current_length == u64::from(length) {
356 break;
357 }
358
359 read_padding(reader, offset)?;
360 }
361
362 Ok(Self { key, children })
363 }
364
365 }
377
378#[derive(Debug)]
379struct StringStruct {
380 pub key: String,
381 pub value: Vec<u16>,
382}
383
384impl StringStruct {
385 fn parse<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<Self>
386 where
387 R: object::read::ReadRef<'data>,
388 {
389 let start_offset = *offset;
390
391 let length: U16<LE> = *reader.read(offset).ok().context("failed to read length")?;
392 let length = length.get(LE);
393
394 let value_length: U16<LE> = *reader
395 .read(offset)
396 .ok()
397 .context("failed to read value length")?;
398 let value_length = value_length.get(LE);
399
400 let type_: U16<LE> = *reader
401 .read(offset)
402 .ok()
403 .context("failed to read value length")?;
404 let type_ = type_.get(LE);
405 ensure!(type_ == 1, "unsupported string struct type {type_}");
406
407 let key = read_utf16_nul_string(reader, offset)?;
408
409 read_padding(reader, offset)?;
410
411 let value: &[U16<LE>] = reader
412 .read_slice(offset, value_length.into())
413 .ok()
414 .context("failed to read value")?;
415 let value: Vec<u16> = value.iter().map(|value| value.get(LE)).collect();
416
417 ensure!(*offset - start_offset == u64::from(length));
418
419 Ok(Self { key, value })
420 }
421}
422
423fn guess_game_kind_from_exe(game_exe: &[u8]) -> anyhow::Result<Option<GameKind>> {
426 use object::read::File;
427
428 let file = File::parse(game_exe)?;
429 let (section_table, data_directories) = match file {
430 File::Pe32(file) => (file.section_table(), file.data_directories()),
431 File::Pe64(file) => (file.section_table(), file.data_directories()),
432 _ => bail!("unknown object file format {:?}", file.format()),
433 };
434
435 let resource_directory = data_directories.resource_directory(game_exe, §ion_table)?;
436 let resource_directory = match resource_directory {
437 Some(resource_directory) => resource_directory,
438 None => return Ok(None),
439 };
440
441 let root = resource_directory.root()?;
442
443 if let Some(game_kind) =
444 guess_from_version_entry(game_exe, section_table, resource_directory, &root)?
445 {
446 return Ok(Some(game_kind));
447 }
448
449 if let Some(game_kind) =
450 guess_from_manifest_entry(game_exe, section_table, resource_directory, &root)?
451 {
452 return Ok(Some(game_kind));
453 }
454
455 Ok(None)
456}
457
458fn guess_from_version_entry(
459 game_exe: &[u8],
460 section_table: object::read::pe::SectionTable<'_>,
461 resource_directory: object::read::pe::ResourceDirectory<'_>,
462 root: &object::read::pe::ResourceDirectoryTable<'_>,
463) -> anyhow::Result<Option<GameKind>> {
464 use object::pe::RT_VERSION;
465
466 let entry = root
467 .entries
468 .iter()
469 .find(|entry| entry.name_or_id().id() == Some(RT_VERSION));
470 let entry = match entry {
471 Some(entry) => entry,
472 None => return Ok(None),
473 };
474
475 let data = entry.data(resource_directory)?;
476 let table = data.table().context("object VERSION data is not a table")?;
477
478 let data = table
479 .entries
480 .first()
481 .context("object VERSION table missing entry 0")?
482 .data(resource_directory)?;
483 let table = data
484 .table()
485 .context("object VERSION table entry 0 is not a table")?;
486
487 let data = table
488 .entries
489 .first()
490 .context("object VERSION table entry 0 table missing entry 0")?
491 .data(resource_directory)?
492 .data()
493 .context("object VERSION table entry 0 table entry 0 is not data")?;
494 let offset = data.offset_to_data.get(LE);
495 let size = usize::try_from(data.size.get(LE))?;
496 let (offset, _) = section_table
499 .pe_file_range_at(offset)
500 .context("section missing version offset address")?;
501 let mut offset = u64::from(offset);
502 let version_info = VersionInfo::parse(game_exe, &mut offset, u64::try_from(size)?)?;
503
504 let string_file_info = match version_info.string_file_info.as_ref() {
505 Some(string_file_info) => string_file_info,
506 None => return Ok(None),
507 };
508
509 for table in string_file_info.children.iter() {
510 for string in table.children.iter() {
511 if string.key != "FileDescription\0" {
512 continue;
513 }
514
515 let value = String::from_utf16(&string.value)?;
517 match value.as_str() {
518 "RGSS Player\0" => return Ok(Some(GameKind::Xp)),
519 "RGSS2 Player\0" => return Ok(Some(GameKind::Vx)),
520 _ => {}
521 }
522 }
523 }
524
525 Ok(None)
526}
527
528fn guess_from_manifest_entry(
529 game_exe: &[u8],
530 section_table: object::read::pe::SectionTable<'_>,
531 resource_directory: object::read::pe::ResourceDirectory<'_>,
532 root: &object::read::pe::ResourceDirectoryTable<'_>,
533) -> anyhow::Result<Option<GameKind>> {
534 use object::pe::RT_MANIFEST;
535 use object::LittleEndian as LE;
536
537 let manifest_entry = root
538 .entries
539 .iter()
540 .find(|entry| entry.name_or_id().id() == Some(RT_MANIFEST));
541 let manifest_entry = match manifest_entry {
542 Some(manifest_entry) => manifest_entry,
543 None => return Ok(None),
544 };
545
546 let manifest_entry_data = manifest_entry.data(resource_directory)?;
547 let manifest_entry_table = manifest_entry_data
548 .table()
549 .context("object MANIFEST data is not a table")?;
550
551 let manifest_entry_table_entry_data = manifest_entry_table
552 .entries
553 .first()
554 .context("object MANIFEST table missing entry 0")?
555 .data(resource_directory)?;
556 let manifest_entry_table_entry_data_table = manifest_entry_table_entry_data
557 .table()
558 .context("object MANIFEST table entry 0 is not a table")?;
559
560 let manifest_entry_table_entry_data_table_entry_data = manifest_entry_table_entry_data_table
561 .entries
562 .first()
563 .context("object MANIFEST table entry 0 table missing entry 0")?
564 .data(resource_directory)?
565 .data()
566 .context("object MANIFEST table entry 0 table entry 0 is not data")?;
567 let manifest_offset = manifest_entry_table_entry_data_table_entry_data
568 .offset_to_data
569 .get(LE);
570 let manifest_size = usize::try_from(
571 manifest_entry_table_entry_data_table_entry_data
572 .size
573 .get(LE),
574 )?;
575 let code_page = manifest_entry_table_entry_data_table_entry_data
576 .code_page
577 .get(LE);
578
579 let bytes = §ion_table
580 .pe_data_at(game_exe, manifest_offset)
581 .context("failed to get object manifest bytes")?
582 .get(..manifest_size)
583 .context("object manifest smaller than declared")?;
584
585 let manifest_string = match code_page {
586 0 => {
587 std::str::from_utf8(bytes)?.to_string()
592 }
593 _ => bail!("unknown MANIFEST LCID {code_page}"),
594 };
595
596 let manifest: Assembly = quick_xml::de::from_str(&manifest_string)?;
597 if manifest.assembly_identity.name == "Enterbrain.RGSS.Game"
598 && manifest
599 .description
600 .as_ref()
601 .map(|description| description.value.as_str())
602 == Some("RGSS Player")
603 {
604 return Ok(Some(GameKind::Xp));
605 }
606
607 Ok(None)
608}
609
610pub enum FileEntryIter {
612 WalkDir {
613 input_path: PathBuf,
614 iter: walkdir::IntoIter,
615 game_kind: GameKind,
616 },
617 Rgssad {
618 reader: rgssad::Reader<File>,
619 game_kind: GameKind,
620 },
621}
622
623impl FileEntryIter {
624 pub fn new<P>(path: P) -> anyhow::Result<Self>
628 where
629 P: AsRef<Path>,
630 {
631 let path = path.as_ref();
632
633 if !path.is_dir() {
634 return Self::new_rgssad_path(path);
636 }
637
638 let rgssad_path = path.join("Game.rgssad");
639 match File::open(&rgssad_path) {
640 Ok(file) => {
641 return Self::new_rgssad_file(file, GameKind::Xp);
642 }
643 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
644 Err(error) => {
645 return Err(error)
646 .with_context(|| format!("failed to open \"{}\"", rgssad_path.display()));
647 }
648 };
649
650 let rgssad_path = path.join("Game.rgss2a");
651 match File::open(&rgssad_path) {
652 Ok(file) => {
653 return Self::new_rgssad_file(file, GameKind::Vx);
654 }
655 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
656 Err(error) => {
657 return Err(error)
658 .with_context(|| format!("failed to open \"{}\"", rgssad_path.display()));
659 }
660 };
661
662 ensure!(
663 path.join("Data").exists(),
664 "Data directory is missing. Are you sure the input folder is correct?"
665 );
666 ensure!(
667 path.join("Graphics").exists(),
668 "Graphics directory is missing. Are you sure the input folder is correct?"
669 );
670
671 Self::new_walkdir_path(path)
672 }
673
674 pub fn new_walkdir_path<P>(path: P) -> anyhow::Result<Self>
676 where
677 P: AsRef<Path>,
678 {
679 let path = path.as_ref();
680
681 let game_kind = (|| {
682 let game_exe = std::fs::read(path.join("Game.exe"))?;
683
684 if let Some(game_kind) = guess_game_kind_from_exe(&game_exe)? {
685 return Ok(game_kind);
686 }
687
688 bail!("failed to determine game type");
689 })()?;
690
691 let iter = WalkDir::new(path).into_iter();
692
693 Ok(FileEntryIter::WalkDir {
694 input_path: path.into(),
695 iter,
696 game_kind,
697 })
698 }
699
700 pub fn new_rgssad_path<P>(path: P) -> anyhow::Result<Self>
702 where
703 P: AsRef<Path>,
704 {
705 let path = path.as_ref();
706 let extension = path
707 .extension()
708 .context("missing extension")?
709 .to_str()
710 .context("extension is not unicode")?;
711 let game_kind: GameKind = extension.parse()?;
712 let file = File::open(path)
713 .with_context(|| format!("failed to open input file from \"{}\"", path.display()))?;
714 Self::new_rgssad_file(file, game_kind)
715 }
716
717 pub fn new_rgssad_file(file: File, game_kind: GameKind) -> anyhow::Result<Self> {
719 let mut reader = rgssad::Reader::new(file);
720 reader.read_header()?;
721
722 Ok(Self::Rgssad { reader, game_kind })
723 }
724
725 pub fn next_file_entry(&mut self) -> anyhow::Result<Option<FileEntry>> {
727 match self {
728 Self::WalkDir {
729 input_path, iter, ..
730 } => {
731 let entry = loop {
732 let entry = match iter.next() {
733 Some(Ok(entry)) => entry,
734 Some(Err(error)) => return Err(error).context("failed to read dir entry"),
735 None => return Ok(None),
736 };
737
738 if entry.depth() == 1
741 && ![OsStr::new("Data"), OsStr::new("Graphics")]
742 .contains(&entry.file_name())
743 {
744 if entry.file_type().is_dir() {
745 iter.skip_current_dir();
746 }
747 continue;
748 }
749
750 if entry.file_type().is_dir() {
752 continue;
753 }
754
755 break entry;
756 };
757 ensure!(!entry.path_is_symlink());
758
759 let file = File::open(entry.path())?;
760
761 let entry_path = entry.into_path();
762 let relative_path = entry_path.strip_prefix(input_path)?;
763 let relative_path = relative_path
764 .to_str()
765 .context("relative path is not utf8")?;
766
767 Ok(Some(FileEntry::WalkDir {
768 relative_path: relative_path.into(),
769 file,
770 }))
771 }
772 Self::Rgssad { reader, .. } => {
773 let file = match reader.read_file()? {
774 Some(file) => file,
775 None => return Ok(None),
776 };
777
778 Ok(Some(FileEntry::Rgssad { file }))
779 }
780 }
781 }
782
783 pub fn game_kind(&self) -> GameKind {
785 match self {
786 Self::WalkDir { game_kind, .. } => *game_kind,
787 Self::Rgssad { game_kind, .. } => *game_kind,
788 }
789 }
790}
791
792pub enum FileEntry<'a> {
794 WalkDir {
795 relative_path: Utf8PathBuf,
796 file: File,
797 },
798 Rgssad {
799 file: rgssad::reader::File<'a, File>,
800 },
801}
802
803impl FileEntry<'_> {
804 pub fn relative_path(&self) -> &Utf8Path {
806 match self {
807 Self::WalkDir { relative_path, .. } => relative_path,
808 Self::Rgssad { file } => Utf8Path::new(file.name()),
809 }
810 }
811}
812
813impl Read for FileEntry<'_> {
814 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
815 match self {
816 Self::WalkDir { file, .. } => file.read(buffer),
817 Self::Rgssad { file } => file.read(buffer),
818 }
819 }
820}