rom_analyzer/console/
gb.rs

1//! Provides header analysis functionality for Game Boy (GB) and Game Boy Color (GBC) ROMs.
2//!
3//! This module can parse GB/GBC ROM headers to extract game title, system type,
4//! and region information.
5//!
6//! Gameboy/Color header documentation referenced here:
7//! <https://gbdev.io/pandocs/The_Cartridge_Header.html>
8
9use 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/// Struct to hold the analysis results for a Game Boy ROM.
24#[derive(Debug, PartialEq, Clone, Serialize)]
25pub struct GbAnalysis {
26    /// The name of the source file.
27    pub source_name: String,
28    /// The identified region(s) as a region::Region bitmask.
29    pub region: Region,
30    /// The identified region name (e.g., "Japan").
31    pub region_string: String,
32    /// If the region in the ROM header doesn't match the region in the filename.
33    pub region_mismatch: bool,
34    /// The identified system type (e.g., "Game Boy (GB)" or "Game Boy Color (GBC)").
35    pub system_type: String,
36    /// The game title extracted from the ROM header.
37    pub game_title: String,
38    /// The raw destination code byte.
39    pub destination_code: u8,
40}
41
42impl GbAnalysis {
43    /// Returns a printable String of the analysis results.
44    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
56/// Determines the Game Boy game region based on a given region byte.
57///
58/// The region byte typically comes from the ROM header. This function extracts the relevant bits
59/// from the byte and maps it to a human-readable region string and a Region bitmask.
60///
61/// # Arguments
62///
63/// * `region_byte` - The byte containing the region code, usually found in the ROM header.
64///
65/// # Returns
66///
67/// A tuple containing:
68/// - A `&'static str` representing the region as written in the ROM header (e.g., "Japan",
69///   "Non-Japan (International)") or "Unknown" if the region code is not recognized.
70/// - A `Region` bitmask representing the region(s) associated with the code.
71///
72/// # Examples
73///
74/// ```rust
75/// use rom_analyzer::console::gb::map_region;
76/// use rom_analyzer::region::Region;
77///
78/// let (region_str, region_mask) = map_region(0x00);
79/// assert_eq!(region_str, "Japan");
80/// assert_eq!(region_mask, Region::JAPAN);
81///
82/// let (region_str, region_mask) = map_region(0x01);
83/// assert_eq!(region_str, "Non-Japan (International)");
84/// assert_eq!(region_mask, Region::USA | Region::EUROPE);
85///
86/// let (region_str, region_mask) = map_region(0x02);
87/// assert_eq!(region_str, "Unknown");
88/// assert_eq!(region_mask, Region::UNKNOWN);
89/// ```
90pub 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
98/// Analyzes Game Boy (GB) and Game Boy Color (GBC) ROM data.
99///
100/// This function reads the ROM header to determine the system type (GB or GBC),
101/// extract the game title and identify the destination code which indicates the region.
102/// It also performs 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(GbAnalysis)` containing the detailed analysis results.
113/// - `Err(Box<dyn Error>)` if the ROM data is too small to contain a valid header.
114pub fn analyze_gb_data(data: &[u8], source_name: &str) -> Result<GbAnalysis, Box<dyn Error>> {
115    // The Game Boy header is located at offset 0x100.
116    // The relevant information for region and system type are within the first 0x150 bytes.
117    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    // System type is determined by a specific byte in the header.
127    // 0x80 or 0xC0 indicates GBC
128    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    /// Helper function to generate a minimal Game Boy header for testing.
165    fn generate_gb_header(destination_code: u8, system_byte: u8, title: &str) -> Vec<u8> {
166        let mut data = vec![0; 0x150]; // Ensure enough space for header
167
168        // Signature (usually present, but not strictly required for region/system analysis)
169        data[0x100..0x104].copy_from_slice(b"LOGO"); // Dummy signature
170
171        // Game Title (11 chars for GBC, 15 for GB)
172        let mut title_bytes = title.as_bytes().to_vec();
173        let mut title_length = 11;
174        // Check if GBC
175        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        // System Type Byte
184        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"); // Japan, GB
192        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"); // Non-Japan, GB
206        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"); // Japan, GBC
220        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"); // Non-Japan, GBC (using 0xC0 for system byte)
234        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    // GB uses 15 bits for title name while GBC uses 11
246    // Test that we properly read longer title names
247    #[test]
248    fn test_analyze_gb_long_title() -> Result<(), Box<dyn Error>> {
249        let data = generate_gb_header(0x00, 0x00, "LOOOOOONG TITLE"); // Japan, GB
250        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"); // Japan, GB
264        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"); // Unknown region code
278        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        // Test with data smaller than the minimum required size for analysis.
289        let data = vec![0; 100]; // Smaller than 0x150
290        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}