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::card_info::CardInfo;
use crate::card_info::CardInfoRead;
use crate::crypto::AESCtr;
use crate::error::Error;
use crate::ncch::NCCH;
use crate::ncsd::NCSD;
use crate::ncsd::NCSDRead;

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

use ctr::cipher::StreamCipher;
use ctr::cipher::StreamCipherSeek;

/// Represents a 3DS cartridge.
pub struct Cart<T: Read + Seek> {
    /// The main [`NCSD`] header for the cartridge.
    pub ncsd: NCSD,
    /// Information about the cartridge.
    pub card_info: CardInfo,
    /// Manages AES CTR block mode decryption to read `ExeFS` info partitions.
    exefs_ctr: Option<AESCtr>,
    /// IO object to read data from.
    io: T,
}

impl<T: Read + Seek> Cart<T> {
    /// Returns a new Cart instance, using the provided Readable and Seekable object as the
    /// underlying data source representing the cart.
    ///
    /// # Errors
    ///
    /// See [`HeaderRead::read_3ds_header`] for error deetails.
    pub fn new(mut io: T) -> Result<Self, Error> {
        let ncsd = io.read_ncsd()?;
        let card_info = io.read_card_info()?;
        let exefs_ctr = ncsd.find_exefs_ncch().map(NCCH::exefs_ctr);
        Ok(Self {
            ncsd,
            card_info,
            exefs_ctr,
            io,
        })
    }
}

const fn is_overlap(region1: (u32, u32), region2: (u32, u32)) -> bool {
    (region1.0 <= region2.1) && (region1.1 >= region2.0)
}

impl<T: Read + Seek> Read for Cart<T> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let cur = self.io.stream_position()?;
        let cur = u32::try_from(cur).map_err(|_| io::Error::from(io::ErrorKind::Other))?;
        // FIXME I don't know how I want to handle this. In reality, for the next... what, 20
        // years? We won't ever have a case where usize won't unwrap into a u64

        let end = cur
            + u32::try_from(buf.len()).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
        let result = self.io.read(buf)?;

        // Handle ExeFS descryption. However, right now we're only using the info key so we can
        // read the icon from ExeFS
        // FIXME: what if nocrypto flag?
        let exefs_ncch = self.ncsd.find_exefs_ncch();
        if let Some(exefs_ncch) = exefs_ncch {
            let exefs_offset =
                (exefs_ncch.partition_entry.offset + exefs_ncch.exefs_offset) * 0x200;
            let exefs_region = (exefs_offset, exefs_offset + exefs_ncch.exefs_size * 0x200);
            if is_overlap(exefs_region, (cur, end)) {
                // Figure out the overlap, then decrypt that
                let start = cmp::max(exefs_region.0, cur);
                let end = cmp::min(exefs_region.1, end);
                // no risk of panic, since this wouldn't be None if there's an ExeFS section
                self.exefs_ctr
                    .as_mut()
                    .unwrap()
                    .seek(start - exefs_region.0);
                // FIXME not a fan of unwrap, but I'm so tired of dealing with usize
                let start = usize::try_from(start).unwrap();
                let end = usize::try_from(end).unwrap();
                let cur = usize::try_from(cur).unwrap();
                let start = start - cur;
                let end = end - cur;
                // no risk of panic, since this wouldn't be None if there's an ExeFS section
                self.exefs_ctr
                    .as_mut()
                    .unwrap()
                    .apply_keystream(&mut buf[start..end]);
            }
        }
        Ok(result)
    }
}

impl<T: Read + Seek> Seek for Cart<T> {
    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
        self.io.seek(pos)
    }
}