1use log::error;
10use serde::Serialize;
11
12use crate::error::RomAnalyzerError;
13use crate::region::{Region, check_region_mismatch};
14use crate::{SEGA_GENESIS_SIG, SEGA_MEGA_DRIVE_SIG};
15
16const SYSTEM_TYPE_START: usize = 0x100;
17const SYSTEM_TYPE_END: usize = 0x110;
18const DOMESTIC_TITLE_START: usize = 0x120;
19const DOMESTIC_TITLE_END: usize = 0x150;
20const INTL_TITLE_START: usize = 0x150;
21const INTL_TITLE_END: usize = 0x180;
22const REGION_CODE_BYTE: usize = 0x1F0;
23
24#[derive(Debug, PartialEq, Clone, Serialize)]
26pub struct GenesisAnalysis {
27 pub source_name: String,
29 pub region: Region,
31 pub region_string: String,
33 pub region_mismatch: bool,
35 pub region_code_byte: u8,
37 pub console_name: String,
39 pub game_title_domestic: String,
41 pub game_title_international: String,
43}
44
45impl GenesisAnalysis {
46 pub fn print(&self) -> String {
48 format!(
49 "{}\n\
50 System: {}\n\
51 Game Title (Domestic): {}\n\
52 Game Title (Int.): {}\n\
53 Region Code: 0x{:02X} ('{}')\n\
54 Region: {}",
55 self.source_name,
56 self.console_name,
57 self.game_title_domestic,
58 self.game_title_international,
59 self.region_code_byte,
60 self.region_code_byte as char,
61 self.region
62 )
63 }
64}
65
66pub fn map_region(region_byte: u8) -> (&'static str, Region) {
106 match region_byte {
107 b'J' => ("Japan (NTSC-J)", Region::JAPAN),
108 b'U' => ("USA (NTSC-U)", Region::USA),
109 b'E' => ("Europe (PAL)", Region::EUROPE),
110 b'A' => ("Asia (NTSC)", Region::ASIA),
111 b'B' => ("Brazil (PAL-M)", Region::EUROPE),
112 b'C' => ("China (NTSC)", Region::CHINA),
113 b'F' => ("France (PAL)", Region::EUROPE),
114 b'K' => ("Korea (NTSC)", Region::KOREA),
115 b'L' => ("UK (PAL)", Region::EUROPE),
116 b'S' => ("Scandinavia (PAL)", Region::EUROPE),
117 b'T' => ("Taiwan (NTSC)", Region::ASIA),
118 0x34 => ("USA/Europe (NTSC/PAL)", Region::USA | Region::EUROPE),
119 _ => ("Unknown", Region::UNKNOWN),
120 }
121}
122
123pub fn analyze_genesis_data(
141 data: &[u8],
142 source_name: &str,
143) -> Result<GenesisAnalysis, RomAnalyzerError> {
144 const HEADER_SIZE: usize = 0x200; if data.len() < HEADER_SIZE {
148 return Err(RomAnalyzerError::DataTooSmall {
149 file_size: data.len(),
150 required_size: HEADER_SIZE,
151 details: "Sega header".to_string(),
152 });
153 }
154
155 let console_name_bytes = &data[SYSTEM_TYPE_START..SYSTEM_TYPE_END];
158 let console_name = String::from_utf8_lossy(console_name_bytes)
159 .trim_matches(char::from(0))
160 .trim()
161 .to_string();
162
163 let is_valid_signature = console_name_bytes.starts_with(SEGA_MEGA_DRIVE_SIG)
166 || console_name_bytes.starts_with(SEGA_GENESIS_SIG);
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
209 fn generate_genesis_header(
211 console_sig: &[u8],
212 region_byte: u8,
213 domestic_title: &str,
214 international_title: &str,
215 ) -> Vec<u8> {
216 let mut data = vec![0; 0x200]; data[SYSTEM_TYPE_START..SYSTEM_TYPE_END].copy_from_slice(console_sig);
220
221 let mut domestic_title_bytes = domestic_title.as_bytes().to_vec();
223 domestic_title_bytes.resize(48, 0);
224 data[DOMESTIC_TITLE_START..DOMESTIC_TITLE_END].copy_from_slice(&domestic_title_bytes);
225
226 let mut international_title_bytes = international_title.as_bytes().to_vec();
228 international_title_bytes.resize(48, 0);
229 data[INTL_TITLE_START..INTL_TITLE_END].copy_from_slice(&international_title_bytes);
230
231 data[REGION_CODE_BYTE] = region_byte;
233
234 data
235 }
236
237 #[test]
238 fn test_analyze_genesis_data_usa() -> Result<(), RomAnalyzerError> {
239 let data =
240 generate_genesis_header(b"SEGA MEGA DRIVE ", b'U', "DOMESTIC US", "INTERNATIONAL US");
241 let analysis = analyze_genesis_data(&data, "test_rom_us.md")?;
242
243 assert_eq!(analysis.source_name, "test_rom_us.md");
244 assert_eq!(analysis.console_name, "SEGA MEGA DRIVE");
245 assert_eq!(analysis.game_title_domestic, "DOMESTIC US");
246 assert_eq!(analysis.game_title_international, "INTERNATIONAL US");
247 assert_eq!(analysis.region_code_byte, b'U');
248 assert_eq!(analysis.region, Region::USA);
249 assert_eq!(analysis.region_string, "USA (NTSC-U)");
250 assert_eq!(
251 analysis.print(),
252 "test_rom_us.md\n\
253 System: SEGA MEGA DRIVE\n\
254 Game Title (Domestic): DOMESTIC US\n\
255 Game Title (Int.): INTERNATIONAL US\n\
256 Region Code: 0x55 ('U')\n\
257 Region: USA"
258 );
259 Ok(())
260 }
261
262 #[test]
263 fn test_analyze_genesis_data_japan() -> Result<(), RomAnalyzerError> {
264 let data =
265 generate_genesis_header(b"SEGA MEGA DRIVE ", b'J', "DOMESTIC JP", "INTERNATIONAL JP");
266 let analysis = analyze_genesis_data(&data, "test_rom_jp.md")?;
267
268 assert_eq!(analysis.source_name, "test_rom_jp.md");
269 assert_eq!(analysis.console_name, "SEGA MEGA DRIVE");
270 assert_eq!(analysis.game_title_domestic, "DOMESTIC JP");
271 assert_eq!(analysis.game_title_international, "INTERNATIONAL JP");
272 assert_eq!(analysis.region_code_byte, b'J');
273 assert_eq!(analysis.region, Region::JAPAN);
274 assert_eq!(analysis.region_string, "Japan (NTSC-J)");
275 Ok(())
276 }
277
278 #[test]
279 fn test_analyze_genesis_data_brazil() -> Result<(), RomAnalyzerError> {
280 let data = generate_genesis_header(b"SEGA MEGA DRIVE ", b'B', "DOMESTIC BRA", "INT BRA");
281 let analysis = analyze_genesis_data(&data, "test_rom_bra.md")?;
282
283 assert_eq!(analysis.source_name, "test_rom_bra.md");
284 assert_eq!(analysis.region, Region::EUROPE); assert_eq!(analysis.region_string, "Brazil (PAL-M)");
286 assert_eq!(analysis.region_code_byte, b'B');
287 Ok(())
288 }
289
290 #[test]
291 fn test_analyze_genesis_data_genesis_signature() -> Result<(), RomAnalyzerError> {
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_asia() -> Result<(), RomAnalyzerError> {
305 let data = generate_genesis_header(b"SEGA MEGA DRIVE ", b'A', "DOMESTIC ASIA", "INT ASIA");
306 let analysis = analyze_genesis_data(&data, "test_rom_asia.md")?;
307
308 assert_eq!(analysis.source_name, "test_rom_asia.md");
309 assert_eq!(analysis.region, Region::ASIA);
310 assert_eq!(analysis.region_string, "Asia (NTSC)");
311 assert_eq!(analysis.region_code_byte, b'A');
312 Ok(())
313 }
314
315 #[test]
316 fn test_analyze_genesis_data_china() -> Result<(), RomAnalyzerError> {
317 let data =
318 generate_genesis_header(b"SEGA MEGA DRIVE ", b'C', "DOMESTIC CHINA", "INT CHINA");
319 let analysis = analyze_genesis_data(&data, "test_rom_chn.md")?;
320
321 assert_eq!(analysis.source_name, "test_rom_chn.md");
322 assert_eq!(analysis.region, Region::CHINA);
323 assert_eq!(analysis.region_string, "China (NTSC)");
324 assert_eq!(analysis.region_code_byte, b'C');
325 Ok(())
326 }
327
328 #[test]
329 fn test_analyze_genesis_data_too_small() {
330 let data = vec![0; 100]; let result = analyze_genesis_data(&data, "too_small.md");
333 assert!(result.is_err());
334 assert!(result.unwrap_err().to_string().contains("too small"));
335 }
336
337 #[test]
338 fn test_map_region_all_codes() {
339 let test_cases = vec![
341 (b'J', "Japan (NTSC-J)", Region::JAPAN),
342 (b'U', "USA (NTSC-U)", Region::USA),
343 (b'E', "Europe (PAL)", Region::EUROPE),
344 (b'A', "Asia (NTSC)", Region::ASIA),
345 (b'B', "Brazil (PAL-M)", Region::EUROPE),
346 (b'C', "China (NTSC)", Region::CHINA),
347 (b'F', "France (PAL)", Region::EUROPE),
348 (b'K', "Korea (NTSC)", Region::KOREA),
349 (b'L', "UK (PAL)", Region::EUROPE),
350 (b'S', "Scandinavia (PAL)", Region::EUROPE),
351 (b'T', "Taiwan (NTSC)", Region::ASIA),
352 (0x34, "USA/Europe (NTSC/PAL)", Region::USA | Region::EUROPE),
353 (b'Z', "Unknown", Region::UNKNOWN), ];
355 for (code, expected_name, expected_region) in test_cases {
356 let (name, region) = map_region(code);
357 assert_eq!(name, expected_name, "Failed for code 0x{:02X}", code);
358 assert_eq!(region, expected_region, "Failed for code 0x{:02X}", code);
359 }
360 }
361}