gcn_disk 0.1.3

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: 2024 Gabriel Marcano <gabemarcano@yahoo.com>

use crate::error::Error;
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 std::vec::Vec;

pub struct Banner {
    pub data: Vec<u16>,
    pub game_name: String,
    pub developer: String,
    pub full_title: String,
    pub full_developer: String,
    pub description: String,
}

impl TryFrom<&[u8]> for Banner {
    type Error = Error;

    fn try_from(binary: &[u8]) -> Result<Self, Self::Error> {
        if binary.len() < 6496 {
            return Err(Error::Parse(format!(
                "wrong size for a banner: {}",
                binary.len()
            )));
        }

        let data = binary[0x20..0x1820]
            .chunks_exact(2)
            .map(|a| u16::from_be_bytes([a[0], a[1]]))
            .collect();
        let game_name = trim(&from_latin1_or_shift_jis(&binary[0x1820..0x1840])?).to_string();
        let developer = trim(&from_latin1_or_shift_jis(&binary[0x1840..0x1860])?).to_string();
        let full_title = trim(&from_latin1_or_shift_jis(&binary[0x1860..0x18A0])?).to_string();
        let full_developer = trim(&from_latin1_or_shift_jis(&binary[0x18A0..0x18e0])?).to_string();
        let description = trim(&from_latin1_or_shift_jis(&binary[0x18E0..0x1960])?).to_string();

        Ok(Self {
            data,
            game_name,
            developer,
            full_title,
            full_developer,
            description,
        })
    }
}

/// No truncation cam happen because of the masking and scaling that is going on.
#[allow(clippy::cast_possible_truncation)]
const fn scale_to_bits<const BITS: usize>(data: u16) -> u8 {
    const { assert!(BITS <= 16) };
    // Use u32 to make sure if BITS == 16 we don't trigger an error, and it's safe to truncate to
    // 16 bits because for all valid BITS, mask will always fit in a u16
    let mask: u16 = ((1u32 << BITS) - 1) as u16;
    let t_mask: u16 = u8::MAX as u16;
    (((data & mask) * t_mask / mask) & t_mask) as u8
}

impl Banner {
    /// Returns a new instance of a Gamecube banner.
    ///
    /// # Errors
    /// [`Error::Io`] if there's an error while reading or seeking from the IO object,
    /// [`Error::Utf8`] if there are strings that are not valid UTF8 (FIXME is this a valid
    /// complaint for GCN strings?)
    pub fn new<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
        io.seek(SeekFrom::Start(0x20))?;
        let mut data = vec![0u8; 0x1800];
        io.read_exact(&mut data)?;
        let data = data
            .chunks_exact(2)
            .map(|a| u16::from_be_bytes([a[0], a[1]]))
            .collect();
        let mut tmp = [0u8; 0x20];
        io.read_exact(&mut tmp)?;
        let game_name = trim(&from_latin1_or_shift_jis(&tmp)?).to_string();
        io.read_exact(&mut tmp)?;
        let developer = trim(&from_latin1_or_shift_jis(&tmp)?).to_string();
        let mut tmp = [0u8; 0x40];
        io.read_exact(&mut tmp)?;
        let full_title = trim(&from_latin1_or_shift_jis(&tmp)?).to_string();
        io.read_exact(&mut tmp)?;
        let full_developer = trim(&from_latin1_or_shift_jis(&tmp)?).to_string();
        let mut tmp = [0u8; 0x80];
        io.read_exact(&mut tmp)?;
        let description = trim(&from_latin1_or_shift_jis(&tmp)?).to_string();

        Ok(Self {
            data,
            game_name,
            developer,
            full_title,
            full_developer,
            description,
        })
    }

    #[must_use]
    pub fn extract_rgba_banner_image(&self) -> Vec<u8> {
        let width = 96;
        let height = 32;
        let tile_w = 4;
        let tile_h = 4;
        let mut index = 0;
        let mut output = vec![0u8; width * height * 4];
        // 4x4 tiles
        for y in (0..height).step_by(tile_h) {
            for x in (0..width).step_by(tile_w) {
                // Iterate through each pixel in the tile
                for ty in 0..tile_h {
                    for tx in 0..tile_w {
                        // Each pixel is in RGBA5A3 format
                        // https://wiki.tockdom.com/wiki/Image_Formats#RGB5A3
                        let pixel: u16 = self.data[index];
                        index += 1;
                        let (a, r, g, b) = if pixel >> 15 == 0 {
                            (
                                scale_to_bits::<3>(pixel >> 12),
                                scale_to_bits::<4>(pixel >> 8),
                                scale_to_bits::<4>(pixel >> 4),
                                scale_to_bits::<4>(pixel),
                            )
                        } else {
                            (
                                0xFF,
                                scale_to_bits::<5>(pixel >> 10),
                                scale_to_bits::<5>(pixel >> 5),
                                scale_to_bits::<5>(pixel),
                            )
                        };

                        output[4 * ((y + ty) * 96 + (x + tx))] = r;
                        output[4 * ((y + ty) * 96 + (x + tx)) + 1] = g;
                        output[4 * ((y + ty) * 96 + (x + tx)) + 2] = b;
                        output[4 * ((y + ty) * 96 + (x + tx)) + 3] = a;
                    }
                }
            }
        }
        output
    }
}