use crate::error::Error;
use std::fmt;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::str::from_utf8;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use crc::Crc;
#[derive(Debug)]
pub enum LanguageRegion {
Japanese,
English,
German,
French,
Italian,
Spanish,
NotApplicable,
Asian,
Chinese,
Dutch,
Korean,
USA2,
Swedish,
Nor,
Int,
Europe,
Danish,
Russian,
USAAUS,
Australia,
EURAUS,
Europe2,
Europe3,
Europe4,
Europe5,
Unknown(char),
}
impl fmt::Display for LanguageRegion {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Japanese => write!(fmt, "Japanese")?,
Self::English => write!(fmt, "English")?,
Self::German => write!(fmt, "German")?,
Self::French => write!(fmt, "French")?,
Self::Italian => write!(fmt, "Italian")?,
Self::Spanish => write!(fmt, "Spanish")?,
Self::NotApplicable => write!(fmt, "not applicable")?,
Self::Asian => write!(fmt, "Asian")?,
Self::Chinese => write!(fmt, "Chinese")?,
Self::Dutch => write!(fmt, "Dutch")?,
Self::Korean => write!(fmt, "Korean")?,
Self::USA2 => write!(fmt, "USA2")?,
Self::Swedish => write!(fmt, "Swedish")?,
Self::Nor => write!(fmt, "Nor")?,
Self::Int => write!(fmt, "Int")?,
Self::Europe => write!(fmt, "Europe")?,
Self::Danish => write!(fmt, "Danish")?,
Self::Russian => write!(fmt, "Russian")?,
Self::USAAUS => write!(fmt, "USA + Australia")?,
Self::Australia => write!(fmt, "Australia")?,
Self::EURAUS => write!(fmt, "Europe + Australia")?,
Self::Europe2 => write!(fmt, "Europe 2")?,
Self::Europe3 => write!(fmt, "Europe 3")?,
Self::Europe4 => write!(fmt, "Eurioe 4")?,
Self::Europe5 => write!(fmt, "Europe 5")?,
Self::Unknown(ch) => write!(fmt, "Unknown region, code {ch}")?,
}
Ok(())
}
}
impl TryFrom<u8> for LanguageRegion {
type Error = Error;
fn try_from(data: u8) -> Result<Self, Error> {
if !data.is_ascii() {
return Err(Error::Parse("invalid language character".into()));
}
let data = data as char;
Ok(match data {
'J' => Self::Japanese,
'E' => Self::English,
'P' => Self::Europe,
'D' => Self::German,
'F' => Self::French,
'I' => Self::Italian,
'S' => Self::Spanish,
'B' | 'G' => Self::NotApplicable,
'A' => Self::Asian,
'C' => Self::Chinese,
'H' => Self::Dutch,
'K' => Self::Korean,
'L' => Self::USA2,
'M' => Self::Swedish,
'N' => Self::Nor,
'O' => Self::Int,
'Q' => Self::Danish,
'R' => Self::Russian,
'T' => Self::USAAUS,
'U' => Self::Australia,
'V' => Self::EURAUS,
'W' => Self::Europe2,
'X' => Self::Europe3,
'Y' => Self::Europe4,
'Z' => Self::Europe5,
_ => Self::Unknown(data),
})
}
}
#[derive(Debug)]
pub enum Region {
Normal,
China,
Korea,
Unknown(u8),
}
impl From<u8> for Region {
fn from(data: u8) -> Self {
match data {
0 => Self::Normal,
0x80 => Self::China,
0x40 => Self::Korea,
_ => Self::Unknown(data),
}
}
}
impl fmt::Display for Region {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Normal => write!(fmt, "Normal"),
Self::China => write!(fmt, "China"),
Self::Korea => write!(fmt, "Korea"),
Self::Unknown(data) => write!(fmt, "unknown region {data}"),
}
}
}
#[derive(Debug)]
pub enum Manufacturer {
Nintendo,
Capcom,
SquareEnix,
Thq,
Other([u8; 2]),
}
impl fmt::Display for Manufacturer {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Nintendo => write!(fmt, "Nintendo"),
Self::Capcom => write!(fmt, "Capcom"),
Self::SquareEnix => write!(fmt, "Square Enix"),
Self::Thq => write!(fmt, "THQ"),
Self::Other(code) => write!(fmt, "Unknown {:02X}{:02X}", code[0], code[1]),
}
}
}
impl TryFrom<&str> for Manufacturer {
type Error = Error;
fn try_from(data: &str) -> Result<Self, Error> {
if data.len() != 2 || !data.is_ascii() {
return Err(Error::Parse("invalid manufacturer code".into()));
}
let data = data.as_bytes();
Ok(match data {
[48, 49] => Self::Nintendo, [48, 56] => Self::Capcom, [71, 68] => Self::SquareEnix, [55, 56] => Self::Thq, _ => Self::Other([data[0], data[1]]),
})
}
}
#[derive(Debug)]
pub enum UnitCode {
Nds,
NdsDsi,
Dsi,
}
impl TryFrom<u8> for UnitCode {
type Error = Error;
fn try_from(data: u8) -> Result<Self, Self::Error> {
match data {
0 => Ok(Self::Nds),
1 => Ok(Self::NdsDsi),
2 => Ok(Self::Dsi),
_ => Err(Self::Error::Parse("unable to parse unit code".into())),
}
}
}
impl fmt::Display for UnitCode {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Nds => write!(fmt, "NDS"),
Self::NdsDsi => write!(fmt, "NDS + DSi"),
Self::Dsi => write!(fmt, "DSi"),
}
}
}
#[derive(Debug)]
pub struct Metadata {
pub title: String,
pub game_code: String,
pub manufacturer: Manufacturer,
pub unit_code: UnitCode,
pub encryption_seed_select: u8,
pub device_capacity: u32,
pub language: LanguageRegion,
pub region: Region,
pub version: u8,
pub autostart: bool,
pub arm9_rom_offset: u32,
pub arm9_entry_address: u32,
pub arm9_ram_address: u32,
pub arm9_size: u32,
pub arm7_rom_offset: u32,
pub arm7_entry_address: u32,
pub arm7_ram_address: u32,
pub arm7_size: u32,
pub file_name_table_offset: u32,
pub file_name_table_size: u32,
pub file_allocation_table_offset: u32,
pub file_allocation_table_size: u32,
pub arm9_overlay_offset: u32,
pub arm9_overlay_size: u32,
pub arm7_overlay_offset: u32,
pub arm7_overlay_size: u32,
pub romctl_setting_normal: u32,
pub romctl_setting_key1: u32,
pub icon_offset: u32,
pub secure_area_checksum: u16,
pub secure_area_delay: u16,
pub arm9_autoload: u32,
pub arm7_autoload: u32,
pub secure_area_disable: u64,
pub used_rom_size: u32,
pub rom_header_size: u32,
pub nand_end_of_rom: u16, pub nand_start_of_rw: u16,
pub nintendo_logo: [u8; 156],
pub nintendo_logo_checksum: u16,
pub header_checksum: u16,
pub debug_rom_offset: u32,
pub debug_size: u32,
pub debug_ram_address: u32,
}
pub trait MetadataRead {
fn read_nds_header(&mut self) -> Result<Metadata, Error>;
}
impl Metadata {
pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
io.read_nds_header()
}
}
fn trim(string: &str) -> &str {
string.trim_matches('\0').trim()
}
impl<T: Read + Seek> MetadataRead for T {
fn read_nds_header(&mut self) -> Result<Metadata, Error> {
self.seek(SeekFrom::Start(0))?;
let mut title = [0u8; 12];
self.read_exact(&mut title)?;
let title = trim(from_utf8(&title)?).to_string();
let mut game_code = [0u8; 4];
self.read_exact(&mut game_code)?;
let language: LanguageRegion = game_code[3].try_into()?;
let game_code = trim(from_utf8(&game_code)?).to_string();
let mut manufacturer_code = [0u8; 2];
self.read_exact(&mut manufacturer_code)?;
let manufacturer_code = trim(from_utf8(&manufacturer_code)?);
let manufacturer = manufacturer_code.try_into()?;
let unit_code: UnitCode = self.read_u8()?.try_into()?;
let encryption_seed_select = self.read_u8()?;
let device_capacity: u32 = 128 << self.read_u8()?;
self.seek(SeekFrom::Current(8))?;
let region: Region = self.read_u8()?.into();
let version = self.read_u8()?;
let autostart = self.read_u8()? != 0;
let arm9_rom_offset = self.read_u32::<LittleEndian>()?;
let arm9_entry_address = self.read_u32::<LittleEndian>()?;
let arm9_ram_address = self.read_u32::<LittleEndian>()?;
let arm9_size = self.read_u32::<LittleEndian>()?;
let arm7_rom_offset = self.read_u32::<LittleEndian>()?;
let arm7_entry_address = self.read_u32::<LittleEndian>()?;
let arm7_ram_address = self.read_u32::<LittleEndian>()?;
let arm7_size = self.read_u32::<LittleEndian>()?;
let file_name_table_offset = self.read_u32::<LittleEndian>()?;
let file_name_table_size = self.read_u32::<LittleEndian>()?;
let file_allocation_table_offset = self.read_u32::<LittleEndian>()?;
let file_allocation_table_size = self.read_u32::<LittleEndian>()?;
let arm9_overlay_offset = self.read_u32::<LittleEndian>()?;
let arm9_overlay_size = self.read_u32::<LittleEndian>()?;
let arm7_overlay_offset = self.read_u32::<LittleEndian>()?;
let arm7_overlay_size = self.read_u32::<LittleEndian>()?;
let romctl_setting_normal = self.read_u32::<LittleEndian>()?;
let romctl_setting_key1 = self.read_u32::<LittleEndian>()?;
let icon_offset = self.read_u32::<LittleEndian>()?;
let secure_area_checksum = self.read_u16::<LittleEndian>()?;
let secure_area_delay = self.read_u16::<LittleEndian>()?; let arm9_autoload = self.read_u32::<LittleEndian>()?;
let arm7_autoload = self.read_u32::<LittleEndian>()?;
let secure_area_disable = self.read_u64::<LittleEndian>()?;
let used_rom_size = self.read_u32::<LittleEndian>()?;
let rom_header_size = self.read_u32::<LittleEndian>()?;
self.seek(SeekFrom::Current(12))?;
let nand_end_of_rom = self.read_u16::<LittleEndian>()?;
let nand_start_of_rw = self.read_u16::<LittleEndian>()?;
self.seek(SeekFrom::Current(0x28))?;
let mut nintendo_logo = [0u8; 156];
self.read_exact(&mut nintendo_logo)?;
let nintendo_logo_checksum = self.read_u16::<LittleEndian>()?;
let header_checksum = self.read_u16::<LittleEndian>()?;
let debug_rom_offset = self.read_u32::<LittleEndian>()?;
let debug_size = self.read_u32::<LittleEndian>()?;
let debug_ram_address = self.read_u32::<LittleEndian>()?;
Ok(Metadata {
title,
game_code,
manufacturer,
unit_code,
encryption_seed_select,
device_capacity,
language,
region,
version,
autostart,
arm9_rom_offset,
arm9_entry_address,
arm9_ram_address,
arm9_size,
arm7_rom_offset,
arm7_entry_address,
arm7_ram_address,
arm7_size,
file_name_table_offset,
file_name_table_size,
file_allocation_table_offset,
file_allocation_table_size,
arm9_overlay_offset,
arm9_overlay_size,
arm7_overlay_offset,
arm7_overlay_size,
romctl_setting_normal,
romctl_setting_key1,
icon_offset,
secure_area_checksum,
secure_area_delay,
arm9_autoload,
arm7_autoload,
secure_area_disable,
used_rom_size,
rom_header_size,
nand_end_of_rom,
nand_start_of_rw,
nintendo_logo,
nintendo_logo_checksum,
header_checksum,
debug_rom_offset,
debug_size,
debug_ram_address,
})
}
}
#[allow(clippy::cast_possible_truncation)]
const fn scale_5bits_to_8bits(data: u16) -> u8 {
((data & 0x1F) * 0xFF / 0x1F) as u8
}
struct RGBAPixel(u8, u8, u8, u8);
impl Metadata {
pub fn header_checksum<T: Read + Seek>(&mut self, io: &mut T) -> Result<u16, Error> {
let crc = Crc::<u16>::new(&crc::CRC_16_MODBUS);
io.seek(SeekFrom::Start(0))?;
let mut data = [0u8; 0x15E];
io.read_exact(&mut data)?;
Ok(crc.checksum(&data))
}
pub fn extract_rgba_icon<T: Read + Seek>(&mut self, io: &mut T) -> Result<Vec<u8>, Error> {
let width = 32;
let height = 32;
let tile_w = 8;
let tile_h = 8;
let mut index = 0;
let mut output = vec![0u8; width * height * 4];
let icon_offset = self.icon_offset;
let bitmap_offset = 0x20;
io.seek(SeekFrom::Start(u64::from(icon_offset + bitmap_offset)))?;
let mut bitmap = vec![0u8; 0x200];
io.read_exact(&mut bitmap)?;
let mut palette = vec![0u16; 0x10];
io.read_u16_into::<LittleEndian>(&mut palette)?;
for y in (0..height).step_by(tile_h) {
for x in (0..width).step_by(tile_w) {
for ty in 0..tile_h {
for tx in 0..tile_w {
let pixel: u8 = (bitmap[index] >> (4 * (tx % 2))) & 0xF;
if (tx % 2) == 1 {
index += 1;
}
let pixel = if pixel == 0 {
RGBAPixel(0xFF, 0xFF, 0xFF, 0)
} else {
let color16 = palette[usize::from(pixel)];
RGBAPixel(
scale_5bits_to_8bits(color16),
scale_5bits_to_8bits(color16 >> 5),
scale_5bits_to_8bits(color16 >> 10),
0xFF,
)
};
output[4 * ((y + ty) * 32 + (x + tx))] = pixel.0;
output[4 * ((y + ty) * 32 + (x + tx)) + 1] = pixel.1;
output[4 * ((y + ty) * 32 + (x + tx)) + 2] = pixel.2;
output[4 * ((y + ty) * 32 + (x + tx)) + 3] = pixel.3;
}
}
}
}
Ok(output)
}
}