#include #include #include #include #include "amsbtok.h" /* this should always be defined in , but just in case... */ #ifndef BUFSIZ #define BUFSIZ 4096 #endif /* range for one-byte tokens */ #define MIN_STD_TOK 0x80 /* END */ #define MAX_STD_TOK 0xf8 /* < */ /* range for 2nd byte of two-byte tokens (1st is always 0xff) */ #define MIN_EXT_TOK 0xa3 /* SGN */ #define MAX_EXT_TOK 0xc5 /* STACK */ /* AMSB's tokens for "!" and "'", used to introduce comments */ #define TOK_SQUOTE 0x9a #define TOK_BANG 0x9b /* good old Atari EOL character */ #define EOL 0x9b /* every MS BASIC I ever saw had the same line number limit. it's kind of arbitrary: why not allow the full range, up to 65535? */ #define MAX_LINENO 63999 /* a program bigger than this can't possibly fit into memory, even with cart-based AMSB2 and no DOS loaded. */ #define MAX_PROGLEN 30000 /* there should never be a valid line of BASIC longer than MAX_LINE_LEN bytes, since there would be no way to enter it in the editor. AMSB uses the standard E: device, which limits you to 4 screen lines, or max 120 bytes. there *cannot* be a line longer than MAX_LINE_LEN_HARD, because AMSB would crash when you try to LOAD such a program. */ #define MAX_LINE_LEN 0x80 #define MAX_LINE_LEN_HARD 0xff /* a program whose header has a length less than MIN_PROGLEN can't be a real AMSB program. EMPTY_PROGLEN is what you get if you SAVE when there's no program in memory (right after boot or a NEW). The minimum size for a program that actually contains code seems to be 5 (for 10 PRINT) */ #define MIN_PROGLEN 5 #define EMPTY_PROGLEN 2 /* an EOL address below this has to be an error, since this is the lowest MEMLO can ever be. */ #define MIN_PTR 0x0700 /* an EOL address higher than this has to be an error, since it would overlap the GR.0 display list or the ROMs at $c000 */ #define MAX_PTR 0xbc1f const char *self; char pipe_command[BUFSIZ + 1] = { "a8cat" }; int verbose = 0; /* -v */ int raw_output = 0; /* -a */ int check_only = 0; /* -c */ int startline = 0; /* -r */ int endline = MAX_LINENO; /* -r */ int need_pclose = 0; int bytes_read = 0; int warnings = 0; int proglen = 0; FILE *infile; FILE *outfile; void set_self(const char *argv0) { char *p; self = argv0; p = strrchr(self, '/'); if(p) self = p + 1; } void die_with(const char *msg, int status) { fprintf(stderr, "%s: %s\n", self, msg); exit(status); } #define die(x) die_with(x,1) #define die2(x) die_with(x,2) unsigned char read_byte(void) { int c; c = fgetc(infile); if(c < 0) die2("unexpected EOF, file truncated?"); bytes_read++; return (unsigned char)c; } int read_word(void) { int w; w = read_byte(); w |= (read_byte() << 8); return w; } void read_header(void) { unsigned char b; b = read_byte(); if(b) die2("not an AMSB file: first byte not $00"); proglen = read_word(); if(verbose) fprintf(stderr, "proglen == %d (%04x)\n", proglen, proglen); if(proglen > MAX_PROGLEN) { fprintf(stderr, "%s: not an AMSB file: too big (%d bytes), won't fit in Atari memory\n", self, proglen); exit(2); } if(proglen == EMPTY_PROGLEN) { fprintf(stderr, "%s: program length is 2, no code in file (SAVE after NEW)\n", self); warnings++; } else { if(proglen < MIN_PROGLEN) { fprintf(stderr, "%s: not an AMSB file: program size too small (%d). Atari BASIC file?\n", self, proglen); exit(2); } } } void unknown_token(int lineno, unsigned char byte, int ext) { fprintf(outfile, "", (ext ? "$ff ": ""), byte); } int next_line(void) { static int last_lineno = -1; static int last_ptr = -1; int ptr, lineno, was_ff, in_string, offset, len; int printing; unsigned char byte; offset = bytes_read; /* pointer to last token on the line, offset by whatever MEMLO happened to be when the file was SAVEd. 0 means this is the last line. */ ptr = read_word(); if(!ptr) { if(verbose) fprintf(stderr, "end of program\n"); return 0; } lineno = read_word(); if(verbose) fprintf(stderr, "found line %d, offset %d, end-of-line %d\n", lineno, offset, ptr); printing = (lineno >= startline) && (lineno <= endline); if(ptr < MIN_PTR) { fprintf(stderr, "%s: line %d: EOL address $%04x too low (<$%04x)\n", self, lineno, ptr, MIN_PTR); warnings++; } else if(ptr >= MAX_PTR) { fprintf(stderr, "%s: line %d: EOL address $%04x too high (>$%04x)\n", self, lineno, ptr, MAX_PTR); warnings++; } if(last_ptr != -1) { if(ptr <= last_ptr) { fprintf(stderr, "%s: line %d: EOL address $%04x <= previous $%04x\n", self, lineno, ptr, last_ptr); warnings++; } } if(lineno <= last_lineno) { fprintf(stderr, "%s: line number out of order (%d <= %d)\n", self, lineno, last_lineno); warnings++; } if(lineno > MAX_LINENO) { fprintf(stderr, "%s: line number out range (%d > %d)\n", self, lineno, MAX_LINENO); warnings++; } last_lineno = lineno; /* note that AMSB always puts a space after the line number in LIST */ if(printing) fprintf(outfile, "%d ", lineno); was_ff = 0; in_string = 0; /* walk and print the tokens. when we hit a null byte, we're done. */ while(1) { byte = read_byte(); if(in_string) { if(byte == 0x00 || byte == '|') { /* end of string */ in_string = 0; if(printing) putc('"', outfile); /* if we read a null, that means the line ends with a string that's missing its closing double-quote. */ if(byte == 0x00) { break; } else { continue; } } if(printing) putc(byte, outfile); } else if(byte == ':') { /* don't print the colon if the next token is a ! or ' for a comment */ unsigned char next = read_byte(); if( !(next == TOK_SQUOTE || next == TOK_BANG) ) if(printing) putc(byte, outfile); ungetc(next, infile); bytes_read--; } else if(byte == '"') { /* strings start but *don't end* with a double-quote */ in_string = 1; if(printing) putc(byte, outfile); } else if(was_ff) { if(byte >= MIN_EXT_TOK && byte <= MAX_EXT_TOK) { if(printing) fputs(ext_tokens[byte - MIN_EXT_TOK], outfile); } else { if(printing) unknown_token(lineno, byte, 1); warnings++; } was_ff = 0; } else if(byte == 0xff) { was_ff = 1; } else if(byte >= MIN_STD_TOK && byte <= MAX_STD_TOK) { if(printing) fputs(std_tokens[byte - MIN_STD_TOK], outfile); } else if(byte >= 0x80) { if(printing) unknown_token(lineno, byte, 0); warnings++; } else { if(!byte) break; if(byte < 0x20) { /* ATASCII graphics outside of a string */ fprintf(stderr, "%s: line %d has character %d outside of a string, " "maybe Atari BASIC?\n", self, lineno, byte); warnings++; } if(printing) putc(byte, outfile); } } len = bytes_read - offset; if(verbose) { fprintf(stderr, " line %d length: %d\n", lineno, len); } if(len > MAX_LINE_LEN) { int hard = len > MAX_LINE_LEN_HARD; fprintf(stderr, "%s: line %d is %s long (length %d > %d)\n", self, lineno, hard ? "impossibly" : "supiciously", len, hard ? MAX_LINE_LEN : MAX_LINE_LEN_HARD); } if(last_ptr != -1) { int plen = ptr - last_ptr; if(len != plen) { fprintf(stderr, "%s: line %d: EOL address doesn't match actual line length %d\n", self, lineno, len); warnings++; } } last_ptr = ptr; if(printing) putc(EOL, outfile); return 1; } void print_help(void) { printf("%s v" VERSION " - detokenize Atari Microsoft BASIC files\n", self); puts("By B. Watson , released under the WTFPL"); printf("Usage: %s [-a] [-v] [-h] [-i] [-u] [-t] [-m] [-s] [-r *start,end*] [file]\n", self); puts(" -a: raw ATASCII output"); puts(" -c: check only (no listing)"); puts(" -v: verbose"); puts(" -r: only list lines numbered from *start* to *end*"); puts(" --help, -h: print this help and exit"); puts(" --version: print version number and exit"); puts(" -i -u -t -m -s: passed to a8cat (try 'a8cat -h')"); puts("file must be a tokenized (SAVEd) AMSB file. if not given, reads from stdin."); } void version(void) { printf("%s " VERSION "\n", self); } void get_line_range(const char *arg) { int val = 0, comma = 0; const char *p = arg; while(*p) { if(*p >= '0' && *p <= '9') { val *= 10; val += *p - '0'; if(val > MAX_LINENO) { fprintf(stderr, "invalid line number for -r (range is 0-%d)\n", MAX_LINENO); exit(1); } } else if(*p == ',' || *p == '-') { if(comma) die("invalid argument for -r (too many commas)"); comma++; startline = val; val = 0; } else { if(comma) die("invalid argument for -r (only digits and comma allowed)"); } p++; } if(comma) endline = val ? val : MAX_LINENO; else startline = endline = val; if(endline < startline) die("invalid argument for -r (start > end)"); } void parse_args(int argc, char **argv) { char tmp[10]; int opt; if(argc >= 2) { if(strcmp(argv[1], "--help") == 0) { print_help(); exit(0); } else if(strcmp(argv[1], "--version") == 0) { version(); exit(0); } } while( (opt = getopt(argc, argv, "r:cvaiutmsh")) != -1) { switch(opt) { case 'c': check_only = 1; break; case 'a': raw_output = 1; break; case 'h': print_help(); exit(0); case 'v': verbose = 1; break; case 'r': get_line_range(optarg); break; case 'i': case 'u': case 't': case 'm': case 's': if(strlen(pipe_command) > (BUFSIZ - 10)) die("too many a8cat options"); sprintf(tmp, " -%c", opt); strcat(pipe_command, tmp); break; default: print_help(); exit(1); } } if(optind >= argc) { if(isatty(fileno(stdin))) { fprintf(stderr, "%s: can't read binary data from a terminal\n", self); print_help(); exit(1); } freopen(NULL, "rb", stdin); infile = stdin; } else { infile = fopen(argv[optind], "rb"); if(!infile) { fprintf(stderr, "%s: ", self); perror(argv[optind]); exit(1); } } } void open_output() { if(check_only) { outfile = freopen("/dev/null", "wb", stdout); if(!outfile) { fprintf(stderr, "%s: ", self); perror("/dev/null"); exit(1); } if(verbose) fprintf(stderr, "using /dev/null for output (check_only)\n"); } else if(raw_output) { if(isatty(fileno(stdout))) die("refusing to write raw ATASCII to a terminal"); outfile = stdout; if(verbose) fprintf(stderr, "using stdout for output\n"); } else { if(verbose) fprintf(stderr, "using pipe for output: %s\n", pipe_command); outfile = popen(pipe_command, "w"); if(!outfile) { perror(pipe_command); exit(1); } need_pclose = 1; } } int main(int argc, char **argv) { int rv = 0, linecount = 0; set_self(argv[0]); parse_args(argc, argv); open_output(); read_header(); while(next_line()) linecount++; if(verbose) { fprintf(stderr, "read %d bytes\n", bytes_read); fprintf(stderr, "listed %d lines\n", linecount); } if(!linecount) { fprintf(stderr, "%s: no lines of code in program\n", self); warnings++; } if(proglen == (bytes_read - 3)) { if(verbose) fprintf(stderr, "file size matches proglen\n"); } else { fprintf(stderr, "%s: actual program size doesn't match program size in header\n", self); warnings++; } if(fgetc(infile) != EOF) { fprintf(stderr, "%s: trailing garbage at end of file\n", self); warnings++; } if(warnings) { fprintf(stderr, "%s: file has %d warnings\n", self, warnings); rv = 2; } if(need_pclose) { int got = pclose(outfile); if(verbose) fprintf(stderr, "return value from pipe is %d\n", got); if(got != 0) { die("a8cat child process failed, do you have a8cat on your PATH?"); } } exit(rv); }