rpgmxp_tool/commands/extract_assets/
file_entry_iter.rs

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    /// See: https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
56    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                // TODO: Parse this
154                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    /*
366    /// Get the language code
367    pub fn language(&self) -> u16 {
368        u16::from_str_radix(&self.key[..4], 16).unwrap()
369    }
370
371    /// Get the code page
372    pub fn code_page(&self) -> u16 {
373        u16::from_str_radix(&self.key[4..], 16).unwrap()
374    }
375    */
376}
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
423/// See: https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
424/// See: https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a
425fn 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, &section_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 code_page = data.code_page.get(LE);
497
498    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            // TODO: Can this ever not be UTF16?
516            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 = &section_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            // This isn't a real LCID from what I can tell,
588            // but rather a null value. Assume ASCII for now.
589            // TODO: Detect encoding dynamically?
590
591            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
610/// A lending iter over files.
611pub 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    /// Create a new iter from a path.
625    ///
626    /// This will determine whether the path is a dir or an rgssad.
627    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            // TODO: Add option to change rgssad version instead of assuming v1.
635            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    /// Create a new iter from the given dir path.
675    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    /// Create a new iter from the given rgssad path.
701    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    /// Create a new iter from the given rgssad file.
718    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    /// Get the next file entry.
726    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                    // Rgssad archives only contain the "Data" and "Graphics" folders at the top level.
739                    // Only include these folders for parity with rgssad archives.
740                    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                    // Filter out dir entries, to keep similar behavior with rgssad.
751                    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    /// Get the determined game kind
784    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
792/// A file entry
793pub 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    /// Get the relative path of this entry.
805    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}