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}