aboutsummaryrefslogtreecommitdiff
path: root/rom2cart.c
diff options
context:
space:
mode:
Diffstat (limited to 'rom2cart.c')
-rw-r--r--rom2cart.c641
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);
+}