diff options
Diffstat (limited to 'crates/runrom/src/main.rs')
| -rw-r--r-- | crates/runrom/src/main.rs | 225 |
1 files changed, 225 insertions, 0 deletions
diff --git a/crates/runrom/src/main.rs b/crates/runrom/src/main.rs new file mode 100644 index 0000000..ab1ea83 --- /dev/null +++ b/crates/runrom/src/main.rs @@ -0,0 +1,225 @@ +use header::Header; +use std::fs::File; +use std::io::{ErrorKind, Read, Seek, SeekFrom}; +use vnf::{Machine, MemoryInitialValue, MemoryPolicy}; + +/// Version for this program. +const VERSION: &str = "0.1.0"; + +#[derive(Default)] +struct Args { + file: String, + start: Option<u16>, + assume_function: bool, + dump_memory: bool, +} + +fn print_help() { + println!("Run an NES/Famicom ROM to test its code under a set of conditions.\n"); + println!("usage: runrom [OPTIONS] <FILE>\n"); + println!("Options:"); + println!(" -d, --dump-memory\tShow the memory that has changed after a run."); + println!(" -f, --function\tRun the code by assuming it's a function."); + println!(" -h, --help\t\tPrint this message and quit."); + println!(" -s, --start\t\tAddress from where to start (default: reset vector)."); + println!(" -v, --version\t\tPrint version information."); + std::process::exit(0); +} + +// Print the given `message` and exit(1). +fn die(message: String) { + eprintln!("error: {message}"); + std::process::exit(1); +} + +fn parse_hex_digit(c: char) -> Result<u16, String> { + match c.to_digit(16) { + Some(val) => Ok(val as u16), + None => Err("cannot convert digit to hexadecimal".to_string()), + } +} + +fn parse_hex_argument(given: &str) -> Result<u16, String> { + // Skip a leading '$' character. + let arg = if given.starts_with('$') { + given.get(1..).unwrap_or("") + } else { + given + }; + let mut chars = arg.chars(); + + match arg.len() { + 0 => Err("you need to provide an address".to_string()), + 1 => Ok(parse_hex_digit(chars.next().unwrap())?), + 2 => Ok((parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + 3 => Ok((parse_hex_digit(chars.next().unwrap())? << 8) + + (parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + 4 => Ok((parse_hex_digit(chars.next().unwrap())? << 12) + + (parse_hex_digit(chars.next().unwrap())? << 8) + + (parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + _ => Err("hex literal is too big".to_string()), + } +} + +fn parse_arguments() -> Args { + let mut args = std::env::args(); + let mut res = Args::default(); + + // Skip command name. + args.next(); + + while let Some(arg) = args.next() { + match arg.as_str() { + "-h" | "--help" => print_help(), + "-s" | "--start" => { + if res.start.is_some() { + die("do not specify the '-s/--start' flag twice".to_string()); + } + let Some(val) = args.next() else { + die("you need to specify a value for the -s/--start flag!".to_string()); + return res; + }; + match parse_hex_argument(&val) { + Ok(n) => res.start = Some(n), + Err(e) => die(e), + } + } + "-d" | "--dump-memory" => { + res.dump_memory = true; + } + "-f" | "--function" => { + res.assume_function = true; + } + "-v" | "--version" => { + println!("runrom {VERSION}"); + std::process::exit(0); + } + _ => { + if arg.starts_with('-') { + die(format!("don't know how to handle the '{arg}' flag")); + } + if !res.file.is_empty() { + die("cannot have multiple source files".to_string()); + } + res.file = arg; + } + } + } + + if res.file.is_empty() { + die("you need to specify the file to be run".to_string()); + } + + res +} + +// Given a ROM file identified by the `file` parameter, fetch the 16-bit address +// as pointed out by the reset vector. +fn start_from_reset_vector(file: &String) -> u16 { + // 1. Read the ROM header so we fetch the size of PRG ROM. + + let Ok(mut input) = File::open(file) else { + die(format!("failed to open the given file '{file}'")); + return 0; + }; + + let mut buf = vec![0u8; 0x10]; + if let Err(e) = input.read_exact(&mut buf) { + match e.kind() { + ErrorKind::UnexpectedEof => die("malformed ROM file".to_string()), + _ => die(e.to_string()), + } + } + + let header = match Header::try_from(buf.as_slice()) { + Ok(h) => h, + Err(e) => { + die(e.to_string()); + return 0; + } + }; + + // 2. With a known PRG ROM size, fetch the two bytes pertaining to the reset + // vector. + + // The two bytes of the reset address are located as follows: + // 1. Skip the ROM header, guaranteed to be exactly 0x10 bytes long. + // 2. Go to the end of PRG ROM. + // 3. -6: NMI addres; -4: reset addres; -2: IRQ address. + let offset: u64 = (0x10 + (header.prg_rom_size * 16 * 1024) - 4) + .try_into() + .unwrap(); + + if input.seek(SeekFrom::Start(offset)).is_err() { + die("cannot peek into the ROM's reset address".to_string()); + return 0; + }; + let mut buf = [0u8; 0x02]; + if let Err(e) = input.read_exact(&mut buf) { + match e.kind() { + ErrorKind::UnexpectedEof => die("malformed ROM file".to_string()), + _ => die(e.to_string()), + } + } + + ((buf[1] as u16) << 8) + buf[0] as u16 +} + +fn run(file: &String, start: u16, assume_function: bool, dump_memory: bool) -> Result<(), String> { + let mut machine = Machine::from( + file, + start, + #[allow(clippy::single_range_in_vec_init)] + MemoryPolicy { + initial_value: MemoryInitialValue::Fixed(0), + allowed_reads: vec![(0..0x800)], + allowed_writes: vec![(0..0x800)], + minimum_stack_value: 0, + }, + )?; + machine.verbose = true; + + if assume_function { + machine.run_function()?; + } else { + machine.until_address(0xFFFF)?; + } + + if dump_memory { + let mut title = false; + + for (idx, cell) in machine.ram.iter().enumerate() { + if cell.reads > 0 || cell.writes > 0 { + if !title { + println!("\n== Memory dump ==\n"); + title = true; + } + + println!( + "[${:X}] = ${:02X} [reads={}, writes={}]", + idx, cell.value, cell.reads, cell.writes + ); + } + } + } + + Ok(()) +} + +fn main() { + let args = parse_arguments(); + let start = match args.start { + Some(s) => s, + None => start_from_reset_vector(&args.file), + }; + + match run(&args.file, start, args.assume_function, args.dump_memory) { + Ok(m) => m, + Err(e) => { + die(e); + } + } +} |
