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