rpgmxp_tool/commands/extract_assets/file_entry_iter/
util.rs

1use 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    /// See: https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
49    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                // TODO: Parse this
147                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    /*
359    /// Get the language code
360    pub fn language(&self) -> u16 {
361        u16::from_str_radix(&self.key[..4], 16).unwrap()
362    }
363
364    /// Get the code page
365    pub fn code_page(&self) -> u16 {
366        u16::from_str_radix(&self.key[4..], 16).unwrap()
367    }
368    */
369}
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 code_page = data.code_page.get(LE);
453
454    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            // TODO: Can this ever not be UTF16?
472            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 = &section_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            // This isn't a real LCID from what I can tell,
545            // but rather a null value. Assume ASCII for now.
546            // TODO: Detect encoding dynamically?
547
548            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
576/// See: https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
577/// See: https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a
578pub 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, &section_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}