rpgmxp_tool/commands/extract_assets/
file_entry_iter.rs

1mod util;
2
3pub use self::util::guess_game_kind_from_exe;
4use crate::GameKind;
5use anyhow::bail;
6use anyhow::ensure;
7use anyhow::Context;
8use camino::Utf8Path;
9use camino::Utf8PathBuf;
10use std::ffi::OsStr;
11use std::fs::File;
12use std::io::BufReader;
13use std::io::Read;
14use std::path::Path;
15use std::path::PathBuf;
16use walkdir::WalkDir;
17
18/// A lending iter over files.
19pub enum FileEntryIter {
20    WalkDir {
21        input_path: PathBuf,
22        iter: walkdir::IntoIter,
23        game_kind: GameKind,
24    },
25    Rgssad {
26        reader: rgssad::Reader<File>,
27        game_kind: GameKind,
28    },
29}
30
31impl FileEntryIter {
32    /// Create a new iter from a path.
33    ///
34    /// This will determine whether the path is a dir or an rgssad.
35    pub fn new<P>(path: P) -> anyhow::Result<Self>
36    where
37        P: AsRef<Path>,
38    {
39        let path = path.as_ref();
40
41        if !path.is_dir() {
42            // TODO: Add option to change rgssad version instead of assuming v1.
43            return Self::new_rgssad_path(path);
44        }
45
46        let rgssad_path = path.join("Game.rgssad");
47        match File::open(&rgssad_path) {
48            Ok(file) => {
49                return Self::new_rgssad_file(file, GameKind::Xp);
50            }
51            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
52            Err(error) => {
53                return Err(error)
54                    .with_context(|| format!("failed to open \"{}\"", rgssad_path.display()));
55            }
56        };
57
58        let rgssad_path = path.join("Game.rgss2a");
59        match File::open(&rgssad_path) {
60            Ok(file) => {
61                return Self::new_rgssad_file(file, GameKind::Vx);
62            }
63            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
64            Err(error) => {
65                return Err(error)
66                    .with_context(|| format!("failed to open \"{}\"", rgssad_path.display()));
67            }
68        };
69
70        ensure!(
71            path.join("Data").exists(),
72            "Data directory is missing. Are you sure the input folder is correct?"
73        );
74        ensure!(
75            path.join("Graphics").exists(),
76            "Graphics directory is missing. Are you sure the input folder is correct?"
77        );
78
79        Self::new_walkdir_path(path)
80    }
81
82    /// Create a new iter from the given dir path.
83    pub fn new_walkdir_path<P>(path: P) -> anyhow::Result<Self>
84    where
85        P: AsRef<Path>,
86    {
87        let path = path.as_ref();
88
89        let game_kind = (|| {
90            let game_path = path.join("Game.exe");
91            let game_exe = std::fs::read(&game_path)
92                .with_context(|| format!("failed to read \"{}\"", game_path.display()))?;
93
94            if let Some(game_kind) =
95                guess_game_kind_from_exe(&game_exe).context("failed to guess game type")?
96            {
97                return Ok(game_kind);
98            }
99
100            bail!("failed to determine game type");
101        })()?;
102
103        let iter = WalkDir::new(path).into_iter();
104
105        Ok(FileEntryIter::WalkDir {
106            input_path: path.into(),
107            iter,
108            game_kind,
109        })
110    }
111
112    /// Create a new iter from the given rgssad path.
113    pub fn new_rgssad_path<P>(path: P) -> anyhow::Result<Self>
114    where
115        P: AsRef<Path>,
116    {
117        let path = path.as_ref();
118        let extension = path
119            .extension()
120            .context("missing extension")?
121            .to_str()
122            .context("extension is not unicode")?;
123        let game_kind: GameKind = extension.parse()?;
124        let file = File::open(path)
125            .with_context(|| format!("failed to open input file from \"{}\"", path.display()))?;
126        Self::new_rgssad_file(file, game_kind)
127    }
128
129    /// Create a new iter from the given rgssad file.
130    pub fn new_rgssad_file(file: File, game_kind: GameKind) -> anyhow::Result<Self> {
131        let mut reader = rgssad::Reader::new(file);
132        reader.read_header()?;
133
134        Ok(Self::Rgssad { reader, game_kind })
135    }
136
137    /// Get the next file entry.
138    pub fn next_file_entry(&mut self) -> anyhow::Result<Option<FileEntry>> {
139        match self {
140            Self::WalkDir {
141                input_path, iter, ..
142            } => {
143                let entry = loop {
144                    let entry = match iter.next() {
145                        Some(Ok(entry)) => entry,
146                        Some(Err(error)) => return Err(error).context("failed to read dir entry"),
147                        None => return Ok(None),
148                    };
149
150                    // Rgssad archives only contain the "Data" and "Graphics" folders at the top level.
151                    // Only include these folders for parity with rgssad archives.
152                    if entry.depth() == 1
153                        && ![OsStr::new("Data"), OsStr::new("Graphics")]
154                            .contains(&entry.file_name())
155                    {
156                        if entry.file_type().is_dir() {
157                            iter.skip_current_dir();
158                        }
159                        continue;
160                    }
161
162                    // Filter out dir entries, to keep similar behavior with rgssad.
163                    if entry.file_type().is_dir() {
164                        continue;
165                    }
166
167                    break entry;
168                };
169                ensure!(!entry.path_is_symlink());
170
171                let file = File::open(entry.path())?;
172
173                let entry_path = entry.into_path();
174                let relative_path = entry_path.strip_prefix(input_path)?;
175                let relative_path = relative_path
176                    .to_str()
177                    .context("relative path is not utf8")?;
178
179                Ok(Some(FileEntry::WalkDir {
180                    relative_path: relative_path.into(),
181                    file: BufReader::new(file),
182                }))
183            }
184            Self::Rgssad { reader, .. } => {
185                let file = match reader.read_file()? {
186                    Some(file) => file,
187                    None => return Ok(None),
188                };
189
190                Ok(Some(FileEntry::Rgssad { file }))
191            }
192        }
193    }
194
195    /// Get the determined game kind
196    pub fn game_kind(&self) -> GameKind {
197        match self {
198            Self::WalkDir { game_kind, .. } => *game_kind,
199            Self::Rgssad { game_kind, .. } => *game_kind,
200        }
201    }
202}
203
204/// A file entry
205pub enum FileEntry<'a> {
206    WalkDir {
207        relative_path: Utf8PathBuf,
208        file: BufReader<File>,
209    },
210    Rgssad {
211        file: rgssad::reader::File<'a, File>,
212    },
213}
214
215impl FileEntry<'_> {
216    /// Get the relative path of this entry.
217    pub fn relative_path(&self) -> &Utf8Path {
218        match self {
219            Self::WalkDir { relative_path, .. } => relative_path,
220            Self::Rgssad { file } => Utf8Path::new(file.name()),
221        }
222    }
223}
224
225impl Read for FileEntry<'_> {
226    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
227        match self {
228            Self::WalkDir { file, .. } => file.read(buffer),
229            Self::Rgssad { file } => file.read(buffer),
230        }
231    }
232}