use crate::error::Error;
use std::fmt;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::str;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
#[derive(Debug)]
pub enum Speed {
Slow,
Fast,
}
impl fmt::Display for Speed {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Slow => write!(fmt, "slow")?,
Self::Fast => write!(fmt, "fast")?,
}
Ok(())
}
}
#[derive(Debug)]
pub enum Mode {
Lorom,
SDD1,
Hirom,
SA1,
Exhirom,
}
impl fmt::Display for Mode {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Lorom => write!(fmt, "LoROM")?,
Self::SDD1 => write!(fmt, "S-DD1")?,
Self::Hirom => write!(fmt, "HiROM")?,
Self::SA1 => write!(fmt, "SA-1")?,
Self::Exhirom => write!(fmt, "ExHiROM")?,
}
Ok(())
}
}
#[derive(Debug)]
pub enum Chipset {
RomOnly,
RomRam,
RomRamBattery,
RomCoprocessor,
RomCoprocessorRam,
RomCoprocessorRamBattery,
RomCoprocessorBattery,
}
impl fmt::Display for Chipset {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::RomOnly => write!(fmt, "ROM only")?,
Self::RomRam => write!(fmt, "ROM + RAM")?,
Self::RomRamBattery => write!(fmt, "ROM + RAM + battery")?,
Self::RomCoprocessor => write!(fmt, "ROM + coprocessor")?,
Self::RomCoprocessorRam => write!(fmt, "ROM + coprocessor + RAM")?,
Self::RomCoprocessorRamBattery => write!(fmt, "ROM + coprocessor + RAM + battery")?,
Self::RomCoprocessorBattery => write!(fmt, "ROM + coprocessor + battery")?,
}
Ok(())
}
}
#[derive(Debug)]
pub enum Coprocessor {
None,
DSP,
SuperFX,
OBC1,
SA1,
SDD1,
SRTC,
Other,
Custom,
}
impl fmt::Display for Coprocessor {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::None => write!(fmt, "None")?,
Self::DSP => write!(fmt, "DSP")?,
Self::SuperFX => write!(fmt, "GSU/SuperFX")?,
Self::OBC1 => write!(fmt, "OBC1")?,
Self::SA1 => write!(fmt, "SA-1")?,
Self::SDD1 => write!(fmt, "S-DD1")?,
Self::SRTC => write!(fmt, "S-RTC")?,
Self::Other => write!(fmt, "other")?,
Self::Custom => write!(fmt, "custom")?,
}
Ok(())
}
}
#[derive(Debug)]
pub enum Region {
Japan,
USA,
Europe,
}
impl fmt::Display for Region {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Japan => write!(fmt, "Japan")?,
Self::USA => write!(fmt, "USA")?,
Self::Europe => write!(fmt, "Europe")?,
}
Ok(())
}
}
#[derive(Debug)]
pub struct NativeVectors {
pub cop: u16,
pub brk: u16,
pub abort: u16,
pub nmi: u16,
pub irq: u16,
}
#[derive(Debug)]
pub struct EmulationVectors {
pub cop: u16,
pub abort: u16,
pub nmi: u16,
pub reset: u16,
pub irq_brk: u16,
}
#[derive(Debug)]
pub struct Vectors {
pub native: NativeVectors,
pub emulation: EmulationVectors,
}
#[derive(Debug)]
pub struct Cart {
pub title: String,
pub speed: Speed,
pub mode: Mode,
pub chipset: Chipset,
pub coprocessor: Coprocessor,
pub rom_size: u32,
pub save_ram_size: u32,
pub region: Region,
pub dev_id: u8,
pub version: String,
pub checksum: u16,
pub vectors: Vectors,
pub expanded_header: Option<ExpandedHeader>,
}
#[derive(Debug)]
pub struct ExpandedHeader {
pub maker_code: String,
pub game_code: String,
pub expansion_flash_size: u32,
pub expansion_ram_size: u32,
pub special_version: u8,
pub chipset_subtype: u8,
}
fn compute_sfc_checksum(data: &[u8]) -> u16 {
let mut checksum = 0u16;
for byte in data {
checksum = checksum.wrapping_add(u16::from(*byte));
}
let size = data.len();
if size == 0 || size.is_power_of_two() {
return checksum;
}
let rom1_size = 1 << size.ilog2();
let rom2_size = size - rom1_size;
let full_size = rom1_size << 1;
let mirror_size = full_size - size;
let times = mirror_size / rom2_size;
for _ in 0..times {
for byte in &data[rom1_size..] {
checksum = checksum.wrapping_add(u16::from(*byte));
}
}
checksum
}
impl Cart {
pub fn from_io<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
io.read_sfc_header()
}
pub fn compute_checksum<T: Read + Seek>(&self, io: &mut T) -> Result<u16, Error> {
io.seek(SeekFrom::Start(0))?;
let mut data = Vec::new();
io.read_to_end(&mut data)?;
Ok(compute_sfc_checksum(&data))
}
}
fn trim(string: &str) -> &str {
string.trim_matches('\0').trim()
}
fn check_header_name(io: &mut (impl Read + Seek), location: u32) -> bool {
if io.seek(SeekFrom::Start((location + 0x10).into())).is_err() {
return false;
}
let mut header = [0u8; 21];
if io.read_exact(&mut header).is_err() {
return false;
}
if !header.is_ascii() {
return false;
}
let name = str::from_utf8(&header);
name.is_ok()
}
fn check_crc(io: &mut (impl Read + Seek), data: &[u8], location: u32) -> bool {
if io.seek(SeekFrom::Start((location + 0x2E).into())).is_err() {
return false;
}
let crc = io.read_u16::<LittleEndian>();
if crc.is_err() {
return false;
}
let crc = crc.unwrap();
crc == compute_sfc_checksum(data)
}
fn find_header(io: &mut (impl Read + Seek)) -> Result<u32, Error> {
io.seek(SeekFrom::Start(0))?;
let mut data = Vec::new();
io.read_to_end(&mut data)?;
let data = data;
let valid_locations = [0x0040_FFB0_u32, 0x0000_FFB0_u32, 0x0000_7FB0_u32];
for location in &valid_locations {
if check_crc(io, &data, *location) {
return Ok(*location);
}
}
for location in &valid_locations {
if check_header_name(io, *location) {
return Ok(*location);
}
}
Err(Error::Parse("failed to find header".into()))
}
pub trait CartRead {
fn read_sfc_header(&mut self) -> Result<Cart, Error>;
}
impl TryFrom<u8> for Mode {
type Error = Error;
fn try_from(data: u8) -> Result<Self, Self::Error> {
match data & 0xF {
0 => Ok(Self::Lorom),
1 => Ok(Self::Hirom),
2 => Ok(Self::SDD1), 3 => Ok(Self::SA1),
5 => Ok(Self::Exhirom),
_ => Err(Self::Error::Parse("failed to parse cart mode".into())),
}
}
}
impl From<u8> for Speed {
fn from(data: u8) -> Self {
if ((data >> 4) & 0x1) == 1 {
Self::Fast
} else {
Self::Slow
}
}
}
impl TryFrom<u8> for Chipset {
type Error = Error;
fn try_from(data: u8) -> Result<Self, Self::Error> {
match data & 0xF {
0x0 => Ok(Self::RomOnly),
0x1 => Ok(Self::RomRam),
0x2 => Ok(Self::RomRamBattery),
0x3 => Ok(Self::RomCoprocessor),
0x4 => Ok(Self::RomCoprocessorRam),
0x5 => Ok(Self::RomCoprocessorRamBattery),
0x6 => Ok(Self::RomCoprocessorBattery),
_ => Err(Self::Error::Parse("failed to parse cart chipset".into())),
}
}
}
impl TryFrom<u8> for Coprocessor {
type Error = Error;
fn try_from(data: u8) -> Result<Self, Self::Error> {
if data <= 2 {
return Ok(Self::None);
}
match data >> 4 {
0x0 => Ok(Self::DSP),
0x1 => Ok(Self::SuperFX),
0x2 => Ok(Self::OBC1),
0x3 => Ok(Self::SA1),
0x4 => Ok(Self::SDD1),
0x5 => Ok(Self::SRTC),
0xE => Ok(Self::Other),
0xF => Ok(Self::Custom),
_ => Err(Self::Error::Parse(
"failed to parse cart coprocessor".into(),
)),
}
}
}
impl TryFrom<u8> for Region {
type Error = Error;
fn try_from(data: u8) -> Result<Self, Self::Error> {
match data {
0x00 => Ok(Self::Japan),
0x01 => Ok(Self::USA),
0x02 => Ok(Self::Europe),
_ => Err(Self::Error::Parse("failed to parse cart region".into())),
}
}
}
impl TryFrom<&[u8]> for Vectors {
type Error = Error;
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
Ok(Self {
native: NativeVectors {
cop: (&data[4..6]).read_u16::<LittleEndian>()?,
brk: (&data[6..8]).read_u16::<LittleEndian>()?,
abort: (&data[8..0xA]).read_u16::<LittleEndian>()?,
nmi: (&data[0xA..0xC]).read_u16::<LittleEndian>()?,
irq: (&data[0xE..0x10]).read_u16::<LittleEndian>()?,
},
emulation: EmulationVectors {
cop: (&data[0x14..0x16]).read_u16::<LittleEndian>()?,
abort: (&data[0x18..0x1A]).read_u16::<LittleEndian>()?,
nmi: (&data[0x1A..0x1C]).read_u16::<LittleEndian>()?,
reset: (&data[0x1C..0x1E]).read_u16::<LittleEndian>()?,
irq_brk: (&data[0x1E..0x20]).read_u16::<LittleEndian>()?,
},
})
}
}
fn parse_version(data: u8) -> String {
format!("1.{data}")
}
impl<T: Read + Seek> CartRead for T {
fn read_sfc_header(&mut self) -> Result<Cart, Error> {
let location = find_header(self)?;
self.seek(SeekFrom::Start(location.into()))?;
let mut data = [0u8; 80];
self.read_exact(&mut data)?;
let dev_id = data[0x2A];
let expanded_header = if dev_id == 0x33 {
let maker_code = &data[0..2];
let game_code = &data[2..6];
let maker_code = if maker_code.is_ascii() {
str::from_utf8(maker_code)?.to_string()
} else {
return Err(Error::Parse("maker code is not ASCII".into()));
};
let game_code = if game_code.is_ascii() {
str::from_utf8(game_code)?.to_string()
} else {
return Err(Error::Parse("game code is not ASCII".into()));
};
Some(ExpandedHeader {
maker_code,
game_code,
expansion_flash_size: 1 << data[0xC],
expansion_ram_size: 1 << data[0xD],
special_version: data[0xE],
chipset_subtype: data[0xF],
})
} else {
None
};
if data[0x27] > 31 || data[0x28] > 31 {
return Err(Error::Parse("cart size too large".into()));
}
let rom_size: u32 = match data[0x27] {
0 => 0,
x => 1 << x,
};
let save_ram_size: u32 = match data[0x28] {
0 => 0,
x => 1 << x,
};
let title = &data[0x10..0x25];
let title = if title.is_ascii() {
trim(str::from_utf8(title)?).to_string()
} else {
return Err(Error::Parse("title is not ASCII".into()));
};
Ok(Cart {
title,
speed: data[0x25].into(),
mode: data[0x25].try_into()?,
chipset: data[0x26].try_into()?,
coprocessor: data[0x26].try_into()?,
rom_size,
save_ram_size,
region: data[0x29].try_into()?,
dev_id: data[0x2A],
version: parse_version(data[0x2B]),
checksum: (&data[0x2E..0x30]).read_u16::<LittleEndian>()?,
vectors: data[0x30..0x50].try_into()?,
expanded_header,
})
}
}
#[cfg(test)]
mod tests {
use crate::cart::Cart;
use crate::cart::CartRead;
use crate::cart::Chipset;
use crate::cart::Coprocessor;
use crate::cart::Mode;
use crate::cart::Region;
use crate::cart::Speed;
use std::env;
use std::fs::File;
use std::io::Cursor;
use std::io::Read;
use std::path::Path;
fn setup(filename: &str) -> Cursor<Vec<u8>> {
let root = env::var("CARGO_MANIFEST_DIR").unwrap();
let test_dir = Path::new(&root).join("resources/test");
let mut file = File::open(test_dir.join(filename)).unwrap();
let mut data = Vec::new();
file.read_to_end(&mut data).unwrap();
Cursor::new(data)
}
fn test_parse(data: &mut Cursor<Vec<u8>>) -> Cart {
let cart = data.read_sfc_header();
assert!(cart.is_ok());
cart.unwrap()
}
#[test]
fn test_lorom() {
let mut sfc = setup("lorom.sfc");
let cart = test_parse(&mut sfc);
assert!(cart.title == "LoROM Test");
assert!(matches!(cart.speed, Speed::Slow));
assert!(matches!(cart.mode, Mode::Lorom));
assert!(matches!(cart.chipset, Chipset::RomRam));
assert!(matches!(cart.coprocessor, Coprocessor::None));
assert!(cart.rom_size == 4096);
assert!(cart.save_ram_size == 32);
assert!(matches!(cart.region, Region::USA));
assert!(cart.dev_id == 0xFF);
assert!(cart.version == "1.3");
assert!(cart.checksum == cart.compute_checksum(&mut sfc).unwrap());
}
#[test]
fn test_hirom() {
let mut sfc = setup("hirom.sfc");
let cart = test_parse(&mut sfc);
assert!(cart.title == "HiROM Test");
assert!(matches!(cart.speed, Speed::Slow));
assert!(matches!(cart.mode, Mode::Lorom));
assert!(matches!(cart.chipset, Chipset::RomRam));
assert!(matches!(cart.coprocessor, Coprocessor::None));
assert!(cart.rom_size == 4096);
assert!(cart.save_ram_size == 32);
assert!(matches!(cart.region, Region::USA));
assert!(cart.dev_id == 0xFF);
assert!(cart.version == "1.3");
assert!(cart.checksum == cart.compute_checksum(&mut sfc).unwrap());
}
#[test]
fn test_exhirom() {
let mut sfc = setup("exhirom.sfc");
let cart = test_parse(&mut sfc);
assert!(cart.title == "ExHiROM Test");
assert!(matches!(cart.speed, Speed::Slow));
assert!(matches!(cart.mode, Mode::Lorom));
assert!(matches!(cart.chipset, Chipset::RomRam));
assert!(matches!(cart.coprocessor, Coprocessor::None));
assert!(cart.rom_size == 4096);
assert!(cart.save_ram_size == 32);
assert!(matches!(cart.region, Region::USA));
assert!(cart.dev_id == 0xFF);
assert!(cart.version == "1.3");
assert!(cart.checksum == cart.compute_checksum(&mut sfc).unwrap());
}
}