ctr_cart 0.1.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::ncsd::NCCHPartitionEntry;

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

#[derive(Debug)]
pub struct ExeFsFileHeader {
    pub filename: String,
    pub offset: u32,
    pub size: u32,
}

#[derive(Debug)]
pub struct ExeFs {
    pub files: Vec<ExeFsFileHeader>,
    pub file_hashes: Vec<[u8; 32]>,
    /// FIXME should I subtype this? It's not really an [`NCCHPartitionEntry`], but just a more general
    /// Partition Entry
    pub partition_entry: NCCHPartitionEntry,
}

pub trait ExeFsRead {
    /// 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_exefs_header(&mut self) -> Result<Option<ExeFs>, Error>;
}

/// Trims null and whitespace characters (in that order) from the given string.
fn trim(string: &str) -> &str {
    string.trim_matches('\0').trim()
}

impl<T: Read + Seek> ExeFsRead for Cart<T> {
    fn read_exefs_header(&mut self) -> Result<Option<ExeFs>, 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_location = (ncch.exefs_offset + ncch.partition_entry.offset) * 0x200;
        let partition_entry = NCCHPartitionEntry {
            offset: exefs_location,
            size: ncch.exefs_size * 0x200,
        };
        self.seek(SeekFrom::Start(u64::from(exefs_location)))?;
        let mut exefs_header = [0u8; 0x200];
        self.read_exact(&mut exefs_header)?;
        let mut files = vec![];
        let mut file_hashes: Vec<[u8; 32]> = vec![];
        for i in 0..10 {
            if exefs_header[i * 16] == 0 {
                continue;
            }
            let filename = trim(str::from_utf8(&exefs_header[i * 16..(i * 16 + 8)])?).to_string();
            let offset =
                u32::from_le_bytes(exefs_header[i * 16 + 8..i * 16 + 12].try_into().unwrap());
            let size =
                u32::from_le_bytes(exefs_header[i * 16 + 12..i * 16 + 16].try_into().unwrap());
            files.push(ExeFsFileHeader {
                filename,
                offset,
                size,
            });

            file_hashes.push(
                exefs_header[(9 - i) * 32 + 0xC0..(9 - i) * 32 + 0xC0 + 32]
                    .try_into()
                    .unwrap(),
            );
        }
        Ok(Some(ExeFs {
            files,
            file_hashes,
            partition_entry,
        }))
    }
}

/*
#[allow(clippy::cast_possible_truncation)]
const fn scale_5bits_to_8bits(data: u16) -> u8 {
    ((data & 0x1F) * 0xFF / 0x1F) as u8
}

struct RGBAPixel(u8, u8, u8, u8);

impl<T: Read + Seek> Cart<T> {
    /// Computes the checksum of the 3DS header.
    ///
    /// # Errors
    /// Returns [`Error::Io`] if there are any issues reading or seeking the underlying data.
    pub fn header_checksum(&mut self) -> Result<u16, Error> {
        let crc = Crc::<u16>::new(&crc::CRC_16_MODBUS);
        self.io.seek(SeekFrom::Start(0))?;
        //let mut checksum = 0u8;
        let mut data = [0u8; 0x15E];
        // It's faster to read in the entire header
        self.io.read_exact(&mut data)?;
        Ok(crc.checksum(&data))
    }

    /// Returns the 3DS icon a 32x32 RGBA8888 image.
    ///
    /// # Errors
    /// Returns [`Error::Io`] if there are any issues reading or seeking the underlying data.
    pub fn extract_rgba_icon(&mut self) -> Result<Vec<u8>, Error> {
        let width = 32;
        let height = 32;
        let tile_w = 8;
        let tile_h = 8;
        let mut index = 0;
        let mut output = vec![0u8; width * height * 4];
        let exefs_offset =
            (self.header.ncch[0].1.exefs_offset + self.header.ncsd.partition_table[0].offset) * 0x200;
        self.io.seek(SeekFrom::Start(exefs_offset.into()))?;
        let mut exefs_header = [0u8; 0x200];
        self.io.read_exact(&mut exefs_header);
        let mut cipher = self.header.ncch[0].1.exefs_ctr();
        cipher.apply_keystream(&mut exefs_header);
        println!("{exefs_header:02X?}");
        /*
        let icon_offset = self.io.read_u32::<LittleEndian>()?;
        self.io
            .seek(SeekFrom::Start(u64::from(icon_offset + 0x20)))?;
        let mut bitmap = vec![0u8; 0x200];
        self.io.read_exact(&mut bitmap)?;
        let mut palette = vec![0u16; 0x10];
        self.io.read_u16_into::<LittleEndian>(&mut palette)?;

        // 8x8 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 {
                        let pixel: u8 = (bitmap[index] >> (4 * (tx % 2))) & 0xF;
                        if (tx % 2) == 1 {
                            index += 1;
                        }
                        let pixel = if pixel == 0 {
                            RGBAPixel(0xFF, 0xFF, 0xFF, 0)
                        } else {
                            let color16 = palette[usize::from(pixel)];
                            RGBAPixel(
                                scale_5bits_to_8bits(color16),
                                scale_5bits_to_8bits(color16 >> 5),
                                scale_5bits_to_8bits(color16 >> 10),
                                0xFF,
                            )
                        };

                        output[4 * ((y + ty) * 32 + (x + tx))] = pixel.0;
                        output[4 * ((y + ty) * 32 + (x + tx)) + 1] = pixel.1;
                        output[4 * ((y + ty) * 32 + (x + tx)) + 2] = pixel.2;
                        output[4 * ((y + ty) * 32 + (x + tx)) + 3] = pixel.3;
                    }
                }
            }
        }*/
        Ok(vec![])
    }
}*/

// FIXME need to implement 3DS unit tests