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