rom_analyzer/console/
mastersystem.rs

1//! Provides header analysis functionality for Sega Master System ROMs.
2//!
3//! This module can parse Master System ROM headers to extract region information.
4//!
5//! Master System header documentation referenced here:
6//! <https://www.smspower.org/Development/ROMHeader>
7
8use std::error::Error;
9
10use serde::Serialize;
11
12use crate::error::RomAnalyzerError;
13use crate::region::{Region, check_region_mismatch};
14
15/// Struct to hold the analysis results for a Master System ROM.
16#[derive(Debug, PartialEq, Clone, Serialize)]
17pub struct MasterSystemAnalysis {
18    /// The name of the source file.
19    pub source_name: String,
20    /// The identified region(s) as a region::Region bitmask.
21    pub region: Region,
22    /// The identified region name (e.g., "Japan (NTSC)").
23    pub region_string: String,
24    /// If the region in the ROM header doesn't match the region in the filename.
25    pub region_mismatch: bool,
26    /// The raw region byte value.
27    pub region_byte: u8,
28}
29
30impl MasterSystemAnalysis {
31    /// Returns a printable String of the analysis results.
32    pub fn print(&self) -> String {
33        format!(
34            "{}\n\
35             System:       Sega Master System\n\
36             Region Code:  0x{:02X}\n\
37             Region:       {}",
38            self.source_name, self.region_byte, self.region
39        )
40    }
41}
42
43/// Determines the Sega Master System game region name based on a given region byte.
44///
45/// The region byte typically comes from the ROM header. This function extracts the relevant bits
46/// from the byte and maps it to a human-readable region string and a Region bitmask.
47///
48/// # Arguments
49///
50/// * `region_byte` - The byte containing the region code, usually found in the ROM header.
51///
52/// # Returns
53///
54/// A tuple containing:
55/// - A `&'static str` representing the region as written in the ROM header (e.g., "Japan (NTSC-J)",
56///   "Europe / Overseas (PAL/NTSC)") or "Unknown" if the region code is not recognized.
57/// - A `Region` bitmask representing the region(s) associated with the code.
58///
59/// # Examples
60///
61/// ```rust
62/// use rom_analyzer::console::mastersystem::map_region;
63/// use rom_analyzer::region::Region;
64///
65/// let (region_str, region_mask) = map_region(0x30);
66/// assert_eq!(region_str, "Japan (NTSC)");
67/// assert_eq!(region_mask, Region::JAPAN);
68///
69/// let (region_str, region_mask) = map_region(0x4C);
70/// assert_eq!(region_str, "Europe / Overseas (PAL/NTSC)");
71/// assert_eq!(region_mask, Region::USA | Region::EUROPE);
72///
73/// let (region_str, region_mask) = map_region(0x99);
74/// assert_eq!(region_str, "Unknown");
75/// assert_eq!(region_mask, Region::UNKNOWN);
76/// ```
77pub fn map_region(region_byte: u8) -> (&'static str, Region) {
78    match region_byte {
79        0x30 => ("Japan (NTSC)", Region::JAPAN),
80        0x4C => ("Europe / Overseas (PAL/NTSC)", Region::USA | Region::EUROPE),
81        _ => ("Unknown", Region::UNKNOWN),
82    }
83}
84
85/// Analyzes Master System ROM data.
86///
87/// This function reads the Master System ROM header to extract the region byte.
88/// It then maps the region byte to a human-readable region name and performs
89/// a region mismatch check against the `source_name`.
90///
91/// # Arguments
92///
93/// * `data` - A byte slice (`&[u8]`) containing the raw ROM data.
94/// * `source_name` - The name of the ROM file, used for region mismatch checks.
95///
96/// # Returns
97///
98/// A `Result` which is:
99/// - `Ok(MasterSystemAnalysis)` containing the detailed analysis results.
100/// - `Err(Box<dyn Error>)` if the ROM data is too small to contain the region byte.
101pub fn analyze_mastersystem_data(
102    data: &[u8],
103    source_name: &str,
104) -> Result<MasterSystemAnalysis, Box<dyn Error>> {
105    // SMS Region/Language byte is at offset 0x7FFC.
106    // The header size for SMS is not strictly defined in a way that guarantees a fixed length for all ROMs,
107    // but 0x7FFD is a common size for the data containing this byte.
108    const REQUIRED_SIZE: usize = 0x7FFD;
109    if data.len() < REQUIRED_SIZE {
110        return Err(Box::new(RomAnalyzerError::new(&format!(
111            "ROM data is too small to contain Master System region byte (size: {} bytes, requires at least {} bytes).",
112            data.len(),
113            REQUIRED_SIZE
114        ))));
115    }
116
117    let sms_region_byte = data[0x7FFC];
118    let (region_name, region) = map_region(sms_region_byte);
119
120    let region_mismatch = check_region_mismatch(source_name, region);
121
122    Ok(MasterSystemAnalysis {
123        source_name: source_name.to_string(),
124        region,
125        region_string: region_name.to_string(),
126        region_mismatch,
127        region_byte: sms_region_byte,
128    })
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use std::error::Error;
135
136    #[test]
137    fn test_analyze_mastersystem_data_japan() -> Result<(), Box<dyn Error>> {
138        let mut data = vec![0; 0x7FFD];
139        data[0x7FFC] = 0x30; // Japan region
140        let analysis = analyze_mastersystem_data(&data, "test_rom_jp.sms")?;
141
142        assert_eq!(analysis.source_name, "test_rom_jp.sms");
143        assert_eq!(analysis.region_byte, 0x30);
144        assert_eq!(analysis.region, Region::JAPAN);
145        assert_eq!(analysis.region_string, "Japan (NTSC)");
146        Ok(())
147    }
148
149    #[test]
150    fn test_analyze_mastersystem_data_europe() -> Result<(), Box<dyn Error>> {
151        let mut data = vec![0; 0x7FFD];
152        data[0x7FFC] = 0x4C; // Europe / Overseas region
153        let analysis = analyze_mastersystem_data(&data, "test_rom_eur.sms")?;
154
155        assert_eq!(analysis.source_name, "test_rom_eur.sms");
156        assert_eq!(analysis.region_byte, 0x4C);
157        assert_eq!(analysis.region, Region::USA | Region::EUROPE);
158        assert_eq!(analysis.region_string, "Europe / Overseas (PAL/NTSC)");
159        Ok(())
160    }
161
162    #[test]
163    fn test_analyze_mastersystem_data_unknown() -> Result<(), Box<dyn Error>> {
164        let mut data = vec![0; 0x7FFD];
165        data[0x7FFC] = 0x00; // Unknown region
166        let analysis = analyze_mastersystem_data(&data, "test_rom.sms")?;
167
168        assert_eq!(analysis.source_name, "test_rom.sms");
169        assert_eq!(analysis.region_byte, 0x00);
170        assert_eq!(analysis.region, Region::UNKNOWN);
171        assert_eq!(analysis.region_string, "Unknown");
172        Ok(())
173    }
174
175    #[test]
176    fn test_analyze_mastersystem_data_too_small() {
177        // Test with data smaller than the minimum required size for analysis.
178        let data = vec![0; 100]; // Smaller than 0x7FFD
179        let result = analyze_mastersystem_data(&data, "too_small.sms");
180        assert!(result.is_err());
181        assert!(result.unwrap_err().to_string().contains("too small"));
182    }
183}