n64_cart 0.4.0

N64 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::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::str;

use byteorder::BigEndian;
use byteorder::ReadBytesExt;

use ipl3checksum;

/// Represents the metadata held by the N64 internal header.
#[derive(Debug)]
pub struct Metadata {
    /// Configuration flags used by IPL2 to configure access to the ROM (which is mapped to PI DOM1
    /// memory space).
    pub pi_dom1_configuration_flags: u32,
    /// Constant value used by `<=libultra-2.0I` to naively compute the amount of time passed.
    ///
    /// Roughly: `time = CPU_Count_register / ((0xFFFFFFF0 & clock_rate) * 0.75)`.
    ///
    /// If the masked result `0xFFFFFFF0 & clock_rate` is equal to zero, apparently libultra
    /// defaults that value to `0x03B9ACA0`, or `62,500,000`, before the `0.75` multiplication.
    pub clock_rate: u32,
    /// The location for IPL3 to copy ROM memory to and jump there.
    pub boot_address: u32,
    /// The version of libultra used to build this ROM.
    /// Consists of a `[major].[minor][revision]`, where `major` and `minor` are digits, and
    /// `revision` is a single ASCII letter.
    pub libultra_version: String,
    /// Used for checking the integrity of the ROM by IPL3.
    pub check_code: u64,
    /// Reserved. Most games have all 0, but some have some unknown data here.
    pub reserved_maybe: u64,
    /// The game title. It's either ASCII or JIS X 0201. Padded with spaces.
    pub game_title: [u8; 0x14],
    /// Reserved on official cartriges, but used by homebrew to encode information about the types
    /// of controllers the games expect, game ID, and the type of save in the cartridge.
    pub reserved2: [u8; 7],
    /// 4 byte game code. First byte is a category code, middle two are a unique identifier, and
    /// the last is a region code.
    pub game_code: String,
    /// The version of the ROM.
    pub rom_version: u8,
    /// IPL3 boot code, which is included in ROM.
    pub ipl3: [u8; 0xFC0],
}

impl Metadata {
    /// Returns a new Metadata object, after parsing the header.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Io`] if an IO error took place while reading from the file.
    pub fn from_io<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
        io.read_n64_metadata()
    }
}

pub trait MetadataRead {
    /// Parses the N64 file metadata from the object provided, returning a `Metadata` object with the
    /// N64 file 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_n64_metadata(&mut self) -> Result<Metadata, Error>;
}

impl<T: Read + Seek> MetadataRead for T {
    fn read_n64_metadata(&mut self) -> Result<Metadata, Error> {
        self.seek(SeekFrom::Start(0))?;
        let pi_dom1_configuration_flags = self.read_u32::<BigEndian>()?;
        let clock_rate = self.read_u32::<BigEndian>()?;
        let boot_address = self.read_u32::<BigEndian>()?;
        let libultra_version = self.read_u32::<BigEndian>()?;
        let major_minor = ((libultra_version >> 8) & 0xFF) as u8;
        let major = major_minor / 10;
        let minor = major_minor % 10;
        let revision = str::from_utf8(&[(libultra_version & 0xFF) as u8])?.to_string();
        let libultra_version = format!("{major}.{minor}{revision}");
        let check_code = self.read_u64::<BigEndian>()?;
        let reserved_maybe = self.read_u64::<BigEndian>()?;
        let mut game_title = [0u8; 0x14];
        self.read_exact(&mut game_title)?;
        // FIXME how to check if JIS X 0201?

        let mut reserved2 = [0u8; 7];
        self.read_exact(&mut reserved2)?;
        let mut game_code = [0u8; 4];
        self.read_exact(&mut game_code)?;
        if !game_code.is_ascii() {
            return Err(Error::Parse("game code is not valid ASCII".into()));
        }
        let game_code = str::from_utf8(&game_code)?.to_string();
        let rom_version = self.read_u8()?;
        let mut ipl3 = [0u8; 0xFC0];
        self.read_exact(&mut ipl3)?;

        Ok(Metadata {
            pi_dom1_configuration_flags,
            clock_rate,
            boot_address,
            libultra_version,
            check_code,
            reserved_maybe,
            game_title,
            reserved2,
            game_code,
            rom_version,
            ipl3,
        })
    }
}

impl Metadata {
    /// Computes the checksum for the ROM, as found in the [`Metadata::check_code`] field.
    ///
    /// # Errors
    /// Returns [`Error::Io`] if there are any errors reading from the underlying IO object, and
    /// [`Error::Parse`] if it wasn't possible to determine the checksum (usually due to the IPL3
    /// portion of the ROM being scrambled).
    pub fn compute_checksum<T: Read + Seek>(&mut self, io: &mut T) -> Result<u64, Error> {
        let mut data = vec![0u8; 0x10_1000];
        io.seek(SeekFrom::Start(0))?;
        io.read_exact(&mut data)?;
        match ipl3checksum::calculate_checksum_autodetect(&data) {
            Ok((a1, a2)) => Ok(u64::from(a1) << 32 | u64::from(a2)),
            Err(_) => Err(Error::Parse("unable to compute ROM checksum".into())),
        }
    }
}