use log::error;
use serde::Serialize;
use crate::error::RomAnalyzerError;
use crate::region::{Region, check_region_mismatch};
use crate::{SEGA_GENESIS_SIG, SEGA_MEGA_DRIVE_SIG};
const SYSTEM_TYPE_START: usize = 0x100;
const SYSTEM_TYPE_END: usize = 0x110;
const DOMESTIC_TITLE_START: usize = 0x120;
const DOMESTIC_TITLE_END: usize = 0x150;
const INTL_TITLE_START: usize = 0x150;
const INTL_TITLE_END: usize = 0x180;
const REGION_CODE_BYTE: usize = 0x1F0;
#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct GenesisAnalysis {
pub source_name: String,
pub region: Region,
pub region_string: String,
pub region_mismatch: bool,
pub region_code_byte: u8,
pub console_name: String,
pub game_title_domestic: String,
pub game_title_international: String,
}
impl GenesisAnalysis {
pub fn print(&self) -> String {
format!(
"{}\n\
System: {}\n\
Game Title (Domestic): {}\n\
Game Title (Int.): {}\n\
Region Code: 0x{:02X} ('{}')\n\
Region: {}",
self.source_name,
self.console_name,
self.game_title_domestic,
self.game_title_international,
self.region_code_byte,
self.region_code_byte as char,
self.region
)
}
}
pub fn map_region(region_byte: u8) -> (&'static str, Region) {
match region_byte {
b'J' => ("Japan (NTSC-J)", Region::JAPAN),
b'U' => ("USA (NTSC-U)", Region::USA),
b'E' => ("Europe (PAL)", Region::EUROPE),
b'A' => ("Asia (NTSC)", Region::ASIA),
b'B' => ("Brazil (PAL-M)", Region::EUROPE),
b'C' => ("China (NTSC)", Region::CHINA),
b'F' => ("France (PAL)", Region::EUROPE),
b'K' => ("Korea (NTSC)", Region::KOREA),
b'L' => ("UK (PAL)", Region::EUROPE),
b'S' => ("Scandinavia (PAL)", Region::EUROPE),
b'T' => ("Taiwan (NTSC)", Region::ASIA),
0x34 => ("USA/Europe (NTSC/PAL)", Region::USA | Region::EUROPE),
_ => ("Unknown", Region::UNKNOWN),
}
}
pub fn analyze_genesis_data(
data: &[u8],
source_name: &str,
) -> Result<GenesisAnalysis, RomAnalyzerError> {
const HEADER_SIZE: usize = 0x200; if data.len() < HEADER_SIZE {
return Err(RomAnalyzerError::DataTooSmall {
file_size: data.len(),
required_size: HEADER_SIZE,
details: "Sega header".to_string(),
});
}
let console_name_bytes = &data[SYSTEM_TYPE_START..SYSTEM_TYPE_END];
let console_name = String::from_utf8_lossy(console_name_bytes)
.trim_matches(char::from(0))
.trim()
.to_string();
let is_valid_signature = console_name_bytes.starts_with(SEGA_MEGA_DRIVE_SIG)
|| console_name_bytes.starts_with(SEGA_GENESIS_SIG);
if !is_valid_signature {
error!(
"[!] Warning: Unexpected Sega header signature for {} at 0x{:x}. Found: '{}'",
source_name, SYSTEM_TYPE_START, console_name
);
}
let game_title_domestic =
String::from_utf8_lossy(&data[DOMESTIC_TITLE_START..DOMESTIC_TITLE_END])
.trim_matches(char::from(0))
.trim()
.to_string();
let game_title_international = String::from_utf8_lossy(&data[INTL_TITLE_START..INTL_TITLE_END])
.trim_matches(char::from(0))
.trim()
.to_string();
let region_code_byte = data[REGION_CODE_BYTE];
let (region_name, region) = map_region(region_code_byte);
let region_mismatch = check_region_mismatch(source_name, region);
Ok(GenesisAnalysis {
source_name: source_name.to_string(),
region,
region_string: region_name.to_string(),
region_mismatch,
region_code_byte,
console_name,
game_title_domestic,
game_title_international,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_genesis_header(
console_sig: &[u8],
region_byte: u8,
domestic_title: &str,
international_title: &str,
) -> Vec<u8> {
let mut data = vec![0; 0x200];
data[SYSTEM_TYPE_START..SYSTEM_TYPE_END].copy_from_slice(console_sig);
let mut domestic_title_bytes = domestic_title.as_bytes().to_vec();
domestic_title_bytes.resize(48, 0);
data[DOMESTIC_TITLE_START..DOMESTIC_TITLE_END].copy_from_slice(&domestic_title_bytes);
let mut international_title_bytes = international_title.as_bytes().to_vec();
international_title_bytes.resize(48, 0);
data[INTL_TITLE_START..INTL_TITLE_END].copy_from_slice(&international_title_bytes);
data[REGION_CODE_BYTE] = region_byte;
data
}
#[test]
fn test_analyze_genesis_data_usa() -> Result<(), RomAnalyzerError> {
let data =
generate_genesis_header(b"SEGA MEGA DRIVE ", b'U', "DOMESTIC US", "INTERNATIONAL US");
let analysis = analyze_genesis_data(&data, "test_rom_us.md")?;
assert_eq!(analysis.source_name, "test_rom_us.md");
assert_eq!(analysis.console_name, "SEGA MEGA DRIVE");
assert_eq!(analysis.game_title_domestic, "DOMESTIC US");
assert_eq!(analysis.game_title_international, "INTERNATIONAL US");
assert_eq!(analysis.region_code_byte, b'U');
assert_eq!(analysis.region, Region::USA);
assert_eq!(analysis.region_string, "USA (NTSC-U)");
assert_eq!(
analysis.print(),
"test_rom_us.md\n\
System: SEGA MEGA DRIVE\n\
Game Title (Domestic): DOMESTIC US\n\
Game Title (Int.): INTERNATIONAL US\n\
Region Code: 0x55 ('U')\n\
Region: USA"
);
Ok(())
}
#[test]
fn test_analyze_genesis_data_japan() -> Result<(), RomAnalyzerError> {
let data =
generate_genesis_header(b"SEGA MEGA DRIVE ", b'J', "DOMESTIC JP", "INTERNATIONAL JP");
let analysis = analyze_genesis_data(&data, "test_rom_jp.md")?;
assert_eq!(analysis.source_name, "test_rom_jp.md");
assert_eq!(analysis.console_name, "SEGA MEGA DRIVE");
assert_eq!(analysis.game_title_domestic, "DOMESTIC JP");
assert_eq!(analysis.game_title_international, "INTERNATIONAL JP");
assert_eq!(analysis.region_code_byte, b'J');
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan (NTSC-J)");
Ok(())
}
#[test]
fn test_analyze_genesis_data_brazil() -> Result<(), RomAnalyzerError> {
let data = generate_genesis_header(b"SEGA MEGA DRIVE ", b'B', "DOMESTIC BRA", "INT BRA");
let analysis = analyze_genesis_data(&data, "test_rom_bra.md")?;
assert_eq!(analysis.source_name, "test_rom_bra.md");
assert_eq!(analysis.region, Region::EUROPE); assert_eq!(analysis.region_string, "Brazil (PAL-M)");
assert_eq!(analysis.region_code_byte, b'B');
Ok(())
}
#[test]
fn test_analyze_genesis_data_genesis_signature() -> Result<(), RomAnalyzerError> {
let data = generate_genesis_header(b"SEGA GENESIS ", b'U', "GENESIS DOM", "GENESIS INT");
let analysis = analyze_genesis_data(&data, "test_rom_genesis.gen")?;
assert_eq!(analysis.source_name, "test_rom_genesis.gen");
assert_eq!(analysis.console_name, "SEGA GENESIS");
assert_eq!(analysis.region_code_byte, b'U');
assert_eq!(analysis.region, Region::USA);
assert_eq!(analysis.region_string, "USA (NTSC-U)");
Ok(())
}
#[test]
fn test_analyze_genesis_data_asia() -> Result<(), RomAnalyzerError> {
let data = generate_genesis_header(b"SEGA MEGA DRIVE ", b'A', "DOMESTIC ASIA", "INT ASIA");
let analysis = analyze_genesis_data(&data, "test_rom_asia.md")?;
assert_eq!(analysis.source_name, "test_rom_asia.md");
assert_eq!(analysis.region, Region::ASIA);
assert_eq!(analysis.region_string, "Asia (NTSC)");
assert_eq!(analysis.region_code_byte, b'A');
Ok(())
}
#[test]
fn test_analyze_genesis_data_china() -> Result<(), RomAnalyzerError> {
let data =
generate_genesis_header(b"SEGA MEGA DRIVE ", b'C', "DOMESTIC CHINA", "INT CHINA");
let analysis = analyze_genesis_data(&data, "test_rom_chn.md")?;
assert_eq!(analysis.source_name, "test_rom_chn.md");
assert_eq!(analysis.region, Region::CHINA);
assert_eq!(analysis.region_string, "China (NTSC)");
assert_eq!(analysis.region_code_byte, b'C');
Ok(())
}
#[test]
fn test_analyze_genesis_data_too_small() {
let data = vec![0; 100]; let result = analyze_genesis_data(&data, "too_small.md");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
#[test]
fn test_map_region_all_codes() {
let test_cases = vec![
(b'J', "Japan (NTSC-J)", Region::JAPAN),
(b'U', "USA (NTSC-U)", Region::USA),
(b'E', "Europe (PAL)", Region::EUROPE),
(b'A', "Asia (NTSC)", Region::ASIA),
(b'B', "Brazil (PAL-M)", Region::EUROPE),
(b'C', "China (NTSC)", Region::CHINA),
(b'F', "France (PAL)", Region::EUROPE),
(b'K', "Korea (NTSC)", Region::KOREA),
(b'L', "UK (PAL)", Region::EUROPE),
(b'S', "Scandinavia (PAL)", Region::EUROPE),
(b'T', "Taiwan (NTSC)", Region::ASIA),
(0x34, "USA/Europe (NTSC/PAL)", Region::USA | Region::EUROPE),
(b'Z', "Unknown", Region::UNKNOWN), ];
for (code, expected_name, expected_region) in test_cases {
let (name, region) = map_region(code);
assert_eq!(name, expected_name, "Failed for code 0x{:02X}", code);
assert_eq!(region, expected_region, "Failed for code 0x{:02X}", code);
}
}
}