aboutsummaryrefslogtreecommitdiff
path: root/listamsb.c
blob: 7195443f39f14aff369b802d5b29986b4b6d18cb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.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

/* AMSB's token for ELSE */
#define TOK_ELSE   0x9c

/* 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";
}

/* 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, "<unknown %stoken ", (ext ? "extended " : ""));
	fprintf(outfile, "%s%02x>", (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(byte == TOK_SQUOTE || byte == TOK_BANG || byte == TOK_REM)
				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(byte == TOK_REM || byte == TOK_SQUOTE || byte == TOK_BANG) {
					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)
		result = ext_tokens[t - MIN_EXT_TOK];
	else
		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 */

	if(t1 > MAX_STD_TOK || t2 > MAX_STD_TOK)
		die("invalid token in program, can't decrunch");

	expand_token(ext1, t1, tok1);
	expand_token(ext2, t2, tok2);
	t1last = tok1[strlen((char *)tok1) - 1]; /* "PRINT" => "T" */
	t2first = tok2[0];                       /* "PRINT" => "P" */

	/* space not really required between OPEN/PRINT/CLOSE and #,
	   but put one there for neatness. */
	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(t1last == '|' && isalnum(t2first)) return 1;

	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(in_string) {
				if(byte == '|')
					in_string = 0;
			} else if(in_comment) {
				/* NOP */
			} else {
				if(byte == '"') {
					in_string = 1;
				} else if(byte == TOK_REM || byte == TOK_SQUOTE || byte == TOK_BANG) {
					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(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");
}

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 [[-l] | [-[acClnviutms] ... ] [-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("  -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) {
	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, "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 {
		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 */
}