aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile16
-rw-r--r--README.txt14
-rw-r--r--a8eol.17
-rw-r--r--a8utf8.17
-rw-r--r--atascii.711
-rw-r--r--atascii.rst2
-rw-r--r--atr2xfd.17
-rw-r--r--atrsize.17
-rw-r--r--axe.17
-rw-r--r--bas.c361
-rw-r--r--bas.h133
-rw-r--r--blob2c.17
-rw-r--r--blob2xex.17
-rw-r--r--cart2xex.17
-rw-r--r--dasm2atasm.17
-rw-r--r--dumpbas.1235
-rw-r--r--dumpbas.c310
-rw-r--r--dumpbas.rst155
-rw-r--r--fenders.17
-rw-r--r--jindroush/acvt/Makefile4
-rw-r--r--jindroush/adir/Makefile4
-rw-r--r--jindroush/chkbas/chkbas.cpp2
-rw-r--r--jindroush/man/chkbas.14
-rw-r--r--jindroush/man/chkbas.rst2
-rw-r--r--manftr.rst5
-rw-r--r--manhdr5.rst7
-rw-r--r--protbas.1142
-rw-r--r--protbas.c195
-rw-r--r--protbas.rst86
-rw-r--r--renumbas.1184
-rw-r--r--renumbas.c403
-rw-r--r--renumbas.rst116
-rw-r--r--rom2cart.17
-rw-r--r--unmac65.17
-rw-r--r--unprotbas.1188
-rw-r--r--unprotbas.c526
-rw-r--r--unprotbas.rst174
-rw-r--r--vxrefbas.1187
-rw-r--r--vxrefbas.c118
-rw-r--r--vxrefbas.rst102
-rw-r--r--xex.57
-rw-r--r--xex1to2.17
-rw-r--r--xexamine.17
-rw-r--r--xexcat.17
-rw-r--r--xexsplit.17
-rw-r--r--xfd2atr.17
46 files changed, 3453 insertions, 359 deletions
diff --git a/Makefile b/Makefile
index d1bb32f..8bd3ebf 100644
--- a/Makefile
+++ b/Makefile
@@ -16,9 +16,9 @@ CC=gcc
CFLAGS=-Wall $(COPT) -ansi -D_GNU_SOURCE -DVERSION=\"$(VERSION)\"
# BINS and SCRIPTS go in $BINDIR, DOCS go in $DOCDIR
-BINS=a8eol xfd2atr atr2xfd blob2c cart2xex fenders xexsplit xexcat atrsize rom2cart unmac65 axe blob2xex xexamine xex1to2 unprotbas
+BINS=a8eol atr2xfd atrsize axe blob2c blob2xex cart2xex dumpbas fenders protbas renumbas rom2cart unmac65 unprotbas vxrefbas xex1to2 xexamine xexcat xexsplit xfd2atr
SCRIPTS=dasm2atasm a8utf8
-MANS=a8eol.1 xfd2atr.1 atr2xfd.1 blob2c.1 cart2xex.1 fenders.1 xexsplit.1 xexcat.1 atrsize.1 rom2cart.1 unmac65.1 axe.1 dasm2atasm.1 a8utf8.1 blob2xex.1 xexamine.1 xex1to2.1 unprotbas.1
+MANS=a8eol.1 xfd2atr.1 atr2xfd.1 blob2c.1 cart2xex.1 fenders.1 xexsplit.1 xexcat.1 atrsize.1 rom2cart.1 unmac65.1 axe.1 dasm2atasm.1 a8utf8.1 blob2xex.1 xexamine.1 xex1to2.1 unprotbas.1 protbas.1 renumbas.1 dumpbas.1 vxrefbas.1
MAN5S=xex.5
MAN7S=atascii.7
DOCS=README.txt equates.inc *.dasm LICENSE ksiders/atr.txt
@@ -49,6 +49,18 @@ RST2MAN=rst2man
all: $(BINS) manpages symlinks subdirs
+unprotbas: bas.o
+
+protbas: bas.o
+
+renumbas: bas.o
+
+dumpbas: bas.o
+
+vxrefbas: bas.o
+
+bas.o: bas.c bas.h
+
subdirs:
for dir in $(SUBDIRS); do make -C $$dir COPT=$(COPT); done
diff --git a/README.txt b/README.txt
index 76003f8..8d12230 100644
--- a/README.txt
+++ b/README.txt
@@ -7,9 +7,6 @@ a8eol - Convert Atari 8-bit text files to/from UNIX / DOS / Mac Classic
a8utf8 - Convert Atari 8-bit text to UTF-8 encoded Unicode.
-axe - ATR/XFD Editor. Copy files into & out of ATR and XFD images,
- create blank ATR images, etc.
-
atr2xfd - Convert an Atari 8-bit ATR disk image to a raw (XFD) image.
atrcheck - Check an Atari 8-bit ATR disk image for various types of problems.
@@ -17,6 +14,9 @@ atrcheck - Check an Atari 8-bit ATR disk image for various types of problems.
atrsize - Change the size of an Atari 8-bit ATR disk image, or create
a blank ATR image.
+axe - ATR/XFD Editor. Copy files into & out of ATR and XFD images,
+ create blank ATR images, etc.
+
blob2c - Create C source and header files from a binary file.
blob2xex - Create a XEX file from arbitrary data.
@@ -28,8 +28,14 @@ cart2xex - Convert an Atari 8-bit ROM cartridge image to a binary load
dasm2atasm - Convert 6502 assembly in DASM syntax to ATASM (or MAC/65) format.
+dumpbas - Formatted hexdump for tokenized Atari 8-bit BASIC files.
+
fenders - Install Fenders 3-sector loader in boot sectors of an ATR image.
+protbas - LIST-protect Atari 8-bit BASIC programs.
+
+renumbas - Renumber Atari 8-bit BASIC programs.
+
rom2cart - Convert a raw Atari 8-bit cartridge ROM image to a CART
image for use with emulators such as Atari800.
@@ -56,7 +62,7 @@ the Atari 8-bit system equates. It's meant to be used with either the
DASM or ATASM 6502 cross assemblers.
Also included: collections of utilities by Ken Siders and Jindrich
-Kubec. See the README.txt files in the ksiders/ and jindroush/
+Kubec. See the README.txt files in the ksiders/ and jindroush/
directories for details.
To install, use the standard "make && make install" process. The default
diff --git a/a8eol.1 b/a8eol.1
index b68779d..a68ed2b 100644
--- a/a8eol.1
+++ b/a8eol.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "A8EOL" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "A8EOL" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
a8eol \- convert Atari 8-bit text files to/from UNIX/Windows/Mac
.\" RST source for a8eol(1) man page. Convert with:
@@ -470,10 +470,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/a8utf8.1 b/a8utf8.1
index beee34b..ada18ea 100644
--- a/a8utf8.1
+++ b/a8utf8.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "A8UTF8" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "A8UTF8" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
a8utf8 \- Convert Atari 8-bit text to UTF-8 encoded Unicode.
.\" RST source for a8utf8(1) man page. Convert with:
@@ -101,10 +101,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/atascii.7 b/atascii.7
index 4498caf..7032d78 100644
--- a/atascii.7
+++ b/atascii.7
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "ATASCII" 7 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "ATASCII" 7 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
atascii \- Atari 8-bit character set
.\" RST source for atascii(7) man page. Convert with:
@@ -1687,9 +1687,7 @@ T{
T} T{
7c
T} T{
-.nf
-
-.fi
+|
T} T{
252
T} T{
@@ -2161,10 +2159,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/atascii.rst b/atascii.rst
index b2ea369..8f96d2a 100644
--- a/atascii.rst
+++ b/atascii.rst
@@ -153,7 +153,7 @@ brackets denote screen control codes.
"121", "79", "y", "249", "f9", ""
"122", "7a", "z", "250", "fa", ""
"123", "7b", "♠", "251", "fb", ""
- "124", "7c", "|", "252", "fc", ""
+ "124", "7c", "\|", "252", "fc", ""
"125", "7d", "[clear screen]", "253", "fd", "[bell]"
"126", "7e", "[delete]", "254", "fe", "[delete char]"
"127", "7f", "[tab]", "255", "ff", "[insert char]"
diff --git a/atr2xfd.1 b/atr2xfd.1
index a192c2c..e4a91e9 100644
--- a/atr2xfd.1
+++ b/atr2xfd.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "ATR2XFD" 1 "2024-05-05" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "ATR2XFD" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
atr2xfd \- Convert an Atari 8-bit ATR disk image to a raw (XFD) image
.\" RST source for atr2xfd(1) man page. Convert with:
@@ -190,10 +190,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/atrsize.1 b/atrsize.1
index 031e230..f87edce 100644
--- a/atrsize.1
+++ b/atrsize.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "ATRSIZE" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "ATRSIZE" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
atrsize \- Change the size of an Atari 8-bit ATR disk image, or create a blank ATR image
.\" RST source for atrsize(1) man page. Convert with:
@@ -204,10 +204,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/axe.1 b/axe.1
index f22b54f..c0b658d 100644
--- a/axe.1
+++ b/axe.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "AXE" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "AXE" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
axe \- ATR/XFD Editor
.\" RST source for axe(1) man page. Convert with:
@@ -145,10 +145,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/bas.c b/bas.c
new file mode 100644
index 0000000..fc5517b
--- /dev/null
+++ b/bas.c
@@ -0,0 +1,361 @@
+/* bas.c - API for writing standalone programs that deal with
+ tokenized Atari 8-bit BASIC program. */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+
+int verbose = 0;
+unsigned short lomem;
+unsigned short vntp;
+unsigned short vntd;
+unsigned short vvtp;
+unsigned short stmtab;
+unsigned short stmcur;
+unsigned short starp;
+unsigned short codestart;
+unsigned short code_end;
+unsigned short vnstart;
+unsigned short vvstart;
+int filelen;
+const char *self;
+unsigned char program[BUFSIZE];
+FILE *input_file;
+FILE *output_file;
+char *output_filename = NULL;
+
+void die(const char *msg) {
+ fprintf(stderr, "%s: %s\n", self, msg);
+ exit(1);
+}
+
+void parse_general_args(int argc, char **argv, void (*helpfunc)()) {
+ if(argc < 2) {
+ (*helpfunc)();
+ exit(1);
+ }
+
+ if(strcmp(argv[1], "--help") == 0) {
+ (*helpfunc)();
+ exit(0);
+ }
+
+ if(strcmp(argv[1], "--version") == 0) {
+ printf("%s %s\n", self, VERSION);
+ exit(0);
+ }
+}
+
+/* read entire file into memory */
+void readfile(void) {
+ filelen = fread(program, 1, BUFSIZE - 1, input_file);
+ if(verbose) fprintf(stderr, "Read %d bytes.\n", filelen);
+ if(!feof(input_file))
+ fprintf(stderr, "Warning: file is >64KB, way too big for a BASIC program.\n");
+ else if(filelen > MAX_PROG_SIZE)
+ fprintf(stderr, "Warning: file is %d bytes, suspiciously large for a BASIC program.\n", filelen);
+ fclose(input_file);
+ if(filelen < MIN_PROG_SIZE)
+ die("File too short to be a BASIC program (truncated?)\n");
+}
+
+int writefile(void) {
+ int outbytes;
+
+ outbytes = fwrite(program, 1, filelen, output_file);
+ fclose(output_file);
+ if(verbose) fprintf(stderr, "Wrote %d bytes.\n", outbytes);
+ return outbytes;
+}
+
+/* get a 16-bit value from the file, in 6502 LSB/MSB order. */
+unsigned short getword(int addr) {
+ return program[addr] | (program[addr + 1] << 8);
+}
+
+void setword(int addr, int value) {
+ program[addr] = value & 0xff;
+ program[addr + 1] = value >> 8;
+}
+
+void dump_header_vars(void) {
+ fprintf(stderr, "LOMEM $%04x VNTP $%04x VNTD $%04x VVTP $%04x\n", lomem, vntp, vntd, vvtp);
+ fprintf(stderr, "STMTAB $%04x STMCUR $%04x STARP $%04x\n", stmtab, stmcur, starp);
+ fprintf(stderr, "vnstart $%04x, vvstart $%04x, codestart $%04x, code_end $%04x\n", vnstart, vvstart, codestart, code_end);
+}
+
+void parse_header(void) {
+ lomem = getword(0);
+ vntp = getword(2);
+ vntd = getword(4);
+ vvtp = getword(6);
+ stmtab = getword(8);
+ stmcur = getword(10);
+ starp = getword(12);
+ codestart = stmtab - TBL_OFFSET - (vntp - 256);
+ vnstart = vntp - TBL_OFFSET;
+ vvstart = vvtp - TBL_OFFSET;
+ code_end = starp - TBL_OFFSET;
+
+ if(lomem) die("This doesn't look like an Atari BASIC program (no $0000 signature).");
+
+ if(filelen < code_end) {
+ fprintf(stderr, "Warning: file is truncated: %d bytes, should be %d.\n", filelen, code_end);
+ }
+
+ if(verbose) dump_header_vars();
+}
+
+void update_header(void) {
+ setword(0, lomem);
+ setword(2, vntp);
+ setword(4, vntd);
+ setword(6, vvtp);
+ setword(8, stmtab);
+ setword(10, stmcur);
+ setword(12, starp);
+}
+
+/* sometimes the variable name table isn't large enough to hold
+ the generated variable names. move_code() makes more space,
+ by moving the rest of the program (including the variable value
+ table) up in memory. */
+void move_code(int offset) {
+ unsigned char *dest = program + vvstart + offset;
+
+ if(dest < program || ((filelen + offset) > (BUFSIZE - 1))) {
+ die("Attempt to move memory out of range; corrupt header bytes?\n");
+ }
+
+ memmove(dest, program + vvstart, filelen);
+
+ vntd += offset;
+ vvtp += offset;
+ stmtab += offset;
+ stmcur += offset;
+ starp += offset;
+ filelen += offset;
+ update_header();
+ parse_header();
+}
+
+void adjust_vntable_size(int oldsize, int newsize) {
+ int move_by;
+ if(oldsize != newsize) {
+ move_by = newsize - oldsize;
+ if(verbose) fprintf(stderr,
+ "Need %d bytes for vntable, have %d, moving VVTP by %d to $%04x.\n",
+ newsize, oldsize, move_by, vvtp + move_by);
+ move_code(move_by);
+ }
+}
+
+unsigned char get_vartype(unsigned char tok) {
+ return program[vvstart + (tok & 0x7f) * 8] >> 6;
+}
+
+/* return true if the variable name table is OK */
+int vntable_ok(void) {
+ int vp, bad;
+
+ if(vntp == vntd) {
+ if(verbose) fprintf(stderr, "No variables.\n");
+ return 1;
+ }
+
+ /* first pass: bad = 1 if all the bytes in the table have the same
+ value, no matter what it is. */
+ vp = vnstart + 1;
+ bad = 1;
+ while(vp < vvstart - 1) {
+ if(program[vp] != program[vnstart]) {
+ bad = 0;
+ break;
+ }
+ vp++;
+ }
+ if(bad) return 0;
+
+ /* 2nd pass: bad = 1 if there's any invalid character in the table. */
+ vp = vnstart;
+ while(vp < vvstart) {
+ unsigned char c = program[vp];
+
+ /* treat a null byte as end-of-table, ignore any junk between it and VNTP. */
+ if(c == 0) break;
+
+ vp++;
+
+ /* inverse $ or ( is OK */
+ if(c == 0xa4 || c == 0xa8) continue;
+
+ /* numbers and letters are allowed, inverse or normal. */
+ c &= 0x7f;
+ if(c >= 0x30 && c <= 0x39) continue;
+ if(c >= 0x41 && c <= 0x5a) continue;
+
+ bad++;
+ break;
+ }
+
+ return !bad;
+}
+
+void invalid_args(const char *arg) {
+ fprintf(stderr, "%s: Invalid argument '%s'.\n\n", self, arg);
+ exit(1);
+}
+
+FILE *open_file(const char *name, const char *mode) {
+ FILE *fp;
+ if(!(fp = fopen(name, mode))) {
+ perror(name);
+ exit(1);
+ }
+ return fp;
+}
+
+void open_input(const char *name) {
+ if(!name || strcmp(name, "-") == 0) {
+ if(isatty(fileno(stdin))) {
+ die("Can't read binary data from the terminal.");
+ }
+ if(freopen(NULL, "rb", stdin)) {
+ input_file = stdin;
+ return;
+ } else {
+ perror("stdin");
+ exit(1);
+ }
+ }
+
+ input_file = open_file(name, "rb");
+}
+
+void open_output(const char *name) {
+ if(!name || (strcmp(name, "-") == 0)) {
+ if(isatty(fileno(stdout))) {
+ die("Refusing to write binary data to the terminal.");
+ }
+ if(freopen(NULL, "wb", stdout)) {
+ output_file = stdout;
+ return;
+ } else {
+ perror("stdout");
+ exit(1);
+ }
+ }
+ output_file = open_file(name, "wb");
+}
+
+void set_self(const char *argv0) {
+ char *p;
+
+ self = argv0;
+ p = strrchr(self, '/');
+ if(p) self = p + 1;
+}
+
+/* callbacks */
+CALLBACK_PTR(on_start_line);
+CALLBACK_PTR(on_bad_line_length);
+CALLBACK_PTR(on_end_line);
+CALLBACK_PTR(on_start_stmt);
+CALLBACK_PTR(on_end_stmt);
+CALLBACK_PTR(on_cmd_token);
+CALLBACK_PTR(on_text);
+CALLBACK_PTR(on_exp_token);
+CALLBACK_PTR(on_var_token);
+CALLBACK_PTR(on_string_const);
+CALLBACK_PTR(on_num_const);
+
+#define CALL(x) if(x) (*x)(lineno, pos, program[pos], end)
+
+void walk_code(unsigned int startlineno, unsigned int endlineno) {
+ int linepos, nextpos, offset, soffset, lineno, pos, end, tok;
+
+ linepos = codestart;
+ while(linepos < filelen) { /* loop over lines */
+ lineno = getword(linepos);
+ offset = program[linepos + 2];
+ nextpos = linepos + offset;
+
+ end = nextpos;
+ pos = linepos;
+
+ if(offset < 6) {
+ CALL(on_bad_line_length);
+ offset = program[linepos + 2]; /* on_bad_line_length fixed it (we hope) */
+ if(offset < 6)
+ die("Program is code-protected; unprotect it first.");
+ }
+
+ if(lineno < startlineno) {
+ linepos = nextpos;
+ continue;
+ }
+
+ CALL(on_start_line);
+
+ pos = linepos + 3;
+ while(pos < nextpos) { /* loop over statements within a line */
+ soffset = program[pos];
+ end = linepos + soffset;
+ CALL(on_start_stmt);
+
+ while(pos < end) { /* loop over tokens within a statement */
+ pos++;
+ CALL(on_cmd_token);
+ switch(program[pos]) {
+ case CMD_REM:
+ case CMD_DATA:
+ case CMD_ERROR:
+ pos++;
+ CALL(on_text);
+ pos = end;
+ break;
+ default:
+ pos++;
+ break;
+ }
+
+ while(pos < end) { /* loop over operators */
+ tok = program[pos];
+ switch(tok) {
+ case OP_NUMCONST:
+ CALL(on_exp_token);
+ pos++;
+ CALL(on_num_const);
+ pos += 6;
+ break;
+ case OP_STRCONST:
+ CALL(on_exp_token);
+ pos++;
+ CALL(on_string_const);
+ pos += program[pos] + 1;
+ break;
+ default:
+ if(tok & 0x80) {
+ CALL(on_var_token);
+ } else {
+ CALL(on_exp_token);
+ }
+ pos++;
+ break;
+ }
+ }
+ CALL(on_end_stmt);
+ }
+ }
+
+ CALL(on_end_line);
+
+ if(lineno >= endlineno) break;
+ linepos = nextpos;
+ }
+}
diff --git a/bas.h b/bas.h
new file mode 100644
index 0000000..ce67309
--- /dev/null
+++ b/bas.h
@@ -0,0 +1,133 @@
+/* bas.h - API for writing standalone programs that deal with
+ tokenized Atari 8-bit BASIC program. */
+
+/* maximum size of the program in memory. 64KB is actually way overkill. */
+#define BUFSIZE 65536
+
+/* the difference between the VVTP and VNTP values in the file, and the
+ actual file positions of the variable names and values. */
+#define TBL_OFFSET 0xf2
+
+/* minimum program size, for a program that has no variables and
+ only one line of code (the immediate line 32768, consisting only of
+ one token, which would be CSAVE). anything smaller than this, we
+ can't process. */
+#define MIN_PROG_SIZE 21
+
+/* maximum practical size for a BASIC program. if a file exceeds this
+ size, we warn about it, but otherwise process it normally.
+ this value is derived by subtracting the default LOMEM without DOS
+ ($0700) from the start of the display list in GR.0 ($9c20, on a 48K Atari).
+ */
+#define MAX_PROG_SIZE 38176
+
+/* maximum number of variables in the variable name and value tables. this
+ could be 128, but "ERROR- 4" still expands the tables. Entries >128
+ don't have tokens, can't be referred to in code, but we'll preserve
+ them anyway. */
+#define MAXVARS 256
+
+/* tokenized colon (statement separator) */
+#define TOK_COLON 0x14
+
+/* BASIC tokens. Not a full set. BASIC uses 2 sets of tokens, one
+ for commands and the other for operators (which is to say, everything
+ *not* a command). */
+#define CMD_GOTO 0x0a
+#define CMD_GO_TO 0x0b
+#define CMD_GOSUB 0x0c
+#define CMD_TRAP 0x0d
+#define CMD_LIST 0x04
+#define CMD_RESTORE 0x23
+#define CMD_REM 0x00
+#define CMD_DATA 0x01
+#define CMD_ERROR 0x37
+#define CMD_FOR 0x08
+#define CMD_NEXT 0x09
+#define CMD_LET 0x06
+#define CMD_ILET 0x36
+#define CMD_DIM 0x14
+#define CMD_READ 0x22
+#define CMD_INPUT 0x02
+#define OP_GOTO 0x17
+#define OP_GOSUB 0x18
+#define OP_THEN 0x1b
+#define OP_COMMA 0x12
+#define OP_EOS 0x14
+#define OP_EOL 0x16
+#define OP_NUMCONST 0x0e
+#define OP_STRCONST 0x0f
+
+/* variable types, bits 6-7 of byte 0 of each vvtable entry. */
+#define TYPE_SCALAR 0
+#define TYPE_ARRAY 1
+#define TYPE_STRING 2
+
+/* callbacks */
+#define CALLBACK(x) void x(unsigned int lineno, unsigned int pos, unsigned int tok, unsigned int end)
+#define CALLBACK_PTR(x) void (*x)(unsigned int lineno, unsigned int pos, unsigned int tok, unsigned int end)
+#define walk_all_code() walk_code(0, 32768)
+
+void walk_code(unsigned int startlineno, unsigned int endlineno);
+unsigned char get_vartype(unsigned char tok);
+
+extern CALLBACK_PTR(on_start_line);
+extern CALLBACK_PTR(on_bad_line_length);
+extern CALLBACK_PTR(on_end_line);
+extern CALLBACK_PTR(on_start_stmt);
+extern CALLBACK_PTR(on_end_stmt);
+extern CALLBACK_PTR(on_cmd_token);
+extern CALLBACK_PTR(on_text);
+extern CALLBACK_PTR(on_exp_token);
+extern CALLBACK_PTR(on_var_token);
+extern CALLBACK_PTR(on_string_const);
+extern CALLBACK_PTR(on_num_const);
+
+/* BASIC 14-byte header values */
+extern unsigned short lomem;
+extern unsigned short vntp;
+extern unsigned short vntd;
+extern unsigned short vvtp;
+extern unsigned short stmtab;
+extern unsigned short stmcur;
+extern unsigned short starp;
+
+/* positions where various parts of the file start,
+ derived from the header vars above. */
+extern unsigned short codestart;
+extern unsigned short code_end;
+extern unsigned short vnstart;
+extern unsigned short vvstart;
+extern int filelen;
+
+/* name of executable, taken from argv[0] */
+extern const char *self;
+
+/* entire file gets read into memory (for now) */
+extern unsigned char program[BUFSIZE];
+
+/* file handles */
+extern FILE *input_file;
+extern FILE *output_file;
+
+extern char *output_filename;
+
+extern int verbose;
+
+extern void set_self(const char *argv0);
+extern void die(const char *msg);
+extern void parse_general_args(int argc, char **argv, void (*helpfunc)());
+extern int writefile(void);
+extern void readfile(void);
+extern unsigned short getword(int addr);
+extern void setword(int addr, int value);
+extern void dump_header_vars(void);
+extern void parse_header(void);
+extern void update_header(void);
+extern void move_code(int offset);
+extern void adjust_vntable_size(int oldsize, int newsize);
+extern int vntable_ok(void);
+extern void invalid_args(const char *arg);
+extern FILE *open_file(const char *name, const char *mode);
+extern void open_input(const char *name);
+extern void open_output(const char *name);
diff --git a/blob2c.1 b/blob2c.1
index f59dbe2..fefda0e 100644
--- a/blob2c.1
+++ b/blob2c.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "BLOB2C" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "BLOB2C" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
blob2c \- Create C source and header files from a binary file
.\" RST source for blob2c(1) man page. Convert with:
@@ -125,10 +125,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/blob2xex.1 b/blob2xex.1
index a13f982..4a37c3b 100644
--- a/blob2xex.1
+++ b/blob2xex.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "BLOB2XEX" 1 "2024-05-17" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "BLOB2XEX" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
blob2xex \- Create Atari 8-bit executables from arbitrary data
.\" RST source for blob2xex(1) man page. Convert with:
@@ -216,10 +216,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/cart2xex.1 b/cart2xex.1
index 19ea56b..c20e98f 100644
--- a/cart2xex.1
+++ b/cart2xex.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "CART2XEX" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "CART2XEX" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
cart2xex \- Convert an Atari 8-bit ROM cartridge image to a binary load file
.\" RST source for cart2xex(1) man page. Convert with:
@@ -235,10 +235,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/dasm2atasm.1 b/dasm2atasm.1
index 699f97d..f37187a 100644
--- a/dasm2atasm.1
+++ b/dasm2atasm.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "DASM2ATASM" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "DASM2ATASM" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
dasm2atasm \- Convert 6502 assembly source from dasm syntax to atasm or ca65 syntax.
.\" RST source for dasm2atasm(1) man page. Convert with:
@@ -232,10 +232,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/dumpbas.1 b/dumpbas.1
new file mode 100644
index 0000000..f2fa471
--- /dev/null
+++ b/dumpbas.1
@@ -0,0 +1,235 @@
+.\" Man page generated from reStructuredText.
+.
+.
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+.TH "DUMPBAS" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+dumpbas \- Formatted hexdump for tokenized Atari 8-bit BASIC files
+.SH SYNOPSIS
+.sp
+dumpbas [\fB\-v\fP] [\fB\-l\fP \fIlineno\fP] [\fB\-s\fP \fIstart\-lineno\fP] [\fB\-e\fP \fIend\-lineno\fP] \fIinput\-file\fP
+.SH DESCRIPTION
+.sp
+\fBdumpbas\fP reads a tokenized Atari 8\-bit BASIC program and prints a
+formatted hexdump on standard output. The formatting groups the hex bytes
+by line and statement, and includes special characters to mark different
+types of token (see \fBFORMATTING\fP, below).
+.sp
+\fBdumpbas\fP does not detokenize BASIC programs or dump information
+about variable names/values. Use \fBchkbas\fP(1) for that. This tool is
+intended to help the user learn about the tokenized BASIC format, or
+as an aid for developing/debugging other tools that process tokenized
+files. It\(aqs an alternative to looking at raw hex dumps.
+.sp
+It\(aqs assumed the user has at least some knowledge of BASIC\(aqs tokenized
+SAVE format. The \fBAtari BASIC Sourcebook\fP is a good starting point
+for learning the tokenized format.
+.SH OPTIONS
+.SS General Options
+.INDENT 0.0
+.TP
+.B \fB\-\-help\fP
+Print usage message and exit.
+.TP
+.B \fB\-\-version\fP
+Print version number and exit.
+.TP
+.B \fB\-v\fP
+Verbose operation. When displaying a number in verbose mode, it will
+be prefixed with \fI$\fP if it\(aqs in hex, or no prefix for decimal.
+.UNINDENT
+.SS Dump Options
+.INDENT 0.0
+.TP
+.B \fB\-s\fP \fIstart\-lineno\fP
+Don\(aqt dump lines before \fBstart\-lineno\fP\&. Default: \fI0\fP\&.
+.TP
+.B \fB\-e\fP \fIend\-lineno\fP
+Don\(aqt dump lines after \fBstart\-lineno\fP\&. Default: \fI32768\fP\&.
+.TP
+.B \fB\-l\fP \fIlineno\fP
+Only dump one line. This is exactly equivalent to "\fB\-s\fP \fIlineno\fP \fB\-e\fP \fIlineno\fP".
+.UNINDENT
+.SH FORMATTING
+.sp
+Every byte in the file is displayed in hex. However, they are grouped by line
+and statement, and certain tokens get marker characters to help keep track
+of what they\(aqre for. Strings are displayed in quotes, in both hex and ASCII. Floating
+point constants are displayed as 6 hex bytes with square brackets around them.
+.SS Line Header Markers
+.INDENT 0.0
+.TP
+.B \fB@\fP
+Separates decimal line number from hex file offset.
+.TP
+.B \fB^\fP
+Prefix for line length.
+.TP
+.B \fB(\fP \fB)\fP
+Surrounds the 2 hex bytes for the line number.
+.UNINDENT
+.SS Statement Markers
+.INDENT 0.0
+.TP
+.B \fB>\fP
+Prefix for next\-statement offset. Every statement begins with this.
+.TP
+.B \fB!\fP
+Prefix for a command token. Every line of BASIC code begins with a
+command.
+.TP
+.B \fB:\fP
+Suffix for the \fI14\fP token; end of statement.
+.TP
+.B \fB#\fP
+Prefix for the \fI0e\fP token, which introduces a BCD floating point constant.
+.TP
+.B \fB[\fP \fB]\fP
+Surrounds the 6 bytes of a BCD floating point constant.
+.TP
+.B \fB$\fP
+Prefix for the \fI0f\fP token, which introduces a string constant.
+.TP
+.B \fB=\fP
+Prefix for the string\-length byte of a string constant.
+.UNINDENT
+.SS String Byte Markers
+.INDENT 0.0
+.TP
+.B \fB"\fP
+A string constant is surrounded by double\-quotes.
+.TP
+.B \fB^\fP
+Prefix for a control character. For instance, \fI03\fP is displayed as \fI^C\fP\&.
+.TP
+.B \fB|\fP
+Prefix for an inverse video character. Example: \fIc1\fP (inverse video \fIA\fP)
+is displayed as \fI|A\fP\&. May be combined with \fI^\fP, for inverse control characters.
+.TP
+.B \fB/\fP
+Separates the printable ASCII representation of a character from its hex byte.
+Example: \fIA/41\fP\&.
+.UNINDENT
+.SS Line header
+.sp
+Each line number begins with the line number (decimal) and offset from
+the start of the file (hex), followed by the 2 hex bytes for the line
+number in parentheses, followed by the line length (hex, preceded by
+^). Example:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+10@0018 (0a 00): ^19
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+The line number is \fI10\fP, and the file offset is \fI0018\fP\&. The \fI0a 00\fP
+are 10 again, in hex, LSB first. The \fI19\fP is the line length.
+.SS Statements
+.sp
+Each statement within the line is displayed separately. For this line of code:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+10 ? "HELLO":A=1
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+The dump looks like:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+10@0018 (0a 00): ^19
+ >0d !28 $0f =05 "H/48 E/45 L/4c L/4c O/4f" 14:
+ >19 !36 80 2d #0e [40 01 00 00 00 00] 16
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+The first line is the line header. The next two are the two
+statements. The first one ends with token \fI14\fP (the end\-of\-statement
+or tokenized colon) and the second ends with \fI16\fP (the end\-of\-line
+token). The string \fI"HELLO"\fP is visible, and so is the floating point
+constant \fI1\fP\&.
+.sp
+Long statements will wrap, if they\(aqre wider than the terminal. If this
+is a problem, use a wider terminal, and/or pipe through a pager that
+knows how to scroll horizontally.
+.SH EXIT STATUS
+.sp
+0 for success, 1 for failure.
+.SH COPYRIGHT
+.sp
+WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details.
+.SH AUTHOR
+.INDENT 0.0
+.IP B. 3
+Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&.
+.UNINDENT
+.SH SEE ALSO
+.sp
+\fBa8eol\fP(1),
+\fBa8utf8\fP(1),
+\fBatr2xfd\fP(1),
+\fBatrsize\fP(1),
+\fBaxe\fP(1),
+\fBblob2c\fP(1),
+\fBblob2xex\fP(1),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
+\fBxexamine\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1),
+\fBxex\fP(5),
+\fBatascii\fP(7).
+.sp
+Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference
+Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc.
+.\" Generated by docutils manpage writer.
+.
diff --git a/dumpbas.c b/dumpbas.c
new file mode 100644
index 0000000..d5404d2
--- /dev/null
+++ b/dumpbas.c
@@ -0,0 +1,310 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+
+int startlineno = 0;
+int endlineno = 32768;
+
+/* dump tokens for each line in a BASIC program. easier to read than
+ a plain hex dump. */
+void print_help(void) {
+ fprintf(stderr, "Usage: %s [-v] [-s start-lineno] [-e end-lineno] <inputfile>\n", self);
+}
+
+unsigned short getlineno(char opt, const char *arg) {
+ int lineno;
+ char *e;
+
+ lineno = (int)strtol(arg, &e, 10);
+
+ if(*e) {
+ fprintf(stderr, "%s: Invalid line number for -%c option: %s is not a number.\n",
+ self, opt, arg);
+ exit(1);
+ }
+
+ if(lineno < 0 || lineno > 32767) {
+ fprintf(stderr, "%s: Invalid line number for -%c option: %d > 32767.\n",
+ self, opt, lineno);
+ exit(1);
+ }
+
+ return ((unsigned short)lineno);
+}
+
+void parse_args(int argc, char **argv) {
+ int opt;
+
+ while( (opt = getopt(argc, argv, "vs:e:l:")) != -1) {
+ switch(opt) {
+ case 'v': verbose = 1; break;
+ case 's': startlineno = getlineno(opt, optarg); break;
+ case 'e': endlineno = getlineno(opt, optarg); break;
+ case 'l': startlineno = getlineno(opt, optarg);
+ endlineno = startlineno; break;
+ default:
+ print_help();
+ exit(1);
+ }
+ }
+
+ if(optind >= argc)
+ die("No input file given (use - for stdin).");
+ else
+ open_input(argv[optind]);
+}
+
+void print_atascii(unsigned char c) {
+ if(c & 0x80) {
+ putchar('|');
+ c &= 0x7f;
+ }
+
+ if(c < 32) {
+ putchar('^');
+ c += 0x40;
+ }
+
+ if(c == 0x7f)
+ printf("del");
+ else
+ putchar(c);
+ putchar('/');
+}
+
+/* REM, DATA, ERROR lines are terminated by $9B, a real EOL, not
+ the BASIC token. Since they're strings, print them in ASCII too. */
+/*
+int handle_text_stmt(int pos) {
+ unsigned char c;
+
+ do {
+ c = program[pos];
+ print_atascii(c);
+ printf("%02x ", c);
+ pos++;
+ } while(c != 0x9b);
+
+ return pos;
+}
+*/
+
+CALLBACK(handle_text) {
+ unsigned char c;
+
+ do {
+ c = program[pos];
+ print_atascii(c);
+ printf("%02x ", c);
+ pos++;
+ } while(c != 0x9b);
+}
+
+CALLBACK(handle_cmd) {
+ printf("!%02x ", tok);
+}
+
+/*
+int handle_cmd(int pos) {
+ unsigned char tok = program[pos];
+
+ printf("!%02x ", tok);
+ switch(tok) {
+ case CMD_REM:
+ case CMD_DATA:
+ case CMD_ERROR:
+ return handle_text_stmt(pos + 1);
+ default:
+ return pos + 1;
+ }
+}
+*/
+
+/*
+void handle_string(int pos) {
+ int i, len;
+ len = program[pos + 1];
+ printf("$%02x =%02x \"", program[pos], len);
+ for(i = pos; i < pos + len; i++) {
+ unsigned char c = program[i + 2];
+ print_atascii(c);
+ printf("%02x%c", c, (i == (pos + len - 1) ? '"' : ' '));
+ }
+ putchar(' ');
+}
+*/
+
+/*
+void handle_num(int pos) {
+ int i;
+ printf("#%02x [", program[pos]);
+ for(i = 0; i < 6; i++)
+ printf("%02x%c", program[pos + 1 + i], (i == 5 ? ']' : ' '));
+ putchar(' ');
+}
+*/
+
+CALLBACK(handle_op) {
+ switch(tok) {
+ case OP_EOS:
+ printf("%02x:", tok);
+ return;
+ case OP_NUMCONST:
+ putchar('#'); break;
+ case OP_STRCONST:
+ putchar('$'); break;
+ default: break;
+ }
+ printf("%02x ", tok);
+}
+
+CALLBACK(handle_var) {
+ printf("%02x", tok);
+ switch(get_vartype(tok)) {
+ case TYPE_ARRAY:
+ putchar('('); break;
+ case TYPE_STRING:
+ putchar('$'); break;
+ default: break;
+ }
+ putchar(' ');
+}
+
+CALLBACK(handle_string) {
+ int i, len;
+ len = program[pos];
+ printf("=%02x \"", len);
+ for(i = pos; i < pos + len; i++) {
+ unsigned char c = program[i + 1];
+ print_atascii(c);
+ printf("%02x%c", c, (i == (pos + len - 1) ? '"' : ' '));
+ }
+ putchar(' ');
+}
+
+CALLBACK(handle_num) {
+ int i;
+ putchar('[');
+ for(i = 0; i < 6; i++)
+ printf("%02x%c", program[pos + i], (i == 5 ? ']' : ' '));
+ putchar(' ');
+}
+
+CALLBACK(handle_start_line) {
+ printf("%5d @%04x (%02x %02x): ^%02x\n",
+ lineno, pos, program[pos], program[pos + 1], program[pos + 2]);
+}
+
+CALLBACK(handle_end_line) {
+ putchar('\n');
+}
+
+CALLBACK(handle_start_stmt) {
+ printf(" >%02x ", tok);
+}
+
+CALLBACK(handle_end_stmt) {
+ putchar('\n');
+}
+
+int main(int argc, char **argv) {
+ set_self(*argv);
+ parse_general_args(argc, argv, print_help);
+ parse_args(argc, argv);
+
+ readfile();
+ parse_header();
+
+ on_start_line = handle_start_line;
+ on_end_line = handle_end_line;
+ on_start_stmt = handle_start_stmt;
+ on_end_stmt = handle_end_stmt;
+ on_exp_token = handle_op;
+ on_cmd_token = handle_cmd;
+ on_num_const = handle_num;
+ on_string_const = handle_string;
+ on_text = handle_text;
+ on_var_token = handle_var;
+
+ walk_code(startlineno, endlineno);
+
+ return 0;
+}
+
+/* sorry, this is horrid, more like assembly than C. */
+#if 0
+int main(int argc, char **argv) {
+ int linepos, nextpos, offset, soffset, lineno, pos, end, tok;
+
+ set_self(*argv);
+ parse_general_args(argc, argv, print_help);
+ parse_args(argc, argv);
+
+ readfile();
+ parse_header();
+
+ linepos = codestart;
+ while(linepos < filelen) { /* loop over lines */
+ lineno = getword(linepos);
+ offset = program[linepos + 2];
+ nextpos = linepos + offset;
+
+ if(offset < 6)
+ die("Can't dump a protected program, unprotect it first.");
+
+ if(lineno < startlineno) {
+ linepos = nextpos;
+ continue;
+ }
+
+ /* line header */
+ printf("%5d@%04x (%02x %02x): ^%02x ",
+ lineno, linepos, program[linepos], program[linepos + 1], offset);
+
+ pos = linepos + 3;
+ while(pos < nextpos) { /* loop over statements within a line */
+ soffset = program[pos];
+ end = linepos + soffset;
+
+ while(pos < end) { /* loop over tokens within a statement */
+ printf("\n >%02x ", program[pos++]); /* offset */
+ pos = handle_cmd(pos++); /* 1st token is the command */
+
+ while(pos < end) { /* loop over operators */
+ tok = program[pos];
+ switch(tok) {
+ case OP_NUMCONST:
+ handle_num(pos);
+ pos += 7;
+ break;
+ case OP_STRCONST:
+ handle_string(pos);
+ pos += program[pos + 1] + 2;
+ break;
+ default:
+ printf("%02x", program[pos]);
+ if(pos == (end - 1) && tok == OP_EOS)
+ putchar(':');
+ else
+ putchar(' ');
+ pos++;
+ break;
+ }
+ }
+ }
+ }
+
+ putchar('\n');
+
+ if(lineno == endlineno) break;
+ linepos = nextpos;
+ }
+
+ return 0;
+}
+#endif
diff --git a/dumpbas.rst b/dumpbas.rst
new file mode 100644
index 0000000..59cf93b
--- /dev/null
+++ b/dumpbas.rst
@@ -0,0 +1,155 @@
+=======
+dumpbas
+=======
+
+-------------------------------------------------------
+Formatted hexdump for tokenized Atari 8-bit BASIC files
+-------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+dumpbas [**-v**] [**-l** *lineno*] [**-s** *start-lineno*] [**-e** *end-lineno*] *input-file*
+
+DESCRIPTION
+===========
+**dumpbas** reads a tokenized Atari 8-bit BASIC program and prints a
+formatted hexdump on standard output. The formatting groups the hex bytes
+by line and statement, and includes special characters to mark different
+types of token (see **FORMATTING**, below).
+
+**dumpbas** does not detokenize BASIC programs or dump information
+about variable names/values. Use **chkbas**\(1) for that. This tool is
+intended to help the user learn about the tokenized BASIC format, or
+as an aid for developing/debugging other tools that process tokenized
+files. It's an alternative to looking at raw hex dumps.
+
+It's assumed the user has at least some knowledge of BASIC's tokenized
+SAVE format. The **Atari BASIC Sourcebook** is a good starting point
+for learning the tokenized format.
+
+OPTIONS
+=======
+
+General Options
+---------------
+**--help**
+ Print usage message and exit.
+
+**--version**
+ Print version number and exit.
+
+**-v**
+ Verbose operation. When displaying a number in verbose mode, it will
+ be prefixed with *$* if it's in hex, or no prefix for decimal.
+
+Dump Options
+------------
+**-s** *start-lineno*
+ Don't dump lines before **start-lineno**. Default: *0*.
+
+**-e** *end-lineno*
+ Don't dump lines after **start-lineno**. Default: *32768*.
+
+**-l** *lineno*
+ Only dump one line. This is exactly equivalent to "**-s** *lineno* **-e** *lineno*".
+
+FORMATTING
+==========
+Every byte in the file is displayed in hex. However, they are grouped by line
+and statement, and certain tokens get marker characters to help keep track
+of what they're for. Strings are displayed in quotes, in both hex and ASCII. Floating
+point constants are displayed as 6 hex bytes with square brackets around them.
+
+Line Header Markers
+-------------------
+**@**
+ Separates decimal line number from hex file offset.
+
+**^**
+ Prefix for line length.
+
+**(** **)**
+ Surrounds the 2 hex bytes for the line number.
+
+Statement Markers
+-----------------
+**>**
+ Prefix for next-statement offset. Every statement begins with this.
+
+**!**
+ Prefix for a command token. Every line of BASIC code begins with a
+ command.
+
+**:**
+ Suffix for the *14* token; end of statement.
+
+**#**
+ Prefix for the *0e* token, which introduces a BCD floating point constant.
+
+**[** **]**
+ Surrounds the 6 bytes of a BCD floating point constant.
+
+**$**
+ Prefix for the *0f* token, which introduces a string constant.
+
+**=**
+ Prefix for the string-length byte of a string constant.
+
+String Byte Markers
+-------------------
+**"**
+ A string constant is surrounded by double-quotes.
+
+**^**
+ Prefix for a control character. For instance, *03* is displayed as *^C*.
+
+**|**
+ Prefix for an inverse video character. Example: *c1* (inverse video *A*)
+ is displayed as *|A*. May be combined with *^*, for inverse control characters.
+
+**/**
+ Separates the printable ASCII representation of a character from its hex byte.
+ Example: *A/41*.
+
+Line header
+-----------
+Each line number begins with the line number (decimal) and offset from
+the start of the file (hex), followed by the 2 hex bytes for the line
+number in parentheses, followed by the line length (hex, preceded by
+^). Example::
+
+ 10@0018 (0a 00): ^19
+
+The line number is *10*, and the file offset is *0018*. The *0a 00*
+are 10 again, in hex, LSB first. The *19* is the line length.
+
+Statements
+----------
+Each statement within the line is displayed separately. For this line of code::
+
+ 10 ? "HELLO":A=1
+
+The dump looks like::
+
+ 10@0018 (0a 00): ^19
+ >0d !28 $0f =05 "H/48 E/45 L/4c L/4c O/4f" 14:
+ >19 !36 80 2d #0e [40 01 00 00 00 00] 16
+
+The first line is the line header. The next two are the two
+statements. The first one ends with token *14* (the end-of-statement
+or tokenized colon) and the second ends with *16* (the end-of-line
+token). The string *"HELLO"* is visible, and so is the floating point
+constant *1*.
+
+Long statements will wrap, if they're wider than the terminal. If this
+is a problem, use a wider terminal, and/or pipe through a pager that
+knows how to scroll horizontally.
+
+EXIT STATUS
+===========
+
+0 for success, 1 for failure.
+
+.. include:: manftr.rst
diff --git a/fenders.1 b/fenders.1
index ba1d7d4..44998ff 100644
--- a/fenders.1
+++ b/fenders.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "FENDERS" 1 "2024-05-09" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "FENDERS" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
fenders \- Install Fenders 3-sector loader in boot sectors of an ATR image
.\" RST source for fenders(1) man page. Convert with:
@@ -279,10 +279,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/jindroush/acvt/Makefile b/jindroush/acvt/Makefile
index 498e2fc..34f0e41 100644
--- a/jindroush/acvt/Makefile
+++ b/jindroush/acvt/Makefile
@@ -11,22 +11,18 @@ all: release
release:
@$(MAKE) $(PRGNAME) CFLAGS="-c -O2 -Wall -D__CDISK_SAVE__" LDFLAGS=""
- @echo RELEASE: Compiled.
rel_strip: release
@strip $(PRGNAME)
install: rel_strip
cp $(PRGNAME) $(DESTDIR)/$(PREFIX)/bin/
- @echo RELEASE: Installed.
debug:
@$(MAKE) $(PRGNAME) CFLAGS="-c -g -Wall -D_DEBUG -D__CDISK_SAVE__" LDFLAGS="-g"
- @echo DEBUG: Compiled.
clean:
rm -rf *.o $(PRGNAME) $(PRGNAME).exe switches.cpp
- @echo DEBUG: Cleaned.
OBJECTS = acvt.o
diff --git a/jindroush/adir/Makefile b/jindroush/adir/Makefile
index 284add5..cf025be 100644
--- a/jindroush/adir/Makefile
+++ b/jindroush/adir/Makefile
@@ -11,22 +11,18 @@ all: release
release:
@$(MAKE) $(PRGNAME) CFLAGS="-c -O2 -Wall -D__CDISK_SAVE__" LDFLAGS=""
- @echo RELEASE: Compiled.
rel_strip: release
@strip $(PRGNAME)
install: rel_strip
cp $(PRGNAME) $(DESTDIR)/$(PREFIX)/bin/
- @echo RELEASE: Installed.
debug:
@$(MAKE) $(PRGNAME) CFLAGS="-c -g -Wall -D_DEBUG -D__CDISK_SAVE__" LDFLAGS="-g"
- @echo DEBUG: Compiled.
clean:
rm -rf *.o $(PRGNAME) $(PRGNAME).exe switches.cpp
- @echo DEBUG: Cleaned.
OBJECTS = adir.o
diff --git a/jindroush/chkbas/chkbas.cpp b/jindroush/chkbas/chkbas.cpp
index a1ad505..aa6b161 100644
--- a/jindroush/chkbas/chkbas.cpp
+++ b/jindroush/chkbas/chkbas.cpp
@@ -222,7 +222,7 @@ int main( int argc, char* argv[] )
fprintf( g_fout, "Constants & pointers:\n" );
fprintf( g_fout, "Start of Name Table (VNT) : %04X\n", wVNT );
fprintf( g_fout, "End of Name Table (VNTE) : %04X\n", wVNTE );
- fprintf( g_fout, "Lenght of Name Table (VNTL) : %04X\n", wVNTL );
+ fprintf( g_fout, "Length of Name Table (VNTL) : %04X\n", wVNTL );
fprintf( g_fout, "Start of Variable Table (VVT) : %04X\n", wVVT );
fprintf( g_fout, "End of Variable Table (VVTE) : %04X\n", wVVTE );
diff --git a/jindroush/man/chkbas.1 b/jindroush/man/chkbas.1
index 1069d6c..5fbcbae 100644
--- a/jindroush/man/chkbas.1
+++ b/jindroush/man/chkbas.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "CHKBAS" 1 "2024-05-16" "1.10" "Jindroush's Atari 8-bit tools"
+.TH "CHKBAS" 1 "2024-05-19" "1.10" "Jindroush's Atari 8-bit tools"
.SH NAME
chkbas \- check and detokenize Atari BASIC SAVEd files
.SH SYNOPSIS
@@ -69,7 +69,7 @@ Input file: HELLO.BAS
Constants & pointers:
Start of Name Table (VNT) : 000E
End of Name Table (VNTE) : 0011
-Lenght of Name Table (VNTL) : 0004
+Length of Name Table (VNTL) : 0004
Start of Variable Table (VVT) : 0012
End of Variable Table (VVTE) : 0021
Length of Variable Table (VVTL) : 0010
diff --git a/jindroush/man/chkbas.rst b/jindroush/man/chkbas.rst
index 2f9e6b7..2ab92e7 100644
--- a/jindroush/man/chkbas.rst
+++ b/jindroush/man/chkbas.rst
@@ -48,7 +48,7 @@ Sample run, with no options::
Constants & pointers:
Start of Name Table (VNT) : 000E
End of Name Table (VNTE) : 0011
- Lenght of Name Table (VNTL) : 0004
+ Length of Name Table (VNTL) : 0004
Start of Variable Table (VVT) : 0012
End of Variable Table (VVTE) : 0021
Length of Variable Table (VVTL) : 0010
diff --git a/manftr.rst b/manftr.rst
index 12e78d2..ac50295 100644
--- a/manftr.rst
+++ b/manftr.rst
@@ -20,10 +20,15 @@ SEE ALSO
**blob2xex**\(1),
**cart2xex**\(1),
**dasm2atasm**\(1),
+**dumpbas**\(1),
**f2toxex**\(1),
**fenders**\(1),
+**protbas**\(1),
+**renumbas**\(1),
**rom2cart**\(1),
**unmac65**\(1),
+**unprotbas**\(1),
+**vxrefbas**\(1),
**xexamine**\(1),
**xexcat**\(1),
**xexsplit**\(1),
diff --git a/manhdr5.rst b/manhdr5.rst
new file mode 100644
index 0000000..e0b3c67
--- /dev/null
+++ b/manhdr5.rst
@@ -0,0 +1,7 @@
+.. include:: ver.rst
+.. |date| date::
+
+:Manual section: 5
+:Manual group: Urchlay's Atari 8-bit Tools
+:Date: |date|
+:Version: |version|
diff --git a/protbas.1 b/protbas.1
new file mode 100644
index 0000000..33946b7
--- /dev/null
+++ b/protbas.1
@@ -0,0 +1,142 @@
+.\" Man page generated from reStructuredText.
+.
+.
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+.TH "PROTBAS" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+protbas \- LIST-protect Atari 8-bit BASIC programs
+.SH SYNOPSIS
+.sp
+protbas [\fB\-v\fP] [\fB\-nc | **\-nv\fP] [\fB\-s\fP] [\fB\-xr\fP | \fB\-xNN\fP] \fBinput\-file\fP \fBoutput\-file\fP
+.SH DESCRIPTION
+.sp
+\fBprotbas\fP reads a tokenized Atari 8\-bit BASIC program and writes a
+LIST\-protected copy of the program. See the \fBDETAILS\fP section of the
+\fBunprotbas\fP(1) man page, to understand how the protection works.
+.sp
+\fBinput\-file\fP must be a tokenized (SAVEd) Atari BASIC program. Use
+\fI\-\fP to read from standard input, but \fBprotbas\fP will refuse to
+read from standard input if it\(aqs a terminal.
+.sp
+\fBoutput\-file\fP will be the protected tokenized BASIC program. If it
+already exists, it will be overwritten. Use \fI\-\fP to write to standard
+output, but \fBprotbas\fP will refuse to write to standard output if
+it\(aqs a terminal (since tokenized BASIC is binary data and may confuse
+the terminal).
+.sp
+The code protection works by adding a line 32767 to the program, with
+a bad next\-line pointer. This will fail if there\(aqs already a line
+32767.
+.SH OPTIONS
+.sp
+Options may appear in any order. The first non\-option argument is used
+for \fBinput\-file\fP; the second is \fBoutput\-file\fP\&. A third non\-option
+argument is an error.
+.SS General Options
+.INDENT 0.0
+.TP
+.B \fB\-\-help\fP
+Print usage message and exit.
+.TP
+.B \fB\-\-version\fP
+Print version number and exit.
+.TP
+.B \fB\-v\fP
+Verbose operation. When displaying a number in verbose mode, it will
+be prefixed with \fI$\fP if it\(aqs in hex, or no prefix for decimal.
+.UNINDENT
+.SS Protection Options
+.INDENT 0.0
+.TP
+.B \fB\-nc\fP
+Do not code\-protect the program; only protect the variable names.
+.TP
+.B \fB\-nv\fP
+Do not protect the variable names; only code\-protect the program.
+.TP
+.B \fB\-s\fP
+Shrink variable name table to minimum size, one byte per variable
+name. Programs protected this way are very similar to ones
+protected with \fBPROTECT.BAS\fP\&.
+.TP
+.B \fB\-xr\fP, \fB\-x\fP \fINN\fP
+Character to use for variable name protection. \fINN\fP is the
+character code in hex, e.g. \fB\-x20\fP to use a space. Default is
+\fB9b\fP (the EOL character). \fB\-xr\fP means random codes.
+.UNINDENT
+.SH EXIT STATUS
+.INDENT 0.0
+.TP
+.B 0
+\fBinput\-file\fP was unprotected, protection was successful.
+.TP
+.B 1
+I/O error, or \fBinput\-file\fP isn\(aqt a valid BASIC program.
+.TP
+.B 2
+\fBinput\-file\fP is already a protected BASIC program.
+.UNINDENT
+.SH COPYRIGHT
+.sp
+WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details.
+.SH AUTHOR
+.INDENT 0.0
+.IP B. 3
+Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&.
+.UNINDENT
+.SH SEE ALSO
+.sp
+\fBa8eol\fP(1),
+\fBa8utf8\fP(1),
+\fBatr2xfd\fP(1),
+\fBatrsize\fP(1),
+\fBaxe\fP(1),
+\fBblob2c\fP(1),
+\fBblob2xex\fP(1),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
+\fBxexamine\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1),
+\fBxex\fP(5),
+\fBatascii\fP(7).
+.sp
+Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference
+Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc.
+.\" Generated by docutils manpage writer.
+.
diff --git a/protbas.c b/protbas.c
new file mode 100644
index 0000000..5b62496
--- /dev/null
+++ b/protbas.c
@@ -0,0 +1,195 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+
+int protect_vars = 1;
+int protect_code = 1;
+int shrinktable = 0;
+int varname_char = 0x9b;
+
+/* 32767 END */
+unsigned char badcode[] = {
+ 0xff, 0x7f, /* line number 32767 */
+ 0x00, /* *bad* next-line offset */
+ 0x06, /* next-statement offset */
+ 0x15, /* END token */
+ 0x16, /* end-of-line token */
+};
+
+void scramble_vars(void) {
+ int i;
+
+ if(!vntable_ok()) {
+ fprintf(stderr, "%s: Program already was variable-protected.\n", self);
+ exit(2);
+ }
+
+ if(shrinktable) {
+ if(verbose) fprintf(stderr, "Shrinking variable name table.\n");
+ adjust_vntable_size((vvstart - 1) - vnstart, (codestart - vvstart) / 8);
+ }
+
+ if(varname_char == -1) srand(time(NULL));
+
+ for(i = vnstart; i < vvstart - 1; i++)
+ if(varname_char == -1)
+ program[i] = (rand() >> 8) & 0xff;
+ else
+ program[i] = varname_char & 0xff;
+
+ if(verbose) {
+ i -= vnstart;
+ if(i) {
+ fprintf(stderr, "Replaced %d byte variable name table with ", i);
+ if(varname_char == -1)
+ fprintf(stderr, "random characters.\n");
+ else
+ fprintf(stderr, "character $%02x.\n", varname_char);
+ } else {
+ fprintf(stderr, "Can't protect variables because there are no variables.\n");
+ }
+ }
+}
+
+/* iterate over all the lines, insert a poisoned line 32767 just
+ before line 32768 */
+void breakcode(void) {
+ int pos = codestart, oldpos = 0;
+ int offset, lineno = -1, tmpno = -1;
+
+ while(pos < filelen) {
+ lineno = tmpno;
+ tmpno = getword(pos);
+ if(tmpno == 32768) {
+ break;
+ } else {
+ offset = program[pos + 2];
+ if(!offset) {
+ fprintf(stderr, "%s: program already was code-protected.\n", self);
+ exit(2);
+ }
+ oldpos = pos;
+ pos += offset;
+ }
+ }
+
+ if(!oldpos) die("Can't protect code because there are no lines of code.");
+ if(lineno == 32767) die("Can't protect code because there is already a line 32767.");
+
+ /* pos is now the start of line 32768, move it up to make room for
+ the new line */
+ offset = sizeof(badcode);
+ memmove(program + pos + offset, program + pos, filelen);
+
+ /* insert new line */
+ memmove(program + pos, badcode, offset);
+
+ if(verbose)
+ fprintf(stderr, "Inserted line 32767 with invalid offset at file offset $%04x.\n", pos);
+
+ /* update pointers that would be affected by the code move */
+ stmcur += offset;
+ starp += offset;
+ filelen += offset;
+ update_header();
+ parse_header();
+}
+
+void print_help(void) {
+ fprintf(stderr, "Usage: %s [-v] [-nc|-nv] [-s] [-x[r|NN]] <inputfile> <outputfile>\n", self);
+ fprintf(stderr, " -v: Verbose.\n");
+ fprintf(stderr, " -nc: Don't protect code.\n");
+ fprintf(stderr, " -nv: Don't protect variable names.\n");
+ fprintf(stderr, " -s: Shrink variable name table to min size.\n");
+ fprintf(stderr, "-xNN: Hex code NN for variable names.\n");
+ fprintf(stderr, " -xr: Random variable names.\n");
+ fprintf(stderr, "Use - as a filename to read from stdin and/or write to stdout.\n");
+}
+
+void parse_args(int argc, char **argv) {
+ int opt, xopt_used = 0;
+
+ while( (opt = getopt(argc, argv, "vn:x:s")) != -1) {
+ switch(opt) {
+ case 'v': verbose = 1; break;
+ case 's': shrinktable = 1; break;
+ case 'n':
+ switch(optarg[0]) {
+ case 'c': protect_code = 0; break;
+ case 'v': protect_vars = 0; break;
+ default:
+ die("Invalid argument for -n (must be 'c' or 'v').");
+ }
+ break;
+ case 'x':
+ xopt_used = 1;
+ switch(optarg[0]) {
+ case 'r':
+ varname_char = -1; break;
+ case 0:
+ die("-x option requires a hex number or 'r'."); break;
+ default:
+ {
+ char *e;
+ varname_char = (int)strtol(optarg, &e, 16);
+ if(*e != 0 || varname_char > 0xff)
+ fprintf(stderr, "%s: Invalid hex value '%s' for -x option (range is 0 to ff).\n", self, optarg);
+ }
+ }
+ break;
+ default:
+ print_help();
+ exit(1);
+ }
+ }
+
+ if(!protect_code && !protect_vars) {
+ die("Nothing to do: -nc and -nv both given.");
+ }
+
+ if(!protect_vars) {
+ if(xopt_used)
+ die("-x option not valid with -nv.");
+ if(shrinktable)
+ die("-s option not valid with -nv.");
+ }
+
+ if(optind >= argc)
+ die("No input file given (use - for stdin).");
+ else
+ open_input(argv[optind]);
+
+ if(++optind >= argc)
+ die("No output file given (use - for stdout).");
+ else
+ output_filename = argv[optind];
+}
+
+int main(int argc, char **argv) {
+ set_self(*argv);
+ parse_general_args(argc, argv, print_help);
+ parse_args(argc, argv);
+ readfile();
+ parse_header();
+
+ if(verbose) {
+ fprintf(stderr, "Protecting program, ");
+ if(protect_vars && !protect_code)
+ fprintf(stderr, "variables only.\n");
+ else if(protect_code && !protect_vars)
+ fprintf(stderr, "code only.\n");
+ else
+ fprintf(stderr, "both code and variables.\n");
+ }
+ if(protect_vars) scramble_vars();
+ if(protect_code) breakcode();
+
+ open_output(output_filename);
+ writefile();
+ return 0; /* TODO: meaningful return status */
+}
diff --git a/protbas.rst b/protbas.rst
new file mode 100644
index 0000000..5ceb838
--- /dev/null
+++ b/protbas.rst
@@ -0,0 +1,86 @@
+=======
+protbas
+=======
+
+---------------------------------------
+LIST-protect Atari 8-bit BASIC programs
+---------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+protbas [**-v**] [**-nc | **-nv**] [**-s**] [**-xr** | **-xNN**] **input-file** **output-file**
+
+DESCRIPTION
+===========
+
+**protbas** reads a tokenized Atari 8-bit BASIC program and writes a
+LIST-protected copy of the program. See the **DETAILS** section of the
+**unprotbas**\(1) man page, to understand how the protection works.
+
+**input-file** must be a tokenized (SAVEd) Atari BASIC program. Use
+*-* to read from standard input, but **protbas** will refuse to
+read from standard input if it's a terminal.
+
+**output-file** will be the protected tokenized BASIC program. If it
+already exists, it will be overwritten. Use *-* to write to standard
+output, but **protbas** will refuse to write to standard output if
+it's a terminal (since tokenized BASIC is binary data and may confuse
+the terminal).
+
+The code protection works by adding a line 32767 to the program, with
+a bad next-line pointer. This will fail if there's already a line
+32767.
+
+OPTIONS
+=======
+
+Options may appear in any order. The first non-option argument is used
+for **input-file**; the second is **output-file**. A third non-option
+argument is an error.
+
+General Options
+---------------
+**--help**
+ Print usage message and exit.
+
+**--version**
+ Print version number and exit.
+
+**-v**
+ Verbose operation. When displaying a number in verbose mode, it will
+ be prefixed with *$* if it's in hex, or no prefix for decimal.
+
+Protection Options
+------------------
+**-nc**
+ Do not code-protect the program; only protect the variable names.
+
+**-nv**
+ Do not protect the variable names; only code-protect the program.
+
+**-s**
+ Shrink variable name table to minimum size, one byte per variable
+ name. Programs protected this way are very similar to ones
+ protected with **PROTECT.BAS**.
+
+**-xr**, **-x** *NN*
+ Character to use for variable name protection. *NN* is the
+ character code in hex, e.g. **-x20** to use a space. Default is
+ **9b** (the EOL character). **-xr** means random codes.
+
+EXIT STATUS
+===========
+
+0
+ **input-file** was unprotected, protection was successful.
+
+1
+ I/O error, or **input-file** isn't a valid BASIC program.
+
+2
+ **input-file** is already a protected BASIC program.
+
+.. include:: manftr.rst
diff --git a/renumbas.1 b/renumbas.1
new file mode 100644
index 0000000..3cd0e65
--- /dev/null
+++ b/renumbas.1
@@ -0,0 +1,184 @@
+.\" Man page generated from reStructuredText.
+.
+.
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+.TH "RENUMBAS" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+renumbas \- Renumber Atari 8-bit BASIC programs
+.SH SYNOPSIS
+.sp
+renumbas [\fB\-v\fP] [\fB\-s\fP \fIstart\-lineno\fP] [\fB\-i\fP \fIincrement\fP] [\fB\-f\fP \fIfirst\-lineno\fP] \fIinput\-file\fP \fIoutput\-file\fP
+.SH DESCRIPTION
+.sp
+\fBrenumbas\fP reads a tokenized Atari 8\-bit BASIC program and writes a
+renumbered copy of the program.
+.sp
+\fBinput\-file\fP must be a tokenized (SAVEd) Atari BASIC program. Use
+\fI\-\fP to read from standard input, but \fBrenumbas\fP will refuse to
+read from standard input if it\(aqs a terminal.
+.sp
+\fBoutput\-file\fP will be the renumbered BASIC program. If it already
+exists, it will be overwritten. Use \fI\-\fP to write to standard output,
+but \fBrenumbas\fP will refuse to write to standard output if it\(aqs a
+terminal (since tokenized BASIC is binary data and may confuse the
+terminal).
+.sp
+Line number references are changed in every line where they occur, so
+e.g. if line 100 gets changed to 200, any other line that does a GOTO
+100 (or GOSUB, RESTORE, TRAP, etc) will be updated with the new line
+number.
+.sp
+Computed line numbers can\(aqt be updated (e.g. GOTO A or GOSUB
+1000+A*100). These will draw warnings on stderr, so you can fix them
+manually.
+.sp
+Line numbers that don\(aqt exist will not be changed (e.g. TRAP 40000).
+.sp
+Remember that the maximum line number for Atari BASIC is 32767.
+Renumbering will fail, if the chosen start and increment values
+would result in lines with numbers higher than this.
+.SH OPTIONS
+.sp
+Options may appear in any order. The first non\-option argument is used
+for \fBinput\-file\fP; the second is \fBoutput\-file\fP\&. A third non\-option
+argument is an error.
+.SS General Options
+.INDENT 0.0
+.TP
+.B \fB\-\-help\fP
+Print usage message and exit.
+.TP
+.B \fB\-\-version\fP
+Print version number and exit.
+.TP
+.B \fB\-v\fP
+Verbose operation. When displaying a number in verbose mode, it will
+be prefixed with \fI$\fP if it\(aqs in hex, or no prefix for decimal.
+.UNINDENT
+.SS Renumber Options
+.INDENT 0.0
+.TP
+.B \fB\-s\fP \fIstart\-lineno\fP
+First line number in the renumbered program. Default: 10.
+.TP
+.B \fB\-i\fP \fIincrement\fP
+Line number increment between successive lines. Default: 10.
+.TP
+.B \fB\-f\fP \fIfirst\-lineno\fP
+Line number in original program where renumbering will start. Lines
+numbered lower that this will not be renumbered. Default: 0.
+.UNINDENT
+.SH LIMITATIONS
+.SS Computed line numbers with ON
+.sp
+If an ON/GOTO or ON/GOSUB uses computed line numbers (and causes a
+warning), none of the line numbers after the first computed one will
+be updated, even if they are constant. Example:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+100 ON X GOTO 10,20*Y,30
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+The 10 will be changed to whatever line 10 got renumbered to, as expected. The 20
+will \fInot\fP be changed. \fBrenumbas\fP just gives up, after the first computed
+line number.
+.sp
+A pathological case:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+100 ON X GOTO 10+0,20+0
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+The 10+0 and 20+0 are considered computed line numbers, even though
+the results of the computation are constant. This is because neither
+Atari BASIC nor \fBrenumbas\fP does constant folding.
+.sp
+None of this should be a real\-world problem: computed line numbers in
+ON/GOTO or ON/GOSUB are exceedingly rare. The whole \fIpoint\fP of ON is
+to avoid computing line numbers.
+.SS Warning line numbers
+.sp
+Any warning that includes a line number (such as "Computed line number") will
+have the original line number, \fInot\fP the renumbered one. This is because
+the warnings are generated while scanning the program to find line number
+references, which happens before the actual renumbering (so the new number
+isn\(aqt known yet).
+.SH EXIT STATUS
+.sp
+0 for success, 1 for failure.
+.SH COPYRIGHT
+.sp
+WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details.
+.SH AUTHOR
+.INDENT 0.0
+.IP B. 3
+Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&.
+.UNINDENT
+.SH SEE ALSO
+.sp
+\fBa8eol\fP(1),
+\fBa8utf8\fP(1),
+\fBatr2xfd\fP(1),
+\fBatrsize\fP(1),
+\fBaxe\fP(1),
+\fBblob2c\fP(1),
+\fBblob2xex\fP(1),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
+\fBxexamine\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1),
+\fBxex\fP(5),
+\fBatascii\fP(7).
+.sp
+Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference
+Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc.
+.\" Generated by docutils manpage writer.
+.
diff --git a/renumbas.c b/renumbas.c
new file mode 100644
index 0000000..272b832
--- /dev/null
+++ b/renumbas.c
@@ -0,0 +1,403 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+
+/* remove/comment to turn off debug printing */
+#define DEBUG
+
+#ifdef DEBUG
+# define IFDEBUG(x) x
+#else
+# define IFDEBUG(x)
+#endif
+
+int startlineno = 10;
+int increment = 10;
+int limit = 0;
+
+unsigned short *linerefs[32768];
+int linerefcounts[32768];
+
+void print_help(void) {
+ fprintf(stderr, "Usage: %s [-v] [-s start-lineno] [-i increment] [-f first-lineno] <inputfile> <outputfile>\n", self);
+ fprintf(stderr, " -v: Verbose.\n");
+ fprintf(stderr, " -s <num>: Starting line number (default: 10).\n");
+ fprintf(stderr, " -i <num>: Increment (default: 10).\n");
+ fprintf(stderr, " -f <num>: Don't renumber lines less than <num> (default: 0).\n");
+}
+
+unsigned short getlineno(char opt, const char *arg) {
+ int lineno;
+ char *e;
+
+ lineno = (int)strtol(arg, &e, 10);
+
+ if(*e) {
+ fprintf(stderr, "%s: Invalid line number for -%c option: %s is not a number.\n",
+ self, opt, arg);
+ exit(1);
+ }
+
+ if(lineno < 0 || lineno > 32767) {
+ fprintf(stderr, "%s: Invalid line number for -%c option: %d > 32767.\n",
+ self, opt, lineno);
+ exit(1);
+ }
+
+ return ((unsigned short)lineno);
+}
+
+void parse_args(int argc, char **argv) {
+ int opt;
+
+ while( (opt = getopt(argc, argv, "vs:i:f:")) != -1) {
+ switch(opt) {
+ case 'v': verbose = 1; break;
+ case 's': startlineno = getlineno(opt, optarg); break;
+ case 'i': increment = getlineno(opt, optarg); break;
+ case 'f': limit = getlineno(opt, optarg); break;
+ default:
+ print_help();
+ exit(1);
+ }
+ }
+
+ if(optind >= argc)
+ die("No input file given (use - for stdin).");
+ else
+ open_input(argv[optind]);
+
+ if(++optind >= argc)
+ die("No output file given (use - for stdout).");
+ else
+ output_filename = argv[optind];
+}
+
+unsigned char bcd2int(unsigned char bcd) {
+ return (bcd >> 4) * 10 + (bcd & 0x0f);
+}
+
+unsigned char int2bcd(unsigned char i) {
+ return ((i / 10) << 4) | (i % 10);
+}
+
+unsigned short fp2int(const unsigned char *fp) {
+ unsigned short result = 0;
+
+ /* examine the exponent/sign byte */
+ if(fp[0] == 0) return 0; /* special case */
+ if(fp[0] & 0x80) die("negative numbers not supported");
+
+ switch(fp[0]) {
+ case 0x40:
+ result = bcd2int(fp[1]); break;
+ case 0x41:
+ result = bcd2int(fp[1]) * 100 + bcd2int(fp[2]); break;
+ case 0x42:
+ result = bcd2int(fp[1]) * 10000 + bcd2int(fp[2]) * 100 + bcd2int(fp[3]); break;
+ default:
+ die("number out of range"); break;
+ }
+
+ return result;
+}
+
+void int2fp(unsigned short num, unsigned char *fp) {
+ memset(fp, 0, 6);
+
+ if(num == 0) return;
+
+ if(num >= 10000) {
+ fp[0] = 0x42;
+ fp[3] = int2bcd(num % 100);
+ num /= 100;
+ fp[2] = int2bcd(num % 100);
+ num /= 100;
+ fp[1] = int2bcd(num);
+ } else if(num >= 100) {
+ fp[0] = 0x41;
+ fp[2] = int2bcd(num % 100);
+ num /= 100;
+ fp[1] = int2bcd(num);
+ } else {
+ fp[0] = 0x40;
+ fp[1] = int2bcd(num);
+ }
+}
+
+void addlineref(unsigned short refaddr) {
+ int target = fp2int(program + refaddr);
+ unsigned short *p = linerefs[target];
+ int c = linerefcounts[target];
+
+ IFDEBUG(printf("addlineref: target=%d, 0x%04x\n", target, refaddr));
+
+ if(c) {
+ p = realloc(p, sizeof(unsigned short) * (c + 1));
+ } else {
+ p = malloc(sizeof(unsigned short));
+ }
+
+ if(!p) die("Out of memory.");
+
+ linerefs[target] = p;
+ p[c] = refaddr;
+ c++;
+ linerefcounts[target] = c;
+}
+
+void printlinerefs(void) {
+ int i, j;
+ printf("linerefs:\n");
+ for(i = 0; i < 32768; i++) {
+ if(linerefcounts[i]) {
+ printf("%d: ", i);
+ for(j = 0; j < linerefcounts[i]; j++) {
+ printf("%04x ", linerefs[i][j]);
+ }
+ putchar('\n');
+ }
+ }
+}
+
+void freelinerefs(void) {
+ int i;
+ for(i = 0; i < 32768; i++) {
+ if(linerefcounts[i]) {
+ free(linerefs[i]);
+ }
+ }
+}
+
+/* tokens that can take line numbers:
+ Commands:
+ GOTO 0x0a
+ GO TO 0x0b
+ GOSUB 0x0c
+ TRAP 0x0d
+ LIST 0x04 (but don't bother)
+ RESTORE 0x23
+
+ Operators:
+ GOTO 0x17 (as in, ON (0x1e) GOTO)
+ GOSUB 0x18 (ON = 0x1e again)
+ THEN 0x1b (but not really!)
+
+ beware: e.g. GOTO 1000+A should not have the 1000 changed.
+
+ numeric constant introduced with 0x0e, followed by 6 BCD bytes.
+ string constant 0x0f, length byte, then (length) bytes.
+*/
+
+int is_xfer_cmd(unsigned char tok) {
+ int ret;
+ switch(tok) {
+ case CMD_GOTO:
+ case CMD_GO_TO:
+ case CMD_GOSUB:
+ case CMD_TRAP:
+ case CMD_LIST:
+ case CMD_RESTORE:
+ ret = 1; break;
+ default:
+ ret = 0; break;
+ }
+ IFDEBUG(printf("is_xfer_cmd(%02x) == %d\n", tok, ret));
+ return ret;
+}
+
+int skip_op_token(int pos) {
+ switch(program[pos]) {
+ case OP_EOS:
+ return pos + 2; /* skip next-statement offset */
+ case OP_EOL:
+ return pos + 3; /* skip 2-byte line number */
+ case OP_NUMCONST:
+ return pos + 7; /* skip 6-byte BCD float */
+ case OP_STRCONST:
+ return pos + 2 + program[pos + 1]; /* 2nd byte is string len */
+ default:
+ return pos + 1;
+ }
+}
+
+/* ON/GOTO and ON/GOSUB can have any number of arguments, separated
+ by OP_COMMA. *Normally* these are simple FP constants, since the
+ whole point of ON is to avoid computed line numbers... but they're
+ allowed to be expressions so we have to check. */
+int handle_on_goto(int lineno, int pos) {
+ unsigned char tok, nexttok, main_tok;
+
+ IFDEBUG(printf("handle_on_goto(%02x)\n", pos));
+
+ main_tok = program[pos]; /* save this, for use in 'computed' warning */
+
+ pos++; /* skip GOTO/GOSUB token */
+
+ while(1) {
+ tok = program[pos];
+ if(tok == OP_EOS || tok == OP_EOL)
+ break;
+ if(tok == OP_COMMA) {
+ pos++;
+ continue;
+ }
+ nexttok = program[pos + 7];
+ if(tok != OP_NUMCONST || !(nexttok == OP_COMMA || nexttok == OP_EOS || nexttok == OP_EOL)) {
+ fprintf(stderr, "Computed line number in ON/%s at line %d.\n",
+ (main_tok == OP_GOTO ? "GOTO" : "GOSUB"), lineno);
+ break;
+ }
+ addlineref(pos + 1);
+ pos += 7;
+ }
+
+ return pos;
+}
+
+/* IF/THEN can be followed by a simple line number (not a full expression)
+ or a statement offset (*without* an OP_EOS token!) followed by a
+ statement.
+ The right way to do this would be to track the statement offsets, but
+ this works fine. It relies on the fact that line numbers always have
+ an exponent byte of 0x40 to 0x42, and the fact that 0x40 to 0x42 are
+ not valid command tokens.
+ */
+int handle_then(int pos) {
+ unsigned char tok1 = program[pos + 1];
+ unsigned char tok2 = program[pos + 2];
+ if(tok1 == OP_NUMCONST && (tok2 >= 0x40 && tok2 <= 0x42)) {
+ addlineref(pos + 2);
+ return pos + 7;
+ } else {
+ return 0;
+ }
+}
+
+void renumber(void) {
+ int pos = codestart, nextpos;
+ int lineno, offset;
+ int newno, i;
+ unsigned char tok;
+ unsigned char fpnewno[6];
+
+ /* pass 1: find references to line numbers, flag them. */
+ while(pos < filelen) {
+ lineno = getword(pos);
+ if(lineno == 32768) break;
+ offset = program[pos + 2];
+ IFDEBUG(printf("checking line %d, pos %04x...\n", lineno, pos));
+ if(offset < 6)
+ die("Can't renumber a code-protected program, unprotect it first.");
+ nextpos = pos + offset;
+
+ pos += 4; /* skip line number, line length, 1st statement length */
+
+ /* loop over the statements in this line */
+ while(pos < nextpos) {
+ /* every statement starts with a command token */
+ tok = program[pos];
+
+ if(tok == CMD_REM || tok == CMD_DATA || tok == CMD_ERROR)
+ break; /* ignore rest of line */
+
+ if(is_xfer_cmd(tok)) {
+ unsigned char nexttok = program[pos + 8];
+ if(program[pos + 1] == OP_NUMCONST && (nexttok == OP_EOS || nexttok == OP_EOL)) {
+ addlineref(pos + 2);
+ } else {
+ fprintf(stderr, "Computed line number at line %d.\n", lineno);
+ }
+ }
+
+ pos++;
+
+ /* rest of statement is expressions/operators */
+ while(pos < nextpos) {
+ tok = program[pos];
+
+ if(tok == OP_EOL) break;
+
+ if(tok == OP_EOS) {
+ pos += 2; /* end statement, skip statement length of next one */
+ break;
+ }
+
+ if(tok == OP_GOTO || tok == OP_GOSUB) {
+ pos = handle_on_goto(lineno, pos);
+ continue;
+ } else if(tok == OP_THEN) {
+ i = handle_then(pos);
+ if(i) {
+ pos = i;
+ continue;
+ } else {
+ pos += 2; /* skip statement length */
+ break;
+ }
+ }
+
+ IFDEBUG(printf("tok is %02x, pos was %02x before skip_op_token... ", tok, pos));
+ pos = skip_op_token(pos);
+ IFDEBUG(printf("pos now %02x\n", pos));
+ }
+ }
+
+ pos = nextpos; /* point to next line */
+ }
+
+ IFDEBUG(printlinerefs());
+
+ /* pass 2: renumber the lines, and update the references in other lines */
+ newno = startlineno;
+ pos = codestart;
+ while(pos < filelen) {
+ if(newno >= 32768) {
+ fprintf(stderr, "New line number %d out of range, renumber failed.", newno);
+ exit(1);
+ }
+
+ lineno = getword(pos);
+ offset = program[pos + 2];
+
+ if(lineno >= limit) {
+ if(lineno == 32768) break;
+
+ IFDEBUG(printf("renumbering line %d as %d, %d refs\n", lineno, newno, linerefcounts[lineno]));
+
+ /* update refs to old line number with new one */
+ int2fp(newno, fpnewno);
+ for(i = 0; i < linerefcounts[lineno]; i++)
+ memmove(program + linerefs[lineno][i], fpnewno, 6);
+
+ /* update the actual line number */
+ setword(pos, newno);
+
+ newno += increment;
+ }
+ pos += offset;
+ }
+
+ freelinerefs();
+}
+
+int main(int argc, char **argv) {
+ set_self(*argv);
+ parse_general_args(argc, argv, print_help);
+ parse_args(argc, argv);
+
+ readfile();
+ parse_header();
+
+ renumber();
+
+ open_output(output_filename);
+ writefile();
+
+ return 0;
+}
diff --git a/renumbas.rst b/renumbas.rst
new file mode 100644
index 0000000..a0b79ba
--- /dev/null
+++ b/renumbas.rst
@@ -0,0 +1,116 @@
+========
+renumbas
+========
+
+-----------------------------------
+Renumber Atari 8-bit BASIC programs
+-----------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+renumbas [**-v**] [**-s** *start-lineno*] [**-i** *increment*] [**-f** *first-lineno*] *input-file* *output-file*
+
+DESCRIPTION
+===========
+**renumbas** reads a tokenized Atari 8-bit BASIC program and writes a
+renumbered copy of the program.
+
+**input-file** must be a tokenized (SAVEd) Atari BASIC program. Use
+*-* to read from standard input, but **renumbas** will refuse to
+read from standard input if it's a terminal.
+
+**output-file** will be the renumbered BASIC program. If it already
+exists, it will be overwritten. Use *-* to write to standard output,
+but **renumbas** will refuse to write to standard output if it's a
+terminal (since tokenized BASIC is binary data and may confuse the
+terminal).
+
+Line number references are changed in every line where they occur, so
+e.g. if line 100 gets changed to 200, any other line that does a GOTO
+100 (or GOSUB, RESTORE, TRAP, etc) will be updated with the new line
+number.
+
+Computed line numbers can't be updated (e.g. GOTO A or GOSUB
+1000+A*100). These will draw warnings on stderr, so you can fix them
+manually.
+
+Line numbers that don't exist will not be changed (e.g. TRAP 40000).
+
+Remember that the maximum line number for Atari BASIC is 32767.
+Renumbering will fail, if the chosen start and increment values
+would result in lines with numbers higher than this.
+
+OPTIONS
+=======
+
+Options may appear in any order. The first non-option argument is used
+for **input-file**; the second is **output-file**. A third non-option
+argument is an error.
+
+General Options
+---------------
+**--help**
+ Print usage message and exit.
+
+**--version**
+ Print version number and exit.
+
+**-v**
+ Verbose operation. When displaying a number in verbose mode, it will
+ be prefixed with *$* if it's in hex, or no prefix for decimal.
+
+Renumber Options
+----------------
+**-s** *start-lineno*
+ First line number in the renumbered program. Default: 10.
+
+**-i** *increment*
+ Line number increment between successive lines. Default: 10.
+
+**-f** *first-lineno*
+ Line number in original program where renumbering will start. Lines
+ numbered lower that this will not be renumbered. Default: 0.
+
+LIMITATIONS
+===========
+
+Computed line numbers with ON
+-----------------------------
+If an ON/GOTO or ON/GOSUB uses computed line numbers (and causes a
+warning), none of the line numbers after the first computed one will
+be updated, even if they are constant. Example::
+
+ 100 ON X GOTO 10,20*Y,30
+
+The 10 will be changed to whatever line 10 got renumbered to, as expected. The 20
+will *not* be changed. **renumbas** just gives up, after the first computed
+line number.
+
+A pathological case::
+
+ 100 ON X GOTO 10+0,20+0
+
+The 10+0 and 20+0 are considered computed line numbers, even though
+the results of the computation are constant. This is because neither
+Atari BASIC nor **renumbas** does constant folding.
+
+None of this should be a real-world problem: computed line numbers in
+ON/GOTO or ON/GOSUB are exceedingly rare. The whole *point* of ON is
+to avoid computing line numbers.
+
+Warning line numbers
+--------------------
+Any warning that includes a line number (such as "Computed line number") will
+have the original line number, *not* the renumbered one. This is because
+the warnings are generated while scanning the program to find line number
+references, which happens before the actual renumbering (so the new number
+isn't known yet).
+
+EXIT STATUS
+===========
+
+0 for success, 1 for failure.
+
+.. include:: manftr.rst
diff --git a/rom2cart.1 b/rom2cart.1
index a3dfcb9..83a3654 100644
--- a/rom2cart.1
+++ b/rom2cart.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "ROM2CART" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "ROM2CART" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
rom2cart \- Convert a raw ROM image to an Atari800 CART image, or vice versa
.\" RST source for rom2cart(1) man page. Convert with:
@@ -248,10 +248,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/unmac65.1 b/unmac65.1
index 8e17857..6694892 100644
--- a/unmac65.1
+++ b/unmac65.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "UNMAC65" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "UNMAC65" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
unmac65 \- Detokenize Atari 8-bit Mac/65 SAVEd files.
.\" RST source for unmac65(1) man page. Convert with:
@@ -380,10 +380,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/unprotbas.1 b/unprotbas.1
index 2b389ac..9f4c8c5 100644
--- a/unprotbas.1
+++ b/unprotbas.1
@@ -27,20 +27,21 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "UNPROTBAS" 1 "2024-05-19" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "UNPROTBAS" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
unprotbas \- Unprotect LIST-protected Atari 8-bit BASIC programs
.SH SYNOPSIS
.sp
-unprotbas [\fB\-v\fP] [\fB\-f\fP] [\fB\-n\fP] [\fB\-g\fP] [\fB\-c\fP] \fBinput\-file\fP \fBoutput\-file\fP
+unprotbas [\fB\-v\fP] [\fB\-f\fP] [\fB\-n\fP] [\fB\-g\fP] [\fB\-c\fP] [\fB\-r\fP | \fB\-w\fP] \fBinput\-file\fP \fBoutput\-file\fP
.SH DESCRIPTION
.sp
-\fBunprotbas\fP modifies a LIST\-protected Atari 8\-bit BASIC program,
-creating a new non\-protected copy. See \fBDETAILS\fP, below, to
-understand how the protection and unprotection works.
+\fBunprotbas\fP modifies a tokenized LIST\-protected Atari 8\-bit BASIC
+program, creating a new non\-protected copy. See \fBDETAILS\fP, below,
+to understand how the protection and unprotection works.
.sp
\fBinput\-file\fP must be a tokenized (SAVEd) Atari BASIC program. Use
-\fI\-\fP to read from standard input.
+\fI\-\fP to read from standard input, but \fBunprotbas\fP will refuse to
+read from standard input if it\(aqs a terminal.
.sp
\fBoutput\-file\fP will be the unprotected tokenized BASIC program. If it
already exists, it will be overwritten. Use \fI\-\fP to write to standard
@@ -49,13 +50,28 @@ it\(aqs a terminal (since tokenized BASIC is binary data and may confuse
the terminal).
.SH OPTIONS
.sp
+Options may appear in any order. The first non\-option argument is used
+for \fBinput\-file\fP; the second is \fBoutput\-file\fP\&. A third non\-option
+argument is an error.
+.sp
Option bundling is not supported, use e.g. \fB\-v \-f\fP, not \fB\-vf\fP\&.
To use filenames beginning with \fI\-\fP, write them as \fI\&./\-file\fP, or they
will be treated as options.
+.SS General Options
.INDENT 0.0
.TP
+.B \fB\-\-help\fP
+Print usage message and exit.
+.TP
+.B \fB\-\-version\fP
+Print version number and exit.
+.TP
.B \fB\-v\fP
-Verbose operation.
+Verbose operation. When displaying a number in verbose mode, it will
+be prefixed with \fI$\fP if it\(aqs in hex, or no prefix for decimal.
+.UNINDENT
+.SS Unprotection Options
+.INDENT 0.0
.TP
.B \fB\-f\fP
Force the variable name table to be rebuilt, even if it looks OK.
@@ -73,6 +89,18 @@ it\(aqs left as\-is, in case it\(aqs actually data used by the program.
Check only. Does a dry run. Loads the program, unprotects it in
memory, but doesn\(aqt write the result anywhere. In this mode, there
is no \fBoutput\-file\fP\&.
+.TP
+.B \fB\-w\fP
+Write the variable names to \fBvarnames.txt\fP, one per line.
+This can be edited, and later used with \fB\-r\fP to set the variable names
+to something sensible rather than A, B, C, etc. For an unprotected
+program, you can use \fB\-n\fP to write the existing names rather than
+generating new ones. See \fBVARIABLE NAMES\fP, below. If \fBvarnames.txt\fP
+already exists, it will be overwritten.
+.TP
+.B \fB\-r\fP
+Read variable names from \fBvarnames.txt\fP, and use them instead of
+generating the names. See \fBVARIABLE NAMES\fP, below.
.UNINDENT
.SH EXIT STATUS
.INDENT 0.0
@@ -140,7 +168,7 @@ a new one that\(aqs valid. However, since there are no real variable
names in the program, the recovery process just invents new ones,
named A through Z, A1 through A9, B1 through B9, etc, etc. It\(aqll
require human intelligence to figure out what each variable is for,
-since the names are meaningless.
+since the names are meaningless. See \fBVARIABLE NAMES\fP, below.
.sp
The \fBoutput\-file\fP may not be the exact size that the
\fBinput\-file\fP was. Some types of variable\-name scrambling shrink
@@ -149,12 +177,18 @@ the rebuilt table will be larger. Other types of scrambling leave
the variable name table at its original size, but \fBunprotbas\fP
generates only one\- and two\-character variable names, so the rebuilt
table might be smaller.
+.sp
+The program \fBPROTECT.BAS\fP, found on Disk 2 of the Holmes Archive,
+creates protected BASIC programs that only use variable name
+scrambling.
+.sp
+\fBprotbas\fP(1) also does variable name scrambling.
.TP
.B Bad next\-line pointer
Every line of tokenized BASIC contains a line length byte, which
-BASIC uses as a pointer to the next line of code. Before printing
-the READY prompt, BASIC iterates over every line of code in the
-program, using the next\-line pointers, in order to delete any
+BASIC uses as a pointer to the next line of code. Before executing
+an immediate mode command, BASIC iterates over every line of code in
+the program, using the next\-line pointers, in order to delete any
existing line 32768 (the previous immediate mode command). If any
line\(aqs pointer is set to zero, that means it points to itself.
.sp
@@ -164,35 +198,136 @@ prevents LIST, it actually prevents any immediate mode command:
after LOADing such a file, \fInothing\fP will work (even pressing RESET
won\(aqt get you out of it). The only way to use such a program is to
use the RUN command with a filename, and if the program ever exits
-(due to END, STOP, an error, or the Break key), BASIC will get stuck
-again.
+(due to END, STOP, an error, Break key, or even System Reset), BASIC
+will get stuck again.
.sp
-This doesn\(aqt \fIhave\fP to be done with the last line in the
-program. The "poisoned" line could be followed by more lines of
-code, though they could never actually execute.
+This doesn\(aqt \fIhave\fP to be done with the last line in the program,
+though it normally is. The "poisoned" line can never be executed (or
+BASIC will lock up), but it could be followed by more lines of code
+(which also could never be executed).
.sp
Line 32100 in the example above does this job, taking advantage of
the STMCUR pointer used by BASIC, which holds the address of the
line of tokenized code currently being executed.
.sp
-\fBunprotbas\fP fixes this simply by calculating what the pointer
-should be (based on the tokens in the line) and changing it. No
-information is lost by doing this.
+Each statement in the line also has a statement\-length byte. For
+lines with only one statement, its value is the same as the line
+length. For lines with multiple statements (separated by \fI:\fP), it\(aqs
+a pointer to the next statement, counting from the start of the
+current line. For the last statement on a line, it\(aqs a pointer to
+the next line of code, meaning it\(aqs identical to the line length.
+.sp
+\fBunprotbas\fP fixes bad line lengths by setting the line length to
+the statement length of the last statement. No information is lost
+by doing this.
+.sp
+The program \fBUNPROTEC\fP, from the \fIPirate\(aqs Treasure Chest\fP, can
+fix bad pointers in protected programs, though it doesn\(aqt do
+anything about variable name scrambling.
+.sp
+\fBprotbas\fP also does this type of protection.
.UNINDENT
.sp
One more thing \fBunprotbas\fP can do is remove extra data from the end
of the file. It\(aqs possible for BASIC files to contain extra data that
-occurs after the end of the program. Some programs use this as a way
-to load arbitrary binary data into memory along with the program; for
-other programs, the extra data is truly garbage (e.g. an EOF character
-if the file came from a CP/M system, or padding to a block size if a
-dumb implementation of XMODEM was used to transfer the file).
+occurs after the end of the program. Such data might be:
+.INDENT 0.0
+.IP \(bu 2
+Pre\-defined strings and/or arrays, saved with the program by
+modifying the STARP pointer.
+.IP \(bu 2
+Arbitrary binary data used by the program at runtime, such as
+machine language routines, or fonts.
+.IP \(bu 2
+Zero bytes, caused by SAVEing the program with revision B BASIC. Every
+time a program is LOADed, edited (or not) and then SAVEd again, 16
+bytes of extra (garbage) data gets added to the program. To avoid
+this, don\(aqt use revision B (use rev C if possible, A otherwise).
+.IP \(bu 2
+Garbage added by some system previously used to store or transmit
+the file. CP/M systems might add an EOF (^Z) character. Dumb
+file transfer software might pad the file up to its block size.
+.UNINDENT
.sp
Normally, such "garbage" doesn\(aqt hurt anything. BASIC ignores it. Or
it normally does... if you suspect it\(aqs causing a problem, you can
remove it with the \fB\-g\fP option. If removing the "garbage" causes the
program to fail to run, it wasn\(aqt garbage! \fBunprotbas\fP doesn\(aqt
remove extra data by default, to be on the safe side.
+.SH VARIABLE NAMES
+.sp
+If variable name scrambling was used, the original variable names no
+longer exist. \fBunprotbas\fP will generate them, according to these rules:
+.INDENT 0.0
+.INDENT 3.5
+The first 26 numeric variables will be called \fIA\fP through \fIZ\fP\&. Further
+numeric variables will be \fIA1\fP through \fIA9\fP, \fIB1\fP through \fIB9\fP, etc.
+.sp
+The first 26 string variables will be \fIA$\fP to \fIZ$\fP, then \fIA1$\fP to
+\fIA9$\fP, \fIB1$\fP to \fIB9$\fP, etc.
+.sp
+The first 26 array variables will be \fIA(\fP to \fIZ(\fP, then \fIA1(\fP to
+\fIA9(\fP, \fIB1(\fP to \fIB9(\fP, etc.
+.UNINDENT
+.UNINDENT
+.sp
+Note that array variables have only the \fI(\fP as part of the name. The
+closing \fI)\fP is "cosmetic" and not part of the actual name.
+.sp
+To properly reverse\-engineer the protected program, it\(aqs necessary to assign
+meaningful variable names. \fBunprotbas\fP isn\(aqt smart enough to do this for you,
+but it can semi\-automate the process.
+.sp
+First, run \fBunprotbas\fP with the \fB\-w\fP option. This will create a
+file called \fBvarnames.txt\fP, containing the generated variable names.
+These are in order, one line per variable name, ending with \fI$\fP for strings
+and the \fI(\fP for arrays.
+.sp
+Load the unprotected program on the Atari and LIST it (or use \fBchkbas\fP to get a
+listing), and edit \fBvarnames.txt\fP in a text editor.
+.sp
+As you figure out what each variable\(aqs purpose is, change its name
+in the text file. When editing the file:
+.INDENT 0.0
+.IP \(bu 2
+Don\(aqt add or delete any lines.
+.IP \(bu 2
+Don\(aqt get rid of the \fI$\fP or \fI(\fP at the end of any line.
+.IP \(bu 2
+You may enter the names in lowercase (\fBunprotbas\fP will convert them to uppercase).
+.IP \(bu 2
+Remember to follow the rules for BASIC variable names:
+The first character must be a letter, other characters must be a letter
+or a number, and only the last character can be \fI$\fP or \fI(\fP\&.
+.IP \(bu 2
+No duplicates of the same type are allowed (you can have \fIFOO\fP and \fIFOO$\fP,
+but not two numerics called \fIFOO\fP).
+.UNINDENT
+.sp
+When you\(aqre finished, re\-run \fBunprotbas\fP, this time with the \fB\-r\fP
+option. If all is well, the unprotected program will use your variable
+names, rather than generating new ones. If you broke the rules, you
+should get an informative error message explaining what and where the
+problem is.
+.sp
+This process can also be used for regular unprotected programs. Use
+\fB\-n \-w\fP the first time, to save the existing variable names to
+\fBvarnames.txt\fP rather than generating new ones.
+.SH NOTES
+.sp
+Atari BASIC has a limit of 128 variables in a program. It\(aqs actually
+possible for the variable name table to contain up to 256 variables,
+though the 129th and further ones won\(aqt be usable in the program. The
+variable value table can hold more than 256 values, though the
+variable numbers wrap around once they pass 255. The attempt to add
+variables past the 128th causes BASIC to respond with \fIERROR\- 4\fP, but
+the variable does get added to the tables. \fBunprotbas\fP will preserve
+these extra (useless) entries in the tables.
+.sp
+If there more than 256 entries in the value table, you will see
+"Warning: skipping variable numbers >=256 in value table". This is
+a pathological case, and shouldn\(aqt happen with programs that aren\(aqt
+deliberately crafted to test this behaviour.
.SH COPYRIGHT
.sp
WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details.
@@ -212,10 +347,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/unprotbas.c b/unprotbas.c
index 0b11e0e..5bde236 100644
--- a/unprotbas.c
+++ b/unprotbas.c
@@ -2,6 +2,10 @@
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
/* attempt to fix a "list-protected" Atari 8-bit BASIC program.
we don't fully detokenize, so this won't fix truly corrupted
@@ -14,29 +18,11 @@
or whatever), we "fix" that by making up new variable names.
*/
-#define STM_OFFSET 0xf2
-
-/* entire file gets read into memory (for now) */
-unsigned char data[65536];
-
-/* BASIC 14-byte header values */
-unsigned short lomem;
-unsigned short vntp;
-unsigned short vntd;
-unsigned short vvtp;
-unsigned short stmtab;
-unsigned short stmcur;
-unsigned short starp;
-
-/* positions where various parts of the file start,
- derived from the header vars above. */
-unsigned short codestart;
-unsigned short vnstart;
-unsigned short vvstart;
-int filelen;
-
-/* name of executable, taken from argv[0] */
-char *self;
+/* for the -r option */
+#define MAP_FILE "varnames.txt"
+unsigned char varnames[BUFSIZE];
+unsigned char *varmap[MAXVARS];
+int varmap_count;
/* these are set by the various command-line switches */
int keepvars = 0;
@@ -44,64 +30,8 @@ int forcevars = 0;
int keepgarbage = 1;
int checkonly = 0;
int was_protected = 0;
-int verbose = 0;
-
-/* file handles */
-FILE *input_file = NULL;
-FILE *output_file = NULL;
-
-void die(const char *msg) {
- fprintf(stderr, "%s: %s\n", self, msg);
- exit(1);
-}
-
-/* read entire file into memory */
-int readfile(void) {
- int got = fread(data, 1, 65535, input_file);
- if(verbose) fprintf(stderr, "read %d bytes\n", got);
- fclose(input_file);
- return got;
-}
-
-/* get a 16-bit value from the file, in 6502 LSB/MSB order. */
-unsigned short getword(int addr) {
- return data[addr] | (data[addr + 1] << 8);
-}
-
-void setword(int addr, int value) {
- data[addr] = value & 0xff;
- data[addr + 1] = value >> 8;
-}
-
-void dump_header_vars(void) {
- fprintf(stderr, "LOMEM $%04x VNTP $%04x VNTD $%04x VVTP $%04x\n", lomem, vntp, vntd, vvtp);
- fprintf(stderr, "STMTAB $%04x STMCUR $%04x STARP $%04x\n", stmtab, stmcur, starp);
- fprintf(stderr, "vnstart $%04x, vvstart $%04x, codestart $%04x\n", vnstart, vvstart, codestart);
-}
-
-void read_header(void) {
- lomem = getword(0);
- vntp = getword(2);
- vntd = getword(4);
- vvtp = getword(6);
- stmtab = getword(8);
- stmcur = getword(10);
- starp = getword(12);
- codestart = stmtab - STM_OFFSET - (vntp - 256);
- vnstart = vntp - 256 + 14;
- vvstart = vvtp - 256 + 14;
- if(verbose) dump_header_vars();
-}
-
-void set_header_vars(void) {
- setword(0, lomem);
- setword(2, vntp);
- setword(4, vntd);
- setword(6, vvtp);
- setword(8, stmtab);
- setword(10, stmcur);
- setword(12, starp);
-}
+int readmap = 0;
+int writemap = 0;
/* fixline() calculates & sets correct line length, by iterating
over the statement(s) within the line. the last statement's
@@ -122,30 +52,29 @@ void set_header_vars(void) {
2 09 line length (or, offset to next line) [!]
3 06 offset to next statement *from the start of the line*
4 28 token for "?"
- 5 14 token for : (end of statement)
+ 5 14 token for : (end of statement), we call it TOK_COLON
6 09 offset to next statement [!]
7 15 token for END
8 16 token for end-of-line [*]
9 ?? (line number of next statement)
Note the values marked with [!] are equal.
+ The line length at offset 2 is what gets zeroed out by the
+ protection. To fix it, we follow the next-statement offsets. If
+ there's not a colon before the offset, replace the byte at
+ offset 2 with that statement's offset.
[*] end-of-line is $16 *except* for REM and DATA, which are
terminated with $9B instead.
*/
int fixline(int linepos) {
/* +3 here to skip the line number + line length */
- int token, done = 0, offset = data[linepos + 3];
-
- while(!done) {
- offset = data[linepos + offset];
- token = data[linepos + offset - 1];
- /* fprintf(stderr, "offset %02x token %02x\n", offset, token); */
- if(token != 0x14)
- done++;
- }
+ int offset = program[linepos + 3];
- data[linepos + 2] = offset;
+ while(program[linepos + offset - 1] == TOK_COLON)
+ offset = program[linepos + offset];
+
+ program[linepos + 2] = offset;
return offset;
}
@@ -159,15 +88,15 @@ int fixcode(void) {
while(pos < filelen) {
tmpno = getword(pos);
if(tmpno <= lineno) {
- fprintf(stderr, "Warning: line number %d at offset %04x is <= previous line number %d\n",
+ fprintf(stderr, "Warning: line number %d at offset $%04x is <= previous line number %d.\n",
tmpno, pos, lineno);
}
lineno = tmpno;
- offset = data[pos + 2];
+ offset = program[pos + 2];
/* fprintf(stderr, "pos %d, line #%d, offset %d\n", pos, lineno, offset); */
if(offset < 6) {
- if(verbose) fprintf(stderr, "Found invalid offset %d (<6) at line %d\n", offset, lineno);
+ if(verbose) fprintf(stderr, "Found invalid offset %d (<6) at line %d, file offset $%04x.\n", offset, lineno, pos);
offset += fixline(pos);
result++;
}
@@ -178,33 +107,29 @@ int fixcode(void) {
if(lineno == 32768) break;
}
- if(verbose) fprintf(stderr, "End program pos $%04x/%d\n", pos, pos);
+ if(verbose) fprintf(stderr, "End program file offset: $%04x/%d\n", pos, pos);
if(filelen > pos) {
- if(verbose) fprintf(stderr, "trailing garbage at EOF, %d bytes, %s\n",
- filelen - pos, (keepgarbage ? "keeping" : "removing"));
+ int i, same = 1;
+ for(i = pos; i < filelen; i++) {
+ if(program[i] != program[pos]) same = 0;
+ }
+ if(verbose) {
+ fprintf(stderr, "Trailing garbage at EOF, %d bytes, ", filelen - pos);
+ if(same)
+ fprintf(stderr, "all $%02x", program[pos]);
+ else
+ fprintf(stderr, "maybe valid data");
+ fprintf(stderr, ", %s.\n", (keepgarbage ? "keeping" : "removing"));
+ }
if(!keepgarbage) filelen = pos;
+ } else {
+ if(verbose)
+ fprintf(stderr, "No trailing garbage at EOF.\n");
}
-
return result;
}
-/* sometimes the variable name table isn't large enough to hold
- the generated variable names. move_code() makes more space,
- by moving the rest of the program (including the variable value
- table) up in memory. */
-void move_code(int offset) {
- memmove(data + vvstart + offset, data + vvstart, filelen);
- vntd += offset;
- vvtp += offset;
- stmtab += offset;
- stmcur += offset;
- starp += offset;
- set_header_vars();
- read_header();
- filelen += offset;
-}
-
/* Fixing the variables is a bit more work than it seems like
it might be, because the last byte of the name has to match
the type (inverse video "(" for numeric array, inverse "$" for
@@ -229,52 +154,6 @@ void move_code(int offset) {
or letter+number or one-letter string/array names).
*/
-int vntable_ok(void) {
- int vp, bad;
-
- if(vntp == vntd) {
- if(verbose) fprintf(stderr, "No variables\n");
- return 1;
- }
-
- /* first pass: bad = 1 if all the bytes in the table have the same
- value, no matter what it is. */
- vp = vnstart + 1;
- bad = 1;
- while(vp < vvstart - 1) {
- if(data[vp] != data[vnstart]) {
- bad = 0;
- break;
- }
- vp++;
- }
- if(bad) return 0;
-
- /* 2nd pass: bad = 1 if there's any invalid character in the table. */
- vp = vnstart;
- while(vp < vvstart) {
- unsigned char c = data[vp];
-
- /* treat a null byte as end-of-table, ignore any junk between it and VNTP. */
- if(c == 0) break;
-
- vp++;
-
- /* inverse $ or ( is OK */
- if(c == 0xa4 || c == 0xa8) continue;
-
- /* numbers and letters are allowed, inverse or normal. */
- c &= 0x7f;
- if(c >= 0x30 && c <= 0x39) continue;
- if(c >= 0x41 && c <= 0x5a) continue;
-
- bad++;
- break;
- }
-
- return !bad;
-}
-
/* walk the variable value table, generating variable names.
if write is 0, just return the size the table will be.
if write is 1, actually write the names to memory. */
@@ -287,28 +166,35 @@ int rebuild_vntable(int write) {
while(vv < codestart) {
unsigned char sigil = 0;
/* type: scalar = 0, array = 1, string = 2 */
- unsigned char type = data[vv] >> 6;
- /* fprintf(stderr, "%04x: %04x, %d\n", vv, data[vv], type); */
+ unsigned char type = program[vv] >> 6;
+ /* fprintf(stderr, "%04x: %04x, %d\n", vv, program[vv], type); */
+
+ if(varnum == MAXVARS) {
+ fprintf(stderr, "Warning: skipping variable numbers >=%d in value table.\n", MAXVARS);
+ break;
+ }
- if(varnum != data[vv+1]) {
- fprintf(stderr, "Warning: variable value is corrupt!\n");
+ if(varnum != program[vv+1]) {
+ fprintf(stderr, "Warning: variable #%d value is corrupt!\n", varnum);
}
- varnum++;
switch(type) {
- case 1: varname = arrays++; sigil = 0xa8; break;
- case 2: varname = strings++; sigil = 0xa4; break;
- default: varname = scalars++; break;
+ case TYPE_SCALAR: varname = scalars++; break;
+ case TYPE_ARRAY: varname = arrays++; sigil = 0xa8; break;
+ case TYPE_STRING: varname = strings++; sigil = 0xa4; break;
+ default:
+ fprintf(stderr, "Warning: variable value #%d has unknown type.\n", varnum);
+ break;
}
if(varname < 26) {
- if(write) data[vp] = ('A' + varname);
+ if(write) program[vp] = ('A' + varname);
size++;
} else {
varname -= 26;
if(write) {
- data[vp++] = 'A' + varname / 9;
- data[vp] = '1' + varname % 9;
+ program[vp++] = 'A' + varname / 9;
+ program[vp] = '1' + varname % 9;
}
size += 2;
}
@@ -316,41 +202,25 @@ int rebuild_vntable(int write) {
if(sigil) {
size++;
vp++;
- if(write) data[vp++] = sigil;
+ if(write) program[vp++] = sigil;
} else {
- if(write) data[vp] |= 0x80;
+ if(write) program[vp] |= 0x80;
vp++;
}
vv += 8;
+ varnum++;
}
/* there's supposed to be a null byte at the end of the table, unless
- all 128 table slots are used. */
- if(write) {
- if(varnum < 128) data[vp] = 0;
- /* fixup the VNTD pointer */
- /*
- vntd = vntp + (vp - vnstart);
- fprintf(stderr, "%04x\n", vntd);
- data[4] = vntd & 0xff;
- data[5] = vntd >> 8;
- */
- }
+ all 128 table slots are used... except really, there can be >=129
+ entries, and there's always a null byte. */
+ if(write) program[vp] = 0;
+ size++;
return size;
}
-void adjust_vntable_size(int oldsize, int newsize) {
- int move_by;
- if(oldsize != newsize) {
- move_by = newsize - oldsize;
- if(verbose) fprintf(stderr, "need %d bytes for vntable, have %d, moving VVTP by %d to %04x\n",
- newsize, oldsize, move_by, vvtp + move_by);
- move_code(move_by);
- }
-}
-
int fixvars(void) {
int old_vntable_size, new_vntable_size;
@@ -366,69 +236,172 @@ int fixvars(void) {
return 1;
}
-void print_help(void) {
- fprintf(stderr, "Usage: %s [-v] [-f] [-n] [-g] <inputfile> <outputfile>\n", self);
- fprintf(stderr, "-v: verbose\n");
- fprintf(stderr, "-f: force variable name table rebuild\n");
- fprintf(stderr, "-n: do not rebuild variable name table, even if it's invalid\n");
- fprintf(stderr, "-g: remove trailing garbage, if present\n");
- fprintf(stderr, "-c: check only; no output file\n");
- fprintf(stderr, "Use - as a filename to read from stdin and/or write to stdout\n");
+void write_var_map(void) {
+ FILE *f;
+ int vp, count = 0;
+
+ if(verbose) fprintf(stderr, "Writing variable names to '" MAP_FILE "'.\n");
+ f = fopen(MAP_FILE, "w");
+ if(!f) {
+ perror(MAP_FILE);
+ die("Can't create map file for -w option.");
+ }
+
+ for(vp = vnstart; (vp < vntd) && (program[vp] != 0); vp++) {
+ unsigned char c = program[vp];
+ if(c < 0x80) {
+ fputc(c, f);
+ } else {
+ fputc(c & 0x7f, f);
+ fputc('\n', f);
+ count++;
+ }
+ }
+
+ fclose(f);
+
+ if(verbose) fprintf(stderr, "Wrote %d variable names to '" MAP_FILE "'.\n", count);
}
-void invalid_args(const char *arg) {
- fprintf(stderr, "%s: Invalid argument '%s'\n\n", self, arg);
- print_help();
+void die_mapfile(char *msg, int num) {
+ fprintf(stderr, MAP_FILE ": line %d: %s.\n", num, msg);
exit(1);
}
-FILE *open_file(const char *name, const char *mode) {
- FILE *fp;
- if(!(fp = fopen(name, mode))) {
- perror(name);
- exit(1);
+void check_varname(const unsigned char *name, int line) {
+ int len = strlen((char *)name);
+ int i;
+ unsigned char c = 0, type;
+
+ /* fprintf(stderr, "check_varname(\"%s\", %d)\n", name, line); */
+
+ if(len < 1) die_mapfile("Blank variable name", line);
+ if(len > 128) die_mapfile("Variable name >128 characters", line);
+ if(name[0] < 'A' || name[0] > 'Z')
+ die_mapfile("Invalid variable name: First character must be a letter", line);
+
+ for(i = 1; i < len; i++) {
+ c = name[i];
+ if(c >= 'A' && c <= 'Z') continue;
+ if(c >= '0' && c <= '9') continue;
+ if(c == '$' || c == '(') {
+ if(i == (len - 1))
+ continue;
+ else
+ die_mapfile("Invalid variable name: $ and ( only allowed at end", line);
+ }
+ die_mapfile("Invalid character in variable name", line);
+ }
+
+ if(c == 0) c = name[0];
+
+ /* c now has the last char of the name, make sure it matches the variable type */
+ type = program[vvstart + 8 * (line - 1)] >> 6;
+ /* type: scalar = 0, array = 1, string = 2 */
+ if(type == TYPE_SCALAR) {
+ if(c == '$')
+ die_mapfile("Type mismatch: numeric variable may not end with $", line);
+ else if(c == '(')
+ die_mapfile("Type mismatch: numeric variable may not end with (", line);
+ } else if(type == TYPE_ARRAY) {
+ if(c != '(')
+ die_mapfile("Type mismatch: array variable must end with (", line);
+ } else if(type == TYPE_STRING) {
+ if(c != '$')
+ die_mapfile("Type mismatch: string variable must end with $", line);
+ } else {
+ fprintf(stderr, "Warning: variable value table is corrupt (invalid type).\n");
+ }
+
+ /* check for dups */
+ for(i = 0; i < line - 1; i++) {
+ if(strcmp((char *)name, (char *)varmap[i]) == 0)
+ die_mapfile("duplicate variable name", line);
}
- return fp;
}
-void open_input(const char *name) {
- if(!name) {
- if(freopen(NULL, "rb", stdin)) {
- input_file = stdin;
- return;
- } else {
- perror("stdin");
- exit(1);
+void read_var_map(void) {
+ FILE *f;
+ unsigned char *p = varnames, *curname = varnames;
+ int count = 0, vvcount = (codestart - vvstart) / 8;
+
+ if(verbose) fprintf(stderr, "Reading variable names from " MAP_FILE ".\n");
+ f = fopen(MAP_FILE, "r");
+ if(!f) {
+ perror(MAP_FILE);
+ die("Can't read map file for -r option.");
+ }
+
+ while(!feof(f)) {
+ *p = toupper(fgetc(f)); /* allow lowercase */
+
+ if(*p == ' ' || *p == '\t' || *p == '\r')
+ continue; /* ignore whitespace */
+
+ if(*p == '\n') {
+ *p = '\0';
+ varmap[count++] = curname;
+ check_varname(curname, count);
+ curname = p + 1;
}
+ p++;
+ }
+ fclose(f);
+
+ if(verbose) fprintf(stderr, "Read %d variable names from " MAP_FILE ".\n", count);
+
+ if(vvcount > count) {
+ fprintf(stderr, MAP_FILE ": not enough variables (have %d, need %d).\n", count, vvcount);
+ exit(1);
+ } else if(count > vvcount) {
+ fprintf(stderr, MAP_FILE ": too many variables (have %d, need %d).\n", count, vvcount);
+ exit(1);
}
- input_file = open_file(name, "rb");
+ varmap_count = count;
}
-void open_output(const char *name) {
- if(!name) {
- if(isatty(fileno(stdout))) {
- fprintf(stderr, "%s: refusing to write binary data to standard output\n", self);
- exit(1);
- }
- if(freopen(NULL, "wb", stdout)) {
- output_file = stdout;
- return;
- } else {
- perror("stdout");
- exit(1);
+void apply_var_map(void) {
+ unsigned char new_vntable[BUFSIZE];
+ int i, newp = 0;
+ unsigned char *v;
+
+ if(verbose)
+ fprintf(stderr, "Using variable names from " MAP_FILE ".\n");
+
+ for(i = 0; i < varmap_count; i++) {
+ v = varmap[i];
+ while(*v) {
+ new_vntable[newp++] = *v;
+ v++;
}
+ new_vntable[newp - 1] |= 0x80;
}
- output_file = open_file(name, "wb");
+ new_vntable[newp++] = '\0';
+
+ i = vvstart - vnstart;
+ adjust_vntable_size(i, newp);
+ memmove(program + vnstart, new_vntable, newp);
+}
+
+void print_help(void) {
+ fprintf(stderr, "Usage: %s [-v] [-f] [-n] [-g] [-c] [-r|-w] <inputfile> <outputfile>\n", self);
+ fprintf(stderr, " -v: Verbose.\n");
+ fprintf(stderr, " -f: Force variable name table rebuild.\n");
+ fprintf(stderr, " -n: Do not rebuild variable name table, even if it's invalid.\n");
+ fprintf(stderr, " -g: Remove trailing garbage, if present.\n");
+ fprintf(stderr, " -c: Check only; no output file.\n");
+ fprintf(stderr, " -w: Write variable names to 'varnames.txt'.\n");
+ fprintf(stderr, " -r: Read variable names from 'varnames.txt'.\n");
+ fprintf(stderr, "Use - as a filename to read from stdin and/or write to stdout.\n");
}
void parse_args(int argc, char **argv) {
- self = *argv;
- if(argc < 2) {
- print_help();
- exit(0);
- }
+ set_self(*argv);
+
+ parse_general_args(argc, argv, print_help);
+
while(++argv, --argc) {
if((*argv)[0] == '-') {
switch((*argv)[1]) {
@@ -437,73 +410,84 @@ void parse_args(int argc, char **argv) {
case 'n': keepvars++; break;
case 'g': keepgarbage = 0; break;
case 'c': checkonly = 1; break;
+ case 'r': readmap = 1; break;
+ case 'w': writemap = 1; break;
case 0:
if(!input_file)
open_input(NULL);
- else if(!output_file)
- open_output(NULL);
+ else if(!output_filename)
+ output_filename = *argv;
else
invalid_args(*argv);
break;
default: invalid_args(*argv); break;
}
} else {
+ /* arg doesn't start with a -, must be a filename */
if(!input_file)
open_input(*argv);
- else if(!checkonly && !output_file)
- open_output(*argv);
+ else if(!checkonly && !output_filename)
+ output_filename = *argv;
else
invalid_args(*argv);
}
}
- if(!input_file) die("no input file given (use - for stdin)");
- if(!checkonly && !output_file) die("no output file given (use - for stdout)");
- if(keepvars && forcevars) die("-f and -n are mutually exclusive");
+ if(!input_file) die("No input file given (use - for stdin).");
+ if(!checkonly && !output_filename) die("No output file given (use - for stdout).");
+ if(keepvars && forcevars) die("-f and -n are mutually exclusive.");
+ if(readmap && writemap) die("-r and -w are mutually exclusive.");
+ if(readmap && keepvars) die("-r and -n are mutually exclusive, maybe you want -w?");
+ if(checkonly && (readmap || writemap)) die("-c and -r/-w are mutually exclusive.");
}
int main(int argc, char **argv) {
+ int invoffs = 0;
parse_args(argc, argv);
- filelen = readfile();
- read_header();
-
- if(lomem) die("This doesn't look like an Atari BASIC program (no $0000 signature)");
+ readfile();
+ parse_header();
- if(!keepvars) {
- if(fixvars()) {
- was_protected = 1;
- if(verbose) fprintf(stderr, "Variable names replaced\n");
- } else {
- if(verbose) fprintf(stderr, "Variable names were already OK\n");
+ if(readmap) {
+ was_protected = !vntable_ok();
+ read_var_map();
+ apply_var_map();
+ } else {
+ if(!keepvars) {
+ if(fixvars()) {
+ was_protected = 1;
+ if(verbose) fprintf(stderr, "Variable names replaced.\n");
+ } else {
+ if(verbose) fprintf(stderr, "Variable names were already OK.\n");
+ }
}
}
- if(fixcode()) {
- if(verbose) fprintf(stderr, "Fixed invalid offset in code\n");
+ invoffs = fixcode();
+ if(invoffs) {
+ if(verbose)
+ fprintf(stderr, "Fixed %d invalid offset%s in code.\n",
+ invoffs, (invoffs == 1 ? "" : "s"));
was_protected = 1;
} else {
- if(verbose) fprintf(stderr, "No invalid offsets\n");
+ if(verbose) fprintf(stderr, "No invalid offsets.\n");
}
if(verbose) {
- if(was_protected)
- fprintf(stderr, "Program was protected.\n");
- else
- fprintf(stderr, "Program was NOT protected.\n");
+ fprintf(stderr, "Program was %sprotected.\n", (was_protected ? "" : "NOT "));
}
if(checkonly) {
if(verbose) fprintf(stderr, "Check-only mode; no output written.\n");
- if(was_protected)
- return 0;
- else
- return 2;
+ return was_protected ? 0 : 2;
}
- int got = fwrite(data, 1, filelen, output_file);
- fclose(output_file);
- if(verbose) fprintf(stderr, "wrote %d bytes\n", got);
+ /* we don't open the output file until all processing is done, to
+ avoid leaving invalid output files if we exit on error. */
+ open_output(output_filename);
+ writefile();
+
+ if(writemap) write_var_map();
- return 0;
+ return was_protected ? 0 : 2;
}
diff --git a/unprotbas.rst b/unprotbas.rst
index 28ccd8b..d24b1f3 100644
--- a/unprotbas.rst
+++ b/unprotbas.rst
@@ -11,17 +11,18 @@ Unprotect LIST-protected Atari 8-bit BASIC programs
SYNOPSIS
========
-unprotbas [**-v**] [**-f**] [**-n**] [**-g**] [**-c**] **input-file** **output-file**
+unprotbas [**-v**] [**-f**] [**-n**] [**-g**] [**-c**] [**-r** | **-w**] **input-file** **output-file**
DESCRIPTION
===========
-**unprotbas** modifies a LIST-protected Atari 8-bit BASIC program,
-creating a new non-protected copy. See **DETAILS**, below, to
-understand how the protection and unprotection works.
+**unprotbas** modifies a tokenized LIST-protected Atari 8-bit BASIC
+program, creating a new non-protected copy. See **DETAILS**, below,
+to understand how the protection and unprotection works.
**input-file** must be a tokenized (SAVEd) Atari BASIC program. Use
-*-* to read from standard input.
+*-* to read from standard input, but **unprotbas** will refuse to
+read from standard input if it's a terminal.
**output-file** will be the unprotected tokenized BASIC program. If it
already exists, it will be overwritten. Use *-* to write to standard
@@ -32,13 +33,28 @@ the terminal).
OPTIONS
=======
+Options may appear in any order. The first non-option argument is used
+for **input-file**; the second is **output-file**. A third non-option
+argument is an error.
+
Option bundling is not supported, use e.g. **-v -f**, not **-vf**.
To use filenames beginning with *-*, write them as *./-file*, or they
will be treated as options.
+General Options
+---------------
+**--help**
+ Print usage message and exit.
+
+**--version**
+ Print version number and exit.
+
**-v**
- Verbose operation.
+ Verbose operation. When displaying a number in verbose mode, it will
+ be prefixed with *$* if it's in hex, or no prefix for decimal.
+Unprotection Options
+--------------------
**-f**
Force the variable name table to be rebuilt, even if it looks OK.
This option cannot be combined with **-n**.
@@ -56,6 +72,18 @@ will be treated as options.
memory, but doesn't write the result anywhere. In this mode, there
is no **output-file**.
+**-w**
+ Write the variable names to **varnames.txt**, one per line.
+ This can be edited, and later used with **-r** to set the variable names
+ to something sensible rather than A, B, C, etc. For an unprotected
+ program, you can use **-n** to write the existing names rather than
+ generating new ones. See **VARIABLE NAMES**, below. If **varnames.txt**
+ already exists, it will be overwritten.
+
+**-r**
+ Read variable names from **varnames.txt**, and use them instead of
+ generating the names. See **VARIABLE NAMES**, below.
+
EXIT STATUS
===========
@@ -114,7 +142,7 @@ Variable name table scrambling
names in the program, the recovery process just invents new ones,
named A through Z, A1 through A9, B1 through B9, etc, etc. It'll
require human intelligence to figure out what each variable is for,
- since the names are meaningless.
+ since the names are meaningless. See **VARIABLE NAMES**, below.
The **output-file** may not be the exact size that the
**input-file** was. Some types of variable-name scrambling shrink
@@ -124,11 +152,17 @@ Variable name table scrambling
generates only one- and two-character variable names, so the rebuilt
table might be smaller.
+ The program **PROTECT.BAS**, found on Disk 2 of the Holmes Archive,
+ creates protected BASIC programs that only use variable name
+ scrambling.
+
+ **protbas**\(1) also does variable name scrambling.
+
Bad next-line pointer
Every line of tokenized BASIC contains a line length byte, which
- BASIC uses as a pointer to the next line of code. Before printing
- the READY prompt, BASIC iterates over every line of code in the
- program, using the next-line pointers, in order to delete any
+ BASIC uses as a pointer to the next line of code. Before executing
+ an immediate mode command, BASIC iterates over every line of code in
+ the program, using the next-line pointers, in order to delete any
existing line 32768 (the previous immediate mode command). If any
line's pointer is set to zero, that means it points to itself.
@@ -138,28 +172,53 @@ Bad next-line pointer
after LOADing such a file, *nothing* will work (even pressing RESET
won't get you out of it). The only way to use such a program is to
use the RUN command with a filename, and if the program ever exits
- (due to END, STOP, an error, or the Break key), BASIC will get stuck
- again.
+ (due to END, STOP, an error, Break key, or even System Reset), BASIC
+ will get stuck again.
- This doesn't *have* to be done with the last line in the
- program. The "poisoned" line could be followed by more lines of
- code, though they could never actually execute.
+ This doesn't *have* to be done with the last line in the program,
+ though it normally is. The "poisoned" line can never be executed (or
+ BASIC will lock up), but it could be followed by more lines of code
+ (which also could never be executed).
Line 32100 in the example above does this job, taking advantage of
the STMCUR pointer used by BASIC, which holds the address of the
line of tokenized code currently being executed.
- **unprotbas** fixes this simply by calculating what the pointer
- should be (based on the tokens in the line) and changing it. No
- information is lost by doing this.
+ Each statement in the line also has a statement-length byte. For
+ lines with only one statement, its value is the same as the line
+ length. For lines with multiple statements (separated by *:*), it's
+ a pointer to the next statement, counting from the start of the
+ current line. For the last statement on a line, it's a pointer to
+ the next line of code, meaning it's identical to the line length.
+
+ **unprotbas** fixes bad line lengths by setting the line length to
+ the statement length of the last statement. No information is lost
+ by doing this.
+
+ The program **UNPROTEC**, from the *Pirate's Treasure Chest*, can
+ fix bad pointers in protected programs, though it doesn't do
+ anything about variable name scrambling.
+
+ **protbas** also does this type of protection.
One more thing **unprotbas** can do is remove extra data from the end
of the file. It's possible for BASIC files to contain extra data that
-occurs after the end of the program. Some programs use this as a way
-to load arbitrary binary data into memory along with the program; for
-other programs, the extra data is truly garbage (e.g. an EOF character
-if the file came from a CP/M system, or padding to a block size if a
-dumb implementation of XMODEM was used to transfer the file).
+occurs after the end of the program. Such data might be:
+
+- Pre-defined strings and/or arrays, saved with the program by
+ modifying the STARP pointer.
+
+- Arbitrary binary data used by the program at runtime, such as
+ machine language routines, or fonts.
+
+- Zero bytes, caused by SAVEing the program with revision B BASIC. Every
+ time a program is LOADed, edited (or not) and then SAVEd again, 16
+ bytes of extra (garbage) data gets added to the program. To avoid
+ this, don't use revision B (use rev C if possible, A otherwise).
+
+- Garbage added by some system previously used to store or transmit
+ the file. CP/M systems might add an EOF (^Z) character. Dumb
+ file transfer software might pad the file up to its block size.
Normally, such "garbage" doesn't hurt anything. BASIC ignores it. Or
it normally does... if you suspect it's causing a problem, you can
@@ -167,4 +226,73 @@ remove it with the **-g** option. If removing the "garbage" causes the
program to fail to run, it wasn't garbage! **unprotbas** doesn't
remove extra data by default, to be on the safe side.
+VARIABLE NAMES
+==============
+
+If variable name scrambling was used, the original variable names no
+longer exist. **unprotbas** will generate them, according to these rules:
+
+ The first 26 numeric variables will be called *A* through *Z*. Further
+ numeric variables will be *A1* through *A9*, *B1* through *B9*, etc.
+
+ The first 26 string variables will be *A$* to *Z$*, then *A1$* to
+ *A9$*, *B1$* to *B9$*, etc.
+
+ The first 26 array variables will be *A(* to *Z(*, then *A1(* to
+ *A9(*, *B1(* to *B9(*, etc.
+
+Note that array variables have only the *(* as part of the name. The
+closing *)* is "cosmetic" and not part of the actual name.
+
+To properly reverse-engineer the protected program, it's necessary to assign
+meaningful variable names. **unprotbas** isn't smart enough to do this for you,
+but it can semi-automate the process.
+
+First, run **unprotbas** with the **-w** option. This will create a
+file called **varnames.txt**, containing the generated variable names.
+These are in order, one line per variable name, ending with *$* for strings
+and the *(* for arrays.
+
+Load the unprotected program on the Atari and LIST it (or use **chkbas** to get a
+listing), and edit **varnames.txt** in a text editor.
+
+As you figure out what each variable's purpose is, change its name
+in the text file. When editing the file:
+
+- Don't add or delete any lines.
+- Don't get rid of the *$* or *(* at the end of any line.
+- You may enter the names in lowercase (**unprotbas** will convert them to uppercase).
+- Remember to follow the rules for BASIC variable names:
+ The first character must be a letter, other characters must be a letter
+ or a number, and only the last character can be *$* or *(*.
+- No duplicates of the same type are allowed (you can have *FOO* and *FOO$*,
+ but not two numerics called *FOO*).
+
+When you're finished, re-run **unprotbas**, this time with the **-r**
+option. If all is well, the unprotected program will use your variable
+names, rather than generating new ones. If you broke the rules, you
+should get an informative error message explaining what and where the
+problem is.
+
+This process can also be used for regular unprotected programs. Use
+**-n -w** the first time, to save the existing variable names to
+**varnames.txt** rather than generating new ones.
+
+NOTES
+=====
+
+Atari BASIC has a limit of 128 variables in a program. It's actually
+possible for the variable name table to contain up to 256 variables,
+though the 129th and further ones won't be usable in the program. The
+variable value table can hold more than 256 values, though the
+variable numbers wrap around once they pass 255. The attempt to add
+variables past the 128th causes BASIC to respond with *ERROR- 4*, but
+the variable does get added to the tables. **unprotbas** will preserve
+these extra (useless) entries in the tables.
+
+If there more than 256 entries in the value table, you will see
+"Warning: skipping variable numbers >=256 in value table". This is
+a pathological case, and shouldn't happen with programs that aren't
+deliberately crafted to test this behaviour.
+
.. include:: manftr.rst
diff --git a/vxrefbas.1 b/vxrefbas.1
new file mode 100644
index 0000000..17780d9
--- /dev/null
+++ b/vxrefbas.1
@@ -0,0 +1,187 @@
+.\" Man page generated from reStructuredText.
+.
+.
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+.TH "VXREFBAS" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+vxrefbas \- Variable cross-reference for tokenized Atari 8-bit BASIC files
+.SH SYNOPSIS
+.sp
+vxrefbas \fBinput\-file\fP
+.SH DESCRIPTION
+.sp
+\fBvxrefbas\fP reads an Atari 8\-bit BASIC tokenized program and prints a
+list of variables (names and token numbers), each with a list of line
+numbers where the variable is referenced.
+.sp
+String variable names end with \fI$\fP\&. Arrays end with \fI(\fP\&. Numeric
+(scalar) variable names don\(aqt have a special character.
+.sp
+After the list of lines, the reference count is shown in parentheses.
+Variables that aren\(aqt used by the program are listed as \fI(no references)\fP\&.
+.sp
+Each line number may be followed by an \fI=\fP and one or more markers,
+which show the type of variable access.
+.INDENT 0.0
+.TP
+.B \fBA\fP
+Variable is assigned on this line, with \fILET\fP or "implied LET" (e.g.
+\fIA=1\fP).
+.TP
+.B \fBF\fP
+Variable is used as the control variable of a \fIFOR\fP loop on this line.
+.TP
+.B \fBN\fP
+Variable is used in a \fINEXT\fP on this line.
+.TP
+.B \fBD\fP
+The variable is dimensioned (\fIDIM\fP command) on this line. Only applies to
+string and array variables.
+.TP
+.B \fBI\fP
+Variable was \fIINPUT\fP on this line.
+.TP
+.B \fBR\fP
+Variable was \fIREAD\fP on this line.
+.UNINDENT
+.SH OPTIONS
+.SS General Options
+.INDENT 0.0
+.TP
+.B \fB\-\-help\fP
+Print usage message and exit.
+.TP
+.B \fB\-\-version\fP
+Print version number and exit.
+.TP
+.B \fB\-v\fP
+Verbose operation. When displaying a number in verbose mode, it will
+be prefixed with \fI$\fP if it\(aqs in hex, or no prefix for decimal.
+.UNINDENT
+.SH BUGS
+.sp
+This program:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+10 FILE=1:INPUT #FILE,LINE$
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+Results in this:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+FILE/80: 10=AI (1)
+LINE$/81: 10=AI (1)
+ 2 variables, 0 unreferenced.
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+\fBvxrefbas\fP thinks \fIFILE\fP is being \fIINPUT\fP on line 10, which it isn\(aqt.
+The output for \fIFILE\fP should read \fI10=A (1)\fP\&.
+.sp
+Also, this program:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+10 DIM A(1):A(0)=10
+20 DIM B(A(0))
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+Results in this:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+A(/80: 10=AD 20=D (2)
+B(/81: 20=D (1)
+ 2 variables, 0 unreferenced.
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+\fBvxrefbas\fP thinks \fIA(\fP is being DIMensioned on line 20, which it isn\(aqt. The
+output for \fIA(\fP should read \fI10=AD 20 (2)\fP\&.
+.SH EXIT STATUS
+.sp
+0 for success, 1 for failure.
+.SH COPYRIGHT
+.sp
+WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details.
+.SH AUTHOR
+.INDENT 0.0
+.IP B. 3
+Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&.
+.UNINDENT
+.SH SEE ALSO
+.sp
+\fBa8eol\fP(1),
+\fBa8utf8\fP(1),
+\fBatr2xfd\fP(1),
+\fBatrsize\fP(1),
+\fBaxe\fP(1),
+\fBblob2c\fP(1),
+\fBblob2xex\fP(1),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
+\fBxexamine\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1),
+\fBxex\fP(5),
+\fBatascii\fP(7).
+.sp
+Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference
+Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc.
+.\" Generated by docutils manpage writer.
+.
diff --git a/vxrefbas.c b/vxrefbas.c
new file mode 100644
index 0000000..acc830c
--- /dev/null
+++ b/vxrefbas.c
@@ -0,0 +1,118 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+
+int target_var, lastline, assign, is_for, is_next, is_dim, is_read, is_input;
+int in_dim, in_read, in_input;
+int refcounts[128];
+
+void print_help(void) {
+ fprintf(stderr, "Usage: %s program.bas\n", self);
+ exit(0);
+}
+
+CALLBACK(new_line) {
+ assign = is_for = is_next = is_dim = is_read = is_input = 0;
+}
+
+CALLBACK(end_line) {
+ if(lastline != lineno) return;
+
+ printf("%d", lineno);
+ if(assign || is_for || is_next || is_dim || is_read || is_input) {
+ putchar('=');
+ if(assign) putchar('A');
+ if(is_for) putchar('F');
+ if(is_next) putchar('N');
+ if(is_dim) putchar('D');
+ if(is_read) putchar('R');
+ if(is_input) putchar('I');
+ }
+ putchar(' ');
+}
+
+CALLBACK(new_command) {
+ switch(tok) {
+ case CMD_LET:
+ case CMD_ILET:
+ assign = 1; break;
+ case CMD_FOR:
+ is_for = 1; break;
+ case CMD_NEXT:
+ is_next = 1; break;
+ case CMD_DIM:
+ in_dim = 1; break;
+ case CMD_READ:
+ in_read= 1; break;
+ case CMD_INPUT:
+ in_input= 1; break;
+ default: break;
+ }
+}
+
+CALLBACK(end_stmt) {
+ in_dim = in_read = in_input = 0;
+}
+
+CALLBACK(handle_var) {
+ if(in_dim) is_dim = 1;
+ if(in_input) is_input = 1;
+ if(in_read) is_read = 1;
+ if(lastline == lineno) return;
+ if((tok & 0x7f) == target_var) {
+ refcounts[target_var]++;
+ lastline = lineno;
+ }
+}
+
+int main(int argc, char **argv) {
+ unsigned short pos;
+ unsigned short vnpos;
+ int unref = 0;
+
+ set_self(*argv);
+ parse_general_args(argc, argv, print_help);
+
+ open_input(argv[1]);
+
+ readfile();
+ parse_header();
+
+ on_var_token = handle_var;
+ on_start_line = new_line;
+ on_end_line = end_line;
+ on_cmd_token = new_command;
+ on_end_stmt = end_stmt;
+
+ target_var = 0;
+ vnpos = vnstart;
+
+ for(pos = vvstart; pos < codestart; pos += 8) {
+ /* print the variable name */
+ while(program[vnpos] < 0x80)
+ putchar(program[vnpos++]);
+ putchar(program[vnpos++] & 0x7f);
+ printf("/%02x: ", target_var | 0x80);
+
+ lastline = -1;
+ walk_all_code();
+
+ if(!refcounts[target_var]) {
+ unref++;
+ printf("(no references)");
+ } else {
+ printf("(%d)", refcounts[target_var]);
+ }
+ putchar('\n');
+
+ if(target_var++ == 128) break;
+ }
+
+ printf(" %d variables, %d unreferenced.\n", target_var, unref);
+ return 0;
+}
diff --git a/vxrefbas.rst b/vxrefbas.rst
new file mode 100644
index 0000000..510d016
--- /dev/null
+++ b/vxrefbas.rst
@@ -0,0 +1,102 @@
+========
+vxrefbas
+========
+
+--------------------------------------------------------------
+Variable cross-reference for tokenized Atari 8-bit BASIC files
+--------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+vxrefbas **input-file**
+
+DESCRIPTION
+===========
+
+**vxrefbas** reads an Atari 8-bit BASIC tokenized program and prints a
+list of variables (names and token numbers), each with a list of line
+numbers where the variable is referenced.
+
+String variable names end with *$*. Arrays end with *(*. Numeric
+(scalar) variable names don't have a special character.
+
+After the list of lines, the reference count is shown in parentheses.
+Variables that aren't used by the program are listed as *(no references)*.
+
+Each line number may be followed by an *=* and one or more markers,
+which show the type of variable access.
+
+**A**
+ Variable is assigned on this line, with *LET* or "implied LET" (e.g.
+ *A=1*).
+
+**F**
+ Variable is used as the control variable of a *FOR* loop on this line.
+
+**N**
+ Variable is used in a *NEXT* on this line.
+
+**D**
+ The variable is dimensioned (*DIM* command) on this line. Only applies to
+ string and array variables.
+
+**I**
+ Variable was *INPUT* on this line.
+
+**R**
+ Variable was *READ* on this line.
+
+OPTIONS
+=======
+
+General Options
+---------------
+**--help**
+ Print usage message and exit.
+
+**--version**
+ Print version number and exit.
+
+**-v**
+ Verbose operation. When displaying a number in verbose mode, it will
+ be prefixed with *$* if it's in hex, or no prefix for decimal.
+
+BUGS
+====
+
+This program::
+
+ 10 FILE=1:INPUT #FILE,LINE$
+
+Results in this::
+
+ FILE/80: 10=AI (1)
+ LINE$/81: 10=AI (1)
+ 2 variables, 0 unreferenced.
+
+**vxrefbas** thinks *FILE* is being *INPUT* on line 10, which it isn't.
+The output for *FILE* should read *10=A (1)*.
+
+Also, this program::
+
+ 10 DIM A(1):A(0)=10
+ 20 DIM B(A(0))
+
+Results in this::
+
+ A(/80: 10=AD 20=D (2)
+ B(/81: 20=D (1)
+ 2 variables, 0 unreferenced.
+
+**vxrefbas** thinks *A(* is being DIMensioned on line 20, which it isn't. The
+output for *A(* should read *10=AD 20 (2)*.
+
+EXIT STATUS
+===========
+
+0 for success, 1 for failure.
+
+.. include:: manftr.rst
diff --git a/xex.5 b/xex.5
index 82774fe..b67c6b4 100644
--- a/xex.5
+++ b/xex.5
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "XEX" 5 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "XEX" 5 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
xex \- Atari 8-bit executable file format.
.\" RST source for xex(5) man page. Convert with:
@@ -307,10 +307,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/xex1to2.1 b/xex1to2.1
index 18ef638..3d66d3d 100644
--- a/xex1to2.1
+++ b/xex1to2.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "XEX1TO2" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "XEX1TO2" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
xex1to2 \- Convert an Atari DOS 1.0 executable to a standard Atari executable
.\" RST source for xex1to2(1) man page. Convert with:
@@ -81,10 +81,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/xexamine.1 b/xexamine.1
index 381deb8..73031a8 100644
--- a/xexamine.1
+++ b/xexamine.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "XEXAMINE" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "XEXAMINE" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
xexamine \- Show information on Atari 8-bit executables (XEX)
.\" RST source for xexamine(1) man page. Convert with:
@@ -139,10 +139,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/xexcat.1 b/xexcat.1
index 0b6708b..57069fb 100644
--- a/xexcat.1
+++ b/xexcat.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "XEXCAT" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "XEXCAT" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
xexcat \- Concatenate Atari 8-bit executables (XEX) into a single XEX file.
.\" RST source for xexcat(1) man page. Convert with:
@@ -199,10 +199,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/xexsplit.1 b/xexsplit.1
index efe9ba0..674dd22 100644
--- a/xexsplit.1
+++ b/xexsplit.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "XEXSPLIT" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "XEXSPLIT" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
xexsplit \- Split a multi-segment Atari 8-bit executable (XEX) into multiple single-segment files.
.\" RST source for xexsplit(1) man page. Convert with:
@@ -192,10 +192,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),
diff --git a/xfd2atr.1 b/xfd2atr.1
index 468361b..14e06ef 100644
--- a/xfd2atr.1
+++ b/xfd2atr.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "XFD2ATR" 1 "2024-05-03" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.TH "XFD2ATR" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
xfd2atr \- Convert an Atari 8-bit XFD (raw) disk image to an ATR image.
.\" RST source for xfd2atr(1) man page. Convert with:
@@ -120,10 +120,15 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBprotbas\fP(1),
+\fBrenumbas\fP(1),
\fBrom2cart\fP(1),
\fBunmac65\fP(1),
+\fBunprotbas\fP(1),
+\fBvxrefbas\fP(1),
\fBxexamine\fP(1),
\fBxexcat\fP(1),
\fBxexsplit\fP(1),