rpgmxp_tool/commands/extract_assets/file_entry_iter/
util.rs1use crate::GameKind;
2use anyhow::bail;
3use anyhow::ensure;
4use anyhow::Context;
5use object::pe::RT_VERSION;
6use object::LittleEndian as LE;
7use object::U16;
8use object::U32;
9
10#[derive(serde::Deserialize, Debug)]
11#[expect(dead_code)]
12pub struct AssemblyIdentity {
13 #[serde(rename = "@version")]
14 pub version: String,
15
16 #[serde(rename = "@processorArchitecture")]
17 pub processor_architecture: Option<String>,
18
19 #[serde(rename = "@name")]
20 pub name: String,
21
22 #[serde(rename = "@type")]
23 pub type_: String,
24}
25
26#[derive(serde::Deserialize, Debug)]
27pub struct Assembly {
28 #[serde(rename = "assemblyIdentity")]
29 pub assembly_identity: Option<AssemblyIdentity>,
30
31 pub description: Option<Description>,
32}
33
34#[derive(serde::Deserialize, Debug)]
35pub struct Description {
36 #[serde(rename = "$value")]
37 pub value: String,
38}
39
40#[derive(Debug)]
41#[expect(dead_code)]
42struct VersionInfo {
43 pub fixed_file_info: Option<FixedFileInfo>,
44 pub string_file_info: Option<StringFileInfo>,
45}
46
47impl VersionInfo {
48 fn parse<'data, R>(reader: R, offset: &mut u64, expected_size: u64) -> anyhow::Result<Self>
50 where
51 R: object::read::ReadRef<'data>,
52 {
53 let start_offset = *offset;
54
55 let _length: U16<LE> = *reader.read(offset).ok().context("failed to read length")?;
56
57 let value_length: U16<LE> = *reader
58 .read(offset)
59 .ok()
60 .context("failed to read value length")?;
61
62 let type_: U16<LE> = *reader.read(offset).ok().context("failed to read type")?;
63 ensure!(type_.get(LE) == 0, "text version data is not supported");
64
65 let expected_key = "VS_VERSION_INFO\0";
66 let key: &[u16] = reader
67 .read_slice(offset, expected_key.len())
68 .ok()
69 .context("failed to read key")?;
70 let key = String::from_utf16(key)?;
71 ensure!(expected_key == key);
72
73 read_padding(reader, offset)?;
74
75 let value_length_u64 = u64::from(value_length.get(LE));
76 let fixed_file_info = if value_length_u64 != 0 {
77 ensure!(value_length.get(LE) == 52);
78 Some(FixedFileInfo::parse(reader, offset)?)
79 } else {
80 None
81 };
82
83 let read_size = *offset - start_offset;
84 ensure!(read_size <= expected_size);
85 if read_size == expected_size {
86 return Ok(Self {
87 fixed_file_info,
88 string_file_info: None,
89 });
90 }
91
92 let mut maybe_string_file_info: Option<Option<StringFileInfo>> = None;
93 let string_file_info_key = "StringFileInfo\0";
94 let var_file_info_key = "VarFileInfo\0";
95 let key_peek_len = std::cmp::min(string_file_info_key.len(), var_file_info_key.len());
96 loop {
97 read_padding(reader, offset)?;
98
99 let start_offset = *offset;
100
101 let length: U16<LE> = *reader.read(offset).ok().context("failed to read length")?;
102 let length = length.get(LE);
103
104 let value_length: U16<LE> = *reader
105 .read(offset)
106 .ok()
107 .context("failed to read value length")?;
108 ensure!(value_length.get(LE) == 0);
109
110 let type_: U16<LE> = *reader.read(offset).ok().context("failed to read type")?;
111 ensure!(type_.get(LE) == 1);
112
113 let key_bytes: &[u16] = reader
114 .read_slice(offset, key_peek_len)
115 .ok()
116 .context("failed to read key bytes")?;
117 let key = String::from_utf16(key_bytes)?;
118 if key == string_file_info_key[..key_peek_len] {
119 ensure!(maybe_string_file_info.is_none());
120
121 let remaining_key_bytes: &[u16] = reader
122 .read_slice(offset, string_file_info_key.len() - key_peek_len)
123 .ok()
124 .context("failed to read remaining key bytes")?;
125 let remaining_key_bytes = String::from_utf16(remaining_key_bytes)?;
126 ensure!(string_file_info_key[key_peek_len..] == remaining_key_bytes);
127
128 read_padding(reader, offset)?;
129
130 let mut children = Vec::with_capacity(1);
131 loop {
132 let table = StringTable::parse(reader, offset)?;
133 children.push(table);
134
135 let current_length = *offset - start_offset;
136 ensure!(current_length <= u64::from(length));
137 if current_length == u64::from(length) {
138 break;
139 }
140 }
141
142 let string_file_info = StringFileInfo { children };
143
144 maybe_string_file_info = Some(Some(string_file_info));
145 } else if key == var_file_info_key[..key_peek_len] {
146 break;
148 } else {
149 bail!("unknown key \"{key}\"");
150 }
151 }
152 let string_file_info = maybe_string_file_info.unwrap();
153
154 Ok(Self {
155 fixed_file_info,
156 string_file_info,
157 })
158 }
159}
160
161#[derive(Debug)]
162struct StringFileInfo {
163 pub children: Vec<StringTable>,
164}
165
166fn read_padding<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<()>
167where
168 R: object::read::ReadRef<'data>,
169{
170 let padding_size = 4 - (*offset % 4);
171 if padding_size != 4 {
172 let padding = reader
173 .read_bytes(offset, padding_size)
174 .ok()
175 .context("failed to read padding")?;
176 ensure!(padding.iter().all(|b| *b == 0));
177 }
178
179 Ok(())
180}
181
182fn read_utf16_nul_string<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<String>
183where
184 R: object::read::ReadRef<'data>,
185{
186 let mut raw = Vec::new();
187 while raw.is_empty() || *raw.last().unwrap() != 0 {
188 let value: U16<LE> = *reader
189 .read(offset)
190 .ok()
191 .context("failed to read wide char")?;
192 raw.push(value.get(LE));
193 }
194
195 let value = String::from_utf16(&raw)?;
196
197 Ok(value)
198}
199
200#[derive(Debug)]
201#[expect(dead_code)]
202struct FixedFileInfo {
203 struct_version: u32,
204 file_version: u64,
205 product_version: u64,
206 file_flags_mask: u32,
207 file_flags: u32,
208 file_os: u32,
209 file_type: u32,
210 file_subtype: u32,
211 file_date: u64,
212}
213
214impl FixedFileInfo {
215 fn parse<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<Self>
216 where
217 R: object::read::ReadRef<'data>,
218 {
219 let signature: U32<LE> = *reader
220 .read(offset)
221 .ok()
222 .context("failed to read signature")?;
223 ensure!(signature.get(LE) == 0xFEEF04BD);
224
225 let struct_version: U32<LE> = *reader
226 .read(offset)
227 .ok()
228 .context("failed to read struct version")?;
229 let struct_version = struct_version.get(LE);
230
231 let file_version_ms: U32<LE> = *reader
232 .read(offset)
233 .ok()
234 .context("failed to read file version ms")?;
235 let file_version_ls: U32<LE> = *reader
236 .read(offset)
237 .ok()
238 .context("failed to read file version ls")?;
239 let file_version =
240 (u64::from(file_version_ms.get(LE)) << 32) | u64::from(file_version_ls.get(LE));
241
242 let product_version_ms: U32<LE> = *reader
243 .read(offset)
244 .ok()
245 .context("failed to read product version ms")?;
246 let product_version_ls: U32<LE> = *reader
247 .read(offset)
248 .ok()
249 .context("failed to read product version ls")?;
250 let product_version =
251 (u64::from(product_version_ms.get(LE)) << 32) | u64::from(product_version_ls.get(LE));
252
253 let file_flags_mask: U32<LE> = *reader
254 .read(offset)
255 .ok()
256 .context("failed to read file flags mask")?;
257 let file_flags_mask = file_flags_mask.get(LE);
258
259 let file_flags: U32<LE> = *reader
260 .read(offset)
261 .ok()
262 .context("failed to read file flags")?;
263 let file_flags = file_flags.get(LE);
264
265 let file_os: U32<LE> = *reader.read(offset).ok().context("failed to read file os")?;
266 let file_os = file_os.get(LE);
267
268 let file_type: U32<LE> = *reader
269 .read(offset)
270 .ok()
271 .context("failed to read file type")?;
272 let file_type = file_type.get(LE);
273
274 let file_subtype: U32<LE> = *reader
275 .read(offset)
276 .ok()
277 .context("failed to read file subtype")?;
278 let file_subtype = file_subtype.get(LE);
279
280 let file_date_ms: U32<LE> = *reader
281 .read(offset)
282 .ok()
283 .context("failed to read file date ms")?;
284
285 let file_date_ls: U32<LE> = *reader
286 .read(offset)
287 .ok()
288 .context("failed to read file date ls")?;
289 let file_date = (u64::from(file_date_ms.get(LE)) << 32) | u64::from(file_date_ls.get(LE));
290
291 Ok(Self {
292 struct_version,
293 file_version,
294 product_version,
295 file_flags_mask,
296 file_flags,
297 file_os,
298 file_type,
299 file_subtype,
300 file_date,
301 })
302 }
303}
304
305#[derive(Debug)]
306#[allow(dead_code)]
307struct StringTable {
308 pub key: String,
309 pub children: Vec<StringStruct>,
310}
311
312impl StringTable {
313 fn parse<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<Self>
314 where
315 R: object::read::ReadRef<'data>,
316 {
317 let start_offset = *offset;
318
319 let length: U16<LE> = *reader.read(offset).ok().context("failed to read length")?;
320 let length = length.get(LE);
321
322 let value_length: U16<LE> = *reader
323 .read(offset)
324 .ok()
325 .context("failed to read value length")?;
326 ensure!(value_length.get(LE) == 0);
327
328 let type_: U16<LE> = *reader.read(offset).ok().context("failed to read type")?;
329 ensure!(type_.get(LE) == 1);
330
331 let key: &[u16] = reader
332 .read_slice(offset, 8)
333 .ok()
334 .context("failed to read key")?;
335 let key = String::from_utf16(key)?;
336 ensure!(key.bytes().all(|b| b.is_ascii_hexdigit()));
337 ensure!(key.len() == 8);
338
339 read_padding(reader, offset)?;
340
341 let mut children = Vec::new();
342 loop {
343 let string = StringStruct::parse(reader, offset)?;
344 children.push(string);
345
346 let current_length = *offset - start_offset;
347 ensure!(current_length <= u64::from(length));
348 if current_length == u64::from(length) {
349 break;
350 }
351
352 read_padding(reader, offset)?;
353 }
354
355 Ok(Self { key, children })
356 }
357
358 }
370
371#[derive(Debug)]
372struct StringStruct {
373 pub key: String,
374 pub value: Vec<u16>,
375}
376
377impl StringStruct {
378 fn parse<'data, R>(reader: R, offset: &mut u64) -> anyhow::Result<Self>
379 where
380 R: object::read::ReadRef<'data>,
381 {
382 let start_offset = *offset;
383
384 let length: U16<LE> = *reader.read(offset).ok().context("failed to read length")?;
385 let length = length.get(LE);
386
387 let value_length: U16<LE> = *reader
388 .read(offset)
389 .ok()
390 .context("failed to read value length")?;
391 let value_length = value_length.get(LE);
392
393 let type_: U16<LE> = *reader
394 .read(offset)
395 .ok()
396 .context("failed to read value length")?;
397 let type_ = type_.get(LE);
398 ensure!(type_ == 1, "unsupported string struct type {type_}");
399
400 let key = read_utf16_nul_string(reader, offset)?;
401
402 read_padding(reader, offset)?;
403
404 let value: &[U16<LE>] = reader
405 .read_slice(offset, value_length.into())
406 .ok()
407 .context("failed to read value")?;
408 let value: Vec<u16> = value.iter().map(|value| value.get(LE)).collect();
409
410 ensure!(*offset - start_offset == u64::from(length));
411
412 Ok(Self { key, value })
413 }
414}
415
416fn guess_from_version_entry(
417 game_exe: &[u8],
418 section_table: object::read::pe::SectionTable<'_>,
419 resource_directory: object::read::pe::ResourceDirectory<'_>,
420 root: &object::read::pe::ResourceDirectoryTable<'_>,
421) -> anyhow::Result<Option<GameKind>> {
422 let entry = root
423 .entries
424 .iter()
425 .find(|entry| entry.name_or_id().id() == Some(RT_VERSION));
426 let entry = match entry {
427 Some(entry) => entry,
428 None => return Ok(None),
429 };
430
431 let data = entry.data(resource_directory)?;
432 let table = data.table().context("object VERSION data is not a table")?;
433
434 let data = table
435 .entries
436 .first()
437 .context("object VERSION table missing entry 0")?
438 .data(resource_directory)?;
439 let table = data
440 .table()
441 .context("object VERSION table entry 0 is not a table")?;
442
443 let data = table
444 .entries
445 .first()
446 .context("object VERSION table entry 0 table missing entry 0")?
447 .data(resource_directory)?
448 .data()
449 .context("object VERSION table entry 0 table entry 0 is not data")?;
450 let offset = data.offset_to_data.get(LE);
451 let size = usize::try_from(data.size.get(LE))?;
452 let (offset, _) = section_table
455 .pe_file_range_at(offset)
456 .context("section missing version offset address")?;
457 let mut offset = u64::from(offset);
458 let version_info = VersionInfo::parse(game_exe, &mut offset, u64::try_from(size)?)?;
459
460 let string_file_info = match version_info.string_file_info.as_ref() {
461 Some(string_file_info) => string_file_info,
462 None => return Ok(None),
463 };
464
465 for table in string_file_info.children.iter() {
466 for string in table.children.iter() {
467 if string.key != "FileDescription\0" {
468 continue;
469 }
470
471 let value = String::from_utf16(&string.value)?;
473 match value.as_str() {
474 "RGSS Player\0" => return Ok(Some(GameKind::Xp)),
475 "RGSS2 Player\0" => return Ok(Some(GameKind::Vx)),
476 "RGSS3 Player\0" => return Ok(Some(GameKind::VxAce)),
477 _ => {}
478 }
479 }
480 }
481
482 Ok(None)
483}
484
485fn guess_from_manifest_entry(
486 game_exe: &[u8],
487 section_table: object::read::pe::SectionTable<'_>,
488 resource_directory: object::read::pe::ResourceDirectory<'_>,
489 root: &object::read::pe::ResourceDirectoryTable<'_>,
490) -> anyhow::Result<Option<GameKind>> {
491 use object::pe::RT_MANIFEST;
492 use object::LittleEndian as LE;
493
494 let manifest_entry = root
495 .entries
496 .iter()
497 .find(|entry| entry.name_or_id().id() == Some(RT_MANIFEST));
498 let manifest_entry = match manifest_entry {
499 Some(manifest_entry) => manifest_entry,
500 None => return Ok(None),
501 };
502
503 let manifest_entry_data = manifest_entry.data(resource_directory)?;
504 let manifest_entry_table = manifest_entry_data
505 .table()
506 .context("object MANIFEST data is not a table")?;
507
508 let manifest_entry_table_entry_data = manifest_entry_table
509 .entries
510 .first()
511 .context("object MANIFEST table missing entry 0")?
512 .data(resource_directory)?;
513 let manifest_entry_table_entry_data_table = manifest_entry_table_entry_data
514 .table()
515 .context("object MANIFEST table entry 0 is not a table")?;
516
517 let manifest_entry_table_entry_data_table_entry_data = manifest_entry_table_entry_data_table
518 .entries
519 .first()
520 .context("object MANIFEST table entry 0 table missing entry 0")?
521 .data(resource_directory)?
522 .data()
523 .context("object MANIFEST table entry 0 table entry 0 is not data")?;
524 let manifest_offset = manifest_entry_table_entry_data_table_entry_data
525 .offset_to_data
526 .get(LE);
527 let manifest_size = usize::try_from(
528 manifest_entry_table_entry_data_table_entry_data
529 .size
530 .get(LE),
531 )?;
532 let code_page = manifest_entry_table_entry_data_table_entry_data
533 .code_page
534 .get(LE);
535
536 let bytes = §ion_table
537 .pe_data_at(game_exe, manifest_offset)
538 .context("failed to get object manifest bytes")?
539 .get(..manifest_size)
540 .context("object manifest smaller than declared")?;
541
542 let manifest_string = match code_page {
543 0 => {
544 std::str::from_utf8(bytes)?.to_string()
549 }
550 1252 => {
551 let (value, _encoding, malformed) = encoding_rs::WINDOWS_1252.decode(bytes);
552 ensure!(!malformed);
553
554 value.into()
555 }
556 _ => bail!("unknown MANIFEST LCID {code_page}"),
557 };
558
559 let manifest: Assembly =
560 quick_xml::de::from_str(&manifest_string).context("failed to parse manifest string")?;
561 if manifest
562 .assembly_identity
563 .is_some_and(|assembly_identity| assembly_identity.name == "Enterbrain.RGSS.Game")
564 && manifest
565 .description
566 .as_ref()
567 .map(|description| description.value.as_str())
568 == Some("RGSS Player")
569 {
570 return Ok(Some(GameKind::Xp));
571 }
572
573 Ok(None)
574}
575
576pub fn guess_game_kind_from_exe(game_exe: &[u8]) -> anyhow::Result<Option<GameKind>> {
579 use object::read::File;
580
581 let file = File::parse(game_exe)?;
582 let (section_table, data_directories) = match file {
583 File::Pe32(file) => (file.section_table(), file.data_directories()),
584 File::Pe64(file) => (file.section_table(), file.data_directories()),
585 _ => bail!("unknown object file format {:?}", file.format()),
586 };
587
588 let resource_directory = data_directories.resource_directory(game_exe, §ion_table)?;
589 let resource_directory = match resource_directory {
590 Some(resource_directory) => resource_directory,
591 None => return Ok(None),
592 };
593
594 let root = resource_directory.root()?;
595
596 if let Some(game_kind) =
597 guess_from_version_entry(game_exe, section_table, resource_directory, &root)?
598 {
599 return Ok(Some(game_kind));
600 }
601
602 if let Some(game_kind) =
603 guess_from_manifest_entry(game_exe, section_table, resource_directory, &root)?
604 {
605 return Ok(Some(game_kind));
606 }
607
608 Ok(None)
609}