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

use crate::error::Error;
use crate::smdh::ApplicationTitle;
use crate::smdh::SMDH;
use crate::smdh::SMDHRead;

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

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

/// CIA container metadata.
///
/// [`Cia`] containers have the following sections in order:
///  - certificate chain
///  - Ticket
///  - TMD file data
///  - Content file data
///  - Meta (if [`CiaMetadata::meta_size`] is not 0)
///
/// Each section starts aligned to 64 bytes.
#[derive(Debug)]
pub struct CiaMetadata {
    /// 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>,
}

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

impl CiaMetadata {
    /// Returns a new instance of a [`Cia`] container.
    ///
    /// # Errors
    ///
    /// See [`CiaRead::read_cia_metadata`] for possible errors.
    pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
        io.read_cia_metadata()
    }
}

/// Readable representation of a CIA archive.
pub struct Cia<T: Read + Seek> {
    /// CIA metadata associated with this archive.
    pub cia: CiaMetadata,
    /// IO object used to read from this archive.
    io: T,
}

impl<T: Read + Seek> Cia<T> {
    /// Creates a [`Cia`] from a IO object, taking ownership of it.
    ///
    /// # Errors
    ///
    /// See [`CiaRead::read_cia_metadata`] for details.
    pub fn try_from(mut io: T) -> Result<Self, Error> {
        Ok(Self::new(io.read_cia_metadata()?, io))
    }

    /// Creates a [`Cia`] from pre-parsed metadata and an IO object, taking ownership of both.
    pub const fn new(cia: CiaMetadata, io: T) -> Self {
        Self { cia, io }
    }

    /// Consumes self and releases ownership over the [`CiaMetadata`] and IO object.
    #[must_use]
    pub fn release(self) -> (CiaMetadata, T) {
        (self.cia, self.io)
    }
}

impl<T: Read + Seek> CiaRead for T {
    fn read_cia_metadata(&mut self) -> Result<CiaMetadata, 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(CiaMetadata {
            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.cia.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.cia.archive_header_size.into())
            + round_align_64(self.cia.certificate_chain_size.into())
            + round_align_64(self.cia.ticket_size.into())
            + round_align_64(self.cia.tmd_file_size.into())
            + round_align_64(self.cia.content_size);

        self.io.seek(SeekFrom::Start(skip + 0x400))?;
        let icon_size = self.cia.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