aboutsummaryrefslogtreecommitdiff
path: root/listamsb.c
diff options
context:
space:
mode:
Diffstat (limited to 'listamsb.c')
-rw-r--r--listamsb.c1010
1 files changed, 1010 insertions, 0 deletions
diff --git a/listamsb.c b/listamsb.c
new file mode 100644
index 0000000..3815db2
--- /dev/null
+++ b/listamsb.c
@@ -0,0 +1,1010 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+#include <stdarg.h>
+#include <ctype.h>
+#include <sys/stat.h>
+
+#include "atascii.h"
+#include "amsbtok.h"
+
+/* this should always be defined in <stdio.h>, 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
+
+#define ENV_VAR "COLORIZE_AMSB"
+
+const char *self;
+
+atascii_ctx actx;
+
+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 keep_rems = 0; /* -k */
+int color = 1; /* -m */
+
+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);
+}
+
+off_t get_file_len(const char *name) {
+ struct stat st;
+ if(stat(name, &st) != 0) return 0;
+ return st.st_size;
+}
+
+/* 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(header_read) {
+ if(!linecount) warn("no lines of code in program");
+ 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);
+ }
+ }
+ if(fgetc(infile) != EOF) warn("trailing garbage at end of file");
+ } else {
+ if(feof(infile))
+ warn("file is %d byte%s, too short to be an AMSB program", bytes_read, plural(bytes_read));
+ }
+
+ 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("colorize-amsb child process failed, do you have it on your PATH?");
+ }
+ } else {
+ fclose(outfile);
+ /* if we were writing to actual file and no data was written, rm it */
+ if(outname && get_file_len(outname) == 0)
+ unlink(outname);
+ }
+
+ 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");
+ else
+ verbose(1, "program is not locked");
+
+ 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(int tok) {
+ int ext;
+ if(!printing) return;
+ ext = (tok > 0xff);
+ tok &= 0xff;
+ fprintf(outfile, "<unknown %stoken ", (ext ? "extended " : ""));
+ fprintf(outfile, "%s%02x>", (ext ? "$ff ": ""), tok);
+}
+
+void list_char(unsigned char c) {
+ if(!printing) return;
+ if(raw_output) {
+ fputc(c, outfile);
+ } else {
+ char buf[20];
+ fputs(atascii_a2utf(&actx, c, buf), outfile);
+ }
+}
+
+int expand_token(int t, unsigned char *buf);
+
+int list_token(int tok) {
+ unsigned char text[10];
+ if(!expand_token(tok, text)) {
+ unknown_token(tok);
+ return 0;
+ }
+ if(printing) fputs((char *)text, outfile);
+ return 1;
+}
+
+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) {
+ if(raw_output) {
+ fputc(0x9b, outfile);
+ } else {
+ fputc('\n', outfile);
+ }
+ }
+}
+
+int read_token(int literal);
+
+/* 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 tok, ptr, lineno, was_colon = 0, in_string = 0, in_comment = 0, offset, len;
+
+ 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);
+
+ /* walk and print the tokens. when we hit a null byte, break out of the
+ loop, we're done with this line. */
+ while(1) {
+ tok = read_token(in_string || in_comment);
+
+ if(in_string) {
+ if(tok == 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(tok == '|') {
+ /* 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(tok);
+ /* one " character embedded in a string gets printed as "" */
+ if(tok == '"') list_char(tok);
+ }
+ continue;
+ } else if(in_comment) {
+ /* null byte ends both the comment and the line of code. */
+ if(tok == 0x00) break;
+ list_char(tok);
+ continue;
+ }
+
+ if(was_colon) {
+ if(tok != TOK_SQUOTE && tok != TOK_BANG && tok != TOK_ELSE) {
+ list_char(':');
+ }
+ was_colon = 0;
+ }
+
+ if(tok == ':') {
+ /* statement separator. don't print the colon yet, the next token
+ might be ELSE or a ! or ' for a comment */
+ was_colon = 1;
+ } else if(tok == '"') {
+ /* begin string. strings start but *don't end* with a double-quote */
+ in_string = 1;
+ list_char(tok);
+ } else if(tok >= 0x80) {
+ if(!list_token(tok))
+ warn("unknown %stoken %s$%02x at line %d",
+ (tok > 0xff ? "extended " : ""),
+ (tok > 0xff ? "$ff " : ""),
+ tok & 0xff,
+ lineno);
+ if(is_comment_start(tok)) in_comment = 1;
+ } else {
+ /* null byte means the line of code is done */
+ if(!tok) break;
+ if(tok < 0x20) {
+ /* ATASCII graphics outside of a string */
+ warn("line %d has character %d outside of a string, maybe Atari BASIC?",
+ lineno, tok);
+ }
+ if(tok == ' ') spacecount++;
+ list_char(tok);
+ }
+ }
+
+ 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);
+
+ if(!raw_output && (actx.inv)) {
+ char buf[20];
+ fputs(atascii_a2utf(&actx, ATA_CHR_FINISH, buf), outfile);
+ }
+
+ 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 comment_only_line(const unsigned char *code) {
+ if(code[0] == TOK_REM) return 1;
+ if(code[0] == ':') {
+ if(code[1] == TOK_SQUOTE || code[1] == TOK_BANG)
+ return 1;
+ }
+ return 0;
+}
+
+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(!keep_rems) {
+ if(comment_only_line(code)) {
+ verbose(1, "removing comment-only line %d", lineno);
+ return 0;
+ }
+ }
+
+ /* omit trailing comments */
+ if(commentstart > 1) {
+ code[commentstart - 1] = 0; /* null out the colon before the comment */
+ codelen = commentstart;
+
+ /* removing the comment from this: 10 PRINT :! BLAH
+ ...leaves a trailing colon. get rid of it if needed. */
+ if(codelen >= 2) {
+ if(code[codelen - 2] == ':') {
+ code[codelen - 2] = 0;
+ codelen--;
+ }
+ }
+ }
+
+ codelen += 4; /* account for ptr and lineno */
+ addr += codelen;
+
+ write_code(addr, lineno, code);
+
+ return codelen;
+}
+
+int expand_token(int t, unsigned char *buf) {
+ const char *result;
+
+ if(t < 0x80) {
+ buf[0] = t;
+ buf[1] = 0;
+ return 1;
+ }
+
+ if(t > 0xff) {
+ t &= 0xff;
+ if(t > MAX_EXT_TOK)
+ return 0;
+ result = ext_tokens[t - MIN_EXT_TOK];
+ } else {
+ if(t > MAX_STD_TOK)
+ return 0;
+ result = std_tokens[t - MIN_STD_TOK];
+ }
+
+ strcpy((char *)buf, result);
+ return 1;
+}
+
+int read_token(int literal) {
+ int tok = read_prog_byte();
+ if(literal) return tok;
+ if(tok == 0xff) {
+ tok <<= 8;
+ tok |= read_prog_byte();
+ }
+ return tok;
+}
+
+int token_is_extended(int tok) {
+ return tok > 0xff;
+}
+
+int append_token(int tok, unsigned char *buf, int len) {
+ if(token_is_extended(tok)) {
+ buf[len++] = 0xff;
+ buf[len++] = tok & 0xff;
+ } else {
+ buf[len++] = tok;
+ }
+ return len;
+}
+
+/* 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 t1, int t2) {
+ unsigned char text1[10], text2[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 */
+
+ if( !(expand_token(t1, text1) && expand_token(t2, text2)) )
+ die("invalid token in program, decrunching failed");
+
+ t1last = text1[strlen((char *)text1) - 1]; /* "PRINT" => "T" */
+ t2first = text2[0]; /* "PRINT" => "P" */
+
+ /* if we already have a space, don't need to add another */
+ if(t1last == ' ' || t2first == ' ') return 0;
+
+ /* 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;
+ }
+
+ /* 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];
+ int lineno, tok = 0, prev, 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 = tok;
+ tok = read_token(in_string || in_comment);
+
+ if(tok != 0) {
+ if(in_string) {
+ if(tok == '|')
+ in_string = 0;
+ } else if(in_comment) {
+ /* NOP */
+ } else {
+ if(tok == ' ' || is_comment_start(tok)) {
+ verbose(1, "found space/comment at line %d", lineno);
+ spacecount++;
+ }
+ if(tok == '"') {
+ in_string = 1;
+ if(need_space_between(prev, tok))
+ code[codelen++] = ' ';
+ } else if(is_comment_start(tok)) {
+ in_comment = 1;
+ } else if(prev == ':' && tok == 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(prev, tok)) {
+ code[codelen++] = ' ';
+ }
+ }
+ }
+
+ codelen = append_token(tok, code, codelen);
+ if(tok == 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)bytes_read);
+ 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 <urchlay@slackware.uk>, released under the WTFPL");
+ printf("Usage: %s [[-[acCDUMnviutms] ...] [-r *start,end*]] <file> <outfile>\n", self);
+ puts(" -a: raw ATASCII output");
+ puts(" -c: check only (no listing)");
+ puts(" -C: crunch program");
+ puts(" -D: decrunch program");
+ puts(" -U: lock or unlock program");
+ puts(" -M: monochrome listing (no colorization)");
+ 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: same as 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");
+}
+
+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) {
+ int opt, ropt = 0;
+ int ata_mode = ATA_MODE_UTF8, ata_flags = ATA_FLAG_NONE, a8catopt = 0;
+
+ 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, "MDCnLUlr:cvaiutmshk")) != -1) {
+ switch(opt) {
+ case 'L': crunch = decrunch = unlock_mode = 0; break;
+ case 'D': decrunch = 1; break;
+ case 'C': crunch = 1; break;
+ case 'l':
+ case 'U': unlock_mode = 1; break;
+ case 'k': keep_rems = 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 'M': color = 0; break;
+ case 'h': print_help(); exit(0);
+ case 'r': get_line_range(optarg); ropt++; break;
+ case 'i': ata_flags |= ATA_FLAG_ICS; a8catopt++; break;
+ case 'u': ata_flags |= ATA_FLAG_UNDERLINE; a8catopt++; break;
+ case 't': ata_flags |= ATA_FLAG_ICS; a8catopt++; break;
+ case 'm': ata_mode = ATA_MODE_MAGAZINE; a8catopt++; break;
+ case 's': ata_flags |= ATA_FLAG_STRIP_INVERSE; a8catopt++; break;
+ default: print_help(); exit(1);
+ }
+ }
+
+ if((unlock_mode + crunch + decrunch) > 1) {
+ os_err("only one of -C, -D, -L, -U may be used");
+ }
+
+ if(keep_rems && !decrunch) {
+ warn("-k option ignored since -D not given");
+ }
+
+ if(unlock_mode || crunch || decrunch) {
+ if(!initial_eol || check_only || raw_output || ropt || a8catopt || !color) {
+ warn("-[acimMnrstu] options ignored with -U, -C, or -D");
+ }
+ } else if(!raw_output) {
+ atascii_context_init(&actx, ata_mode, ata_flags);
+ }
+
+ 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 || decrunch) {
+ os_err("can't use stdout with -%c, must give an output filename",
+ crunch ? 'C' : 'D');
+ }
+}
+
+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 || decrunch) {
+ 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 {
+ if(color) {
+ char *exe = getenv(ENV_VAR);
+ if(!exe) exe = "colorize-amsb";
+ verbose(1, "using pipe for output: %s", exe);
+ outfile = popen(exe, "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", exe, strerror(errno));
+ need_pclose = 1;
+ } else {
+ outfile = stdout;
+ }
+ }
+}
+
+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 || decrunch) {
+ crunch_program();
+ exit(0);
+ }
+
+ start_listing();
+
+ while(next_line())
+ linecount++;
+
+ finish(0);
+ return 0; /* never executes; shuts up gcc warning */
+}