gba_cart 0.4.2

GBA file header library and utilities.
Documentation
// SPDX-License-Identifier: LGPL-2.1-or-later OR GPL-2.0-or-later OR MPL-2.0
// SPDX-FileCopyrightText: 2026 Gabriel Marcano <gabemarcano@yahoo.com>

use crate::error::Error;

use std::fmt;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::str;

use byteorder::LittleEndian;
use byteorder::ReadBytesExt;

/// Represents the language regions the cartridge is released for.
#[derive(Debug)]
pub enum LanguageRegion {
    Japan,
    English,
    EuropeElsewhere,
    German,
    French,
    Italian,
    Spanish,
    Unknown(char),
}

impl fmt::Display for LanguageRegion {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Japan => write!(fmt, "Japan")?,
            Self::English => write!(fmt, "English")?,
            Self::EuropeElsewhere => write!(fmt, "Other Europe")?,
            Self::German => write!(fmt, "German")?,
            Self::French => write!(fmt, "French")?,
            Self::Italian => write!(fmt, "Italian")?,
            Self::Spanish => write!(fmt, "Spanish")?,
            Self::Unknown(ch) => write!(fmt, "Unknown region, code {ch}")?,
        }
        Ok(())
    }
}

impl From<char> for LanguageRegion {
    fn from(data: char) -> Self {
        match data {
            'J' => Self::Japan,
            'E' => Self::English,
            'P' => Self::EuropeElsewhere,
            'D' => Self::German,
            'F' => Self::French,
            'I' => Self::Italian,
            'S' => Self::Spanish,
            _ => Self::Unknown(data),
        }
    }
}

/// GBA multiplay boot mode.
#[derive(Debug)]
pub enum MultiplayBootMode {
    None,
    Joybus,
    Normal,
    Multiplay,
    /// Any other data that's not known. This can happen on carts that do not support multiplay as
    /// other data may be placed in the multiplay header region.
    Unknown(u8),
}

impl From<u8> for MultiplayBootMode {
    fn from(data: u8) -> Self {
        match data {
            0 => Self::None,
            1 => Self::Joybus,
            2 => Self::Normal,
            3 => Self::Multiplay,
            _ => Self::Unknown(data),
        }
    }
}

impl fmt::Display for MultiplayBootMode {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::None => write!(fmt, "none")?,
            Self::Joybus => write!(fmt, "joybus")?,
            Self::Normal => write!(fmt, "normal")?,
            Self::Multiplay => write!(fmt, "multiplay")?,
            Self::Unknown(data) => write!(fmt, "unknown {data}")?,
        }
        Ok(())
    }
}

/// Represents the metadata held by the GBA internal header.
#[derive(Debug)]
pub struct Metadata {
    /// The entry point the bootrom jumps to after it finishes.
    pub entry_point: u32,
    /// Compressed Nintendo logo.
    pub nintendo_logo: [u8; 156],
    /// The title of the game, maximum of 12 uppercase ASCII characters.
    pub title: String,
    /// Game code, 4 uppercase ASCII characters. The first character is some sort of category,
    /// second and third are unique game identifiers, and the fourth is a language code.
    pub game_code: String,
    /// Manufacturer code, 2 uppercase ASCII characters.
    pub manufacturer_code: String,
    /// GBA unit code. Seems to be 0x00 for all units?
    pub main_unit_code: u8,
    /// The type of device. Usually 0x00 for GBA, bit 7 is Debugging And Communication System
    /// (DACS) related apparently.
    pub device_type: u8,
    /// Software version.
    pub software_version: u8,
    /// Checksum for the header. The checksum covers all bytes from the title through the software
    /// version fields, inclusive.
    pub header_checksum: u8,
}

/// Multiboot related metadata.
#[derive(Debug)]
pub struct MultiBootHeader {
    /// Entry point if booted using Normal or Multiplay transfer mode (not Joybus mode).
    pub ram_entry_point: u32,
    /// Boot mode.
    pub boot_mode: MultiplayBootMode,
    /// The ID of the current device when booted in Normal or Multiplay modes.
    pub slave_id_number: u8,
    /// Entry point if booted using Joybus mode.
    pub joybus_entry_point: u32,
}

pub trait MetadataRead {
    /// Parses the GBA ROM metadata from the object provided, returning a Metadata object with the
    /// header metadata.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Parse`] if the header cannot be found or if a field in the header contains
    /// an unexpected value.
    /// Returns [`Error::Io`] if an IO error took place while reading from the file.
    fn read_gba_metadata(&mut self) -> Result<Metadata, Error>;
}

impl Metadata {
    /// Returns a new Metadata instance.
    ///
    /// # Errors
    ///
    /// See [`MetadataRead::read_gba_metadata`] for possible errors.
    pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
        io.read_gba_metadata()
    }
}

/// Trims null and whitespace characters (in that order) from the given string.
fn trim(string: &str) -> &str {
    string.trim_matches('\0').trim()
}

impl<T: Read + Seek> MetadataRead for T {
    fn read_gba_metadata(&mut self) -> Result<Metadata, Error> {
        self.seek(SeekFrom::Start(0))?;

        let entry_point = self.read_u32::<LittleEndian>()?;
        let mut nintendo_logo = [0u8; 156];
        self.read_exact(&mut nintendo_logo)?;

        self.seek(SeekFrom::Start(0xA0))?;
        let mut title = [0u8; 12];
        self.read_exact(&mut title)?;
        let title = trim(str::from_utf8(&title)?).to_string();
        let mut game_code = [0u8; 4];
        self.read_exact(&mut game_code)?;
        let game_code = trim(str::from_utf8(&game_code)?).to_string();
        let mut manufacturer_code = [0u8; 2];
        self.read_exact(&mut manufacturer_code)?;
        let manufacturer_code = trim(str::from_utf8(&manufacturer_code)?).to_string();

        self.seek(SeekFrom::Current(1))?;
        let main_unit_code = self.read_u8()?;
        let device_type = self.read_u8()?;
        self.seek(SeekFrom::Current(7))?;
        let software_version = self.read_u8()?;
        let header_checksum = self.read_u8()?;

        Ok(Metadata {
            entry_point,
            nintendo_logo,
            title,
            game_code,
            manufacturer_code,
            main_unit_code,
            device_type,
            software_version,
            header_checksum,
        })
    }
}

impl Metadata {
    /// Returns the header's checksum.
    ///
    /// # Errors
    /// [`Error::Io`] if there are any IO errors while seeking and reading the header from the
    /// cart's underlying IO object.
    pub fn header_checksum<T: Read + Seek>(&mut self, io: &mut T) -> Result<u8, Error> {
        io.seek(SeekFrom::Start(0xA0))?;
        let mut checksum = 0u8;
        let mut data = [0u8; 0xBC - 0xA0];
        // It's faster to read in the entire header
        io.read_exact(&mut data)?;
        for byte in &data {
            checksum = checksum.wrapping_sub(*byte);
        }
        checksum = checksum.wrapping_sub(0x19);
        Ok(checksum)
    }
}