1pub mod archive;
13pub mod console;
14pub mod error;
15pub mod region;
16
17use std::fs::{self, File};
18use std::path::Path;
19
20use serde::Serialize;
21
22use crate::archive::chd::analyze_chd_file;
23use crate::archive::zip::process_zip_file;
24use crate::console::gamegear::{self, GameGearAnalysis};
25use crate::console::gb::{self, GbAnalysis};
26use crate::console::gba::{self, GbaAnalysis};
27use crate::console::genesis::{self, GenesisAnalysis};
28use crate::console::mastersystem::{self, MasterSystemAnalysis};
29use crate::console::n64::{self, N64Analysis};
30use crate::console::nes::{self, NesAnalysis};
31use crate::console::psx::{self, PsxAnalysis};
32use crate::console::segacd::{self, SegaCdAnalysis};
33use crate::console::snes::{self, SnesAnalysis};
34use crate::error::RomAnalyzerError;
35
36pub const SUPPORTED_ROM_EXTENSIONS: &[&str] = &[
39 ".nes", ".smc", ".sfc", ".n64", ".v64", ".z64", ".sms", ".gg", ".md", ".gen", ".32x", ".gb", ".gbc", ".gba", ".scd", ".iso", ".bin", ".img", ".psx", ];
50
51pub const SEGA_MEGA_DRIVE_SIG: &[u8] = b"SEGA MEGA DRIVE";
52pub const SEGA_GENESIS_SIG: &[u8] = b"SEGA GENESIS";
53
54#[derive(Debug, PartialEq, Clone, Serialize)]
56#[serde(tag = "console")]
57pub enum RomAnalysisResult {
58 GameGear(GameGearAnalysis),
59 GB(GbAnalysis),
60 GBA(GbaAnalysis),
61 Genesis(GenesisAnalysis),
62 MasterSystem(MasterSystemAnalysis),
63 N64(N64Analysis),
64 NES(NesAnalysis),
65 PSX(PsxAnalysis),
66 SegaCD(SegaCdAnalysis),
67 SNES(SnesAnalysis),
68}
69
70#[derive(Debug, PartialEq, Eq)]
73pub enum RomFileType {
74 Nes,
75 Snes,
76 N64,
77 MasterSystem,
78 GameGear,
79 GameBoy,
80 GameBoyAdvance,
81 Genesis,
82 SegaCD,
83 CDSystem,
84 Unknown,
85}
86
87fn get_file_extension_lowercase(file_path: &str) -> String {
98 Path::new(file_path)
99 .extension()
100 .and_then(std::ffi::OsStr::to_str)
101 .unwrap_or_default()
102 .to_lowercase()
103}
104
105pub fn get_rom_file_type(name: &str) -> RomFileType {
145 let ext = get_file_extension_lowercase(name);
146
147 match ext.as_str() {
148 "nes" => RomFileType::Nes,
149 "smc" | "sfc" => RomFileType::Snes,
150 "n64" | "v64" | "z64" => RomFileType::N64,
151 "sms" => RomFileType::MasterSystem,
152 "gg" => RomFileType::GameGear,
153 "gb" | "gbc" => RomFileType::GameBoy,
154 "gba" => RomFileType::GameBoyAdvance,
155 "md" | "gen" | "32x" => RomFileType::Genesis,
156 "scd" => RomFileType::SegaCD,
157 "iso" | "bin" | "img" | "psx" | "chd" => RomFileType::CDSystem,
158 _ => RomFileType::Unknown,
159 }
160}
161
162fn process_rom_data(data: Vec<u8>, rom_path: &str) -> Result<RomAnalysisResult, RomAnalyzerError> {
178 match get_rom_file_type(rom_path) {
179 RomFileType::Nes => nes::analyze_nes_data(&data, rom_path).map(RomAnalysisResult::NES),
180 RomFileType::Snes => snes::analyze_snes_data(&data, rom_path).map(RomAnalysisResult::SNES),
181 RomFileType::N64 => n64::analyze_n64_data(&data, rom_path).map(RomAnalysisResult::N64),
182 RomFileType::MasterSystem => mastersystem::analyze_mastersystem_data(&data, rom_path)
183 .map(RomAnalysisResult::MasterSystem),
184 RomFileType::GameGear => {
185 gamegear::analyze_gamegear_data(&data, rom_path).map(RomAnalysisResult::GameGear)
186 }
187 RomFileType::GameBoy => gb::analyze_gb_data(&data, rom_path).map(RomAnalysisResult::GB),
188 RomFileType::GameBoyAdvance => {
189 gba::analyze_gba_data(&data, rom_path).map(RomAnalysisResult::GBA)
190 }
191 RomFileType::Genesis => {
192 genesis::analyze_genesis_data(&data, rom_path).map(RomAnalysisResult::Genesis)
193 }
194 RomFileType::SegaCD => {
195 segacd::analyze_segacd_data(&data, rom_path).map(RomAnalysisResult::SegaCD)
196 }
197 RomFileType::CDSystem => {
198 const SEGA_HEADER_START: usize = 0x100;
202 const SEGA_GENESIS_HEADER_END: usize = 0x110;
203 const SEGA_CD_SIGNATURE_END: usize = 0x107;
204 const SEGA_CD_MIN_LEN: usize = 0x10C; if data.len() >= SEGA_GENESIS_HEADER_END
207 && (data[SEGA_HEADER_START..SEGA_GENESIS_HEADER_END]
208 .starts_with(SEGA_MEGA_DRIVE_SIG)
209 || data[SEGA_HEADER_START..SEGA_GENESIS_HEADER_END]
210 .starts_with(SEGA_GENESIS_SIG))
211 {
212 genesis::analyze_genesis_data(&data, rom_path).map(RomAnalysisResult::Genesis)
213 } else if data.len() >= SEGA_CD_MIN_LEN
214 && data[SEGA_HEADER_START..SEGA_CD_SIGNATURE_END].eq_ignore_ascii_case(b"SEGA CD")
215 {
216 segacd::analyze_segacd_data(&data, rom_path).map(RomAnalysisResult::SegaCD)
217 } else {
218 psx::analyze_psx_data(&data, rom_path).map(RomAnalysisResult::PSX)
219 }
220 }
221 RomFileType::Unknown => Err(RomAnalyzerError::UnsupportedFormat(format!(
222 "Unrecognized ROM file extension for dispatch: {}",
223 rom_path
224 ))),
225 }
226}
227
228pub fn analyze_rom_data(file_path: &str) -> Result<RomAnalysisResult, RomAnalyzerError> {
256 match get_file_extension_lowercase(file_path).as_str() {
257 "zip" => {
258 let file = File::open(file_path)?;
259 let (data, rom_file_name) = process_zip_file(file, file_path)?;
260 process_rom_data(data, &rom_file_name)
261 }
262 "chd" => {
263 let decompressed_chd = analyze_chd_file(Path::new(file_path))?;
264 process_rom_data(decompressed_chd, file_path)
265 }
266 _ => {
267 let data = fs::read(file_path)?;
268 process_rom_data(data, file_path)
269 }
270 }
271}
272
273macro_rules! impl_rom_analysis_method {
274 ($fn_name:ident, $return_type:ty) => {
275 pub fn $fn_name(&self) -> $return_type {
278 match self {
279 RomAnalysisResult::GameGear(a) => a.$fn_name(),
280 RomAnalysisResult::GB(a) => a.$fn_name(),
281 RomAnalysisResult::GBA(a) => a.$fn_name(),
282 RomAnalysisResult::Genesis(a) => a.$fn_name(),
283 RomAnalysisResult::MasterSystem(a) => a.$fn_name(),
284 RomAnalysisResult::N64(a) => a.$fn_name(),
285 RomAnalysisResult::NES(a) => a.$fn_name(),
286 RomAnalysisResult::PSX(a) => a.$fn_name(),
287 RomAnalysisResult::SegaCD(a) => a.$fn_name(),
288 RomAnalysisResult::SNES(a) => a.$fn_name(),
289 }
290 }
291 };
292}
293
294macro_rules! impl_rom_analysis_accessor {
295 ($fn_name:ident, $field:ident, &$return_type:ty) => {
296 pub fn $fn_name(&self) -> &$return_type {
298 match self {
299 RomAnalysisResult::GameGear(a) => &a.$field,
300 RomAnalysisResult::GB(a) => &a.$field,
301 RomAnalysisResult::GBA(a) => &a.$field,
302 RomAnalysisResult::Genesis(a) => &a.$field,
303 RomAnalysisResult::MasterSystem(a) => &a.$field,
304 RomAnalysisResult::N64(a) => &a.$field,
305 RomAnalysisResult::NES(a) => &a.$field,
306 RomAnalysisResult::PSX(a) => &a.$field,
307 RomAnalysisResult::SegaCD(a) => &a.$field,
308 RomAnalysisResult::SNES(a) => &a.$field,
309 }
310 }
311 };
312 ($fn_name:ident, $field:ident, $return_type:ty) => {
313 pub fn $fn_name(&self) -> $return_type {
315 match self {
316 RomAnalysisResult::GameGear(a) => a.$field,
317 RomAnalysisResult::GB(a) => a.$field,
318 RomAnalysisResult::GBA(a) => a.$field,
319 RomAnalysisResult::Genesis(a) => a.$field,
320 RomAnalysisResult::MasterSystem(a) => a.$field,
321 RomAnalysisResult::N64(a) => a.$field,
322 RomAnalysisResult::NES(a) => a.$field,
323 RomAnalysisResult::PSX(a) => a.$field,
324 RomAnalysisResult::SegaCD(a) => a.$field,
325 RomAnalysisResult::SNES(a) => a.$field,
326 }
327 }
328 };
329}
330
331impl RomAnalysisResult {
332 impl_rom_analysis_method!(print, String);
333 impl_rom_analysis_accessor!(source_name, source_name, &str);
334 impl_rom_analysis_accessor!(region, region_string, &str);
335 impl_rom_analysis_accessor!(region_mismatch, region_mismatch, bool);
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use std::io::Write;
342 use tempfile::tempdir;
343 use zip::write::{FileOptions, ZipWriter};
344
345 const TEST_SEGA_MEGA_DRIVE_HEADER: &[u8] = b"SEGA MEGA DRIVE "; const TEST_SEGA_GENESIS_HEADER: &[u8] = b"SEGA GENESIS ";
347
348 #[test]
349 fn test_get_rom_file_type() {
350 assert_eq!(get_rom_file_type("game.nes"), RomFileType::Nes);
351 assert_eq!(get_rom_file_type("game.smc"), RomFileType::Snes);
352 assert_eq!(get_rom_file_type("game.sfc"), RomFileType::Snes);
353 assert_eq!(get_rom_file_type("game.n64"), RomFileType::N64);
354 assert_eq!(get_rom_file_type("game.v64"), RomFileType::N64);
355 assert_eq!(get_rom_file_type("game.z64"), RomFileType::N64);
356 assert_eq!(get_rom_file_type("game.sms"), RomFileType::MasterSystem);
357 assert_eq!(get_rom_file_type("game.gg"), RomFileType::GameGear);
358 assert_eq!(get_rom_file_type("game.gb"), RomFileType::GameBoy);
359 assert_eq!(get_rom_file_type("game.gbc"), RomFileType::GameBoy);
360 assert_eq!(get_rom_file_type("game.gba"), RomFileType::GameBoyAdvance);
361 assert_eq!(get_rom_file_type("game.md"), RomFileType::Genesis);
362 assert_eq!(get_rom_file_type("game.gen"), RomFileType::Genesis);
363 assert_eq!(get_rom_file_type("game.32x"), RomFileType::Genesis);
364 assert_eq!(get_rom_file_type("game.scd"), RomFileType::SegaCD);
365 assert_eq!(get_rom_file_type("game.iso"), RomFileType::CDSystem);
366 assert_eq!(get_rom_file_type("game.bin"), RomFileType::CDSystem);
367 assert_eq!(get_rom_file_type("game.img"), RomFileType::CDSystem);
368 assert_eq!(get_rom_file_type("game.psx"), RomFileType::CDSystem);
369 assert_eq!(get_rom_file_type("game.chd"), RomFileType::CDSystem);
370 assert_eq!(get_rom_file_type("game.zip"), RomFileType::Unknown);
371 assert_eq!(get_rom_file_type("game.txt"), RomFileType::Unknown);
372 }
373
374 #[test]
375 fn test_process_rom_data_unrecognized_extension() {
376 let data = vec![];
377 let name = "game.xyz";
378 let result = process_rom_data(data, name);
379 let err = result.expect_err(
380 "process_rom_data should have returned an error for unrecognized extension",
381 );
382 assert!(err.to_string().contains("Unrecognized ROM file extension"));
383 }
384
385 #[test]
386 fn test_process_rom_data_cd_system_sega_genesis_header() {
387 let mut data = vec![0; 0x120];
388 data[0x100..0x110].copy_from_slice(TEST_SEGA_MEGA_DRIVE_HEADER);
389 let name = "game.bin";
390 let result = process_rom_data(data, name);
394 assert!(result.is_err());
396 let err = result.expect_err("process_rom_data should have returned an error for mock data");
397 assert!(!err.to_string().contains("Unrecognized ROM file extension"));
398 assert!(!err.to_string().contains("PSX"));
399 }
400
401 #[test]
402 fn test_process_rom_data_cd_system_sega_genesis_header_genesis() {
403 let mut data = vec![0; 0x120];
404 data[0x100..0x110].copy_from_slice(TEST_SEGA_GENESIS_HEADER);
405 let name = "game.bin";
406 let result = process_rom_data(data, name);
407 assert!(result.is_err());
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 assert!(!err.to_string().contains("PSX"));
411 }
412
413 #[test]
414 fn test_process_rom_data_cd_system_sega_cd_header() {
415 let mut data = vec![0; 0x120];
416 data[0x100..0x107].copy_from_slice(b"SEGA CD");
417 let name = "game.iso";
418 let result = process_rom_data(data, name);
419 let err = result.expect_err("process_rom_data should have returned an error for mock data");
420 assert!(!err.to_string().contains("Unrecognized ROM file extension"));
421 }
422
423 #[test]
424 fn test_process_rom_data_cd_system_psx() {
425 let data = vec![0; 0x100]; let name = "game.bin";
427 let result = process_rom_data(data, name);
428 let err = result.expect_err("process_rom_data should have returned an error for mock data");
429 assert!(!err.to_string().contains("Unrecognized ROM file extension"));
430 }
431
432 #[test]
433 fn test_analyze_rom_data_zip() {
434 let dir = tempdir().unwrap();
435 let zip_path = dir.path().join("test.zip");
436 let zip_file = File::create(&zip_path).unwrap();
437 let mut zip = ZipWriter::new(zip_file);
438 zip.start_file("game.nes", FileOptions::default()).unwrap();
439 zip.write_all(b"NES ROM DATA").unwrap();
440 zip.finish().unwrap();
441 let zip_path_str = zip_path.to_str().unwrap();
442 let result = analyze_rom_data(zip_path_str);
443 assert!(result.is_err());
444 let err = result.unwrap_err();
445 assert!(!err.to_string().contains("Unrecognized ROM file extension"));
446 }
447
448 #[test]
449 fn test_analyze_rom_data_chd() {
450 let dir = tempdir().unwrap();
451 let chd_path = dir.path().join("test.chd");
452 std::fs::write(&chd_path, b"invalid chd data").unwrap();
453 let chd_path_str = chd_path.to_str().unwrap();
454 let result = analyze_rom_data(chd_path_str);
455 assert!(result.is_err());
456 let err = result.unwrap_err();
457 assert!(!err.to_string().contains("Unrecognized ROM file extension"));
458 assert!(!err.to_string().contains("PSX"));
459 }
460}