diff options
Diffstat (limited to 'rom2cart.c')
-rw-r--r-- | rom2cart.c | 641 |
1 files changed, 641 insertions, 0 deletions
diff --git a/rom2cart.c b/rom2cart.c new file mode 100644 index 0000000..f6f8754 --- /dev/null +++ b/rom2cart.c @@ -0,0 +1,641 @@ +/* Convert a raw cart dump into a .CAR + + Either the user sets the type, in which case we make sure the file size + matches, or else he doesn't, and we try to guess from the file size. + Also give options to force the type in case our cart_types is out of + data, and to force the checksum for debugging purposes. + + Type guessing algorithm... we have some or all of these: + + File size - always have. Eliminate all types of any other size. + + Machine type - optional, via -m. If we have it, eliminate all types of + the wrong machine type. + + Type name partial string - optional, via -t. If we have it, eliminate + all non-matching types. + + Type number - optional, via -t. If we have it, eliminate all but the + given type number. + + If list is now empty, give error message. + If list is only one element long, use it as-is. + Otherwise... there are >1 elements in the list. + + If the user has disabled guessing, show the him list, ask him to be + more specific. + + (optional heuristics:) + - Look at the option byte (3rd to last byte of the ROM) + - If the list contains both machine types, try to narrow it down by + looking at the init address ($4000-7FFF is definitely 5200). + - Also try looking for 3 or more STA $E8xx instructions: on the 5200, + these are writes to POKEY; on the 8bit, they're writes to ROM (unlikely). + + If list is only one element long, use it as-is. + + If he hasn't disabled guessing, then guess! Take lowest-numbered type... +*/ + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> +#include <ctype.h> + +#include "cart.h" + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "rom2cart" + +#define BANNER \ + SELF " v" VERSION " by B. Watson (WTFPL)\n" + +char *usage = + "\nUsage: " SELF " [-chl] [-t type] [-T type] [-C sum] [infile] [outfile]\n" + " cart2rom [infile] [outfile] (equivalent to " SELF " -r)\n" + " -h Print this help.\n" + " -v Verbose operation (-vv for 'very verbose').\n" + " -c Check only; do not create output file (enables -vv).\n" + " -t type Set cartridge type. [type] may be a numeric type or a\n" + " partial name to match.\n" + " -l List all known cartridge types.\n" + " -m mach Cart is for given machine, either '8bit' or '5200'.\n" + " -n Do not guess cart type, if unable to narrow down to one.\n" + " -r Write a raw ROM image as output, rather than a CART image.\n" + " -T type Like -t, but forces the type. Numeric type only.\n" + " -C sum Force the checksum to sum. Not very useful.\n" + " -U data Set 'unused' bytes in CART header to data.\n" + "\n" + "infile may be either a raw dump or a CART image, and may be '-' to read\n" + "from standard input. outfile may be omitted, in which case output will\n" + "be written to a file named after infile, with its extension changed to\n" + ".car (or .rom, if -r given). outfile may be '-' to write to standard\n" + "output.\n"; + +#define OPTIONS "vchlm:nT:t:C:rU:" + +void print_type_table(int *set) { + /* print a nicely-formatted type table. If set is NULL, print all types, + otherwise only print types whose bit in set is true. */ + int i; + fprintf(stderr, "%5s | %8s | %6s | %s\n", + "Type", "Machine", "SizeKB", "Name"); + fprintf(stderr, "------|----------|--------|------------------------\n"); + for(i=1; i<=MAX_CART_TYPE; i++) { + if(set == NULL || set[i]) + fprintf(stderr, "%5d | %8s | %6d | %s\n", + i, + (cart_types[i].machine == M_5200 ? "5200" : "8-bit"), + cart_types[i].size, + cart_types[i].name); + } +} + +/* read_file() reads and mallocs this many bytes at a time. There should + never be a reason to change this. */ +#define READ_CHUNK_SIZE 4096 + +int read_file(FILE *f, unsigned char **bufp) { + /* read an entire file, malloc'ing as we go. caller must free(*bufp). + return value is number of bytes read. */ + int bytes_read = 0, total_read = 0; + + *bufp = malloc(READ_CHUNK_SIZE); + if(!*bufp) { + fclose(f); + fputs("Out of memory (initial malloc failed)\n", stderr); + exit(1); + } + + while((bytes_read = fread(*bufp + total_read, 1, READ_CHUNK_SIZE, f)) > 0) { + unsigned char *newbuf; + + total_read += bytes_read; + newbuf = realloc(*bufp, total_read + READ_CHUNK_SIZE); + + if(!newbuf) { + fclose(f); + free(*bufp); + fputs("Out of memory (realloc failed)\n", stderr); + exit(1); + } + + *bufp = newbuf; + } + + return total_read; +} + +/* case-insensitive strstr()-like search */ +char *string_match(char *haystack, char *needle) { + while(*haystack) { + char *htmp = haystack; + char *ntmp = needle; + + if(tolower(*ntmp) == tolower(*haystack)) { + while(tolower(*ntmp) == tolower(*htmp)) { + ntmp++; + htmp++; + + if(!*ntmp) + return haystack; + + if(!*htmp) + break; + } + } + + haystack++; + } + + return NULL; +} + +machine_t code_heuristics(unsigned char *rom, int size, int verbose) { + /* Look at the first 8K (minus 6-byte header), count possible + occurences of POKEY writes. The POKEY is at $E800 on the 5200 and + $D200 on the 8-bit. Return value is the (probable) machine type, + which may be M_INVALID if the test is inconclusive. */ + int i, a8 = 0, a52 = 0; + int machine = M_INVALID; + + for(i=0; i < (size < 8192 ? (size-6) : (8192-6) ); i++) { + unsigned char opcode = rom[i]; + unsigned char page = rom[i+2]; + + /* STA abs STX abs STA abs,X */ + if(opcode == 0x8d || opcode == 0x8e || opcode == 0x9d) { + if(page == 0xe8) + a52++; + else if(page == 0xd2) + a8++; + } + } + + if(verbose > 1) { + fprintf(stderr, + SELF ": found %d probable 8-bit POKEY writes\n", a8); + fprintf(stderr, + SELF ": found %d probable 5200 POKEY writes\n", a52); + } + + if(a8 >= 3 && a52 <= 1) + machine = M_ATARI8; + else if(a52 >= 3 && a8 <= 1) + machine = M_5200; + + return machine; +} + +machine_t guess_machine_type(unsigned char *rom, int size, int verbose) { + /* Try to guess the machine type based on the option byte and/or + init address. If that fails, call code_heuristics(). + Return value is the guessed machine type, which may be M_INVALID if + we can't figure it out. */ + machine_t machine = M_INVALID; + unsigned char option = rom[size - 3]; + int init = rom[size - 2] | (rom[size - 1] << 8); + + if(verbose) + fprintf(stderr, + SELF ": using heuristics to guess machine type " + "(-n or -m to inhibit)\n"); + + if(option == 0xff || (option >= 0x50 && option <= 0x59)) { + /* 0xff means diagnostic cart on the 5200. 0x50 thru 0x59 are the + digits 0-9 for non-diagnostic carts (the 2nd digit of the + copyright year) */ + machine = M_5200; + if(verbose > 1) + fprintf(stderr, SELF ": machine type is 5200 based on option byte " + "$%02X\n", option); + } else if(option == 4 || option == 5 || option == 0x80) { + /* These are the most common option bytes for A8. */ + machine = M_ATARI8; + if(verbose > 1) + fprintf(stderr, SELF ": machine type is 8-bit based on option byte " + "$%02X\n", option); + } else if(init >= 0x4000 && init < 0x8000 && size >= 32768) { + /* 32K and up 5200 carts may have an init address below $8000. On + the 8-bit, addresses below $8000 are RAM, not part of the cart + address window. */ + machine = M_5200; + if(verbose > 1) + fprintf(stderr, SELF ": machine type is 5200 based on init address " + "$%04X\n", init); + } else { + /* If all else fails, look for STA $E8xx (5200 POKEY writes), + or STA $D2xx (A8 POKEY writes). Of course, data tables might + happen to contain these sequences of bytes... */ + machine = code_heuristics(rom, size, verbose); + } + + if(verbose) { + if(machine == M_INVALID) + fprintf(stderr, SELF ": heuristics couldn't guess machine type\n"); + else + fprintf(stderr, SELF ": heuristics guessed machine type: %s\n", + (machine == M_ATARI8 ? "8-bit" : "5200")); + } + + return machine; +} + +int determine_type( + int verbose, + int allow_guess, + int type_override, + char *type_param, + machine_t machine, + int size, + unsigned char *rom) +{ + /* Figure out the cartridge type (ID), based on the image size, machine + type (optional), and/or some heuristics that look at the ROM itself. + Return value is the type, which may be 0 if we can't guess. + candidates[] has one "bit" (int) per cart type, and is initialized to + all zeroes. As we find possible type matches, we turn on the bit(s) in + candidates[] (or turn them off, as types are eliminated). + + All this ugly table-searching code would be much prettier as an + SQL select, but it'd be overkill to require a Real Database for a + small niche-purpose utility like this... + */ + int type = 0, i, match, both, candidates[MAX_CART_TYPE + 1]; + + /* process -t/-T first */ + if(type_param && *type_param >= '1' && *type_param <= '9') { + type = atoi(type_param); + if(type_override) { + if(type) + return type; /* valid numeric -T */ + else + return 0; /* bogus -T */ + } else { + if(!type || type > MAX_CART_TYPE) { + fprintf(stderr, "Invalid numeric type '%d' for -t (valid types " + "are 1 to %d; use -l for list)\n", type, MAX_CART_TYPE); + return 0; + } + } + } + + /* no -T, so do a search */ + for(i=0; i<=MAX_CART_TYPE; i++) + candidates[i] = 0; + + if(type) { + /* -t numeric type */ + if(size != (cart_types[type].size * 1024)) { + fprintf(stderr, SELF ": ROM size does not match, type %d " + "should be %dK, but input is %dK\n", + type, cart_types[type].size, size/1024); + return 0; + } + + candidates[type] = 1; + machine = cart_types[type].machine; /* bypass machine-type guessing */ + } else if(type_param) { + /* -t string match */ + match = 0; + if(verbose) + fprintf(stderr, SELF ": trying to match '%s'\n", type_param); + + for(i=1; i<=MAX_CART_TYPE; i++) { + if(string_match(cart_types[i].name, type_param)) { + if(verbose > 1) + fprintf(stderr, SELF ": matched type %d (%s)\n", + i, cart_types[i].name); + candidates[i] = 1; + match++; + } + } + + if(!match) { + fprintf(stderr, SELF ": no types match '%s'\n", type_param); + return 0; + } + } else { + /* no -t param at all, all types are potential matches */ + for(i=0; i<=MAX_CART_TYPE; i++) + candidates[i] = 1; + } + + /* now eliminate all wrong-sized types */ + match = 0; + for(i=1; i<=MAX_CART_TYPE; i++) { + if(cart_types[i].size * 1024 == size) + match++; + else + candidates[i] = 0; + } + + /* size doesn't match anything in the table! Whoops! */ + if(!match) { + fprintf(stderr, SELF ": no known types match size %.1fKB\n", + (double)size / 1024.0); + return 0; + } + + /* see if both machine types are in the list */ + match = both = 0; + if(machine == M_INVALID) { + for(i=1; i<=MAX_CART_TYPE; i++) { + if(!candidates[i]) + continue; + + machine_t mt = cart_types[i].machine; + if(match) { + if(mt != match) { + both = 1; + break; + } + } else { + match = mt; + } + } + + if(!both) machine = match; + } + + if((verbose > 1) && both) + fprintf(stderr, SELF ": candidate list contains both machine types\n"); + + /* if no machine type override, try some heuristics to determine the + machine type. */ + if(allow_guess && (machine == M_INVALID)) + machine = guess_machine_type(rom, size, verbose); + + /* if -m given or guessed, eliminate all types of wrong machine type */ + if(machine != M_INVALID) { + for(i=1; i<=MAX_CART_TYPE; i++) { + if(cart_types[i].machine != machine) + candidates[i] = 0; + } + } + + /* now see whether we have 0, 1, or >1 matches */ + type = match = 0; + for(i=1; i<=MAX_CART_TYPE; i++) { + if(candidates[i]) { + match++; + if(!type) type = i; /* take the lowest-numbered type that matches */ + } + } + + if(!match) { + /* d'oh! Nothing matched... */ + return 0; + } else if(match == 1) { + /* Exact match, use it */ + if(verbose) { + fprintf(stderr, SELF ": exact match:\n"); + print_type_table(candidates); + } + + return type; + } else { + /* >1 match... */ + if(allow_guess) { + if(verbose) { + fprintf(stderr, SELF ": %d types match:\n", match); + print_type_table(candidates); + } + fprintf(stderr, SELF ": guessing type %d (%s)\n", + type, cart_types[type].name); + return type; + } else { + fprintf(stderr, SELF ": %d types match, not guessing:\n", match); + print_type_table(candidates); + return 0; + } + } +} + +/* helper for -C and -U options */ +unsigned int get_u32_value(char opt, char *arg) { + unsigned int got; + + if(sscanf(arg, "0x%x", &got) != 1) + if(sscanf(arg, "$%x", &got) != 1) + if(sscanf(arg, "%u", &got) != 1) { + fprintf(stderr, "Invalid value for -%c: '%s'\n", opt, arg); + exit(1); + } + + return got; +} + +int main(int argc, char **argv) { + char *infile = "-", outfile[4096] = "-"; + FILE *in = stdin, *out = stdout; + unsigned char *rom; + char *type_param = NULL; + int check_only = 0, type = 0, type_override = 0, allow_guess = 1, + verbose = 0, raw_output = 0; + machine_t machine = M_INVALID; + int c, size; + unsigned int cksum = 0, cksum_override = 0, unused_data = 0; + unsigned char *buffer; + unsigned char **bufp = &buffer; + unsigned char cart[16]; + + if(strstr(argv[0], "cart2rom") != NULL) { + raw_output = 1; + } + + while( (c = getopt(argc, argv, OPTIONS)) != -1 ) { + switch(c) { + case 'v': + verbose++; + break; + + case 'c': + check_only++; + verbose = 2; + break; + + case 'h': + fputs(BANNER, stderr); + fprintf(stderr, usage); + exit(0); + break; + + case 'l': + print_type_table(NULL); + exit(0); + break; + + case 'T': + type_override++; + /* fall through */ + + case 't': + type_param = optarg; + break; + + case 'C': + cksum = get_u32_value('C', optarg); + cksum_override++; + break; + + case 'U': + unused_data = get_u32_value('U', optarg); + break; + + case 'n': + allow_guess = 0; + break; + + case 'm': + if(optarg[0] == '5') + machine = M_5200; + else if(optarg[0] == 'a' || optarg[0] == '8') + machine = M_ATARI8; + else { + fprintf(stderr, SELF ": Invalid machine type (-m)\n" + "Valid types are '8bit' and '5200' (or '8' and '5')\n"); + exit(1); + } + break; + + case 'r': + raw_output++; + break; + + default: + fputs(BANNER, stderr); + fprintf(stderr, usage); + exit(1); + break; + } + } + + if(verbose) fputs(BANNER, stderr); + + if(optind < argc) { + infile = argv[optind]; + if(strcmp(infile, "-") == 0) { + in = stdin; + if(verbose) fprintf(stderr, SELF ": reading from standard input\n"); + } else { + char *p; + strcpy(outfile, infile); + p = strrchr(outfile, '.'); + if(!p) p = outfile + strlen(outfile); + *p = '\0'; + if(raw_output) + strcat(outfile, ".rom"); + else + strcat(outfile, ".car"); + + if( !(in = fopen(infile, "rb")) ) { + if(verbose) + fprintf(stderr, SELF ": (fatal) %s: %s\n", + infile, strerror(errno)); + exit(1); + } + } + optind++; + } + + size = read_file(in, bufp); + fclose(in); + + if(has_cart_signature(buffer)) { + rom = buffer + 16; + size -= 16; + type = get_cart_type(buffer); + if(verbose) fprintf(stderr, SELF ": input is CART image, type %d\n", type); + } else { + rom = buffer; + if(verbose) fprintf(stderr, SELF ": input is raw dump, %d bytes\n", size); + } + + if(size <= 0) { + fprintf(stderr, SELF ": input file is empty!\n"); + exit(1); + } else if(size % 4096) { + fprintf(stderr, SELF ": %s: input size not a multiple of 4KB!\n", + (type_override ? "warning" : "fatal")); + if(!type_override) exit(1); + } + + if(!type) { + type = determine_type( + verbose, allow_guess, type_override, type_param, machine, size, rom); + } + + if(!type) { + fprintf(stderr, SELF ": " + "Can't determine image type from supplied info and ROM size.\n" + "Please re-run with -t, -T, and/or -m options.\n"); + free(buffer); + exit(1); + } + + /* Built cart header. Don't use create_cart_header() because it assumes + the ROM size always matches the expected ROM size for the type. This + may NOT be the case, if someone uses -T to e.g. set the type to 16K + when there's only 8K of actual ROM. */ + memcpy(cart, CART_SIGNATURE, 4); + set_cart_type(cart, type); + if(!cksum_override) + cksum = calc_rom_checksum(buffer, size); + set_cart_checksum(cart, cksum); + set_cart_unused(cart, unused_data); + + if(verbose > 1) cart_dump_header(cart, 0); + + if(!check_only) { + if(optind < argc) { + strcpy(outfile, argv[optind]); + optind++; + } + + if(strcmp(outfile, "-") == 0) { + out = stdout; + if(verbose) fprintf(stderr, SELF ": writing to standard output\n"); + } else { + if(strcmp(infile, outfile) == 0) { + fprintf(stderr, SELF ": Input and output filenames are the same, " + "aborting!\n"); + exit(1); + } + if(verbose) fprintf(stderr, SELF ": output file is '%s'\n", outfile); + + if( !(out = fopen(outfile, "wb")) ) { + fprintf(stderr, SELF ": (fatal) %s: %s\n", outfile, strerror(errno)); + exit(1); + } + } + + if(isatty(fileno(out))) { + fprintf(stderr, SELF ": Standard output is a terminal, " + "not writing binary data.\n" + "Either redirect to a file or set the output filename.\n"); + exit(1); + } + } + + if(optind < argc) { + fprintf(stderr, SELF ": ignoring trailing junk on command line: " + "'%s' ...\n", + argv[optind]); + } + + if(!check_only) { + if(!raw_output) fwrite(cart, 16, 1, out); + fwrite(rom, size, 1, out); + fclose(out); + } + + free(buffer); + + exit(0); +} |