gcn_disk 0.5.0

Gamecube 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 crate::fst::Fst;
use crate::fst::FstRead;
use crate::utils::from_latin1_or_shift_jis;
use crate::utils::trim;

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

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

/// Represents the metadata held by the GCN disk internal header.
#[derive(Debug)]
pub struct Header {
    /// Console ID. The first character of the game code field on disk.
    pub console_id: String,
    /// Unique game code. The second and third characters of the game code field.
    pub game_code: String,
    /// Country code. The last character of the game code field.
    pub country_code: String,
    /// Make code.
    pub maker_code: String,
    /// The ID of the disk.
    pub disk_id: u8,
    /// The version of the disk.
    pub version: u8,
    /// Whether or not audio streaming is enabled.
    pub audio_streaming: u8,
    /// The size of the audio stream buffer. Apparently 0 means 10?
    pub stream_buffer_size: u8,
    /// GCN magic word.
    pub magic: u32,
    /// The name of the game. On disk it is padded with null characters.
    pub game_name: String,

    pub debug_monitor_offset: u32,
    pub debug_monitor_address: u32,

    /// The offset to the main executable DOL (bootfile)
    pub executable_offset: u32,
    /// Offset to the filesystem table (often referred to as fst.bin).
    pub fst_offset: u32,
    /// The size of the FST.
    pub fst_size: u32,
    /// The maximum size of the FST (may be more than [`Header::fst_size`] for multidisks?).
    pub fst_max_size: u32,

    pub user_position: u32,
    pub user_length: u32,
}

/// Represents a GCN disk.
pub struct Metadata {
    pub header: Header,
    pub filesystem: Fst,
}

pub trait HeaderRead {
    /// Parses the GCN ISO metadata from the object provided, returning a [`Header`] object with
    /// the GCN ISO 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_gcn_header(&mut self) -> Result<Header, Error>;
}

impl Metadata {
    /// Returns a new Metadata instance.
    ///
    /// # Errors
    ///
    /// See [`HeaderRead::read_gcn_header`] and [`FstRead::read_fst`] for details on possible
    /// errors.
    pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
        let header = io.read_gcn_header()?;
        let filesystem = io.read_fst()?;
        Ok(Self { header, filesystem })
    }
}

impl<T: Read + Seek> HeaderRead for T {
    fn read_gcn_header(&mut self) -> Result<Header, Error> {
        // Check file header, and 2 magic bytes
        self.seek(SeekFrom::Start(0))?;
        let mut game_code = [0u8; 4];
        self.read_exact(&mut game_code)?;
        let game_code = from_latin1_or_shift_jis(&game_code)?;
        let maker_code = self.read_u16::<BigEndian>()?.to_be_bytes();
        let maker_code = from_latin1_or_shift_jis(&maker_code)?;
        let disk_id = self.read_u8()?;
        let version = self.read_u8()?;
        let audio_streaming = self.read_u8()?;
        let stream_buffer_size = self.read_u8()?;
        self.seek(SeekFrom::Current(0x12))?;
        let magic = self.read_u32::<BigEndian>()?;
        let mut game_name = [0u8; 0x3E0];
        self.read_exact(&mut game_name)?;
        let game_name = trim(&from_latin1_or_shift_jis(&game_name)?).to_string();

        if game_code.len() != 4 {
            return Err(Error::Parse("bad game code string, wrong size".into()));
        }

        if !game_code.is_ascii() {
            return Err(Error::Parse("bad game code string, not ASCII".into()));
        }

        let debug_monitor_offset = self.read_u32::<BigEndian>()?;
        let debug_monitor_address = self.read_u32::<BigEndian>()?;
        self.seek(SeekFrom::Current(0x18))?;
        let executable_offset = self.read_u32::<BigEndian>()?;
        let fst_offset = self.read_u32::<BigEndian>()?;
        let fst_size = self.read_u32::<BigEndian>()?;
        let fst_max_size = self.read_u32::<BigEndian>()?;
        let user_position = self.read_u32::<BigEndian>()?;
        let user_length = self.read_u32::<BigEndian>()?;

        Ok(Header {
            console_id: game_code[0..1].to_string(),
            game_code: game_code[1..3].to_string(),
            country_code: game_code[3..4].to_string(),
            maker_code,
            disk_id,
            version,
            audio_streaming,
            stream_buffer_size,
            magic,
            game_name,
            debug_monitor_offset,
            debug_monitor_address,
            executable_offset,
            fst_offset,
            fst_size,
            fst_max_size,
            user_position,
            user_length,
        })
    }
}