1pub mod archive;
13pub mod console;
14pub mod error;
15pub mod region;
16
17use std::error::Error;
18use std::fs::{self, File};
19use std::path::Path;
20
21use serde::Serialize;
22
23use crate::archive::chd::analyze_chd_file;
24use crate::archive::zip::process_zip_file;
25use crate::console::gamegear::{self, GameGearAnalysis};
26use crate::console::gb::{self, GbAnalysis};
27use crate::console::gba::{self, GbaAnalysis};
28use crate::console::genesis::{self, GenesisAnalysis};
29use crate::console::mastersystem::{self, MasterSystemAnalysis};
30use crate::console::n64::{self, N64Analysis};
31use crate::console::nes::{self, NesAnalysis};
32use crate::console::psx::{self, PsxAnalysis};
33use crate::console::segacd::{self, SegaCdAnalysis};
34use crate::console::snes::{self, SnesAnalysis};
35use crate::error::RomAnalyzerError;
36
37pub const SUPPORTED_ROM_EXTENSIONS: &[&str] = &[
40 ".nes", ".smc", ".sfc", ".n64", ".v64", ".z64", ".sms", ".gg", ".md", ".gen", ".32x", ".gb", ".gbc", ".gba", ".scd", ".iso", ".bin", ".img", ".psx", ];
51
52#[derive(Debug, PartialEq, Clone, Serialize)]
54#[serde(tag = "console")]
55pub enum RomAnalysisResult {
56 GameGear(GameGearAnalysis),
57 GB(GbAnalysis),
58 GBA(GbaAnalysis),
59 Genesis(GenesisAnalysis),
60 MasterSystem(MasterSystemAnalysis),
61 N64(N64Analysis),
62 NES(NesAnalysis),
63 PSX(PsxAnalysis),
64 SegaCD(SegaCdAnalysis),
65 SNES(SnesAnalysis),
66}
67
68#[derive(Debug, PartialEq, Eq)]
71pub enum RomFileType {
72 Nes,
73 Snes,
74 N64,
75 MasterSystem,
76 GameGear,
77 GameBoy,
78 GameBoyAdvance,
79 Genesis,
80 SegaCD,
81 CDSystem,
82 Unknown,
83}
84
85fn get_file_extension_lowercase(file_path: &str) -> String {
96 Path::new(file_path)
97 .extension()
98 .and_then(std::ffi::OsStr::to_str)
99 .unwrap_or_default()
100 .to_lowercase()
101}
102
103pub fn get_rom_file_type(name: &str) -> RomFileType {
143 let ext = get_file_extension_lowercase(name);
144
145 match ext.as_str() {
146 "nes" => RomFileType::Nes,
147 "smc" | "sfc" => RomFileType::Snes,
148 "n64" | "v64" | "z64" => RomFileType::N64,
149 "sms" => RomFileType::MasterSystem,
150 "gg" => RomFileType::GameGear,
151 "gb" | "gbc" => RomFileType::GameBoy,
152 "gba" => RomFileType::GameBoyAdvance,
153 "md" | "gen" | "32x" => RomFileType::Genesis,
154 "scd" => RomFileType::SegaCD,
155 "iso" | "bin" | "img" | "psx" | "chd" => RomFileType::CDSystem,
156 _ => RomFileType::Unknown,
157 }
158}
159
160fn process_rom_data(data: Vec<u8>, rom_path: &str) -> Result<RomAnalysisResult, Box<dyn Error>> {
176 match get_rom_file_type(rom_path) {
177 RomFileType::Nes => nes::analyze_nes_data(&data, rom_path).map(RomAnalysisResult::NES),
178 RomFileType::Snes => snes::analyze_snes_data(&data, rom_path).map(RomAnalysisResult::SNES),
179 RomFileType::N64 => n64::analyze_n64_data(&data, rom_path).map(RomAnalysisResult::N64),
180 RomFileType::MasterSystem => mastersystem::analyze_mastersystem_data(&data, rom_path)
181 .map(RomAnalysisResult::MasterSystem),
182 RomFileType::GameGear => {
183 gamegear::analyze_gamegear_data(&data, rom_path).map(RomAnalysisResult::GameGear)
184 }
185 RomFileType::GameBoy => gb::analyze_gb_data(&data, rom_path).map(RomAnalysisResult::GB),
186 RomFileType::GameBoyAdvance => {
187 gba::analyze_gba_data(&data, rom_path).map(RomAnalysisResult::GBA)
188 }
189 RomFileType::Genesis => {
190 genesis::analyze_genesis_data(&data, rom_path).map(RomAnalysisResult::Genesis)
191 }
192 RomFileType::SegaCD => {
193 segacd::analyze_segacd_data(&data, rom_path).map(RomAnalysisResult::SegaCD)
194 }
195 RomFileType::CDSystem => {
196 const SEGA_HEADER_START: usize = 0x100;
200 const SEGA_GENESIS_HEADER_END: usize = 0x110;
201 const SEGA_CD_SIGNATURE_END: usize = 0x107;
202 const SEGA_CD_MIN_LEN: usize = 0x10C; if data.len() >= SEGA_GENESIS_HEADER_END
205 && (data[SEGA_HEADER_START..SEGA_GENESIS_HEADER_END]
206 .starts_with(b"SEGA MEGA DRIVE")
207 || data[SEGA_HEADER_START..SEGA_GENESIS_HEADER_END]
208 .starts_with(b"SEGA GENESIS"))
209 {
210 genesis::analyze_genesis_data(&data, rom_path).map(RomAnalysisResult::Genesis)
211 } else if data.len() >= SEGA_CD_MIN_LEN
212 && data[SEGA_HEADER_START..SEGA_CD_SIGNATURE_END].eq_ignore_ascii_case(b"SEGA CD")
213 {
214 segacd::analyze_segacd_data(&data, rom_path).map(RomAnalysisResult::SegaCD)
215 } else {
216 psx::analyze_psx_data(&data, rom_path).map(RomAnalysisResult::PSX)
217 }
218 }
219 RomFileType::Unknown => Err(Box::new(RomAnalyzerError::new(&format!(
220 "Unrecognized ROM file extension for dispatch: {}",
221 rom_path
222 )))
223 .into()),
224 }
225}
226
227pub fn analyze_rom_data(file_path: &str) -> Result<RomAnalysisResult, Box<dyn Error>> {
255 match get_file_extension_lowercase(file_path).as_str() {
256 "zip" => {
257 let file = File::open(file_path)?;
258 let (data, rom_file_name) = process_zip_file(file, file_path)?;
259 process_rom_data(data, &rom_file_name)
260 }
261 "chd" => {
262 let decompressed_chd = analyze_chd_file(Path::new(file_path))?;
263 process_rom_data(decompressed_chd, file_path)
264 }
265 _ => {
266 let data = fs::read(file_path)?;
267 process_rom_data(data, file_path)
268 }
269 }
270}
271
272macro_rules! impl_rom_analysis_method {
273 ($fn_name:ident, $return_type:ty) => {
274 pub fn $fn_name(&self) -> $return_type {
277 match self {
278 RomAnalysisResult::GameGear(a) => a.$fn_name(),
279 RomAnalysisResult::GB(a) => a.$fn_name(),
280 RomAnalysisResult::GBA(a) => a.$fn_name(),
281 RomAnalysisResult::Genesis(a) => a.$fn_name(),
282 RomAnalysisResult::MasterSystem(a) => a.$fn_name(),
283 RomAnalysisResult::N64(a) => a.$fn_name(),
284 RomAnalysisResult::NES(a) => a.$fn_name(),
285 RomAnalysisResult::PSX(a) => a.$fn_name(),
286 RomAnalysisResult::SegaCD(a) => a.$fn_name(),
287 RomAnalysisResult::SNES(a) => a.$fn_name(),
288 }
289 }
290 };
291}
292
293macro_rules! impl_rom_analysis_accessor {
294 ($fn_name:ident, $field:ident, &$return_type:ty) => {
295 pub fn $fn_name(&self) -> &$return_type {
297 match self {
298 RomAnalysisResult::GameGear(a) => &a.$field,
299 RomAnalysisResult::GB(a) => &a.$field,
300 RomAnalysisResult::GBA(a) => &a.$field,
301 RomAnalysisResult::Genesis(a) => &a.$field,
302 RomAnalysisResult::MasterSystem(a) => &a.$field,
303 RomAnalysisResult::N64(a) => &a.$field,
304 RomAnalysisResult::NES(a) => &a.$field,
305 RomAnalysisResult::PSX(a) => &a.$field,
306 RomAnalysisResult::SegaCD(a) => &a.$field,
307 RomAnalysisResult::SNES(a) => &a.$field,
308 }
309 }
310 };
311 ($fn_name:ident, $field:ident, $return_type:ty) => {
312 pub fn $fn_name(&self) -> $return_type {
314 match self {
315 RomAnalysisResult::GameGear(a) => a.$field,
316 RomAnalysisResult::GB(a) => a.$field,
317 RomAnalysisResult::GBA(a) => a.$field,
318 RomAnalysisResult::Genesis(a) => a.$field,
319 RomAnalysisResult::MasterSystem(a) => a.$field,
320 RomAnalysisResult::N64(a) => a.$field,
321 RomAnalysisResult::NES(a) => a.$field,
322 RomAnalysisResult::PSX(a) => a.$field,
323 RomAnalysisResult::SegaCD(a) => a.$field,
324 RomAnalysisResult::SNES(a) => a.$field,
325 }
326 }
327 };
328}
329
330impl RomAnalysisResult {
331 impl_rom_analysis_method!(print, String);
332 impl_rom_analysis_accessor!(source_name, source_name, &str);
333 impl_rom_analysis_accessor!(region, region_string, &str);
334 impl_rom_analysis_accessor!(region_mismatch, region_mismatch, bool);
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn test_get_rom_file_type() {
343 assert_eq!(get_rom_file_type("game.nes"), RomFileType::Nes);
344 assert_eq!(get_rom_file_type("game.smc"), RomFileType::Snes);
345 assert_eq!(get_rom_file_type("game.sfc"), RomFileType::Snes);
346 assert_eq!(get_rom_file_type("game.n64"), RomFileType::N64);
347 assert_eq!(get_rom_file_type("game.v64"), RomFileType::N64);
348 assert_eq!(get_rom_file_type("game.z64"), RomFileType::N64);
349 assert_eq!(get_rom_file_type("game.sms"), RomFileType::MasterSystem);
350 assert_eq!(get_rom_file_type("game.gg"), RomFileType::GameGear);
351 assert_eq!(get_rom_file_type("game.gb"), RomFileType::GameBoy);
352 assert_eq!(get_rom_file_type("game.gbc"), RomFileType::GameBoy);
353 assert_eq!(get_rom_file_type("game.gba"), RomFileType::GameBoyAdvance);
354 assert_eq!(get_rom_file_type("game.md"), RomFileType::Genesis);
355 assert_eq!(get_rom_file_type("game.gen"), RomFileType::Genesis);
356 assert_eq!(get_rom_file_type("game.32x"), RomFileType::Genesis);
357 assert_eq!(get_rom_file_type("game.scd"), RomFileType::SegaCD);
358 assert_eq!(get_rom_file_type("game.iso"), RomFileType::CDSystem);
359 assert_eq!(get_rom_file_type("game.bin"), RomFileType::CDSystem);
360 assert_eq!(get_rom_file_type("game.img"), RomFileType::CDSystem);
361 assert_eq!(get_rom_file_type("game.psx"), RomFileType::CDSystem);
362 assert_eq!(get_rom_file_type("game.chd"), RomFileType::CDSystem);
363 assert_eq!(get_rom_file_type("game.zip"), RomFileType::Unknown);
364 assert_eq!(get_rom_file_type("game.txt"), RomFileType::Unknown);
365 }
366
367 #[test]
368 fn test_process_rom_data_unrecognized_extension() {
369 let data = vec![];
370 let name = "game.xyz";
371 let result = process_rom_data(data, name);
372 let err = result.expect_err(
373 "process_rom_data should have returned an error for unrecognized extension",
374 );
375 assert!(err.to_string().contains("Unrecognized ROM file extension"));
376 }
377
378 #[test]
379 fn test_process_rom_data_cd_system_sega_genesis_header() {
380 let mut data = vec![0; 0x120];
381 data[0x100..0x110].copy_from_slice(b"SEGA MEGA DRIVE\0"); let name = "game.bin";
383 let result = process_rom_data(data, name);
387 assert!(result.is_err());
389 let err = result.expect_err("process_rom_data should have returned an error for mock data");
390 assert!(!err.to_string().contains("Unrecognized ROM file extension"));
391 }
392
393 #[test]
394 fn test_process_rom_data_cd_system_sega_cd_header() {
395 let mut data = vec![0; 0x120];
396 data[0x100..0x107].copy_from_slice(b"SEGA CD");
397 let name = "game.iso";
398 let result = process_rom_data(data, name);
399 let err = result.expect_err("process_rom_data should have returned an error for mock data");
400 assert!(!err.to_string().contains("Unrecognized ROM file extension"));
401 }
402
403 #[test]
404 fn test_process_rom_data_cd_system_psx() {
405 let data = vec![0; 0x100]; let name = "game.bin";
407 let result = process_rom_data(data, name);
408 let err = result.expect_err("process_rom_data should have returned an error for mock data");
409 assert!(!err.to_string().contains("Unrecognized ROM file extension"));
410 }
411}