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}