aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock13
-rw-r--r--Cargo.toml3
-rw-r--r--README.md7
-rw-r--r--crates/nasm/src/main.rs2
-rw-r--r--crates/readrom/Cargo.toml12
-rw-r--r--crates/readrom/src/main.rs55
-rw-r--r--lib/header/Cargo.toml9
-rw-r--r--lib/header/src/lib.rs332
8 files changed, 431 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e86a0da..5002d34 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -104,6 +104,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
+name = "header"
+version = "0.1.0"
+
+[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -163,6 +167,15 @@ dependencies = [
]
[[package]]
+name = "readrom"
+version = "1.0.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "header",
+]
+
+[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 552c48f..9a387eb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,10 +6,11 @@ resolver = "2"
rust-version = "1.82"
edition = "2021"
license = "GPLv3+"
-authors = ["Miquel Sabaté Solà"]
+authors = ["Miquel Sabaté Solà <mikisabate@gmail.com>"]
[workspace.dependencies]
xixanta = { path = "./lib/xixanta", version = "0.1.0" }
+header = { path = "./lib/header", version = "0.1.0" }
[profile.release]
codegen-units = 1
diff --git a/README.md b/README.md
index 4a74a66..6d99cd4 100644
--- a/README.md
+++ b/README.md
@@ -19,3 +19,10 @@ changed with the `-c/--configuration` flag, which accepts the following values:
`CODE`, `VECTORS`, and `CHARS`.
- `nrom65`: same as `nrom` but it also has `STARTUP` for compatibility with the
default linker configuration from [cc65](https://github.com/cc65/cc65).
+
+## `readrom`
+
+The `readrom` reads a given ROM file and shows all the information that can be
+gathered from it. For now this only applies to information on the header, but in
+the future we might want to add disassembling user-specified segments, for
+example.
diff --git a/crates/nasm/src/main.rs b/crates/nasm/src/main.rs
index 9ad710b..105a995 100644
--- a/crates/nasm/src/main.rs
+++ b/crates/nasm/src/main.rs
@@ -5,7 +5,7 @@ use std::io::{self, Read, Write};
use xixanta::assembler::Assembler;
use xixanta::mapping::{Mapping, EMPTY, NROM, NROM65};
-/// Assembler for the 6502 microprocessor that targets the NES.
+/// Assembler for the 6502 microprocessor that targets the NES/Famicom.
#[derive(ClapParser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
diff --git a/crates/readrom/Cargo.toml b/crates/readrom/Cargo.toml
new file mode 100644
index 0000000..98a5106
--- /dev/null
+++ b/crates/readrom/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "readrom"
+version = "1.0.0"
+rust-version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+
+[dependencies]
+anyhow = "^1"
+clap = { version = "^4", features = ["derive"] }
+header.workspace = true
diff --git a/crates/readrom/src/main.rs b/crates/readrom/src/main.rs
new file mode 100644
index 0000000..56e60e0
--- /dev/null
+++ b/crates/readrom/src/main.rs
@@ -0,0 +1,55 @@
+use anyhow::{bail, Result};
+use clap::Parser as ClapParser;
+use header::{Header, Kind};
+use std::fs::File;
+use std::io::{ErrorKind, Read};
+
+/// Display information about NES/Famicom ROM files.
+#[derive(ClapParser, Debug)]
+#[command(version, about, long_about = None)]
+struct Args {
+ /// NES/Famicom ROM file from which to display information.
+ file: String,
+}
+
+fn print_header(header: &Header) {
+ println!("Header:");
+
+ match header.kind {
+ Kind::INes => println!(" Kind:\t\tiNES"),
+ Kind::Nes20 => println!(" Kind:\t\tNES 2.0"),
+ }
+
+ println!(
+ " PRG ROM size:\t{} bytes ({}KB)",
+ header.prg_rom_size * 16 * 1024,
+ header.prg_rom_size * 16
+ );
+ println!(
+ " CHR ROM size:\t{} bytes ({}KB)",
+ header.chr_rom_size * 8 * 1024,
+ header.chr_rom_size * 8
+ );
+ println!(" Mapper:\t{}", header.mapper);
+}
+
+fn main() -> Result<()> {
+ let args = Args::parse();
+ let mut input = File::open(args.file)?;
+
+ let mut buf = vec![0u8; 0x10];
+ if let Err(e) = input.read_exact(&mut buf) {
+ match e.kind() {
+ ErrorKind::UnexpectedEof => bail!("malformed ROM file."),
+ _ => return Err(e.into()),
+ }
+ }
+
+ let header = match Header::try_from(buf.as_slice()) {
+ Ok(h) => h,
+ Err(e) => bail!("{}.", e),
+ };
+ print_header(&header);
+
+ Ok(())
+}
diff --git a/lib/header/Cargo.toml b/lib/header/Cargo.toml
new file mode 100644
index 0000000..56eb36c
--- /dev/null
+++ b/lib/header/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "header"
+version = "0.1.0"
+rust-version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+
+[dependencies]
diff --git a/lib/header/src/lib.rs b/lib/header/src/lib.rs
new file mode 100644
index 0000000..f7e2758
--- /dev/null
+++ b/lib/header/src/lib.rs
@@ -0,0 +1,332 @@
+/// ROM header formats.
+#[derive(Debug)]
+pub enum Kind {
+ /// Historical header used since the iNES emulator.
+ INes,
+
+ /// Modern header which is backwards-compatible in regards to iNES. Using
+ /// this header is helpful to disambiguate certain mappers or to be more
+ /// specific about some of them (e.g. submappers inside of MMC3).
+ Nes20,
+}
+
+/// Nametable arrangement as defined by the header. Typically it's either
+/// Vertical or Horizontal, since this library does not support "alternative"
+/// types. Hence, an "Unknown" value is most probably either a defect on the ROM
+/// file, or a weird alternative arrangement what we don't care.
+#[derive(Debug)]
+pub enum NameTableArrangement {
+ Vertical,
+ Horizontal,
+ Unknown,
+}
+
+/// Memory mappers. Note that not all of them are listed here, but they will be
+/// added with time (and if I even care).
+#[derive(Debug)]
+pub enum Mapper {
+ Axrom,
+ BnromCombo,
+ BnromOnly,
+ Cnrom,
+ Mmc1,
+ Mmc2,
+ Mmc3Acc,
+ Mmc3c,
+ Mmc3Nec,
+ Mmc3Sharp,
+ Mmc3T9552,
+ Mmc4,
+ Mmc5,
+ Mmc6,
+ Nina001,
+ Nrom,
+ Unknown,
+ Unrom512,
+ Uxrom,
+}
+
+impl std::fmt::Display for Mapper {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Mapper::Axrom => write!(f, "AxROM"),
+ Mapper::BnromCombo => write!(f, "BNROM / NINA-001"),
+ Mapper::BnromOnly => write!(f, "BNROM"),
+ Mapper::Cnrom => write!(f, "CNROM"),
+ Mapper::Mmc1 => write!(f, "MMC1"),
+ Mapper::Mmc2 => write!(f, "MMC2"),
+ Mapper::Mmc3Acc => write!(f, "MC-ACC"),
+ Mapper::Mmc3c => write!(f, "MMC3C"),
+ Mapper::Mmc3Nec => write!(f, "MMC3 (NEC)"),
+ Mapper::Mmc3Sharp => write!(f, "MMC3 (Sharp)"),
+ Mapper::Mmc3T9552 => write!(f, "MMC3 (variant with a T9552 scrambling chip)"),
+ Mapper::Mmc4 => write!(f, "MMC4"),
+ Mapper::Mmc5 => write!(f, "MMC5"),
+ Mapper::Mmc6 => write!(f, "MMC6"),
+ Mapper::Nina001 => write!(f, "NINA 001"),
+ Mapper::Nrom => write!(f, "NROM"),
+ Mapper::Unknown => write!(f, "unknown"),
+ Mapper::Unrom512 => write!(f, "UNROM 512"),
+ Mapper::Uxrom => write!(
+ f,
+ "UxROM (NES-UNROM, NES-UOROM, HVC-UN1ROM their HVC counterparts, and clone boards)"
+ ),
+ }
+ }
+}
+
+/// Information that has been parsed from an NES/Famicom ROM header. The
+/// `Header` struct implements the `TryFrom` trait. Hence, use
+/// `Header::try_from` in order to parse a header.
+#[derive(Debug)]
+pub struct Header {
+ /// PRG ROM size in units of 16KB. That is, a "1" here actually means
+ /// "16KB". This is in accordance to the header itself, but callers that
+ /// need to display this information should perform the translation to be
+ /// more useful to the human eye.
+ pub prg_rom_size: usize,
+
+ /// CHR ROM size in units of 8KB. That is, a "1" here actually means "8KB".
+ /// This is in accordance to the header itself, but callers that need to
+ /// display this information should perform the translation to be more
+ /// useful to the human eye.
+ pub chr_rom_size: usize,
+
+ /// Nametable arrangement.
+ pub nametable_arrangement: NameTableArrangement,
+
+ /// Whether the memory region $6000-$7FFF is persistent or not (e.g.
+ /// battery-backed and handled through a mapper like MMC1).
+ pub has_persistent_memory: bool,
+
+ /// Whether there is a 512-byte trainer at $7000-$71FF or not.
+ pub has_trainer: bool,
+
+ /// The memory mapper being used.
+ pub mapper: Mapper,
+
+ /// The kind of the header.
+ pub kind: Kind,
+}
+
+impl TryFrom<&[u8]> for Header {
+ type Error = &'static str;
+
+ fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
+ let kind = get_rom_kind(bytes)?;
+ let ninth = bytes.get(9).unwrap_or(&0);
+
+ Ok(Self {
+ prg_rom_size: if matches!(kind, Kind::Nes20) {
+ (((ninth & 0x0F) as usize) << 8) + bytes[4] as usize
+ } else {
+ bytes[4] as usize
+ },
+ chr_rom_size: if matches!(kind, Kind::Nes20) {
+ (((ninth & 0xF0) as usize) << 4) + bytes[5] as usize
+ } else {
+ bytes[5] as usize
+ },
+ nametable_arrangement: parse_nametable(bytes.get(6)),
+ has_persistent_memory: (bytes.get(6).unwrap_or(&0) & 0x2) == 0x2,
+ has_trainer: (bytes.get(6).unwrap_or(&0) & 0x4) == 0x4,
+ mapper: parse_mapper(bytes.get(6), bytes.get(7), bytes.get(8)),
+ kind,
+ })
+ }
+}
+
+fn parse_nametable(byte: Option<&u8>) -> NameTableArrangement {
+ match byte {
+ Some(b) => {
+ if b & 0x1 == 0 {
+ NameTableArrangement::Vertical
+ } else {
+ NameTableArrangement::Horizontal
+ }
+ }
+ None => NameTableArrangement::Unknown,
+ }
+}
+
+fn parse_mapper(sixth: Option<&u8>, seventh: Option<&u8>, eighth: Option<&u8>) -> Mapper {
+ match sixth {
+ Some(l) => {
+ let byte = (l & 0xF0) >> 4;
+ let s = seventh.unwrap_or(&0);
+ let result = (s & 0xF0) | byte;
+
+ // Is this NES 2.0 format? If so, then the eighth byte will contain
+ // further information on mapper/submapper.
+ if (s & 0x0C) == 0x08 {
+ let e = eighth.unwrap_or(&0);
+ get_mapper_from_id(
+ ((*e as usize & 0x0F) << 16) + result as usize,
+ (e & 0xF0) >> 4,
+ )
+ } else {
+ get_mapper_from_id(result as usize, 0)
+ }
+ }
+ None => Mapper::Unknown,
+ }
+}
+
+fn get_mapper_from_id(mapper_id: usize, submapper_id: u8) -> Mapper {
+ match mapper_id {
+ 0 => Mapper::Nrom,
+ 1 => Mapper::Mmc1,
+ 2 => Mapper::Uxrom,
+ 3 => Mapper::Cnrom,
+ 4 => match submapper_id {
+ 0 => Mapper::Mmc3Sharp,
+ 1 => Mapper::Mmc6,
+ 2 => Mapper::Mmc3c,
+ 3 => Mapper::Mmc3Acc,
+ 4 => Mapper::Mmc3Nec,
+ 5 => Mapper::Mmc3T9552,
+ _ => Mapper::Unknown,
+ },
+ 5 => Mapper::Mmc5,
+ 7 => Mapper::Axrom,
+ 9 => Mapper::Mmc2,
+ 10 => Mapper::Mmc4,
+ 30 => Mapper::Unrom512,
+ 34 => match submapper_id {
+ 0 => Mapper::BnromCombo,
+ 1 => Mapper::BnromOnly,
+ 2 => Mapper::Nina001,
+ _ => Mapper::Unknown,
+ },
+ _ => Mapper::Unknown,
+ }
+}
+
+/// Get the Kind as parsed from the header expressed in `bytes`.
+pub fn get_rom_kind(bytes: &[u8]) -> Result<Kind, &'static str> {
+ if bytes.len() < 6 {
+ return Err("given header is too short");
+ }
+ if bytes[0] != b'N' || bytes[1] != b'E' || bytes[2] != b'S' || bytes[3] != 0x1A {
+ return Err("invalid magic value for header");
+ }
+
+ if (bytes.get(7).unwrap_or(&0) & 0x0C) == 0x08 {
+ Ok(Kind::Nes20)
+ } else {
+ Ok(Kind::INes)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn get_rom_kind_test() {
+ assert_eq!(
+ get_rom_kind(vec![].as_slice()).unwrap_err(),
+ "given header is too short"
+ );
+
+ assert_eq!(
+ get_rom_kind(vec![b'N', b'E', b'S', b'\0', 0x01, 0x00].as_slice()).unwrap_err(),
+ "invalid magic value for header"
+ );
+
+ // iNES without the seventh byte.
+ assert!(matches!(
+ get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00].as_slice()).unwrap(),
+ Kind::INes
+ ));
+
+ // iNES with seventh byte.
+ assert!(matches!(
+ get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00, 0x00].as_slice()).unwrap(),
+ Kind::INes
+ ));
+
+ assert!(matches!(
+ get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00, 0x08].as_slice()).unwrap(),
+ Kind::Nes20
+ ));
+ }
+
+ #[test]
+ fn nrom_test() {
+ let mut header =
+ Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x00, 0x00].as_slice())
+ .unwrap();
+ assert_eq!(header.prg_rom_size, 1);
+ assert_eq!(header.chr_rom_size, 1);
+ assert!(matches!(
+ header.nametable_arrangement,
+ NameTableArrangement::Vertical
+ ));
+ assert!(!header.has_persistent_memory);
+ assert!(!header.has_trainer);
+ assert!(matches!(header.mapper, Mapper::Nrom));
+ assert!(matches!(header.kind, Kind::INes));
+
+ header = Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x00, 0x08].as_slice())
+ .unwrap();
+ assert_eq!(header.prg_rom_size, 1);
+ assert_eq!(header.chr_rom_size, 1);
+ assert!(matches!(
+ header.nametable_arrangement,
+ NameTableArrangement::Vertical
+ ));
+ assert!(!header.has_persistent_memory);
+ assert!(!header.has_trainer);
+ assert!(matches!(header.mapper, Mapper::Nrom));
+ assert!(matches!(header.kind, Kind::Nes20));
+ }
+
+ #[test]
+ fn mmc3_mmc6_test() {
+ // MMC3 Sharp in iNES format.
+ let mut header =
+ Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x00].as_slice())
+ .unwrap();
+ assert_eq!(header.prg_rom_size, 1);
+ assert_eq!(header.chr_rom_size, 1);
+ assert!(matches!(
+ header.nametable_arrangement,
+ NameTableArrangement::Vertical
+ ));
+ assert!(!header.has_persistent_memory);
+ assert!(!header.has_trainer);
+ assert!(matches!(header.mapper, Mapper::Mmc3Sharp));
+ assert!(matches!(header.kind, Kind::INes));
+
+ // MMC3 Sharp but with NES 2.0 (notice explicit 0 submapper).
+ header =
+ Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x08, 0x00].as_slice())
+ .unwrap();
+ assert_eq!(header.prg_rom_size, 1);
+ assert_eq!(header.chr_rom_size, 1);
+ assert!(matches!(
+ header.nametable_arrangement,
+ NameTableArrangement::Vertical
+ ));
+ assert!(!header.has_persistent_memory);
+ assert!(!header.has_trainer);
+ assert!(matches!(header.mapper, Mapper::Mmc3Sharp));
+ assert!(matches!(header.kind, Kind::Nes20));
+
+ // Disambiguate to MMC6 thanks to submapper
+ header =
+ Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x08, 0x10].as_slice())
+ .unwrap();
+ assert_eq!(header.prg_rom_size, 1);
+ assert_eq!(header.chr_rom_size, 1);
+ assert!(matches!(
+ header.nametable_arrangement,
+ NameTableArrangement::Vertical
+ ));
+ assert!(!header.has_persistent_memory);
+ assert!(!header.has_trainer);
+ assert!(matches!(header.mapper, Mapper::Mmc6));
+ assert!(matches!(header.kind, Kind::Nes20));
+ }
+}