Skip to main content

gba_cart/
metadata.rs

1// SPDX-License-Identifier: LGPL-2.1-or-later OR GPL-2.0-or-later OR MPL-2.0
2// SPDX-FileCopyrightText: 2026 Gabriel Marcano <gabemarcano@yahoo.com>
3
4use crate::error::Error;
5
6use std::fmt;
7use std::io::Read;
8use std::io::Seek;
9use std::io::SeekFrom;
10use std::str;
11
12use byteorder::LittleEndian;
13use byteorder::ReadBytesExt;
14
15/// Represents the language regions the cartridge is released for.
16#[derive(Debug)]
17pub enum LanguageRegion {
18    Japan,
19    English,
20    EuropeElsewhere,
21    German,
22    French,
23    Italian,
24    Spanish,
25    Unknown(char),
26}
27
28impl fmt::Display for LanguageRegion {
29    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
30        match self {
31            Self::Japan => write!(fmt, "Japan")?,
32            Self::English => write!(fmt, "English")?,
33            Self::EuropeElsewhere => write!(fmt, "Other Europe")?,
34            Self::German => write!(fmt, "German")?,
35            Self::French => write!(fmt, "French")?,
36            Self::Italian => write!(fmt, "Italian")?,
37            Self::Spanish => write!(fmt, "Spanish")?,
38            Self::Unknown(ch) => write!(fmt, "Unknown region, code {ch}")?,
39        }
40        Ok(())
41    }
42}
43
44impl From<char> for LanguageRegion {
45    fn from(data: char) -> Self {
46        match data {
47            'J' => Self::Japan,
48            'E' => Self::English,
49            'P' => Self::EuropeElsewhere,
50            'D' => Self::German,
51            'F' => Self::French,
52            'I' => Self::Italian,
53            'S' => Self::Spanish,
54            _ => Self::Unknown(data),
55        }
56    }
57}
58
59/// GBA multiplay boot mode.
60#[derive(Debug)]
61pub enum MultiplayBootMode {
62    None,
63    Joybus,
64    Normal,
65    Multiplay,
66    /// Any other data that's not known. This can happen on carts that do not support multiplay as
67    /// other data may be placed in the multiplay header region.
68    Unknown(u8),
69}
70
71impl From<u8> for MultiplayBootMode {
72    fn from(data: u8) -> Self {
73        match data {
74            0 => Self::None,
75            1 => Self::Joybus,
76            2 => Self::Normal,
77            3 => Self::Multiplay,
78            _ => Self::Unknown(data),
79        }
80    }
81}
82
83impl fmt::Display for MultiplayBootMode {
84    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
85        match self {
86            Self::None => write!(fmt, "none")?,
87            Self::Joybus => write!(fmt, "joybus")?,
88            Self::Normal => write!(fmt, "normal")?,
89            Self::Multiplay => write!(fmt, "multiplay")?,
90            Self::Unknown(data) => write!(fmt, "unknown {data}")?,
91        }
92        Ok(())
93    }
94}
95
96/// Represents the metadata held by the GBA internal header.
97#[derive(Debug)]
98pub struct Metadata {
99    /// The entry point the bootrom jumps to after it finishes.
100    pub entry_point: u32,
101    /// Compressed Nintendo logo.
102    pub nintendo_logo: [u8; 156],
103    /// The title of the game, maximum of 12 uppercase ASCII characters.
104    pub title: String,
105    /// Game code, 4 uppercase ASCII characters. The first character is some sort of category,
106    /// second and third are unique game identifiers, and the fourth is a language code.
107    pub game_code: String,
108    /// Manufacturer code, 2 uppercase ASCII characters.
109    pub manufacturer_code: String,
110    /// GBA unit code. Seems to be 0x00 for all units?
111    pub main_unit_code: u8,
112    /// The type of device. Usually 0x00 for GBA, bit 7 is Debugging And Communication System
113    /// (DACS) related apparently.
114    pub device_type: u8,
115    /// Software version.
116    pub software_version: u8,
117    /// Checksum for the header. The checksum covers all bytes from the title through the software
118    /// version fields, inclusive.
119    pub header_checksum: u8,
120}
121
122/// Multiboot related metadata.
123#[derive(Debug)]
124pub struct MultiBootHeader {
125    /// Entry point if booted using Normal or Multiplay transfer mode (not Joybus mode).
126    pub ram_entry_point: u32,
127    /// Boot mode.
128    pub boot_mode: MultiplayBootMode,
129    /// The ID of the current device when booted in Normal or Multiplay modes.
130    pub slave_id_number: u8,
131    /// Entry point if booted using Joybus mode.
132    pub joybus_entry_point: u32,
133}
134
135pub trait MetadataRead {
136    /// Parses the GBA ROM metadata from the object provided, returning a Metadata object with the
137    /// header metadata.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`Error::Parse`] if the header cannot be found or if a field in the header contains
142    /// an unexpected value.
143    /// Returns [`Error::Io`] if an IO error took place while reading from the file.
144    fn read_gba_metadata(&mut self) -> Result<Metadata, Error>;
145}
146
147impl Metadata {
148    /// Returns a new Metadata instance.
149    ///
150    /// # Errors
151    ///
152    /// See [`MetadataRead::read_gba_metadata`] for possible errors.
153    pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
154        io.read_gba_metadata()
155    }
156}
157
158/// Trims null and whitespace characters (in that order) from the given string.
159fn trim(string: &str) -> &str {
160    string.trim_matches('\0').trim()
161}
162
163impl<T: Read + Seek> MetadataRead for T {
164    fn read_gba_metadata(&mut self) -> Result<Metadata, Error> {
165        self.seek(SeekFrom::Start(0))?;
166
167        let entry_point = self.read_u32::<LittleEndian>()?;
168        let mut nintendo_logo = [0u8; 156];
169        self.read_exact(&mut nintendo_logo)?;
170
171        self.seek(SeekFrom::Start(0xA0))?;
172        let mut title = [0u8; 12];
173        self.read_exact(&mut title)?;
174        let title = trim(str::from_utf8(&title)?).to_string();
175        let mut game_code = [0u8; 4];
176        self.read_exact(&mut game_code)?;
177        let game_code = trim(str::from_utf8(&game_code)?).to_string();
178        let mut manufacturer_code = [0u8; 2];
179        self.read_exact(&mut manufacturer_code)?;
180        let manufacturer_code = trim(str::from_utf8(&manufacturer_code)?).to_string();
181
182        self.seek(SeekFrom::Current(1))?;
183        let main_unit_code = self.read_u8()?;
184        let device_type = self.read_u8()?;
185        self.seek(SeekFrom::Current(7))?;
186        let software_version = self.read_u8()?;
187        let header_checksum = self.read_u8()?;
188
189        Ok(Metadata {
190            entry_point,
191            nintendo_logo,
192            title,
193            game_code,
194            manufacturer_code,
195            main_unit_code,
196            device_type,
197            software_version,
198            header_checksum,
199        })
200    }
201}
202
203impl Metadata {
204    /// Returns the header's checksum.
205    ///
206    /// # Errors
207    /// [`Error::Io`] if there are any IO errors while seeking and reading the header from the
208    /// cart's underlying IO object.
209    pub fn header_checksum<T: Read + Seek>(&mut self, io: &mut T) -> Result<u8, Error> {
210        io.seek(SeekFrom::Start(0xA0))?;
211        let mut checksum = 0u8;
212        let mut data = [0u8; 0xBC - 0xA0];
213        // It's faster to read in the entire header
214        io.read_exact(&mut data)?;
215        for byte in &data {
216            checksum = checksum.wrapping_sub(*byte);
217        }
218        checksum = checksum.wrapping_sub(0x19);
219        Ok(checksum)
220    }
221}