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::ncch::NCCHRead;
use crate::ncch::NCCH;
use crate::ncsd::NCCHPartitionEntry;

use std::fmt;
use std::fmt::Display;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;

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

#[derive(Debug)]
pub struct InitialData {
    /// Seed, consisting of the title ID in little endian, followed by all zeros.
    /// This is the Key Y used to decrypt the title key. Key X is keyslot 0x3B on retail carts, or
    /// all zeros for dev carts
    pub seed: [u8; 0x10],
    /// Title Key (AES-CCM encrypted)
    pub title_key: [u8; 0x10],
    /// The AES-CCM MAC for the encrypted title key.
    pub aes_ccm_mac: [u8; 0x10],
    /// The AES-CCM nonce for the encrypted title key.
    pub aes_ccm_nonce: [u8; 0xC],
    /// First NCCH header in the cartridge, without the RSA signature.
    pub ncch: NCCH,
}

#[derive(Debug)]
pub struct Version {
    major: u8,
    minor: u8,
    patch: u8,
}

impl From<u16> for Version {
    fn from(data: u16) -> Self {
        Self {
            major: (data >> 10) as u8,
            minor: ((data >> 4) & 0x3F) as u8,
            patch: (data & 0xF) as u8,
        }
    }
}

impl Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

#[derive(Debug)]
pub struct CardInfo {
    /// Writeable address in game flash, in media units.
    pub writeable_address: u32,
    pub flags: u32,
    /// The actual size of the 3DS cartridge in bytes.
    pub filled_size: u32,
    pub title_version: Version,
    pub card_revision: u16,
    /// Title ID of the `CVer` in the cart's update partition.
    pub cver_title_id: u64,
    /// Version number of the `CVer` in the cart's update partition.
    pub cver_version: Version,
    /// Initial data, sent by the cartridge with command 0x82.
    pub initial_data: InitialData,
}

pub trait CardInfoRead {
    /// Parses the 3DS ROM Cart Info metadata from the object provided, returning a NCSD 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_card_info(&mut self) -> Result<CardInfo, Error>;
}

impl<T: Read + Seek> CardInfoRead for T {
    fn read_card_info(&mut self) -> Result<CardInfo, Error> {
        self.seek(SeekFrom::Start(0x200))?;
        let writeable_address = self.read_u32::<LittleEndian>()?;
        let flags = self.read_u32::<LittleEndian>()?;
        self.seek(SeekFrom::Start(0x300))?;
        let filled_size = self.read_u32::<LittleEndian>()?;
        self.seek(SeekFrom::Start(0x310))?;
        let title_version = self.read_u16::<LittleEndian>()?.into();
        let card_revision = self.read_u16::<LittleEndian>()?;
        self.seek(SeekFrom::Start(0x320))?;
        let cver_title_id = self.read_u64::<LittleEndian>()?;
        let cver_version = self.read_u16::<LittleEndian>()?.into();

        // now read InitialData
        self.seek(SeekFrom::Start(0x1000))?;
        let mut seed = [0u8; 0x10];
        self.read_exact(&mut seed)?;
        let mut title_key = [0u8; 0x10];
        self.read_exact(&mut title_key)?;
        let mut aes_ccm_mac = [0u8; 0x10];
        self.read_exact(&mut aes_ccm_mac)?;
        let mut aes_ccm_nonce = [0u8; 0xC];
        self.read_exact(&mut aes_ccm_nonce)?;
        // We're abusing the fact that we already have NCCHRead, so read 0x100 garbage data as the
        // "signature"
        let partition = NCCHPartitionEntry {
            offset: (0x1100 - 0x100) / 0x200,
            size: 0x100, // BS size, just needs to be > 0
        };
        let ncch = self.read_ncch(partition)?;
        if ncch.is_none() {
            return Err(Error::Parse(
                "failed to parse NCCH in card info header".into(),
            ));
        }
        let mut ncch = ncch.unwrap();
        ncch.signature.fill(0);

        Ok(CardInfo {
            writeable_address,
            flags,
            filled_size,
            title_version,
            card_revision,
            cver_title_id,
            cver_version,
            initial_data: InitialData {
                seed,
                title_key,
                aes_ccm_mac,
                aes_ccm_nonce,
                ncch,
            },
        })
    }
}