rom_analyzer/console/
snes.rs

1//! Provides header analysis functionality for Super Nintendo Entertainment System (SNES) ROMs.
2//!
3//! This module can detect SNES ROM mapping types (LoROM, HiROM),
4//! validate checksums, and extract game title and region information.
5//!
6//! Super Nintendo header documentation referenced here:
7//! <https://snes.nesdev.org/wiki/ROM_header>
8
9use std::error::Error;
10
11use log::error;
12use serde::Serialize;
13
14use crate::error::RomAnalyzerError;
15use crate::region::{Region, check_region_mismatch};
16
17// Map Mode byte offset relative to the header start (0x7FC0 for LoROM, 0xFFC0 for HiROM)
18const MAP_MODE_OFFSET: usize = 0x15;
19
20// Expected Map Mode byte values for LoROM and HiROM
21const LOROM_MAP_MODES: &[u8] = &[0x20, 0x30, 0x25, 0x35];
22const HIROM_MAP_MODES: &[u8] = &[0x21, 0x31, 0x22, 0x32];
23
24/// Struct to hold the analysis results for a SNES ROM.
25#[derive(Debug, PartialEq, Clone, Serialize)]
26pub struct SnesAnalysis {
27    /// The name of the source file.
28    pub source_name: String,
29    /// The identified region(s) as a region::Region bitmask.
30    pub region: Region,
31    /// The identified region name (e.g., "Japan (NTSC)").
32    pub region_string: String,
33    /// If the region in the ROM header doesn't match the region in the filename.
34    pub region_mismatch: bool,
35    /// The raw region code byte.
36    pub region_code: u8,
37    /// The game title extracted from the ROM header.
38    pub game_title: String,
39    /// The detected mapping type (e.g., "LoROM", "HiROM").
40    pub mapping_type: String,
41}
42
43impl SnesAnalysis {
44    /// Returns a printable String of the analysis results.
45    pub fn print(&self) -> String {
46        format!(
47            "{}\n\
48             System:       Super Nintendo (SNES)\n\
49             Game Title:   {}\n\
50             Mapping:      {}\n\
51             Region Code:  0x{:02X}\n\
52             Region:       {}",
53            self.source_name, self.game_title, self.mapping_type, self.region_code, self.region
54        )
55    }
56}
57
58/// Determines the SNES game region name based on a given region byte.
59///
60/// The region byte typically comes from the ROM header. This function extracts the relevant bits
61/// from the byte and maps it to a human-readable region string and a Region bitmask.
62///
63/// # Arguments
64///
65/// * `region_byte` - The byte containing the region code, usually found in the ROM header.
66///
67/// # Returns
68///
69/// A tuple containing:
70/// - A `&'static str` representing the region as written in the ROM header (e.g., "Japan (NTSC)",
71///   "USA / Canada (NTSC)", etc.) or "Unknown" if the region code is not recognized.
72/// - A `Region` bitmask representing the region(s) associated with the code.
73///
74/// # Examples
75///
76/// ```rust
77/// use rom_analyzer::console::snes::map_region;
78/// use rom_analyzer::region::Region;
79///
80/// let (region_str, region_mask) = map_region(0x00);
81/// assert_eq!(region_str, "Japan (NTSC)");
82/// assert_eq!(region_mask, Region::JAPAN);
83///
84/// let (region_str, region_mask) = map_region(0x01);
85/// assert_eq!(region_str, "USA / Canada (NTSC)");
86/// assert_eq!(region_mask, Region::USA);
87///
88/// let (region_str, region_mask) = map_region(0x02);
89/// assert_eq!(region_str, "Europe / Oceania / Asia (PAL)");
90/// assert_eq!(region_mask, Region::EUROPE | Region::ASIA);
91/// ```
92pub fn map_region(code: u8) -> (&'static str, Region) {
93    match code {
94        0x00 => ("Japan (NTSC)", Region::JAPAN),
95        0x01 => ("USA / Canada (NTSC)", Region::USA),
96        0x02 => (
97            "Europe / Oceania / Asia (PAL)",
98            Region::EUROPE | Region::ASIA,
99        ),
100        0x03 => ("Sweden / Scandinavia (PAL)", Region::EUROPE),
101        0x04 => ("Finland (PAL)", Region::EUROPE),
102        0x05 => ("Denmark (PAL)", Region::EUROPE),
103        0x06 => ("France (PAL)", Region::EUROPE),
104        0x07 => ("Netherlands (PAL)", Region::EUROPE),
105        0x08 => ("Spain (PAL)", Region::EUROPE),
106        0x09 => ("Germany (PAL)", Region::EUROPE),
107        0x0A => ("Italy (PAL)", Region::EUROPE),
108        0x0B => ("China (PAL)", Region::CHINA),
109        0x0C => ("Indonesia (PAL)", Region::EUROPE | Region::ASIA),
110        0x0D => ("South Korea (NTSC)", Region::KOREA),
111        0x0E => (
112            "Common / International",
113            Region::USA | Region::EUROPE | Region::JAPAN | Region::ASIA,
114        ),
115        0x0F => ("Canada (NTSC)", Region::USA),
116        0x10 => ("Brazil (NTSC)", Region::USA),
117        0x11 => ("Australia (PAL)", Region::EUROPE),
118        0x12 => ("Other (Variation 1)", Region::UNKNOWN),
119        0x13 => ("Other (Variation 2)", Region::UNKNOWN),
120        0x14 => ("Other (Variation 3)", Region::UNKNOWN),
121        _ => ("Unknown", Region::UNKNOWN),
122    }
123}
124
125/// Helper function to validate the SNES ROM checksum.
126///
127/// This function checks if the 16-bit checksum and its complement, located
128/// within the SNES header, sum up to `0xFFFF`. This is a common method
129/// for validating the integrity of SNES ROM headers.
130///
131/// # Arguments
132///
133/// * `rom_data` - A byte slice (`&[u8]`) containing the raw ROM data.
134/// * `header_offset` - The starting offset of the SNES header within `rom_data`.
135///
136/// # Returns
137///
138/// `true` if the checksum and its complement are valid (sum to 0xFFFF),
139/// `false` otherwise, or if the `header_offset` is out of bounds.
140pub fn validate_snes_checksum(rom_data: &[u8], header_offset: usize) -> bool {
141    // Ensure we have enough data for checksum and complement bytes.
142    if header_offset + 0x20 > rom_data.len() {
143        return false;
144    }
145
146    // Checksum is at 0x1E (relative to header start), complement at 0x1C.
147    // Both are 16-bit values, little-endian.
148    let complement_bytes: [u8; 2] =
149        match rom_data[header_offset + 0x1C..header_offset + 0x1E].try_into() {
150            Ok(b) => b,
151            Err(_) => return false, // Should not happen if header_offset + 0x20 is within bounds.
152        };
153    let checksum_bytes: [u8; 2] =
154        match rom_data[header_offset + 0x1E..header_offset + 0x20].try_into() {
155            Ok(b) => b,
156            Err(_) => return false, // Should not happen if header_offset + 0x20 is within bounds.
157        };
158
159    let complement = u16::from_le_bytes(complement_bytes);
160    let checksum = u16::from_le_bytes(checksum_bytes);
161
162    // The checksum algorithm: (checksum + complement) should equal 0xFFFF.
163    (checksum as u32 + complement as u32) == 0xFFFF
164}
165
166/// Analyzes SNES ROM data.
167///
168/// This function first attempts to detect a copier header. It then tries to determine
169/// the ROM's mapping type (LoROM or HiROM) by validating checksums and examining
170/// the Map Mode byte at expected header locations. If both checksum and Map Mode
171/// are consistent, that mapping is chosen. If only the checksum is valid, it uses
172/// that mapping with an "Map Mode Unverified" tag. If neither is fully consistent,
173/// it falls back to LoROM (Unverified). Once the header location is determined,
174/// it extracts the game title and region code, maps the region code to a human-readable
175/// name, and performs a region mismatch check against the `source_name`.
176///
177/// # Arguments
178///
179/// * `data` - A byte slice (`&[u8]`) containing the raw ROM data.
180/// * `source_name` - The name of the ROM file, used for logging and region mismatch checks.
181///
182/// # Returns
183///
184/// A `Result` which is:
185/// - `Ok(SnesAnalysis)` containing the detailed analysis results.
186/// - `Err(Box<dyn Error>)` if the ROM data is too small or the header is deemed invalid
187///   such that critical information cannot be read.
188pub fn analyze_snes_data(data: &[u8], source_name: &str) -> Result<SnesAnalysis, Box<dyn Error>> {
189    let file_size = data.len();
190    let mut header_offset = 0;
191
192    // Detect copier header (often 512 bytes, common for some older dumps/tools)
193    if file_size >= 512 && (file_size % 1024 == 512) {
194        // Heuristic: If file size ends in 512 and is divisible by 1024
195        header_offset = 512;
196        // Note: This copier header detection is a simple heuristic and might not be foolproof.
197        // More advanced detection could involve checking for specific patterns.
198    }
199
200    // Determine ROM mapping type (LoROM vs HiROM) by checking checksums and Map Mode byte.
201    // The relevant header information is usually found at 0x7FC0 for LoROM and 0xFFC0 for HiROM
202    // (relative to the start of the ROM, accounting for the header_offset).
203    let lorom_header_start = 0x7FC0 + header_offset; // Header block starts here
204    let hirom_header_start = 0xFFC0 + header_offset; // Header block starts here
205
206    let mapping_type: String;
207    let valid_header_offset: usize;
208
209    let lorom_checksum_valid = validate_snes_checksum(data, lorom_header_start);
210    let hirom_checksum_valid = validate_snes_checksum(data, hirom_header_start);
211
212    // Get Map Mode bytes if headers are within bounds
213    let lorom_map_mode_byte = if lorom_header_start + MAP_MODE_OFFSET < file_size {
214        Some(data[lorom_header_start + MAP_MODE_OFFSET])
215    } else {
216        None
217    };
218    let hirom_map_mode_byte = if hirom_header_start + MAP_MODE_OFFSET < file_size {
219        Some(data[hirom_header_start + MAP_MODE_OFFSET])
220    } else {
221        None
222    };
223
224    let is_lorom_map_mode = lorom_map_mode_byte.map_or(false, |b| LOROM_MAP_MODES.contains(&b));
225    let is_hirom_map_mode = hirom_map_mode_byte.map_or(false, |b| HIROM_MAP_MODES.contains(&b));
226
227    // Decision logic: Prioritize HiROM if both checksum and map mode are consistent.
228    // Then check LoROM similarly. If only one checksum is valid, use that.
229    // If neither is fully consistent, fallback to LoROM (unverified) with a warning.
230    if hirom_checksum_valid && is_hirom_map_mode {
231        mapping_type = "HiROM".to_string();
232        valid_header_offset = hirom_header_start;
233    } else if lorom_checksum_valid && is_lorom_map_mode {
234        mapping_type = "LoROM".to_string();
235        valid_header_offset = lorom_header_start;
236    } else if hirom_checksum_valid {
237        mapping_type = "HiROM (Map Mode Unverified)".to_string();
238        valid_header_offset = hirom_header_start;
239        error!(
240            "[!] HiROM checksum valid for {}, but Map Mode byte (0x{:02X?}) is not a typical HiROM value. Falling back to HiROM.",
241            source_name, hirom_map_mode_byte
242        );
243    } else if lorom_checksum_valid {
244        mapping_type = "LoROM (Map Mode Unverified)".to_string();
245        valid_header_offset = lorom_header_start;
246        error!(
247            "[!] LoROM checksum valid for {}, but Map Mode byte (0x{:02X?}) is not a typical LoROM value. Falling back to LoROM.",
248            source_name, lorom_map_mode_byte
249        );
250    } else {
251        // If neither checksum is valid, log a warning and try LoROM as a fallback, as it's more common.
252        error!(
253            "[!] Checksum validation failed for {}. Attempting to read header from LoROM location ({:X}) as fallback.",
254            source_name, lorom_header_start
255        );
256        mapping_type = "LoROM (Unverified)".to_string();
257        valid_header_offset = lorom_header_start; // Fallback to LoROM offset
258    }
259
260    // Ensure the determined header offset plus the header size needed for analysis is within the file bounds.
261    // We need at least up to the region code (offset 0x19 relative to header start) and game title (offset 0x0 to 0x14).
262    // Thus, we check if `valid_header_offset + 0x20` is within bounds, as this covers the checksum bytes.
263    if valid_header_offset + 0x20 > file_size {
264        return Err(Box::new(RomAnalyzerError::new(&format!(
265            "ROM data is too small or header is invalid. File size: {} bytes. Checked header at offset: {}. Required minimum size for header region: {}.",
266            file_size,
267            valid_header_offset,
268            valid_header_offset + 0x20
269        ))));
270    }
271
272    // Extract region code and game title from the identified header.
273    let region_byte_offset = valid_header_offset + 0x19; // Offset for region code within the header
274    let region_code = data[region_byte_offset];
275    let (region_name, region) = map_region(region_code);
276
277    // Game title is located at the beginning of the header (offset 0x0 relative to valid_header_offset) for 21 bytes.
278    // It is null-terminated, so we trim null bytes and leading/trailing whitespace.
279    let game_title = String::from_utf8_lossy(&data[valid_header_offset..valid_header_offset + 21])
280        .trim_matches(char::from(0)) // Remove null bytes
281        .trim()
282        .to_string();
283
284    let region_mismatch = check_region_mismatch(source_name, region);
285
286    Ok(SnesAnalysis {
287        source_name: source_name.to_string(),
288        region,
289        region_string: region_name.to_string(),
290        region_mismatch,
291        region_code,
292        game_title,
293        mapping_type,
294    })
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use std::error::Error;
301
302    /// Helper to create a dummy SNES ROM with a valid checksum.
303    /// It allows specifying ROM size, copier header offset, region code, mapping type.
304    fn generate_snes_header(
305        rom_size: usize,
306        copier_header_offset: usize,
307        region_code: u8,
308        is_hirom: bool,
309        title: &str,
310        map_mode_byte: Option<u8>,
311    ) -> Vec<u8> {
312        let mut data = vec![0; rom_size];
313
314        // Calculate the actual start of the SNES header based on mapping type and copier offset.
315        let header_start = (if is_hirom { 0xFFC0 } else { 0x7FC0 }) + copier_header_offset;
316
317        // Ensure the data is large enough
318        if header_start + 0x20 > rom_size {
319            panic!(
320                "Provided ROM size {} is too small for SNES header at offset {} (needs at least {}).",
321                rom_size,
322                header_start,
323                header_start + 0x20
324            );
325        }
326
327        // 1. Set Title (21 bytes starting at header_start + 0x00)
328        let mut title_bytes: Vec<u8> = title.as_bytes().to_vec();
329        // Truncate if longer than 21 bytes, then pad with spaces if shorter.
330        title_bytes.truncate(21);
331        title_bytes.resize(21, b' '); // Pad with spaces, standard SNES header practice
332
333        data[header_start..header_start + 21].copy_from_slice(&title_bytes);
334
335        // 2. Set Region Code (at header_start + 0x19)
336        data[header_start + 0x19] = region_code;
337
338        // 3. Set Map Mode byte if provided (at header_start + MAP_MODE_OFFSET)
339        if let Some(map_mode) = map_mode_byte {
340            data[header_start + MAP_MODE_OFFSET] = map_mode;
341        }
342
343        // 4. Set a valid checksum and its complement.
344        // The checksum algorithm is (checksum + complement) == 0xFFFF. We use a simple pair.
345        let complement: u16 = 0x5555;
346        let checksum: u16 = 0xFFFF - complement; // 0xAAAA
347
348        // Set Checksum Complement (0x1C relative to header start)
349        data[header_start + 0x1C..header_start + 0x1E].copy_from_slice(&complement.to_le_bytes());
350        // Set Checksum (0x1E relative to header start)
351        data[header_start + 0x1E..header_start + 0x20].copy_from_slice(&checksum.to_le_bytes());
352
353        data
354    }
355
356    #[test]
357    fn test_analyze_snes_data_lorom_japan() -> Result<(), Box<dyn Error>> {
358        let data = generate_snes_header(0x80000, 0, 0x00, false, "TEST GAME TITLE", None); // 512KB ROM, LoROM, Japan
359        let analysis = analyze_snes_data(&data, "test_lorom_jp.sfc")?;
360
361        assert_eq!(analysis.source_name, "test_lorom_jp.sfc");
362        assert_eq!(analysis.game_title, "TEST GAME TITLE");
363        assert_eq!(analysis.mapping_type, "LoROM (Map Mode Unverified)");
364        assert_eq!(analysis.region_code, 0x00);
365        assert_eq!(analysis.region, Region::JAPAN);
366        assert_eq!(analysis.region_string, "Japan (NTSC)");
367        Ok(())
368    }
369
370    #[test]
371    fn test_analyze_snes_data_hirom_usa() -> Result<(), Box<dyn Error>> {
372        let data = generate_snes_header(0x100000, 0, 0x01, true, "TEST GAME TITLE", None); // 1MB ROM, HiROM, USA
373        let analysis = analyze_snes_data(&data, "test_hirom_us.sfc")?;
374
375        assert_eq!(analysis.source_name, "test_hirom_us.sfc");
376        assert_eq!(analysis.game_title, "TEST GAME TITLE");
377        assert_eq!(analysis.mapping_type, "HiROM (Map Mode Unverified)");
378        assert_eq!(analysis.region_code, 0x01);
379        assert_eq!(analysis.region, Region::USA);
380        assert_eq!(analysis.region_string, "USA / Canada (NTSC)");
381        Ok(())
382    }
383
384    #[test]
385    fn test_analyze_snes_data_lorom_europe_copier_header() -> Result<(), Box<dyn Error>> {
386        // Rom size ends with 512 bytes, e.g., 800KB + 512 bytes = 800512 bytes.
387        let data = generate_snes_header(0x80000 + 512, 512, 0x02, false, "TEST GAME TITLE", None); // LoROM, Europe, with 512-byte copier header
388        let analysis = analyze_snes_data(&data, "test_lorom_eur_copier.sfc")?;
389
390        assert_eq!(analysis.source_name, "test_lorom_eur_copier.sfc");
391        assert_eq!(analysis.game_title, "TEST GAME TITLE");
392        assert_eq!(analysis.mapping_type, "LoROM (Map Mode Unverified)"); // Should detect copier header but still identify LoROM
393        assert_eq!(analysis.region_code, 0x02);
394        assert_eq!(analysis.region, Region::EUROPE | Region::ASIA);
395        assert_eq!(analysis.region_string, "Europe / Oceania / Asia (PAL)");
396        Ok(())
397    }
398
399    #[test]
400    fn test_analyze_snes_data_hirom_canada_copier_header() -> Result<(), Box<dyn Error>> {
401        // Data size: 1MB + 512 bytes for copier header
402        let data = generate_snes_header(
403            0x100200,
404            512,  // Copier Header offset
405            0x0F, // Region: Canada (0x0F)
406            true, // HiROM
407            "TEST GAME TITLE",
408            None,
409        );
410        let analysis = analyze_snes_data(&data, "test_hirom_can_copier.sfc")?;
411
412        assert_eq!(analysis.source_name, "test_hirom_can_copier.sfc");
413        assert_eq!(analysis.game_title, "TEST GAME TITLE");
414        assert_eq!(analysis.mapping_type, "HiROM (Map Mode Unverified)");
415        assert_eq!(analysis.region_code, 0x0F);
416        assert_eq!(analysis.region, Region::USA);
417        assert_eq!(analysis.region_string, "Canada (NTSC)");
418        Ok(())
419    }
420
421    #[test]
422    fn test_analyze_snes_data_unknown_region() -> Result<(), Box<dyn Error>> {
423        let data = generate_snes_header(0x80000, 0, 0xFF, false, "TEST GAME TITLE", None); // LoROM, Unknown region
424        let analysis = analyze_snes_data(&data, "test_lorom_unknown.sfc")?;
425
426        assert_eq!(analysis.source_name, "test_lorom_unknown.sfc");
427        assert_eq!(analysis.game_title, "TEST GAME TITLE");
428        assert_eq!(analysis.mapping_type, "LoROM (Map Mode Unverified)");
429        assert_eq!(analysis.region_code, 0xFF);
430        assert_eq!(analysis.region, Region::UNKNOWN);
431        assert_eq!(analysis.region_string, "Unknown");
432        Ok(())
433    }
434
435    #[test]
436    fn test_analyze_snes_data_invalid_checksum() -> Result<(), Box<dyn Error>> {
437        // FIX: Use the robust helper to generate a correctly formatted header first.
438        let mut data = generate_snes_header(
439            0x8000, // 32KB is enough for LoROM
440            0,
441            0x01,               // USA region code
442            false,              // LoROM base
443            "INVALID CHECKSUM", // Title to assert on
444            None,
445        );
446
447        // Manually invalidate the checksum/complement pair.
448        // LoROM header start is 0x7FC0. Checksum area starts at 0x7FC0 + 0x1C.
449        let checksum_start = 0x7FC0 + 0x1C;
450
451        // Overwrite the 4 bytes of checksum/complement with invalid data (e.g., all zeros)
452        data[checksum_start..checksum_start + 4].copy_from_slice(&[0x00, 0x00, 0x00, 0x00]);
453
454        let analysis = analyze_snes_data(&data, "test_invalid_checksum.sfc")?;
455
456        assert_eq!(analysis.source_name, "test_invalid_checksum.sfc");
457        assert_eq!(analysis.game_title, "INVALID CHECKSUM");
458        assert_eq!(analysis.mapping_type, "LoROM (Unverified)"); // Expecting fallback
459        assert_eq!(analysis.region_code, 0x01);
460        assert_eq!(analysis.region, Region::USA);
461        assert_eq!(analysis.region_string, "USA / Canada (NTSC)");
462        Ok(())
463    }
464
465    #[test]
466    fn test_analyze_snes_data_too_small() {
467        // Test with data smaller than the minimum required size for header analysis.
468        // The minimal size depends on mapping type and copier header. For LoROM without copier header,
469        // it's header_start + 0x20 = 0x7FFC + 0x20 = 0x801C bytes.
470        let data = vec![0; 0x1000]; // Significantly smaller than required.
471        let result = analyze_snes_data(&data, "too_small.sfc");
472        assert!(result.is_err());
473        assert!(
474            result
475                .unwrap_err()
476                .to_string()
477                .contains("too small or header is invalid")
478        );
479    }
480
481    #[test]
482    fn test_analyze_snes_data_hirom_checksum_map_mode_consistent() -> Result<(), Box<dyn Error>> {
483        let data =
484            generate_snes_header(0x100000, 0, 0x01, true, "TEST HIROM CONSISTENT", Some(0x21)); // HiROM, USA, HiROM Map Mode
485        let analysis = analyze_snes_data(&data, "test_hirom_consistent.sfc")?;
486
487        assert_eq!(analysis.mapping_type, "HiROM");
488        assert_eq!(analysis.game_title, "TEST HIROM CONSISTENT");
489        Ok(())
490    }
491
492    #[test]
493    fn test_analyze_snes_data_lorom_checksum_map_mode_consistent() -> Result<(), Box<dyn Error>> {
494        let data =
495            generate_snes_header(0x80000, 0, 0x00, false, "TEST LOROM CONSISTENT", Some(0x20)); // LoROM, Japan, LoROM Map Mode
496        let analysis = analyze_snes_data(&data, "test_lorom_consistent.sfc")?;
497
498        assert_eq!(analysis.mapping_type, "LoROM");
499        assert_eq!(analysis.game_title, "TEST LOROM CONSISTENT");
500        Ok(())
501    }
502
503    #[test]
504    fn test_analyze_snes_data_hirom_checksum_map_mode_inconsistent() -> Result<(), Box<dyn Error>> {
505        let data = generate_snes_header(
506            0x100000,
507            0,
508            0x01,
509            true,
510            "TEST HIROM INCONSISTENT",
511            Some(0x20),
512        ); // HiROM, USA, LoROM Map Mode
513        let analysis = analyze_snes_data(&data, "test_hirom_inconsistent.sfc")?;
514
515        assert_eq!(analysis.mapping_type, "HiROM (Map Mode Unverified)");
516        assert_eq!(analysis.game_title, "TEST HIROM INCONSISTE");
517        Ok(())
518    }
519
520    #[test]
521    fn test_analyze_snes_data_lorom_checksum_map_mode_inconsistent() -> Result<(), Box<dyn Error>> {
522        let data = generate_snes_header(
523            0x80000,
524            0,
525            0x00,
526            false,
527            "TEST LOROM INCONSISTENT",
528            Some(0x21),
529        ); // LoROM, Japan, HiROM Map Mode
530        let analysis = analyze_snes_data(&data, "test_lorom_inconsistent.sfc")?;
531
532        assert_eq!(analysis.mapping_type, "LoROM (Map Mode Unverified)");
533        assert_eq!(analysis.game_title, "TEST LOROM INCONSISTE");
534        Ok(())
535    }
536
537    #[test]
538    fn test_analyze_snes_data_no_valid_checksum_map_mode_consistent_hirom_only()
539    -> Result<(), Box<dyn Error>> {
540        let mut data = generate_snes_header(
541            0x100000,
542            0,
543            0x01,
544            true,
545            "TEST NO CHECKSUM HIROM MAP",
546            Some(0x21),
547        ); // HiROM, USA, HiROM Map Mode
548        // Invalidate both checksums
549        let lorom_checksum_start = 0x7FC0 + 0x1C;
550        data[lorom_checksum_start..lorom_checksum_start + 4]
551            .copy_from_slice(&[0x00, 0x00, 0x00, 0x00]);
552        let hirom_checksum_start = 0xFFC0 + 0x1C;
553        data[hirom_checksum_start..hirom_checksum_start + 4]
554            .copy_from_slice(&[0x00, 0x00, 0x00, 0x00]);
555
556        let analysis = analyze_snes_data(&data, "test_no_checksum_hirom_map.sfc")?;
557
558        assert_eq!(analysis.mapping_type, "LoROM (Unverified)"); // Expect fallback
559        Ok(())
560    }
561    #[test]
562    fn test_analyze_snes_data_no_valid_checksum_map_mode_consistent_lorom_only()
563    -> Result<(), Box<dyn Error>> {
564        let mut data = generate_snes_header(
565            0x80000,
566            0,
567            0x00,
568            false,
569            "TEST NO CHECKSUM LOROM MAP",
570            Some(0x20),
571        ); // LoROM, Japan, LoROM Map Mode
572        // Invalidate both checksums
573        let lorom_checksum_start = 0x7FC0 + 0x1C;
574        data[lorom_checksum_start..lorom_checksum_start + 4]
575            .copy_from_slice(&[0x00, 0x00, 0x00, 0x00]);
576        let hirom_checksum_start = 0xFFC0 + 0x1C;
577        data[hirom_checksum_start..hirom_checksum_start + 4]
578            .copy_from_slice(&[0x00, 0x00, 0x00, 0x00]);
579
580        let analysis = analyze_snes_data(&data, "test_no_checksum_lorom_map.sfc")?;
581
582        assert_eq!(analysis.mapping_type, "LoROM (Unverified)"); // Expect fallback
583        Ok(())
584    }
585}