1use std::error::Error;
10
11use serde::Serialize;
12
13use crate::error::RomAnalyzerError;
14use crate::region::{Region, check_region_mismatch};
15
16#[derive(Debug, PartialEq, Clone, Serialize)]
18pub struct GbaAnalysis {
19 pub source_name: String,
21 pub region: Region,
23 pub region_string: String,
25 pub region_mismatch: bool,
27 pub game_title: String,
29 pub game_code: String,
31 pub maker_code: String,
33}
34
35impl GbaAnalysis {
36 pub fn print(&self) -> String {
38 format!(
39 "{}\n\
40 System: Game Boy Advance (GBA)\n\
41 Game Title: {}\n\
42 Game Code: {}\n\
43 Maker Code: {}\n\
44 Region: {}",
45 self.source_name, self.game_title, self.game_code, self.maker_code, self.region
46 )
47 }
48}
49
50pub fn map_region(region_byte: u8) -> (&'static str, Region) {
85 match region_byte {
86 0x00 => ("Japan", Region::JAPAN),
87 0x01 => ("USA", Region::USA),
88 0x02 => ("Europe", Region::EUROPE),
89 b'J' => ("Japan", Region::JAPAN),
91 b'U' => ("USA", Region::USA),
92 b'E' => ("Europe", Region::EUROPE),
93 b'P' => ("Europe", Region::EUROPE), _ => ("Unknown", Region::UNKNOWN),
95 }
96}
97
98pub fn analyze_gba_data(data: &[u8], source_name: &str) -> Result<GbaAnalysis, Box<dyn Error>> {
115 const HEADER_SIZE: usize = 0xC0;
118 if data.len() < HEADER_SIZE {
119 return Err(Box::new(RomAnalyzerError::new(&format!(
120 "ROM data is too small to contain a GBA header (size: {} bytes, requires at least {} bytes).",
121 data.len(),
122 HEADER_SIZE
123 ))));
124 }
125
126 let game_title = String::from_utf8_lossy(&data[0xA0..0xAC])
128 .trim_matches(char::from(0)) .to_string();
130
131 let game_code = String::from_utf8_lossy(&data[0xAC..0xB0])
133 .trim_matches(char::from(0)) .to_string();
135
136 let maker_code = String::from_utf8_lossy(&data[0xB0..0xB2])
138 .trim_matches(char::from(0)) .to_string();
140
141 let region_code_byte = data[0xB4];
143
144 let (region_name, region) = map_region(region_code_byte);
146
147 let region_mismatch = check_region_mismatch(source_name, region);
148
149 Ok(GbaAnalysis {
150 source_name: source_name.to_string(),
151 region,
152 region_string: region_name.to_string(),
153 region_mismatch,
154 game_title,
155 game_code,
156 maker_code,
157 })
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use std::error::Error;
164
165 fn generate_gba_header(
167 game_code: &str,
168 maker_code: &str,
169 region_byte: u8,
170 title: &str,
171 ) -> Vec<u8> {
172 let mut data = vec![0; 0xC0]; let mut title_bytes = title.as_bytes().to_vec();
176 title_bytes.resize(12, 0);
177 data[0xA0..0xAC].copy_from_slice(&title_bytes);
178
179 let mut game_code_bytes = game_code.as_bytes().to_vec();
181 game_code_bytes.resize(4, 0);
182 data[0xAC..0xB0].copy_from_slice(&game_code_bytes);
183
184 let mut maker_code_bytes = maker_code.as_bytes().to_vec();
186 maker_code_bytes.resize(2, 0);
187 data[0xB0..0xB2].copy_from_slice(&maker_code_bytes);
188
189 data[0xB4] = region_byte;
191
192 data
193 }
194
195 #[test]
196 fn test_analyze_gba_data_japan_code() -> Result<(), Box<dyn Error>> {
197 let data = generate_gba_header("ABCD", "XX", 0x00, "GBA JP GAME"); let analysis = analyze_gba_data(&data, "test_rom_jp.gba")?;
199
200 assert_eq!(analysis.source_name, "test_rom_jp.gba");
201 assert_eq!(analysis.game_title, "GBA JP GAME");
202 assert_eq!(analysis.game_code, "ABCD");
203 assert_eq!(analysis.maker_code, "XX");
204 assert_eq!(analysis.region, Region::JAPAN);
205 assert_eq!(analysis.region_string, "Japan");
206 Ok(())
207 }
208
209 #[test]
210 fn test_analyze_gba_data_usa_code() -> Result<(), Box<dyn Error>> {
211 let data = generate_gba_header("EFGH", "YY", 0x01, "GBA US GAME"); let analysis = analyze_gba_data(&data, "test_rom_us.gba")?;
213
214 assert_eq!(analysis.source_name, "test_rom_us.gba");
215 assert_eq!(analysis.game_title, "GBA US GAME");
216 assert_eq!(analysis.game_code, "EFGH");
217 assert_eq!(analysis.maker_code, "YY");
218 assert_eq!(analysis.region, Region::USA);
219 assert_eq!(analysis.region_string, "USA");
220 Ok(())
221 }
222
223 #[test]
224 fn test_analyze_gba_data_europe_char() -> Result<(), Box<dyn Error>> {
225 let data = generate_gba_header("IJKL", "ZZ", b'E', "GBA EUR GAME"); let analysis = analyze_gba_data(&data, "test_rom_eur.gba")?;
227
228 assert_eq!(analysis.source_name, "test_rom_eur.gba");
229 assert_eq!(analysis.game_title, "GBA EUR GAME");
230 assert_eq!(analysis.game_code, "IJKL");
231 assert_eq!(analysis.maker_code, "ZZ");
232 assert_eq!(analysis.region, Region::EUROPE);
233 assert_eq!(analysis.region_string, "Europe");
234 Ok(())
235 }
236
237 #[test]
238 fn test_analyze_gba_data_japan_char() -> Result<(), Box<dyn Error>> {
239 let data = generate_gba_header("MNOP", "AA", b'J', "GBA JP CHAR"); let analysis = analyze_gba_data(&data, "test_rom_jp_char.gba")?;
241
242 assert_eq!(analysis.source_name, "test_rom_jp_char.gba");
243 assert_eq!(analysis.game_title, "GBA JP CHAR");
244 assert_eq!(analysis.game_code, "MNOP");
245 assert_eq!(analysis.maker_code, "AA");
246 assert_eq!(analysis.region, Region::JAPAN);
247 assert_eq!(analysis.region_string, "Japan");
248 Ok(())
249 }
250
251 #[test]
252 fn test_analyze_gba_data_unknown_code() -> Result<(), Box<dyn Error>> {
253 let data = generate_gba_header("QRST", "BB", 0xFF, "GBA UNKNOWN"); let analysis = analyze_gba_data(&data, "test_rom_unknown.gba")?;
255
256 assert_eq!(analysis.source_name, "test_rom_unknown.gba");
257 assert_eq!(analysis.region, Region::UNKNOWN);
258 assert_eq!(analysis.region_string, "Unknown");
259 Ok(())
260 }
261
262 #[test]
263 fn test_analyze_gba_data_too_small() {
264 let data = vec![0; 50]; let result = analyze_gba_data(&data, "too_small.gba");
267 assert!(result.is_err());
268 assert!(result.unwrap_err().to_string().contains("too small"));
269 }
270}