use serde::Serialize;
use crate::error::RomAnalyzerError;
use crate::region::{Region, check_region_mismatch};
#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct GbaAnalysis {
pub source_name: String,
pub region: Region,
pub region_string: String,
pub region_mismatch: bool,
pub game_title: String,
pub game_code: String,
pub maker_code: String,
}
impl GbaAnalysis {
pub fn print(&self) -> String {
format!(
"{}\n\
System: Game Boy Advance (GBA)\n\
Game Title: {}\n\
Game Code: {}\n\
Maker Code: {}\n\
Region: {}",
self.source_name, self.game_title, self.game_code, self.maker_code, self.region
)
}
}
pub fn map_region(region_byte: u8) -> (&'static str, Region) {
match region_byte {
0x00 => ("Japan", Region::JAPAN),
0x01 => ("USA", Region::USA),
0x02 => ("Europe", Region::EUROPE),
b'J' => ("Japan", Region::JAPAN),
b'U' => ("USA", Region::USA),
b'E' => ("Europe", Region::EUROPE),
b'P' => ("Europe", Region::EUROPE), _ => ("Unknown", Region::UNKNOWN),
}
}
pub fn analyze_gba_data(data: &[u8], source_name: &str) -> Result<GbaAnalysis, RomAnalyzerError> {
const HEADER_SIZE: usize = 0xC0;
if data.len() < HEADER_SIZE {
return Err(RomAnalyzerError::DataTooSmall {
file_size: data.len(),
required_size: HEADER_SIZE,
details: "GBA header".to_string(),
});
}
let game_title = String::from_utf8_lossy(&data[0xA0..0xAC])
.trim_matches(char::from(0)) .to_string();
let game_code = String::from_utf8_lossy(&data[0xAC..0xB0])
.trim_matches(char::from(0)) .to_string();
let maker_code = String::from_utf8_lossy(&data[0xB0..0xB2])
.trim_matches(char::from(0)) .to_string();
let region_code_byte = data[0xB4];
let (region_name, region) = map_region(region_code_byte);
let region_mismatch = check_region_mismatch(source_name, region);
Ok(GbaAnalysis {
source_name: source_name.to_string(),
region,
region_string: region_name.to_string(),
region_mismatch,
game_title,
game_code,
maker_code,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_gba_header(
game_code: &str,
maker_code: &str,
region_byte: u8,
title: &str,
) -> Vec<u8> {
let mut data = vec![0; 0xC0];
let mut title_bytes = title.as_bytes().to_vec();
title_bytes.resize(12, 0);
data[0xA0..0xAC].copy_from_slice(&title_bytes);
let mut game_code_bytes = game_code.as_bytes().to_vec();
game_code_bytes.resize(4, 0);
data[0xAC..0xB0].copy_from_slice(&game_code_bytes);
let mut maker_code_bytes = maker_code.as_bytes().to_vec();
maker_code_bytes.resize(2, 0);
data[0xB0..0xB2].copy_from_slice(&maker_code_bytes);
data[0xB4] = region_byte;
data
}
#[test]
fn test_analyze_gba_data_japan_code() -> Result<(), RomAnalyzerError> {
let data = generate_gba_header("ABCD", "XX", 0x00, "GBA JP GAME"); let analysis = analyze_gba_data(&data, "test_rom_jp.gba")?;
assert_eq!(analysis.source_name, "test_rom_jp.gba");
assert_eq!(analysis.game_title, "GBA JP GAME");
assert_eq!(analysis.game_code, "ABCD");
assert_eq!(analysis.maker_code, "XX");
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan");
assert_eq!(
analysis.print(),
"test_rom_jp.gba\n\
System: Game Boy Advance (GBA)\n\
Game Title: GBA JP GAME\n\
Game Code: ABCD\n\
Maker Code: XX\n\
Region: Japan"
);
Ok(())
}
#[test]
fn test_analyze_gba_data_pal_char() -> Result<(), RomAnalyzerError> {
let data = generate_gba_header("YZAB", "DD", b'P', "GBA PAL GAME"); let analysis = analyze_gba_data(&data, "test_rom_pal.gba")?;
assert_eq!(analysis.source_name, "test_rom_pal.gba");
assert_eq!(analysis.game_title, "GBA PAL GAME");
assert_eq!(analysis.game_code, "YZAB");
assert_eq!(analysis.maker_code, "DD");
assert_eq!(analysis.region, Region::EUROPE);
assert_eq!(analysis.region_string, "Europe");
assert_eq!(
analysis.print(),
"test_rom_pal.gba\n\
System: Game Boy Advance (GBA)\n\
Game Title: GBA PAL GAME\n\
Game Code: YZAB\n\
Maker Code: DD\n\
Region: Europe"
);
Ok(())
}
#[test]
fn test_analyze_gba_data_europe_char() -> Result<(), RomAnalyzerError> {
let data = generate_gba_header("IJKL", "ZZ", b'E', "GBA EUR GAME"); let analysis = analyze_gba_data(&data, "test_rom_eur.gba")?;
assert_eq!(analysis.source_name, "test_rom_eur.gba");
assert_eq!(analysis.game_title, "GBA EUR GAME");
assert_eq!(analysis.game_code, "IJKL");
assert_eq!(analysis.maker_code, "ZZ");
assert_eq!(analysis.region, Region::EUROPE);
assert_eq!(analysis.region_string, "Europe");
Ok(())
}
#[test]
fn test_analyze_gba_data_japan_char() -> Result<(), RomAnalyzerError> {
let data = generate_gba_header("MNOP", "AA", b'J', "GBA JP CHAR"); let analysis = analyze_gba_data(&data, "test_rom_jp_char.gba")?;
assert_eq!(analysis.source_name, "test_rom_jp_char.gba");
assert_eq!(analysis.game_title, "GBA JP CHAR");
assert_eq!(analysis.game_code, "MNOP");
assert_eq!(analysis.maker_code, "AA");
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan");
Ok(())
}
#[test]
fn test_analyze_gba_data_usa_char() -> Result<(), RomAnalyzerError> {
let data = generate_gba_header("UVWX", "CC", b'U', "GBA US CHAR"); let analysis = analyze_gba_data(&data, "test_rom_us_char.gba")?;
assert_eq!(analysis.source_name, "test_rom_us_char.gba");
assert_eq!(analysis.game_title, "GBA US CHAR");
assert_eq!(analysis.game_code, "UVWX");
assert_eq!(analysis.maker_code, "CC");
assert_eq!(analysis.region, Region::USA);
assert_eq!(analysis.region_string, "USA");
assert_eq!(
analysis.print(),
"test_rom_us_char.gba\n\
System: Game Boy Advance (GBA)\n\
Game Title: GBA US CHAR\n\
Game Code: UVWX\n\
Maker Code: CC\n\
Region: USA"
);
Ok(())
}
#[test]
fn test_analyze_gba_data_too_small() {
let data = vec![0; 50]; let result = analyze_gba_data(&data, "too_small.gba");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
}