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