#include #include #include #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 "!", "'", REM. used to introduce comments */ #define TOK_REM 0x98 #define TOK_SQUOTE 0x9a #define TOK_BANG 0x9b /* various other AMSB tokens */ #define TOK_ELSE 0x9c #define TOK_IF 0x97 #define TOK_THEN 0x9d #define TOK_AND 0xf3 #define TOK_OR 0xf4 #define TOK_NOT 0xf5 /* 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 3 40-column screen lines, or max 120 bytes (after POKE 82,0). 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 /* a file shorter than this can't be an AMSB program */ #define MIN_BYTES 5 /* 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 /* SAVE "filename" LOCK does 'encryption' by subtracting every byte from this (except the 3-byte header) */ #define UNLOCK_KEY 0x54 const char *self; char pipe_command[BUFSIZ + 1] = { "a8cat" }; int verbosity = 0; /* -v */ int raw_output = 0; /* -a */ int check_only = 0; /* -c */ int startline = 0; /* -r */ int endline = 65536; /* -r */ int unlock_mode = 0; /* -l */ int initial_eol = 1; /* -n */ int crunch = 0; /* -C, -D */ int decrunch = 0; /* -D */ int locked = 0; int need_pclose = 0; int bytes_read = 0; int warnings = 0; int proglen = 0; int linecount = 0; int header_read = 0; int printing = 0; int spacecount = 0; FILE *infile; FILE *outfile; const char *outname = NULL; void verbose(int level, const char *fmt, ...) { va_list ap; if(verbosity < level) return; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); } void finish(int rv); #define VA_FUNC(func, prefix, action) \ void func(const char *fmt, ...) { \ va_list ap; \ va_start(ap, fmt); \ fprintf(stderr, "%s: %s: ", self, prefix); \ vfprintf(stderr, fmt, ap); \ va_end(ap); \ fputc('\n', stderr); \ action; \ } VA_FUNC(die, "error", finish(2)); VA_FUNC(os_err, "fatal", exit(1)); VA_FUNC(warn, "warning", warnings++); void set_self(const char *argv0) { char *p; self = argv0; p = strrchr(self, '/'); if(p) self = p + 1; } const char *plural(int count) { return count == 1 ? "" : "s"; } int is_comment_start(unsigned char tok) { return (tok == TOK_REM || tok == TOK_SQUOTE || tok == TOK_BANG); } /* post-processing: print "summary", exit. called by either die() or main() (on normal exit). */ void finish(int rv) { int progsize; verbose(1, "read %d bytes", bytes_read); verbose(1, "listed %d line%s", linecount, plural(linecount)); if(!linecount) warn("no lines of code in program"); if(header_read) { progsize = bytes_read - 3; if(proglen == progsize) { verbose(1, "file size matches proglen"); } else { warn("program size %d doesn't match size %d in header,", progsize, proglen); fputs(" ", stderr); if(proglen > progsize) { fputs("AMSB will give #136 ERROR and fail to LOAD this file\n", stderr); } else { fputs("AMSB will stop LOADing before the end of this file\n", stderr); } } } else { warn("file is %d byte%s, too short to be an AMSB program", bytes_read, plural(bytes_read)); } if(fgetc(infile) != EOF) warn("trailing garbage at end of file"); if(warnings) { fprintf(stderr, "%s: file has %d warning%s\n", self, warnings, plural(warnings)); rv = 2; } if(need_pclose) { int got = pclose(outfile); verbose(1, "return value from pipe is %d", got); if(got != 0) { os_err("a8cat child process failed, do you have a8cat on your PATH?"); } } verbose(2, "spaces outside strings/comments: %d (%.1f%%)", spacecount, (float)(spacecount) / (float)(bytes_read) * 100.0); verbose(1, "exit status: %d (%s)", rv, (rv ? "ERROR" : "OK")); exit(rv); } unsigned char read_byte(void) { int c; c = fgetc(infile); if(c < 0) die("unexpected EOF in %s (byte %d), file truncated?", (header_read ? "program" : "header"), bytes_read); bytes_read++; return (unsigned char)c; } /* "decrypt" a byte from a "SAVE x LOCK" program. */ unsigned char unlock_byte(unsigned char b) { return ((UNLOCK_KEY - b) & 0xff); } /* the "encryption" is the same (process is reciprocal) */ #define lock_byte(x) unlock_byte(x) /* read and (if needed) decrypt a byte from the program. */ unsigned char read_prog_byte(void) { unsigned char b = read_byte(); return locked ? unlock_byte(b) : b; } /* read a word from the program, does not decrypt */ int read_word(void) { int w; w = read_byte(); w |= (read_byte() << 8); return w; } /* read and (if needed) decrypt a word from the program. */ int read_prog_word(void) { int w; w = read_prog_byte(); w |= (read_prog_byte() << 8); return w; } void read_header(void) { /* $00 for plain, $01 for SAVE with LOCK */ locked = read_byte(); if(locked > 1) die("not an AMSB file: first byte is $%02x, not $00 or $01", locked); if(locked) verbose(1, "program is locked, decrypting"); proglen = read_word(); verbose(1, "proglen == %d (%04x)", proglen, proglen); if(proglen > MAX_PROGLEN) { die("not an AMSB file: too big (%d bytes), won't fit in Atari memory", proglen); } if(proglen == EMPTY_PROGLEN) { warn("program length is 2, no code in file (SAVE after NEW)"); } else { if(proglen < MIN_PROGLEN) { die("not an AMSB file: program size too small (%d). Atari BASIC file?", proglen); } } header_read = 1; } void unknown_token(unsigned char byte, int ext) { if(!printing) return; fprintf(outfile, "", (ext ? "$ff ": ""), byte); } void list_char(unsigned char c) { if(printing) fputc(c, outfile); } void list_token(unsigned char c) { if(printing) fputs(std_tokens[c - MIN_STD_TOK], outfile); } void list_ext_token(unsigned char c) { if(printing) fputs(ext_tokens[c - MIN_EXT_TOK], outfile); } void list_lineno(int l) { /* note that AMSB always puts a space after the line number in LIST */ if(printing) fprintf(outfile, "%d ", l); } void start_listing(void) { /* AMSB always prints a blank line when it LISTs, so we do, too. unless it's disabled with -n, of course. */ if(initial_eol) fputc(EOL, outfile); } /* meat and potatoes. does the actual detokenizing. gets called once per line of code. returns false when it hits the last line, or true if there are more lines. */ int next_line(void) { static int last_lineno = -1; static int last_ptr = -1; int ptr, lineno, was_ff, was_colon, in_string, in_comment, offset, len; 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_prog_word(); if(!ptr) { verbose(1, "end of program"); return 0; } lineno = read_prog_word(); verbose(2, "found line %d, offset %d, end-of-line %d", lineno, offset, ptr); printing = (lineno >= startline) && (lineno <= endline); if(ptr < MIN_PTR) { warn("line %d: EOL address $%04x too low (<$%04x)", lineno, ptr, MIN_PTR); } else if(ptr >= MAX_PTR) { warn("line %d: EOL address $%04x too high (>$%04x)", lineno, ptr, MAX_PTR); } if(last_ptr != -1) { if(ptr <= last_ptr) { warn("line %d: EOL address $%04x <= previous $%04x", lineno, ptr, last_ptr); } } if(lineno <= last_lineno) { warn("line number out of order (%d <= %d)", lineno, last_lineno); } if(lineno > MAX_LINENO) { warn("line number out range (%d > %d)", lineno, MAX_LINENO); } last_lineno = lineno; list_lineno(lineno); was_ff = 0; was_colon = 0; in_string = 0; in_comment = 0; /* walk and print the tokens. when we hit a null byte, break out of the loop, we're done with this line. */ while(1) { byte = read_prog_byte(); if(in_string) { if(byte == 0x00) { /* null byte ends both the string and the line of code. don't print a closing quote because AMSB doesn't. */ break; } else if(byte == '|') { /* pipe is how AMSB stores the closing quote. end the string but not the line of code, and print a " character. */ in_string = 0; list_char('"'); } else { /* normal string character. */ list_char(byte); /* one " character embedded in a string gets printed as "" */ if(byte == '"') list_char(byte); } continue; } else if(in_comment) { /* null byte ends both the comment and the line of code. */ if(byte == 0x00) break; list_char(byte); continue; } if(was_colon) { if(byte != TOK_SQUOTE && byte != TOK_BANG && byte != TOK_ELSE) { list_char(':'); } was_colon = 0; } if(byte == ':') { /* statement separator. don't print the colon yet, the next token might be a ! or ' for a comment */ was_colon = 1; } else if(byte == '"') { /* begin string. strings start but *don't end* with a double-quote */ in_string = 1; list_char(byte); } else if(was_ff) { /* previous token was $ff, so this is a function token */ if(byte >= MIN_EXT_TOK && byte <= MAX_EXT_TOK) { list_ext_token(byte); } else { unknown_token(byte, 1); warn("unknown extended token $ff $%02x at line %d", byte, lineno); } was_ff = 0; } else if(byte == 0xff) { /* next token will be a function token */ was_ff = 1; } else if(byte >= MIN_STD_TOK && byte <= MAX_STD_TOK) { /* statement token */ list_token(byte); if(is_comment_start(byte)) in_comment = 1; } else if(byte >= 0x80) { /* invalid token */ unknown_token(byte, 0); warn("unknown token $%02x at line %d", byte, lineno); } else { /* null byte means the line of code is done */ if(!byte) break; if(byte < 0x20) { /* ATASCII graphics outside of a string */ warn("line %d has character %d outside of a string, maybe Atari BASIC?", lineno, byte); } if(byte == ' ') spacecount++; list_char(byte); } } len = bytes_read - offset; verbose(2, " line %d length: %d", lineno, len); if(len > MAX_LINE_LEN) { int hard = len > MAX_LINE_LEN_HARD; warn("line %d is %s long (length %d > %d)", lineno, hard ? "impossibly" : "supiciously", len, hard ? MAX_LINE_LEN_HARD : MAX_LINE_LEN); } if(last_ptr != -1) { int plen = ptr - last_ptr; if(len != plen) { warn("line %d: EOL address doesn't match actual line length %d (should be %d)", lineno, len, plen); } } last_ptr = ptr; list_char(EOL); return 1; } /* when this gets called, input and output are open, read_header() has already run. "locking" and "unlocking" are the same transform, so this function does both. note that *no* checking of the program code is done here, so there's no need to finish() afterwards. */ void unlock_program(void) { int c; /* do not convert this to warn() (it ain't a warning) */ fprintf(stderr, "%s: program is %slocked, output will be %slocked\n", self, locked ? "" : "un", locked ? "un" : ""); /* 3-byte header: 0 for unlocked, 1 for locked */ fputc(!locked, outfile); /* LSB of program length (not encrypted) */ fputc(proglen & 0xff, outfile); /* MSB */ fputc((proglen >> 8) & 0xff, outfile); /* rest of file, including trailing nulls, is transformed */ while( (c = fgetc(infile)) >= 0) fputc(unlock_byte(c & 0xff), outfile); fclose(outfile); exit(0); } void write_code(int addr, int lineno, const unsigned char *code) { fputc(addr & 0xff, outfile); fputc((addr >> 8) & 0xff, outfile); fputc(lineno & 0xff, outfile); fputc((lineno >> 8) & 0xff, outfile); fputs((char *)code, outfile); fputc(0x00, outfile); } int crunch_line(void) { unsigned char code[MAX_LINE_LEN_HARD + 1], byte; int lineno, ptr, codelen = 0, in_string = 0, in_comment = 0, commentstart = 0; static int addr = 0x700; /* doesn't really matter where */ ptr = read_prog_word(); if(!ptr) return -1; lineno = read_prog_word(); verbose(2, "crunching line %d", lineno); while(1) { if(codelen >= MAX_LINE_LEN_HARD) die("line %d too long, crunching failed", lineno); byte = read_prog_byte(); if(byte != 0) { if(in_string) { if(byte == '|') in_string = 0; } else if(in_comment) { continue; } else { if(byte == '"') in_string = 1; else if(is_comment_start(byte)) { in_comment = 1; commentstart = codelen; } else if(byte == ' ') continue; } } code[codelen++] = byte; if(byte == 0) break; } /* omit comment-only lines */ if(code[0] == TOK_REM) return 0; if(code[0] == ':') { if(code[1] == TOK_SQUOTE || code[1] == TOK_BANG) return 0; } /* omit trailing comments */ if(commentstart) { code[commentstart - 1] = 0; /* null out the colon before the comment */ codelen = commentstart; } codelen += 4; /* account for ptr and lineno */ addr += codelen; write_code(addr, lineno, code); return codelen; } /* only called during decrunching. only handles one-byte tokens (only has to). */ void expand_token(int ext, unsigned char t, unsigned char *buf) { const char *result; if(t < 0x80) { buf[0] = t; buf[1] = 0; return; } if(ext) { if(t > MAX_EXT_TOK) die("invalid token in program, can't decrunch"); result = ext_tokens[t - MIN_EXT_TOK]; } else { if(t > MAX_STD_TOK) die("invalid token in program, can't decrunch"); result = std_tokens[t - MIN_STD_TOK]; } strcpy((char *)buf, result); } /* only called during decrunching. the tokenizer in AMSB requires spaces to separate keywords and variables/numbers. "IF A THEN 100" really needs the spaces before and after the A, for example. */ int need_space_between(int ext1, int ext2, unsigned char t1, unsigned char t2) { unsigned char tok1[10], tok2[10]; unsigned char t1last, t2first; if(!t1) return 0; /* start of line */ if(!t2) return 0; /* end of line */ if(t1 < 0x80 && t2 < 0x80) return 0; /* 2 ASCII chars */ expand_token(ext1, t1, tok1); expand_token(ext2, t2, tok2); t1last = tok1[strlen((char *)tok1) - 1]; /* "PRINT" => "T" */ t2first = tok2[0]; /* "PRINT" => "P" */ /* if we already have a space, don't need to add another */ if(t1last == ' ' || t2first == ' ') return 0; if(!ext1) { /* IF, THEN, and operators like AND/OR/NOT always get a space after them, for readability. */ switch(t1) { case TOK_IF: case TOK_THEN: case TOK_ELSE: case TOK_AND: case TOK_OR: case TOK_NOT: return 1; default: break; } } if(!ext2) { /* these always get a space before them, for readability. */ switch(t2) { case TOK_THEN: case TOK_AND: case TOK_OR: case TOK_NOT: return 1; default: break; } } if(t1 >= 0x80 && isalnum(t1last)) { /* space not really required between OPEN/PRINT/CLOSE and #, but put one there for neatness. */ if(t2first == '#') return 1; /* same for POKE &52,0 or DATA &FF */ if(t2first == '&') return 1; /* INPUT "PROMPT";A$ or DATA "FOO" */ if(t2first == '"') return 1; } /* space not really required between a closing quote and a keyword, but put it in for neatness. examples: OPEN #1,"D:X" INPUT IF A$="FOO" THEN 10 PRINT "YOUR IQ IS" IQ these look weird without the space after the " */ if(isalnum(t2first)) { if(t1last == '|') return 1; if(t1last == '$') return 1; /* OPEN #1,F$ OUTPUT */ } return(isalnum(t1last) && isalnum(t2first)); } int decrunch_line(void) { unsigned char code[MAX_LINE_LEN_HARD + 10], byte = 0, prev; int lineno, ptr, codelen = 0, in_string = 0, in_comment = 0; static int addr = 0x700; /* doesn't really matter where */ ptr = read_prog_word(); if(!ptr) return -1; lineno = read_prog_word(); verbose(2, "decrunching line %d", lineno); while(1) { if(codelen >= MAX_LINE_LEN_HARD) die("line %d too long, decrunching failed", lineno); prev = byte; byte = read_prog_byte(); if(byte != 0) { if(byte == ' ' || is_comment_start(byte)) spacecount++; if(in_string) { if(byte == '|') in_string = 0; } else if(in_comment) { /* NOP */ } else { if(byte == '"') { in_string = 1; /* XXX: if a " ever occurs immediately after an extended token, this is wrong. I don't *think* the syntax allows that... */ if(need_space_between(0, 0, prev, byte)) code[codelen++] = ' '; } else if(is_comment_start(byte)) { in_comment = 1; } else if(byte == 0xff) { byte = read_prog_byte(); if(need_space_between(0, 1, prev, byte)) code[codelen++] = ' '; code[codelen++] = 0xff; code[codelen++] = byte; prev = byte; /* XXX: this next bit assumes there will never be two extended tokens in a row. I *think* this is true, but I'm not 100% certain. */ byte = read_prog_byte(); if(need_space_between(1, 0, prev, byte)) code[codelen++] = ' '; } else if(prev == ':' && byte == TOK_ELSE) { /* special case, ELSE needs a space before the (invisible) colon that comes before it. */ code[codelen - 1] = ' '; code[codelen++] = prev; } else if(need_space_between(0, 0, prev, byte)) { code[codelen++] = ' '; } } } code[codelen++] = byte; if(byte == 0) break; } codelen += 4; /* account for ptr and lineno */ addr += codelen; write_code(addr, lineno, code); return codelen; } /* despite the name, this handles both crunching and decrunching */ void crunch_program(void) { int newproglen = 0, linelen = 0; float percent; fputc(0x00, outfile); /* signature (0 = not locked) */ fputc(0x00, outfile); /* length LSB (fill in later) */ fputc(0x00, outfile); /* length MSB " " */ while((linelen = decrunch ? decrunch_line() : crunch_line()) != -1) newproglen += linelen; /* trailing $00 $00 */ fputc(0x00, outfile); fputc(0x00, outfile); /* fill in length in header */ if(fseek(outfile, 1L, SEEK_SET) < 0) os_err("fseek() failed"); newproglen += 2; /* account for trailing $00 $00 */ fputc(newproglen & 0xff, outfile); fputc((newproglen >> 8) & 0xff, outfile); fclose(outfile); /* show the user how the size changed */ newproglen += 3; percent = 100.0 * (1.0 - (float)newproglen / (float)proglen); verbose(1, "%scrunched %d byte program to %d bytes (%.1f%% %s)", decrunch ? "de" : "", bytes_read, newproglen, percent < 0 ? -percent : percent, percent < 0 ? "larger" : "smaller"); if(decrunch && spacecount) warn("%d spaces/comments, are you sure it was crunched?", spacecount); } 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 [[-l] | [-[acClnviutms] ... ] [-r *start,end*]] \n", self); puts(" -a: raw ATASCII output"); puts(" -c: check only (no listing)"); puts(" -C: crunch program"); puts(" -D: decrunch program"); puts(" -l: lock or unlock program"); puts(" -n: no blank line at start of listing"); puts(" -v: verbosity"); 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."); puts("if outfile not given, writes to stdout (via pipe to a8cat)"); } 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) { os_err("invalid line number for -r (range is 0-%d)", MAX_LINENO); } } else if(*p == ',' || *p == '-') { if(comma) os_err("invalid argument for -r (too many commas)"); comma++; startline = val; val = 0; } else { if(comma) os_err("invalid argument for -r (only digits and comma allowed)"); } p++; } if(comma) endline = val ? val : MAX_LINENO; else startline = endline = val; if(endline < startline) os_err("invalid argument for -r (start > end)"); } void parse_args(int argc, char **argv) { const char *a8cat; char tmp[10]; int opt; a8cat = getenv("A8CAT"); if(!a8cat) a8cat = "a8cat"; strncpy(pipe_command, a8cat, BUFSIZ); 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, "DCnlr:cvaiutmsh")) != -1) { switch(opt) { case 'D': crunch = decrunch = 1; break; case 'C': crunch = 1; break; case 'l': unlock_mode = 1; break; case 'c': check_only = 1; break; case 'a': raw_output = 1; break; case 'v': verbosity++ ; break; case 'n': initial_eol = 0; break; case 'h': print_help(); exit(0); case 'r': get_line_range(optarg); break; case 'i': case 'u': case 't': case 'm': case 's': if(strlen(pipe_command) > (BUFSIZ - 10)) os_err("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))) { print_help(); os_err("can't read binary data from a terminal"); } freopen(NULL, "rb", stdin); infile = stdin; } else { infile = fopen(argv[optind], "rb"); if(!infile) { os_err("%s: %s", argv[optind], strerror(errno)); } } optind++; if(optind < argc) { /* we were passed an output file */ outname = argv[optind]; if(check_only) os_err("can't use output filename %s with -c", outname); if(!freopen(outname, "wb", stdout)) os_err("%s: %s", outname, strerror(errno)); verbose(1, "redirected stdout to %s", outname); } else if(crunch) { os_err("can't use stdout with -C, must give an output filename"); } } void open_output() { if(check_only) { outfile = freopen("/dev/null", "wb", stdout); if(!outfile) { os_err("/dev/null: %s", strerror(errno)); } verbose(1, "using /dev/null for output (check_only)"); } else if(raw_output || unlock_mode || crunch) { if(isatty(fileno(stdout))) { os_err("refusing to write %s to a terminal", (raw_output ? "raw ATASCII" : "tokenized BASIC")); } outfile = stdout; verbose(1, "using stdout for output"); } else { const char *a8cat; if((a8cat = getenv("A8CAT"))) { verbose(1, "read A8CAT=%s from environment", a8cat); } else { verbose(1, "A8CAT not set in environment, using 'a8cat'"); } verbose(1, "using pipe for output: %s", pipe_command); outfile = popen(pipe_command, "w"); /* "wb" not allowed! */ /* we probably never see this error. popen() fails only if we feed it an invalid mode, or if fork() or pipe() fails, or I suppose if some idjit has done a "rm -f /bin/sh"... all other errors are caught at pclose(). */ if(!outfile) os_err("|%s: %s", pipe_command, strerror(errno)); need_pclose = 1; } } int main(int argc, char **argv) { set_self(argv[0]); parse_args(argc, argv); open_output(); read_header(); if(unlock_mode) { unlock_program(); exit(0); /* don't need finish() here, no parsing done */ } else if(crunch) { crunch_program(); exit(0); } start_listing(); while(next_line()) linecount++; finish(0); return 0; /* never executes; shuts up gcc warning */ }