aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--Makefile25
-rw-r--r--README.txt18
-rw-r--r--a8eol.113
-rw-r--r--a8eol.rst4
-rw-r--r--a8utf8.19
-rw-r--r--atascii.713
-rw-r--r--atascii.rst2
-rw-r--r--atr2xfd.19
-rw-r--r--atrsize.19
-rw-r--r--axe.19
-rw-r--r--bas.c375
-rw-r--r--bas.h142
-rw-r--r--bcdfp.c74
-rw-r--r--bcdfp.h4
-rw-r--r--blob2c.19
-rw-r--r--blob2xex.19
-rw-r--r--cart2xex.114
-rw-r--r--cart2xex.rst5
-rw-r--r--cxrefbas.1187
-rw-r--r--cxrefbas.c74
-rw-r--r--cxrefbas.rst106
-rw-r--r--dasm2atasm.19
-rw-r--r--dumpbas.1237
-rw-r--r--dumpbas.c310
-rw-r--r--dumpbas.rst145
-rw-r--r--fenders.19
-rw-r--r--genopts.rst11
-rw-r--r--jindroush/Makefile6
-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.110
-rw-r--r--jindroush/man/chkbas.rst8
-rw-r--r--linetab.c199
-rw-r--r--linetab.h23
-rw-r--r--listbas.1135
-rw-r--r--listbas.c214
-rw-r--r--listbas.rst70
-rw-r--r--manftr.rst7
-rw-r--r--manhdr5.rst7
-rw-r--r--protbas.1144
-rw-r--r--protbas.c196
-rw-r--r--protbas.rst76
-rw-r--r--renumbas.1206
-rw-r--r--renumbas.c141
-rw-r--r--renumbas.rst133
-rw-r--r--rom2cart.19
-rw-r--r--tokens.c135
-rw-r--r--tokens.h4
-rw-r--r--unmac65.19
-rw-r--r--unmac65.xexbin0 -> 13899 bytes
-rw-r--r--unprotbas.1194
-rw-r--r--unprotbas.c528
-rw-r--r--unprotbas.rst168
-rw-r--r--vxrefbas.1147
-rw-r--r--vxrefbas.c167
-rw-r--r--vxrefbas.rst80
-rw-r--r--xex.59
-rw-r--r--xex1to2.19
-rw-r--r--xexamine.19
-rw-r--r--xexcat.19
-rw-r--r--xexsplit.19
-rw-r--r--xfd2atr.19
64 files changed, 4527 insertions, 392 deletions
diff --git a/.gitignore b/.gitignore
index a03f602..df9ea4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ atrsize
axe
blob2c
cart2xex
+cart2rom
fenders
rom2cart
unmac65
@@ -27,6 +28,13 @@ xexsplit
xfd2atr
xexamine
blob2xex
+vxrefbas
+cxrefbas
+dumpbas
+renumbas
+protbas
+unprotbas
+atrcheck
ksiders/atrdir
ksiders/atrextr
ksiders/makeatr
diff --git a/Makefile b/Makefile
index d1bb32f..6b54cb9 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 cxrefbas dumpbas fenders protbas renumbas rom2cart unmac65 unprotbas vxrefbas xex1to2 xexamine xexcat xexsplit xfd2atr listbas
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 cxrefbas.1 listbas.1
MAN5S=xex.5
MAN7S=atascii.7
DOCS=README.txt equates.inc *.dasm LICENSE ksiders/atr.txt
@@ -49,8 +49,25 @@ RST2MAN=rst2man
all: $(BINS) manpages symlinks subdirs
+unprotbas: bas.o
+
+protbas: bas.o
+
+renumbas: bas.o bcdfp.o linetab.o
+
+dumpbas: bas.o
+
+vxrefbas: bas.o
+
+cxrefbas: bas.o bcdfp.o linetab.o
+
+listbas: listbas.c bas.o bcdfp.o tokens.o
+ $(CC) $(CFLAGS) -o listbas listbas.c bas.o bcdfp.o tokens.o -lm
+
+bas.o: bas.c bas.h
+
subdirs:
- for dir in $(SUBDIRS); do make -C $$dir COPT=$(COPT); done
+ for dir in $(SUBDIRS); do make -C $$dir COPT="$(COPT)"; done
a8eol: a8eol.c
@@ -140,7 +157,7 @@ cart2rom: rom2cart
manpages: $(MANS) $(MAN5S) $(MAN7S)
-%.1: %.rst manhdr.rst manftr.rst
+%.1: %.rst manhdr.rst manftr.rst genopts.rst
$(RST2MAN) $< > $@
%.5: %.rst manhdr5.rst manftr.rst
diff --git a/README.txt b/README.txt
index 76003f8..c712ebe 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.
@@ -26,15 +26,25 @@ cart2rom - Convert an Atari800 CART image to a raw ROM image.
cart2xex - Convert an Atari 8-bit ROM cartridge image to a binary load
(XEX) file.
+cxrefbas - Code cross-reference for tokenized Atari 8-bit BASIC files.
+
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.
unmac65 - Detokenize Atari 8-bit Mac/65 SAVEd files.
+vxrefbas - Variable cross-reference for tokenized Atari 8-bit BASIC files.
+
unprotbas - Unprotect LIST-protected BASIC programs.
xex1to2 - Convert an Atari DOS 1.0 executable into a standard XEX file.
@@ -56,7 +66,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..78d501f 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-25" "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:
@@ -79,8 +79,8 @@ tabs and backspaces).
.B \-c
Replace non\-printing characters with ^x (ASCII input) or {x}
(ATASCII input). This option also enables the \fB\-8\fP option. When
-the input file is ATASCII, the output resembles a program list‐
-ing from an old computer magazine (see \fBATASCII CODES\fP, below).
+the input file is ATASCII, the output resembles a program listing
+from an old computer magazine (see \fBATASCII CODES\fP, below).
.TP
.B \-p
Replace non\-printing characters with \fI\&.\fP (period, dot).
@@ -469,11 +469,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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/a8eol.rst b/a8eol.rst
index ff81d82..c1c3b7b 100644
--- a/a8eol.rst
+++ b/a8eol.rst
@@ -61,8 +61,8 @@ Translation options:
-c
Replace non-printing characters with ^x (ASCII input) or {x}
(ATASCII input). This option also enables the **-8** option. When
- the input file is ATASCII, the output resembles a program list‐
- ing from an old computer magazine (see **ATASCII CODES**, below).
+ the input file is ATASCII, the output resembles a program listing
+ from an old computer magazine (see **ATASCII CODES**, below).
-p
Replace non-printing characters with *.* (period, dot).
diff --git a/a8utf8.1 b/a8utf8.1
index beee34b..c2986eb 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-25" "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:
@@ -100,11 +100,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..fde78c4 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-25" "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{
@@ -2160,11 +2158,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..824cd87 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-25" "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:
@@ -189,11 +189,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..878df71 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-25" "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:
@@ -203,11 +203,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..6bef0a5 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-25" "0.2.1" "Urchlay's Atari 8-bit Tools"
.SH NAME
axe \- ATR/XFD Editor
.\" RST source for axe(1) man page. Convert with:
@@ -144,11 +144,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..efb985e
--- /dev/null
+++ b/bas.c
@@ -0,0 +1,375 @@
+/* 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);
+CALLBACK_PTR(on_trailing_garbage);
+
+#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 = -1, tmpno, pos, end, tok;
+
+ linepos = codestart;
+ while(linepos < filelen) { /* loop over lines */
+ tmpno = getword(linepos);
+ if(tmpno <= lineno) {
+ fprintf(stderr, "Warning: line number %d at offset $%04x is <= previous line number %d.\n",
+ tmpno, linepos, lineno);
+ }
+ lineno = tmpno;
+ 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("Fatal: Program is code-protected; unprotect it first.");
+ }
+
+ if(lineno < startlineno) {
+ linepos = nextpos;
+ continue;
+ }
+
+ if(lineno > endlineno) break;
+
+ 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);
+
+ linepos = nextpos;
+ if(lineno == 32768) break;
+ }
+
+ if(endlineno == 32768 && linepos < filelen) {
+ if(verbose)
+ fprintf(stderr, "%s: Trailing garbage at EOF, %d bytes.\n", self, filelen - linepos);
+ CALL(on_trailing_garbage);
+ }
+}
diff --git a/bas.h b/bas.h
new file mode 100644
index 0000000..a464e0c
--- /dev/null
+++ b/bas.h
@@ -0,0 +1,142 @@
+/* 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_ON 0x1e
+#define CMD_GOSUB 0x0c
+#define CMD_TRAP 0x0d
+#define CMD_IF 0x07
+#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 CMD_GET 0x29
+#define CMD_LOCATE 0x31
+#define CMD_NOTE 0x1b
+#define OP_GOTO 0x17
+#define OP_GOSUB 0x18
+#define OP_THEN 0x1b
+#define OP_COMMA 0x12
+#define OP_ARR_COMMA 0x3c
+#define OP_SEMICOLON 0x15
+#define OP_EOS 0x14
+#define OP_EOL 0x16
+#define OP_NUMCONST 0x0e
+#define OP_STRCONST 0x0f
+#define OP_HASH 0x1c
+
+/* 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);
+extern CALLBACK_PTR(on_trailing_garbage);
+
+/* 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/bcdfp.c b/bcdfp.c
new file mode 100644
index 0000000..a80e55d
--- /dev/null
+++ b/bcdfp.c
@@ -0,0 +1,74 @@
+#include <string.h>
+#include <stdio.h>
+#include "bcdfp.h"
+
+/* very dumb and limited BCD floating point conversions.
+ they're written this way because they're only required to
+ support line numbers, and I don't want to have to link with
+ the math library (-lm). */
+
+extern void die(const char *msg);
+
+void die_range(void) {
+ die("Line number out of range (>65535)");
+}
+
+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 int result = 0;
+
+ /* examine the exponent/sign byte */
+ if(fp[0] == 0) return 0; /* special case */
+ if(fp[0] & 0x80) die("Negative line numbers not supported");
+
+ switch(fp[0]) {
+ case 0x40:
+ result = bcd2int(fp[1]);
+ if(fp[2] >= 0x50) result++;
+ break;
+ case 0x41:
+ result = bcd2int(fp[1]) * 100 + bcd2int(fp[2]);
+ if(fp[3] >= 0x50) result++;
+ break;
+ case 0x42:
+ result = bcd2int(fp[1]) * 10000 + bcd2int(fp[2]) * 100 + bcd2int(fp[3]);
+ if(fp[4] >= 0x50) result++;
+ break;
+ default:
+ die_range(); break;
+ }
+
+ if(result > 0xffff) die_range();
+
+ return (unsigned short)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);
+ }
+}
diff --git a/bcdfp.h b/bcdfp.h
new file mode 100644
index 0000000..a640122
--- /dev/null
+++ b/bcdfp.h
@@ -0,0 +1,4 @@
+unsigned char bcd2int(unsigned char bcd);
+unsigned char int2bcd(unsigned char i);
+unsigned short fp2int(const unsigned char *fp);
+void int2fp(unsigned short num, unsigned char *fp);
diff --git a/blob2c.1 b/blob2c.1
index f59dbe2..3f73de7 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-25" "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:
@@ -124,11 +124,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..507a16f 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-25" "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:
@@ -215,11 +215,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..7340d1f 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-25" "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:
@@ -65,9 +65,8 @@ Print help (usage) message and exit.
Sets the init address of the executable. Default is to use the
init address in the ROM image, at addresses $BFFE/BFFF. An init
address of zero means "no init address"; in this case, the output
-won\(aqt contain an init address at all. Executables created
-with this option probably won\(aqt run without further modifica‐
-tions.
+won\(aqt contain an init address at all. Executables created
+with this option probably won\(aqt run without further modifications.
.TP
.BI \-l \ address
Sets the load address of the executable. Default is to use the
@@ -234,11 +233,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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.rst b/cart2xex.rst
index aff6272..db6d967 100644
--- a/cart2xex.rst
+++ b/cart2xex.rst
@@ -45,9 +45,8 @@ OPTIONS
Sets the init address of the executable. Default is to use the
init address in the ROM image, at addresses $BFFE/BFFF. An init
address of zero means "no init address"; in this case, the output
- won't contain an init address at all. Executables created
- with this option probably won't run without further modifica‐
- tions.
+ won't contain an init address at all. Executables created
+ with this option probably won't run without further modifications.
-l address
Sets the load address of the executable. Default is to use the
diff --git a/cxrefbas.1 b/cxrefbas.1
new file mode 100644
index 0000000..c9a705f
--- /dev/null
+++ b/cxrefbas.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 "CXREFBAS" 1 "2024-06-25" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+cxrefbas \- Code cross-reference for tokenized Atari 8-bit BASIC files
+.SH SYNOPSIS
+.sp
+cxrefbas [\fB\-v\fP] \fBinput\-file\fP
+.SH DESCRIPTION
+.sp
+\fBcxrefbas\fP reads an Atari 8\-bit BASIC tokenized program. For each
+line number in the program, it prints a list of lines that reference
+it.
+.sp
+\fBinput\-file\fP must be a tokenized (SAVEd) Atari BASIC program. Use
+\fI\-\fP to read from standard input, but \fBcxrefbas\fP will refuse to read
+from standard input if it\(aqs a terminal.
+.sp
+Each line number reference in the output is followed by a letter that
+indicates the type of reference:
+.INDENT 0.0
+.TP
+.B \fBG\fP
+\fIGOTO\fP (without \fION\fP) or \fIGO TO\fP\&.
+.TP
+.B \fBS\fP
+\fIGOSUB\fP (without \fION\fP).
+.TP
+.B \fBI\fP
+\fIIF\fP with line number only, e.g. \fIIF X THEN 1000\fP\&.
+.TP
+.B \fBO\fP
+\fION/GOTO\fP or \fION/GOSUB\fP\&.
+.TP
+.B \fBR\fP
+\fIRESTORE\fP\&.
+.TP
+.B \fBT\fP
+\fITRAP\fP\&.
+.TP
+.B \fBL\fP
+\fILIST\fP\&. It\(aqs very rare for a program to \fILIST\fP parts of itself, but
+it\(aqs allowed by BASIC so it\(aqs supported here.
+.UNINDENT
+.sp
+If a line doesn\(aqt exist, but is referenced (e.g. \fIGOTO 100\fP, but there
+is no line 100), it\(aqs printed in the table, prefixed with \fI!\fP\&.
+.sp
+Any command that uses a computed value for a line number will print a
+warning on standard error, e.g. \fIGOTO A\fP or \fIGOSUB 100*A\fP\&. Even \fIGOTO
+100+0\fP is a computed value, since BASIC doesn\(aqt do constant folding.
+.sp
+Line numbers above 32767, e.g. \fITRAP 40000\fP, are not listed.
+.sp
+Atari BASIC allows fractional line numbers, such as \fIGOTO 123.4\fP\&.
+These are rounded to the nearest integer when the program is
+executed. \fBcxrefbas\fP handles these correctly, although you\(aqre
+not likely to run into them in real\-world programs.
+.SH OPTIONS
+.sp
+There are no application\-specific 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 EXAMPLE
+.sp
+This program:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+10 GOSUB 100:GOSUB 200
+100 RESTORE 1000:IF A THEN 120
+110 GOTO 200
+120 ON B GOTO 300,310,320
+200 RETURN
+300 PRINT 1
+310 PRINT 2
+1000 DATA XYZ
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+Produces this output:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+100: 10:S
+120: 100:I
+200: 10:S 110:G
+300: 120:O
+310: 120:O
+!320: 120:O
+1000: 100:R
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+Note that line 320 doesn\(aqt exist in the program, so it\(aqs shown with
+\fI!\fP in the output. Line 120 has \fI100:I\fP; if the \fITHEN 120\fP were
+changed to \fITHEN GOTO 120\fP, line 120 would read \fI100:G\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),
+\fBcxrefbas\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBlistbas\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/cxrefbas.c b/cxrefbas.c
new file mode 100644
index 0000000..8a7f8c1
--- /dev/null
+++ b/cxrefbas.c
@@ -0,0 +1,74 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+#include "bcdfp.h"
+#include "linetab.h"
+
+void print_ref_table(void) {
+ char c;
+ int i, j;
+ for(i = 0; i < 32768; i++) {
+ if(refcounts[i]) {
+ if(!lines_exist[i]) putchar('!');
+ printf("%d: ", i);
+ for(j = 0; j < refcounts[i]; j++) {
+ printf("%d:", linerefs[i][j].lineno);
+ switch(linerefs[i][j].cmd) {
+ case CMD_GOTO: c = 'G'; break;
+ case CMD_GO_TO: c = 'G'; break;
+ case CMD_GOSUB: c = 'S'; break;
+ case CMD_RESTORE: c = 'R'; break;
+ case CMD_TRAP: c = 'T'; break;
+ case CMD_IF: c = 'I'; break;
+ case CMD_ON: c = 'O'; break;
+ case CMD_LIST: c = 'L'; break;
+ default: c = '?'; break;
+ }
+ putchar(c);
+ putchar(' ');
+ }
+ putchar('\n');
+ }
+ }
+}
+
+void print_help(void) {
+ printf("Usage: %s [-v] program.bas\n", self);
+ exit(0);
+}
+
+void parse_args(int argc, char **argv) {
+ int opt;
+
+ while( (opt = getopt(argc, argv, "v")) != -1) {
+ switch(opt) {
+ case 'v': verbose = 1; break;
+ default: print_help(); exit(1);
+ }
+ }
+
+ if(optind >= argc)
+ die("No input file given (use - for stdin).");
+ else
+ open_input(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();
+
+ build_ref_table();
+ print_ref_table();
+ free_ref_table();
+
+ return 0;
+}
diff --git a/cxrefbas.rst b/cxrefbas.rst
new file mode 100644
index 0000000..7004217
--- /dev/null
+++ b/cxrefbas.rst
@@ -0,0 +1,106 @@
+========
+cxrefbas
+========
+
+----------------------------------------------------------
+Code cross-reference for tokenized Atari 8-bit BASIC files
+----------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+cxrefbas [**-v**] **input-file**
+
+DESCRIPTION
+===========
+
+**cxrefbas** reads an Atari 8-bit BASIC tokenized program. For each
+line number in the program, it prints a list of lines that reference
+it.
+
+**input-file** must be a tokenized (SAVEd) Atari BASIC program. Use
+*-* to read from standard input, but **cxrefbas** will refuse to read
+from standard input if it's a terminal.
+
+Each line number reference in the output is followed by a letter that
+indicates the type of reference:
+
+**G**
+ *GOTO* (without *ON*) or *GO TO*.
+
+**S**
+ *GOSUB* (without *ON*).
+
+**I**
+ *IF* with line number only, e.g. *IF X THEN 1000*.
+
+**O**
+ *ON/GOTO* or *ON/GOSUB*.
+
+**R**
+ *RESTORE*.
+
+**T**
+ *TRAP*.
+
+**L**
+ *LIST*. It's very rare for a program to *LIST* parts of itself, but
+ it's allowed by BASIC so it's supported here.
+
+If a line doesn't exist, but is referenced (e.g. *GOTO 100*, but there
+is no line 100), it's printed in the table, prefixed with *!*.
+
+Any command that uses a computed value for a line number will print a
+warning on standard error, e.g. *GOTO A* or *GOSUB 100\*A*. Even *GOTO
+100+0* is a computed value, since BASIC doesn't do constant folding.
+
+Line numbers above 32767, e.g. *TRAP 40000*, are not listed.
+
+Atari BASIC allows fractional line numbers, such as *GOTO 123.4*.
+These are rounded to the nearest integer when the program is
+executed. **cxrefbas** handles these correctly, although you're
+not likely to run into them in real-world programs.
+
+OPTIONS
+=======
+
+There are no application-specific options.
+
+.. include:: genopts.rst
+
+EXAMPLE
+=======
+
+This program::
+
+ 10 GOSUB 100:GOSUB 200
+ 100 RESTORE 1000:IF A THEN 120
+ 110 GOTO 200
+ 120 ON B GOTO 300,310,320
+ 200 RETURN
+ 300 PRINT 1
+ 310 PRINT 2
+ 1000 DATA XYZ
+
+Produces this output::
+
+ 100: 10:S
+ 120: 100:I
+ 200: 10:S 110:G
+ 300: 120:O
+ 310: 120:O
+ !320: 120:O
+ 1000: 100:R
+
+Note that line 320 doesn't exist in the program, so it's shown with
+*!* in the output. Line 120 has *100:I*; if the *THEN 120* were
+changed to *THEN GOTO 120*, line 120 would read *100:G*.
+
+EXIT STATUS
+===========
+
+0 for success, 1 for failure.
+
+.. include:: manftr.rst
diff --git a/dasm2atasm.1 b/dasm2atasm.1
index 699f97d..e67f694 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-25" "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:
@@ -231,11 +231,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..5867bf4
--- /dev/null
+++ b/dumpbas.1
@@ -0,0 +1,237 @@
+.\" 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-25" "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 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
+.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 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),
+\fBcxrefbas\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBlistbas\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..087e5b1
--- /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) {
+ printf("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..6dc32b9
--- /dev/null
+++ b/dumpbas.rst
@@ -0,0 +1,145 @@
+=======
+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
+=======
+
+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*".
+
+.. include:: genopts.rst
+
+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..2cc4dcf 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-25" "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:
@@ -278,11 +278,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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/genopts.rst b/genopts.rst
new file mode 100644
index 0000000..dfc588a
--- /dev/null
+++ b/genopts.rst
@@ -0,0 +1,11 @@
+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.
diff --git a/jindroush/Makefile b/jindroush/Makefile
index 0a11a8f..605dc70 100644
--- a/jindroush/Makefile
+++ b/jindroush/Makefile
@@ -54,9 +54,3 @@ install: all
chmod 644 $(DESTDIR)/$(MAN1DIR)/$$i.1 ; \
[ "$(GZIP_MAN)" = "y" ] && gzip -f $(DESTDIR)/$(MAN1DIR)/$$i.1 ; \
done
- ln -sf chkbas $(DESTDIR)$(BINDIR)/listbas
- if [ "$(GZIP_MAN)" = "y" ]; then \
- ln -sf chkbas.1.gz $(DESTDIR)$(MAN1DIR)/listbas.1.gz ; \
- else \
- ln -sf chkbas.1 $(DESTDIR)$(MAN1DIR)/listbas.1 ; \
- fi
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..1f5313a 100644
--- a/jindroush/man/chkbas.1
+++ b/jindroush/man/chkbas.1
@@ -27,14 +27,12 @@ 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-06-25" "1.10" "Jindroush's Atari 8-bit tools"
.SH NAME
chkbas \- check and detokenize Atari BASIC SAVEd files
.SH SYNOPSIS
.sp
chkbas [\fB\-atari\fP] [\fB\-short\fP] [\fB\-verbose\fP] [\fB\-noinverse\fP] [\fB\-tbs\fP | \fB\-bxl\fP | \fB\-bxe\fP] \fBbasic\-file\fP [\fBoutput\-file\fP]
-.sp
-listbas [\fB\-atari\fP] [\fB\-verbose\fP] [\fB\-noinverse\fP] [\fB\-tbs\fP | \fB\-bxl\fP | \fB\-bxe\fP] \fBbasic\-file\fP [\fBoutput\-file\fP]
.SH DESCRIPTION
.sp
\fBchkbas\fP detokenizes Atari 8\-bit BASIC SAVEd files. It lists the BASIC
@@ -47,8 +45,6 @@ standard output. If \fBbasic\-file\fP is anything other than standard
Atari BASIC, the BASIC dialect must be specified (see \fBOPTIONS\fP,
below).
.sp
-\fBlistbas\fP is simply an alias for \fBchkbas \-short\fP\&.
-.sp
Sample run, with no options:
.INDENT 0.0
.INDENT 3.5
@@ -69,7 +65,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
@@ -125,7 +121,7 @@ EOL characters (\fB$9B\fP).
.B \fB\-short\fP
Output only the program listing, with lines terminated by the
system default newline character(s), e.g. \fBn\fP on UNIX\-like OSes,
-\fBrn\fP on MS\-DOS or Windows. This is the default for \fBlistbas\fP\&.
+\fBrn\fP on MS\-DOS or Windows.
.TP
.B \fB\-verbose\fP
Program listing will be interspersed with per\-line and per\-statement
diff --git a/jindroush/man/chkbas.rst b/jindroush/man/chkbas.rst
index 2f9e6b7..e16f475 100644
--- a/jindroush/man/chkbas.rst
+++ b/jindroush/man/chkbas.rst
@@ -15,8 +15,6 @@ SYNOPSIS
chkbas [**-atari**] [**-short**] [**-verbose**] [**-noinverse**] [**-tbs** | **-bxl** | **-bxe**] **basic-file** [**output-file**]
-listbas [**-atari**] [**-verbose**] [**-noinverse**] [**-tbs** | **-bxl** | **-bxe**] **basic-file** [**output-file**]
-
DESCRIPTION
===========
@@ -30,8 +28,6 @@ standard output. If **basic-file** is anything other than standard
Atari BASIC, the BASIC dialect must be specified (see **OPTIONS**,
below).
-**listbas** is simply an alias for **chkbas -short**.
-
Sample run, with no options::
$ chkbas HELLO.BAS
@@ -48,7 +44,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
@@ -103,7 +99,7 @@ Output Options
**-short**
Output only the program listing, with lines terminated by the
system default newline character(s), e.g. **\n** on UNIX-like OSes,
- **\r\n** on MS-DOS or Windows. This is the default for **listbas**.
+ **\r\n** on MS-DOS or Windows.
**-verbose**
Program listing will be interspersed with per-line and per-statement
diff --git a/linetab.c b/linetab.c
new file mode 100644
index 0000000..55a5c97
--- /dev/null
+++ b/linetab.c
@@ -0,0 +1,199 @@
+#include "linetab.h"
+
+lineref_t *linerefs[32769];
+int refcounts[32769];
+int lines_exist[32769];
+unsigned char last_cmd, on_op;
+int last_cmd_pos;
+
+void add_lineref(unsigned short from, unsigned short pos) {
+ lineref_t *p;
+ int c;
+ unsigned short to;
+
+ to = fp2int(program + pos);
+ if(to > 32767) return;
+
+ p = linerefs[to];
+ c = refcounts[to];
+
+ if(c) {
+ p = realloc(p, sizeof(lineref_t) * (c + 1));
+ } else {
+ p = malloc(sizeof(lineref_t));
+ }
+
+ if(!p) die("Out of memory.");
+
+ linerefs[to] = p;
+ linerefs[to][c].lineno = from;
+ linerefs[to][c].pos = pos;
+ linerefs[to][c].cmd = last_cmd;
+ c++;
+ refcounts[to] = c;
+}
+
+/* makes sure a numeric constant isn't start of an expression. */
+int is_standalone_num(unsigned short pos) {
+ if(program[pos] != OP_NUMCONST) return 0;
+ switch(program[pos + 7]) {
+ case OP_EOS:
+ case OP_EOL:
+ case OP_COMMA:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+CALLBACK(start_stmt) {
+ lines_exist[lineno] = 1;
+}
+
+CALLBACK(got_cmd) {
+ last_cmd = tok;
+ last_cmd_pos = pos;
+ on_op = 0;
+}
+
+void computed_msg(unsigned short lineno) {
+ static int last_lineno = -1;
+ char *cmd;
+
+ /* avoid duplicate warnings */
+ if(lineno == last_lineno) return;
+ last_lineno = lineno;
+
+ switch(last_cmd) {
+ case CMD_GOTO:
+ cmd = "GOTO"; break;
+ case CMD_GO_TO:
+ cmd = "GO TO"; break;
+ case CMD_GOSUB:
+ cmd = "GOSUB"; break;
+ case CMD_RESTORE:
+ cmd = "RESTORE"; break;
+ case CMD_TRAP:
+ cmd = "TRAP"; break;
+ case CMD_ON:
+ if(on_op == OP_GOSUB)
+ cmd = "ON/GOSUB";
+ else
+ cmd = "ON/GOTO";
+ break;
+ case CMD_LIST:
+ cmd = "LIST";
+ break;
+ default: /* should never happen! */
+ cmd = "???"; break;
+ }
+
+ fprintf(stderr, "%s: Warning: Computed %s at line %d.\n", self, cmd, lineno);
+}
+
+CALLBACK(got_var) {
+ switch(last_cmd) {
+ /* any use of a variable in the arguments to these means
+ we can't renumber that argument. */
+ case CMD_GOTO:
+ case CMD_GO_TO:
+ case CMD_GOSUB:
+ case CMD_RESTORE:
+ case CMD_TRAP:
+ case CMD_LIST:
+ computed_msg(lineno);
+ break;
+ case CMD_ON:
+ /* vars are OK in ON, before the GOTO or GOSUB */
+ if(on_op) computed_msg(lineno);
+ break;
+ default:
+ break;
+ }
+}
+
+CALLBACK(got_exp) {
+ unsigned char last_tok = program[pos - 1];
+ int standalone;
+
+ if(last_cmd == CMD_ON) {
+ if(tok == OP_GOTO || tok == OP_GOSUB)
+ on_op = tok;
+ }
+
+ if(tok != OP_NUMCONST) return;
+
+ /* beware: standalone only means nothing *follows* the constant
+ in the same expression. still have to check last_tok to see
+ what came before. */
+ standalone = is_standalone_num(pos);
+
+ switch(last_cmd) {
+ /* these take a single argument */
+ case CMD_GOTO:
+ case CMD_GO_TO:
+ case CMD_GOSUB:
+ case CMD_RESTORE:
+ case CMD_TRAP:
+ if((pos == last_cmd_pos + 1) && standalone) {
+ add_lineref(lineno, pos + 1);
+ } else {
+ computed_msg(lineno);
+ }
+ break;
+ case CMD_IF:
+ /* this only applies to bare line numbers, like IF A THEN 1000,
+ not IF A THEN GOTO 1000 (or anything else after THEN). */
+ if(last_tok == OP_THEN) {
+ add_lineref(lineno, pos + 1);
+ }
+ break;
+ case CMD_ON: {
+ /* takes arbitrary number of arguments */
+ switch(last_tok) {
+ case OP_GOTO:
+ case OP_GOSUB:
+ case OP_COMMA:
+ if(standalone)
+ add_lineref(lineno, pos + 1);
+ else
+ computed_msg(lineno);
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+ case CMD_LIST: {
+ /* takes one or two arguments */
+ switch(last_tok) {
+ case CMD_LIST:
+ case OP_COMMA:
+ if(standalone)
+ add_lineref(lineno, pos + 1);
+ else
+ computed_msg(lineno);
+ break;
+ default:
+ break;
+ }
+ }
+ default:
+ break;
+ }
+}
+
+void build_ref_table(void) {
+ on_start_stmt = start_stmt;
+ on_cmd_token = got_cmd;
+ on_exp_token = got_exp;
+ on_var_token = got_var;
+ walk_all_code();
+ on_start_stmt = on_cmd_token = on_exp_token = on_var_token = 0;
+}
+
+void free_ref_table(void) {
+ int i;
+ for(i = 0; i < 32768; i++)
+ if(linerefs[i]) free(linerefs[i]);
+}
diff --git a/linetab.h b/linetab.h
new file mode 100644
index 0000000..e38335a
--- /dev/null
+++ b/linetab.h
@@ -0,0 +1,23 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+#include "bcdfp.h"
+
+typedef struct {
+ unsigned short lineno;
+ unsigned short pos;
+ unsigned char cmd;
+} lineref_t;
+
+extern lineref_t *linerefs[];
+extern int refcounts[];
+extern int lines_exist[];
+
+extern void add_lineref(unsigned short from, unsigned short pos);
+extern void build_ref_table(void);
+extern void free_ref_table(void);
diff --git a/listbas.1 b/listbas.1
new file mode 100644
index 0000000..04f2f91
--- /dev/null
+++ b/listbas.1
@@ -0,0 +1,135 @@
+.\" 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 "LISTBAS" 1 "2024-06-25" "0.2.1" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+listbas \- List the source of a tokenized Atari 8-bit BASIC program
+.SH SYNOPSIS
+.sp
+listbas [\fB\-v\fP] [\fB\-i\fP] [\fB\-a\fP | \fB\-u\fP ] \fBinput\-file\fP
+.SH DESCRIPTION
+.sp
+\fBlistbas\fP acts like the \fILIST\fP command in BASIC. It reads a
+tokenized (SAVEd) BASIC program and prints the code in human\-readable
+format.
+.sp
+By default, output is piped through \fBa8eol\fP(1), to convert ATASCII
+characters to human\-readable sequences. Raw ATASCII and Unicode output
+are also available.
+.SH OPTIONS
+.SS List options
+.INDENT 0.0
+.TP
+.B \fB\-i\fP
+Include the immediate mode command (line 32768) in the output.
+.TP
+.B \fB\-a\fP
+Output raw ATASCII; no translation to the host character set. Must be
+used with redirection; \fBlistbas\fP will not write ATASCII to the terminal.
+.TP
+.B \fB\-u\fP
+Use \fBa8utf8\fP(1) to translate ATASCII to ASCII. Requires \fBa8utf8\fP
+somewhere in \fIPATH\fP\&.
+.UNINDENT
+.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 NOTES
+.sp
+\fBlistbas\fP is similar to Jindroush\(aqs \fBchkbas\fP(1). The main differences are:
+.INDENT 0.0
+.IP \(bu 2
+\fBlistbas\fP only supports Atari BASIC, not Turbo BASIC or BASIC XL/XE.
+.IP \(bu 2
+\fBlistbas\fP doesn\(aqt show information about the variables. Use \fBvxrefbas\fP(1)
+for that.
+.IP \(bu 2
+\fBlistbas\fP will not write ATASCII data to your terminal. Instead, it uses
+\fBa8eol\fP(1) or \fBa8utf8\fP(1) to convert the output to something human\-readable
+that won\(aqt confuse the terminal.
+.IP \(bu 2
+\fBlistbas\fP only lists line 32768 (the immediate mode command) if
+specifically asked to do so.
+.IP \(bu 2
+\fBlistbas\fP doesn\(aqt print a banner on startup.
+.UNINDENT
+.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),
+\fBcxrefbas\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBlistbas\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/listbas.c b/listbas.c
new file mode 100644
index 0000000..2cb9dcc
--- /dev/null
+++ b/listbas.c
@@ -0,0 +1,214 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include <math.h>
+#include <errno.h>
+
+#include "bas.h"
+#include "bcdfp.h"
+#include "tokens.h"
+
+int immediate = 0, a8utf8 = 0, a8eol = 1;
+
+FILE *outfh;
+
+void print_help(void) {
+ printf("Usage: %s [-v] [-i] [-a|-u] <inputfile>\n", self);
+ printf(" -v: verbose.\n");
+ printf(" -i: show immediate mode command (line 32768).\n");
+ printf(" -a: output raw ATASCII.\n");
+ printf(" -u: output Unicode.\n");
+}
+
+void parse_args(int argc, char **argv) {
+ int opt;
+
+ while( (opt = getopt(argc, argv, "viau")) != -1) {
+ switch(opt) {
+ case 'v': verbose = 1; break;
+ case 'i': immediate = 1; break;
+ case 'a': a8utf8 = a8eol = 0; break;
+ case 'u': a8utf8 = 1; a8eol = 0; break;
+ default:
+ print_help();
+ exit(1);
+ }
+ }
+
+ if(optind >= argc)
+ die("No input file given (use - for stdin).");
+ else
+ open_input(argv[optind]);
+}
+
+void setup_outfh(void) {
+ const char *cmd;
+
+ /* search current dir before PATH. no easy way to detect errors here,
+ have to wait until we call pclose(). */
+ if(a8eol) {
+ cmd = "./a8eol -u -c 2>/dev/null || a8eol -u -c 2>/dev/null || exit 1";
+ } else if(a8utf8) {
+ cmd = "./a8utf8 2>/dev/null || a8utf8 2>/dev/null || exit 1";
+ } else {
+ if(isatty(fileno(stdout))) {
+ die("Refusing to write ATASCII data to the terminal.");
+ }
+ outfh = stdout;
+ return;
+ }
+
+ outfh = popen(cmd, "w");
+ if(!outfh) {
+ /* fork() or pipe() failed. does NOT detect if the command
+ wasn't found. */
+ perror(self);
+ exit(1);
+ }
+}
+
+void close_outfh(void) {
+ if(a8eol || a8utf8) {
+ if(pclose(outfh)) {
+ die("output filter failed; a8eol or a8utf8 not in current dir or $PATH.");
+ }
+ }
+}
+
+void outchr(char c) {
+ putc(c, outfh);
+}
+
+/* this should probably be moved to bcdfp.c */
+double bcd2double(const unsigned char *num) {
+ double result = 0, sign;
+ int exp, i;
+
+ exp = *num;
+ if(!exp) {
+ return 0.0;
+ }
+
+ sign = (exp & 0x80 ? -1.0 : 1.0);
+ exp &= 0x7f;
+ exp -= 0x40;
+
+ for(i = 1; i < 6; i++) {
+ result *= 100.0;
+ result += bcd2int(num[i]);
+ }
+
+ result *= pow(100, exp - 4);
+ result *= sign;
+
+ return result;
+}
+
+void print_number(unsigned int pos) {
+ fprintf(outfh, "%G", bcd2double(program + pos));
+}
+
+void print_string(unsigned int pos, unsigned int len) {
+ outchr('"');
+ while(len--) outchr(program[pos++]);
+ outchr('"');
+}
+
+CALLBACK(print_lineno) {
+ fprintf(outfh, "%d ", lineno);
+}
+
+CALLBACK(print_cmd) {
+ const char *name;
+
+ if(tok == CMD_ILET) return;
+
+ if(tok > last_command || (!(name = commands[tok])))
+ fprintf(outfh, "(bad cmd token $%02x) ", tok);
+ else
+ fprintf(outfh, "%s ", name);
+}
+
+CALLBACK(print_op) {
+ const char *name;
+
+ switch(tok) {
+ case OP_NUMCONST:
+ print_number(pos + 1);
+ return;
+ case OP_STRCONST:
+ print_string(pos + 2, program[pos + 1]);
+ return;
+ case OP_EOL:
+ return;
+ default: break;
+ }
+
+
+ if(tok > last_operator || (!(name = operators[tok])))
+ fprintf(outfh, "(bad op token $%02x)", tok);
+ else
+ fprintf(outfh, "%s", name);
+}
+
+CALLBACK(print_varname) {
+ int i, count;
+ unsigned char c;
+
+ tok &= 0x7f;
+ for(i = vnstart, count = 0; count < tok; i++) {
+ if(program[i] & 0x80) count++;
+ }
+ while(1) {
+ c = program[i++];
+ outchr(c & 0x7f);
+ if(c & 0x80) break;
+ }
+}
+
+CALLBACK(print_text) {
+ while(program[pos] != 0x9b) outchr(program[pos++]);
+}
+
+CALLBACK(print_newline) {
+ outchr(0x9b);
+}
+
+CALLBACK(code_prot) {
+ fprintf(stderr, "%s: Program is code-protected, stopping at line %d.\n", self, lineno);
+ close_outfh();
+ exit(0);
+}
+
+void list(void) {
+ on_start_line = print_lineno;
+ on_cmd_token = print_cmd;
+ on_exp_token = print_op;
+ on_var_token = print_varname;
+ on_end_line = print_newline;
+ on_text = print_text;
+ on_bad_line_length = code_prot;
+ walk_code(0, 32767 + immediate);
+}
+
+int main(int argc, char **argv) {
+ set_self(*argv);
+ parse_general_args(argc, argv, print_help);
+ parse_args(argc, argv);
+
+ readfile();
+ parse_header();
+
+ if(!vntable_ok())
+ die("Program is variable-protected; unprotect it first.");
+
+ setup_outfh();
+ list();
+ close_outfh();
+
+ return 0;
+}
diff --git a/listbas.rst b/listbas.rst
new file mode 100644
index 0000000..ed6c559
--- /dev/null
+++ b/listbas.rst
@@ -0,0 +1,70 @@
+=======
+listbas
+=======
+
+--------------------------------------------------------
+List the source of a tokenized Atari 8-bit BASIC program
+--------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+listbas [**-v**] [**-i**] [**-a** | **-u** ] **input-file**
+
+DESCRIPTION
+===========
+
+**listbas** acts like the *LIST* command in BASIC. It reads a
+tokenized (SAVEd) BASIC program and prints the code in human-readable
+format.
+
+By default, output is piped through **a8eol**\(1), to convert ATASCII
+characters to human-readable sequences. Raw ATASCII and Unicode output
+are also available.
+
+OPTIONS
+=======
+
+List options
+------------
+
+**-i**
+ Include the immediate mode command (line 32768) in the output.
+
+**-a**
+ Output raw ATASCII; no translation to the host character set. Must be
+ used with redirection; **listbas** will not write ATASCII to the terminal.
+
+**-u**
+ Use **a8utf8**\(1) to translate ATASCII to ASCII. Requires **a8utf8**
+ somewhere in *PATH*.
+
+.. include:: genopts.rst
+
+NOTES
+=====
+
+**listbas** is similar to Jindroush's **chkbas**\(1). The main differences are:
+
+- **listbas** only supports Atari BASIC, not Turbo BASIC or BASIC XL/XE.
+
+- **listbas** doesn't show information about the variables. Use **vxrefbas**\(1)
+ for that.
+
+- **listbas** will not write ATASCII data to your terminal. Instead, it uses
+ **a8eol**\(1) or **a8utf8**\(1) to convert the output to something human-readable
+ that won't confuse the terminal.
+
+- **listbas** only lists line 32768 (the immediate mode command) if
+ specifically asked to do so.
+
+- **listbas** doesn't print a banner on startup.
+
+EXIT STATUS
+===========
+
+0 for success, 1 for failure.
+
+.. include:: manftr.rst
diff --git a/manftr.rst b/manftr.rst
index 12e78d2..bdc3e58 100644
--- a/manftr.rst
+++ b/manftr.rst
@@ -19,11 +19,18 @@ SEE ALSO
**blob2c**\(1),
**blob2xex**\(1),
**cart2xex**\(1),
+**cxrefbas**\(1),
**dasm2atasm**\(1),
+**dumpbas**\(1),
**f2toxex**\(1),
**fenders**\(1),
+**listbas**\(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..59c249c
--- /dev/null
+++ b/protbas.1
@@ -0,0 +1,144 @@
+.\" 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-25" "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 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
+.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 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),
+\fBcxrefbas\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBlistbas\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..4697fac
--- /dev/null
+++ b/protbas.c
@@ -0,0 +1,196 @@
+#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");
+ }
+ }
+}
+
+CALLBACK(bad_offset) {
+ fprintf(stderr, "%s: program already was code-protected.\n", self);
+ exit(2);
+}
+
+unsigned short last_pos = 0;
+int last_lineno = -1;
+
+CALLBACK(save_linepos) {
+ last_pos = pos;
+ if(lineno == 32768) return;
+ last_lineno = lineno;
+}
+
+/* iterate over all the lines, insert a poisoned line 32767 just
+ before line 32768 */
+void breakcode(void) {
+ int offset;
+
+ on_start_line = save_linepos;
+ on_bad_line_length = bad_offset;
+ walk_all_code();
+
+ if(last_lineno == -1) die("Can't protect code because there are no lines of code.");
+ if(last_lineno == 32767) die("Can't protect code because there is already a line 32767.");
+
+ /* last_pos is now the start of line 32768, move it up to make room for
+ the new line */
+ offset = sizeof(badcode);
+ memmove(program + last_pos + offset, program + last_pos, filelen);
+
+ /* insert new line */
+ memmove(program + last_pos, badcode, offset);
+
+ if(verbose)
+ fprintf(stderr, "Inserted line 32767 with invalid offset at file offset $%04x.\n", last_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) {
+ printf("Usage: %s [-v] [-nc|-nv] [-s] [-x[r|NN]] <inputfile> <outputfile>\n", self);
+ printf(" -v: Verbose.\n");
+ printf(" -nc: Don't protect code.\n");
+ printf(" -nv: Don't protect variable names.\n");
+ printf(" -s: Shrink variable name table to min size.\n");
+ printf("-xNN: Hex code NN for variable names.\n");
+ printf(" -xr: Random variable names.\n");
+ printf("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..56fb7c1
--- /dev/null
+++ b/protbas.rst
@@ -0,0 +1,76 @@
+=======
+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.
+
+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.
+
+.. include:: genopts.rst
+
+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..c0284a5
--- /dev/null
+++ b/renumbas.1
@@ -0,0 +1,206 @@
+.\" 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-25" "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] [\fB\-b\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 \fIGOTO\fP
+\fI100\fP (or \fIGOSUB\fP, \fIRESTORE\fP, \fITRAP\fP, etc) will be updated with the new line
+number.
+.sp
+Computed line numbers can\(aqt be updated (e.g. \fIGOTO A or GOSUB
+1000+A*100\fP). These will cause warnings on stderr, so you can fix them
+manually.
+.sp
+Valid line numbers (0 to 32767) that don\(aqt exist will not be changed,
+but will cause a warning. Invalid line numbers (e.g. \fITRAP 40000\fP)
+will be ignored (no change, no warning).
+.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.
+.sp
+Atari BASIC allows fractional line numbers, such as \fIGOTO 123.4\fP\&.
+These are rounded to the nearest integer when the program is
+executed. \fBrenumbas\fP handles these correctly, although you\(aqre
+not likely to run into them in real\-world programs.
+.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 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.
+.TP
+.B \fB\-b\fP
+Renumber program backwards (line numbers in descending
+order). This option is completely useless, but exists for testing
+purposes. Programs renumbered this way won\(aqt \fIRUN\fP correctly,
+although they will \fILOAD\fP and \fILIST\fP\&. When using this option, set
+\fB\-s\fP to a higher number than the default.
+.UNINDENT
+.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 DIAGNOSTICS
+.INDENT 0.0
+.TP
+.B Fatal: Program is code\-protected; unprotect it first.
+Use \fBunprotbas\fP to remove the protection, if you get this error.
+.TP
+.B Fatal: New line number \fInum\fP would be >32767.
+32767 is the highest line number BASIC allows.
+Use a lower starting line (\fB\-s\fP) and/or increment (\fB\-i\fP).
+.TP
+.B Warning: Computed \fIcmd\fP at line \fInum\fP\&.
+The line number for a \fIGOTO\fP, \fIGOSUB\fP, etc is normally adjusted to
+whatever the line got renumbered to. This warning means that \fBrenumbas\fP
+couldn\(aqt adjust the line number, because it\(aqs a computed value such as
+\fIGOTO A+100\fP\&. You\(aqll have to fix this manually. \fInum\fP is the original
+line number, not the renumbered one.
+.TP
+.B Warning: Line \fInum1\fP references nonexistent line \fInum2\fP\&.
+Usually indicates a bug in the BASIC program. Example: \fIGOTO 100\fP,
+but there is no line 100. \fInum1\fP is the original line number, not the
+renumbered one.
+.TP
+.B Renumbering line \fInum1\fP as \fInum2\fP (\fInum3\fP refs).
+Only seen when the \fB\-v\fP (verbose) option is used. Just an informational
+message.
+.UNINDENT
+.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 LIMITATIONS
+.sp
+A pathological case:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+100 GOTO 200+0
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+200+0 is considered a computed line number, even though the results of
+the computation are constant. This is because neither Atari BASIC nor
+\fBrenumbas\fP does constant folding.
+.sp
+This shouldn\(aqt be a real\-world problem; did \fIyou\fP ever write code like
+that in Atari BASIC?
+.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),
+\fBcxrefbas\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBlistbas\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..6f4f51e
--- /dev/null
+++ b/renumbas.c
@@ -0,0 +1,141 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+#include "bcdfp.h"
+#include "linetab.h"
+
+unsigned short startlineno = 10;
+unsigned short increment = 10;
+unsigned short limit = 0;
+unsigned short newno;
+int backwards = 0;
+
+void print_help(void) {
+ printf("Usage: %s [-v] [-s start-lineno] [-i increment] [-f first-lineno] <inputfile> <outputfile>\n", self);
+ printf(" -v: Verbose.\n");
+ printf(" -s <num>: Starting line number (default: 10).\n");
+ printf(" -i <num>: Increment (default: 10).\n");
+ printf(" -f <num>: Don't renumber lines less than <num> (default: 0).\n");
+ printf(" -b: Number backwards (creates invalid program).\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, "vbs:i:f:")) != -1) {
+ switch(opt) {
+ case 'v': verbose = 1; break;
+ case 'b': backwards = 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];
+}
+
+CALLBACK(renumber_line) {
+ int i;
+ unsigned char fpnewno[6];
+
+ if(lineno < limit || lineno == 32768) return;
+ if(newno >= 32768) {
+ fprintf(stderr, "%s: Fatal: New line number %d would be >32767.\n", self, newno);
+ exit(1);
+ }
+
+ if(verbose)
+ fprintf(stderr, "Renumbering line %d as %d (%d refs).\n", lineno, newno, refcounts[lineno]);
+
+ int2fp(newno, fpnewno);
+ for(i = 0; i < refcounts[lineno]; i++)
+ memmove(program + linerefs[lineno][i].pos, fpnewno, 6);
+ setword(pos, newno);
+
+ if(backwards) {
+ if(newno < increment) {
+ fprintf(stderr, "%s: Fatal: New line number %d would be <0.\n", self, newno);
+ exit(1);
+ } else {
+ newno -= increment;
+ }
+ } else {
+ newno += increment;
+ }
+}
+
+void check_refs(void) {
+ int i, j;
+
+ for(i = 0; i < 32768; i++) {
+ if(refcounts[i] && !lines_exist[i]) {
+ for(j = 0; j < refcounts[i]; j++) {
+ fprintf(stderr, "%s: Warning: Line %d references nonexistent line %d.\n",
+ self, linerefs[i][j].lineno, i);
+ }
+ }
+ }
+}
+
+void renumber(void) {
+ check_refs();
+ newno = startlineno;
+ on_start_line = renumber_line;
+ walk_all_code();
+}
+
+int main(int argc, char **argv) {
+ set_self(*argv);
+ parse_general_args(argc, argv, print_help);
+ parse_args(argc, argv);
+
+ readfile();
+ parse_header();
+
+ build_ref_table();
+ renumber();
+ free_ref_table();
+
+ open_output(output_filename);
+ writefile();
+
+ return 0;
+}
diff --git a/renumbas.rst b/renumbas.rst
new file mode 100644
index 0000000..a442ccb
--- /dev/null
+++ b/renumbas.rst
@@ -0,0 +1,133 @@
+========
+renumbas
+========
+
+-----------------------------------
+Renumber Atari 8-bit BASIC programs
+-----------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+renumbas [**-v**] [**-s** *start-lineno*] [**-i** *increment*] [**-f** *first-lineno*] [**-b**] *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 cause warnings on stderr, so you can fix them
+manually.
+
+Valid line numbers (0 to 32767) that don't exist will not be changed,
+but will cause a warning. Invalid line numbers (e.g. *TRAP 40000*)
+will be ignored (no change, no warning).
+
+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.
+
+Atari BASIC allows fractional line numbers, such as *GOTO 123.4*.
+These are rounded to the nearest integer when the program is
+executed. **renumbas** handles these correctly, although you're
+not likely to run into them in real-world programs.
+
+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.
+
+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.
+
+**-b**
+ Renumber program backwards (line numbers in descending
+ order). This option is completely useless, but exists for testing
+ purposes. Programs renumbered this way won't *RUN* correctly,
+ although they will *LOAD* and *LIST*. When using this option, set
+ **-s** to a higher number than the default.
+
+.. include:: genopts.rst
+
+DIAGNOSTICS
+===========
+
+Fatal: Program is code-protected; unprotect it first.
+ Use **unprotbas** to remove the protection, if you get this error.
+
+Fatal: New line number *num* would be >32767.
+ 32767 is the highest line number BASIC allows.
+ Use a lower starting line (**-s**) and/or increment (**-i**).
+
+Warning: Computed *cmd* at line *num*.
+ The line number for a *GOTO*, *GOSUB*, etc is normally adjusted to
+ whatever the line got renumbered to. This warning means that **renumbas**
+ couldn't adjust the line number, because it's a computed value such as
+ *GOTO A+100*. You'll have to fix this manually. *num* is the original
+ line number, not the renumbered one.
+
+Warning: Line *num1* references nonexistent line *num2*.
+ Usually indicates a bug in the BASIC program. Example: *GOTO 100*,
+ but there is no line 100. *num1* is the original line number, not the
+ renumbered one.
+
+Renumbering line *num1* as *num2* (*num3* refs).
+ Only seen when the **-v** (verbose) option is used. Just an informational
+ message.
+
+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).
+
+LIMITATIONS
+===========
+
+A pathological case::
+
+ 100 GOTO 200+0
+
+200+0 is considered a computed line number, even though the results of
+the computation are constant. This is because neither Atari BASIC nor
+**renumbas** does constant folding.
+
+This shouldn't be a real-world problem; did *you* ever write code like
+that in Atari BASIC?
+
+EXIT STATUS
+===========
+
+0 for success, 1 for failure.
+
+.. include:: manftr.rst
diff --git a/rom2cart.1 b/rom2cart.1
index a3dfcb9..bf5551d 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-25" "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:
@@ -247,11 +247,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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/tokens.c b/tokens.c
new file mode 100644
index 0000000..08cf48a
--- /dev/null
+++ b/tokens.c
@@ -0,0 +1,135 @@
+const char *commands[] = {
+ "REM",
+ "DATA",
+ "INPUT",
+ "COLOR",
+ "LIST",
+ "ENTER",
+ "LET",
+ "IF",
+ "FOR",
+ "NEXT",
+ "GOTO",
+ "GO TO",
+ "GOSUB",
+ "TRAP",
+ "BYE",
+ "CONT",
+ "COM",
+ "CLOSE",
+ "CLR",
+ "DEG",
+ "DIM",
+ "END",
+ "NEW",
+ "OPEN",
+ "LOAD",
+ "SAVE",
+ "STATUS",
+ "NOTE",
+ "POINT",
+ "XIO",
+ "ON",
+ "POKE",
+ "PRINT",
+ "RAD",
+ "READ",
+ "RESTORE",
+ "RETURN",
+ "RUN",
+ "STOP",
+ "POP",
+ "?",
+ "GET",
+ "PUT",
+ "GRAPHICS",
+ "PLOT",
+ "POSITION",
+ "DOS",
+ "DRAWTO",
+ "SETCOLOR",
+ "LOCATE",
+ "SOUND",
+ "LPRINT",
+ "CSAVE",
+ "CLOAD",
+ "", /* implied LET */
+ "ERROR -"
+};
+
+const unsigned short last_command = (sizeof(commands) / sizeof(char *)) - 1;
+
+const char *operators[] = {
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0,
+ ",",
+ "$",
+ ":",
+ ";",
+ "", /* $16, EOL */
+ " GOTO ",
+ " GOSUB ", /* $18 */
+ " TO ",
+ " STEP ",
+ " THEN ",
+ "#",
+ "<=",
+ "<>",
+ ">=",
+ "<", /* $20 */
+ ">",
+ "=",
+ "^",
+ "*",
+ "+",
+ "-",
+ "/",
+ " NOT ", /* $28 */
+ " OR ",
+ " AND ",
+ "(",
+ ")",
+ "=",
+ "=",
+ "<=",
+ "<>", /* $30 */
+ ">=",
+ "<",
+ ">",
+ "=",
+ "+",
+ "-",
+ "(",
+ "", /* $38, redunant for arrays */
+ "", /* $39, ditto */
+ "(",
+ "(",
+ ",",
+ "STR$",
+ "CHR$",
+ "USR",
+ "ASC", /* $40 */
+ "VAL",
+ "LEN",
+ "ADR",
+ "ATN",
+ "COS",
+ "PEEK",
+ "SIN",
+ "RND", /* $48 */
+ "FRE",
+ "EXP",
+ "LOG",
+ "CLOG",
+ "SQR",
+ "SGN",
+ "ABS",
+ "INT", /* $50 */
+ "PADDLE",
+ "STICK",
+ "PTRIG",
+ "STRIG" /* $54 */
+};
+
+const unsigned short last_operator = (sizeof(operators) / sizeof(char *)) - 1;
diff --git a/tokens.h b/tokens.h
new file mode 100644
index 0000000..265122e
--- /dev/null
+++ b/tokens.h
@@ -0,0 +1,4 @@
+extern const char *commands[];
+extern const char *operators[];
+extern const unsigned short last_command;
+extern const unsigned short last_operator;
diff --git a/unmac65.1 b/unmac65.1
index 8e17857..64dd41d 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-25" "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:
@@ -379,11 +379,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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.xex b/unmac65.xex
new file mode 100644
index 0000000..987667b
--- /dev/null
+++ b/unmac65.xex
Binary files differ
diff --git a/unprotbas.1 b/unprotbas.1
index 2b389ac..b6655e7 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-25" "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,14 +50,16 @@ 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 Unprotection Options
.INDENT 0.0
.TP
-.B \fB\-v\fP
-Verbose operation.
-.TP
.B \fB\-f\fP
Force the variable name table to be rebuilt, even if it looks OK.
This option cannot be combined with \fB\-n\fP\&.
@@ -73,6 +76,31 @@ 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
+.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 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.
@@ -211,11 +346,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..ae01b50 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,16 +88,16 @@ 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);
- offset += fixline(pos);
+ if(verbose) fprintf(stderr, "Found invalid offset %d (<6) at line %d, file offset $%04x.\n", offset, lineno, pos);
+ offset = fixline(pos);
result++;
}
pos += offset;
@@ -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) {
+ printf("Usage: %s [-v] [-f] [-n] [-g] [-c] [-r|-w] <inputfile> <outputfile>\n", self);
+ printf(" -v: Verbose.\n");
+ printf(" -f: Force variable name table rebuild.\n");
+ printf(" -n: Do not rebuild variable name table, even if it's invalid.\n");
+ printf(" -g: Remove trailing garbage, if present.\n");
+ printf(" -c: Check only; no output file.\n");
+ printf(" -w: Write variable names to 'varnames.txt'.\n");
+ printf(" -r: Read variable names from 'varnames.txt'.\n");
+ printf("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..21bbdb1 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,16 @@ 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.
-**-v**
- Verbose operation.
-
+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 +60,20 @@ 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.
+
+.. include:: genopts.rst
+
EXIT STATUS
===========
@@ -114,7 +132,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 +142,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 +162,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 +216,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..2038878
--- /dev/null
+++ b/vxrefbas.1
@@ -0,0 +1,147 @@
+.\" 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-25" "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.
+Multiple references on the same line of code are not counted
+separately, so this is a count of \fIlines\fP that reference the variable.
+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.
+.TP
+.B \fBG\fP
+Variable was read with \fIGET\fP on this line.
+.TP
+.B \fBO\fP
+Variable was set by \fINOTE\fP on this line. Sorry, this can\(aqt be \fIN\fP, it\(aqs
+already used for \fINEXT\fP\&.
+.TP
+.B \fBL\fP
+Variable was set by \fILOCATE\fP on this line.
+.UNINDENT
+.sp
+The last line of output shows the total number of variables and the
+number of unreferenced variables.
+.SH OPTIONS
+.sp
+There are no application\-specific 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 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),
+\fBcxrefbas\fP(1),
+\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
+\fBf2toxex\fP(1),
+\fBfenders\fP(1),
+\fBlistbas\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..1c4e1ab
--- /dev/null
+++ b/vxrefbas.c
@@ -0,0 +1,167 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "bas.h"
+
+int A, F, N, D, I, R, G, O, L;
+int target_var, lastline;
+unsigned char last_cmd = 0;
+int refcounts[128];
+
+void print_help(void) {
+ printf("Usage: %s [-v] program.bas\n", self);
+ exit(0);
+}
+
+CALLBACK(new_line) {
+ A = F = N = D = I = R = G = O = L = 0;
+}
+
+CALLBACK(end_line) {
+ if(lastline != lineno) return;
+
+ if(A || F || N || D || I || R || G || O || L) {
+ putchar('=');
+ if(A) putchar('A');
+ if(F) putchar('F');
+ if(N) putchar('N');
+ if(D) putchar('D');
+ if(I) putchar('I');
+ if(R) putchar('R');
+ if(G) putchar('G');
+ if(O) putchar('O');
+ if(L) putchar('L');
+ }
+
+ putchar(' ');
+}
+
+CALLBACK(new_command) {
+ last_cmd = tok;
+}
+
+CALLBACK(end_stmt) {
+ last_cmd = 0;
+}
+
+CALLBACK(handle_var) {
+ unsigned char last_tok, next_tok;
+ int was_cmd, was_comma, was_semicolon;
+
+ if(tok != (target_var | 0x80)) return;
+
+ if(lastline != lineno) {
+ printf("%d", lineno);
+ refcounts[target_var]++;
+ }
+
+ lastline = lineno;
+
+ last_tok = program[pos - 1];
+ next_tok = program[pos + 1];
+ was_cmd = (last_tok == last_cmd);
+ was_comma = (last_tok == OP_COMMA);
+ was_semicolon = (last_tok == OP_SEMICOLON);
+
+ switch(last_cmd) {
+ case CMD_LET:
+ case CMD_ILET:
+ if(was_cmd) A = 1;
+ break;
+ case CMD_FOR:
+ if(was_cmd) F = 1;
+ break;
+ case CMD_NEXT:
+ if(was_cmd) N = 1;
+ break;
+ case CMD_DIM:
+ if(was_cmd || was_comma) D = 1;
+ break;
+ case CMD_INPUT: /* INPUT #1;A and INPUT #1,A are both allowed, grr. */
+ if(was_cmd || was_comma || was_semicolon) I = 1;
+ break;
+ case CMD_READ:
+ if(was_cmd || was_comma) R = 1;
+ break;
+ case CMD_GET:
+ if(was_comma) G = 1;
+ break;
+ case CMD_NOTE:
+ if(was_comma) O = 1;
+ break;
+ case CMD_LOCATE:
+ if(next_tok == OP_EOS || next_tok == OP_EOL) L = 1;
+ break;
+ }
+}
+
+void parse_args(int argc, char **argv) {
+ int opt;
+
+ while( (opt = getopt(argc, argv, "v")) != -1) {
+ switch(opt) {
+ case 'v': verbose = 1; break;
+ default: print_help(); exit(1);
+ }
+ }
+
+ if(optind >= argc)
+ die("No input file given (use - for stdin).");
+ else
+ open_input(argv[optind]);
+}
+
+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);
+ parse_args(argc, argv);
+
+ readfile();
+ parse_header();
+
+ if(!vntable_ok())
+ die("Program is variable-protected; unprotect it first.");
+
+ 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;
+
+ /* walk the variable value table */
+ 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');
+
+ /* ignore any ERROR-4 vars, since they don't have tokens anyway. */
+ 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..d77e6c5
--- /dev/null
+++ b/vxrefbas.rst
@@ -0,0 +1,80 @@
+========
+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.
+Multiple references on the same line of code are not counted
+separately, so this is a count of *lines* that reference the variable.
+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.
+
+**G**
+ Variable was read with *GET* on this line.
+
+**O**
+ Variable was set by *NOTE* on this line. Sorry, this can't be *N*, it's
+ already used for *NEXT*.
+
+**L**
+ Variable was set by *LOCATE* on this line.
+
+The last line of output shows the total number of variables and the
+number of unreferenced variables.
+
+OPTIONS
+=======
+
+There are no application-specific options.
+
+.. include:: genopts.rst
+
+EXIT STATUS
+===========
+
+0 for success, 1 for failure.
+
+.. include:: manftr.rst
diff --git a/xex.5 b/xex.5
index 82774fe..8400444 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-25" "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:
@@ -306,11 +306,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..7ad599b 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-25" "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:
@@ -80,11 +80,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..56fbc2b 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-25" "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:
@@ -138,11 +138,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..9925c07 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-25" "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:
@@ -198,11 +198,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..35727b1 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-25" "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:
@@ -191,11 +191,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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..3ca34ea 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-25" "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:
@@ -119,11 +119,18 @@ Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\
\fBblob2c\fP(1),
\fBblob2xex\fP(1),
\fBcart2xex\fP(1),
+\fBcxrefbas\fP(1),
\fBdasm2atasm\fP(1),
+\fBdumpbas\fP(1),
\fBf2toxex\fP(1),
\fBfenders\fP(1),
+\fBlistbas\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),