1use std::error::Error;
11
12use serde::Serialize;
13
14use crate::error::RomAnalyzerError;
15use crate::region::{Region, check_region_mismatch};
16
17const INES_REGION_BYTE: usize = 9;
18const INES_REGION_MASK: u8 = 0x01;
19
20const NES2_REGION_BYTE: usize = 12;
21const NES2_REGION_MASK: u8 = 0x03;
22const NES2_FORMAT_BYTE: usize = 7;
23const NES2_FORMAT_MASK: u8 = 0x0C;
24const NES2_FORMAT_EXPECTED_VALUE: u8 = 0x08;
25
26#[derive(Debug, PartialEq, Clone, Serialize)]
28pub struct NesAnalysis {
29 pub source_name: String,
31 pub region: Region,
33 pub region_string: String,
35 pub region_mismatch: bool,
37 pub region_byte_value: u8,
39 pub is_nes2_format: bool,
41}
42
43impl NesAnalysis {
44 pub fn print(&self) -> String {
46 let nes_flag_display = if self.is_nes2_format {
47 format!("\nNES2.0 Flag 12: 0x{:02X}", self.region_byte_value)
48 } else {
49 format!("\niNES Flag 9: 0x{:02X}", self.region_byte_value)
50 };
51
52 format!(
53 "{}\n\
54 System: Nintendo Entertainment System (NES)\n\
55 Region: {}\
56 {}",
57 self.source_name, self.region, nes_flag_display
58 )
59 }
60}
61
62pub fn map_region(region_byte: u8, nes2_format: bool) -> (&'static str, Region) {
96 if nes2_format {
97 match region_byte & NES2_REGION_MASK {
100 0 => ("NTSC (USA/Japan)", Region::USA | Region::JAPAN),
101 1 => ("PAL (Europe/Oceania)", Region::EUROPE),
102 2 => ("Multi-region", Region::USA | Region::JAPAN | Region::EUROPE),
103 3 => ("Dendy (Russia)", Region::RUSSIA),
104 _ => ("Unknown", Region::UNKNOWN),
105 }
106 } else {
107 match region_byte & INES_REGION_MASK {
111 0 => ("NTSC (USA/Japan)", Region::USA | Region::JAPAN),
112 1 => ("PAL (Europe/Oceania)", Region::EUROPE),
113 _ => ("Unknown", Region::UNKNOWN),
114 }
115 }
116}
117
118pub fn analyze_nes_data(data: &[u8], source_name: &str) -> Result<NesAnalysis, Box<dyn Error>> {
137 if data.len() < 16 {
138 return Err(Box::new(RomAnalyzerError::new(&format!(
139 "ROM data is too small to contain an iNES header (size: {} bytes).",
140 data.len()
141 ))));
142 }
143
144 let signature = &data[0..4];
146 if signature != b"NES\x1a" {
147 return Err(Box::new(RomAnalyzerError::new(
148 "Invalid iNES header signature. Not a valid NES ROM.",
149 )));
150 }
151
152 let mut region_byte_val = data[INES_REGION_BYTE];
153 let is_nes2_format = (data[NES2_FORMAT_BYTE] & NES2_FORMAT_MASK) == NES2_FORMAT_EXPECTED_VALUE;
154
155 if is_nes2_format {
156 region_byte_val = data[NES2_REGION_BYTE];
157 }
158
159 let (region_name, region) = map_region(region_byte_val, is_nes2_format);
160 let region_mismatch = check_region_mismatch(source_name, region);
161
162 Ok(NesAnalysis {
163 source_name: source_name.to_string(),
164 region,
165 region_string: region_name.to_string(),
166 region_mismatch,
167 region_byte_value: region_byte_val,
168 is_nes2_format,
169 })
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use std::error::Error;
176
177 enum NesHeaderType {
179 Ines,
180 Nes2,
181 }
182
183 fn generate_nes_header(header_type: NesHeaderType, region_value: u8) -> Vec<u8> {
187 let mut data = vec![0; 16];
188 data[0..4].copy_from_slice(b"NES\x1a"); match header_type {
191 NesHeaderType::Ines => {
192 data[INES_REGION_BYTE] = region_value;
195 data[NES2_FORMAT_BYTE] &= !NES2_FORMAT_MASK;
197 }
198 NesHeaderType::Nes2 => {
199 data[NES2_FORMAT_BYTE] |= NES2_FORMAT_EXPECTED_VALUE;
201 data[NES2_REGION_BYTE] = region_value;
204 }
205 }
206 data
207 }
208
209 #[test]
210 fn test_analyze_ines_data_ntsc() -> Result<(), Box<dyn Error>> {
211 let data = generate_nes_header(NesHeaderType::Ines, 0x00);
213 let analysis = analyze_nes_data(&data, "test_rom_ntsc.nes")?;
214
215 assert_eq!(analysis.source_name, "test_rom_ntsc.nes");
216 assert_eq!(analysis.region, Region::USA | Region::JAPAN);
217 assert_eq!(analysis.region_string, "NTSC (USA/Japan)");
218 assert!(!analysis.is_nes2_format);
219 assert_eq!(analysis.region_byte_value, 0x00);
220 Ok(())
221 }
222
223 #[test]
224 fn test_analyze_ines_data_pal() -> Result<(), Box<dyn Error>> {
225 let data = generate_nes_header(NesHeaderType::Ines, 0x01);
227 let analysis = analyze_nes_data(&data, "test_rom_pal.nes")?;
228
229 assert_eq!(analysis.source_name, "test_rom_pal.nes");
230 assert_eq!(analysis.region, Region::EUROPE);
231 assert_eq!(analysis.region_string, "PAL (Europe/Oceania)");
232 assert!(!analysis.is_nes2_format);
233 assert_eq!(analysis.region_byte_value, 0x01);
234 Ok(())
235 }
236
237 #[test]
238 fn test_analyze_nes2_data_ntsc() -> Result<(), Box<dyn Error>> {
239 let data = generate_nes_header(NesHeaderType::Nes2, 0x00);
241 let analysis = analyze_nes_data(&data, "test_rom_nes2_ntsc.nes")?;
242
243 assert_eq!(analysis.source_name, "test_rom_nes2_ntsc.nes");
244 assert_eq!(analysis.region, Region::USA | Region::JAPAN);
245 assert_eq!(analysis.region_string, "NTSC (USA/Japan)");
246 assert!(analysis.is_nes2_format);
247 assert_eq!(analysis.region_byte_value, 0x00);
248 Ok(())
249 }
250
251 #[test]
252 fn test_analyze_nes2_data_pal() -> Result<(), Box<dyn Error>> {
253 let data = generate_nes_header(NesHeaderType::Nes2, 0x01);
255 let analysis = analyze_nes_data(&data, "test_rom_nes2_pal.nes")?;
256
257 assert_eq!(analysis.source_name, "test_rom_nes2_pal.nes");
258 assert_eq!(analysis.region, Region::EUROPE);
259 assert_eq!(analysis.region_string, "PAL (Europe/Oceania)");
260 assert!(analysis.is_nes2_format);
261 assert_eq!(analysis.region_byte_value, 0x01);
262 Ok(())
263 }
264
265 #[test]
266 fn test_analyze_nes2_data_world() -> Result<(), Box<dyn Error>> {
267 let data = generate_nes_header(NesHeaderType::Nes2, 0x02);
269 let analysis = analyze_nes_data(&data, "test_rom_nes2_world.nes")?;
270
271 assert_eq!(analysis.source_name, "test_rom_nes2_world.nes");
272 assert_eq!(
273 analysis.region,
274 Region::USA | Region::JAPAN | Region::EUROPE
275 );
276 assert_eq!(analysis.region_string, "Multi-region");
277 assert!(analysis.is_nes2_format);
278 assert_eq!(analysis.region_byte_value, 0x02);
279 Ok(())
280 }
281
282 #[test]
283 fn test_analyze_nes2_data_dendy() -> Result<(), Box<dyn Error>> {
284 let data = generate_nes_header(NesHeaderType::Nes2, 0x03);
286 let analysis = analyze_nes_data(&data, "test_rom_nes2_dendy.nes")?;
287
288 assert_eq!(analysis.source_name, "test_rom_nes2_dendy.nes");
289 assert_eq!(analysis.region, Region::RUSSIA);
290 assert_eq!(analysis.region_string, "Dendy (Russia)");
291 assert!(analysis.is_nes2_format);
292 assert_eq!(analysis.region_byte_value, 0x03);
293 Ok(())
294 }
295
296 #[test]
297 fn test_analyze_nes_data_too_small() {
298 let data = vec![0; 10];
300 let result = analyze_nes_data(&data, "too_small.nes");
301 assert!(result.is_err());
302 assert!(result.unwrap_err().to_string().contains("too small"));
303 }
304
305 #[test]
306 fn test_analyze_nes_invalid_signature() {
307 let mut data = vec![0; 16];
309 data[0..4].copy_from_slice(b"XXXX"); let result = analyze_nes_data(&data, "invalid_sig.nes");
311 assert!(result.is_err());
312 assert!(
313 result
314 .unwrap_err()
315 .to_string()
316 .contains("Invalid iNES header signature")
317 );
318 }
319}