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 ctr_cart::Cart;
use ctr_cart::SMDHRead;
use ctr_cart::CIA;

use std::fs::File;
use std::io::BufWriter;
use std::io::Read;
use std::io::Seek;
use std::path::Path;
use std::path::PathBuf;
use std::str;

use clap::Parser;

use png::BitDepth;
use png::ColorType;
use png::Encoder;

#[derive(Parser)]
struct Cli {
    #[arg(short, long)]
    fetch_icon: bool,
    files: Vec<PathBuf>,
}

fn process_cia<T: Read + Seek>(
    path: &Path,
    i: usize,
    max: usize,
    fetch_icon: bool,
    mut cia: CIA<T>,
) {
    let header = &cia.header;

    println!("\t{{");
    println!("\t\t\"path\": \"{}\",", path.to_string_lossy());
    println!(
        "\t\t\"archive header size\": {},",
        header.archive_header_size
    );
    println!("\t\t\"type\": \"{}\",", header.type_);
    println!("\t\t\"version\": {},", header.version);
    println!(
        "\t\t\"certificate chain size\": {},",
        header.certificate_chain_size
    );
    println!("\t\t\"ticket size\": {},", header.ticket_size);
    println!("\t\t\"TMD file size\": {},", header.tmd_file_size);
    println!("\t\t\"meta size\": {},", header.meta_size);
    println!("\t\t\"content size\": {},", header.content_size);
    if i == (max - 1) {
        println!("\t}}");
    } else {
        println!("\t}},");
    }

    if fetch_icon {
        let smdh = cia.read_smdh().unwrap();
        if smdh.is_none() {
            return;
        }
        let smdh = smdh.unwrap();
        let icon = smdh.extract_rgba_icon();
        let mut path = String::default();
        path.push_str(&smdh.titles[1].short_description);
        path.push_str(".png");
        let f = File::create(path).unwrap();
        let w = BufWriter::new(f);
        let mut encoder = Encoder::new(w, 48, 48);
        encoder.set_color(ColorType::Rgba);
        encoder.set_depth(BitDepth::Eight);
        let mut writer = encoder.write_header().unwrap();
        writer.write_image_data(&icon).unwrap();
    }
}

