1use std::error::Error;
10
11use serde::Serialize;
12
13use crate::error::RomAnalyzerError;
14use crate::region::{Region, check_region_mismatch};
15
16const GB_TITLE_START: usize = 0x134;
17const GB_TITLE_END: usize = 0x143;
18const GB_DESTINATION: usize = 0x14A;
19
20const GBC_SYSTEM_TYPE: usize = 0x143;
21const GBC_TITLE_END: usize = 0x13F;
22
23#[derive(Debug, PartialEq, Clone, Serialize)]
25pub struct GbAnalysis {
26 pub source_name: String,
28 pub region: Region,
30 pub region_string: String,
32 pub region_mismatch: bool,
34 pub system_type: String,
36 pub game_title: String,
38 pub destination_code: u8,
40}
41
42impl GbAnalysis {
43 pub fn print(&self) -> String {
45 format!(
46 "{}\n\
47 System: {}\n\
48 Game Title: {}\n\
49 Region Code: 0x{:02X}\n\
50 Region: {}",
51 self.source_name, self.system_type, self.game_title, self.destination_code, self.region
52 )
53 }
54}
55
56pub fn map_region(region_byte: u8) -> (&'static str, Region) {
91 match region_byte {
92 0x00 => ("Japan", Region::JAPAN),
93 0x01 => ("Non-Japan (International)", Region::USA | Region::EUROPE),
94 _ => ("Unknown", Region::UNKNOWN),
95 }
96}
97
98pub fn analyze_gb_data(data: &[u8], source_name: &str) -> Result<GbAnalysis, Box<dyn Error>> {
115 const HEADER_SIZE: usize = 0x150;
118 if data.len() < HEADER_SIZE {
119 return Err(Box::new(RomAnalyzerError::new(&format!(
120 "ROM data is too small to contain a Game Boy header (size: {} bytes, requires at least {} bytes).",
121 data.len(),
122 HEADER_SIZE
123 ))));
124 }
125
126 let system_type = if data[GBC_SYSTEM_TYPE] == 0x80 || data[GBC_SYSTEM_TYPE] == 0xC0 {
129 "Game Boy Color (GBC)"
130 } else {
131 "Game Boy (GB)"
132 };
133
134 let title_end = if system_type == "Game Boy Color (GBC)" {
135 GBC_TITLE_END
136 } else {
137 GB_TITLE_END
138 };
139 let game_title = String::from_utf8_lossy(&data[GB_TITLE_START..title_end])
140 .trim_matches(char::from(0))
141 .to_string();
142
143 let destination_code = data[GB_DESTINATION];
144 let (region_name, region) = map_region(destination_code);
145
146 let region_mismatch = check_region_mismatch(source_name, region);
147
148 Ok(GbAnalysis {
149 source_name: source_name.to_string(),
150 region,
151 region_string: region_name.to_string(),
152 region_mismatch,
153 system_type: system_type.to_string(),
154 game_title,
155 destination_code,
156 })
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use std::error::Error;
163
164 fn generate_gb_header(destination_code: u8, system_byte: u8, title: &str) -> Vec<u8> {
166 let mut data = vec![0; 0x150]; data[0x100..0x104].copy_from_slice(b"LOGO"); let mut title_bytes = title.as_bytes().to_vec();
173 let mut title_length = 11;
174 if system_byte & 0x80 == 0x00 {
176 title_length = 15;
177 }
178 title_bytes.resize(title_length, 0);
179 data[GB_TITLE_START..(GB_TITLE_START + title_length)].copy_from_slice(&title_bytes);
180
181 data[GB_DESTINATION] = destination_code;
182
183 data[GBC_SYSTEM_TYPE] = system_byte;
185
186 data
187 }
188
189 #[test]
190 fn test_analyze_gb_data_japan() -> Result<(), Box<dyn Error>> {
191 let data = generate_gb_header(0x00, 0x00, "GAMETITLE"); let analysis = analyze_gb_data(&data, "test_rom_jp.gb")?;
193
194 assert_eq!(analysis.source_name, "test_rom_jp.gb");
195 assert_eq!(analysis.system_type, "Game Boy (GB)");
196 assert_eq!(analysis.game_title, "GAMETITLE");
197 assert_eq!(analysis.destination_code, 0x00);
198 assert_eq!(analysis.region, Region::JAPAN);
199 assert_eq!(analysis.region_string, "Japan");
200 Ok(())
201 }
202
203 #[test]
204 fn test_analyze_gb_data_non_japan() -> Result<(), Box<dyn Error>> {
205 let data = generate_gb_header(0x01, 0x00, "GAMETITLE"); let analysis = analyze_gb_data(&data, "test_rom_us.gb")?;
207
208 assert_eq!(analysis.source_name, "test_rom_us.gb");
209 assert_eq!(analysis.system_type, "Game Boy (GB)");
210 assert_eq!(analysis.game_title, "GAMETITLE");
211 assert_eq!(analysis.destination_code, 0x01);
212 assert_eq!(analysis.region, Region::USA | Region::EUROPE);
213 assert_eq!(analysis.region_string, "Non-Japan (International)");
214 Ok(())
215 }
216
217 #[test]
218 fn test_analyze_gbc_data_japan() -> Result<(), Box<dyn Error>> {
219 let data = generate_gb_header(0x00, 0x80, "GBC TITLE"); let analysis = analyze_gb_data(&data, "test_rom_jp.gbc")?;
221
222 assert_eq!(analysis.source_name, "test_rom_jp.gbc");
223 assert_eq!(analysis.system_type, "Game Boy Color (GBC)");
224 assert_eq!(analysis.game_title, "GBC TITLE");
225 assert_eq!(analysis.destination_code, 0x00);
226 assert_eq!(analysis.region, Region::JAPAN);
227 assert_eq!(analysis.region_string, "Japan");
228 Ok(())
229 }
230
231 #[test]
232 fn test_analyze_gbc_data_non_japan() -> Result<(), Box<dyn Error>> {
233 let data = generate_gb_header(0x01, 0xC0, "GBC TITLE"); let analysis = analyze_gb_data(&data, "test_rom_eur.gbc")?;
235
236 assert_eq!(analysis.source_name, "test_rom_eur.gbc");
237 assert_eq!(analysis.system_type, "Game Boy Color (GBC)");
238 assert_eq!(analysis.game_title, "GBC TITLE");
239 assert_eq!(analysis.destination_code, 0x01);
240 assert_eq!(analysis.region, Region::USA | Region::EUROPE);
241 assert_eq!(analysis.region_string, "Non-Japan (International)");
242 Ok(())
243 }
244
245 #[test]
248 fn test_analyze_gb_long_title() -> Result<(), Box<dyn Error>> {
249 let data = generate_gb_header(0x00, 0x00, "LOOOOOONG TITLE"); let analysis = analyze_gb_data(&data, "test_rom_jp.gbc")?;
251
252 assert_eq!(analysis.source_name, "test_rom_jp.gbc");
253 assert_eq!(analysis.system_type, "Game Boy (GB)");
254 assert_eq!(analysis.game_title, "LOOOOOONG TITLE");
255 assert_eq!(analysis.destination_code, 0x00);
256 assert_eq!(analysis.region, Region::JAPAN);
257 assert_eq!(analysis.region_string, "Japan");
258 Ok(())
259 }
260
261 #[test]
262 fn test_analyze_gbc_long_title() -> Result<(), Box<dyn Error>> {
263 let data = generate_gb_header(0x00, 0x80, "LOONG TITLE"); let analysis = analyze_gb_data(&data, "test_rom_jp.gbc")?;
265
266 assert_eq!(analysis.source_name, "test_rom_jp.gbc");
267 assert_eq!(analysis.system_type, "Game Boy Color (GBC)");
268 assert_eq!(analysis.game_title, "LOONG TITLE");
269 assert_eq!(analysis.destination_code, 0x00);
270 assert_eq!(analysis.region, Region::JAPAN);
271 assert_eq!(analysis.region_string, "Japan");
272 Ok(())
273 }
274
275 #[test]
276 fn test_analyze_gb_unknown_code() -> Result<(), Box<dyn Error>> {
277 let data = generate_gb_header(0x02, 0x00, "UNKNOWN REG"); let analysis = analyze_gb_data(&data, "test_rom_unknown.gb")?;
279
280 assert_eq!(analysis.source_name, "test_rom_unknown.gb");
281 assert_eq!(analysis.region, Region::UNKNOWN);
282 assert_eq!(analysis.region_string, "Unknown");
283 Ok(())
284 }
285
286 #[test]
287 fn test_analyze_gb_data_too_small() {
288 let data = vec![0; 100]; let result = analyze_gb_data(&data, "too_small.gb");
291 assert!(result.is_err());
292 assert!(result.unwrap_err().to_string().contains("too small"));
293 }
294}