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::error::Error;
use crate::smdh::ApplicationTitle;
use crate::smdh::SMDHRead;
use crate::smdh::SMDH;

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

use byteorder::LittleEndian;
use byteorder::ReadBytesExt;

#[derive(Debug)]
pub struct CIAHeader {
    /// The size of the header (usually 0x2020 bytes).
    pub archive_header_size: u32,
    /// The type of content (?).
    pub type_: u16,
    /// The version of CIA.
    pub version: u16,
    /// The size of the certificate chain in bytes.
    pub certificate_chain_size: u32,
    /// The ticket size.
    pub ticket_size: u32,
    /// Tile metadata (TMD) file size in bytes.
    pub tmd_file_size: u32,
    /// Meta size in bytes. 0 if no Meta is present.
    pub meta_size: u32,
    /// The size of the Content.
    pub content_size: u64,
    /// The content index (every byte corresponds to some Content).
    pub content_index: Vec<u8>,
}

/// [`CIA`] container.
///
/// [`CIA`] containers have the following sections in order:
///  - certificate chain
///  - Ticket
///  - TMD file data
///  - Content file data
///  - Meta (if [`CIAHeader::meta_size`] is not 0)
///
/// Each section starts aligned to 64 bytes.
pub struct CIA<T: Read + Seek> {
    /// CIA header metadata.
    pub header: CIAHeader,
    io: T,
}

pub trait CIARead {
    /// Parses the 3DS ROM [`CIA`] metadata from the object provided, returning a [`CIAHeader`]
    /// 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_cia_header(&mut self) -> Result<CIAHeader, Error>;
}

impl<T: Read + Seek> CIA<T> {
    /// Returns a new instance of a [`CIA`] container.
    ///
    /// # Errors
    /// See [`CIARead::read_cia_header`] for possible errors.
    pub fn new(mut io: T) -> Result<Self, Error> {
        let header = io.read_cia_header()?;
        Ok(Self { header, io })
    }
}

impl<T: Read + Seek> CIARead for T {
    fn read_cia_header(&mut self) -> Result<CIAHeader, Error> {
        self.seek(SeekFrom::Start(0))?;

        let archive_header_size = self.read_u32::<LittleEndian>()?;
        let type_ = self.read_u16::<LittleEndian>()?;
        let version = self.read_u16::<LittleEndian>()?;
        let certificate_chain_size = self.read_u32::<LittleEndian>()?;
        let ticket_size = self.read_u32::<LittleEndian>()?;
        let tmd_file_size = self.read_u32::<LittleEndian>()?;
        let meta_size = self.read_u32::<LittleEndian>()?;
        let content_size = self.read_u64::<LittleEndian>()?;
        let content_index = Vec::<u8>::default();

        Ok(CIAHeader {
            archive_header_size,
            type_,
            version,
            certificate_chain_size,
            ticket_size,
            tmd_file_size,
            meta_size,
            content_size,
            content_index,
        })
    }
}

const fn round_align_64(size: u64) -> u64 {
    if size.trailing_zeros() >= 6 {
        size
    } else {
        (size + 0x40) & !0x3f
    }
}

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 CIA<T> {
    fn read_smdh(&mut self) -> Result<Option<SMDH>, Error> {
        if self.header.meta_size == 0 {
            return Ok(None);
        }
        // Find actual size of all sections, they need to be rounded up to next 64 byte boundary
        let skip = round_align_64(self.header.archive_header_size.into())
            + round_align_64(self.header.certificate_chain_size.into())
            + round_align_64(self.header.ticket_size.into())
            + round_align_64(self.header.tmd_file_size.into())
            + round_align_64(self.header.content_size);

        self.io.seek(SeekFrom::Start(skip + 0x400))?;
        let icon_size = self.header.meta_size - 0x400;
        let mut icon_data = vec![0u8; usize::try_from(icon_size).unwrap()];
        self.io.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,
        }))
    }
}

// FIXME need to implement 3DS unit tests