use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use crate::core::error::Error;
use crate::core::space::{RamStore, RomStore};
const MAGIC: [u8; 4] = *b"NES\x1a";
const HEADER_LEN: u64 = 16;
const TRAINER_LEN: u64 = 512;
const PRG_UNIT: u64 = 16 * 1024;
const CHR_UNIT: u64 = 8 * 1024;
const DEFAULT_8K: u64 = 8 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RomPart {
Header,
Trainer,
PrgRom,
ChrRom,
}
impl RomPart {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
RomPart::Header => "header",
RomPart::Trainer => "trainer",
RomPart::PrgRom => "PRG ROM",
RomPart::ChrRom => "CHR ROM",
}
}
}
impl fmt::Display for RomPart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RomError {
HeaderTooShort {
have: u64,
},
BadMagic {
found: [u8; 4],
},
NoPrgRom,
SizeOverflow {
what: RomPart,
exponent: u8,
multiplier: u8,
},
Truncated {
what: RomPart,
offset: u64,
need: u64,
have: u64,
},
TooLargeForHost {
what: RomPart,
len: u64,
},
}
impl fmt::Display for RomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RomError::HeaderTooShort { have } => write!(
f,
"not an iNES image: {have} byte(s), which is shorter than the {HEADER_LEN}-byte header"
),
RomError::BadMagic { found } => write!(
f,
"not an iNES image: starts with {:02x?}, not {:02x?}",
found, MAGIC
),
RomError::NoPrgRom => f.write_str("the header describes 0 bytes of PRG ROM"),
RomError::SizeOverflow {
what,
exponent,
multiplier,
} => write!(
f,
"{what} size 2^{exponent} * {} overflows a 64-bit byte count",
multiplier * 2 + 1
),
RomError::Truncated {
what,
offset,
need,
have,
} => write!(
f,
"truncated image: {what} needs {need} byte(s) at offset {offset}, \
but the file is {have} byte(s)"
),
RomError::TooLargeForHost { what, len } => write!(
f,
"{what} is {len} byte(s), which does not fit in this host's address space"
),
}
}
}
impl From<RomError> for Error {
fn from(e: RomError) -> Error {
use alloc::string::ToString;
Error::Config {
at: String::from("ines"),
message: e.to_string(),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for RomError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Mirroring {
Horizontal,
Vertical,
SingleScreenLower,
SingleScreenUpper,
FourScreen,
}
impl Mirroring {
#[must_use]
pub const fn banks(self) -> [u8; 4] {
match self {
Mirroring::Horizontal => [0, 0, 1, 1],
Mirroring::Vertical => [0, 1, 0, 1],
Mirroring::SingleScreenLower => [0, 0, 0, 0],
Mirroring::SingleScreenUpper => [1, 1, 1, 1],
Mirroring::FourScreen => [0, 1, 2, 3],
}
}
#[must_use]
pub const fn needs_cartridge_vram(self) -> bool {
matches!(self, Mirroring::FourScreen)
}
}
impl fmt::Display for Mirroring {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Mirroring::Horizontal => "horizontal",
Mirroring::Vertical => "vertical",
Mirroring::SingleScreenLower => "single-screen (lower)",
Mirroring::SingleScreenUpper => "single-screen (upper)",
Mirroring::FourScreen => "four-screen",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HeaderFormat {
Ines,
Nes2,
}
impl fmt::Display for HeaderFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
HeaderFormat::Ines => "iNES",
HeaderFormat::Nes2 => "NES 2.0",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConsoleKind {
Nes,
VsSystem,
Playchoice10,
Extended,
}
impl fmt::Display for ConsoleKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
ConsoleKind::Nes => "NES/Famicom",
ConsoleKind::VsSystem => "Vs. System",
ConsoleKind::Playchoice10 => "PlayChoice-10",
ConsoleKind::Extended => "extended console",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimingMode {
Ntsc,
Pal,
MultiRegion,
Dendy,
}
impl fmt::Display for TimingMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
TimingMode::Ntsc => "NTSC",
TimingMode::Pal => "PAL",
TimingMode::MultiRegion => "multi-region",
TimingMode::Dendy => "Dendy",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct InesHeader {
pub format: HeaderFormat,
pub mapper: u16,
pub submapper: u8,
pub prg_rom_len: u64,
pub chr_rom_len: u64,
pub prg_ram_len: u64,
pub prg_nvram_len: u64,
pub chr_ram_len: u64,
pub chr_nvram_len: u64,
pub mirroring: Mirroring,
pub battery: bool,
pub trainer: bool,
pub console: ConsoleKind,
pub timing: TimingMode,
pub vs_ppu: u8,
pub vs_hardware: u8,
pub extended_console: u8,
pub misc_roms: u8,
pub expansion_device: u8,
pub archaic: bool,
pub trainer_offset: u64,
pub prg_rom_offset: u64,
pub chr_rom_offset: u64,
pub image_len: u64,
}
impl InesHeader {
pub fn parse(bytes: &[u8]) -> Result<InesHeader, RomError> {
let head: [u8; 16] = match bytes.get(..16) {
Some(s) => match <[u8; 16]>::try_from(s) {
Ok(h) => h,
Err(_) => {
return Err(RomError::HeaderTooShort {
have: bytes.len() as u64,
});
}
},
None => {
return Err(RomError::HeaderTooShort {
have: bytes.len() as u64,
});
}
};
let magic = [head[0], head[1], head[2], head[3]];
if magic != MAGIC {
return Err(RomError::BadMagic { found: magic });
}
let flags6 = head[6];
let flags7 = head[7];
let format = if flags7 & 0x0c == 0x08 {
HeaderFormat::Nes2
} else {
HeaderFormat::Ines
};
let nes2 = format == HeaderFormat::Nes2;
let archaic = !nes2 && head[12..16].iter().any(|&b| b != 0);
let mut mapper = u16::from(flags6 >> 4);
if !archaic {
mapper |= u16::from(flags7 & 0xf0);
}
if nes2 {
mapper |= u16::from(head[8] & 0x0f) << 8;
}
let mirroring = if flags6 & 0x08 != 0 {
Mirroring::FourScreen
} else if flags6 & 0x01 != 0 {
Mirroring::Vertical
} else {
Mirroring::Horizontal
};
let (prg_rom_len, chr_rom_len) = if nes2 {
(
rom_size(head[4], head[9] & 0x0f, PRG_UNIT, RomPart::PrgRom)?,
rom_size(head[5], head[9] >> 4, CHR_UNIT, RomPart::ChrRom)?,
)
} else {
(u64::from(head[4]) * PRG_UNIT, u64::from(head[5]) * CHR_UNIT)
};
if prg_rom_len == 0 {
return Err(RomError::NoPrgRom);
}
let battery = flags6 & 0x02 != 0;
let (prg_ram_len, prg_nvram_len, chr_ram_len, chr_nvram_len) = if nes2 {
(
shift_size(head[10] & 0x0f),
shift_size(head[10] >> 4),
shift_size(head[11] & 0x0f),
shift_size(head[11] >> 4),
)
} else {
let units = if archaic {
1
} else {
u64::from(head[8]).max(1)
};
let chr_ram = if chr_rom_len == 0 { DEFAULT_8K } else { 0 };
(units * DEFAULT_8K, 0, chr_ram, 0)
};
let console = if archaic {
ConsoleKind::Nes
} else {
match flags7 & 0x03 {
0 => ConsoleKind::Nes,
1 => ConsoleKind::VsSystem,
2 => ConsoleKind::Playchoice10,
_ => ConsoleKind::Extended,
}
};
let timing = if nes2 {
match head[12] & 0x03 {
0 => TimingMode::Ntsc,
1 => TimingMode::Pal,
2 => TimingMode::MultiRegion,
_ => TimingMode::Dendy,
}
} else if !archaic && head[9] & 0x01 != 0 {
TimingMode::Pal
} else {
TimingMode::Ntsc
};
let (vs_ppu, vs_hardware) = if nes2 && console == ConsoleKind::VsSystem {
(head[13] & 0x0f, head[13] >> 4)
} else {
(0, 0)
};
let extended_console = if nes2 && console == ConsoleKind::Extended {
head[13] & 0x0f
} else {
0
};
let trainer = flags6 & 0x04 != 0;
let trainer_offset = HEADER_LEN;
let prg_rom_offset = HEADER_LEN + if trainer { TRAINER_LEN } else { 0 };
let chr_rom_offset =
prg_rom_offset
.checked_add(prg_rom_len)
.ok_or(RomError::Truncated {
what: RomPart::PrgRom,
offset: prg_rom_offset,
need: prg_rom_len,
have: bytes.len() as u64,
})?;
let image_len = chr_rom_offset
.checked_add(chr_rom_len)
.ok_or(RomError::Truncated {
what: RomPart::ChrRom,
offset: chr_rom_offset,
need: chr_rom_len,
have: bytes.len() as u64,
})?;
Ok(InesHeader {
format,
mapper,
submapper: if nes2 { head[8] >> 4 } else { 0 },
prg_rom_len,
chr_rom_len,
prg_ram_len,
prg_nvram_len,
chr_ram_len,
chr_nvram_len,
mirroring,
battery,
trainer,
console,
timing,
vs_ppu,
vs_hardware,
extended_console,
misc_roms: if nes2 { head[14] & 0x03 } else { 0 },
expansion_device: if nes2 { head[15] & 0x3f } else { 0 },
archaic,
trainer_offset,
prg_rom_offset,
chr_rom_offset,
image_len,
})
}
#[must_use]
pub const fn work_ram_len(&self) -> u64 {
self.prg_ram_len + self.prg_nvram_len
}
#[must_use]
pub const fn chr_ram_total(&self) -> u64 {
self.chr_ram_len + self.chr_nvram_len
}
}
fn rom_size(lsb: u8, msb: u8, unit: u64, what: RomPart) -> Result<u64, RomError> {
if msb == 0x0f {
let exponent = lsb >> 2;
let multiplier = lsb & 0x03;
1u64.checked_shl(u32::from(exponent))
.and_then(|base| base.checked_mul(u64::from(multiplier) * 2 + 1))
.ok_or(RomError::SizeOverflow {
what,
exponent,
multiplier,
})
} else {
Ok(((u64::from(msb) << 8) | u64::from(lsb)) * unit)
}
}
const fn shift_size(shift: u8) -> u64 {
if shift == 0 {
0
} else {
64u64 << shift
}
}
#[derive(Debug, Clone)]
pub enum Chr {
Rom(Arc<RomStore>),
Ram(Arc<RamStore>),
}
impl Chr {
#[must_use]
pub fn len(&self) -> u64 {
match self {
Chr::Rom(r) => r.len(),
Chr::Ram(r) => r.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub const fn is_ram(&self) -> bool {
matches!(self, Chr::Ram(_))
}
#[must_use]
pub const fn as_ram(&self) -> Option<&Arc<RamStore>> {
match self {
Chr::Ram(r) => Some(r),
Chr::Rom(_) => None,
}
}
#[must_use]
pub const fn as_rom(&self) -> Option<&Arc<RomStore>> {
match self {
Chr::Rom(r) => Some(r),
Chr::Ram(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Cartridge {
header: InesHeader,
prg_rom: Arc<RomStore>,
chr: Chr,
work_ram: Option<Arc<RamStore>>,
trainer: Option<Arc<Vec<u8>>>,
}
impl Cartridge {
pub fn from_ines(bytes: &[u8]) -> Result<Cartridge, RomError> {
let header = InesHeader::parse(bytes)?;
let trainer = if header.trainer {
let slice = slice_of(bytes, header.trainer_offset, TRAINER_LEN, RomPart::Trainer)?;
Some(Arc::new(slice.to_vec()))
} else {
None
};
let prg = slice_of(
bytes,
header.prg_rom_offset,
header.prg_rom_len,
RomPart::PrgRom,
)?;
let prg_rom = Arc::new(RomStore::new(prg.to_vec()));
let chr = if header.chr_rom_len > 0 {
let bytes = slice_of(
bytes,
header.chr_rom_offset,
header.chr_rom_len,
RomPart::ChrRom,
)?;
Chr::Rom(Arc::new(RomStore::new(bytes.to_vec())))
} else {
Chr::Ram(Arc::new(RamStore::new(header.chr_ram_total())))
};
let work_ram = match header.work_ram_len() {
0 => None,
len => Some(Arc::new(RamStore::new(len))),
};
Ok(Cartridge {
header,
prg_rom,
chr,
work_ram,
trainer,
})
}
#[must_use]
pub const fn header(&self) -> &InesHeader {
&self.header
}
#[must_use]
pub const fn mapper(&self) -> u16 {
self.header.mapper
}
#[must_use]
pub const fn mirroring(&self) -> Mirroring {
self.header.mirroring
}
#[must_use]
pub const fn battery(&self) -> bool {
self.header.battery
}
#[must_use]
pub const fn prg_rom(&self) -> &Arc<RomStore> {
&self.prg_rom
}
#[must_use]
pub const fn chr(&self) -> &Chr {
&self.chr
}
#[must_use]
pub const fn work_ram(&self) -> Option<&Arc<RamStore>> {
self.work_ram.as_ref()
}
#[must_use]
pub fn trainer(&self) -> Option<&[u8]> {
self.trainer.as_ref().map(|t| t.as_slice())
}
}
fn slice_of(bytes: &[u8], offset: u64, len: u64, what: RomPart) -> Result<&[u8], RomError> {
let have = bytes.len() as u64;
let truncated = || RomError::Truncated {
what,
offset,
need: len,
have,
};
let end = offset.checked_add(len).ok_or_else(truncated)?;
if end > have {
return Err(truncated());
}
let start = usize::try_from(offset).map_err(|_| RomError::TooLargeForHost { what, len })?;
let end = usize::try_from(end).map_err(|_| RomError::TooLargeForHost { what, len })?;
bytes.get(start..end).ok_or_else(truncated)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn ines1(prg_units: u8, chr_units: u8, flags6: u8, flags7: u8) -> Vec<u8> {
let mut v = vec![0u8; 16];
v[..4].copy_from_slice(&MAGIC);
v[4] = prg_units;
v[5] = chr_units;
v[6] = flags6;
v[7] = flags7;
if flags6 & 0x04 != 0 {
v.extend(core::iter::repeat_n(0xaa, TRAINER_LEN as usize));
}
v.extend(core::iter::repeat_n(0x5a, usize::from(prg_units) * 16384));
v.extend(core::iter::repeat_n(0xa5, usize::from(chr_units) * 8192));
v
}
fn reject(bytes: &[u8]) -> RomError {
Cartridge::from_ines(bytes).expect_err("must be rejected")
}
fn nes2_header(bytes: [u8; 16]) -> [u8; 16] {
let mut h = bytes;
h[..4].copy_from_slice(&MAGIC);
h[7] = (h[7] & 0xf3) | 0x08;
h
}
#[test]
fn a_minimal_ines1_image_parses() {
let img = ines1(1, 1, 0, 0);
let cart = Cartridge::from_ines(&img).expect("valid image");
let h = cart.header();
assert_eq!(h.format, HeaderFormat::Ines);
assert_eq!(h.mapper, 0);
assert_eq!(h.prg_rom_len, 16384);
assert_eq!(h.chr_rom_len, 8192);
assert_eq!(h.mirroring, Mirroring::Horizontal);
assert!(!h.battery);
assert!(!h.trainer);
assert_eq!(h.console, ConsoleKind::Nes);
assert_eq!(h.timing, TimingMode::Ntsc);
assert_eq!(h.image_len, 16 + 16384 + 8192);
assert_eq!(h.work_ram_len(), 8192);
assert_eq!(cart.chr().len(), 8192);
assert!(!cart.chr().is_ram());
}
#[test]
fn chr_size_zero_means_the_board_carries_chr_ram() {
let img = ines1(1, 0, 0, 0);
let cart = Cartridge::from_ines(&img).expect("valid image");
assert_eq!(cart.header().chr_rom_len, 0);
assert_eq!(cart.header().chr_ram_total(), 8192);
assert!(cart.chr().is_ram());
assert_eq!(cart.chr().len(), 8192);
}
#[test]
fn the_mapper_number_comes_from_both_nibbles() {
let img = ines1(1, 0, 0xa0, 0xb0);
let cart = Cartridge::from_ines(&img).expect("valid image");
assert_eq!(cart.mapper(), 0xba);
assert!(!cart.header().archaic);
}
#[test]
fn an_archaic_header_ignores_byte_sevens_mapper_nibble() {
let mut img = ines1(1, 0, 0xa0, 0x40);
img[12] = b'D';
img[13] = b'i';
img[14] = b'z';
img[15] = b'!';
let cart = Cartridge::from_ines(&img).expect("valid image");
assert!(cart.header().archaic);
assert_eq!(cart.mapper(), 0x0a, "byte 7 must not contribute");
assert_eq!(cart.header().work_ram_len(), 8192);
}
#[test]
fn battery_and_trainer_flags_move_the_prg_offset() {
let img = ines1(1, 0, 0x02 | 0x04, 0);
let cart = Cartridge::from_ines(&img).expect("valid image");
assert!(cart.battery());
assert!(cart.header().trainer);
assert_eq!(cart.header().prg_rom_offset, 16 + 512);
assert_eq!(cart.trainer().map(<[u8]>::len), Some(512));
assert_eq!(cart.trainer().and_then(|t| t.first()).copied(), Some(0xaa));
let mut prg = [0u8; 4];
cart.prg_rom().read_at(0, &mut prg).expect("in range");
assert_eq!(prg, [0x5a; 4]);
}
#[test]
fn the_four_mirroring_arrangements() {
for (flags6, want) in [
(0x00, Mirroring::Horizontal),
(0x01, Mirroring::Vertical),
(0x08, Mirroring::FourScreen),
(0x09, Mirroring::FourScreen),
] {
let img = ines1(1, 0, flags6, 0);
let cart = Cartridge::from_ines(&img).expect("valid image");
assert_eq!(cart.mirroring(), want, "flags6 = {flags6:#04x}");
}
}
#[test]
fn mirroring_banks_are_the_ciram_a10_wiring() {
assert_eq!(Mirroring::Horizontal.banks(), [0, 0, 1, 1]);
assert_eq!(Mirroring::Vertical.banks(), [0, 1, 0, 1]);
assert_eq!(Mirroring::SingleScreenLower.banks(), [0, 0, 0, 0]);
assert_eq!(Mirroring::SingleScreenUpper.banks(), [1, 1, 1, 1]);
assert_eq!(Mirroring::FourScreen.banks(), [0, 1, 2, 3]);
assert!(Mirroring::FourScreen.needs_cartridge_vram());
assert!(!Mirroring::Vertical.needs_cartridge_vram());
}
#[test]
fn ines1_reports_pal_from_byte_nine() {
let mut img = ines1(1, 0, 0, 0);
img[9] = 0x01;
assert_eq!(
Cartridge::from_ines(&img).expect("valid").header().timing,
TimingMode::Pal
);
}
#[test]
fn nes2_is_detected_and_widens_the_mapper() {
let mut h = nes2_header([0; 16]);
h[4] = 2; h[6] = 0x10; h[7] |= 0x20; h[8] = 0x53; let mut img = h.to_vec();
img.extend(core::iter::repeat_n(0u8, 32768));
let cart = Cartridge::from_ines(&img).expect("valid image");
assert_eq!(cart.header().format, HeaderFormat::Nes2);
assert_eq!(cart.mapper(), 0x321);
assert_eq!(cart.header().submapper, 5);
assert!(!cart.header().archaic, "NES 2.0 defines bytes 12-15");
}
#[test]
fn nes2_takes_the_high_size_bits_from_byte_nine() {
let mut h = nes2_header([0; 16]);
h[4] = 0x02;
h[5] = 0x01;
h[9] = 0x00;
let mut img = h.to_vec();
img.extend(core::iter::repeat_n(0u8, 32768 + 8192));
let cart = Cartridge::from_ines(&img).expect("valid image");
assert_eq!(cart.header().prg_rom_len, 32768);
assert_eq!(cart.header().chr_rom_len, 8192);
let mut h = nes2_header([0; 16]);
h[4] = 0x00;
h[9] = 0x01; let header = InesHeader::parse(&h).expect("header alone parses");
assert_eq!(header.prg_rom_len, 0x100 * 16384);
}
#[test]
fn nes2_exponent_sizes_decode() {
let mut h = nes2_header([0; 16]);
h[4] = (10 << 2) | 1; h[9] = 0x0f;
let header = InesHeader::parse(&h).expect("valid header");
assert_eq!(header.prg_rom_len, 3072);
let mut h = nes2_header([0; 16]);
h[4] = 1; h[5] = 12 << 2; h[9] = 0xf0;
let header = InesHeader::parse(&h).expect("valid header");
assert_eq!(header.chr_rom_len, 4096);
}
#[test]
fn an_exponent_size_that_overflows_is_rejected() {
let mut h = nes2_header([0; 16]);
h[4] = (63 << 2) | 3; h[9] = 0x0f;
assert_eq!(
InesHeader::parse(&h),
Err(RomError::SizeOverflow {
what: RomPart::PrgRom,
exponent: 63,
multiplier: 3,
})
);
}
#[test]
fn nes2_ram_shift_counts_decode() {
let mut h = nes2_header([0; 16]);
h[4] = 1;
h[10] = 0x70; h[11] = 0x07; let header = InesHeader::parse(&h).expect("valid header");
assert_eq!(header.prg_ram_len, 0);
assert_eq!(header.prg_nvram_len, 8192);
assert_eq!(header.work_ram_len(), 8192);
assert_eq!(header.chr_ram_len, 8192);
assert_eq!(header.chr_nvram_len, 0);
let mut h = nes2_header([0; 16]);
h[4] = 1;
let header = InesHeader::parse(&h).expect("valid header");
assert_eq!(header.work_ram_len(), 0);
assert_eq!(header.chr_ram_total(), 0);
}
#[test]
fn nes2_extended_fields_decode() {
let mut h = nes2_header([0; 16]);
h[4] = 1;
h[7] |= 0x01; h[12] = 0x03; h[13] = 0x42; h[14] = 0x02; h[15] = 0x2f; let header = InesHeader::parse(&h).expect("valid header");
assert_eq!(header.console, ConsoleKind::VsSystem);
assert_eq!(header.timing, TimingMode::Dendy);
assert_eq!(header.vs_ppu, 2);
assert_eq!(header.vs_hardware, 4);
assert_eq!(header.extended_console, 0);
assert_eq!(header.misc_roms, 2);
assert_eq!(header.expansion_device, 0x2f);
let mut h = nes2_header([0; 16]);
h[4] = 1;
h[7] |= 0x03; h[13] = 0x09;
let header = InesHeader::parse(&h).expect("valid header");
assert_eq!(header.console, ConsoleKind::Extended);
assert_eq!(header.extended_console, 9);
assert_eq!(header.vs_ppu, 0);
}
#[test]
fn a_file_shorter_than_the_header_is_rejected_at_every_length() {
let img = ines1(1, 0, 0, 0);
for n in 0..16 {
assert_eq!(
reject(&img[..n]),
RomError::HeaderTooShort { have: n as u64 },
"length {n}"
);
}
}
#[test]
fn bad_magic_is_rejected() {
let mut img = ines1(1, 0, 0, 0);
img[3] = 0x1b;
assert_eq!(
reject(&img),
RomError::BadMagic {
found: [b'N', b'E', b'S', 0x1b]
}
);
let zeros = vec![0u8; 64];
assert!(matches!(reject(&zeros), RomError::BadMagic { .. }));
}
#[test]
fn a_header_with_no_prg_rom_is_rejected() {
let img = ines1(0, 1, 0, 0);
assert_eq!(reject(&img), RomError::NoPrgRom);
}
#[test]
fn a_truncated_body_is_rejected_with_the_numbers() {
let img = ines1(2, 1, 0, 0);
let short = &img[..16 + 32768 + 4096];
assert_eq!(
reject(short),
RomError::Truncated {
what: RomPart::ChrRom,
offset: 16 + 32768,
need: 8192,
have: 16 + 32768 + 4096,
}
);
let short = &img[..16 + 1000];
assert_eq!(
reject(short),
RomError::Truncated {
what: RomPart::PrgRom,
offset: 16,
need: 32768,
have: 16 + 1000,
}
);
}
#[test]
fn a_missing_trainer_is_named_as_the_missing_part() {
let img = ines1(1, 0, 0x04, 0);
let short = &img[..16 + 100];
assert_eq!(
reject(short),
RomError::Truncated {
what: RomPart::Trainer,
offset: 16,
need: 512,
have: 16 + 100,
}
);
}
#[test]
fn an_enormous_nes2_size_is_truncated_not_an_allocation() {
let mut h = nes2_header([0; 16]);
h[4] = 0xff;
h[9] = 0x0e; let err = Cartridge::from_ines(&h).expect_err("cannot fit");
assert!(matches!(
err,
RomError::Truncated {
what: RomPart::PrgRom,
..
}
));
}
#[test]
fn trailing_bytes_are_allowed() {
let mut img = ines1(1, 1, 0, 0);
let accounted = img.len() as u64;
img.extend(core::iter::repeat_n(0xcc, 8192));
let cart = Cartridge::from_ines(&img).expect("valid image");
assert_eq!(cart.header().image_len, accounted);
}
#[test]
fn errors_say_something_useful() {
let text = alloc::format!("{}", RomError::HeaderTooShort { have: 3 });
assert!(text.contains('3'), "{text}");
let text = alloc::format!(
"{}",
RomError::Truncated {
what: RomPart::ChrRom,
offset: 16,
need: 8192,
have: 20
}
);
assert!(text.contains("CHR ROM"), "{text}");
assert!(text.contains("8192"), "{text}");
let e: Error = RomError::NoPrgRom.into();
assert!(alloc::format!("{e}").contains("PRG ROM"));
}
#[test]
fn no_truncation_of_a_good_image_panics() {
for flags6 in [0x00u8, 0x01, 0x04, 0x08, 0x0f] {
let img = ines1(2, 1, flags6, 0);
for n in 0..=img.len() {
let _ = Cartridge::from_ines(&img[..n]);
}
}
}
#[test]
fn no_header_bit_pattern_panics() {
let base = ines1(1, 1, 0, 0);
for byte in 0..16usize {
for value in 0..=255u8 {
let mut img = base.clone();
img[byte] = value;
let _ = Cartridge::from_ines(&img);
let _ = InesHeader::parse(&img);
}
}
}
#[test]
fn garbage_does_not_panic() {
let mut state = 0x2545_f491_4f6c_dd1du64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for len in [0usize, 1, 4, 15, 16, 17, 528, 529, 1024] {
for _ in 0..64 {
let mut buf = vec![0u8; len];
for b in &mut buf {
*b = next() as u8;
}
let _ = Cartridge::from_ines(&buf);
if buf.len() >= 4 {
buf[..4].copy_from_slice(&MAGIC);
let _ = Cartridge::from_ines(&buf);
}
}
}
}
#[cfg(feature = "std")]
#[test]
fn a_real_cartridge_parses() {
let Ok(path) = std::env::var("RSEMU_NES_TEST_ROM") else {
return;
};
let bytes = std::fs::read(&path).expect("RSEMU_NES_TEST_ROM is readable");
let cart = Cartridge::from_ines(&bytes).expect("a real image parses");
let h = cart.header();
assert_eq!(h.format, HeaderFormat::Ines);
assert_eq!(h.mapper, 0, "AccuracyCoin is an NROM cart");
assert_eq!(h.prg_rom_len, 32768);
assert_eq!(h.chr_rom_len, 8192);
assert_eq!(h.mirroring, Mirroring::Vertical);
assert!(!h.battery);
assert!(!h.trainer);
assert_eq!(h.image_len, 16 + 32768 + 8192);
assert_eq!(h.image_len, bytes.len() as u64);
}
}