pub const HEADER_SIZE: usize = 0xC0;
pub const TITLE_OFFSET: usize = 0xA0;
pub const TITLE_LEN: usize = 12;
pub const GAME_CODE_OFFSET: usize = 0xAC;
pub const GAME_CODE_LEN: usize = 4;
pub const MAKER_CODE_OFFSET: usize = 0xB0;
pub const MAKER_CODE_LEN: usize = 2;
pub const FIXED_BYTE_OFFSET: usize = 0xB2;
pub const FIXED_BYTE_VALUE: u8 = 0x96;
pub const COMPLEMENT_CHECK_OFFSET: usize = 0xBD;
pub const COMPLEMENT_CHECK_RANGE: std::ops::RangeInclusive<usize> = 0xA0..=0xBC;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GbaHeader {
pub title: String,
pub game_code: String,
pub maker_code: String,
pub fixed_byte: u8,
pub complement_check: u8,
pub computed_complement: u8,
}
impl GbaHeader {
pub fn is_valid(&self) -> bool {
self.fixed_byte == FIXED_BYTE_VALUE && self.complement_check == self.computed_complement
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeaderError {
TooShort,
}
pub fn parse_header(rom: &[u8]) -> Result<GbaHeader, HeaderError> {
if rom.len() < HEADER_SIZE {
return Err(HeaderError::TooShort);
}
let title = ascii_string(&rom[TITLE_OFFSET..TITLE_OFFSET + TITLE_LEN]);
let game_code = ascii_string(&rom[GAME_CODE_OFFSET..GAME_CODE_OFFSET + GAME_CODE_LEN]);
let maker_code = ascii_string(&rom[MAKER_CODE_OFFSET..MAKER_CODE_OFFSET + MAKER_CODE_LEN]);
let fixed_byte = rom[FIXED_BYTE_OFFSET];
let complement_check = rom[COMPLEMENT_CHECK_OFFSET];
let computed_complement = compute_complement_check(rom);
Ok(GbaHeader {
title,
game_code,
maker_code,
fixed_byte,
complement_check,
computed_complement,
})
}
pub fn compute_complement_check(rom: &[u8]) -> u8 {
if rom.len() <= *COMPLEMENT_CHECK_RANGE.end() {
return 0;
}
let sum = rom[COMPLEMENT_CHECK_RANGE]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b));
sum.wrapping_sub(0x19)
}
fn ascii_string(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_valid_header(title: &str, game_code: &str, maker_code: &str) -> Vec<u8> {
let mut rom = vec![0u8; HEADER_SIZE];
let n = title.len().min(TITLE_LEN);
rom[TITLE_OFFSET..TITLE_OFFSET + n].copy_from_slice(&title.as_bytes()[..n]);
rom[GAME_CODE_OFFSET..GAME_CODE_OFFSET + GAME_CODE_LEN]
.copy_from_slice(game_code.as_bytes());
rom[MAKER_CODE_OFFSET..MAKER_CODE_OFFSET + MAKER_CODE_LEN]
.copy_from_slice(maker_code.as_bytes());
rom[FIXED_BYTE_OFFSET] = FIXED_BYTE_VALUE;
rom[COMPLEMENT_CHECK_OFFSET] = compute_complement_check(&rom);
rom
}
#[test]
fn rejects_rom_shorter_than_header() {
let rom = vec![0u8; HEADER_SIZE - 1];
assert_eq!(parse_header(&rom), Err(HeaderError::TooShort));
}
#[test]
fn parses_title_game_and_maker_code() {
let rom = make_valid_header("POKEMON RED ", "AGBE", "01");
let header = parse_header(&rom).unwrap();
assert_eq!(header.title, "POKEMON RED ");
assert_eq!(header.game_code, "AGBE");
assert_eq!(header.maker_code, "01");
}
#[test]
fn valid_header_has_canonical_fixed_byte_and_passes_checksum() {
let rom = make_valid_header("HELLO", "ABCD", "EF");
let header = parse_header(&rom).unwrap();
assert_eq!(header.fixed_byte, FIXED_BYTE_VALUE);
assert_eq!(header.complement_check, header.computed_complement);
assert!(header.is_valid());
}
#[test]
fn detects_bad_fixed_byte() {
let mut rom = make_valid_header("HELLO", "ABCD", "EF");
rom[FIXED_BYTE_OFFSET] = 0x00;
rom[COMPLEMENT_CHECK_OFFSET] = compute_complement_check(&rom);
let header = parse_header(&rom).unwrap();
assert!(!header.is_valid());
}
#[test]
fn detects_bad_complement_check() {
let mut rom = make_valid_header("HELLO", "ABCD", "EF");
rom[COMPLEMENT_CHECK_OFFSET] = rom[COMPLEMENT_CHECK_OFFSET].wrapping_add(1);
let header = parse_header(&rom).unwrap();
assert!(!header.is_valid());
}
#[test]
fn null_padded_title_truncates_at_first_zero() {
let mut rom = vec![0u8; HEADER_SIZE];
rom[TITLE_OFFSET..TITLE_OFFSET + 4].copy_from_slice(b"GAME");
rom[FIXED_BYTE_OFFSET] = FIXED_BYTE_VALUE;
rom[COMPLEMENT_CHECK_OFFSET] = compute_complement_check(&rom);
let header = parse_header(&rom).unwrap();
assert_eq!(header.title, "GAME");
}
#[test]
fn complement_check_matches_known_vector() {
let rom = vec![0u8; HEADER_SIZE];
assert_eq!(compute_complement_check(&rom), 0xE7);
}
}