ctr_cart 0.2.0

3DS 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::cart::Cart;
use crate::error::Error;
use crate::exefs::ExeFsRead;

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

#[derive(Debug)]
pub struct ApplicationTitle {
    pub short_description: String,
    pub long_description: String,
    pub publisher: String,
}

#[derive(Debug)]
pub struct SMDH {
    pub magic: String,
    pub version: u16,
    pub titles: [ApplicationTitle; 16],
    pub application_settings: [u8; 0x30], // FIXME
    pub icons: [u16; 0x1680 / 2],
}

pub trait SMDHRead {
    /// Parses the 3DS ROM metadata from the object provided, returning a Header 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_smdh(&mut self) -> Result<Option<SMDH>, Error>;
}

fn trim(string: &str) -> &str {
    string.trim_matches('\0').trim()
}

fn from_utf16(data: &[u8]) -> Result<String, Error> {
    let data: Vec<u16> = data
        .chunks(2)
        .map(|e| u16::from_le_bytes(e.try_into().unwrap()))
        .collect();
    String::from_utf16(&data).map_err(|_| Error::Parse("could not parse utf16 string".into()))
}

impl<T: Read + Seek> SMDHRead for Cart<T> {
    fn read_smdh(&mut self) -> Result<Option<SMDH>, Error> {
        let ncch = self.ncsd.find_exefs_ncch();
        if ncch.is_none() {
            return Ok(None);
        }
        let ncch = ncch.unwrap();
        if ncch.exefs_size == 0 {
            return Ok(None);
        }
        let exefs_header = self.read_exefs_header()?.unwrap();
        let icon_file = exefs_header
            .files
            .iter()
            .find(|&file| file.filename == "icon");
        if icon_file.is_none() {
            return Ok(None);
        }
        let icon_file = icon_file.unwrap();

        // Need to borrow header again, no need to cehck if this doesn't exist
        let ncch = self.ncsd.find_exefs_ncch().unwrap();
        let offset =
            (ncch.exefs_offset + ncch.partition_entry.offset + 1) * 0x200 + icon_file.offset;
        self.seek(SeekFrom::Start(offset.into()))?;
        let mut icon_data = vec![0u8; usize::try_from(icon_file.size).unwrap()];
        self.read_exact(&mut icon_data)?;

        let magic = str::from_utf8(&icon_data[0..4])?.to_string();
        let version = u16::from_le_bytes(icon_data[4..6].try_into().unwrap());
        let mut titles = Vec::<ApplicationTitle>::default();
        for i in 0..16 {
            titles.push(ApplicationTitle {
                short_description: trim(&from_utf16(
                    &icon_data[0x8 + 0x200 * i..0x8 + 0x200 * i + 0x80],
                )?)
                .to_string(),
                long_description: trim(&from_utf16(
                    &icon_data[0x8 + 0x200 * i + 0x80..0x8 + 0x200 * i + 0x180],
                )?)
                .to_string(),
                publisher: trim(&from_utf16(
                    &icon_data[0x8 + 0x200 * i + 0x180..0x8 + 0x200 * i + 0x200],
                )?)
                .to_string(),
            });
        }
        let application_settings: [u8; 0x30] = icon_data[0x2008..0x2008 + 0x30].try_into().unwrap();
        let icons: [u16; 0x1680 / 2] = icon_data[0x2040..0x2040 + 0x1680]
            .chunks_exact(2)
            .map(|e| u16::from_le_bytes([e[0], e[1]]))
            .collect::<Vec<_>>()
            .try_into()
            .unwrap();

        Ok(Some(SMDH {
            magic,
            version,
            titles: titles.try_into().unwrap(),
            application_settings,
            icons,
        }))
    }
}

fn place_2x2(data: &[u16], output: &mut [u8]) {
    for y in 0..2 {
        for x in 0..2 {
            let pixel = data[x + y * 2];
            let r = ((pixel & 0x1F) * 255 / 0x1F) & 0xFF;
            let g = (((pixel >> 5) & 0x3F) * 255 / 0x3F) & 0xFF;
            let b = (((pixel >> 11) & 0x1F) * 255 / 0x1F) & 0xFF;
            output[(x + y * 48) * 4] = b as u8;
            output[(x + y * 48) * 4 + 1] = g as u8;
            output[(x + y * 48) * 4 + 2] = r as u8;
            output[(x + y * 48) * 4 + 3] = 0xFF;
        }
    }
}

fn place_4x4(data: &[u16], output: &mut [u8]) {
    let mut counter = 0;
    for y in 0..2 {
        for x in 0..2 {
            let pos = (x + y * 48) * 2 * 4;
            place_2x2(&data[counter..], &mut output[pos..]);
            counter += 4;
        }
    }
}

fn place_8x8(data: &[u16], output: &mut [u8]) {
    let mut counter = 0;
    for y in 0..2 {
        for x in 0..2 {
            let pos = (x + y * 48) * 4 * 4;
            place_4x4(&data[counter..], &mut output[pos..]);
            counter += 16;
        }
    }
}

impl SMDH {
    #[must_use]
    pub fn extract_rgba_icon(&self) -> [u8; 9216] {
        let mut output = [0u8; 9216];
        let data = &self.icons[0x480 / 2..];
        // counter tracks the location of data that has yet to be processed from the data slice
        let mut counter = 0;
        for y in (0..48).step_by(8) {
            for x in (0..48).step_by(8) {
                // Position is the location of the top left of the current tile
                // In this case we have 4 bytes per pixel
                let position = (x + y * 48) * 4;
                place_8x8(&data[counter..], &mut output[position..]);
                counter += 64;
            }
        }
        output
    }
}

// FIXME need to implement 3DS unit tests