rom_analyzer/console/
gba.rs

1//! Provides header analysis functionality for Game Boy Advance (GBA) ROMs.
2//!
3//! This module can parse GBA ROM headers to extract game title, game code,
4//! maker code, and region information.
5//!
6//! GBA header documentation referenced here:
7//! <https://problemkaputt.de/gbatek-gba-cartridge-header.htm>
8
9use std::error::Error;
10
11use serde::Serialize;
12
13use crate::error::RomAnalyzerError;
14use crate::region::{Region, check_region_mismatch};
15
16/// Struct to hold the analysis results for a GBA ROM.
17#[derive(Debug, PartialEq, Clone, Serialize)]
18pub struct GbaAnalysis {
19    /// The name of the source file.
20    pub source_name: String,
21    /// The identified region(s) as a region::Region bitmask.
22    pub region: Region,
23    /// The identified region name (e.g., "Japan").
24    pub region_string: String,
25    /// If the region in the ROM header doesn't match the region in the filename.
26    pub region_mismatch: bool,
27    /// The game title extracted from the ROM header.
28    pub game_title: String,
29    /// The game code extracted from the ROM header.
30    pub game_code: String,
31    /// The maker code extracted from the ROM header.
32    pub maker_code: String,
33}
34
35impl GbaAnalysis {
36    /// Returns a printable String of the analysis results.
37    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
50/// Determines the Game Boy Advance game region name based on a given region byte.
51///
52/// The region byte typically comes from the ROM header. This function extracts the relevant bits
53/// from the byte and maps it to a human-readable region string and a Region bitmask.
54///
55/// # Arguments
56///
57/// * `region_byte` - The byte containing the region code, usually found in the ROM header.
58///
59/// # Returns
60///
61/// A tuple containing:
62/// - A `&'static str` representing the region as written in the ROM header (e.g., "USA", "Japan",
63///   "Europe") or "Unknown" if the region code is not recognized.
64/// - A `Region` bitmask representing the region(s) associated with the code.
65///
66/// # Examples
67///
68/// ```rust
69/// use rom_analyzer::console::gba::map_region;
70/// use rom_analyzer::region::Region;
71///
72/// let (region_str, region_mask) = map_region(0x00);
73/// assert_eq!(region_str, "Japan");
74/// assert_eq!(region_mask, Region::JAPAN);
75///
76/// let (region_str, region_mask) = map_region(0x01);
77/// assert_eq!(region_str, "USA");
78/// assert_eq!(region_mask, Region::USA);
79///
80/// let (region_str, region_mask) = map_region(0x02);
81/// assert_eq!(region_str, "Europe");
82/// assert_eq!(region_mask, Region::EUROPE);
83/// ```
84pub 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        // ASCII representations are also common
90        b'J' => ("Japan", Region::JAPAN),
91        b'U' => ("USA", Region::USA),
92        b'E' => ("Europe", Region::EUROPE),
93        b'P' => ("Europe", Region::EUROPE), // PAL
94        _ => ("Unknown", Region::UNKNOWN),
95    }
96}
97
98/// Analyzes Game Boy Advance (GBA) ROM data.
99///
100/// This function reads the GBA ROM header to extract the game title, game code,
101/// maker code, and region information. It then normalizes the region and performs
102/// a region mismatch check against the `source_name`.
103///
104/// # Arguments
105///
106/// * `data` - A byte slice (`&[u8]`) containing the raw ROM data.
107/// * `source_name` - The name of the ROM file, used for region mismatch checks.
108///
109/// # Returns
110///
111/// A `Result` which is:
112/// - `Ok(GbaAnalysis)` containing the detailed analysis results.
113/// - `Err(Box<dyn Error>)` if the ROM data is too small to contain a valid GBA header.
114pub fn analyze_gba_data(data: &[u8], source_name: &str) -> Result<GbaAnalysis, Box<dyn Error>> {
115    // GBA header is at offset 0x0. Relevant info: Game Title (0xA0-0xAC), Game Code (0xAC-0xB0), Maker Code (0xB0-0xB2), Region (0xB4).
116    // The header is typically 192 bytes (0xC0), but we'll use a slightly larger safety margin.
117    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    // Extract Game Title (12 bytes, null-terminated)
127    let game_title = String::from_utf8_lossy(&data[0xA0..0xAC])
128        .trim_matches(char::from(0)) // Remove null bytes
129        .to_string();
130
131    // Extract Game Code (4 bytes, ASCII)
132    let game_code = String::from_utf8_lossy(&data[0xAC..0xB0])
133        .trim_matches(char::from(0)) // Remove null bytes, though usually not null-terminated here
134        .to_string();
135
136    // Extract Maker Code (2 bytes, ASCII)
137    let maker_code = String::from_utf8_lossy(&data[0xB0..0xB2])
138        .trim_matches(char::from(0)) // Remove null bytes
139        .to_string();
140
141    // Extract Region Code (1 byte at 0xB4)
142    let region_code_byte = data[0xB4];
143
144    // Determine region name based on the byte value.
145    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    /// Helper function to generate a minimal GBA header for testing.
166    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]; // Ensure enough space for header
173
174        // Game Title (max 10 chars + null, but we use 0xA0..0xAC which is 12 bytes for safety)
175        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        // Game Code (4 bytes, ASCII)
180        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        // Maker Code (2 bytes, ASCII)
185        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        // Region Code (1 byte at 0xB4)
190        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"); // Japan region code 0x00
198        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"); // USA region code 0x01
212        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"); // Europe region char 'E'
226        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"); // Japan region char 'J'
240        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"); // Unknown region code
254        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        // Test with data smaller than the minimum required size for analysis.
265        let data = vec![0; 50]; // Smaller than 0xC0
266        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}