#[allow(clippy::too_many_lines)]
fn process_cart<T: Read + Seek>(
    path: &Path,
    i: usize,
    max: usize,
    fetch_icon: bool,
    mut cart: Cart<T>,
) {
    //println!("{:?}", cart.;

    //let checksum = cart.checksum().unwrap();
    let ncsd = &cart.ncsd;

    println!("\t{{");
    println!("\t\t\"path\": \"{}\",", path.to_string_lossy());
    let mut signature = String::default();
    for byte in ncsd.signature {
        signature.push_str(&format!("{byte:02X}"));
    }
    println!("\t\t\"signature\": \"{signature}\",");
    println!(
        "\t\t\"magic\": \"{}\",",
        str::from_utf8(&ncsd.magic).unwrap()
    );
    println!("\t\t\"size\": {},", u64::from(ncsd.size) * 0x200);
    println!("\t\t\"main title id\": \"0x{:016X}\",", ncsd.main_title_id);
    for ncch_data in &ncsd.ncch {
        println!("\t\t\"partition {}\": {{", ncch_data.0.index);
        println!("\t\t\t\"fs type\": \"{}\",", ncch_data.0.filesystem_type);
        println!("\t\t\t\"crypt type\": \"{}\",", ncch_data.0.crypt_type);
        println!(
            "\t\t\t\"offset\": \"0x{:08X}\",",
            ncch_data.0.offset * 0x200
        );
        println!(
            "\t\t\t\"size\": \"{} KiB\",",
            ncch_data.0.size * 0x200 / 1024
        );
        println!("\t\t\t\"title ID\": \"0x{:016X}\"", ncch_data.0.title_id,);
        println!("\t\t}},");
    }

    let mut extended_hash = String::default();
    for byte in ncsd.exheader_hash {
        extended_hash.push_str(&format!("{byte:02X}"));
    }
    println!("\t\t\"exheader hash\": \"{extended_hash}\",");
    println!("\t\t\"additional header size\": {},", ncsd.header_size);
    println!("\t\t\"sector zero offset\": {},", ncsd.sector_zero_offset);
    println!("\t\t\"partition flags\": {{");
    println!(
        "\t\t\t\"backup write wait time\": {},",
        ncsd.partition_flags.backup_write_wait_time
    );
    println!(
        "\t\t\t\"media card device\": \"{}\",",
        ncsd.partition_flags.media_card_device
    );
    println!(
        "\t\t\t\"media platform\": \"{}\",",
        ncsd.partition_flags.media_platform
    );
    println!(
        "\t\t\t\"media type\": \"{}\",",
        ncsd.partition_flags.media_type
    );
    println!(
        "\t\t\t\"media unit size\": \"{}\"",
        ncsd.partition_flags.media_unit_size
    );
    println!("\t\t}},");
    for (ncch_partition_header, ncch) in &ncsd.ncch {
        let idx = ncch_partition_header.index;
        println!("\t\t\"NCCH {idx}\": {{");

        let mut signature = String::default();
        for byte in ncch.signature {
            signature.push_str(&format!("{byte:02X}"));
        }

        println!("\t\t\t\"signature\": \"{signature}\",");
        println!(
            "\t\t\t\"magic\": \"{}\",",
            str::from_utf8(&ncch.magic).unwrap()
        );
        println!("\t\t\t\"size\": {},", ncch.size);
        println!(
            "\t\t\t\"partition title ID\": \"0x{:016X}\",",
            ncch.partition_title_id
        );
        println!("\t\t\t\"maker code\": \"0x{:04X}\",", ncch.maker_code);
        println!("\t\t\t\"version\": {},", ncch.version);
        println!("\t\t\t\"sha256_check\": \"0x{:08X}\",", ncch.sha256_check);
        println!("\t\t\t\"program ID\": \"0x{:016X}\",", ncch.program_id);
        //println!("\t\t\t\"logo_sha256\": {},", ncch.);
        println!(
            "\t\t\t\"product code\": \"{}\",",
            str::from_utf8(&ncch.product_code)
                .unwrap()
                .trim_matches('\0')
        );
        //println!("\t\t\t\"extended header sha256\": {},", ncch.);
        println!(
            "\t\t\t\"extended header size\": {},",
            ncch.extended_header_size
        );
        println!("\t\t\t\"flags\": \"0x{:016X}\",", ncch.flags);
        println!(
            "\t\t\t\"plain region offset\": {},",
            0x200 * ncch.plain_region_offset
        );
        println!(
            "\t\t\t\"plain region size\": {},",
            0x200 * ncch.plain_region_size
        );
        if ncch.logo_region_offset.is_some() {
            println!(
                "\t\t\t\"logo region offset\": {},",
                0x200 * ncch.logo_region_offset.unwrap()
            );
            println!(
                "\t\t\t\"logo region size\": {},",
                0x200 * ncch.logo_region_size.unwrap()
            );
        }
        println!("\t\t\t\"exefs offset\": {},", 0x200 * ncch.exefs_offset);
        println!("\t\t\t\"exefs size\": {},", 0x200 * ncch.exefs_size);
        println!(
            "\t\t\t\"exefs hash region size\": {},",
            0x200 * ncch.exefs_hash_region_size
        );
        println!("\t\t\t\"romfs offset\": {},", 0x200 * ncch.romfs_offset);
        println!("\t\t\t\"romfs size\": {},", 0x200 * ncch.romfs_size);
        println!(
            "\t\t\t\"romfs hash region size\": {}",
            0x200 * ncch.romfs_hash_region_size
        );
        println!("\t\t}},");
        //println!("\t\t\t\"exefs sha256\": {},", ncch.);
        //println!("\t\t\t\"romfs sha256\": {},", ncch.);
    }

    println!("\t\t\"cart info\": {{");
    println!(
        "\t\t\t\"writeable address\": \"0x{:08X}\",",
        cart.card_info.writeable_address
    );
    println!("\t\t\t\"flags\": \"0x{:08X}\",", cart.card_info.flags);
    println!("\t\t\t\"filled size\": {},", cart.card_info.filled_size);
    println!(
        "\t\t\t\"title version\": \"{}\",",
        cart.card_info.title_version
    );
    println!("\t\t\t\"card revision\": {},", cart.card_info.card_revision);
    println!(
        "\t\t\t\"CVer title ID\": \"0x{:016X}\",",
        cart.card_info.cver_title_id
    );
    println!(
        "\t\t\t\"CVer version\": \"{}\",",
        cart.card_info.cver_version
    );
    println!("\t\t\t\"initial data\": {{");
    println!(
        "\t\t\t\t\"seed\": \"{:02X?}\",",
        cart.card_info.initial_data.seed
    );
    println!(
        "\t\t\t\t\"title key\": \"{:02X?}\",",
        cart.card_info.initial_data.title_key
    );
    println!(
        "\t\t\t\t\"AES-CCM MAC\": \"{:02X?}\",",
        cart.card_info.initial_data.aes_ccm_mac
    );
    println!(
        "\t\t\t\t\"AES-CCM nonce\": \"{:02X?}\",",
        cart.card_info.initial_data.aes_ccm_nonce
    );
    println!("\t\t\t\t\"NCCH\": {{");
    let ncch = &cart.card_info.initial_data.ncch;
    println!(
        "\t\t\t\t\t\"magic\": \"{}\",",
        str::from_utf8(&ncch.magic).unwrap()
    );
    println!("\t\t\t\t\t\"size\": {},", ncch.size);
    println!(
        "\t\t\t\t\t\"partition title ID\": \"0x{:016X}\",",
        ncch.partition_title_id
    );
    println!("\t\t\t\t\t\"maker code\": \"0x{:04X}\",", ncch.maker_code);
    println!("\t\t\t\t\t\"version\": {},", ncch.version);
    println!(
        "\t\t\t\t\t\"sha256_check\": \"0x{:08X}\",",
        ncch.sha256_check
    );
    println!("\t\t\t\t\t\"program ID\": \"0x{:016X}\",", ncch.program_id);
    //println!("\t\t\t\"logo_sha256\": {},", ncch.);
    println!(
        "\t\t\t\t\t\"product code\": \"{}\",",
        str::from_utf8(&ncch.product_code)
            .unwrap()
            .trim_matches('\0')
    );
    //println!("\t\t\t\"extended header sha256\": {},", ncch.);
    println!(
        "\t\t\t\t\t\"extended header size\": {},",
        ncch.extended_header_size
    );
    println!("\t\t\t\t\t\"flags\": \"0x{:016X}\",", ncch.flags);
    println!(
        "\t\t\t\t\t\"plain region offset\": {},",
        0x200 * ncch.plain_region_offset
    );
    println!(
        "\t\t\t\t\t\"plain region size\": {},",
        0x200 * ncch.plain_region_size
    );
    if ncch.logo_region_offset.is_some() {
        println!(
            "\t\t\t\t\t\"logo region offset\": {},",
            0x200 * ncch.logo_region_offset.unwrap()
        );
        println!(
            "\t\t\t\t\t\"logo region size\": {},",
            0x200 * ncch.logo_region_size.unwrap()
        );
    }
    println!("\t\t\t\t\t\"exefs offset\": {},", 0x200 * ncch.exefs_offset);
    println!("\t\t\t\t\t\"exefs size\": {},", 0x200 * ncch.exefs_size);
    println!(
        "\t\t\t\t\t\"exefs hash region size\": {},",
        0x200 * ncch.exefs_hash_region_size
    );
    println!("\t\t\t\t\t\"romfs offset\": {},", 0x200 * ncch.romfs_offset);
    println!("\t\t\t\t\t\"romfs size\": {},", 0x200 * ncch.romfs_size);
    println!(
        "\t\t\t\t\t\"romfs hash region size\": {}",
        0x200 * ncch.romfs_hash_region_size
    );
    println!("\t\t\t\t}}");
    println!("\t\t\t}}");
    println!("\t\t}}");
    //println!("\t\t\t\"exefs sha256\": {},", ncch.);
    //println!("\t\t\t\"romfs sha256\": {},", ncch.);

    if i == (max - 1) {
        println!("\t}}");
    } else {
        println!("\t}},");
    }

    if fetch_icon {
        let smdh = cart.read_smdh().unwrap().unwrap();
        let icon = smdh.extract_rgba_icon();
        let mut path = String::default();
        path.push_str(&smdh.titles[1].short_description);
        path.push_str(".png");
        let f = File::create(path).unwrap();
        let w = BufWriter::new(f);
        let mut encoder = Encoder::new(w, 48, 48);
        encoder.set_color(ColorType::Rgba);
        encoder.set_depth(BitDepth::Eight);
        let mut writer = encoder.write_header().unwrap();
        writer.write_image_data(&icon).unwrap();
    }
}

pub fn main() {
    let args = Cli::parse();

    if args.files.is_empty() {
        return;
    }

    println!("[");

    for (i, path) in args.files.iter().enumerate() {
        {
            let f = File::open(path).unwrap();
            if let Ok(cart) = Cart::new(f) {
                if cart.ncsd.magic == [78u8, 67, 83, 68] {
                    process_cart(path, i, args.files.len(), args.fetch_icon, cart);
                    continue;
                }
            }
        }

        {
            let f = File::open(path).unwrap();
            if let Ok(cia) = CIA::new(f) {
                process_cia(path, i, args.files.len(), args.fetch_icon, cia);
                continue;
            }
        }
        eprintln!(
            "Skipping {}, was unable to detect a cart or CIA",
            path.to_string_lossy()
        );
    }

    println!("]");
    /*
    let header = match f.read_gb_header() {
        Ok(header) => header,
        Err(err) => {
            eprintln!("Error: {err}");
            return;
        }
    };
    */
}