1use std::error::Error;
10
11use log::error;
12use serde::Serialize;
13
14use crate::error::RomAnalyzerError;
15use crate::region::{Region, check_region_mismatch};
16
17const SYSTEM_TYPE_START: usize = 0x100;
18const SYSTEM_TYPE_END: usize = 0x110;
19const DOMESTIC_TITLE_START: usize = 0x120;
20const DOMESTIC_TITLE_END: usize = 0x150;
21const INTL_TITLE_START: usize = 0x150;
22const INTL_TITLE_END: usize = 0x180;
23const REGION_CODE_BYTE: usize = 0x1F0;
24
25#[derive(Debug, PartialEq, Clone, Serialize)]
27pub struct GenesisAnalysis {
28 pub source_name: String,
30 pub region: Region,
32 pub region_string: String,
34 pub region_mismatch: bool,
36 pub region_code_byte: u8,
38 pub console_name: String,
40 pub game_title_domestic: String,
42 pub game_title_international: String,
44}
45
46impl GenesisAnalysis {
47 pub fn print(&self) -> String {
49 format!(
50 "{}\n\
51 System: {}\n\
52 Game Title (Domestic): {}\n\
53 Game Title (Int.): {}\n\
54 Region Code: 0x{:02X} ('{}')\n\
55 Region: {}",
56 self.source_name,
57 self.console_name,
58 self.game_title_domestic,
59 self.game_title_international,
60 self.region_code_byte,
61 self.region_code_byte as char,
62 self.region
63 )
64 }
65}
66
67pub fn map_region(region_byte: u8) -> (&'static str, Region) {
107 match region_byte {
108 b'J' => ("Japan (NTSC-J)", Region::JAPAN),
109 b'U' => ("USA (NTSC-U)", Region::USA),
110 b'E' => ("Europe (PAL)", Region::EUROPE),
111 b'A' => ("Asia (NTSC)", Region::ASIA),
112 b'B' => ("Brazil (PAL-M)", Region::EUROPE),
113 b'C' => ("China (NTSC)", Region::CHINA),
114 b'F' => ("France (PAL)", Region::EUROPE),
115 b'K' => ("Korea (NTSC)", Region::KOREA),
116 b'L' => ("UK (PAL)", Region::EUROPE),
117 b'S' => ("Scandinavia (PAL)", Region::EUROPE),
118 b'T' => ("Taiwan (NTSC)", Region::ASIA),
119 0x34 => ("USA/Europe (NTSC/PAL)", Region::USA | Region::EUROPE),
120 _ => ("Unknown", Region::UNKNOWN),
121 }
122}
123
124pub fn analyze_genesis_data(
142 data: &[u8],
143 source_name: &str,
144) -> Result<GenesisAnalysis, Box<dyn Error>> {
145 const HEADER_SIZE: usize = 0x200; if data.len() < HEADER_SIZE {
149 return Err(Box::new(RomAnalyzerError::new(&format!(
150 "ROM data is too small to contain a Sega header (size: {} bytes, requires at least {} bytes).",
151 data.len(),
152 HEADER_SIZE
153 ))));
154 }
155
156 let console_name_bytes = &data[SYSTEM_TYPE_START..SYSTEM_TYPE_END];
159 let console_name = String::from_utf8_lossy(console_name_bytes)
160 .trim_matches(char::from(0))
161 .trim()
162 .to_string();
163
164 let is_valid_signature = console_name == "SEGA MEGA DRIVE" || console_name == "SEGA GENESIS";
167 if !is_valid_signature {
168 error!(
169 "[!] Warning: Unexpected Sega header signature for {} at 0x{:x}. Found: '{}'",
170 source_name, SYSTEM_TYPE_START, console_name
171 );
172 }
173
174 let game_title_domestic =
176 String::from_utf8_lossy(&data[DOMESTIC_TITLE_START..DOMESTIC_TITLE_END])
177 .trim_matches(char::from(0))
178 .trim()
179 .to_string();
180 let game_title_international = String::from_utf8_lossy(&data[INTL_TITLE_START..INTL_TITLE_END])
182 .trim_matches(char::from(0))
183 .trim()
184 .to_string();
185
186 let region_code_byte = data[REGION_CODE_BYTE];
188
189 let (region_name, region) = map_region(region_code_byte);
190
191 let region_mismatch = check_region_mismatch(source_name, region);
192
193 Ok(GenesisAnalysis {
194 source_name: source_name.to_string(),
195 region,
196 region_string: region_name.to_string(),
197 region_mismatch,
198 region_code_byte,
199 console_name,
200 game_title_domestic,
201 game_title_international,
202 })
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use std::error::Error;
209
210 fn generate_genesis_header(
212 console_sig: &[u8],
213 region_byte: u8,
214 domestic_title: &str,
215 international_title: &str,
216 ) -> Vec<u8> {
217 let mut data = vec![0; 0x200]; data[SYSTEM_TYPE_START..SYSTEM_TYPE_END].copy_from_slice(console_sig);
221
222 let mut domestic_title_bytes = domestic_title.as_bytes().to_vec();
224 domestic_title_bytes.resize(48, 0);
225 data[DOMESTIC_TITLE_START..DOMESTIC_TITLE_END].copy_from_slice(&domestic_title_bytes);
226
227 let mut international_title_bytes = international_title.as_bytes().to_vec();
229 international_title_bytes.resize(48, 0);
230 data[INTL_TITLE_START..INTL_TITLE_END].copy_from_slice(&international_title_bytes);
231
232 data[REGION_CODE_BYTE] = region_byte;
234
235 data
236 }
237
238 #[test]
239 fn test_analyze_genesis_data_usa() -> Result<(), Box<dyn Error>> {
240 let data =
241 generate_genesis_header(b"SEGA MEGA DRIVE ", b'U', "DOMESTIC US", "INTERNATIONAL US");
242 let analysis = analyze_genesis_data(&data, "test_rom_us.md")?;
243
244 assert_eq!(analysis.source_name, "test_rom_us.md");
245 assert_eq!(analysis.console_name, "SEGA MEGA DRIVE");
246 assert_eq!(analysis.game_title_domestic, "DOMESTIC US");
247 assert_eq!(analysis.game_title_international, "INTERNATIONAL US");
248 assert_eq!(analysis.region_code_byte, b'U');
249 assert_eq!(analysis.region, Region::USA);
250 assert_eq!(analysis.region_string, "USA (NTSC-U)");
251 Ok(())
252 }
253
254 #[test]
255 fn test_analyze_genesis_data_japan() -> Result<(), Box<dyn Error>> {
256 let data =
257 generate_genesis_header(b"SEGA MEGA DRIVE ", b'J', "DOMESTIC JP", "INTERNATIONAL JP");
258 let analysis = analyze_genesis_data(&data, "test_rom_jp.md")?;
259
260 assert_eq!(analysis.source_name, "test_rom_jp.md");
261 assert_eq!(analysis.console_name, "SEGA MEGA DRIVE");
262 assert_eq!(analysis.game_title_domestic, "DOMESTIC JP");
263 assert_eq!(analysis.game_title_international, "INTERNATIONAL JP");
264 assert_eq!(analysis.region_code_byte, b'J');
265 assert_eq!(analysis.region, Region::JAPAN);
266 assert_eq!(analysis.region_string, "Japan (NTSC-J)");
267 Ok(())
268 }
269
270 #[test]
271 fn test_analyze_genesis_data_europe() -> Result<(), Box<dyn Error>> {
272 let data = generate_genesis_header(
273 b"SEGA MEGA DRIVE ",
274 b'E',
275 "DOMESTIC EUR",
276 "INTERNATIONAL EUR",
277 );
278 let analysis = analyze_genesis_data(&data, "test_rom_eur.md")?;
279
280 assert_eq!(analysis.source_name, "test_rom_eur.md");
281 assert_eq!(analysis.console_name, "SEGA MEGA DRIVE");
282 assert_eq!(analysis.game_title_domestic, "DOMESTIC EUR");
283 assert_eq!(analysis.game_title_international, "INTERNATIONAL EUR");
284 assert_eq!(analysis.region_code_byte, b'E');
285 assert_eq!(analysis.region, Region::EUROPE);
286 assert_eq!(analysis.region_string, "Europe (PAL)");
287 Ok(())
288 }
289
290 #[test]
291 fn test_analyze_genesis_data_genesis_signature() -> Result<(), Box<dyn Error>> {
292 let data = generate_genesis_header(b"SEGA GENESIS ", b'U', "GENESIS DOM", "GENESIS INT");
293 let analysis = analyze_genesis_data(&data, "test_rom_genesis.gen")?;
294
295 assert_eq!(analysis.source_name, "test_rom_genesis.gen");
296 assert_eq!(analysis.console_name, "SEGA GENESIS");
297 assert_eq!(analysis.region_code_byte, b'U');
298 assert_eq!(analysis.region, Region::USA);
299 assert_eq!(analysis.region_string, "USA (NTSC-U)");
300 Ok(())
301 }
302
303 #[test]
304 fn test_analyze_genesis_data_unknown_region() -> Result<(), Box<dyn Error>> {
305 let data = generate_genesis_header(
306 b"SEGA MEGA DRIVE ",
307 b'Z',
308 "DOMESTIC UNK",
309 "INTERNATIONAL UNK",
310 );
311 let analysis = analyze_genesis_data(&data, "test_rom_unknown.md")?;
312
313 assert_eq!(analysis.source_name, "test_rom_unknown.md");
314 assert_eq!(analysis.region, Region::UNKNOWN);
315 assert_eq!(analysis.region_string, "Unknown");
316 assert_eq!(analysis.region_code_byte, b'Z');
317 Ok(())
318 }
319
320 #[test]
321 fn test_analyze_genesis_data_too_small() {
322 let data = vec![0; 100]; let result = analyze_genesis_data(&data, "too_small.md");
325 assert!(result.is_err());
326 assert!(result.unwrap_err().to_string().contains("too small"));
327 }
328}