use serde::Serialize;
use crate::error::RomAnalyzerError;
use crate::region::{Region, check_region_mismatch};
const INES_REGION_BYTE: usize = 9;
const INES_REGION_MASK: u8 = 0x01;
const NES2_REGION_BYTE: usize = 12;
const NES2_REGION_MASK: u8 = 0x03;
const NES2_FORMAT_BYTE: usize = 7;
const NES2_FORMAT_MASK: u8 = 0x0C;
const NES2_FORMAT_EXPECTED_VALUE: u8 = 0x08;
#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct NesAnalysis {
pub source_name: String,
pub region: Region,
pub region_string: String,
pub region_mismatch: bool,
pub region_byte_value: u8,
pub is_nes2_format: bool,
}
impl NesAnalysis {
pub fn print(&self) -> String {
let nes_flag_display = if self.is_nes2_format {
format!("\nNES2.0 Flag 12: 0x{:02X}", self.region_byte_value)
} else {
format!("\niNES Flag 9: 0x{:02X}", self.region_byte_value)
};
format!(
"{}\n\
System: Nintendo Entertainment System (NES)\n\
Region: {}\
{}",
self.source_name, self.region, nes_flag_display
)
}
}
pub fn map_region(region_byte: u8, nes2_format: bool) -> (&'static str, Region) {
if nes2_format {
match region_byte & NES2_REGION_MASK {
0 => ("NTSC (USA/Japan)", Region::USA | Region::JAPAN),
1 => ("PAL (Europe/Oceania)", Region::EUROPE),
2 => ("Multi-region", Region::USA | Region::JAPAN | Region::EUROPE),
3 => ("Dendy (Russia)", Region::RUSSIA),
_ => ("Unknown", Region::UNKNOWN),
}
} else {
match region_byte & INES_REGION_MASK {
0 => ("NTSC (USA/Japan)", Region::USA | Region::JAPAN),
1 => ("PAL (Europe/Oceania)", Region::EUROPE),
_ => ("Unknown", Region::UNKNOWN),
}
}
}
pub fn analyze_nes_data(data: &[u8], source_name: &str) -> Result<NesAnalysis, RomAnalyzerError> {
if data.len() < 16 {
return Err(RomAnalyzerError::DataTooSmall {
file_size: data.len(),
required_size: 16,
details: "iNES header".to_string(),
});
}
let signature = &data[0..4];
if signature != b"NES\x1a" {
return Err(RomAnalyzerError::InvalidHeader(
"Invalid iNES header signature. Not a valid NES ROM.".to_string(),
));
}
let mut region_byte_val = data[INES_REGION_BYTE];
let is_nes2_format = (data[NES2_FORMAT_BYTE] & NES2_FORMAT_MASK) == NES2_FORMAT_EXPECTED_VALUE;
if is_nes2_format {
region_byte_val = data[NES2_REGION_BYTE];
}
let (region_name, region) = map_region(region_byte_val, is_nes2_format);
let region_mismatch = check_region_mismatch(source_name, region);
Ok(NesAnalysis {
source_name: source_name.to_string(),
region,
region_string: region_name.to_string(),
region_mismatch,
region_byte_value: region_byte_val,
is_nes2_format,
})
}
#[cfg(test)]
mod tests {
use super::*;
enum NesHeaderType {
Ines,
Nes2,
}
fn generate_nes_header(header_type: NesHeaderType, region_value: u8) -> Vec<u8> {
let mut data = vec![0; 16];
data[0..4].copy_from_slice(b"NES\x1a");
match header_type {
NesHeaderType::Ines => {
data[INES_REGION_BYTE] = region_value;
data[NES2_FORMAT_BYTE] &= !NES2_FORMAT_MASK;
}
NesHeaderType::Nes2 => {
data[NES2_FORMAT_BYTE] |= NES2_FORMAT_EXPECTED_VALUE;
data[NES2_REGION_BYTE] = region_value;
}
}
data
}
#[test]
fn test_analyze_ines_data_ntsc() -> Result<(), RomAnalyzerError> {
let data = generate_nes_header(NesHeaderType::Ines, 0x00);
let analysis = analyze_nes_data(&data, "test_rom_ntsc.nes")?;
assert_eq!(analysis.source_name, "test_rom_ntsc.nes");
assert_eq!(analysis.region, Region::USA | Region::JAPAN);
assert_eq!(analysis.region_string, "NTSC (USA/Japan)");
assert!(!analysis.is_nes2_format);
assert_eq!(analysis.region_byte_value, 0x00);
assert_eq!(
analysis.print(),
"test_rom_ntsc.nes\n\
System: Nintendo Entertainment System (NES)\n\
Region: Japan/USA\n\
iNES Flag 9: 0x00"
);
Ok(())
}
#[test]
fn test_analyze_ines_data_pal() -> Result<(), RomAnalyzerError> {
let data = generate_nes_header(NesHeaderType::Ines, 0x01);
let analysis = analyze_nes_data(&data, "test_rom_pal.nes")?;
assert_eq!(analysis.source_name, "test_rom_pal.nes");
assert_eq!(analysis.region, Region::EUROPE);
assert_eq!(analysis.region_string, "PAL (Europe/Oceania)");
assert!(!analysis.is_nes2_format);
assert_eq!(analysis.region_byte_value, 0x01);
Ok(())
}
#[test]
fn test_analyze_nes2_data_ntsc() -> Result<(), RomAnalyzerError> {
let data = generate_nes_header(NesHeaderType::Nes2, 0x00);
let analysis = analyze_nes_data(&data, "test_rom_nes2_ntsc.nes")?;
assert_eq!(analysis.source_name, "test_rom_nes2_ntsc.nes");
assert_eq!(analysis.region, Region::USA | Region::JAPAN);
assert_eq!(analysis.region_string, "NTSC (USA/Japan)");
assert!(analysis.is_nes2_format);
assert_eq!(analysis.region_byte_value, 0x00);
assert_eq!(
analysis.print(),
"test_rom_nes2_ntsc.nes\n\
System: Nintendo Entertainment System (NES)\n\
Region: Japan/USA\n\
NES2.0 Flag 12: 0x00"
);
Ok(())
}
#[test]
fn test_analyze_nes2_data_pal() -> Result<(), RomAnalyzerError> {
let data = generate_nes_header(NesHeaderType::Nes2, 0x01);
let analysis = analyze_nes_data(&data, "test_rom_nes2_pal.nes")?;
assert_eq!(analysis.source_name, "test_rom_nes2_pal.nes");
assert_eq!(analysis.region, Region::EUROPE);
assert_eq!(analysis.region_string, "PAL (Europe/Oceania)");
assert!(analysis.is_nes2_format);
assert_eq!(analysis.region_byte_value, 0x01);
Ok(())
}
#[test]
fn test_analyze_nes2_data_world() -> Result<(), RomAnalyzerError> {
let data = generate_nes_header(NesHeaderType::Nes2, 0x02);
let analysis = analyze_nes_data(&data, "test_rom_nes2_world.nes")?;
assert_eq!(analysis.source_name, "test_rom_nes2_world.nes");
assert_eq!(
analysis.region,
Region::USA | Region::JAPAN | Region::EUROPE
);
assert_eq!(analysis.region_string, "Multi-region");
assert!(analysis.is_nes2_format);
assert_eq!(analysis.region_byte_value, 0x02);
assert_eq!(
analysis.print(),
"test_rom_nes2_world.nes\n\
System: Nintendo Entertainment System (NES)\n\
Region: Japan/USA/Europe\n\
NES2.0 Flag 12: 0x02"
);
Ok(())
}
#[test]
fn test_analyze_nes2_data_dendy() -> Result<(), RomAnalyzerError> {
let data = generate_nes_header(NesHeaderType::Nes2, 0x03);
let analysis = analyze_nes_data(&data, "test_rom_nes2_dendy.nes")?;
assert_eq!(analysis.source_name, "test_rom_nes2_dendy.nes");
assert_eq!(analysis.region, Region::RUSSIA);
assert_eq!(analysis.region_string, "Dendy (Russia)");
assert!(analysis.is_nes2_format);
assert_eq!(analysis.region_byte_value, 0x03);
Ok(())
}
#[test]
fn test_analyze_nes_data_too_small() {
let data = vec![0; 10];
let result = analyze_nes_data(&data, "too_small.nes");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
#[test]
fn test_analyze_nes_invalid_signature() {
let mut data = vec![0; 16];
data[0..4].copy_from_slice(b"XXXX"); let result = analyze_nes_data(&data, "invalid_sig.nes");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid iNES header signature")
);
}
}