use crate::cart_type::CartType;
use crate::error::Error;
use crate::licensee::Licensee;
use crate::licensee::NewLicensee;
use std::fmt;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
use std::str;
use byteorder::BigEndian;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use byteorder::WriteBytesExt;
#[derive(Debug, Copy, Clone)]
pub enum Region {
Japan,
Elsewhere,
}
impl fmt::Display for Region {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Japan => write!(fmt, "Japan")?,
Self::Elsewhere => write!(fmt, "Not Japan")?,
}
Ok(())
}
}
impl From<u8> for Region {
fn from(data: u8) -> Self {
match data {
0 => Self::Japan,
_ => Self::Elsewhere,
}
}
}
impl From<Region> for u8 {
fn from(data: Region) -> Self {
match data {
Region::Japan => 0,
Region::Elsewhere => 1,
}
}
}
#[derive(Debug)]
pub struct Metadata {
pub entry_point: u32,
pub nintendo_logo: [u8; 48],
title: String,
manufacturer_code: Option<[u8; 4]>,
cgb_flag: Option<u8>,
new_licensee: Option<NewLicensee>,
pub sgb_flag: bool,
pub cart_type: CartType,
pub rom_size: u16,
pub save_ram_size: u8,
pub region: Region,
licensee: Licensee,
pub rom_version: u8,
pub header_checksum: u8,
pub checksum: u16,
}
impl Metadata {
pub fn print_logo(&self) {
let output = self.extract_boot_logo();
for y in 0..16 {
for x in 0..48 {
if output[x + y * 48] == 0 {
print!(" ");
} else {
print!("\u{2588}\u{2588}");
}
}
println!();
}
}
fn extract_boot_logo(&self) -> [u8; 48 * 16] {
let logo = self.nintendo_logo;
let mut output = [0u8; 48 * 16];
for i in (0..logo.len()).step_by(4) {
let tile_num = i / 4;
let output_index = ((tile_num % 6) * 8) + ((tile_num / 6) * 48 * 8);
for y in (0..7).step_by(4) {
for x in 0..8 {
let byte_index = (x / 4) * 2 + (y / 4);
let byte = logo[i + byte_index];
let output_index = output_index + x + y * 48;
let bit = x % 4;
output[output_index] = ((byte >> (7 - bit)) & 0x1) * 0xFF;
output[output_index + 48] = ((byte >> (7 - bit)) & 0x1) * 0xFF;
output[output_index + 48 * 2] = ((byte >> (3 - bit)) & 0x1) * 0xFF;
output[output_index + 48 * 3] = ((byte >> (3 - bit)) & 0x1) * 0xFF;
}
}
}
output
}
}
pub trait MetadataRead {
fn read_gb_cart(&mut self) -> Result<Metadata, Error>;
}
pub trait MetadataWrite {
fn write_gb_cart(&mut self, header: &Metadata) -> Result<(), Error>;
}
fn valid_characters_header(slice: &[u8]) -> bool {
slice
.iter()
.all(|b| !b.is_ascii_lowercase() && (b.is_ascii_graphic() || b.is_ascii_whitespace()))
}
impl<T: Read + Seek> MetadataRead for T {
fn read_gb_cart(&mut self) -> Result<Metadata, Error> {
self.seek(SeekFrom::Start(0x100))?;
let entry_point = self.read_u32::<LittleEndian>()?;
let mut nintendo_logo = [0u8; 48];
self.read_exact(&mut nintendo_logo)?;
let mut title = [0u8; 16];
self.read_exact(&mut title)?;
let cgb_flag = if title[15].is_ascii() {
None
} else {
Some(title[15])
};
let null_position = title.iter().position(|&b| b == 0x00);
let manufacturer_code = if let Some(position) = null_position
&& position < 12
&& title[11..15].iter().all(|b| *b != 0u8)
{
let mut code = [0u8; 4];
code.copy_from_slice(&title[11..15]);
if !valid_characters_header(&code) {
return Err(Error::Parse(
"manufacturer code is not all uppercase ASCII".into(),
));
}
Some(code)
} else {
None
};
let title = null_position.map_or_else(|| &title[..], |position| &title[..position]);
if !valid_characters_header(title) {
return Err(Error::Parse("title is not all uppercase ASCII".into()));
}
let title = str::from_utf8(title)?.trim_matches('\0').to_string();
let mut new_licensee = [0u8; 2];
self.read_exact(&mut new_licensee)?;
let new_licensee = NewLicensee::from(new_licensee);
let sgb_flag = self.read_u8()? == 0x03;
let cart_type = self.read_u8()?.into();
let rom_size = self.read_u8()?;
if rom_size > 8 {
return Err(Error::Parse("ROM size is too large".into()));
}
let rom_size: u16 = 32 * (1 << rom_size);
let save_ram_size = self.read_u8()?;
let save_ram_size = match save_ram_size {
0 => 0,
2 => 8,
3 => 32,
4 => 128,
5 => 64,
_ => return Err(Error::Parse("unknown RAM size".into())),
};
let region = self.read_u8()?.into();
let licensee = self.read_u8()?.into();
let rom_version = self.read_u8()?;
let header_checksum = self.read_u8()?;
let checksum = self.read_u16::<BigEndian>()?;
let new_licensee = match licensee {
Licensee::NewLicenseeField => Some(new_licensee),
_ => None,
};
Ok(Metadata {
entry_point,
nintendo_logo,
title,
manufacturer_code,
cgb_flag,
new_licensee,
sgb_flag,
cart_type,
rom_size,
save_ram_size,
region,
licensee,
rom_version,
header_checksum,
checksum,
})
}
}
impl Metadata {
pub fn header_checksum<T: Seek + Read>(&self, io: &mut T) -> Result<u8, Error> {
io.seek(SeekFrom::Start(0x134))?;
let mut checksum = 0u8;
for _ in 0..(0x14D - 0x134) {
checksum = checksum.wrapping_sub(io.read_u8()?).wrapping_sub(1);
}
Ok(checksum)
}
pub fn checksum<T: Seek + Read>(&self, io: &mut T) -> Result<u16, Error> {
io.seek(SeekFrom::Start(0))?;
let mut checksum = 0u16;
let mut data = Vec::new();
io.read_to_end(&mut data)?;
data[0x14E] = 0;
data[0x14F] = 0;
for byte in &data {
checksum = checksum.wrapping_add(u16::from(*byte));
}
Ok(checksum)
}
pub fn set_title(&mut self, title: &str) -> Result<(), Error> {
let title_max = if self.manufacturer_code.is_some() {
11
} else if self.cgb_flag.is_some() {
15
} else {
16
};
if title.len() > title_max {
return Err(Error::Parse(format!(
"title is too long, max size is {title_max}"
)));
}
self.title = title.into();
Ok(())
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
pub fn set_manufacturer_code(&mut self, code: [u8; 4]) -> Result<(), Error> {
if self.title.len() > 11 {
return Err(Error::Parse(
"title is too long to set manufacturer code".into(),
));
}
self.manufacturer_code = Some(code);
Ok(())
}
#[must_use]
pub const fn manufacturer_code(&self) -> Option<[u8; 4]> {
self.manufacturer_code
}
pub fn set_cgb_flag(&mut self, flag: u8) -> Result<(), Error> {
if self.title.len() == 16 {
return Err(Error::Parse("title is too long to set CGB flag".into()));
}
self.cgb_flag = Some(flag);
Ok(())
}
#[must_use]
pub const fn cgb_flag(&self) -> Option<u8> {
self.cgb_flag
}
pub const fn set_new_licensee(&mut self, new_licensee: NewLicensee) {
self.licensee = Licensee::NewLicenseeField;
self.new_licensee = Some(new_licensee);
}
#[must_use]
pub const fn new_licensee(&self) -> Option<NewLicensee> {
self.new_licensee
}
pub fn set_licensee(&mut self, licensee: Licensee) -> Result<(), Error> {
if matches!(licensee, Licensee::NewLicenseeField) {
Err(Error::Parse(
"cannot set the new licensee field this way".into(),
))
} else {
self.licensee = licensee;
self.new_licensee = None;
Ok(())
}
}
#[must_use]
pub const fn licensee(&self) -> Licensee {
self.licensee
}
pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
io.read_gb_cart()
}
}
impl<T: Write + Seek> MetadataWrite for T {
fn write_gb_cart(&mut self, header: &Metadata) -> Result<(), Error> {
self.seek(SeekFrom::Start(0x100))?;
self.write_u32::<LittleEndian>(header.entry_point)?;
self.write_all(&header.nintendo_logo)?;
let mut title = [0u8; 16];
title[..header.title.len()].copy_from_slice(header.title.as_bytes());
self.write_all(&title)?;
if let Some(manufacturer_code) = header.manufacturer_code {
self.seek(SeekFrom::Start(0x013F))?;
self.write_all(&manufacturer_code)?;
}
if let Some(cgb_flag) = header.cgb_flag {
self.seek(SeekFrom::Start(0x143))?;
self.write_u8(cgb_flag)?;
}
self.write_u16::<LittleEndian>(
header
.new_licensee
.unwrap_or(NewLicensee::Unknown(0))
.into(),
)?;
self.write_u8(if header.sgb_flag { 0x03 } else { 0x00 })?;
self.write_u8(header.cart_type.into())?;
let rom_size = u8::try_from((header.rom_size / 32).trailing_zeros());
if rom_size.is_err() {
return Err(Error::Parse("invalid ROM size".into()));
}
self.write_u8(rom_size.unwrap())?;
let save_ram_size = match header.save_ram_size {
0 => 0x0u8,
8 => 0x2,
32 => 0x3,
128 => 0x4,
64 => 0x5,
_ => return Err(Error::Parse("Unknown RAM size".into())),
};
self.write_u8(save_ram_size)?;
self.write_u8(header.region.into())?;
self.write_u8(header.licensee.into())?;
self.write_u8(header.rom_version)?;
self.write_u8(header.header_checksum)?;
self.write_u16::<BigEndian>(header.checksum)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
const OLD_HEADER: [u8; 0x50] = [
0x00, 0xc3, 0x6e, 0x01, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0x55, 0x4E, 0x49, 0x54, 0x20, 0x54, 0x45, 0x53, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x08, 0x05, 0x01, 0xE9, 0xFF, 0x41, 0x30, 0xD9, ];
#[test]
fn old_cart() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&OLD_HEADER);
let mut fake_cart = Cursor::new(fake_cart);
let cart = fake_cart.read_gb_cart().unwrap();
assert_eq!(cart.entry_point, 0x016EC300);
let fake_logo: [u8; 16 * 3] = [
0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, ];
assert_eq!(cart.nintendo_logo, fake_logo);
assert_eq!(cart.title, "UNIT TEST");
assert_eq!(cart.manufacturer_code, None);
assert_eq!(cart.cgb_flag, None);
assert!(matches!(cart.new_licensee, None));
assert_eq!(cart.sgb_flag, false);
assert!(matches!(cart.cart_type, CartType::Mbc3TimerRamBattery));
assert_eq!(cart.rom_size, 8 * 1024);
assert_eq!(cart.save_ram_size, 64);
assert!(matches!(cart.region, Region::Elsewhere));
assert!(matches!(cart.licensee, Licensee::Natsume));
assert_eq!(cart.rom_version, 255);
assert_eq!(
cart.header_checksum(&mut fake_cart).unwrap(),
cart.header_checksum
);
assert_eq!(cart.checksum(&mut fake_cart).unwrap(), cart.checksum);
}
const NEW_HEADER: [u8; 0x50] = [
0x00, 0xc3, 0x6e, 0x01, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0x55, 0x4E, 0x49, 0x54, 0x20, 0x54, 0x45, 0x53, 0x54, 0x00, 0x00, 0x54, 0x45, 0x53, 0x54, 0x80, 0x36, 0x34, 0x00, 0x10, 0x08, 0x05, 0x01, 0x33, 0xFF, 0xCD, 0x32, 0xD9, ];
#[test]
fn new_cart() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
let mut fake_cart = Cursor::new(fake_cart);
let cart = fake_cart.read_gb_cart().unwrap();
assert_eq!(cart.entry_point, 0x016EC300);
let fake_logo: [u8; 16 * 3] = [
0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, ];
assert_eq!(cart.nintendo_logo, fake_logo);
assert_eq!(cart.title, "UNIT TEST");
assert_eq!(cart.manufacturer_code, Some([0x54, 0x45, 0x53, 0x54]));
assert_eq!(cart.cgb_flag, Some(0x80));
assert!(matches!(cart.new_licensee, Some(NewLicensee::Lucasarts)));
assert_eq!(cart.sgb_flag, false);
assert!(matches!(cart.cart_type, CartType::Mbc3TimerRamBattery));
assert_eq!(cart.rom_size, 8 * 1024);
assert_eq!(cart.save_ram_size, 64);
assert!(matches!(cart.region, Region::Elsewhere));
assert!(matches!(cart.licensee, Licensee::NewLicenseeField));
assert_eq!(cart.rom_version, 255);
assert_eq!(
cart.header_checksum(&mut fake_cart).unwrap(),
cart.header_checksum
);
assert_eq!(cart.checksum(&mut fake_cart).unwrap(), cart.checksum);
}
#[test]
fn new_cart_bad_title() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
fake_cart[0x135] = 0x80; let mut fake_cart = Cursor::new(fake_cart);
let cart = fake_cart.read_gb_cart();
assert!(
matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "title is not all uppercase ASCII"))
);
fake_cart.get_mut()[0x100..0x150].copy_from_slice(&NEW_HEADER);
fake_cart.get_mut()[0x13F] = 0x80;
let cart = fake_cart.read_gb_cart();
assert!(
matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "manufacturer code is not all uppercase ASCII"))
);
fake_cart.get_mut()[0x100..0x150].copy_from_slice(&NEW_HEADER);
fake_cart.get_mut()[0x13F] = 0x61;
let cart = fake_cart.read_gb_cart();
assert!(
matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "manufacturer code is not all uppercase ASCII"))
);
}
#[test]
fn new_cart_test_set_title() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
let mut fake_cart = Cursor::new(fake_cart);
let mut cart = fake_cart.read_gb_cart().unwrap();
let result = cart.set_title("12345678901234567890"); assert!(
matches!(result, Err(err) if matches!(&err, Error::Parse(string) if string == "title is too long, max size is 11"))
);
let result = cart.set_title("123456789012"); assert!(
matches!(result, Err(err) if matches!(&err, Error::Parse(string) if string == "title is too long, max size is 11"))
);
let result = cart.set_title("12345678901"); assert!(result.is_ok());
assert_eq!(cart.title(), "12345678901");
}
#[test]
fn old_cart_test_set_title() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&OLD_HEADER);
let mut fake_cart = Cursor::new(fake_cart);
let mut cart = fake_cart.read_gb_cart().unwrap();
let result = cart.set_title("12345678901234567890"); assert!(
matches!(result, Err(err) if matches!(&err, Error::Parse(string) if string == "title is too long, max size is 16"))
);
let result = cart.set_title("1234567890123456"); assert!(result.is_ok());
assert_eq!(cart.title(), "1234567890123456");
}
#[test]
fn new_cart_bad_manufacturer_code() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
fake_cart[0x140] = 0x80; let mut fake_cart = Cursor::new(fake_cart);
let cart = fake_cart.read_gb_cart();
assert!(
matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "manufacturer code is not all uppercase ASCII"))
);
}
#[test]
fn new_cart_bad_rom_size() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
fake_cart[0x148] = 0x9; let mut fake_cart = Cursor::new(fake_cart);
let cart = fake_cart.read_gb_cart();
assert!(
matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "ROM size is too large"))
);
}
#[test]
fn new_cart_bad_save_ram_size() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
fake_cart[0x149] = 0x6; let mut fake_cart = Cursor::new(fake_cart);
let cart = fake_cart.read_gb_cart();
assert!(
matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "unknown RAM size"))
);
}
#[test]
fn new_cart_read_write() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
let original = fake_cart.clone();
let mut fake_cart = Cursor::new(fake_cart);
let cart = fake_cart.read_gb_cart().unwrap();
let fake_cart = vec![0u8; 8 * 1024 * 1024];
let mut fake_cart = Cursor::new(fake_cart);
fake_cart.write_gb_cart(&cart).unwrap();
let fake_cart = fake_cart.into_inner();
assert_eq!(fake_cart[0x100..0x150], original[0x100..0x150]);
assert_eq!(fake_cart, original);
}
#[test]
fn old_cart_read_write() {
let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; fake_cart[0x100..0x150].copy_from_slice(&OLD_HEADER);
let original = fake_cart.clone();
let mut fake_cart = Cursor::new(fake_cart);
let cart = fake_cart.read_gb_cart().unwrap();
let fake_cart = vec![0u8; 8 * 1024 * 1024];
let mut fake_cart = Cursor::new(fake_cart);
fake_cart.write_gb_cart(&cart).unwrap();
let fake_cart = fake_cart.into_inner();
assert_eq!(fake_cart[0x100..0x150], original[0x100..0x150]);
assert_eq!(fake_cart, original);
}
}