use ctr_cart::CIA;
use ctr_cart::Cart;
use ctr_cart::SMDHRead;
use std::fmt::Write;
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>,
) {
let ncsd = &cart.ncsd;
println!("\t{{");
println!("\t\t\"path\": \"{}\",", path.to_string_lossy());
let mut signature = String::default();
for byte in ncsd.signature {
write!(signature, "{byte:02X}").expect("This should never fail");
}
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 {
write!(extended_hash, "{byte:02X}").expect("This should never fail");
}
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 {
write!(signature, "{byte:02X}").expect("This should never fail");
}
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\"product code\": \"{}\",",
str::from_utf8(&ncch.product_code)
.unwrap()
.trim_matches('\0')
);
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\"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\t\t\"product code\": \"{}\",",
str::from_utf8(&ncch.product_code)
.unwrap()
.trim_matches('\0')
);
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}}");
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)
&& 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!("]");
}