aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Watson <urchlay@slackware.uk>2022-08-29 16:11:13 -0400
committerB. Watson <urchlay@slackware.uk>2022-08-29 16:11:13 -0400
commite2ba8458a5cfdfacfaf103e7ba97d610afa6c970 (patch)
treecd665e602e6e2b636578a7d3d7894380605dafcc
downloadbw-atari8-tools-e2ba8458a5cfdfacfaf103e7ba97d610afa6c970.tar.gz
initial commit
-rw-r--r--.syms0
-rw-r--r--Makefile166
-rw-r--r--README79
-rw-r--r--TODO19
-rw-r--r--a8eol.1482
-rw-r--r--a8eol.c567
-rw-r--r--a8eol.rst196
-rwxr-xr-xa8utf8163
-rw-r--r--a8utf8.1108
-rw-r--r--a8utf8.rst52
-rw-r--r--asmwrapper.sh35
-rw-r--r--atr2xfd.1201
-rw-r--r--atr2xfd.c244
-rw-r--r--atr2xfd.rst109
-rw-r--r--atrsize.1216
-rw-r--r--atrsize.c308
-rw-r--r--atrsize.rst92
-rw-r--r--axe.1166
-rw-r--r--axe.c197
-rw-r--r--axe.h52
-rw-r--r--axe.rst114
-rw-r--r--axelib.c546
-rw-r--r--blob2c.1137
-rw-r--r--blob2c.c117
-rw-r--r--blob2c.rst85
-rw-r--r--cart.c261
-rw-r--r--cart.h55
-rw-r--r--cart2xex.1247
-rw-r--r--cart2xex.c583
-rw-r--r--cart2xex.rst203
-rwxr-xr-xdasm2atasm275
-rw-r--r--dasm2atasm.1244
-rw-r--r--dasm2atasm.rst162
-rw-r--r--equates.inc1384
-rw-r--r--fenders.1282
-rw-r--r--fenders.binbin0 -> 384 bytes
-rw-r--r--fenders.c418
-rw-r--r--fenders.dasm570
-rw-r--r--fenders.rst239
-rw-r--r--fenders_bin.c54
-rw-r--r--fenders_bin.h9
-rw-r--r--fenders_offsets.h5
-rw-r--r--fenders_offsets.pl6
-rw-r--r--fendersdbl.binbin0 -> 640 bytes
-rw-r--r--fendersdbl.dasm411
-rw-r--r--fendersdbl_bin.c86
-rw-r--r--fendersdbl_bin.h9
-rw-r--r--fendersdbl_offsets.h5
-rw-r--r--get_address.c22
-rw-r--r--get_address.h2
-rw-r--r--loadscreen.binbin0 -> 222 bytes
-rw-r--r--loadscreen.dasm153
-rw-r--r--loadscreen_bin.c34
-rw-r--r--loadscreen_bin.h9
-rw-r--r--manftr.rst30
-rw-r--r--manhdr.rst7
-rw-r--r--rom2cart.1244
-rw-r--r--rom2cart.c641
-rw-r--r--rom2cart.rst202
-rw-r--r--rstman.rst58
-rw-r--r--test22
-rw-r--r--testdata1
-rw-r--r--testdata.orig1
-rw-r--r--unmac65.1392
-rw-r--r--unmac65.c1041
-rw-r--r--unmac65.rst324
-rw-r--r--ver.rst1
-rw-r--r--xex.c313
-rw-r--r--xex.h117
-rw-r--r--xexcat.1181
-rw-r--r--xexcat.c248
-rw-r--r--xexcat.rst132
-rw-r--r--xexsplit.1195
-rw-r--r--xexsplit.c123
-rw-r--r--xexsplit.rst128
-rw-r--r--xextest.c87
-rw-r--r--xfd2atr.1132
-rw-r--r--xfd2atr.c227
-rw-r--r--xfd2atr.rst82
79 files changed, 15088 insertions, 0 deletions
diff --git a/.syms b/.syms
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.syms
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..82b4afc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,166 @@
+
+# Install paths. DESTDIR is used for installing to an alternate location,
+# for people making RPM/deb/tgz/etc packages.
+DESTDIR=
+PREFIX=/usr/local
+BINDIR=$(PREFIX)/bin
+MANDIR=$(PREFIX)/share/man
+MAN1DIR=$(MANDIR)/man1
+DOCDIR=$(PREFIX)/share/doc/bw-atari8-tools
+
+# Compiler stuff
+CC=gcc
+CFLAGS=-Wall -O2 -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
+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
+DOCS=README equates.inc *.dasm
+
+# All the programs share this version number...
+VERSION=0.2.1
+
+# If your system doesn't support gzipped man pages, comment this out:
+GZIP_MAN=y
+
+# unmac65 can be built for Atari 8-bit. Don't do it by default, but
+# these variables are used for cross-compiling:
+CC65=cl65
+CC65FLAGS=-O -t atari
+
+# Some distros have this with a .py extension, some don't. Only needed
+# if you're rebuilding the man pages (users shouldn't have to).
+# RST2MAN=rst2man.py
+RST2MAN=rst2man
+
+# Targets below. You probably don't need to edit below this point.
+# WARNING: Don't do a "make realclean" unless you have the DASM or
+# Atasm 6502 cross assembler installed!
+# "make clean" and "make distclean" will not delete the 6502 object
+# code (the *.bin files), but "make realclean" will.
+
+all: $(BINS) manpages
+
+a8eol: a8eol.c
+ $(CC) $(CFLAGS) -o a8eol a8eol.c
+
+xfd2atr: xfd2atr.c
+ $(CC) $(CFLAGS) -o xfd2atr xfd2atr.c
+
+atr2xfd: atr2xfd.c
+ $(CC) $(CFLAGS) -o atr2xfd atr2xfd.c
+
+# note to cross-compiler users: If you're building the *.bin targets,
+# blob2c needs to be executable on the build host. It'd also be nice
+# to build a blob2c for the target platform... Probably you can do
+# something like this:
+
+# make blob2c CC=/usr/bin/cc # build host blob2c
+# make CC=/path/to/cross/cc # build everything else (uses blob2c)
+# rm -f blob2c # get rid of host blob2c so we can...
+# make blob2c CC=/path/to/cross/cc # build the target system's blob2c
+# make install DESTDIR=/tmp/whatever...
+
+# Note that this is only needed if you're building the 6502 object code,
+# which you don't need to do unless you've modified it (the distribution
+# tarball comes with prebuilt *.bin files).
+
+blob2c: blob2c.c
+ $(CC) $(CFLAGS) -o blob2c blob2c.c
+
+fenders.bin: fenders.dasm asmwrapper.sh
+ sh asmwrapper.sh fenders
+
+fenders_bin.c: fenders.bin blob2c
+ ./blob2c fenders.bin > fenders_bin.c 2>fenders_bin.h
+
+fenders_offsets.h: fenders.bin fenders_offsets.pl
+ perl fenders_offsets.pl < fenders.syms > fenders_offsets.h
+
+fenders: fenders.c fenders_bin.c fenders_bin.h fenders_offsets.h \
+ fendersdbl_bin.c fendersdbl_bin.h fendersdbl_offsets.h
+ $(CC) $(CFLAGS) -o fenders fenders.c fenders_bin.c fendersdbl_bin.c
+
+fendersdbl.bin: fendersdbl.dasm asmwrapper.sh
+ sh asmwrapper.sh fendersdbl
+
+fendersdbl_bin.c: fendersdbl.bin blob2c
+ ./blob2c fendersdbl.bin > fendersdbl_bin.c 2>fendersdbl_bin.h
+
+fendersdbl_offsets.h: fendersdbl.bin fenders_offsets.pl
+ perl fenders_offsets.pl < fendersdbl.syms > fendersdbl_offsets.h
+
+loadscreen.bin: loadscreen.dasm asmwrapper.sh
+ sh asmwrapper.sh loadscreen
+
+loadscreen_bin.c: loadscreen.bin blob2c
+ ./blob2c loadscreen.bin > loadscreen_bin.c 2>loadscreen_bin.h
+
+cart2xex: cart2xex.c loadscreen_bin.c get_address.o cart.o
+ $(CC) $(CFLAGS) -o cart2xex cart2xex.c loadscreen_bin.c get_address.o cart.o
+
+rom2cart: rom2cart.c cart.o
+ $(CC) $(CFLAGS) -o rom2cart rom2cart.c cart.o
+
+cart.o: cart.c cart.h
+ $(CC) $(CFLAGS) -c cart.c
+
+get_address.o: get_address.c get_address.h
+ $(CC) $(CFLAGS) -c get_address.c
+
+xex.o: xex.c xex.h
+ $(CC) $(CFLAGS) -c xex.c
+
+xextest: xextest.c xex.o
+ $(CC) $(CFLAGS) -o xextest xextest.c xex.o
+
+xexsplit: xexsplit.c xex.o
+ $(CC) $(CFLAGS) -o xexsplit xexsplit.c xex.o
+
+xexcat: xexcat.c xex.o get_address.o
+ $(CC) $(CFLAGS) -o xexcat xexcat.c xex.o get_address.o
+
+unmac65.xex: unmac65.c
+ @rm -f unmac65.o
+ $(CC65) $(CC65FLAGS) -DVERSION=\"$(VERSION)\" -DTAG=\"$(TAG)\" -t atari -o unmac65.xex unmac65.c
+ @rm -f unmac65.o
+
+axe: axe.c axe.h axelib.c
+
+manpages: $(MANS)
+
+%.1: %.rst
+ $(RST2MAN) $< > $@
+
+# "make clean" does NOT remove the .bin or _bin.[ch] files. This is
+# for people who don't have either dasm or atasm installed.
+# also, it doesn't remove the man pages. these are checked into git, even.
+clean:
+ rm -f core *.o *~ $(BINS)
+
+distclean: clean
+ rm -rf *.syms *.atr 1 2 3 *.xex *.rom *.atasm *.m65 atrcheck cart2rom
+
+realclean: distclean
+ rm -f *.bin *_bin.[ch] *_offsets.h *.1
+
+install: all
+ mkdir -p $(DESTDIR)/$(BINDIR) $(DESTDIR)/$(MAN1DIR) $(DESTDIR)/$(DOCDIR)
+ strip $(BINS)
+ for i in $(BINS) $(SCRIPTS) ; do \
+ install -m0755 -oroot -groot $$i $(DESTDIR)/$(BINDIR) ; \
+ install -m0644 -oroot -groot $$i.1 $(DESTDIR)/$(MAN1DIR) ; \
+ if [ "$(GZIP_MAN)" = "y" ]; then \
+ gzip $(DESTDIR)/$(MAN1DIR)/$$i.1 ; \
+ fi ; \
+ done
+ ( cd $(DESTDIR)/$(BINDIR) && rm -f atrcheck && ln -s atr2xfd atrcheck )
+ ( cd $(DESTDIR)/$(BINDIR) && rm -f cart2rom && ln -s rom2cart cart2rom )
+ if [ "$(GZIP_MAN)" = "y" ]; then \
+ cd $(DESTDIR)/$(MAN1DIR) && rm -f atrcheck.1.gz && ln -s atr2xfd.1.gz atrcheck.1.gz ; \
+ cd $(DESTDIR)/$(MAN1DIR) && rm -f cart2rom.1.gz && ln -s rom2cart.1.gz cart2rom.1.gz ; \
+ else \
+ cd $(DESTDIR)/$(MAN1DIR) && rm -f cart2rom.1 && ln -s rom2cart.1 cart2rom.1 ; \
+ fi
+ install -m0644 -oroot -groot $(DOCS) $(DESTDIR)/$(DOCDIR)
diff --git a/README b/README
new file mode 100644
index 0000000..e993246
--- /dev/null
+++ b/README
@@ -0,0 +1,79 @@
+This is a collection of Atari 8-bit related utilities I've written for Linux.
+They should be usable as-is on other UNIX-like systems, including Cygwin
+for MS-Windows.
+
+a8eol - Convert Atari 8-bit text files to/from UNIX / DOS / Mac Classic
+ text file format.
+
+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.
+
+atrsize - Change the size of an Atari 8-bit ATR disk image, or create
+ a blank ATR image.
+
+blob2c - Create C source and header files from a binary file
+
+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.
+
+dasm2atasm - Convert 6502 assembly in DASM syntax to ATASM (or MAC/65) format.
+
+fenders - Install Fenders 3-sector loader in boot sectors of an ATR image.
+
+rom2cart - Convert a raw Atari 8-bit cartridge ROM image to a CART
+ image for use with eumlators such as Atari800.
+
+unmac65 - Detokenize Atari 8-bit Mac/65 SAVEd files.
+
+xexcat - Concatenate Atari 8-bit executables (XEX) into a single XEX file.
+
+xexsplit - Split a multi-segment Atari 8-bit executable (XEX) into
+ multiple single-segment files.
+
+xfd2atr - Convert an Atari 8-bit XFD (raw) disk image to an ATR image.
+
+All are written in C, except a8utf8 and dasm2atasm which are written
+in Perl. All utilities have man pages.
+
+Also included is "equates.inc", a 6502 assembly header file that defines
+the Atari 8-bit system equates. It's meant to be used with either the
+DASM or ATASM 6502 cross assemblers.
+
+To install, use the standard "make && make install" process. The default
+prefix for installation is /usr/local.
+
+You may use "make install PREFIX=/somewhere/else" to install somewhere
+other than /usr/local. Binaries will be installed to $PREFIX/bin, man
+pages to $PREFIX/share/man/man1, and other documentation (including
+equates.inc) to $PREFIX/share/doc/bw-atari8-tools. You also may use
+BINDIR, MANDIR, MAN1DIR, and DOCDIR to explicitly set the installation
+paths. Man pages are compressed with gzip by default. If your system
+does not support gzipped man pages, try "make install GZIP_MAN=n". If
+you're creating a distribution package (RPM, deb, Slackware tgz),
+use "make install PREFIX=/usr DESTDIR=/tmp/whatever". This will
+build everything for use in /usr, but actually install everything to
+/tmp/whatever/usr, which can then be archived in whichever package
+format you're using.
+
+blob2c is not actually Atari-specific: it could be useful for any project
+where the contents of a file need to be compiled as an unsigned char
+array in a C program.
+
+dasm2atasm is not Atari-specific, since the DASM and ATASM cross
+assemblers can be used to develop code for any 6502-based platform
+(though ATASM does have some nice extra features for the Atari 8-bit).
+DASM supports several other CPUs besides the 6502, but dasm2atasm only
+works with 6502 code.
+
+The latest version of bw-atari8-tools can always be found at
+https://slackware.uk/~urchlay/repos/bw-atari8-tools
+
+-- B. Watson <urchlay@slackware.uk>; Urchlay on irc.libera.chat ##atari.
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..b40a732
--- /dev/null
+++ b/TODO
@@ -0,0 +1,19 @@
+for now:
+- convert man pages to RST (done)
+- update email, website, etc in all docs (done?)
+- update list of cart types in cart.c (done)
+- document (and possibly enhance) a8utf8 (done)
+- rename repo to bw-atari8-tools
+- MANDIR => MAN1DIR in Makefile (done)
+- clean up compiler warnings (mostly done, axe is still a mess)
+- merge axe and unmac65 repos into this one (done).
+- change license to WTFPL (done).
+- fix axe, make it stop allocating an extra empty sector.
+
+later:
+- add cassio?
+- file magic? (finish it, see if file upstream wants it)
+- new tool: a8grep. knows about xex files (but works on others), can
+ search for screen codes or inverse video, etc. also a8strings.
+- new tool: something like ataricom or binload, but show checksums
+ of segments (so we can compare different versions of the same game).
diff --git a/a8eol.1 b/a8eol.1
new file mode 100644
index 0000000..daf4993
--- /dev/null
+++ b/a8eol.1
@@ -0,0 +1,482 @@
+.\" 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 "A8EOL" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py a8eol.rst > a8eol.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fBa8eol\fP [\fI\-admu8tcpsxih\fP] [\fIinfile\fP] [\fIoutfile\fP]
+.SH DESCRIPTION
+.sp
+\fBa8eol\fP converts between ATASCII and UNIX, MS\-DOS/Windows, and
+Mac text file formats. It can auto\-detect the input file format
+and set the output format accordingly, or the user can explicitly
+set the input and output formats. Various options are available for
+translating non\-printing characters, including a mode similar to what
+old computer magazines used for program listings.
+.SH OPTIONS
+.SS File type options:
+.INDENT 0.0
+.TP
+.B \-a
+Input is UNIX, MS\-DOS/Windows, or MacOS < 10 text; convert to Atari (EOL=$9B)
+.TP
+.B \-d
+Input is Atari text; convert to MS\-DOS/Windows (EOL=$0A,$0D)
+.TP
+.B \-m
+Input is Atari text; convert to MacOS < 10 (EOL=$0D)
+.TP
+.B \-u
+Input is Atari text; convert to UNIX (EOL=$0A)
+.UNINDENT
+.sp
+With none of the above: input type is auto\-detected; output type is
+UNIX if input is Atari, or Atari if input is UNIX/DOS/Mac.
+.sp
+Only one file type option can be used per run of \fBa8eol\fP\&. If more than
+one is given, the last one occurring on the command line will be used.
+.SS Translation options:
+.INDENT 0.0
+.TP
+.B \-n
+Translate EOL characters only; pass anything else as\-is (including
+tabs and backspaces).
+.TP
+.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).
+.TP
+.B \-p
+Replace non\-printing characters with \fI\&.\fP (period, dot).
+.TP
+.B \-s
+Remove (strip) non\-printing characters.
+.TP
+.B \-x
+Replace non\-printing characters with \fI\ex[hex]\fP\&.
+.UNINDENT
+.sp
+With none of the above: EOL, tab, and backspace characters are
+translated; everything else is passed through as\-is.
+.sp
+Only one translation option can be used per run of \fBa8eol\fP\&. If more than
+one is given, the last one occurring on the command line will be used.
+.SS Other options:
+.INDENT 0.0
+.TP
+.B \-8
+8\-bit ASCII/ATASCII mode: Do not strip bit 7 (inverse video).
+This option may be used alone or combined with any of the
+translation options, above. Characters with bit 7 set are
+considered non\-printing. This option is always enabled when \fB\-c\fP
+is used.
+.TP
+.B \-i
+In\-place conversion. Original file renamed to \fIinfile~\fP\&. This option
+can\(aqt be used when reading from standard input.
+.TP
+.B \-q
+Quiet operation. Error messages will still be printed.
+.TP
+.B \-v
+Verbose operation. Prints extra info about what \fBa8eol\fP is doing.
+.TP
+.B \-h
+Print built\-in help message and exit.
+.UNINDENT
+.sp
+Leave \fIinfile\fP blank or use \fB\-\fP to read from standard input (in which
+case, don\(aqt use the \fB\-i\fP option).
+.sp
+Leave \fIoutfile\fP blank or use \fB\-\fP to write to standard output.
+.SH NOTES
+.sp
+Without the \fB\-8\fP option, bit 7 is stripped (cleared) for all input
+characters, \fIexcept\fP for the ATASCII EOL ($9B) character (when the
+input is an Atari file). Bit 7 stripping occurs for each input
+character \fIbefore\fP any of the translation options are applied.
+.sp
+The input type auto\-detection isn\(aqt perfect. It scans from the
+beginning of the input, looking for either an ATASCII EOL or
+an ASCII carriage return or linefeed. An Atari file with ATASCII
+graphics may be mis\-detected as an ASCII file, if it contains
+any $0A or $0D bytes before the first EOL ($0A and $0D are graphics
+characters, in ATASCII). If this happens, force Atari input with
+\fB\-d\fP, \fB\-m\fP, or \fB\-u\fP\&.
+.sp
+The auto\-detection also fails with an "Illegal seek" error when
+reading from a pipe (e.g. \fBcat file | a8eol\fP). To avoid this, either
+set the input type explicitly with one of \fB\-[admu]\fP, or read from a
+regular file (possibly a temporary one created just for this purpose).
+This is a bug (not a feature), but probably not worth the time
+it\(aqd take to fix it.
+.sp
+The \fB\-a\fP option is "magical" in that it can handle input with UNIX
+(\fI\en\fP), DOS (\fI\er\en\fP), or Mac Classic (\fI\er\fP only) line endings. In fact, it
+can handle an input file containing any combination of the three line
+ending types in the same file.
+.SH ATASCII CODES
+.sp
+When the \fI\-c\fP option is used on ATASCII input, the special Atari
+character codes are translated into human\-readable descriptive
+strings. This is similar to the way old magazines (Compute!, Antic,
+Analog) printed ATASCII codes in typeset program listings.
+.sp
+List of code translations:
+.TS
+center;
+|l|l|l|l|l|.
+_
+T{
+Dec
+T} T{
+Hex
+T} T{
+Keystroke(s)
+T} T{
+Description
+T} T{
+T}
+_
+T{
+\-\-
+T} T{
+\-\-
+T} T{
+{inv}
+T} T{
+Inverse Video (800: Atari Logo)
+T} T{
+Start a sequence of inverse video characters
+T}
+_
+T{
+\-\-
+T} T{
+\-\-
+T} T{
+{norm}
+T} T{
+Inverse Video (800: Atari Logo)
+T} T{
+End a sequence of inverse video characters (back to normal)
+T}
+_
+T{
+0
+T} T{
+00
+T} T{
+{ctrl\-,}
+T} T{
+Ctrl ,
+T} T{
+Heart; replaces ASCII NUL
+T}
+_
+T{
+27
+T} T{
+1B
+T} T{
+{esc}
+T} T{
+Esc Esc
+T} T{
+Literal Escape character
+T}
+_
+T{
+28
+T} T{
+1C
+T} T{
+{up}
+T} T{
+Esc Ctrl \-
+T} T{
+Cursor Up
+T}
+_
+T{
+29
+T} T{
+1D
+T} T{
+{down}
+T} T{
+Esc Ctrl =
+T} T{
+Cursor Down
+T}
+_
+T{
+30
+T} T{
+1E
+T} T{
+{left}
+T} T{
+Esc Ctrl +
+T} T{
+Cursor Left
+T}
+_
+T{
+31
+T} T{
+1F
+T} T{
+{right}
+T} T{
+Esc Ctrl *
+T} T{
+Cursor Right
+T}
+_
+T{
+96
+T} T{
+60
+T} T{
+{ctrl\-.}
+T} T{
+Ctrl .
+T} T{
+Diamond; replaces ASCII grave accent: \(ga
+T}
+_
+T{
+123
+T} T{
+7B
+T} T{
+{ctrl\-;}
+T} T{
+Ctrl ;
+T} T{
+Club; replaces ASCII left brace: {
+T}
+_
+T{
+125
+T} T{
+7D
+T} T{
+{clear}
+T} T{
+Esc Ctrl < or Esc Shift <
+T} T{
+Clear screen (CLR/HOME on 800); Replaces ASCII right brace: }
+T}
+_
+T{
+126
+T} T{
+7E
+T} T{
+{bksp}
+T} T{
+Esc Backspace
+T} T{
+Backspace (BACK S on 800); Replaces ASCII tilde: ~
+T}
+_
+T{
+127
+T} T{
+7F
+T} T{
+{tab}
+T} T{
+Esc Tab
+T} T{
+Tab to next tab stop; Replaces ASCII DEL: ~
+T}
+_
+T{
+155
+T} T{
+9B
+T} T{
+\-\-
+T} T{
+Enter
+T} T{
+Atari EOL (translated to \en, \er\en, or \er)
+T}
+_
+T{
+156
+T} T{
+9C
+T} T{
+{del\-line}
+T} T{
+Esc Shift BackSp
+T} T{
+Delete logical line @ cursor
+T}
+_
+T{
+157
+T} T{
+9D
+T} T{
+{ins\-line}
+T} T{
+Esc Shift >
+T} T{
+Insert blank line @ cursor
+T}
+_
+T{
+158
+T} T{
+9E
+T} T{
+{clr\-tab}
+T} T{
+Esc Ctrl Tab
+T} T{
+Clear current tab stop
+T}
+_
+T{
+159
+T} T{
+9F
+T} T{
+{set\-tab}
+T} T{
+Esc Shift Tab
+T} T{
+Set tab stop @ cursor position
+T}
+_
+T{
+253
+T} T{
+FD
+T} T{
+{bell}
+T} T{
+Esc Ctrl 2
+T} T{
+Ring bell (800: internal spkr)
+T}
+_
+T{
+254
+T} T{
+FE
+T} T{
+{del\-char}
+T} T{
+Esc Ctrl BackSp
+T} T{
+Delete character @ cursor
+T}
+_
+T{
+255
+T} T{
+FF
+T} T{
+{ins\-char}
+T} T{
+Esc Ctrl >
+T} T{
+Insert one space @ cursor
+T}
+_
+.TE
+.sp
+Other control characters are listed as \fI{ctrl\-X}\fP, where \fIX\fP is the
+keystroke to use for entering the character.
+.\" other sections we might want, uncomment as needed.
+.
+.\" FILES
+.
+.\" =====
+.
+.\" ENVIRONMENT
+.
+.\" ===========
+.
+.SH EXIT STATUS
+.sp
+0 for success, non\-zero for any error. Error messages are printed to \fBstderr\fP\&.
+.\" BUGS
+.
+.\" ====
+.
+.\" EXAMPLES
+.
+.\" ========
+.
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/a8eol.c b/a8eol.c
new file mode 100644
index 0000000..6d6b779
--- /dev/null
+++ b/a8eol.c
@@ -0,0 +1,567 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "a8eol"
+
+#define BANNER \
+ "a8eol v"VERSION" by B. Watson (WTFPL)\n"
+
+#define USAGE \
+ BANNER \
+ "Converts between Atari 8-bit and UNIX / DOS / Mac Classic text file format\n\n" \
+ "Usage: a8eol -[admu8ncpsxih] [infile] [outfile]\n\n" \
+ "File type options:\n" \
+ " -a Input is UNIX, DOS, or MacOS < 10 text; convert to Atari (EOL=$9B)\n" \
+ " -d Input is Atari text; convert to DOS (EOL=$0A,$0D)\n" \
+ " -m Input is Atari text; convert to MacOS < 10 (EOL=$0D)\n" \
+ " -u Input is Atari text; convert to UNIX (EOL=$0A)\n" \
+ "With none of the above: input type is auto-detected; output type\n" \
+ "is UNIX if input is Atari, or Atari if input is UNIX/DOS/Mac\n\n" \
+ "Translation options:\n" \
+ " -n Translate EOL characters only; pass anything else as-is\n" \
+ " -c Replace non-printing characters with ^x or {x} (turns on -8, too)\n" \
+ " -p Replace non-printing characters with '.'\n" \
+ " -s Remove non-printing characters\n" \
+ " -x Replace non-printing characters with \\x[hex]\n" \
+ "With none of the above: EOL, tab, and backspace characters are\n" \
+ "translated; everything else is passed through as-is.\n\n" \
+ "Other options:\n" \
+ " -8 8-bit ASCII/ATASCII mode: Do not strip bit 7 (inverse video).\n" \
+ " -i 'In-place' conversion. Original file renamed to infile~\n" \
+ " -q Quiet operation. Error messages will still be printed.\n" \
+ " -v Verbose operation. Prints extra info about what a8eol is doing.\n" \
+ " -h Print this help message\n\n" \
+ "Leave infile blank or use '-' to read from standard input.\n" \
+ "Leave outfile blank or use '-' to write to standard output.\n"
+
+#define OPTIONS "admu8ncpsxiqvh"
+
+#define FT_AUTO 0
+#define FT_ATARI 1
+#define FT_UNIX 2
+#define FT_DOS 3 /* input_type never gets set to this! */
+#define FT_MAC9 4 /* input_type never gets set to this! */
+
+#define TT_NONE 0
+#define TT_CARET 1
+#define TT_DOT 2
+#define TT_HEX 3
+#define TT_STRIP 4
+#define TT_TABS 5
+
+int input_type = FT_AUTO; /* FT_UNIX works for UNIX/DOS/Mac */
+int output_type = FT_AUTO; /* DOS/Mac need to be FT_DOS or FT_MAC9 */
+int trans_type = TT_TABS;
+int keep_bit_7 = 0;
+int in_place = 0;
+int verbose = 1;
+/* TODO: track bytes/lines read/written, print if verbose > 1 */
+
+static int inverse = 0;
+static char buf[50];
+static char *dot = ".";
+static char *inv = "{inv}";
+static char *norm = "{norm}";
+static char *empty = "";
+static char *crlf = "\r\n";
+static char *cr = "\r";
+static char *lf = "\n";
+static char eol[2] = { 0x9b, '\0' };
+
+/* FIXME: ata2asc() and asc2ata() are crap code. */
+
+char *ata2asc(int c) {
+ char *modifier = empty;
+ static char result[50];
+ int affects_inv = 1;
+ char c7 = c & 0x7f;
+
+ if(c == 0x9b) {
+ switch(output_type) {
+ case FT_DOS:
+ return crlf;
+
+ case FT_MAC9:
+ return cr;
+
+ case FT_UNIX:
+ default:
+ return lf;
+ }
+ }
+
+ if(!keep_bit_7)
+ c &= 0x7f;
+
+ if(trans_type != TT_CARET && (c == '|' || (c >= 32 && c <= 122))) {
+ buf[0] = c;
+ buf[1] = '\0';
+ return buf;
+ }
+
+ if(trans_type == TT_DOT) {
+ return dot;
+ } else if(trans_type == TT_STRIP) {
+ return empty;
+ } else if(trans_type == TT_HEX) {
+ sprintf(buf, "\\x%02X", c);
+ return buf;
+ } else if(trans_type == TT_TABS) {
+ if(c == 127) {
+ buf[0] = '\t';
+ buf[1] = '\0';
+ return buf;
+ } else if(c == 126) {
+ buf[0] = '\b';
+ buf[1] = '\0';
+ return buf;
+ } else {
+ buf[0] = c;
+ buf[1] = '\0';
+ return buf;
+ }
+ }
+
+ if(c7 == '|' || (c7 >= 32 && c7 <= 122 && c7 != 96)) {
+ buf[0] = c7;
+ buf[1] = '\0';
+ } else if(c7 == 0) {
+ sprintf(buf, "{ctrl-,}");
+ } else if(c == 27) {
+ sprintf(buf, "{esc}");
+ affects_inv = 0;
+ } else if(c == 28) {
+ sprintf(buf, "{up}");
+ affects_inv = 0;
+ } else if(c == 29) {
+ sprintf(buf, "{down}");
+ affects_inv = 0;
+ } else if(c == 30) {
+ sprintf(buf, "{left}");
+ affects_inv = 0;
+ } else if(c == 31) {
+ sprintf(buf, "{right}");
+ affects_inv = 0;
+ } else if(c7 == 96) {
+ sprintf(buf, "{ctrl-.}");
+ } else if(c7 == 123) {
+ sprintf(buf, "{ctrl-;}");
+ } else if(c == 125) {
+ sprintf(buf, "{clear}");
+ affects_inv = 0;
+ } else if(c == 126) {
+ sprintf(buf, "{bksp}");
+ affects_inv = 0;
+ } else if(c == 127) {
+ sprintf(buf, "{tab}");
+ affects_inv = 0;
+ } else if(c == 156) {
+ sprintf(buf, "{del-line}");
+ affects_inv = 0;
+ } else if(c == 157) {
+ sprintf(buf, "{ins-line}");
+ affects_inv = 0;
+ } else if(c == 158) {
+ sprintf(buf, "{clr-tab}");
+ affects_inv = 0;
+ } else if(c == 159) {
+ sprintf(buf, "{set-tab}");
+ affects_inv = 0;
+ } else if(c == 253) {
+ sprintf(buf, "{bell}");
+ affects_inv = 0;
+ } else if(c == 254) {
+ sprintf(buf, "{del-char}");
+ affects_inv = 0;
+ } else if(c == 255) {
+ sprintf(buf, "{ins-char}");
+ affects_inv = 0;
+ } else if(c7 < 32) {
+ sprintf(buf, "{ctrl-%c}", c7+64);
+ }
+
+ if(affects_inv) {
+ if(c >= 128) {
+ if(!inverse)
+ modifier = inv;
+
+ inverse = 1;
+ } else {
+ if(inverse)
+ modifier = norm;
+
+ inverse = 0;
+ }
+ }
+
+
+ sprintf(result, "%s%s", modifier, buf);
+ return result;
+}
+
+char *asc2ata(int c) {
+ if(c == '\n') {
+ return eol;
+ }
+
+ if(!keep_bit_7)
+ c &= 0x7f;
+
+ buf[0] = buf[1] = '\0';
+
+ if(trans_type == TT_NONE || c == '|' || (c >= 32 && c <= 122)) {
+ buf[0] = c;
+ return buf;
+ }
+
+ if(trans_type == TT_DOT) {
+ return dot;
+ } else if(trans_type == TT_STRIP) {
+ return empty;
+ } else if(trans_type == TT_HEX) {
+ sprintf(buf, "\\x%02X", c);
+ return buf;
+ }
+
+ /* TT_CARET and TT_TABS both translate tabs */
+ if(c == '\t') {
+ buf[0] = 127;
+ return buf;
+ } else if(c == '\b') {
+ buf[0] = 126;
+ return buf;
+ }
+
+ if(trans_type == TT_TABS) {
+ buf[0] = c;
+ return buf;
+ }
+
+ /* handle TT_CARET */
+ buf[0] = '^';
+ buf[1] = '?';
+ buf[2] = '\0';
+
+ if(c < 32) {
+ buf[1] = c + 64;
+ return buf;
+ }
+
+ return buf;
+}
+
+int main(int argc, char **argv) {
+ int c;
+ char *rename_to = NULL;
+ char *infile = NULL, *outfile = NULL;
+ FILE *in = NULL, *out = NULL;
+ int last = -1;
+
+ /*** Parse args */
+ while( (c = getopt(argc, argv, OPTIONS)) != -1 ) {
+ switch(c) {
+ case 'a':
+ input_type = FT_UNIX;
+ output_type = FT_ATARI;
+ break;
+
+ case 'd':
+ input_type = FT_ATARI;
+ output_type = FT_DOS;
+ break;
+
+ case 'm':
+ input_type = FT_ATARI;
+ output_type = FT_MAC9;
+ break;
+
+ case 'u':
+ input_type = FT_ATARI;
+ output_type = FT_UNIX;
+ break;
+
+ case '8':
+ keep_bit_7 = 1;
+ break;
+
+ case 'n':
+ trans_type = TT_NONE;
+ break;
+
+ case 'c':
+ trans_type = TT_CARET;
+ keep_bit_7 = 1;
+ break;
+
+ case 'p':
+ trans_type = TT_DOT;
+ break;
+
+ case 's':
+ trans_type = TT_STRIP;
+ break;
+
+ case 'x':
+ trans_type = TT_HEX;
+ break;
+
+ case 'i':
+ in_place = 1;
+ break;
+
+ case 'q':
+ verbose = 0;
+ break;
+
+ case 'v':
+ verbose++;
+ break;
+
+ case 'h':
+ default:
+ printf(USAGE);
+ exit(1);
+ }
+ }
+
+ /*** Get input filename, open input if not stdin */
+ if(optind < argc) {
+ infile = argv[optind];
+ if(strcmp(infile, "-") == 0) {
+ in = stdin;
+ } else if( !(in = fopen(infile, "rb")) ) {
+ fprintf(stderr, SELF ": (fatal) %s: %s\n", infile, strerror(errno));
+ exit(1);
+ }
+ optind++;
+ } else {
+ in = stdin;
+ infile = "-";
+ }
+
+ if(in_place) {
+ /*** Setup in-place editing */
+ int len;
+
+ if(in == stdin) {
+ fprintf(stderr,
+ SELF ": (fatal) Can't do in-place edit of standard input. "
+ "Run '" SELF " -h' for help.\n");
+ exit(1);
+ }
+
+ /* Build backup filename */
+ len = strlen(infile);
+ rename_to = (char *)malloc(len + 2);
+ if(!rename_to) {
+ fprintf(stderr, SELF ": (fatal) Out of memory\n");
+ fclose(in);
+ exit(1);
+ }
+
+ snprintf(rename_to, len + 2, "%s~", infile);
+ unlink(rename_to);
+
+ /* Rename (link) input (it's already open, no problem) */
+ if(link(infile, rename_to)) {
+ fprintf(stderr, SELF ": (fatal) can't create %s: %s\n",
+ rename_to, strerror(errno));
+ fclose(in);
+ exit(1);
+ }
+
+ if(verbose)
+ fprintf(stderr, SELF ": backed up '%s' as '%s'\n", infile, rename_to);
+
+ unlink(infile);
+
+ outfile = infile;
+ infile = rename_to;
+ } else if(optind < argc) {
+ /*** Get output filename */
+ outfile = argv[optind];
+ if(strcmp(outfile, "-") == 0)
+ out = stdout;
+ } else {
+ /*** No output filename, will write to stdout */
+ out = stdout;
+ outfile = "-";
+ }
+
+ /*** Open output file, if not stdout */
+ /* FIXME: if we *are* reading from stdin or writing to stdout on
+ DOS or Windows, how do we set binary mode on stdin/stdout? */
+ if( out != stdout && !(out = fopen(outfile, "wb")) ) {
+ fprintf(stderr, SELF ": (fatal) %s: %s\n", outfile, strerror(errno));
+ fclose(in);
+ exit(1);
+ }
+
+ /*** Try not to confuse the newbie users, if we're reading from their
+ console (they may be expecting a help message) */
+ if(verbose && in == stdin && isatty(fileno(in)))
+ fprintf(stderr,
+ SELF ": reading from standard input (run '"
+ SELF " -h' for help)...\n");
+
+ if(verbose > 1) {
+ /*** If requested, show the user what's about to happen */
+ if(in_place)
+ fprintf(stderr, SELF ": Using in-place editing mode.\n");
+
+ fprintf(stderr, SELF ": Input file: '%s', type ", infile);
+ switch(input_type) {
+ case FT_AUTO:
+ fprintf(stderr, "will be auto-detected.\n");
+ break;
+
+ case FT_ATARI:
+ fprintf(stderr, "set to Atari.\n");
+ break;
+
+ case FT_UNIX:
+ fprintf(stderr, "set to UNIX/DOS/Mac\n");
+ break;
+ }
+
+ fprintf(stderr, SELF ": Output file: '%s', type ", outfile);
+ switch(output_type) {
+ case FT_AUTO:
+ fprintf(stderr, "will be auto-detected.\n");
+ break;
+
+ case FT_ATARI:
+ fprintf(stderr, "set to Atari.\n");
+ break;
+
+ case FT_UNIX:
+ fprintf(stderr, "set to UNIX.\n");
+ break;
+
+ case FT_DOS:
+ fprintf(stderr, "set to DOS.\n");
+ break;
+
+ case FT_MAC9:
+ fprintf(stderr, "set to Mac Classic.\n");
+ break;
+ }
+
+ fprintf(stderr, SELF ": Non-printable characters ");
+ switch(trans_type) {
+ case TT_NONE:
+ fprintf(stderr, "(incl. tabs/backspaces) will be passed as-is.\n");
+ break;
+
+ case TT_TABS:
+ fprintf(stderr, "will be passed as-is (tabs/backspaces will be translated).\n");
+ break;
+
+ case TT_CARET:
+ fprintf(stderr, "will be printed as ^x or {x}.\n");
+ break;
+
+ case TT_DOT:
+ fprintf(stderr, "will be printed as dots.\n");
+ break;
+
+ case TT_HEX:
+ fprintf(stderr, "will be printed as hex escapes.\n");
+ break;
+
+ case TT_STRIP:
+ fprintf(stderr, "will be stripped.\n");
+ break;
+ }
+
+ fprintf(stderr, SELF ": Bit 7 (inverse video) will be %s.\n",
+ (keep_bit_7 ? "passed as-is" : "stripped"));
+ }
+
+ /*** Read input, process, write; lather, rinse, repeat */
+ while(!feof(in)) {
+ int rew = 0;
+ c = getc(in);
+ if(c < 0) break;
+
+ switch(input_type) {
+ /* Auto-detection works by reading the input until we find
+ an Atari EOL or a UNIX/DOS/Mac \n or \r, then rewinding
+ the stream. Will fail if reading from a pipe. */
+ case FT_AUTO:
+ if(c == 0x9b) {
+ input_type = FT_ATARI;
+ output_type = FT_UNIX;
+ if(verbose)
+ fprintf(stderr, SELF ": input looks like an Atari file\n");
+ rew++;
+ } else if(c == '\n' || c == '\r') {
+ input_type = FT_UNIX;
+ output_type = FT_ATARI;
+ if(verbose)
+ fprintf(stderr, SELF ": input looks like a UNIX/DOS/Mac file\n");
+ rew++;
+ }
+
+ /* rewind if possible */
+ if(rew) {
+ if(fseek(in, 0L, SEEK_SET)) {
+ fprintf(stderr,
+ SELF ": (fatal) Can't seek in input: %s\n"
+ "Try again without type auto-detection.\n",
+ strerror(errno));
+ fclose(in);
+ fclose(out);
+ exit(1);
+ }
+ continue;
+ }
+ break;
+
+ case FT_ATARI:
+ fputs(ata2asc(c), out);
+ continue;
+ break;
+
+ case FT_UNIX:
+ if(last == '\r' && c != '\n') {
+ /* Must be a Mac Classic text file... */
+ putc(0x9b, out);
+ } else if(c == '\r') {
+ /* Swallow CR's */
+ last = c;
+ continue;
+ }
+
+ last = c;
+ fputs(asc2ata(c), out);
+ break;
+ }
+ }
+
+ /* If the last CR was swallowed, spit it back out */
+ if(input_type == FT_UNIX && last == '\r')
+ putc(0x9b, out);
+
+ /*** All done, clean up. */
+ fclose(in);
+ fclose(out);
+
+ if(rename_to)
+ free(rename_to);
+
+ if(input_type == FT_AUTO) {
+ fprintf(stderr,
+ SELF ": (fatal) Input didn't contain any EOL/CR/LF characters!\n");
+ exit(1);
+ }
+
+ return 0;
+}
diff --git a/a8eol.rst b/a8eol.rst
new file mode 100644
index 0000000..ff81d82
--- /dev/null
+++ b/a8eol.rst
@@ -0,0 +1,196 @@
+.. RST source for a8eol(1) man page. Convert with:
+.. rst2man.py a8eol.rst > a8eol.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+=====
+a8eol
+=====
+
+-------------------------------------------------------
+convert Atari 8-bit text files to/from UNIX/Windows/Mac
+-------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+**a8eol** [*-admu8tcpsxih*] [*infile*] [*outfile*]
+
+DESCRIPTION
+===========
+
+**a8eol** converts between ATASCII and UNIX, MS-DOS/Windows, and
+Mac text file formats. It can auto-detect the input file format
+and set the output format accordingly, or the user can explicitly
+set the input and output formats. Various options are available for
+translating non-printing characters, including a mode similar to what
+old computer magazines used for program listings.
+
+OPTIONS
+=======
+
+File type options:
+------------------
+
+-a
+ Input is UNIX, MS-DOS/Windows, or MacOS < 10 text; convert to Atari (EOL=$9B)
+
+-d
+ Input is Atari text; convert to MS-DOS/Windows (EOL=$0A,$0D)
+
+-m
+ Input is Atari text; convert to MacOS < 10 (EOL=$0D)
+
+-u
+ Input is Atari text; convert to UNIX (EOL=$0A)
+
+With none of the above: input type is auto-detected; output type is
+UNIX if input is Atari, or Atari if input is UNIX/DOS/Mac.
+
+Only one file type option can be used per run of **a8eol**. If more than
+one is given, the last one occurring on the command line will be used.
+
+Translation options:
+--------------------
+
+-n
+ Translate EOL characters only; pass anything else as-is (including
+ tabs and backspaces).
+
+-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).
+
+-p
+ Replace non-printing characters with *.* (period, dot).
+
+-s
+ Remove (strip) non-printing characters.
+
+-x
+ Replace non-printing characters with *\\x[hex]*.
+
+With none of the above: EOL, tab, and backspace characters are
+translated; everything else is passed through as-is.
+
+Only one translation option can be used per run of **a8eol**. If more than
+one is given, the last one occurring on the command line will be used.
+
+Other options:
+--------------
+
+-8
+ 8-bit ASCII/ATASCII mode: Do not strip bit 7 (inverse video).
+ This option may be used alone or combined with any of the
+ translation options, above. Characters with bit 7 set are
+ considered non-printing. This option is always enabled when **-c**
+ is used.
+
+-i
+ In-place conversion. Original file renamed to *infile~*. This option
+ can't be used when reading from standard input.
+
+-q
+ Quiet operation. Error messages will still be printed.
+
+-v
+ Verbose operation. Prints extra info about what **a8eol** is doing.
+
+-h
+ Print built-in help message and exit.
+
+Leave *infile* blank or use **-** to read from standard input (in which
+case, don't use the **-i** option).
+
+Leave *outfile* blank or use **-** to write to standard output.
+
+NOTES
+=====
+
+Without the **-8** option, bit 7 is stripped (cleared) for all input
+characters, *except* for the ATASCII EOL ($9B) character (when the
+input is an Atari file). Bit 7 stripping occurs for each input
+character *before* any of the translation options are applied.
+
+The input type auto-detection isn't perfect. It scans from the
+beginning of the input, looking for either an ATASCII EOL or
+an ASCII carriage return or linefeed. An Atari file with ATASCII
+graphics may be mis-detected as an ASCII file, if it contains
+any $0A or $0D bytes before the first EOL ($0A and $0D are graphics
+characters, in ATASCII). If this happens, force Atari input with
+**-d**, **-m**, or **-u**.
+
+The auto-detection also fails with an "Illegal seek" error when
+reading from a pipe (e.g. **cat file | a8eol**). To avoid this, either
+set the input type explicitly with one of **-[admu]**, or read from a
+regular file (possibly a temporary one created just for this purpose).
+This is a bug (not a feature), but probably not worth the time
+it'd take to fix it.
+
+The **-a** option is "magical" in that it can handle input with UNIX
+(*\\n*), DOS (*\\r\\n*), or Mac Classic (*\\r* only) line endings. In fact, it
+can handle an input file containing any combination of the three line
+ending types in the same file.
+
+ATASCII CODES
+=============
+
+When the *-c* option is used on ATASCII input, the special Atari
+character codes are translated into human-readable descriptive
+strings. This is similar to the way old magazines (Compute!, Antic,
+Analog) printed ATASCII codes in typeset program listings.
+
+List of code translations:
+
+.. csv-table::
+ :header: "Dec", "Hex", "Keystroke(s)", "Description"
+
+ "--","--","{inv}","Inverse Video (800: Atari Logo)","Start a sequence of inverse video characters"
+ "--","--","{norm}","Inverse Video (800: Atari Logo)","End a sequence of inverse video characters (back to normal)"
+ "0","00","{ctrl-,}","Ctrl ,","Heart; replaces ASCII NUL"
+ "27","1B","{esc}","Esc Esc","Literal Escape character"
+ "28","1C","{up}","Esc Ctrl -","Cursor Up"
+ "29","1D","{down}","Esc Ctrl =","Cursor Down"
+ "30","1E","{left}","Esc Ctrl +","Cursor Left"
+ "31","1F","{right}","Esc Ctrl \*","Cursor Right"
+ "96","60","{ctrl-.}","Ctrl .","Diamond; replaces ASCII grave accent: \`"
+ "123","7B","{ctrl-;}","Ctrl ;","Club; replaces ASCII left brace: {"
+ "125","7D","{clear}","Esc Ctrl < or Esc Shift <","Clear screen (CLR/HOME on 800); Replaces ASCII right brace: }"
+ "126","7E","{bksp}","Esc Backspace","Backspace (BACK S on 800); Replaces ASCII tilde: ~"
+ "127","7F","{tab}","Esc Tab","Tab to next tab stop; Replaces ASCII DEL: ~"
+ "155","9B","--","Enter","Atari EOL (translated to \\n, \\r\\n, or \\r)"
+ "156","9C","{del-line}","Esc Shift BackSp","Delete logical line @ cursor"
+ "157","9D","{ins-line}","Esc Shift >","Insert blank line @ cursor"
+ "158","9E","{clr-tab}","Esc Ctrl Tab","Clear current tab stop"
+ "159","9F","{set-tab}","Esc Shift Tab","Set tab stop @ cursor position"
+ "253","FD","{bell}","Esc Ctrl 2","Ring bell (800: internal spkr)"
+ "254","FE","{del-char}","Esc Ctrl BackSp","Delete character @ cursor"
+ "255","FF","{ins-char}","Esc Ctrl >","Insert one space @ cursor"
+
+Other control characters are listed as *{ctrl-X}*, where *X* is the
+keystroke to use for entering the character.
+
+.. other sections we might want, uncomment as needed.
+
+.. FILES
+.. =====
+
+.. ENVIRONMENT
+.. ===========
+
+EXIT STATUS
+===========
+
+0 for success, non-zero for any error. Error messages are printed to **stderr**.
+
+.. BUGS
+.. ====
+
+.. EXAMPLES
+.. ========
+
+.. include:: manftr.rst
+
diff --git a/a8utf8 b/a8utf8
new file mode 100755
index 0000000..f5f2df7
--- /dev/null
+++ b/a8utf8
@@ -0,0 +1,163 @@
+#!/usr/bin/perl -w
+
+# convert A8 text to UTF-8. Control graphics characters are replaced with
+# nearest Unicode equivalents (mostly from the box-drawing range, or from
+# the basic-latin range with -i option).
+
+# Careful editing this script: you need an editor that groks UTF-8, or at
+# least one that won't mangle the UTF-8 sequences embedded in the tables
+# below.
+
+($SELF = $0) =~ s,.*/,,;
+
+binmode(STDOUT, ":utf8");
+binmode(STDIN, ":bytes");
+
+use utf8;
+
+%atascii_table = (
+ 0 => "♥",
+ 1 => "┣",
+ 2 => "┃",
+ 3 => "┛",
+ 4 => "┫",
+ 5 => "┓",
+ 6 => "╱",
+ 7 => "╲",
+ 8 => "◢",
+ 9 => "▗",
+ 10 => "◣",
+ 11 => "▝",
+ 12 => "▘",
+ 13 => "▔",
+ 14 => "▁",
+ 15 => "▖",
+ 16 => "♣",
+ 17 => "┏",
+ 18 => "━",
+ 19 => "╋",
+ 20 => "●",
+ 21 => "▄",
+ 22 => "▎",
+ 23 => "┳",
+ 24 => "┻",
+ 25 => "▌",
+ 26 => "┗",
+ 27 => "␛",
+ 28 => "↑",
+ 29 => "↓",
+ 30 => "←",
+ 31 => "→",
+ 96 => "◆",
+ 123 => "♠",
+ 125 => "↰",
+ 126 => "◀",
+ 127 => "▶",
+ 136 => "◤",
+ 137 => "▛",
+ 138 => "◥",
+ 139 => "▙",
+ 140 => "▟",
+ 141 => "▆",
+ 142 => "🮅",
+ 143 => "▜",
+ 148 => "◙",
+ 149 => "▀",
+ 150 => "🮊",
+ 153 => "▐",
+ 155 => "\n",
+ 156 => "⍐",
+ 157 => "⍗",
+ 158 => "⍇",
+ 159 => "⍈",
+ 160 => "█",
+);
+
+%xl_intl_table = (
+ 0 => "á",
+ 1 => "ù",
+ 2 => "Ñ",
+ 3 => "É",
+ 4 => "ç",
+ 5 => "ô",
+ 6 => "ò",
+ 7 => "ì",
+ 8 => "£",
+ 9 => "ï",
+ 10 => "ü",
+ 11 => "ä",
+ 12 => "Ö",
+ 13 => "ú",
+ 14 => "ó",
+ 15 => "ö",
+ 16 => "Ü",
+ 17 => "â",
+ 18 => "û",
+ 19 => "î",
+ 20 => "é",
+ 21 => "è",
+ 22 => "ñ",
+ 23 => "ê",
+ 24 => "ȧ",
+ 25 => "à",
+ 26 => "Ȧ",
+ 27 => "␛",
+ 28 => "↑",
+ 29 => "↓",
+ 30 => "←",
+ 31 => "→",
+ 96 => "¡",
+ 123 => "Ä",
+ 126 => "◀",
+ 127 => "▶",
+ 155 => "\n",
+);
+
+undef $/;
+
+$table = \%atascii_table;
+$print_table = 0;
+while(@ARGV && $ARGV[0] =~ /^-/) {
+ for($ARGV[0]) {
+ /^-i$/ && do { $table = \%xl_intl_table; next; };
+ /^-t$/ && do { $print_table = 1; next; };
+ /^--?h/ && do { usage(0) };
+ warn "$SELF: unknown option: $_\n";
+ usage(1);
+ }
+ shift @ARGV;
+}
+
+if($print_table) {
+ for(sort { $a <=> $b } keys %$table) {
+ my $chr = translate(chr $_);
+ $chr = '\n' if $chr eq "\n";
+ printf '"%d","$%02x","%s"' . "\n", $_, $_, $chr;
+ }
+ exit 0;
+}
+
+$_ = <>;
+s/(.)/translate($1)/seg;
+print;
+
+sub translate {
+ my $o = ord(shift);
+ my $ret;
+
+ $ret = $table->{$o};
+ return $ret if defined($ret);
+
+ $ret = $table->{$o & 0x7f};
+ return $ret if defined($ret);
+
+ return chr($o & 0x7f);
+}
+
+sub usage {
+ print <<EOF;
+Usage: $SELF [--help] | [-t [-i]] | [-i] infile [infile ...]
+See man page for details.
+EOF
+ exit $_[0];
+}
diff --git a/a8utf8.1 b/a8utf8.1
new file mode 100644
index 0000000..3ff998f
--- /dev/null
+++ b/a8utf8.1
@@ -0,0 +1,108 @@
+.\" 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 "A8UTF8" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py a8utf8.rst > a8utf8.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fIa8utf8\fP [\fB\-i\fP] [\fIinfile\fP] [\fIinfile ...\fP]
+.sp
+\fIa8utf8\fP [\fB\-i\fP] \fB\-t\fP
+.SH DESCRIPTION
+.sp
+Convert Atari 8\-bit ATASCII or International Character Set text to
+UTF\-8 encoded Unicode. Control graphics characters are replaced with
+their nearest Unicode equivalents (mostly from the Box Drawing block,
+or from the Basic Latin block with \fB\-i\fP option).
+.sp
+If no \fIinfile\fP is given, input is read from standard input. Output always
+goes to standard output; to write to a file, use a command like:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+a8utf8 atari.txt > converted.txt
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+The output is plain UTF\-8 Unicode, without BOM.
+.sp
+Inverse video (characters codes above \fB$80\fP) are translated to
+their non\-inverse equivalents, except \fB$9B\fP (Atari EOL), which is
+translated to \fB\en\fP (newline).
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \-i
+Input uses Atari XL/XE International Character Set encoding, rather than
+ATASCII graphics.
+.TP
+.B \-t
+Print table of Atari to Unicode equivalents, in CSV format. Can
+be used with or without \fB\-i\fP (two different tables).
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/a8utf8.rst b/a8utf8.rst
new file mode 100644
index 0000000..2d36193
--- /dev/null
+++ b/a8utf8.rst
@@ -0,0 +1,52 @@
+.. RST source for a8utf8(1) man page. Convert with:
+.. rst2man.py a8utf8.rst > a8utf8.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+======
+a8utf8
+======
+
+--------------------------------------------------
+Convert Atari 8-bit text to UTF-8 encoded Unicode.
+--------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+*a8utf8* [**-i**] [*infile*] [*infile ...*]
+
+*a8utf8* [**-i**] **-t**
+
+DESCRIPTION
+===========
+
+Convert Atari 8-bit ATASCII or International Character Set text to
+UTF-8 encoded Unicode. Control graphics characters are replaced with
+their nearest Unicode equivalents (mostly from the Box Drawing block,
+or from the Basic Latin block with **-i** option).
+
+If no *infile* is given, input is read from standard input. Output always
+goes to standard output; to write to a file, use a command like::
+
+ a8utf8 atari.txt > converted.txt
+
+The output is plain UTF-8 Unicode, without BOM.
+
+Inverse video (characters codes above **$80**) are translated to
+their non-inverse equivalents, except **$9B** (Atari EOL), which is
+translated to **\\n** (newline).
+
+OPTIONS
+=======
+
+-i
+ Input uses Atari XL/XE International Character Set encoding, rather than
+ ATASCII graphics.
+
+-t
+ Print table of Atari to Unicode equivalents, in CSV format. Can
+ be used with or without **-i** (two different tables).
+
+.. include:: manftr.rst
diff --git a/asmwrapper.sh b/asmwrapper.sh
new file mode 100644
index 0000000..56c8d9a
--- /dev/null
+++ b/asmwrapper.sh
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+# Execute either the real dasm, or dasm2atasm + atasm.
+# If we have dasm on the PATH, use it, otherwise fake it.
+# dasm2atasm is NOT perfect! It does however work OK for
+# fenders.dasm, fendersdbl.dasm, and loadscreen.dasm.
+
+# The .syms file format is totally different for dasm and atasm, but
+# fenders_offsets.pl can handle either one (adapt for your purposes).
+
+# If you have dasm, but want to force atasm, pass any non-empty string
+# as the second argument to this script.
+
+# Could also use "dasm2atasm -c" and ca65/ld65 to assemble, but I can't
+# seem to get ca65 or ld65 to emit a symbol file or listing (the -l
+# option doesn't work; the -m option doesn't do what I expect), so
+# I don't know any way to extract the OFFSET_* labels from ca65's output.
+
+if [ -z "$1" ]; then
+ echo "$0: missing argument"
+ exit 1
+fi
+
+if [ -n "`which dasm 2>/dev/null`" -a -z "$2" ] ; then
+ exec dasm $1.dasm -f3 -s$1.syms -o$1.bin
+elif [ -n "`which atasm 2>/dev/null`" ]; then
+ ln -sf equates.inc equates.m65
+ perl dasm2atasm $1.dasm $1.atasm
+ exec atasm -r -s -o$1.bin $1.atasm > $1.syms
+else
+ echo "$0: you need either dasm or atasm on your PATH"
+ exit 1
+fi
+
+exit 0
diff --git a/atr2xfd.1 b/atr2xfd.1
new file mode 100644
index 0000000..0680ea3
--- /dev/null
+++ b/atr2xfd.1
@@ -0,0 +1,201 @@
+.\" 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 "ATR2XFD" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py atr2xfd.rst > atr2xfd.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fBatr2xfd\fP \fIinfile.atr\fP [\fIoutfile.xfd\fP]
+.sp
+\fBatrcheck\fP \fIinfile.atr\fP
+.SH DESCRIPTION
+.sp
+\fBatr2xfd\fP strips the 16\-byte ATR header from an ATR image. While this
+could be done with a command like:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+dd if=infile.atr outfile.xfd bs=16 skip=1
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+\&...**atr2xfd** first checks that its input is in fact an ATR file, then
+checks the ATR header and reports any problems it may find.
+.sp
+\fBatrcheck\fP performs the same checks as \fBatr2xfd\fP, but doesn\(aqt actually
+write an XFD image. In fact, \fBatrcheck\fP is a symbolic link to \fBatr2xfd\fP,
+which changes its behaviour (simply doesn\(aqt write any output) when
+called via the link.
+.sp
+Neither \fBatr2xfd\fP nor \fBatrcheck\fP take any options.
+.SH NOTES
+.sp
+For both commands, you may use \fB\-\fP for \fBinfile\fP to read from
+standard input. For \fBatr2xfd\fP, use \fB\-\fP for \fBoutfile\fP to write
+to standard output. If a filename is supplied for \fBoutfile\fP, it will
+always be used as\-is (no \fI\&.xfd\fP extension will be appended).
+.sp
+If outfile is omitted, it is constructed like so:
+.INDENT 0.0
+.IP \(bu 2
+If reading from standard input, write to standard output.
+.IP \(bu 2
+If reading from a file whose name ends with an \fI\&.atr\fP or \fI\&.ATR\fP extension,
+replace the extension with \fI\&.xfd\fP\&.
+.IP \(bu 2
+Otherwise, append \fI\&.xfd\fP to the input filename.
+.UNINDENT
+.SH EXAMPLES
+.sp
+Check an image:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+$ atrcheck dos_20s.atr
+atrcheck: size is 5760 16\-byte paragraphs
+atrcheck: sectors: 720, sector size: 128 bytes
+atrcheck: dos_20s.atr is a standard SS/SD image, 90K
+atrcheck: ATR image OK (no fatal errors).
+[ exit status is 0 ]
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+Check an image and convert to XFD:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+$ atr2xfd dos_20s.atr
+atr2xfd: input \(aqdos_20s.atr\(aq, output \(aqdos_20s.xfd\(aq
+atr2xfd: size is 5760 16\-byte paragraphs
+atr2xfd: sectors: 720, sector size: 128 bytes
+atr2xfd: dos_20s.atr is a standard SS/SD image, 90K
+atr2xfd: ATR image OK (no fatal errors).
+atr2xfd: XFD image OK, wrote 92160 bytes
+[ exit status is 0 ]
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+Attempt to use atrcheck with an XFD image:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+$ atrcheck dos_20s.xfd
+atrcheck: (fatal) dos_20s.xfd looks like an XFD image, not an ATR
+[ exit status is 2 ]
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+Here, games001.atr is one of the old Yogi/Jellystone 1 meg images:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+$ atrcheck games001.atr
+atrcheck: size is 65536 16\-byte paragraphs
+atrcheck: sectors: 8192, sector size: 128 bytes
+atrcheck: games001.atr is a high\-capacity floppy or hard disk image, SD
+atrcheck: ATR image OK (no fatal errors).
+[ exit status is 0 ]
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+Here is an attempt to treat a non\-image file as an image:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+$ atrcheck /bin/ls
+atrcheck: size is 5120 16\-byte paragraphs
+atrcheck: (fatal) /bin/ls not an ATR file (no NICKATARI signature)!
+[ exit status is 2 ]
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.SH EXIT STATUS
+.sp
+Exit status is zero for success, non\-zero for failure. Further,
+exit status will be 1 for errors involving file I/O (file not found,
+permissions, etc), and 2 for structural errors in the ATR file.
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/atr2xfd.c b/atr2xfd.c
new file mode 100644
index 0000000..c941266
--- /dev/null
+++ b/atr2xfd.c
@@ -0,0 +1,244 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "atr2xfd"
+#define CHECK "atrcheck"
+
+int main(int argc, char **argv) {
+ char *type;
+ char *self = SELF;
+ struct stat st;
+ char infile[4096], outfile[4096];
+ unsigned char buf[16];
+ FILE *in, *out;
+ int i, paras, hparas, secsize, seccount;
+ int checkonly = 0, bytes = 0;
+
+ if(strstr(argv[0], CHECK)) {
+ self = CHECK;
+ checkonly = 1;
+ }
+
+ if(argc < 2 || argc > 3) {
+ fprintf(stderr,
+ "%s v" VERSION " by B. Watson (WTFPL)\n", self);
+ fprintf(stderr,
+ "Usage: %s input.xfd%s\n",
+ self,
+ (checkonly ? "" : " [output.atr]"));
+ exit(1);
+ }
+
+ strcpy(infile, argv[1]);
+ if(argc == 3) {
+ strcpy(outfile, argv[2]);
+ if(checkonly)
+ fprintf(stderr,
+ "%s: output file not used with %s (ignoring it).\n",
+ self, self);
+ } else if(strcmp(infile, "-") == 0) {
+ strcpy(outfile, "-");
+ } else {
+ char *p;
+ strcpy(outfile, argv[1]);
+
+ p = strstr(outfile, ".atr");
+ if(!p) p = strstr(outfile, ".ATR");
+ if(!p) p = outfile + strlen(outfile);
+ strcpy(p, ".xfd");
+ }
+
+ if(!checkonly)
+ fprintf(stderr, "%s: input '%s', output '%s'\n", self, infile, outfile);
+
+ if(strcmp(infile, "-") == 0) {
+ in = stdin;
+ } else {
+ if( !(in = fopen(infile, "rb")) ) {
+ fprintf(stderr, "%s: (fatal) can't read %s: %s\n",
+ self, infile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ if(fstat(fileno(in), &st)) {
+ fprintf(stderr, "%s: (fatal) can't stat %s: %s\n",
+ self, infile, strerror(errno));
+ exit(1);
+ }
+
+ /* A few sanity checks... */
+ if(st.st_size < 400) {
+ fprintf(stderr,
+ "%s: (fatal) %s too small to be an ATR image (<400 bytes)\n",
+ self, infile);
+ exit(2);
+ }
+
+ if(st.st_size % 128 == 0 ) {
+ fprintf(stderr,
+ "%s: (fatal) %s looks like an XFD image, not an ATR\n",
+ self, infile);
+ exit(2);
+ }
+
+ if( (st.st_size - 16) % 128 != 0 ) {
+ fprintf(stderr,
+ "%s: (fatal) %s not a valid ATR image (not an even number "
+ "of sectors)\n",
+ self, infile);
+ exit(2);
+ }
+
+ if(st.st_size > (65535 * 256)) {
+ fprintf(stderr,
+ "%s: (fatal) %s too large to be an ATR image (>16M)\n",
+ self, infile);
+ exit(2);
+ }
+
+ paras = st.st_size / 16 - 1;
+ fprintf(stderr, "%s: size is %d 16-byte paragraphs\n", self, paras);
+
+ if(fread(buf, 1, 16, in) != 16) {
+ fprintf(stderr,
+ "%s: (fatal) can't read ATR header from %s: %s\n",
+ self, infile, strerror(errno));
+ exit(1);
+ }
+
+ if( !(buf[0] == 0x96 && buf[1] == 0x02) ) {
+ fprintf(stderr,
+ "%s: (fatal) %s not an ATR file (no NICKATARI signature)!\n",
+ self, infile);
+ exit(2);
+ }
+
+ secsize = buf[4] + (buf[5] << 8);
+
+ if( !(secsize == 128 || secsize == 256) ) {
+ fprintf(stderr,
+ "%s: (fatal) %s has invalid sector size %d\n",
+ self, infile, secsize);
+ exit(2);
+ }
+
+ if(secsize == 128)
+ seccount = (st.st_size - 16) / secsize;
+ else {
+ seccount = (st.st_size - 400) / secsize + 3;
+ if((st.st_size - 16) % 256 != 128)
+ fprintf(stderr, "%s: partial sector at end of DD image, might "
+ "be a truncated or bogus ATR.\n", self);
+ }
+
+ fprintf(stderr,
+ "%s: sectors: %d, sector size: %d bytes",
+ self, seccount, secsize);
+
+ if(secsize == 256)
+ fprintf(stderr, " (first 3 sectors are 128 bytes)");
+ fputc('\n', stderr);
+
+ if(secsize == 128) {
+ if(seccount < 720)
+ type = "short SS/SD image, <90K";
+ else if(seccount == 720)
+ type = "standard SS/SD image, 90K";
+ else if(seccount == 1040)
+ type = "1050 SS/ED image, 130K";
+ else if(seccount == 1440)
+ type = "XF551 DS/SD image, 180K";
+ else
+ type = "high-capacity floppy or hard disk image, SD";
+ } else {
+ if(seccount < 720)
+ type = "short SS/DD image, <180K";
+ else if(seccount == 720)
+ type = "standard SS/DD image, 180K";
+ else if(seccount == 1440)
+ type = "XF551 DS/DD or ATR8000 SS/QD image, 360K";
+ else if(seccount == 2880)
+ type = "ATR8000 DS/QD or SS/PC image, 720K";
+ else if(seccount == 5760)
+ type = "ATR8000 PC 1.44M image, 1440K";
+ else
+ type = "high-capacity floppy or hard disk image, DD";
+ }
+
+ fprintf(stderr, "%s: %s is a %s\n", self, infile, type);
+
+ fprintf(stderr, "%s: ATR image OK (no fatal errors).\n", self);
+
+ hparas = buf[2] + (buf[3] << 8) + (buf[6] << 16);
+ if(hparas != paras) {
+ /* this is only a fatal error if checkonly is true */
+ fprintf(stderr,
+ "%s: (%s) %s file size (%d paragraphs) doesn't agree with "
+ "ATR header (%d paragraphs). File may be truncated or corrupt.\n",
+ self,
+ (checkonly ? "fatal" : "warning"),
+ infile, paras, hparas);
+
+ if(checkonly)
+ exit(1);
+
+ fprintf(stderr,
+ "%s: Using actual file size for XFD image; expect trouble.\n",
+ self);
+ }
+
+ if(checkonly)
+ exit(0);
+
+ /* Only open the output file after the ATR is known to be good */
+ if(strcmp(outfile, "-") == 0) {
+ out = stdout;
+ } else {
+ if( !(out = fopen(outfile, "wb")) ) {
+ fprintf(stderr,
+ "%s: (fatal) can't write %s: %s\n",
+ self, outfile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ /* copy the data */
+ while( (i = fgetc(in)) != EOF ) {
+ fputc(i, out);
+ bytes++;
+ }
+
+ /* fputc() returns EOF on error *or* EOF;
+ check for I/O errors, return 1 if so */
+ i = 0;
+
+ if(ferror(in)) {
+ i = 1;
+ fprintf(stderr,
+ "%s: error reading %s: %s\n", self, infile, strerror(errno));
+ }
+
+ if(ferror(out)) {
+ i = 1;
+ fprintf(stderr,
+ "%s: error writing %s: %s\n", self, outfile, strerror(errno));
+ }
+
+ fclose(in);
+ fclose(out);
+
+ if(!i)
+ fprintf(stderr, "%s: XFD image OK, wrote %d bytes\n", self, bytes);
+
+ return i;
+}
diff --git a/atr2xfd.rst b/atr2xfd.rst
new file mode 100644
index 0000000..fac2ed4
--- /dev/null
+++ b/atr2xfd.rst
@@ -0,0 +1,109 @@
+.. RST source for atr2xfd(1) man page. Convert with:
+.. rst2man.py atr2xfd.rst > atr2xfd.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+=======
+atr2xfd
+=======
+
+----------------------------------------------------------
+Convert an Atari 8-bit ATR disk image to a raw (XFD) image
+----------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+**atr2xfd** *infile.atr* [*outfile.xfd*]
+
+**atrcheck** *infile.atr*
+
+DESCRIPTION
+===========
+
+**atr2xfd** strips the 16-byte ATR header from an ATR image. While this
+could be done with a command like::
+
+ dd if=infile.atr outfile.xfd bs=16 skip=1
+
+...**atr2xfd** first checks that its input is in fact an ATR file, then
+checks the ATR header and reports any problems it may find.
+
+**atrcheck** performs the same checks as **atr2xfd**, but doesn't actually
+write an XFD image. In fact, **atrcheck** is a symbolic link to **atr2xfd**,
+which changes its behaviour (simply doesn't write any output) when
+called via the link.
+
+Neither **atr2xfd** nor **atrcheck** take any options.
+
+NOTES
+=====
+
+For both commands, you may use **-** for **infile** to read from
+standard input. For **atr2xfd**, use **-** for **outfile** to write
+to standard output. If a filename is supplied for **outfile**, it will
+always be used as-is (no *.xfd* extension will be appended).
+
+If outfile is omitted, it is constructed like so:
+
+- If reading from standard input, write to standard output.
+
+- If reading from a file whose name ends with an *.atr* or *.ATR* extension,
+ replace the extension with *.xfd*.
+
+- Otherwise, append *.xfd* to the input filename.
+
+EXAMPLES
+========
+
+Check an image::
+
+ $ atrcheck dos_20s.atr
+ atrcheck: size is 5760 16-byte paragraphs
+ atrcheck: sectors: 720, sector size: 128 bytes
+ atrcheck: dos_20s.atr is a standard SS/SD image, 90K
+ atrcheck: ATR image OK (no fatal errors).
+ [ exit status is 0 ]
+
+Check an image and convert to XFD::
+
+ $ atr2xfd dos_20s.atr
+ atr2xfd: input 'dos_20s.atr', output 'dos_20s.xfd'
+ atr2xfd: size is 5760 16-byte paragraphs
+ atr2xfd: sectors: 720, sector size: 128 bytes
+ atr2xfd: dos_20s.atr is a standard SS/SD image, 90K
+ atr2xfd: ATR image OK (no fatal errors).
+ atr2xfd: XFD image OK, wrote 92160 bytes
+ [ exit status is 0 ]
+
+Attempt to use atrcheck with an XFD image::
+
+ $ atrcheck dos_20s.xfd
+ atrcheck: (fatal) dos_20s.xfd looks like an XFD image, not an ATR
+ [ exit status is 2 ]
+
+Here, games001.atr is one of the old Yogi/Jellystone 1 meg images::
+
+ $ atrcheck games001.atr
+ atrcheck: size is 65536 16-byte paragraphs
+ atrcheck: sectors: 8192, sector size: 128 bytes
+ atrcheck: games001.atr is a high-capacity floppy or hard disk image, SD
+ atrcheck: ATR image OK (no fatal errors).
+ [ exit status is 0 ]
+
+Here is an attempt to treat a non-image file as an image::
+
+ $ atrcheck /bin/ls
+ atrcheck: size is 5120 16-byte paragraphs
+ atrcheck: (fatal) /bin/ls not an ATR file (no NICKATARI signature)!
+ [ exit status is 2 ]
+
+EXIT STATUS
+===========
+
+Exit status is zero for success, non-zero for failure. Further,
+exit status will be 1 for errors involving file I/O (file not found,
+permissions, etc), and 2 for structural errors in the ATR file.
+
+.. include:: manftr.rst
diff --git a/atrsize.1 b/atrsize.1
new file mode 100644
index 0000000..3b9b571
--- /dev/null
+++ b/atrsize.1
@@ -0,0 +1,216 @@
+.\" 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 "ATRSIZE" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py atrsize.rst > atrsize.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+atrsize [\fB\-bB\fP] \fIinfile.atr\fP [\fIsectors\fP]
+.SH DESCRIPTION
+.sp
+Without the \fB\-b\fP or \fB\-B\fP options:
+.INDENT 0.0
+.INDENT 3.5
+\fIinfile.atr\fP will be backed up to \fIinfile.atr~\fP, and a new \fIinfile.atr\fP will
+be created. If \fIsectors\fP is given, the new image file will be truncated
+or extended to the new size. Without \fIsectors\fP, the new image\(aqs size will
+be set as follows:
+.TS
+center;
+|l|l|l|.
+_
+T{
+Density
+T} T{
+Original Sectors
+T} T{
+New Sectors
+T}
+_
+T{
+Either
+T} T{
+0 \- 2
+T} T{
+Error
+T}
+_
+T{
+Either
+T} T{
+3 \- 719
+T} T{
+720
+T}
+_
+T{
+Either
+T} T{
+720
+T} T{
+720 (no change)
+T}
+_
+T{
+Single
+T} T{
+721 \- 1039
+T} T{
+1040 (aka 1050 enhanced density)
+T}
+_
+T{
+Single
+T} T{
+1040
+T} T{
+1040 (no change)
+T}
+_
+T{
+Single
+T} T{
+1041 or more
+T} T{
+Unknown (must specify size)
+T}
+_
+T{
+Double
+T} T{
+721 \- 1339
+T} T{
+1440 (aka double sided, double density)
+T}
+_
+T{
+Double
+T} T{
+1440
+T} T{
+1440 (no change)
+T}
+_
+T{
+Double
+T} T{
+1441 or more
+T} T{
+Unknown (must specify size)
+T}
+_
+.TE
+.sp
+When \fIsectors\fP is given, its allowed range is from 3 to 65535. \fIinfile.atr\fP
+will be rewritten at the new size.
+.sp
+When \fBatrsize\fP changes the size of an image, the new ATR header
+will reflect the new size. If the new image is larger than the old
+image, \fBatrsize\fP pads the image with empty sectors containing all 0
+data bytes. If the new image is smaller than the old image, it is
+truncated, and any data in the old image that resides in the removed
+sectors will be lost.
+.sp
+For ATR images where the ATR header doesn\(aqt agree with the actual size
+of the file, the actual file size is used to determine the number of
+sectors. The output image will have its ATR header adjusted to reflect
+the actual file size of the image, if sectors is not given.
+.UNINDENT
+.UNINDENT
+.sp
+With \fB\-b\fP or \fB\-B\fP:
+.INDENT 0.0
+.INDENT 3.5
+\fBatrsize\fP will create a new, blank image called \fIinfile.atr\fP\&. If
+this file already exists, however, it will not be overwritten
+(instead, \fBatrsize\fP will exit with a "file exists" message).
+.sp
+If \fIsectors\fP is given, the new image\(aqs size will be set to that many
+sectors. If not given, the new image\(aqs size will be 720 sectors.
+.sp
+\fB\-b\fP creates a new image with 128\-byte sectors (single density)
+.sp
+\fB\-B\fP creates a new image with 256\-byte sectors (double density).
+.sp
+New images created with \fBatrsize\fP consist of a valid ATR header,
+and sectors filled with zeroes. No boot sectors, directory, VTOC,
+or filesystem are created. To use a blank image for file
+storage, it must be formatted with an Atari DOS (either in an
+emulator or with a real Atari via SIO2PC cable). If you\(aqre
+trying to create a blank DOS 2.0S disk, use \fBaxe \-b\fP\&.
+.UNINDENT
+.UNINDENT
+.SH NOTES
+.INDENT 0.0
+.IP \(bu 2
+\fBatrsize\fP cannot change the sector size (density) of an image under
+any circumstances. Only the sector count may be changed.
+.IP \(bu 2
+\fBatrsize\fP will fail if the input file doesn\(aqt have a valid ATR header.
+If it\(aqs an XFD (raw) image, use \fBxfd2atr\fP(1) to convert it to an ATR first.
+.UNINDENT
+.SH EXIT STATUS
+.sp
+Exit status is zero for success, non\-zero 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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/atrsize.c b/atrsize.c
new file mode 100644
index 0000000..5571a02
--- /dev/null
+++ b/atrsize.c
@@ -0,0 +1,308 @@
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+/* FIXME: This code is terrible.
+ I should have written an ATR library for atrsize, atr2xfd, and xfd2atr
+ to use, instead of the copy/paste mess I've created. */
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "atrsize"
+#define USAGE \
+ SELF " v" VERSION " by B. Watson (WTFPL)\n" \
+ "Usage: " SELF " -[bB] image.atr [sectors]\n" \
+ " -b Create image.atr as a new, blank, single-density image\n" \
+ " -B Create image.atr as a new, blank, double-density image\n" \
+ "Original image.atr is backed up as image.atr~\n" \
+ "Without [sectors], size is rounded up to next 'standard' size\n"
+
+char *usage = USAGE;
+
+int main(int argc, char **argv) {
+ struct stat st;
+ int filesize = 0;
+ int headersize = 0;
+ int blank = 0;
+ int len, sec;
+ unsigned int secsize, seccount, paras, newseccount = 0, last_data_sec = 0;
+ char *newsize, infile[4096], *outfile;
+ unsigned char buf[384];
+ FILE *in, *out;
+
+ if(argc < 2 || argc > 4) {
+ fprintf(stderr, usage);
+ exit(1);
+ }
+
+ if(strcmp(argv[1], "-b") == 0) {
+ blank = 128;
+ argv++;
+ argc--;
+ } else if(strcmp(argv[1], "-B") == 0) {
+ blank = 256;
+ argv++;
+ argc--;
+ }
+
+ newsize = argv[2];
+
+ outfile = argv[1];
+ strcpy(infile, outfile);
+ if( !(in = fopen(infile, "rb")) ) {
+ if(!blank) {
+ fprintf(stderr,
+ SELF ": %s: %s\n", infile, strerror(errno));
+ exit(1);
+ }
+
+ fprintf(stderr,
+ SELF ": %s does not exist, creating blank image\n", infile);
+
+ secsize = blank;
+ seccount = 720;
+
+ memset(buf, 0, 16);
+
+ buf[0] = 0x96;
+ buf[1] = 0x02;
+ buf[4] = secsize & 0xff;
+ buf[5] = (secsize >> 8) & 0xff;
+
+ filesize = headersize = 128 * 720 + (secsize - 128) * 717;
+ newseccount = seccount = 720;
+
+ paras = headersize / 16;
+ buf[2] = paras & 0xff;
+ buf[3] = (paras >> 8) & 0xff;
+ buf[6] = (paras >> 16) & 0xff;
+ }
+
+ if(in) {
+ if(blank) {
+ fprintf(stderr,
+ SELF ": won't create blank image %s: file exists\n", infile);
+ exit(1);
+ }
+
+ if(fstat(fileno(in), &st)) {
+ fprintf(stderr, "Can't determine size of %s: %s\n",
+ infile, strerror(errno));
+ exit(1);
+ } else {
+ filesize = st.st_size - 16;
+ if(filesize < 384) {
+ fprintf(stderr,
+ SELF ": %s is too small to be a valid ATR image\n",
+ infile);
+ exit(1);
+ }
+ }
+
+ if(fread(buf, 1, 16, in) != 16) {
+ fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ if( !(buf[0] == 0x96 && buf[1] == 0x02) ) {
+ fprintf(stderr,
+ SELF ": (fatal) %s not an ATR file (no NICKATARI signature)!\n",
+ infile);
+ exit(2);
+ }
+
+ secsize = buf[4] + (buf[5] << 8);
+
+ if( !(secsize == 128 || secsize == 256) ) {
+ fprintf(stderr,
+ SELF ": (fatal) %s has invalid sector size %d\n",
+ infile, secsize);
+ exit(2);
+ }
+
+ paras = buf[2] + (buf[3] << 8) + (buf[6] << 16);
+ headersize = paras * 16;
+
+ if(filesize && (filesize != headersize)) {
+ fprintf(stderr,
+ SELF ": warning: %s file size (%d bytes) doesn't agree with "
+ "ATR header (%d bytes). File may be truncated or corrupt.\n",
+ infile,
+ filesize,
+ headersize);
+ }
+
+ if(secsize == 128) {
+ fprintf(stderr, SELF ": single density image (128 bytes/sector)\n");
+ seccount = filesize / secsize;
+ } else {
+ fprintf(stderr, SELF ": double density image (256 bytes/sector)\n");
+ seccount = (filesize - 128 * 3) / secsize + 3;
+ }
+
+ newseccount = seccount;
+ if(newsize == NULL) {
+ /* figure out appropriate new size for this image */
+ if(secsize == 128) {
+ if(seccount < 720)
+ newseccount = 720;
+ else if(seccount > 720 && seccount < 1040)
+ newseccount = 1040;
+ else if(seccount >= 1040) {
+ fprintf(stderr, SELF ": must specify a sector count "
+ "for SD images >= 1040 sectors.\n");
+ exit(1);
+ }
+ } else {
+ if(seccount < 720)
+ newseccount = 720;
+ else if(seccount > 720 && seccount < 1440)
+ newseccount = 1440;
+ else if(seccount >= 1440) {
+ fprintf(stderr, SELF ": must specify a sector count "
+ "for DD images >= 1440 sectors.\n");
+ exit(1);
+ }
+ }
+
+ /*
+ if(in) {
+ if(newseccount == seccount) {
+ fprintf(stderr,
+ SELF ": image is already %d sectors, nothing to do\n", seccount);
+ exit(0);
+ } else if(!newseccount) {
+ fprintf(stderr,
+ SELF ": image is already standard size, nothing to do\n");
+ exit(0);
+ }
+ }
+ */
+ } else {
+ newseccount = atoi(newsize);
+ if(newseccount < 3 || newseccount > 65535) {
+ fprintf(stderr,
+ SELF ": invalid sector count (must be 3 - 65535)\n");
+ exit(1);
+ }
+ }
+
+ if(newseccount < 368) {
+ fprintf(stderr, SELF ": warning: "
+ "Output image will not have Atari/MyDOS directory sectors.\n");
+ }
+
+ /* fix up ATR header */
+ if(secsize == 128)
+ headersize = secsize * newseccount;
+ else
+ headersize = (128 * 3) + (newseccount - 3) * secsize;
+
+ paras = headersize / 16;
+ buf[2] = paras & 0xff;
+ buf[3] = (paras >> 8) & 0xff;
+ buf[6] = (paras >> 16) & 0xff;
+
+ if(in) {
+ /* make backup file */
+ len = strlen(infile);
+ infile[len] = '~';
+ infile[len + 1] = '\0';
+
+ unlink(infile);
+ if(link(outfile, infile)) {
+ fprintf(stderr, SELF ": can't create %s: %s\n",
+ infile, strerror(errno));
+ exit(1);
+ }
+
+ if(unlink(outfile)) {
+ fprintf(stderr, SELF ": can't delete %s: %s\n",
+ outfile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ if( !(out = fopen(outfile, "wb")) ) {
+ fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno));
+ exit(1);
+ }
+
+ /* write ATR header */
+ if(fwrite(buf, 1, 16, out) != 16) {
+ fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno));
+ exit(1);
+ }
+
+ /* read first 3 sectors (always present, always SD) */
+ if(in) {
+ if(fread(buf, 1, 384, in) != 384) {
+ fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno));
+ exit(1);
+ }
+ } else {
+ memset(buf, 0, 384);
+ }
+
+ /* write first 3 sectors */
+ if(fwrite(buf, 1, 384, out) != 384) {
+ fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno));
+ exit(1);
+ }
+
+ for(sec = 4; (sec <= seccount) && (sec <= newseccount); sec++) {
+ int i, has_data = 0;
+
+ if(in) {
+ if(fread(buf, 1, secsize, in) != secsize) {
+ fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ for(i=0; i<secsize; i++)
+ has_data |= buf[i];
+
+ if(has_data)
+ last_data_sec = sec;
+
+ if(fwrite(buf, 1, secsize, out) != secsize) {
+ fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ if(last_data_sec)
+ fprintf(stderr, SELF ": last non-empty sector was %d\n", last_data_sec);
+ else
+ fprintf(stderr, SELF ": image is blank (no data)\n");
+
+ if(newseccount < seccount) {
+ fprintf(stderr, SELF ": %s truncated to %d sectors, OK\n",
+ outfile, newseccount);
+ } else if(newseccount == seccount) {
+ fprintf(stderr, SELF ": %s rewritten at %d sectors, OK\n",
+ outfile, newseccount);
+ } else {
+ memset(buf, 0, secsize);
+ for( ; sec <= newseccount; sec++) {
+ if(fwrite(buf, 1, secsize, out) != secsize) {
+ fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno));
+ exit(1);
+ }
+ }
+ fprintf(stderr, SELF ": %s extended to %d sectors, OK\n",
+ outfile, newseccount);
+ }
+
+ if(in) fclose(in);
+ fclose(out);
+ exit(0);
+}
diff --git a/atrsize.rst b/atrsize.rst
new file mode 100644
index 0000000..2c81f90
--- /dev/null
+++ b/atrsize.rst
@@ -0,0 +1,92 @@
+.. RST source for atrsize(1) man page. Convert with:
+.. rst2man.py atrsize.rst > atrsize.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+=======
+atrsize
+=======
+
+-----------------------------------------------------------------------------
+Change the size of an Atari 8-bit ATR disk image, or create a blank ATR image
+-----------------------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+atrsize [**-bB**] *infile.atr* [*sectors*]
+
+DESCRIPTION
+===========
+
+Without the **-b** or **-B** options:
+
+ *infile.atr* will be backed up to *infile.atr~*, and a new *infile.atr* will
+ be created. If *sectors* is given, the new image file will be truncated
+ or extended to the new size. Without *sectors*, the new image's size will
+ be set as follows:
+
+ .. csv-table::
+ :header: "Density","Original Sectors","New Sectors"
+
+ "Either","0 - 2","Error"
+ "Either","3 - 719","720"
+ "Either","720","720 (no change)"
+ "Single","721 - 1039","1040 (aka 1050 enhanced density)"
+ "Single","1040","1040 (no change)"
+ "Single","1041 or more","Unknown (must specify size)"
+ "Double","721 - 1339","1440 (aka double sided, double density)"
+ "Double","1440","1440 (no change)"
+ "Double","1441 or more","Unknown (must specify size)"
+
+ When *sectors* is given, its allowed range is from 3 to 65535. *infile.atr*
+ will be rewritten at the new size.
+
+ When **atrsize** changes the size of an image, the new ATR header
+ will reflect the new size. If the new image is larger than the old
+ image, **atrsize** pads the image with empty sectors containing all 0
+ data bytes. If the new image is smaller than the old image, it is
+ truncated, and any data in the old image that resides in the removed
+ sectors will be lost.
+
+ For ATR images where the ATR header doesn't agree with the actual size
+ of the file, the actual file size is used to determine the number of
+ sectors. The output image will have its ATR header adjusted to reflect
+ the actual file size of the image, if sectors is not given.
+
+With **-b** or **-B**:
+
+ **atrsize** will create a new, blank image called *infile.atr*. If
+ this file already exists, however, it will not be overwritten
+ (instead, **atrsize** will exit with a "file exists" message).
+
+ If *sectors* is given, the new image's size will be set to that many
+ sectors. If not given, the new image's size will be 720 sectors.
+
+ **-b** creates a new image with 128-byte sectors (single density)
+
+ **-B** creates a new image with 256-byte sectors (double density).
+
+ New images created with **atrsize** consist of a valid ATR header,
+ and sectors filled with zeroes. No boot sectors, directory, VTOC,
+ or filesystem are created. To use a blank image for file
+ storage, it must be formatted with an Atari DOS (either in an
+ emulator or with a real Atari via SIO2PC cable). If you're
+ trying to create a blank DOS 2.0S disk, use **axe -b**.
+
+NOTES
+=====
+
+- **atrsize** cannot change the sector size (density) of an image under
+ any circumstances. Only the sector count may be changed.
+
+- **atrsize** will fail if the input file doesn't have a valid ATR header.
+ If it's an XFD (raw) image, use **xfd2atr**\(1) to convert it to an ATR first.
+
+EXIT STATUS
+===========
+
+Exit status is zero for success, non-zero for failure.
+
+.. include:: manftr.rst
diff --git a/axe.1 b/axe.1
new file mode 100644
index 0000000..f26293b
--- /dev/null
+++ b/axe.1
@@ -0,0 +1,166 @@
+.\" 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 "AXE" 1 "2022-08-28" "0.2.0" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+axe \- ATR/XFD Editor
+.\" RST source for axe(1) man page. Convert with:
+.
+.\" rst2man.py axe.rst > axe.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fBaxe\fP [\fI\-alvtu\fP] [\fI\-b newimage\fP] [\fI\-D file\fP] [\fI\-x file\fP] [\fI\-w file\fP] [\fI\-c dirname\fP] [\fI\-t\fP] [\fI\-d sector\fP] [\fIimagefile\fP]
+.SH DESCRIPTION
+.sp
+\fBaxe\fP allows the user to access files stored inside a single\-density
+Atari DOS 2.0S disk image (ATR or XFD). It can list the directory,
+copy files into and out of the image, delete files in the image,
+create a new image (either blank or containing files), and dump various
+low\-level information about the image\(aqs filesystem structure.
+.SH OPTIONS
+.SS Standard Options:
+.INDENT 0.0
+.TP
+.B \-b \fIfilename\fP
+Create blank ATR image file called \fIfilename\fP\&. If \fIfilename\fP
+already exists, it will be overwritten with no warning.
+.TP
+.B \-c \fIdirectory\fP
+Create new ATR \fIimagefile\fP with contents of \fIdirectory\fP\&. If \fIimagefile\fP
+already exists, it will be overwritten with no warning. Similar to \fBtar cf\fP\&.
+.TP
+.B \-D \fIfile\fP
+Delete \fIfile\fP from \fIimagefile\fP\&. Ignores "locked" bit.
+.TP
+.B \-t \fIdirectory\fP
+Extract all files in image to \fIdirectory\fP, which will be created and
+must not already exist. Similar to \fBtar xf\fP\&.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \-u
+Unix <\-> Atari newline/EOL translation (use for text files only; breaks other file types).
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \-w \fIfile\fP
+Write \fIfile\fP to \fIimagefile\fP, overwrites if \fIfile\fP already exists. Ignores "locked" bit.
+.TP
+.B \-x \fIfile\fP
+Extract (read) file from \fIimagefile\fP, write to current directory.
+.UNINDENT
+.SS Debugging Options:
+.INDENT 0.0
+.TP
+.B \-a
+List all directory entries, even deleted/empty ones.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \-d \fIsector\fP
+Dump a sector in decimal, hex, and binary.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \-l
+Trace and print sector links for all files on disk.
+.TP
+.B \-v
+Dump VTOC (sector 360) in decimal, hex, and binary.
+.UNINDENT
+.SH LIMITATIONS
+.sp
+\fBaxe\fP is ancient code, from last century. It has various design
+flaws and bugs. At this point, it would be better to rewrite it from
+scratch than to try & fix the existing code.
+.sp
+Only Atari DOS 2.0S and 100% compatible single\-density disk images are
+supported. MyDOS images will work, but there\(aqs no support for MyDOS
+subdirectories. There\(aqs no support for e.g. SpartaDOS or Atari DOS
+3.0/4.0. Atari DOS 2.5 enhanced density images will work, the same
+way they work on DOS 2.0S: files using the extra sectors will not be
+readable, and \fBaxe\fP won\(aqt write to the extra sectors. Atari DOS 1.0
+images (which are \fIvery\fP rare) can at least have the directory listed,
+but I wouldn\(aqt recommend writing to them.
+.sp
+The "file locked" (aka read\-only) bit is ignored when writing, and
+there\(aqs no way to lock or unlock files, though locked files do appear
+with * next to the name in the directory listing.
+.sp
+It\(aqs possible to create files in a disk image with invalid filenames,
+e.g. beginning with a number, or containing punctuation. Atari DOS
+might or might not be able to even delete such files, but \fBaxe\fP will
+be able to if it happens.
+.sp
+One known bug: when writing a file to an image, if the file\(aqs size
+is a multiple of 125 (the number of data bytes per sector), an extra
+sector is allocated (with 0 data bytes in it). This doesn\(aqt actually
+cause a problem for Atari DOSes (it just wastes space on the disk),
+but it prevents simple\-minded XEX loaders like Fenders from being able
+to load the file (technically this is a bug in Fenders, too). Atari
+DOSes actually can create files like this if they\(aqre opened for
+append, then closed without writing new data.
+.sp
+\fBaxe\fP does nothing with the boot sectors (sectors 1\-3) of the disk
+image. When creating a new image, the boot sectors will be blank (all
+zeroes), meaning the disk won\(aqt be bootable. If DOS.SYS is written to
+an image with a DOS boot record, the boot record won\(aqt be updated with
+the first sector of DOS.SYS, so the disk won\(aqt be bootable.
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/axe.c b/axe.c
new file mode 100644
index 0000000..838a73c
--- /dev/null
+++ b/axe.c
@@ -0,0 +1,197 @@
+/* axe - manipulate atari disk images (dos 2.0s only for now)
+ *
+ * Usage: ataridir [-a] [-x filename] [-l] [-v] <disk_image.atr>
+ *
+ * (-a means list all directory entries, regardless of status byte)
+ * -l means dump sector link info for all filenumbers on disk
+ * -x means extract file from image
+ * -v means hex dump of VTOC sector
+ *
+ */
+
+/* indented with: indent -kr -nsai -nsaw -ts3 -i3 -br -brf -brs -cdw -ce */
+
+#include "axe.h"
+
+int all = 0, total_sec, translate = 0;
+
+
+int main(int argc, char *argv[]) {
+#ifdef DEBUG
+ int vtoc_dump = 0;
+#endif
+ int extract = 0, del = 0, dump_sec = 0, print = 1,
+ opt, write = 0, filenum = 0, i, j,
+ lng = 0, tar = 0, blank = 0, create = 0;
+ unsigned char buf[256], diskbuf[720 * 128 + 16];
+ char newblank[13];
+ unsigned char *debuf;
+ char filename[13] = { "\0" };
+ char fnbuf[13] = " \0";
+ DIR *credir;
+ struct dirent *nfile;
+
+ printf("axe (the ATR/XFD Editor) v%s, (c) B. Watson.\n", VERSION);
+ printf("Released under the WTFPL.\n\n");
+ if(argc < 2)
+ usage(argv[0]);
+ while((opt = getopt(argc, argv, "ab:c:lvd:D:x:tuw:")) != EOF) {
+ switch (opt) {
+ case 'a':
+ all = 1;
+ break;
+ case 'b':
+ /*write_blank_disk(optarg); */
+ blank = 1;
+ strcpy(newblank, optarg);
+ break;
+ case 'c':
+ print = 0;
+ create = 1;
+ strcpy(filename, optarg);
+ break;
+ case 'd':
+ dump_sec = atoi(optarg);
+ break;
+ case 'l':
+ lng = 1;
+ break;
+ case 'v':
+ dump_sec = 360;
+ break;
+ case 'D':
+ del = 1;
+ print = 0;
+ strcpy(filename, optarg);
+ break;
+ case 'x':
+ strcpy(filename, optarg);
+ extract = 1;
+ print = 0;
+ break;
+ case 'w':
+ write = 1;
+ print = 0;
+ strcpy(filename, optarg);
+ break;
+ case 't':
+ tar = 1;
+ break;
+ case 'u':
+ translate = 1;
+ break;
+ case '?':
+ case ':':
+ exit(1);
+ default:
+ printf("How'd I get here?\n");
+ exit(255);
+ }
+ }
+
+ if((create + write + extract + del + tar) > 1) {
+ printf("Only one of -t, -w, -c, -x, -D may be specified.\n");
+ exit(1);
+ }
+ if(blank)
+ write_blank_disk(newblank);
+ if(optind == argc) {
+ if(!blank)
+ usage(argv[0]);
+ exit(!blank);
+ }
+ printf("Using image file %s\n\n", argv[argc - 1]);
+#ifdef DEBUG
+ printf("-a:%d -l:%d\n -v:%d", all, lng, vtoc_dump);
+#endif
+ if(filename[0] && (!create)) {
+ parse_filename(filename, fnbuf);
+ }
+#ifdef DEBUG
+ printf("Passed filename %s, parsed as %s\n", filename, fnbuf);
+#endif
+ if(create)
+ write_blank_disk(argv[argc - 1]);
+ load_disk(diskbuf, argv[argc - 1]);
+/* if(write) {
+ if(strcmp(filename,argv[argc-1])==0) {
+ printf("No disk image name given - aborting\n");
+ exit(1);
+ }
+ write_file(diskbuf,filename);
+ write_disk(diskbuf,argv[argc-1]);
+ exit(0);
+ } */
+#ifdef DEBUG
+ printf("-t=%d\n", tar);
+#endif
+ if(create) {
+ char msgbuf[256];
+ if((credir = opendir(filename)) == NULL) {
+ sprintf(msgbuf, "can't open directory %s", filename);
+ perror(msgbuf);
+ exit(1);
+ }
+ while((nfile = readdir(credir)) != NULL) {
+ if(nfile->d_name[0] == '.')
+ continue;
+ write_file(diskbuf, nfile->d_name);
+ }
+ printf("Done.\n");
+ write_disk(diskbuf, argv[argc - 1]);
+ exit(0);
+ }
+
+
+ if(tar) {
+ make_dir(argv[argc - 1]);
+ }
+
+ for (i = 361; i < 369; i++) {
+ read_sector(diskbuf, i, buf);
+ for (j = 0; j < 128; j += 16) {
+ debuf = buf + j;
+ if((tar) && (debuf[0] & 64) && !(debuf[0] & 128)) {
+ strncpy(fnbuf, (char *)(debuf + 5), 11);
+ make_filename(fnbuf, filename);
+ dump_file(diskbuf, debuf, filename);
+ }
+ if(print && !(debuf[0] & 128))
+ print_entry(debuf);
+#ifdef DEBUG
+ printf("strncmp: %s, %s\n", fnbuf, debuf + 5);
+#endif
+ if((strncmp(fnbuf, (char *)(debuf + 5), 11) == 0) && !(debuf[0] & 128)) {
+#ifdef DEBUG
+ printf("matched filename\n");
+#endif
+ if(extract)
+ dump_file(diskbuf, debuf, filename);
+ if(del || write) {
+ traverse_file(diskbuf, debuf, TF_DELETE);
+ debuf[0] |= 128;
+ write_sector(diskbuf, i, buf);
+ write_disk(diskbuf, argv[argc - 1]);
+ }
+ }
+
+ if(lng)
+ traverse_file(diskbuf, debuf, TF_PRINT);
+
+ filenum++;
+ }
+ }
+ if(print) {
+ read_sector(diskbuf, 360, buf);
+ i = buf[3] + 256 * buf[4];
+ printf("%03d FREE SECTORS\n", i);
+/* if(i!=(707-total_sec)) printf("Warning: VTOC free sectors != (707 - total_sec), %d vs. %d\n",i,707-total_sec);*/
+ }
+ if(dump_sec)
+ dump_sector(diskbuf, dump_sec);
+ if(write) {
+ write_file(diskbuf, filename);
+ write_disk(diskbuf, argv[argc - 1]);
+ }
+ exit(0);
+}
diff --git a/axe.h b/axe.h
new file mode 100644
index 0000000..1301d0c
--- /dev/null
+++ b/axe.h
@@ -0,0 +1,52 @@
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <dirent.h>
+
+extern char *optarg;
+extern int optind, optopt, opterr;
+
+/* struct atari_dirent {
+ unsigned char flag; // bit 7=deleted 6=normal 5=locked 0=open4write
+ unsigned char countlo;
+ unsigned char counthi;
+ unsigned char startlo;
+ unsigned char starthi;
+ char namelo[8];
+ char namehi[3];
+};
+*/
+
+#define TF_PRINT 1
+#define TF_DELETE 2
+
+int load_disk(unsigned char *disk, char *);
+int write_disk(unsigned char *disk, char *);
+
+int get_confirm(char *);
+void usage(char *);
+void print_entry(unsigned char *buf);
+/*void dump_links(unsigned char *disk,unsigned char *ent);*/
+void traverse_file(unsigned char *disk, unsigned char *ent, int ACTION);
+void dump_file(unsigned char *disk, unsigned char *ent, char *);
+int parse_filename(char *filename, char *result);
+int make_filename(char *filename, char *result);
+int make_dir(char *filename);
+void dump_sector(unsigned char *disk, int);
+void read_sector(unsigned char *disk, int, unsigned char *buf);
+void write_sector(unsigned char *disk, int, unsigned char *buf);
+void write_blank_disk(char *filename);
+void write_file(unsigned char *disk, char *filename);
+/*int check_dir(unsigned char *disk,char *filename);*/
+int get_dentry(unsigned char *disk);
+int get_free_sector(unsigned char *disk);
+void mark_used(unsigned char *vtoc_sector, int sector);
+void mark_free(unsigned char *vtoc_sector, int sector);
+int vtoc_byte(int sector);
+int vtoc_bit(int sector);
diff --git a/axe.rst b/axe.rst
new file mode 100644
index 0000000..01e8a10
--- /dev/null
+++ b/axe.rst
@@ -0,0 +1,114 @@
+.. RST source for axe(1) man page. Convert with:
+.. rst2man.py axe.rst > axe.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+===
+axe
+===
+
+--------------
+ATR/XFD Editor
+--------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+**axe** [*-alvtu*] [*-b newimage*] [*-D file*] [*-x file*] [*-w file*] [*-c dirname*] [*-t*] [*-d sector*] [*imagefile*]
+
+DESCRIPTION
+===========
+
+**axe** allows the user to access files stored inside a single-density
+Atari DOS 2.0S disk image (ATR or XFD). It can list the directory,
+copy files into and out of the image, delete files in the image,
+create a new image (either blank or containing files), and dump various
+low-level information about the image's filesystem structure.
+
+OPTIONS
+=======
+
+Standard Options:
+-----------------
+
+-b *filename*
+ Create blank ATR image file called *filename*. If *filename*
+ already exists, it will be overwritten with no warning.
+
+-c *directory*
+ Create new ATR *imagefile* with contents of *directory*. If *imagefile*
+ already exists, it will be overwritten with no warning. Similar to **tar cf**.
+
+-D *file*
+ Delete *file* from *imagefile*. Ignores "locked" bit.
+
+-t *directory*
+ Extract all files in image to *directory*, which will be created and
+ must not already exist. Similar to **tar xf**.
+
+-u
+ Unix <-> Atari newline/EOL translation (use for text files only; breaks other file types).
+
+-w *file*
+ Write *file* to *imagefile*, overwrites if *file* already exists. Ignores "locked" bit.
+
+-x *file*
+ Extract (read) file from *imagefile*, write to current directory.
+
+Debugging Options:
+------------------
+
+-a
+ List all directory entries, even deleted/empty ones.
+
+-d *sector*
+ Dump a sector in decimal, hex, and binary.
+
+-l
+ Trace and print sector links for all files on disk.
+
+-v
+ Dump VTOC (sector 360) in decimal, hex, and binary.
+
+LIMITATIONS
+===========
+
+**axe** is ancient code, from last century. It has various design
+flaws and bugs. At this point, it would be better to rewrite it from
+scratch than to try & fix the existing code.
+
+Only Atari DOS 2.0S and 100% compatible single-density disk images are
+supported. MyDOS images will work, but there's no support for MyDOS
+subdirectories. There's no support for e.g. SpartaDOS or Atari DOS
+3.0/4.0. Atari DOS 2.5 enhanced density images will work, the same
+way they work on DOS 2.0S: files using the extra sectors will not be
+readable, and **axe** won't write to the extra sectors. Atari DOS 1.0
+images (which are *very* rare) can at least have the directory listed,
+but I wouldn't recommend writing to them.
+
+The "file locked" (aka read-only) bit is ignored when writing, and
+there's no way to lock or unlock files, though locked files do appear
+with \* next to the name in the directory listing.
+
+It's possible to create files in a disk image with invalid filenames,
+e.g. beginning with a number, or containing punctuation. Atari DOS
+might or might not be able to even delete such files, but **axe** will
+be able to if it happens.
+
+One known bug: when writing a file to an image, if the file's size
+is a multiple of 125 (the number of data bytes per sector), an extra
+sector is allocated (with 0 data bytes in it). This doesn't actually
+cause a problem for Atari DOSes (it just wastes space on the disk),
+but it prevents simple-minded XEX loaders like Fenders from being able
+to load the file (technically this is a bug in Fenders, too). Atari
+DOSes actually can create files like this if they're opened for
+append, then closed without writing new data.
+
+**axe** does nothing with the boot sectors (sectors 1-3) of the disk
+image. When creating a new image, the boot sectors will be blank (all
+zeroes), meaning the disk won't be bootable. If DOS.SYS is written to
+an image with a DOS boot record, the boot record won't be updated with
+the first sector of DOS.SYS, so the disk won't be bootable.
+
+.. include:: manftr.rst
diff --git a/axelib.c b/axelib.c
new file mode 100644
index 0000000..2b6e66b
--- /dev/null
+++ b/axelib.c
@@ -0,0 +1,546 @@
+#include "axe.h"
+
+extern int all, total_sec, translate;
+
+static int atr_offset;
+
+int load_disk(unsigned char *diskbuf, char *filename) {
+ FILE *file;
+ int vtoc_sec;
+ unsigned char tmpbuf[16];
+ char tmp[128];
+
+ if((file = fopen(filename, "r")) == NULL) {
+ sprintf(tmp, "load_disk(): %s", filename);
+ perror(tmp);
+ exit(1);
+ }
+ fread(tmpbuf, 16, 1, file);
+ atr_offset = 16;
+ memcpy(diskbuf, tmpbuf, 16);
+ if((tmpbuf[0] != 0x96) || (tmpbuf[1] != 0x02)) { /* not an atr */
+ atr_offset = 0;
+ rewind(file);
+ total_sec = 720;
+ }
+
+ /* do some sanity checking of images: */
+ if(atr_offset) {
+ total_sec = (diskbuf[2] + 256 * diskbuf[3]) >> 3;
+ if(!total_sec)
+ total_sec = (diskbuf[6] + 256 * diskbuf[7]) * 8192;
+ if(total_sec < 720) {
+ printf("Total sectors (%d) < 720 - aborting\n", total_sec);
+ exit(1);
+ }
+ if(total_sec > 720) {
+ printf
+ ("Warning: Total sectors (%d) > 720 - only reading first 720 sectors\n",
+ total_sec);
+ }
+ if((diskbuf[4] + 256 * diskbuf[5]) != 128) {
+ printf("Fatal: sector size not 128 bytes!\n");
+ exit(1);
+ }
+ } else {
+ printf
+ ("Warning: blindly assuming xfd image is 720 sectors, 128 bytes/sector!\n");
+ }
+
+ if((total_sec = fread(diskbuf + atr_offset, 128, 720, file)) < 720) {
+ printf("Error reading image file, only got %d sectors, aborting\n", total_sec);
+ perror("fread");
+ exit(1);
+ }
+
+ vtoc_sec =
+ diskbuf[atr_offset + 128 * 359 + 1] + 256 * diskbuf[atr_offset +
+ 128 * 359 + 2];
+ if(vtoc_sec != 707)
+ printf
+ ("Warning: VTOC sector count (%d) not 707, are you sure this is an AtariDOS 2.0 image?\n",
+ vtoc_sec);
+ return 0;
+}
+
+int write_disk(unsigned char *diskbuf, char *filename) {
+ FILE *file;
+ unsigned char tmpbuf[16];
+
+ file = fopen(filename, "w");
+ memcpy(tmpbuf, diskbuf, 16);
+ atr_offset = 0;
+ if((tmpbuf[0] == 0x96) && (tmpbuf[1] == 0x02)) { /* is an atr */
+ fwrite(tmpbuf, 16, 1, file);
+ atr_offset = 16;
+ }
+ fwrite(diskbuf + atr_offset, 128, 720, file);
+ fclose(file);
+ return 0;
+}
+
+
+void usage(char *name) {
+ printf
+ ("Usage: %s [options] [-D filename] [-w filename] [-e filename]\n\t\t [-b filename] [-c dirname] [-d sector] disk_image\n\n",
+ name);
+
+ printf("\t-a\tlist all dir entries, even deleted/empty ones\n");
+ printf("\t-l\ttrace & print links for all files on disk\n");
+ printf("\t-v\tdump VTOC (sector 360) in decimal, hex, and binary\n");
+ printf
+ ("\t-t\t'tar xf' style - create dir filled with files in image\n");
+ printf("\t-u\tUn*x <-> Atari newline/EOL translation\n");
+ printf("\t-D\tdelete file from image\n");
+ printf("\t-w\twrite file to image\n");
+ printf("\t-c\tcreate new image from directory\n");
+ printf("\t-x\textract (read) file from image, write to current dir\n");
+ printf("\t-b\tcreate blank image called 'filename'\n");
+ printf("\t-d\tdump a sector in decimal, hex, and binary\n");
+ printf
+ ("\ndisk_image must be a 720 sector, single density .atr or .xfd image\n");
+ printf("\tin Atari DOS 2.0 or compatible format\n");
+ printf
+ ("\nAll UN*X filenames must conform to Atari 8.3 filename.ext format\n");
+ printf("\t(though they are NOT treated case-sensitively)\n");
+ printf("\nEnjoy!\n\n");
+ exit(1);
+}
+
+void print_entry(unsigned char *buf) {
+#ifdef DEBUG
+ static int ents;
+ int i;
+#endif
+ int s;
+ unsigned char tmpbuf[32];
+
+ memcpy(tmpbuf, buf, 16);
+ tmpbuf[16] = '\0';
+#ifdef DEBUG
+ printf("successfully read dir. entry #%d\n", ents++);
+#endif
+ if((tmpbuf[0] & 64) || all) {
+#ifdef DEBUG
+ for (i = 0; i < 16; i++) {
+ printf(" 0x%X", tmpbuf[i]);
+ }
+#endif
+ total_sec += (s = tmpbuf[1] + 256 * tmpbuf[2]);
+ printf("%c %s\t%03d\n", (tmpbuf[0] & 32) ? '*' : ' ', tmpbuf + 5,
+ s);
+ }
+}
+
+void read_sector(unsigned char *disk, int sector, unsigned char *buf) {
+ memcpy(buf, disk + (sector - 1) * 128 + atr_offset, 128);
+}
+
+void write_sector(unsigned char *disk, int sector, unsigned char *buf) {
+ memcpy(disk + (sector - 1) * 128 + atr_offset, buf, 128);
+}
+
+void traverse_file(unsigned char *disk, unsigned char *ent, int action) {
+ int i, next_sec, data_bytes, curfile;
+ unsigned char buf[128];
+ unsigned char vtoc[128];
+
+#ifdef DEBUG
+ printf("traverse_file called with %s\n",
+ (action - 1) ? "TF_DELETE" : "TF_PRINT");
+#endif
+
+ if(action == TF_DELETE)
+ read_sector(disk, 360, vtoc);
+
+ next_sec = ent[3] + 256 * ent[4];
+ while(next_sec) {
+ if(action == TF_DELETE)
+ mark_free(vtoc, next_sec);
+ if(action == TF_PRINT)
+ printf("%d: ", next_sec);
+ read_sector(disk, next_sec, buf);
+ data_bytes = buf[127];
+ curfile = buf[125] >> 2;
+ next_sec = buf[126] + 256 * (buf[125] & 3);
+ if(action == TF_PRINT)
+ printf("data_bytes=%d filenum=%d next_sec=%d\n", data_bytes,
+ curfile, next_sec);
+ }
+ if(action == TF_DELETE) {
+ i = (vtoc[3] + 256 * vtoc[4]);
+#ifdef DEBUG
+ printf("deleting file, old vtoc free count=%d", i);
+#endif
+ i += ent[1] + 256 * ent[2];
+#ifdef DEBUG
+ printf(", new=%d\n", i);
+#endif
+ vtoc[3] = i % 256;
+ vtoc[4] = i / 256;
+ write_sector(disk, 360, vtoc);
+ if(action == TF_DELETE) {
+ printf("Deleted file, now %d free sectors on image\n", i);
+ }
+ }
+}
+
+void dump_file(unsigned char *disk, unsigned char *ent, char *outfile) {
+#ifdef DEBUG
+ int curfile;
+#endif
+ int i, next_sec, data_bytes, total_written = 0;
+ unsigned char buf[128];
+ FILE *out;
+
+/* fseek(handle,atr_seek(361)+16*filenum,SEEK_SET);
+ fread(buf,16,1,handle); */
+ next_sec = ent[3] + 256 * ent[4];
+ out = fopen(outfile, "wb");
+/* #ifdef DEBUG
+ printf("First sector of filenum %d, ",filenum);
+ #endif */
+ while(next_sec) {
+#ifdef DEBUG
+ printf("%d: ", next_sec);
+#endif
+ read_sector(disk, next_sec, buf);
+ data_bytes = buf[127];
+ next_sec = buf[126] + 256 * (buf[125] & 3);
+#ifdef DEBUG
+ curfile = buf[125] >> 2;
+ printf("data_bytes=%d filenum=%d next_sec=%d\n", data_bytes, curfile,
+ next_sec);
+#endif
+ if(translate)
+ for (i = 0; i <= data_bytes; i++)
+ if(buf[i] == 0x9b)
+ buf[i] = '\n';
+ fwrite(buf, data_bytes, 1, out);
+ total_written += data_bytes;
+ fflush(out);
+ }
+ printf("Wrote %d bytes to file %s\n", total_written, outfile);
+}
+
+
+int parse_filename(char *filename, char *result) {
+ int i = 0, j = 0;
+ char ext[4] = " \0";
+ char fn[9] = " \0";
+#ifdef DEBUG
+ printf("parse_filename called, filename=%s\n", filename);
+#endif
+ if(strlen(filename) > 12) {
+ printf("filename too long: %s\n", filename);
+ exit(1);
+ }
+/* if(strlen(strstr(filename,"."))>4) {
+ printf("extension too long: %s\n",filename);
+ exit(1);
+ } */
+ while((filename[i]) && (filename[i] != '.')) {
+ fn[i++] = toupper(filename[i]);
+ }
+ if(filename[i] == '.')
+ while(filename[++i]) {
+ ext[j++] = toupper(filename[i]);
+ }
+
+ strcpy(result, fn);
+ strncat(result, ext, 3);
+#ifdef DEBUG
+ printf("parse_filename exiting, result=%s\n", result);
+#endif
+ return (0);
+}
+
+int make_filename(char *filename, char *result) {
+ int i = 0, j = 8;
+ char fn[13] = { '\0' };
+
+ while((filename[i] && filename[i] != ' ') && (i < 8)) {
+ fn[i++] = tolower(filename[i]);
+ }
+ if(filename[j] != ' ') {
+ fn[i++] = '.';
+ while(filename[j]) {
+ fn[i++] = tolower(filename[j++]);
+ }
+ }
+#ifdef DEBUG
+ printf("made filename %s from %s\n", fn, filename);
+#endif
+ strcpy(result, fn);
+ return 0;
+}
+
+int make_dir(char *filename) {
+ char tmp[15] = { '\0' };
+ char *p;
+
+ strcpy(tmp, filename);
+ if(!(p = strrchr(tmp, '.'))) {
+ strcat(tmp, ".dir");
+ } else {
+ *p = '\0';
+ }
+#ifdef DEBUG
+ printf("made dir: %s\n", tmp);
+#endif
+ if(mkdir(tmp, 0777)) {
+ perror("Can't make directory: ");
+ exit(1);
+ }
+ chdir(tmp);
+ return 0;
+}
+
+void dump_sector(unsigned char *disk, int sector) {
+ unsigned char buf[256];
+ int i, j, k;
+ printf("Dump of sector %d in decimal:\n", sector);
+ read_sector(disk, sector, buf);
+ for (i = 0; i < 128; i += 16) {
+ printf("%03d: ", i);
+ for (j = 0; j < 16; j++) {
+ printf("%03d ", buf[i + j]);
+ }
+ printf("\n");
+ }
+ printf("Dump of sector %d in hex:\n", sector);
+ for (i = 0; i < 128; i += 16) {
+ printf("%03x: ", i);
+ for (j = 0; j < 16; j++) {
+ printf(" %02x ", buf[i + j]);
+ }
+ printf("\n");
+ }
+ printf("Dump of sector %d in binary:\n", sector);
+ for (i = 0; i < 128; i += 16) {
+ printf("%03x: ", i);
+ for (j = 0; j < 16; j++) {
+ for (k = 7; k > -1; k--) {
+ if(buf[i + j] & (1 << k))
+ printf("1");
+ else
+ printf("0");
+ }
+ printf(" ");
+ if(j == 7)
+ printf("\n ");
+ }
+ printf("\n");
+ }
+}
+
+
+/* please, ignore the ugliness of this function:*/
+void write_blank_disk(char *filename) {
+ unsigned char atr_header[16] = { 0x96, 0x02, 0x80, 0x16, 0x80, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ };
+ unsigned char disk[128 * 720 + 16] = { '\0' };
+ int i;
+ unsigned char vtoc[128] =
+ { 0x02, 0xc3, 0x02, 0xc3, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f
+ };
+
+ for (i = 11; i < 100; i++)
+ vtoc[i] = 0xff;
+ vtoc[55] = 0x00;
+ vtoc[56] = 0x7f;
+ for (i = 100; i < 128; i++)
+ vtoc[i] = 0x00;
+ atr_offset = 16;
+ memcpy(disk, atr_header, 16);
+ write_sector(disk, 360, vtoc);
+ write_disk(disk, filename);
+ printf("Wrote formatted blank image to %s\n", filename);
+}
+
+static void xlate_eofs(unsigned char *buf, int len) {
+ int i;
+ for (i = 0; i < len; i++)
+ if(buf[i] == '\n')
+ buf[i] = 0x9b;
+}
+
+void write_file(unsigned char *disk, char *filename) {
+ unsigned char vtoc[128];
+ unsigned char buf[128];
+ char fnbuf[15];
+/* unsigned char dentry[16];*/
+ unsigned char *dindex;
+ int i, dentryno, first_sec, secno, next_sec, count = 0, bytes;
+ FILE *handle;
+
+ printf("Writing %s to image...", filename);
+ parse_filename(filename, fnbuf);
+ if((handle = fopen(filename, "r")) == NULL) {
+ perror("write_file::fopen");
+ exit(1);
+ }
+#ifdef DEBUG
+ printf("write_file: parsed %s as \"%s\"\n", filename, fnbuf);
+#endif
+ if((dentryno = get_dentry(disk)) == -1) {
+ printf("write_file: no free directory entries on image\n");
+ exit(1);
+ }
+#ifdef DEBUG
+ printf("dentryno=%d\n", dentryno);
+#endif
+ read_sector(disk, 360, vtoc);
+ if(!(first_sec = secno = get_free_sector(vtoc))) {
+ printf("write_file: no free sectors in image\n");
+ exit(1);
+ }
+#ifdef DEBUG
+ printf("first_sec=%d\n", first_sec);
+#endif
+ do {
+ int tmp, eof = 0;
+ count++;
+ bytes = fread(buf, 1, 125, handle);
+
+ if(bytes == 125) {
+ /* test for EOF on handle */
+ tmp = fgetc(handle);
+ if(tmp == EOF)
+ eof = 1;
+ else
+ ungetc(tmp, handle);
+ }
+
+ if(translate) xlate_eofs(buf, 125);
+ mark_used(vtoc, secno);
+
+ if(bytes == 125 && !eof) {
+ /* full sector, not at EOF */
+ next_sec = get_free_sector(vtoc);
+ buf[125] = (dentryno << 2) | (next_sec >> 8);
+ buf[126] = next_sec & 255;
+ buf[127] = 125;
+ write_sector(disk, secno, buf);
+ secno = next_sec;
+ } else {
+ /* partial sector or full sector at EOF */
+ buf[125] = dentryno << 2;
+ buf[126] = 0;
+ buf[127] = bytes;
+ write_sector(disk, secno, buf);
+ next_sec = 0;
+ }
+ } while(next_sec);
+ /* update dentry */
+ read_sector(disk, 361 + (dentryno / 8), buf);
+ dindex = buf + (dentryno % 8) * 16;
+ dindex[0] = 66; /* taken from a dos 2.0s disk, hope its right */
+ dindex[1] = count & 255;
+ dindex[2] = count / 256;
+ dindex[3] = first_sec & 255;
+ dindex[4] = first_sec / 256;
+ memcpy(dindex + 5, fnbuf, 11);
+ write_sector(disk, 361 + (dentryno / 8), buf);
+ /*all thats left to do is update free sector count in vtoc: */
+ i = (vtoc[3] + 256 * vtoc[4]) - count;
+ vtoc[3] = i & 255;
+ vtoc[4] = i / 256;
+ write_sector(disk, 360, vtoc);
+ printf("Wrote %d sectors.\n", count);
+}
+
+int get_dentry(unsigned char *disk) {
+ int i = 361, j, dentryno = 0, done = 0;
+ unsigned char buf[128];
+
+ while(!done) {
+ read_sector(disk, i, buf);
+ for (j = 0; j < 128; j += 16) {
+ if((buf[j] == 0) || (buf[j] & 128)) {
+ return dentryno;
+ }
+ dentryno++;
+ }
+ done = (++i == 369);
+ }
+ return -1;
+}
+
+int get_free_sector(unsigned char *vtoc) {
+ int i;
+
+ for (i = 1; i < 720; i++) { /* atari DOS doesn't use sector 720 */
+ if(vtoc[vtoc_byte(i)] & vtoc_bit(i))
+ return i;
+ }
+
+ return 0;
+}
+
+int vtoc_byte(int sector) {
+ return 10 + (sector) / 8;
+}
+
+int vtoc_bit(int sector) {
+ return 128 >> ((sector) % 8);
+}
+
+void mark_used(unsigned char *vtoc_sector, int sector) {
+ vtoc_sector[vtoc_byte(sector)] &= ~vtoc_bit(sector);
+}
+
+void mark_free(unsigned char *vtoc_sector, int sector) {
+ vtoc_sector[vtoc_byte(sector)] |= vtoc_bit(sector);
+}
+
+/*int get_confirm(char *prompt) {
+ printf("%s[y/N]? ",prompt);
+ return ((toupper(getc(stdin)))=='Y');
+}*/
+
+int check_dir(unsigned char *disk, char *filename) {
+ int i = 361, j, done = 0, dentryno = 0;
+ unsigned char buf[128];
+
+ printf("check_dir: passed %s\n", filename);
+ while(!done) {
+ read_sector(disk, i, buf);
+ for (j = 0; j < 128; j += 16) {
+ if((strncmp(filename, (char*)(buf + j + 5), 11) == 0)) {
+ return dentryno;
+ }
+ dentryno++;
+ }
+ done = (++i == 369);
+ }
+
+ return -1;
+}
+
+/* these routines suck, and are not actually used any more.
+ * maybe someday they'll become useful again.
+ *
+ int delete_file(unsigned char *disk,char *filename) {
+ int first_sec,next_sec,i,dentryno;
+ char fnbuf[15];
+ char buf[128];
+ char *debuf;
+ char vtoc[128];
+
+ printf("delete_file doesn't work yet. passed: %s\n",filename);
+ parse_filename(filename,fnbuf);
+ if((dentryno=check_dir(disk,fnbuf))==-1) {
+ printf("delete_file: file %s not found on image\n",fnbuf);
+ exit(1);
+ }
+ printf("delete_file: would delete %s\n",fnbuf);
+ read_sector(disk,361+dentryno/8,buf);
+ read_sector(disk,360,vtoc);
+ debuf=buf+(dentryno%8)*16;
+ next_sec=debuf[3]+256*debuf[4];
+ traverse_file(disk,debuf,TF_DELETE);
+ return 0;
+}
+*/
diff --git a/blob2c.1 b/blob2c.1
new file mode 100644
index 0000000..6553931
--- /dev/null
+++ b/blob2c.1
@@ -0,0 +1,137 @@
+.\" 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 "BLOB2C" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py blob2c.rst > blob2c.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+blob2c \fIblobfile\fP > \fIoutput.c\fP 2> \fIoutput.h\fP
+.SH DESCRIPTION
+.sp
+\fBblob2c\fP prints a C source file to standard output, containing
+\fIan unsigned char\fP array, initialized to the contents of \fIblobfile\fP,
+and an \fIint\fP, initialized to the length of \fIblobfile\fP in bytes.
+.sp
+The name of the array is based on the input filename, with
+non\-alphanumeric characters replaced by underscores. The name of the
+int is the array name, plus the string \fB_len\fP\&.
+.sp
+Also, a header file containing a pair of extern declarations is
+printed to standard error output. This header may be included multiple
+times in the same translation unit, since it contains "guard"
+preprocessor code to prevent multiple declarations.
+.sp
+\fBblob2c\fP takes no options, and requires exactly one argument (the
+input filename \fIblobfile\fP).
+.sp
+Exit status is zero for success and non\-zero for failure. Error
+messages are printed to standard error output as preprocessor
+\fB#error\fP directives, since standard error is expected to be
+redirected to a header file. The \fB#error\fP directives will cause the
+error messages to be printed when the header file is compiled.
+.sp
+Although it\(aqs distributed with the author\(aqs Atari 8\-bit
+utilities, there\(aqs nothing Atari\-specific about \fBblob2c\fP\&. It could be
+useful for any situation where you need to include a file\(aqs contents
+as an array in a C program.
+.SH EXAMPLE
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+$ echo "Hello, World." > hello.bin
+
+$ blob2c hello.bin >hello.c 2>hello.h
+
+## check exit status (0=success)
+$ echo $?
+0
+
+$ cat hello.h
+/* C header created by blob2c from input file hello.bin */
+
+#ifndef hello_bin_H
+#define hello_bin_H
+
+extern unsigned char hello_bin[];
+extern int hello_bin_len;
+
+#endif /* hello_bin_H */
+
+$ cat hello.c
+
+/* C source created by blob2c from input file hello.bin */
+
+unsigned char hello_bin[] = {
+ /* 0 */ 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57, /* Hello, W */
+ /* 8 */ 0x6f,0x72,0x6c,0x64,0x2e,0x0a /* orld.. */
+}; /* hello_bin */
+
+int hello_bin_len = 14;
+.ft P
+.fi
+.UNINDENT
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/blob2c.c b/blob2c.c
new file mode 100644
index 0000000..779b79f
--- /dev/null
+++ b/blob2c.c
@@ -0,0 +1,117 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+
+/* since we're emitting C source, the comments containing the
+ ASCII dump better not contain C comment markers! */
+void fixasc(char *asc, int len) {
+ int j;
+
+ for(j=0; j<(len-1); j++) {
+ /* slash-star or slash-slash become slash-dot */
+ if(asc[j] == '/' && (asc[j+1] == '*' || asc[j+1] == '/'))
+ asc[j+1] = '.';
+ /* star-slash becomes dot-slash */
+ else if(asc[j] == '*' && asc[j+1] == '/')
+ asc[j] = '.';
+ }
+}
+
+int main(int argc, char **argv) {
+ int i, j, count = 0;
+ char *p, asc[9];
+ FILE *in;
+
+ if(argc != 2) {
+ fprintf(stderr,
+ "#error usage: %s blobfile >blobfile.c 2>blobfile.h\n", argv[0]);
+ exit(1);
+ }
+
+ if( !(in = fopen(argv[1], "rb")) ) {
+ fprintf(stderr, "/* %s: %s */\n", argv[1], strerror(errno));
+ exit(1);
+ }
+
+ printf("/* C source created by blob2c from input file %s */\n\n", argv[1]);
+ fprintf(stderr,
+ "/* C header created by blob2c from input file %s */\n\n", argv[1]);
+
+ for(p = argv[1]; *p; p++)
+ if(!isalnum(*p)) *p = '_';
+
+ fprintf(stderr,
+ "#ifndef %s_H\n"
+ "#define %s_H\n\n",
+ argv[1], argv[1]);
+
+ /* start array definition */
+ printf("unsigned char %s[] = {", argv[1]);
+
+ asc[8] = '\0';
+
+ /* read/process each input byte in loop, until EOF */
+ while( (i = getc(in)) != EOF ) {
+ if(count) putchar(',');
+
+ if(count % 8 == 0) {
+ if(count) {
+ /* fix & print ASCII dump */
+ fixasc(asc, 8);
+ printf(" /* %s */", asc);
+ }
+
+ /* start next line */
+ printf("\n\t/* %6d */ ", count);
+ }
+
+ /* store this byte of ASCII dump */
+ asc[count % 8] = isprint(i) ? i : '.';
+
+ /* print this byte of hex data */
+ printf("0x%02x", i);
+
+ count++;
+ }
+
+ /* fix ASCII dump for last line */
+ i = count % 8;
+ if(!i) i = 8;
+ fixasc(asc, i);
+ asc[i] = '\0';
+
+ /* line it up with the previous lines */
+ j = (8 - i) * 5;
+ for(i=0; i<j; i++)
+ putchar(' ');
+
+ /* print it */
+ printf(" /* %s */", asc);
+
+ /* end of array definition */
+ printf("\n}; /* %s */\n\n", argv[1]);
+
+ /* array_len definition */
+ printf("int %s_len = %d;\n", argv[1], count);
+
+ /* check for read errors */
+ i = 0;
+ if(ferror(in)) {
+ fprintf(stderr, "#error %s: %s\n", argv[1], strerror(errno));
+ i = 1;
+ }
+
+ fclose(in);
+
+ /* extern declarations to stderr */
+ fprintf(stderr,
+ "extern unsigned char %s[];\nextern int %s_len;\n",
+ argv[1], argv[1]);
+
+ fprintf(stderr, "\n#endif /* %s_H */\n", argv[1]);
+
+ return i;
+}
diff --git a/blob2c.rst b/blob2c.rst
new file mode 100644
index 0000000..5db5cc2
--- /dev/null
+++ b/blob2c.rst
@@ -0,0 +1,85 @@
+.. RST source for blob2c(1) man page. Convert with:
+.. rst2man.py blob2c.rst > blob2c.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+======
+blob2c
+======
+
+---------------------------------------------------
+Create C source and header files from a binary file
+---------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+blob2c *blobfile* > *output.c* 2> *output.h*
+
+DESCRIPTION
+===========
+
+**blob2c** prints a C source file to standard output, containing
+*an unsigned char* array, initialized to the contents of *blobfile*,
+and an *int*, initialized to the length of *blobfile* in bytes.
+
+The name of the array is based on the input filename, with
+non-alphanumeric characters replaced by underscores. The name of the
+int is the array name, plus the string **_len**.
+
+Also, a header file containing a pair of extern declarations is
+printed to standard error output. This header may be included multiple
+times in the same translation unit, since it contains "guard"
+preprocessor code to prevent multiple declarations.
+
+**blob2c** takes no options, and requires exactly one argument (the
+input filename *blobfile*).
+
+Exit status is zero for success and non-zero for failure. Error
+messages are printed to standard error output as preprocessor
+**#error** directives, since standard error is expected to be
+redirected to a header file. The **#error** directives will cause the
+error messages to be printed when the header file is compiled.
+
+Although it's distributed with the author's Atari 8-bit
+utilities, there's nothing Atari-specific about **blob2c**. It could be
+useful for any situation where you need to include a file's contents
+as an array in a C program.
+
+EXAMPLE
+=======
+
+::
+
+ $ echo "Hello, World." > hello.bin
+
+ $ blob2c hello.bin >hello.c 2>hello.h
+
+ ## check exit status (0=success)
+ $ echo $?
+ 0
+
+ $ cat hello.h
+ /* C header created by blob2c from input file hello.bin */
+
+ #ifndef hello_bin_H
+ #define hello_bin_H
+
+ extern unsigned char hello_bin[];
+ extern int hello_bin_len;
+
+ #endif /* hello_bin_H */
+
+ $ cat hello.c
+
+ /* C source created by blob2c from input file hello.bin */
+
+ unsigned char hello_bin[] = {
+ /* 0 */ 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57, /* Hello, W */
+ /* 8 */ 0x6f,0x72,0x6c,0x64,0x2e,0x0a /* orld.. */
+ }; /* hello_bin */
+
+ int hello_bin_len = 14;
+
+.. include:: manftr.rst
diff --git a/cart.c b/cart.c
new file mode 100644
index 0000000..09c1c12
--- /dev/null
+++ b/cart.c
@@ -0,0 +1,261 @@
+/* CART header support library */
+
+#include <string.h>
+#include <stdio.h>
+#include "cart.h"
+
+/* 20071227 bkw:
+ cart_types list was made from atari800-2.0.3/DOC/cart.txt
+ I didn't use any code from atari800, since it's GPL and this code
+ is WTFPL.
+
+ Format of CART header (from atari800 cart.txt):
+
+ first 4 bytes containing 'C' 'A' 'R' 'T'.
+ next 4 bytes containing cartridge type in MSB format (see the table below).
+ next 4 bytes containing cartridge checksum in MSB format (ROM only).
+ next 4 bytes are currently unused (zero).
+ followed immediately with the ROM data: 4, 8, 16, 32, 40, 64, 128, 256, 512
+ or 1024 kilobytes.
+*/
+
+/* 20071227 bkw: this list is complete as of Atari800 2.0.3 */
+/* 20220827 bkw: list now complete as of Atari800 5.0.0 */
+cart_t cart_types[] = {
+ /* Id */ /* Machine, Size, Name */
+ /* 0 */ { M_INVALID, 0, 0 }, /* 0 is invalid type */
+ /* 1 */ { M_ATARI8, 8, "Standard 8 KB" },
+ /* 2 */ { M_ATARI8, 16, "Standard 16 KB" },
+ /* 3 */ { M_ATARI8, 16, "OSS two chip 16 KB cartridge (034M)" },
+ /* 4 */ { M_5200, 32, "Standard 32 KB 5200" },
+ /* 5 */ { M_ATARI8, 32, "DB 32 KB" },
+ /* 6 */ { M_5200, 16, "Two chip 16 KB 5200" },
+ /* 7 */ { M_5200, 40, "Bounty Bob Strikes Back 40 KB 5200" },
+ /* 8 */ { M_ATARI8, 64, "64 KB Williams" },
+ /* 9 */ { M_ATARI8, 64, "Express 64 KB" },
+ /* 10 */ { M_ATARI8, 64, "Diamond 64 KB" },
+ /* 11 */ { M_ATARI8, 64, "SpartaDos X 64 KB" },
+ /* 12 */ { M_ATARI8, 32, "XEGS 32 KB" },
+ /* 13 */ { M_ATARI8, 64, "XEGS 64 KB (banks 0-7)" },
+ /* 14 */ { M_ATARI8, 128, "XEGS 128 KB" },
+ /* 15 */ { M_ATARI8, 16, "OSS one chip 16 KB" },
+ /* 16 */ { M_5200, 16, "One chip 16 KB 5200" },
+ /* 17 */ { M_ATARI8, 128, "Decoded Atrax 128 KB" },
+ /* 18 */ { M_ATARI8, 40, "Bounty Bob Strikes Back 40 KB" },
+ /* 19 */ { M_5200, 8, "Standard 8 KB 5200" },
+ /* 20 */ { M_5200, 4, "Standard 4 KB 5200" },
+ /* 21 */ { M_ATARI8, 8, "Right slot 8 KB" },
+ /* 22 */ { M_ATARI8, 32, "32 KB Williams" },
+ /* 23 */ { M_ATARI8, 256, "XEGS 256 KB" },
+ /* 24 */ { M_ATARI8, 512, "XEGS 512 KB" },
+ /* 25 */ { M_ATARI8, 1024, "XEGS 1 MB" },
+ /* 26 */ { M_ATARI8, 16, "MegaCart 16 KB" },
+ /* 27 */ { M_ATARI8, 32, "MegaCart 32 KB" },
+ /* 28 */ { M_ATARI8, 64, "MegaCart 64 KB" },
+ /* 29 */ { M_ATARI8, 128, "MegaCart 128 KB" },
+ /* 30 */ { M_ATARI8, 256, "MegaCart 256 KB" },
+ /* 31 */ { M_ATARI8, 512, "MegaCart 512 KB" },
+ /* 32 */ { M_ATARI8, 1024, "MegaCart 1 MB" },
+ /* 33 */ { M_ATARI8, 32, "Switchable XEGS 32 KB" },
+ /* 34 */ { M_ATARI8, 64, "Switchable XEGS 64 KB" },
+ /* 35 */ { M_ATARI8, 128, "Switchable XEGS 128 KB" },
+ /* 36 */ { M_ATARI8, 256, "Switchable XEGS 256 KB" },
+ /* 37 */ { M_ATARI8, 512, "Switchable XEGS 512 KB" },
+ /* 38 */ { M_ATARI8, 1024, "Switchable XEGS 1 MB" },
+ /* 39 */ { M_ATARI8, 8, "Phoenix 8 KB" },
+ /* 40 */ { M_ATARI8, 16, "Blizzard 16 KB" },
+ /* 41 */ { M_ATARI8, 128, "Atarimax 128 KB Flash" },
+ /* 42 */ { M_ATARI8, 1024, "Atarimax 1 MB Flash (old)" },
+ /* 43 */ { M_ATARI8, 128, "SpartaDos X 128 KB" },
+ /* 44 */ { M_ATARI8, 8, "OSS 8 KB" },
+ /* 45 */ { M_ATARI8, 16, "OSS two chip 16 KB (043M)" },
+ /* 46 */ { M_ATARI8, 4, "Blizzard 4 KB" },
+ /* 47 */ { M_ATARI8, 32, "AST 32 KB" },
+ /* 48 */ { M_ATARI8, 64, "Atrax SDX 64 KB" },
+ /* 49 */ { M_ATARI8, 128, "Atrax SDX 128 KB" },
+ /* 50 */ { M_ATARI8, 64, "Turbosoft 64 KB" },
+ /* 51 */ { M_ATARI8, 128, "Turbosoft 128 KB" },
+ /* 52 */ { M_ATARI8, 32, "Ultracart 32 KB" },
+ /* 53 */ { M_ATARI8, 8, "Low bank 8 KB" },
+ /* 54 */ { M_ATARI8, 128, "SIC! 128 KB" },
+ /* 55 */ { M_ATARI8, 256, "SIC! 256 KB" },
+ /* 56 */ { M_ATARI8, 512, "SIC! 512 KB" },
+ /* 57 */ { M_ATARI8, 2, "Standard 2 KB" },
+ /* 58 */ { M_ATARI8, 4, "Standard 4 KB" },
+ /* 59 */ { M_ATARI8, 4, "Right slot 4 KB" },
+ /* 60 */ { M_ATARI8, 32, "Blizzard 32 KB" },
+ /* 61 */ { M_ATARI8, 2048, "MegaMax 2 MB" },
+ /* 62 */ { M_ATARI8,131072, "The!Cart 128 MB" },
+ /* 63 */ { M_ATARI8, 4096, "Flash MegaCart 4 MB" },
+ /* 64 */ { M_ATARI8, 2048, "MegaCart 2 MB" },
+ /* 65 */ { M_ATARI8, 32768, "The!Cart 32 MB" },
+ /* 66 */ { M_ATARI8, 65536, "The!Cart 64 MB" },
+ /* 67 */ { M_ATARI8, 64, "XEGS 64 KB (banks 8-15)" },
+ /* 68 */ { M_ATARI8, 128, "Atrax 128 KB" },
+ /* 69 */ { M_ATARI8, 32, "aDawliah 32 KB" },
+ /* 70 */ { M_ATARI8, 64, "aDawliah 64 KB" },
+ /* 71 */ { M_5200, 64, "Super Cart 64 KB 5200" },
+ /* 72 */ { M_5200, 128, "Super Cart 128 KB 5200" },
+ /* 73 */ { M_5200, 256, "Super Cart 256 KB 5200" },
+ /* 74 */ { M_5200, 512, "Super Cart 512 KB 5200" },
+ /* 75 */ { M_ATARI8, 1024, "Atarimax 1 MB Flash (new)" },
+};
+
+const int MAX_CART_TYPE = (sizeof(cart_types)/sizeof(cart_t))-1;
+
+/* helper functions.
+ get_msb_dword() and put_msb_dword() are written the way they are in
+ order to avoid endian-ness issues caused by casts or calls to some
+ endian-aware function like htonl().
+*/
+
+static unsigned int get_msb_dword(unsigned char *cart, int offset) {
+ return
+ (cart[offset] << 24) |
+ (cart[offset+1] << 16) |
+ (cart[offset+2] << 8) |
+ (cart[offset+3]);
+}
+
+static void put_msb_dword(unsigned char *cart, int offset, unsigned int data) {
+ int i;
+ for(i=3; i>=0; i--) {
+ cart[offset+i] = data & 0xff;
+ data >>= 8;
+ }
+}
+
+/* has_cart_signature() returns boolean */
+int has_cart_signature(unsigned char *cart) {
+ return memcmp(cart, CART_SIGNATURE, 4) == 0;
+}
+
+void cart_dump_header(unsigned char *cart, int calc_cksum) {
+ char *type = "UNKNOWN";
+ unsigned int id = get_cart_type(cart);
+ unsigned int real_id = get_msb_dword(cart, CART_TYPE_OFFSET);
+
+ if(id)
+ type = cart_types[id].name;
+ else
+ calc_cksum = 0;
+
+ /* Show real type, if get_cart_type() returned zero */
+ fprintf(stderr, "CART signature: %s\n",
+ (has_cart_signature(cart) ? "Present" : "MISSING"));
+ fprintf(stderr, "Cartridge type: %s (ID %d)\n", type, real_id);
+ fprintf(stderr, "ROM size: ");
+ if(id)
+ fprintf(stderr, "%d bytes + 16 byte header\n", get_cart_size(cart));
+ else
+ fprintf(stderr, "UNKNOWN\n");
+
+ fprintf(stderr, "Machine type: ");
+ switch(get_cart_machine(cart)) {
+ case M_ATARI8:
+ fprintf(stderr, "Atari 8-bit computer\n");
+ break;
+
+ case M_5200:
+ fprintf(stderr, "Atari 5200\n");
+ break;
+
+ default:
+ fprintf(stderr, "UNKNOWN\n");
+ break;
+ }
+
+ fprintf(stderr, "CART checksum: 0x%08x", get_cart_checksum(cart));
+ if(calc_cksum) {
+ fputs((cart_checksum_ok(cart) ? "OK" : "BAD"), stderr);
+ }
+ fputc('\n', stderr);
+}
+
+/* get_cart_type() returns the cartridge ID from the CART header, or zero
+ if it's an invalid/unknown type. */
+unsigned int get_cart_type(unsigned char *cart) {
+ unsigned int type = get_msb_dword(cart, CART_TYPE_OFFSET);
+
+ if(type == 0 || type > MAX_CART_TYPE)
+ return 0;
+
+ return type;
+}
+
+/* get_cart_size() returns the cartridge ROM size in bytes, NOT including
+ the 16-byte header, or zero if invalid type */
+unsigned int get_cart_size(unsigned char *cart) {
+ unsigned int type = get_cart_type(cart);
+ if(type == 0)
+ return 0;
+
+ return cart_types[type].size * 1024;
+}
+
+/* get_cart_checksum() returns the checksum stored in the header. It does
+ NOT calculate the checksum (use calc_rom_checksum() for that) */
+unsigned int get_cart_checksum(unsigned char *cart) {
+ return get_msb_dword(cart, CART_CHECKSUM_OFFSET);
+}
+
+machine_t get_cart_machine(unsigned char *cart) {
+ unsigned int type = get_cart_type(cart);
+ if(type == 0)
+ return M_INVALID;
+
+ return cart_types[type].machine;
+}
+
+/* calc_rom_checksum() is the odd man out: it takes a pointer to the
+ raw ROM data, NOT to the header+data like all the other functions do! */
+unsigned int calc_rom_checksum(unsigned char *rom, int bytes) {
+ unsigned int cksum = 0;
+
+ while(bytes-- > 0) cksum += *rom++;
+
+ return cksum;
+}
+
+/* cart_checksum_ok() must be called with a complete .CAR image, not just
+ a header! */
+unsigned int cart_checksum_ok(unsigned char *cart) {
+ unsigned int bytes = get_cart_size(cart);
+ unsigned int got = calc_rom_checksum(cart + 16, bytes);
+ unsigned int expected = get_cart_checksum(cart);
+
+ return (got == expected);
+}
+
+/* create_cart_header() must be supplied with a buffer, which must be at least
+ 16 bytes in size. Returns 0 with buffer unchanged if type was invalid. */
+int create_cart_header(
+ unsigned char *buffer,
+ unsigned char *rom,
+ unsigned int type)
+{
+ if(type == 0 || type > MAX_CART_TYPE)
+ return 0;
+
+ memcpy(buffer, CART_SIGNATURE, 4);
+
+ set_cart_type(buffer, type);
+ set_cart_checksum(buffer,
+ calc_rom_checksum(rom, cart_types[type].size * 1024));
+ set_cart_unused(buffer, 0);
+
+ return 1;
+}
+
+void set_cart_checksum(unsigned char *cart, unsigned int sum) {
+ put_msb_dword(cart, CART_CHECKSUM_OFFSET, sum);
+}
+
+void set_cart_type(unsigned char *cart, unsigned int type) {
+ put_msb_dword(cart, CART_TYPE_OFFSET, type);
+}
+
+void set_cart_unused(unsigned char *cart, unsigned int data) {
+ put_msb_dword(cart, CART_UNUSED_OFFSET, data);
+}
+
diff --git a/cart.h b/cart.h
new file mode 100644
index 0000000..f2b10a4
--- /dev/null
+++ b/cart.h
@@ -0,0 +1,55 @@
+
+/* CART header support for cart2xex
+
+ Format of CART header (from atari800 cart.txt):
+
+ first 4 bytes containing 'C' 'A' 'R' 'T'.
+ next 4 bytes containing cartridge type in MSB format (see the table below).
+ next 4 bytes containing cartridge checksum in MSB format (ROM only).
+ next 4 bytes are currently unused (zero).
+ followed immediately with the ROM data: 4, 8, 16, 32, 40, 64, 128, 256, 512
+ or 1024 kilobytes.
+
+ See cart.c for the list of cart types.
+*/
+
+#ifndef CART_H
+#define CART_H
+
+#define CART_SIGNATURE "CART"
+
+#define CART_SIGNATURE_OFFSET 0
+#define CART_TYPE_OFFSET 4
+#define CART_CHECKSUM_OFFSET 8
+#define CART_UNUSED_OFFSET 12
+
+typedef enum {
+ M_INVALID,
+ M_ATARI8,
+ M_5200
+} machine_t;
+
+typedef struct {
+ int machine;
+ int size;
+ char *name;
+} cart_t;
+
+extern cart_t cart_types[];
+extern const int MAX_CART_TYPE;
+
+int has_cart_signature(unsigned char *cart);
+void cart_dump_header(unsigned char *cart, int calc_cksum);
+unsigned int get_cart_type(unsigned char *cart);
+unsigned int get_cart_size(unsigned char *cart);
+unsigned int get_cart_checksum(unsigned char *cart);
+machine_t get_cart_machine(unsigned char *cart);
+unsigned int calc_rom_checksum(unsigned char *rom, int bytes);
+unsigned int cart_checksum_ok(unsigned char *cart);
+int create_cart_header(
+ unsigned char *buffer, unsigned char *rom, unsigned int type);
+void set_cart_checksum(unsigned char *cart, unsigned int sum);
+void set_cart_type(unsigned char *cart, unsigned int type);
+void set_cart_unused(unsigned char *cart, unsigned int data);
+
+#endif
diff --git a/cart2xex.1 b/cart2xex.1
new file mode 100644
index 0000000..862982a
--- /dev/null
+++ b/cart2xex.1
@@ -0,0 +1,247 @@
+.\" 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 "CART2XEX" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py cart2xex.rst > cart2xex.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fBcart2xex\fP [\fI\-cdhn\fP] [\fB\-i\fP \fIaddr\fP] [\fB\-r\fP \fIaddr\fP] [\fB\-p\fP \fIpages\fP] [\fB\-t\fP \fItitle\fP] \fIinfile.rom\fP [\fIoutfile.xex\fP]
+.SH DESCRIPTION
+.sp
+\fBcart2xex\fP creates an Atari executable (XEX/COM/BIN/etc) file from
+an 8K or 16K non\-bankswitched cartridge ROM dump. The resulting
+executable will (by default) have a title screen that displays
+\fBLOADING\fP and the filename while the rest of the file loads.
+.sp
+Input ROM files may be either raw dumps or Atari800 \fB\&.CAR\fP format.
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \-c
+Check ROM and print info only; do not create an executable.
+.TP
+.B \-d
+Don\(aqt include code to make the Atari reboot when RESET is
+pressed. Generally, there\(aqs no advantage to using this option,
+as the Atari either locks up or reboots anyway when reset.
+.TP
+.B \-h
+Print help (usage) message and exit.
+.TP
+.BI \-i \ address
+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.
+.TP
+.BI \-l \ address
+Sets the load address of the executable. Default is to use the
+standard Atari cartridge address ($8000 for a 16K cart, $A000
+for 8K). Executables created with this option \fBwill not run\fP without
+further modifications.
+.TP
+.BI \-r \ address
+Sets the run address of the executable. Default is to use the
+run address in the ROM image, at addresses $BFFA/BFFB. A run address
+of zero means "no run address"; in this case, the output
+won\(aqt contain a run address at all. Executables created with
+this option probably won\(aqt run without further modifications.
+.TP
+.B \-R
+ROM image is a "right cartridge", meant to be used in the right
+slot on an 800. Sets the load address to $8000, equivalent to \fB\-l "$8000"\fP\&.
+Very few right cartridges were ever made. \fB\&.CAR\fP images
+with type 21 (8K right cartridge) will automatically set this
+option.
+.TP
+.B \-n
+Do not prepend the code for the \fBLOADING\fP title screen. Without
+the title screen, the Atari\(aqs display will become corrupted during
+the loading process, although this doesn\(aqt always cause any
+real problems.
+.TP
+.BI \-p \ pages
+Reserve extra memory below RAMTOP, in 256\-byte pages. Maximum
+value for \fIpages\fP is 16 for a 16K ROM, or 48 for an 8K ROM.
+.TP
+.BI \-t \ title
+Set the title to be displayed during the \fILOADING\fP screen, up to
+20 characters. Default: use the output filename as the title
+(minus any extension), or leave the title blank if writing to
+standard output.
+.UNINDENT
+.SH NOTES
+.sp
+\fBcart2xex\fP can\(aqt handle cartridges that use bankswitching (such
+as most Atari XEGS\-era releases, or OSS super carts such as Basic
+XL or Action!). Only standard 8K and 16K cartridge images are
+usable. For raw dumps, this means the input must be exactly 8192 or
+16384 bytes. For \fB\&.CAR\fP images, the image type is read from the
+CART header; it must be type 1, 2, or 21 (Standard 8K, 16K, or 8K
+right\-slot image, respectively).
+.sp
+\fIinfile\fP may be \fB\-\fP to read from standard input. \fIoutfile\fP
+may be \fB\-\fP to write to standard output. \fBcart2xex\fP
+will refuse to write binary data to a terminal.
+.sp
+If \fIoutfile\fP is omitted, but \fIinfile\fP is provided,
+the output filename will be constructed from
+\fIinfile\fP by replacing the filename extension with \fI\&.xex\fP, or by
+appending \fI\&.xex\fP if there is no extension.
+.sp
+The \fBLOADING\fP screen consists of around 200 bytes of 6502 object code,
+loaded at $6000.
+The title (\fB\-t\fP argument, or output filename) will be transformed
+so that it will be displayed in green on a GRAPHICS 2 screen. Any
+~ (tilde) or ] (right square bracket) characters will be replaced with
+spaces, since the tilde doesn\(aqt exist in the ATASCII character set, and
+a green ] would be the Atari clear\-screen code (CHR$(125)).
+.sp
+With the \fB\-n\fP option (suppress \fBLOADING\fP screen), the Atari\(aqs
+display will get corrupted during the load. This is because the display
+list and screen RAM in use during the load is located in the RAM where the
+executable will be loaded, which gets overwritten with code/data. Even
+without the title screen, \fBcart2xex\fP emits code that sets RAMTOP
+to point below the cartridge area, so the screen corruption shouldn\(aqt
+cause any problems for most games (since the first thing they do is set
+up their own graphics display). However, languages such as BASIC don\(aqt
+necessarily re\-open the E: device. In fact, the first thing BASIC does
+is print a READY prompt, which (with \fB\-n\fP) causes it to overwrite
+part of its own code, and lock up (since the screen address points to a
+location within BASIC).
+.sp
+Not all cartridges will run correctly if loaded into RAM. In particular,
+many commercial games contain "self\-destruct" code which attempts to
+write to the cartridge\(aqs own ROM (either by accident or
+as a copy\-protection measure). This of course fails when running from
+ROM, but will succeed if running from RAM. Converting such an image into
+a binload file requires disassembling the code, finding the self\-destruct
+sequence, and removing it (the Atari800 built\-in debugger is very handy
+for this). If you\(aqre looking for an example of a
+self\-destructing ROM, try the Parker Brothers version of Frogger, or
+Atari Pac\-Man or Millipede.
+.sp
+Sometimes, the executable won\(aqt work properly when loaded from a regular
+DOS, but will work fine with a game DOS such as Fenders 3\-sector loader
+or HiassofT\(aqs MyPicoDOS. This seems to happen because the cart code
+assumes that portions of memory will be initialized to zero, but with
+DOS loaded, they actually contain DOS\(aqs code and data. The reason they
+work with bootloaders is that a bootloader is specifically designed to use as
+little memory as possible. An example of this type of ROM image is
+Imagic\(aqs Atlantis.
+.sp
+The opposite is also possible: sometimes the executable will work fine
+when loaded from DOS, but will not work when loaded via a bootloader
+(or the "Load XEX" option in an emulator). This seems to happen for
+carts that want to boot DOS (example: Atari Artist). This isn\(aqt a very
+serious problem though: normally if a cartridge allows DOS boot, you will
+want to be using DOS with it (e.g. so you can save and load your
+drawings in Atari Artist).
+.sp
+Normally, the load/run/init addresses are best left alone. The \fB\-l\fP,
+\fB\-i\fP and
+\fB\-r\fP options are included for expert users, who may find them useful
+for bypassing "self\-destruct" copy protection code. \fBcart2xex\fP will
+print warnings if the user sets the addresses outside the address range
+of the cartridge ($8000\-$CFFF for 16K carts, or $A000\-$CFFF for 8K), but
+will create the executable anyway (always assume the user knows what he\(aqs
+doing). These might also be useful if you want to disassemble the ROM\(aqs
+code with a cartridge\-based disassembler (in which case, you probably
+need \fB\-i0 \-r0\fP as well, to create a file that doesn\(aqt execute
+when loaded).
+.sp
+Addresses for \fB\-l\fP / \fB\-i\fP / \fB\-r\fP and the
+number of pages for \fB\-p\fP
+may be given in decimal,
+hex prefixed with \fB$\fP (don\(aqt forget to quote it for the shell!),
+or hex prefixed with \fB0x\fP\&. Hex digits may be given in upper or lowercase.
+.sp
+Without the \fB\-p\fP option, the title screen
+routine will still lower RAMTOP (location 106) to make room for the image.
+Text\-mode cartridges such as BASIC or ASM/ED may need some "breathing
+room" between RAMTOP and the start of the cartridge code, because some
+versions of the Atari OS contain a bug that causes "clear screen" to
+clear an extra 64 bytes past RAMTOP. This isn\(aqt a problem for real ROM
+cartridges (since ROM can\(aqt be overwritten), but when running from RAM,
+this will clear the first 64 bytes of "cartridge" code!
+.sp
+Some versions of the Atari OS ROM also contain a bug that causes 800 bytes
+of memory above RAMTOP to be scrolled, when scrolling the text window in
+graphics modes. When using a real cartridge, this doesn\(aqt hurt anything,
+but if running from RAM, the code will be scrambled and the Atari will
+lock up. Reserving 4 pages (\fB\-p 4\fP) will avoid this, though of course
+it will reduce the amount of free memory available to the cartridge.
+.sp
+Without the \fB\-d\fP option, the output executable will contain a one\-byte
+segment that loads a value of 1 into location $0244 (equivalent to the BASIC
+\fBPOKE 580,1\fP command). It\(aqs possible that the cartridge\(aqs code may
+reset this location when it runs, so reboot\-on\-RESET may not be 100%
+reliable. The \fB\-d\fP option is probably only useful for languages like
+BASIC or ASM/ED.
+.SH EXIT STATUS
+.sp
+Exit status is zero for success, non\-zero 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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/cart2xex.c b/cart2xex.c
new file mode 100644
index 0000000..4a8acb2
--- /dev/null
+++ b/cart2xex.c
@@ -0,0 +1,583 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <string.h>
+#include <errno.h>
+
+#include "cart.h"
+#include "get_address.h"
+#include "loadscreen_bin.h"
+
+/*
+ cart2xex 20061018 bkw
+ updated 20070524 bkw
+ added atari800 CART format 20071227 bkw
+
+ Quick & dirty utility to convert Atari 8-bit cartridge images
+ to Atari DOS binary load files (suitable for use with DOS the 2.0S
+ "L" command, or your favorite 3-sector loader, etc).
+
+ Works on non-bankswitched 8K or 16K cart images. Output is:
+ - Segment containing code that prints LOADING (suppress with -n)
+ - 6-byte Atari binary load header
+ - The entire input
+ - 4-byte header, followed by the init address (total 6 bytes)
+ - 4-byte header, followed by the run address (total 6 bytes)
+
+ Without the LOADING screen, the resulting binary is 18 bytes
+ larger than the input.
+
+ If the cart image isn't exactly 8192 or 16384 bytes long, an error
+ is printed, and the program exits with status 1. Otherwise, exit
+ status is 0 (success).
+
+ Future options (stuff I might add, if there's any interest):
+
+ - ATR image output support (image would consist of a 3-sector
+ loader plus the binary)... not really necessary: can use "makeatr"
+ to make a k-file if you really want.
+
+ - Simple algorithm to detect STA/STX/STY instructions that write
+ to the ROM address space. If I were going to copy-protect a cart,
+ to keep people from dumping it to a bin, I'd write to what's supposed
+ to be ROM, then read it back. If I got back what I wrote there, it
+ means the "ROM" is actually RAM, so I'd refuse to play the game.
+ Parker Bros. Frogger does this.
+ The trouble is, a *simple* algorithm will probably fail. Several
+ self-destructing carts I've dealt with use STA ($00),Y to write to
+ ROM... meaning I basically need a complete emulator. Atari++ has a
+ nice debugger with conditional breakpoints, maybe there's a way to
+ hook into that?
+
+ - Documentation for all this crap, if I really add it :)
+*/
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "cart2xex"
+
+#define BANNER \
+ SELF " v" VERSION " - by B. Watson (WTFPL)\n"
+
+#define MAX_CART_SIZE (16384 + 16)
+
+char *usage =
+ "\nUsage: " SELF " [-cdhilnprR] [-t title] infile.rom [outfile.xex]\n"
+ " -c Check only; do not create xex file\n"
+ " -d Don't include code to reboot the Atari when RESET is pressed\n"
+ " -h Print this help\n"
+ " -i addr Force init address to addr (0 = no init address)\n"
+ " -r addr Force run address to addr (0 = no run address)\n"
+ " -l addr Force load address to addr\n"
+ " -R ROM is right cartridge image (same as -l0x8000)\n"
+ " -p pages Reserve # of pages below RAMTOP\n"
+ " -n Do not include code for LOADING screen\n"
+ " -t title Set title for LOADING screen (default: outfile)\n";
+
+int loadscreen = 1; /* true if we're going to prepend a title screen */
+int checkonly = 0; /* true if we're not writing an output file */
+int reserve_pages = 0; /* number of extra pages of RAM to reserve */
+int reboot_reset = 1; /* true if RESET key causes a reboot */
+
+/* buffer[] will contain the cart image after it's loaded, which may
+ or may not include a 16-byte CART header. rom will point to the start
+ of the actual ROM data (buffer or buffer+16) */
+unsigned char buffer[MAX_CART_SIZE];
+unsigned char *rom;
+
+/* warn() and die() are like the Perl functions of the same names, kind of */
+void warn(char *msg) {
+ if(msg) fprintf(stderr, "%s\n", msg);
+}
+
+void die(char *msg) {
+ warn(msg);
+ exit(1);
+}
+
+/* cart_image_valid() looks at an atari800-style CART header and returns
+ true if the cartridge type is 8K or 16K non-bankswitched */
+int cart_image_valid(unsigned char *cart) {
+ int cart_type;
+
+ cart_dump_header(cart, 0);
+
+ cart_type = get_cart_type(cart);
+
+ /* magic numbers: types 1 and 2 are "Standard 8K" and "Standard 16K",
+ type 21 is "Standard 8K right slot", which are the only types we
+ can support. */
+ if(cart_type != 1 && cart_type != 2 && cart_type != 21)
+ return 0;
+
+ /* bad cksum is not a fatal error */
+ if(cart_checksum_ok(cart))
+ fprintf(stderr, "CART checksum: 0x%08x (OK)\n", get_cart_checksum(cart));
+ else
+ warn("Warning: CART checksum invalid, file may be corrupt");
+
+ return 1;
+}
+
+/* For now, all the work is done in main() */
+int main(int argc, char **argv) {
+ int maxres, c, size = 0;
+ unsigned char init_lo, init_hi, run_lo, run_hi, option, present, ramtop_adj;
+ int runadr, initadr;
+ int force_init = -1, force_run = -1, force_load = -1;
+ char *infile = "-", outfile[4096] = "-";
+ FILE *in = stdin, *out = stdout;
+ char *title = NULL;
+
+ puts(BANNER);
+
+ argc--; argv++;
+
+ while(argc && *argv[0] == '-') {
+ switch(argv[0][1]) {
+ case 't':
+ if(argv[0][2]) {
+ title = &argv[0][2];
+ } else {
+ argc--; argv++;
+ if(argc) {
+ title = &argv[0][0];
+ } else {
+ die(usage);
+ }
+ }
+ break;
+
+ case 'p':
+ if(argv[0][2]) {
+ reserve_pages = get_address(NULL, &argv[0][2]);
+ } else {
+ argc--; argv++;
+ if(argc) {
+ reserve_pages = get_address(NULL, argv[0]);
+ } else {
+ die(usage);
+ }
+ }
+ if(reserve_pages < 0)
+ exit(1);
+ break;
+
+ case 'R':
+ force_load = 0x8000;
+ break;
+
+ case 'l':
+ if(argv[0][2]) {
+ force_load = get_address(NULL, &argv[0][2]);
+ } else {
+ argc--; argv++;
+ if(argc) {
+ force_load = get_address(NULL, argv[0]);
+ } else {
+ die(usage);
+ }
+ }
+ if(force_load < 0)
+ exit(1);
+ break;
+
+ case 'i':
+ if(argv[0][2]) {
+ force_init = get_address(NULL, &argv[0][2]);
+ } else {
+ argc--; argv++;
+ if(argc) {
+ force_init = get_address(NULL, argv[0]);
+ } else {
+ die(usage);
+ }
+ }
+ if(force_init < 0)
+ exit(1);
+ break;
+
+ case 'r':
+ if(argv[0][2]) {
+ force_run = get_address(NULL, &argv[0][2]);
+ } else {
+ argc--; argv++;
+ if(argc) {
+ force_run = get_address(NULL, argv[0]);
+ } else {
+ die(usage);
+ }
+ }
+ if(force_run < 0)
+ exit(1);
+ break;
+
+ case 'c':
+ checkonly = 1;
+ break;
+
+ case 'n':
+ loadscreen = 0;
+ break;
+
+ case 'd':
+ reboot_reset = 0;
+ break;
+
+ case 'h':
+ printf(usage);
+ exit(0);
+ break;
+
+ default:
+ fprintf(stderr, "Unrecognized option '-%c'\n", argv[0][1]);
+ die(usage);
+ break;
+ }
+
+ argc--; argv++;
+ }
+
+ if(argc) {
+ infile = *argv;
+ argc--; argv++;
+ }
+
+ if(argc) {
+ strcpy(outfile, *argv);
+ argc--; argv++;
+ } else if(!checkonly && strcmp(infile, "-") != 0) {
+ char *p;
+
+ strcpy(outfile, infile);
+ p = strrchr(outfile, '.');
+ if(!p) p = outfile + strlen(outfile);
+ *p = '\0';
+ strcat(outfile, ".xex");
+ fprintf(stderr, "- Output is '%s'\n", outfile);
+
+ if(strcmp(infile, outfile) == 0)
+ die("Input and output filenames are the same, aborting!\n");
+ }
+
+ if(argc)
+ die(usage);
+
+ if(strcmp(infile, "-") != 0) {
+ if( !(in = fopen(infile, "rb")) ) {
+ perror(infile);
+ exit(1);
+ }
+ }
+
+ while( (c = getc(in)) != EOF && size < MAX_CART_SIZE) {
+ buffer[size++] = c;
+ }
+
+ if(has_cart_signature(buffer)) {
+ int header_size;
+
+ warn("Image type: Atari800 CART format");
+ if(!cart_image_valid(buffer))
+ die("Unsupported/invalid CART image\n"
+ "(only 8K/16K non-banked images are supported)");
+
+ if(get_cart_type(buffer) == 21) {
+ /* right cartridge */
+ force_load = 0x8000;
+ warn("Forcing load address to $8000 for right-slot cartridge");
+ }
+
+ header_size = get_cart_size(buffer);
+ rom = buffer + 16;
+ size -= 16;
+
+ if(size != header_size)
+ die("CART header says image should be %d bytes, but only %d bytes "
+ "are in the image! Corrupt/damaged CART image?");
+ } else {
+ warn("Image type: Raw dump");
+ rom = buffer;
+ }
+
+ if( !(c == EOF && (size == 8192 || size == 16384)) ) {
+ if(rom[0] == 0xff && rom[1] == 0xff) {
+ warn("This looks like an Atari binary load file!");
+ } else if(rom[0] == 0x00 && rom[1] == 0x00) {
+ warn("This looks like an Atari BASIC program!");
+ }
+
+ die("Cartridge size must be 8192 or 16384 bytes\n"
+ "(Are you sure this is a valid Atari 8-bit cartridge image?)");
+ }
+
+ /* All this info comes from Mapping the Atari, originally by
+ Ian Chadwick, now available for free at
+ http://www.atariarchives.org/mapping/
+ */
+
+ /* Assume we're using a Left Cartridge. Not many Right Cartridges
+ were ever made... */
+ run_lo = rom[size-6]; /* $BFFA */
+ run_hi = rom[size-5]; /* $BFFB */
+ present = rom[size-4]; /* $BFFC */
+ option = rom[size-3]; /* $BFFD */
+ init_lo = rom[size-2]; /* $BFFE */
+ init_hi = rom[size-1]; /* $BFFF */
+
+ /* Some helpful messages... decode the cart options */
+ if(present) { /* $BFFC */
+ fprintf(stderr,
+ "Hmm, this cart image has $%02X in the \"cartridge present\" "
+ "byte at $BFFC\n"
+ "(should be $00). "
+ "Are you sure it's really a valid image?\n",
+ present);
+ }
+
+ fprintf(stderr, "Image size: %dK (addresses $%04X - $%04X)\n",
+ size/1024, 49152-size, 49152-1);
+ fprintf(stderr, "Run address: $%02X%02X\n", run_hi, run_lo);
+ fprintf(stderr, "Init address: $%02X%02X\n", init_hi, init_lo);
+ fprintf(stderr, "Option byte: $%02X\n", option);
+
+ /* $BFFD, bits 0, 2, 7 */
+ fprintf(stderr, "- Cartridge does %sallow disk boot\n",
+ (option & 1) ? "" : "NOT ");
+
+ fprintf(stderr, "- Cartridge gets initialized, %s\n",
+ (option & 4) ?
+ "then started normally" :
+ "but NOT started (weird)");
+
+ fprintf(stderr, "Cartridge type: %s\n",
+ (option & 128) ?
+ "diagnostic (output binary may not run correctly!)" :
+ "normal (non-diagnostic)");
+
+ if(checkonly)
+ exit(0);
+
+ if(force_load >= 0) {
+ fprintf(stderr, "-l option: Load address forced to $%04X\n", force_load);
+ }
+
+ if(force_init >= 0) {
+ init_lo = rom[size-2] = force_init & 0xff;
+ init_hi = rom[size-1] = (force_init >> 8) & 0xff;
+ fprintf(stderr, "-i option: Init address forced to $%04X\n", force_init);
+ }
+
+ if(force_run >= 0) {
+ run_lo = rom[size-6] = force_run & 0xff;
+ run_hi = rom[size-5] = (force_run >> 8) & 0xff;
+ fprintf(stderr, "-r option: Run address forced to $%04X\n", force_run);
+ }
+
+ runadr = run_lo + (run_hi << 8);
+ if(runadr && (runadr < 49152-size || runadr >= 49152))
+ warn("Warning: Run address not in cartridge address range");
+
+ initadr = init_lo + (init_hi << 8);
+ if(initadr && (initadr < 49152-size || initadr >= 49152))
+ warn("Warning: Init address not in cartridge address range");
+
+ if(size == 8192 && force_load < 0) {
+ if(runadr >= 0x8000 && runadr < 0xa000)
+ warn("This may be a \"right cartridge\" ROM, try -R?");
+ }
+
+ /* Whew! Finally all checks are done, we can open the output file */
+ if(strcmp(outfile, "-") != 0) {
+ if( !(out = fopen(outfile, "wb")) ) {
+ perror(outfile);
+ fclose(in);
+ exit(1);
+ }
+
+ if(!title) {
+ char *p;
+
+ title = outfile;
+
+ /* remove directory if present */
+ p = strrchr(title, '/');
+ if(p) title = p + 1;
+
+ /* remove extension if present */
+ p = strrchr(title, '.');
+ if(p) *p = '\0';
+ }
+
+ }
+
+ if(isatty(fileno(out))) {
+ warn("\nStandard output is a terminal, not writing binary data.\n"
+ "Either redirect to a file or set the output filename.");
+ die(usage);
+ }
+
+ maxres = ((0xc000 - size) - 0x7000) >> 8;
+ if(reserve_pages > maxres) {
+ fprintf(stderr, "Can't reserve %d pages below RAMTOP. Maximum "
+ "reserved pages is %d for a %dK ROM.\n",
+ reserve_pages, maxres, size/1024);
+ exit(1);
+ }
+
+ if(reserve_pages) {
+ fprintf(stderr, "Reserving %d pages below RAMTOP\n", reserve_pages);
+ }
+
+ ramtop_adj = (size >> 8) + reserve_pages;
+
+ /* Write the loading screen object code, unless suppressed */
+ if(loadscreen) {
+ int i;
+ char c, d;
+
+ /* loadscreen.bin adjusts RAMTOP to make room for the cart image,
+ so it needs to know the size. */
+ loadscreen_bin[6] = ramtop_adj;
+
+ if(title) {
+ /* WARNING: magic number here! */
+ /* Look for "Title offset: loadscreen_bin_len - $xx"
+ in dasm output. */
+ int offset = loadscreen_bin_len - 27;
+ int len = strlen(title);
+
+ if(len > 20) {
+ fprintf(stderr, "Title > 20 characters, truncating\n");
+ len = 20;
+ }
+ offset += 10-len/2;
+
+ fprintf(stderr, "- Setting title to \"");
+ for(i=0; i<len; i++) {
+ c = title[i];
+
+ c = tolower(c);
+ d = c;
+
+ if(c >= 33 && c <= 63) {
+ c -= 32;
+ } else if(c >= 64 && c <= 95) {
+ c += 32;
+ } else if(!isalnum(c)) {
+ d = c = 32;
+ }
+ fputc(d, stderr);
+
+ loadscreen_bin[offset+i] = c;
+ }
+
+ fputc('"', stderr);
+ fputc('\n', stderr);
+ } else {
+ fprintf(stderr,
+ "No -t and no output filename, not setting load screen title\n");
+ }
+
+ /* loadscreen_bin already includes $FF, $FF header */
+ for(i=0; i<loadscreen_bin_len; i++)
+ putc(loadscreen_bin[i], out);
+ } else {
+ /* $FF, $FF binary load header. All Atari executables must have this. */
+ putc(0xff, out);
+ putc(0xff, out);
+
+ /* Without the title screen, we still need to set RAMTOP. Do it
+ by loading a 1-byte segment at $6A. */
+ putc(0x6a, out);
+ putc(0x00, out);
+ putc(0x6a, out);
+ putc(0x00, out);
+ putc(0xc0 - ramtop_adj, out); /* assume RAMTOP is 48K */
+ }
+
+ if(reboot_reset) {
+ /* make the Atari reboot when user presses RESET (POKE 580,1) */
+ putc(0x44, out); /* start addr LSB */
+ putc(0x02, out); /* start addr MSB */
+ putc(0x44, out); /* end addr LSB */
+ putc(0x02, out); /* end addr MSB */
+ putc(0x01, out); /* one byte of data */
+ }
+
+ /* Now write the Atari binary load file... Remember, the 6502 expects
+ addresses to be little-endian (LSB, then MSB) */
+
+ /* First segment: the code. */
+ /* Load address (LSB first). $8000 for a 16K cart, or $A000 for 8K,
+ or force_load if user used it */
+ if(force_load >= 0) {
+ putc(force_load & 0xff, out);
+ putc(force_load >> 8, out);
+ force_load += (size - 1);
+ putc(force_load & 0xff, out);
+ putc(force_load >> 8, out);
+ } else {
+ putc(0, out);
+ putc(size == 16384 ? 0x80 : 0xa0, out);
+ /* End address (LSB first). Always $BFFF */
+ putc(0xff, out);
+ putc(0xbf, out);
+ }
+
+ /* Image data */
+ for(c=0; c<size; c++)
+ putc(rom[c], out);
+
+ /* Only write init address if it's not 0 */
+ if(init_hi || init_lo) {
+ /* Next segment loads at INITAD ($2E2), contains the cart's init addr */
+ putc(0xe2, out);
+ putc(0x02, out);
+
+ /* Segment length is 2 bytes (ends at $2E3) */
+ putc(0xe3, out);
+ putc(0x02, out);
+
+ /* Init address will get loaded at $2E2 and $2E3 */
+ putc(init_lo, out);
+ putc(init_hi, out);
+ }
+
+ /* Only write run address if it's not 0 */
+ /* Next segment loads at RUNAD ($2E0), contains the cart's run address */
+ if(run_hi || run_lo) {
+ putc(0xe0, out);
+ putc(0x02, out);
+
+ /* Segment length is 2 bytes (ends at $2E1) */
+ putc(0xe1, out);
+ putc(0x02, out);
+
+ /* Run address will get loaded at $2E0 and $2E1 */
+ putc(run_lo, out);
+ putc(run_hi, out);
+ }
+
+ /* Check for I/O errors before closing files */
+ c = 0;
+ if(ferror(in)) {
+ fprintf(stderr, "%s: %s\n", infile, strerror(errno));
+ c = 1;
+ }
+
+ if(ferror(out)) {
+ fprintf(stderr, "%s: %s\n", outfile, strerror(errno));
+ c = 1;
+ }
+
+ /* Close 'em, we're done */
+ fclose(in);
+ fclose(out);
+
+ /* That's all, folks! */
+ exit(c);
+}
diff --git a/cart2xex.rst b/cart2xex.rst
new file mode 100644
index 0000000..aff6272
--- /dev/null
+++ b/cart2xex.rst
@@ -0,0 +1,203 @@
+.. RST source for cart2xex(1) man page. Convert with:
+.. rst2man.py cart2xex.rst > cart2xex.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+========
+cart2xex
+========
+
+----------------------------------------------------------------
+Convert an Atari 8-bit ROM cartridge image to a binary load file
+----------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+**cart2xex** [*-cdhn*] [**-i** *addr*] [**-r** *addr*] [**-p** *pages*] [**-t** *title*] *infile.rom* [*outfile.xex*]
+
+DESCRIPTION
+===========
+
+**cart2xex** creates an Atari executable (XEX/COM/BIN/etc) file from
+an 8K or 16K non-bankswitched cartridge ROM dump. The resulting
+executable will (by default) have a title screen that displays
+**LOADING** and the filename while the rest of the file loads.
+
+Input ROM files may be either raw dumps or Atari800 **.CAR** format.
+
+OPTIONS
+=======
+
+-c
+ Check ROM and print info only; do not create an executable.
+
+-d
+ Don't include code to make the Atari reboot when RESET is
+ pressed. Generally, there's no advantage to using this option,
+ as the Atari either locks up or reboots anyway when reset.
+
+-h
+ Print help (usage) message and exit.
+
+-i address
+ 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.
+
+-l address
+ Sets the load address of the executable. Default is to use the
+ standard Atari cartridge address ($8000 for a 16K cart, $A000
+ for 8K). Executables created with this option **will not run** without
+ further modifications.
+
+-r address
+ Sets the run address of the executable. Default is to use the
+ run address in the ROM image, at addresses $BFFA/BFFB. A run address
+ of zero means "no run address"; in this case, the output
+ won't contain a run address at all. Executables created with
+ this option probably won't run without further modifications.
+
+-R
+ ROM image is a "right cartridge", meant to be used in the right
+ slot on an 800. Sets the load address to $8000, equivalent to **-l "$8000"**.
+ Very few right cartridges were ever made. **.CAR** images
+ with type 21 (8K right cartridge) will automatically set this
+ option.
+
+-n
+ Do not prepend the code for the **LOADING** title screen. Without
+ the title screen, the Atari's display will become corrupted during
+ the loading process, although this doesn't always cause any
+ real problems.
+
+-p pages
+ Reserve extra memory below RAMTOP, in 256-byte pages. Maximum
+ value for *pages* is 16 for a 16K ROM, or 48 for an 8K ROM.
+
+-t title
+ Set the title to be displayed during the *LOADING* screen, up to
+ 20 characters. Default: use the output filename as the title
+ (minus any extension), or leave the title blank if writing to
+ standard output.
+
+NOTES
+=====
+
+**cart2xex** can't handle cartridges that use bankswitching (such
+as most Atari XEGS-era releases, or OSS super carts such as Basic
+XL or Action!). Only standard 8K and 16K cartridge images are
+usable. For raw dumps, this means the input must be exactly 8192 or
+16384 bytes. For **.CAR** images, the image type is read from the
+CART header; it must be type 1, 2, or 21 (Standard 8K, 16K, or 8K
+right-slot image, respectively).
+
+*infile* may be **-** to read from standard input. *outfile*
+may be **-** to write to standard output. **cart2xex**
+will refuse to write binary data to a terminal.
+
+If *outfile* is omitted, but *infile* is provided,
+the output filename will be constructed from
+*infile* by replacing the filename extension with *.xex*, or by
+appending *.xex* if there is no extension.
+
+The **LOADING** screen consists of around 200 bytes of 6502 object code,
+loaded at $6000.
+The title (**-t** argument, or output filename) will be transformed
+so that it will be displayed in green on a GRAPHICS 2 screen. Any
+~ (tilde) or ] (right square bracket) characters will be replaced with
+spaces, since the tilde doesn't exist in the ATASCII character set, and
+a green ] would be the Atari clear-screen code (CHR$(125)).
+
+With the **-n** option (suppress **LOADING** screen), the Atari's
+display will get corrupted during the load. This is because the display
+list and screen RAM in use during the load is located in the RAM where the
+executable will be loaded, which gets overwritten with code/data. Even
+without the title screen, **cart2xex** emits code that sets RAMTOP
+to point below the cartridge area, so the screen corruption shouldn't
+cause any problems for most games (since the first thing they do is set
+up their own graphics display). However, languages such as BASIC don't
+necessarily re-open the E: device. In fact, the first thing BASIC does
+is print a READY prompt, which (with **-n**) causes it to overwrite
+part of its own code, and lock up (since the screen address points to a
+location within BASIC).
+
+Not all cartridges will run correctly if loaded into RAM. In particular,
+many commercial games contain "self-destruct" code which attempts to
+write to the cartridge's own ROM (either by accident or
+as a copy-protection measure). This of course fails when running from
+ROM, but will succeed if running from RAM. Converting such an image into
+a binload file requires disassembling the code, finding the self-destruct
+sequence, and removing it (the Atari800 built-in debugger is very handy
+for this). If you're looking for an example of a
+self-destructing ROM, try the Parker Brothers version of Frogger, or
+Atari Pac-Man or Millipede.
+
+Sometimes, the executable won't work properly when loaded from a regular
+DOS, but will work fine with a game DOS such as Fenders 3-sector loader
+or HiassofT's MyPicoDOS. This seems to happen because the cart code
+assumes that portions of memory will be initialized to zero, but with
+DOS loaded, they actually contain DOS's code and data. The reason they
+work with bootloaders is that a bootloader is specifically designed to use as
+little memory as possible. An example of this type of ROM image is
+Imagic's Atlantis.
+
+The opposite is also possible: sometimes the executable will work fine
+when loaded from DOS, but will not work when loaded via a bootloader
+(or the "Load XEX" option in an emulator). This seems to happen for
+carts that want to boot DOS (example: Atari Artist). This isn't a very
+serious problem though: normally if a cartridge allows DOS boot, you will
+want to be using DOS with it (e.g. so you can save and load your
+drawings in Atari Artist).
+
+Normally, the load/run/init addresses are best left alone. The **-l**,
+**-i** and
+**-r** options are included for expert users, who may find them useful
+for bypassing "self-destruct" copy protection code. **cart2xex** will
+print warnings if the user sets the addresses outside the address range
+of the cartridge ($8000-$CFFF for 16K carts, or $A000-$CFFF for 8K), but
+will create the executable anyway (always assume the user knows what he's
+doing). These might also be useful if you want to disassemble the ROM's
+code with a cartridge-based disassembler (in which case, you probably
+need **-i0 -r0** as well, to create a file that doesn't execute
+when loaded).
+
+Addresses for **-l** / **-i** / **-r** and the
+number of pages for **-p**
+may be given in decimal,
+hex prefixed with **$** (don't forget to quote it for the shell!),
+or hex prefixed with **0x**. Hex digits may be given in upper or lowercase.
+
+Without the **-p** option, the title screen
+routine will still lower RAMTOP (location 106) to make room for the image.
+Text-mode cartridges such as BASIC or ASM/ED may need some "breathing
+room" between RAMTOP and the start of the cartridge code, because some
+versions of the Atari OS contain a bug that causes "clear screen" to
+clear an extra 64 bytes past RAMTOP. This isn't a problem for real ROM
+cartridges (since ROM can't be overwritten), but when running from RAM,
+this will clear the first 64 bytes of "cartridge" code!
+
+Some versions of the Atari OS ROM also contain a bug that causes 800 bytes
+of memory above RAMTOP to be scrolled, when scrolling the text window in
+graphics modes. When using a real cartridge, this doesn't hurt anything,
+but if running from RAM, the code will be scrambled and the Atari will
+lock up. Reserving 4 pages (**-p 4**) will avoid this, though of course
+it will reduce the amount of free memory available to the cartridge.
+
+Without the **-d** option, the output executable will contain a one-byte
+segment that loads a value of 1 into location $0244 (equivalent to the BASIC
+**POKE 580,1** command). It's possible that the cartridge's code may
+reset this location when it runs, so reboot-on-RESET may not be 100%
+reliable. The **-d** option is probably only useful for languages like
+BASIC or ASM/ED.
+
+EXIT STATUS
+===========
+
+Exit status is zero for success, non-zero for failure.
+
+.. include:: manftr.rst
diff --git a/dasm2atasm b/dasm2atasm
new file mode 100755
index 0000000..016722f
--- /dev/null
+++ b/dasm2atasm
@@ -0,0 +1,275 @@
+#!/usr/bin/perl -w
+
+sub usage {
+ print <<EOF;
+Usage: $0 -[aclmr] infile.asm [outfile.m65]
+See man page for details.
+EOF
+ exit 1;
+}
+
+sub get_mac_sub {
+ my $rex = shift;
+ my $code = "sub { s/($rex)/\\U\$1/gio };";
+ #warn "code is $code";
+ return eval "$code";
+}
+
+sub unhex {
+ # makes a proper $xx, $xx, $xx list of bytes
+ # from a list of hex digits, spaces optional.
+ my $bytes = shift;
+ my $ret = "";
+
+ $bytes =~ s/\s//g;
+
+ #warn "unhex: bytes is $bytes";
+
+ for($bytes =~ /(..)/g) {
+ #warn "unhex: found $_";
+ $ret .= "\$$_, ";
+ }
+
+ chop $ret;
+ chop $ret;
+
+ return $ret;
+}
+
+sub fix_include {
+ my $inc = shift;
+ my $old = $inc;
+ $inc =~ s/\.(\w+)("?)$/.m65$2/;
+
+ if($recursive) {
+ system("$cmd $old $inc");
+ } else {
+ warn "Don't forget to convert included file `$old' to .m65 format!\n";
+ }
+ return $inc;
+}
+
+sub do_subs {
+ # Do the dirty work of the substitutions. Only reason we have this
+ # as a subroutine of its own is for profiling purposes (and we do
+ # spend a *lot* of time here!)
+ my $line = shift;
+
+ for($line) {
+ s/^(\@?\w+):/$1/; # no colons after labels, in atasm
+ s/%/~/g; # binary constant
+ s/!=/<>/g; # inequality
+
+ s/^(\s+)\.?echo(.*)/;;;;;$1.warn$2/i &&
+ do { warn "$in, line $.:\n\t`.warn' not fully compatible with dasm's `echo', commented out\n" }
+ && next;
+
+ # This is supposed to change e.g. `bpl .label' to `bpl @label'
+ s/^(\s+)([a-z]{3})(\s+)\.(\w+)/$1$2$3\@$4/i
+ && next;
+
+
+ s/{(\d)}/%$1/g; # macro arg (gotta do this *after* bin. constants!)
+
+# atasm doesn't support shifts, emulate with multiply/divide
+ s/\s*<<\s*(\d+)/"*" . 2**$1/eg;
+ s/\s*>>\s*(\d+)/"\/" . 2**$1/eg;
+
+# atasm chokes sometimes when there's whitespace around an operator
+# unfortunately, a construct like `bne *-1' can't afford to lose the
+# space before the *... why, oh why, does * have to be both multiply and
+# program counter? *sigh*
+
+# s/\s*([-!|\/+*&])\s*/$1/g;
+
+# ARGH. Why does dasm allow `byte #1, #2, #3'... and why do people *use* it?!
+ s/^(\s+)\.?byte(\s+)/$1.byte$2/i && do { s/#//g } && next;
+ s/^(\s+)\.?word(\s+)/$1.word$2/i && do { s/#//g } && next;
+ s/^(\s+)\.?dc\.w(\s+)/$1.word$2/i && do { s/#//g } && next;
+ s/^(\s+)\.?dc(?:\.b)?(\s+)/$1.byte$2/i && do { s/#//g } && next;
+
+# 20070529 bkw: turn ".DS foo" into ".DC foo 0"
+ s/^(\s+)\.?ds(\s+)(\S+)/$1.dc $3 0 /i && do { s/#//g } && next;
+
+# I really want to add `hex' to atasm. 'til then though, fake with .byte
+ s/^(\s+)\.?hex\s+(.*)/$1 . '.byte ' .
+ unhex($2)/ie && next;
+
+ s/^(\s+)\.?subroutine(.*)/$1.local$2/i && next;
+ s/^(\s+)\.?include(\s+)(.*)/$1 . '.include' . $2 . fix_include($3)/gie
+ && next;
+ s/^(\s+)\.?equ\b/$1=/i && next;
+ s/^(\s+)\.?repeat\b/$1.rept/i && next;
+ s/^(\s+)\.?repend\b/$1.endr/i && next;
+ s/^(\s+)\.?endm\b/$1.endm/i && next;
+ s/^(\s+)\.?org(\s+)([^,]*).*$/$1*=$2$3/i && next;
+ s/^(\s+)\.?incbin\b/$1\.incbin/i && next;
+ s/^(\s+)\.?err(.*)/$1.error$2/i && next; # TODO: see if atasm allows `.error' with no message.
+ s/^(\s+)\.?ifconst\s+(.*)/$1.if .def $2/i
+ && next; # TODO: test this!
+ s/^(\s+)\.?else/$1.else/i && next;
+ s/^(\s+)\.?endif/$1.endif/i && next;
+ s/^(\s+)\.?if\s+(.*)/$1.if $2/i && next;
+
+ # stuff that doesn't work:
+ s/^(\s+)(\.?seg(\..)?\s.*)/;;;;; dasm2atasm: `seg' not supported by atasm\n;;;;;$1$2/i
+ && next;
+ s/^(\s+)(\.?processor\s.*)/;;;;; dasm2atasm: `processor' not supported by atasm\n;;;;;$1$2/i
+ && next;
+
+ s/^(\s+)sta\.w(\s+)(.*)/;;;;; dasm2atasm: was `sta.w $3', using .byte to generate opcode\n$1.byte \$8d, <$3, >$3/i
+ && next;
+
+ s/^(\s+)stx\.w(\s+)(.*)/;;;;; dasm2atasm: was `stx.w $3', using .byte to generate opcode\n$1.byte \$8e, <$3, >$3/i
+ && next;
+
+ s/^(\s+)sta\.w(\s+)(.*)/;;;;; dasm2atasm: was `sty.w $3', using .byte to generate opcode\n$1.byte \$8c, <$3, >$3/i
+ && next;
+
+ # atasm lacks `align', so make up for it with a macro
+ if(s/(\s)\.?align(\s+)(.*)/$1ALIGN$2$3/i) {
+ if(!$align_defined) { # only gotta define it if not already defined.
+ for($align_macro) {
+ $_ =~ s/^/($linenum += 10) . " "/gme if $linenum;
+ $_ =~ s/\n/\x9b/g if $a8eol;
+ }
+
+ print OUT $align_macro; # no, I wouldn't use these globals in a CS class assignment.
+ $align_defined++;
+ }
+ next;
+ }
+
+ # macros. This is by far the biggest pain in the ass yet.
+ s/(\s)\.?mac\b/$1.macro/i;
+ if(/(\s)\.macro(\s+)(\w+)/) {
+ $mac_regex .= "|\\b$3\\b";
+ $mac_sub = get_mac_sub($mac_regex);
+ }
+
+ if(ref $mac_sub) { # if we've found at least one macro so far...
+ &$mac_sub; # CAPITALIZE everything matching a macro name
+ } # note: this code assumes macros are *always* defined before they're
+ # used. atasm requires this, but does dasm?
+
+ }
+ return $line;
+}
+
+## main() ##
+
+$ca65 = 0;
+$a8eol = 0;
+$linenum = 0;
+$recursive = 0;
+
+$cmd = $0;
+
+while($ARGV[0] =~ /^-/i) {
+ my $opt = shift;
+ $cmd .= " $opt";
+
+ if($opt eq "-c") {
+ $ca65++;
+ } elsif($opt eq "-a") {
+ $a8eol++;
+ } elsif($opt eq "-l") {
+ $linenum = 1000;
+ } elsif($opt eq "-m") {
+ $a8eol++;
+ $linenum = 1000;
+ } elsif($opt eq "-r") {
+ $recursive++;
+ } elsif($opt eq "--") {
+ last;
+ } else {
+ warn "Unknown option '$opt'\n";
+ usage;
+ }
+}
+
+if($ca65 && ($linenum || $a8eol)) {
+ die "Can't use line numbers and/or Atari EOLs with ca65 output\n";
+}
+
+$align_macro = <<EOF;
+;;;;;; ALIGN macro defined by dasm2atasm
+ .macro ALIGN
+ *= [[*/%1]+1] * %1
+ .endm
+EOF
+
+$align_defined = 0; # we only need to emit the macro definition once.
+
+$in = shift || usage;
+$out = shift;
+
+($out = $in) =~ s/(\.\w+)?$/.m65/ unless $out;
+
+die "$0: can't use $in for both input and output\n" if $out eq $in;
+
+open IN, "<$in" or die "Can't read $in: $!\n";
+open OUT, ">$out" or die "Can't write to $out: $!\n";
+
+$hdr = <<EOF;
+;;; Converted from DASM syntax with command:
+; $cmd $in $out
+
+EOF
+
+for($hdr) {
+ $_ =~ s/^/($linenum += 10) . " "/gme if $linenum;
+ $_ =~ s/\n/\x9b/g if $a8eol;
+}
+
+print OUT $hdr;
+
+if($ca65) {
+ print OUT <<EOF;
+;;; ca65 features enabled by dasm2atasm
+; To build with ca65:
+; ca65 -o foo.o -t none foo.asm
+; ld65 -o foo.bin -t none foo.o
+.FEATURE pc_assignment
+.FEATURE labels_without_colons
+
+EOF
+}
+
+$mac_regex = "!THIS_ISNT_SUPPOSED_TO_MATCH";
+$mac_sub = ""; # this will be the code ref we call to match $mac_regex
+
+while(<IN>) {
+ chomp;
+ s/\r//; # you might not want this on dos/win, not sure if it matters.
+ $label = "";
+
+ if(/^(\w+)\s*=\s*\1/i) {
+ print OUT ";;;;; dasm2atasm: labels are case-insensitive in atasm\n";
+ $line = ";;;;; $_ ; This assignment is an error in atasm";
+ next;
+ }
+
+# do this before we split out the label:
+ s/^\./\@/; # local label (dot in dasm, @ in atasm)
+
+ if(s/^([^:;\s]*):?(\s+)/$2/) {
+ $label = $1;
+ }
+
+ ($line, $comment) = split /;/, $_, 2;
+ next unless $line;
+
+ $line = do_subs($line);
+
+} continue {
+ if($linenum) {
+ print OUT "$linenum ";
+ $linenum += 10;
+ }
+
+ print OUT $label if $label;
+ print OUT $line if $line;
+ print OUT ";$comment" if $comment;
+ print OUT ($a8eol ? "\x9b" : "\n");
+}
diff --git a/dasm2atasm.1 b/dasm2atasm.1
new file mode 100644
index 0000000..c4738ad
--- /dev/null
+++ b/dasm2atasm.1
@@ -0,0 +1,244 @@
+.\" 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 "DASM2ATASM" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py dasm2atasm.rst > dasm2atasm.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+dasm2atasm \fB\-[aclmr]\fP \fIinfile.dasm\fP [\fIoutfile.m65\fP]
+.SH DESCRIPTION
+.sp
+\fBdasm2atasm\fP tries its best to convert \fBdasm\fP\(aqs syntax into something that
+\fBatasm\fP or \fBca65\fP can use. Since \fBatasm\fP\(aqs syntax is 99% compatible with that
+of \fBMAC/65\fP, \fBdasm2atasm\fP can be used for that as well.
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \-a
+Atari EOLs. The output will have all UNIX \fB\en\fP characters replaced
+with the EOL character \fB0x9b\fP used on the Atari.
+.TP
+.B \-c
+ca65 output. See \fBCA65 NOTES\fP, below.
+.TP
+.B \-l
+Line numbers. Each line in the output file will be numbered,
+starting from \fB1000\fP and counting by \fB10\fP\&.
+.TP
+.B \-m
+MAC/65 mode. Shortcut for \fB\-a \-l\fP\&. Output will be suitable for
+loading in MAC/65 with the ENTER command (not LOAD!).
+.TP
+.B \-r
+Process include files recursively. This is done by spawning a
+new \fBdasm2atasm\fP process for each included file, which is somewhat
+resource\-intensive if there are lots of nested include files.
+.UNINDENT
+.SH NOTES
+.sp
+\fBdasm2atasm\fP is written in Perl, so it requires a Perl interpreter
+to be available at runtime. If your installed perl binary is not located
+at \fB/usr/bin/perl\fP, simply edit the \fBdasm2atasm\fP script and
+change the location of perl in the first line (the one beginning with
+\fI#!/usr/bin/perl\fP). Alternatively, you may run \fBdasm2atasm\fP with
+a command like \fBperl dasm2atasm\fP, though the \fB\-r\fP option will
+not work correctly in that case.
+.sp
+There are a few \fBdasm\fP pseudo\-ops that just aren\(aqt present in
+\fBatasm\fP:
+.INDENT 0.0
+.TP
+.B \fIprocessor\fP
+\fBdasm\fP supports several target CPUs with different instruction sets,
+and requires a \fBprocessor\fP directive in the source code to set the
+CPU type. \fBatasm\fP only supports the 6502. \fBdasm2atasm\fP includes
+the \fBprocessor\fP directive as a comment, in the output file.
+echo
+.sp
+\fBdasm\fP\(aqs \fBecho\fP directive allows multiple arguments, and can interpolate
+values. Example:
+.INDENT 7.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+echo $100\-*, " bytes of zero page left"
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+\fBatasm\fP\(aqs closest equivalent is \fB\&.warn\fP, but it only accepts one
+argument, which it treats as a constant string. \fBdasm2atasm\fP includes
+all \fBecho\fPs in the input as comments in the output.
+.TP
+.B \fIseg, seg.u\fP
+\fBatasm\fP doesn\(aqt support these at all. \fBdasm2atasm\fP\(aqs output will
+include them as comments.
+.UNINDENT
+.sp
+\fIsta.w, sty.w, stx.w\fP
+.INDENT 0.0
+.INDENT 3.5
+\fBatasm\fP doesn\(aqt provide a way to force word addressing, when the operand
+of a store instruction will allow zero page addressing to be used. You\(aqll
+run into this a lot in Atari 2600 code, or any other 6502 code that has to
+maintain sync with an external piece of hardware: using word addressing
+causes the 6502 to use an extra CPU cycle, which is a commonly used
+method of adding a 1\-cycle delay.
+.sp
+\fBdasm2atasm\fP will convert any such instructions into \fB\&.byte\fP
+pseudo\-ops that will generate the correct code. Example:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+;;;;; dasm2atasm: was \(gasta.w COLUPF\(aq, using .byte to generate opcode
+\&.byte $8d, <COLUPF, >COLUPF
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+\fBdasm\fP actually supports the \fI\&.w\fP and \fI\&.b\fP extensions to
+most instructions and a few pseudo\-ops. \fBdasm2atasm\fP doesn\(aqt handle
+this in the general case.
+.UNINDENT
+.UNINDENT
+.sp
+\fI\&. (dot) as program counter\fP
+.INDENT 0.0
+.INDENT 3.5
+\fBdasm\fP allows the use of either \fB\&.\fP or \fB*\fP for the current
+program counter. \fBatasm\fP only allows \fB*\fP\&. \fBdasm2atasm\fP doesn\(aqt
+attempt to translate this, as it doesn\(aqt include a full expression parser.
+.UNINDENT
+.UNINDENT
+.sp
+\fI( ) (parentheses)\fP
+.INDENT 0.0
+.INDENT 3.5
+\fBdasm\fP allows parentheses or square brackets in expressions:
+\fI(1+2)*3\fP and \fI[1+2]*3\fP are equivalent. \fBatasm\fP
+only allows square brackets. \fBdasm2atasm\fP does not attempt to
+translate this currently, though a future version may.
+.UNINDENT
+.UNINDENT
+.sp
+\fImacro arguments\fP
+.INDENT 0.0
+.INDENT 3.5
+\fBdasm\fP uses \fB{1}\fP, \fB{2}\fP, etc. to refer to macro arguments
+within a macro definition. \fBatasm\fP uses \fB$1\fP, \fB$2\fP, etc.
+\fBdasm2atasm\fP makes no attempt to translate these.
+.UNINDENT
+.UNINDENT
+.SH CA65 NOTES
+.sp
+\fBca65\fP output is actually the same as the \fBatasm\fP output, with
+the addition of \fB\&.FEATURE pc_assignment\fP and
+\fB\&.FEATURE labels_without_colons\fP at the beginning of the source.
+.sp
+Most Atari source written with \fBdasm\fP is intended to be assembled
+with the \fB\-f3\fP option (raw output). The equvalent option for
+\fBatasm\fP is \fB\-r\fP, and for \fBca65\fP (and its linker,
+\fBld65\fP) it is \fB\-t none\fP\&.
+.sp
+However, \fBca65\fP\(aqs linker (\fBld65\fP) will not correctly handle
+files with multiple \fBORG\fP directives, when using \fB\-t none\fP\&. Example:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+start ORG $0600 ; or *= $0600 in atasm
+ ; 5 bytes of code here
+ LDA #1 ; example code, doesn\(aqt do anything useful
+ STA 0
+ RTS
+
+; dasm or atasm will include 251 bytes of filler here
+; (dasm fills with $00 by default; atasm fills with $ff)
+
+ORG $0700 ; or *= $0700 in atasm
+ ; 5 more bytes of code here
+ LDA #2
+ STA 1
+ RTS
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+With \fBdasm \-f3\fP or \fBatasm \-r\fP, the output will be 261 bytes of
+object code. With \fBca65 \-t none\fP and \fBld65 \-t none\fP, the filler
+bytes will not be included, and the output will be only 10 bytes long.
+The correct solution to this would be to rewrite the code so that it
+doesn\(aqt include any Atari\-specific header information (e.g. binary
+load headers as data bytes), then use \fB\-t atari\fP to have \fBca65\fP
+generate the binary load headers (though as far as the author knows,
+\fBca65\fP doesn\(aqt know how to generate other records such as Atari
+boot disk or cartridge headers).
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/dasm2atasm.rst b/dasm2atasm.rst
new file mode 100644
index 0000000..cc7b582
--- /dev/null
+++ b/dasm2atasm.rst
@@ -0,0 +1,162 @@
+.. RST source for dasm2atasm(1) man page. Convert with:
+.. rst2man.py dasm2atasm.rst > dasm2atasm.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+==========
+dasm2atasm
+==========
+
+----------------------------------------------------------------------
+Convert 6502 assembly source from dasm syntax to atasm or ca65 syntax.
+----------------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+dasm2atasm **-[aclmr]** *infile.dasm* [*outfile.m65*]
+
+DESCRIPTION
+===========
+
+**dasm2atasm** tries its best to convert **dasm**'s syntax into something that
+**atasm** or **ca65** can use. Since **atasm**'s syntax is 99% compatible with that
+of **MAC/65**, **dasm2atasm** can be used for that as well.
+
+OPTIONS
+=======
+
+-a
+ Atari EOLs. The output will have all UNIX **\\n** characters replaced
+ with the EOL character **0x9b** used on the Atari.
+
+-c
+ ca65 output. See **CA65 NOTES**\, below.
+
+-l
+ Line numbers. Each line in the output file will be numbered,
+ starting from **1000** and counting by **10**.
+
+-m
+ MAC/65 mode. Shortcut for **-a -l**. Output will be suitable for
+ loading in MAC/65 with the ENTER command (not LOAD!).
+
+-r
+ Process include files recursively. This is done by spawning a
+ new **dasm2atasm** process for each included file, which is somewhat
+ resource-intensive if there are lots of nested include files.
+
+NOTES
+=====
+
+**dasm2atasm** is written in Perl, so it requires a Perl interpreter
+to be available at runtime. If your installed perl binary is not located
+at **/usr/bin/perl**, simply edit the **dasm2atasm** script and
+change the location of perl in the first line (the one beginning with
+*#!/usr/bin/perl*). Alternatively, you may run **dasm2atasm** with
+a command like **perl dasm2atasm**, though the **-r** option will
+not work correctly in that case.
+
+There are a few **dasm** pseudo-ops that just aren't present in
+**atasm**:
+
+*processor*
+ **dasm** supports several target CPUs with different instruction sets,
+ and requires a **processor** directive in the source code to set the
+ CPU type. **atasm** only supports the 6502. **dasm2atasm** includes
+ the **processor** directive as a comment, in the output file.
+ echo
+
+ **dasm**'s **echo** directive allows multiple arguments, and can interpolate
+ values. Example::
+
+ echo $100-*, " bytes of zero page left"
+
+ **atasm**'s closest equivalent is **.warn**, but it only accepts one
+ argument, which it treats as a constant string. **dasm2atasm** includes
+ all **echo**\s in the input as comments in the output.
+
+*seg, seg.u*
+ **atasm** doesn't support these at all. **dasm2atasm**'s output will
+ include them as comments.
+
+*sta.w, sty.w, stx.w*
+
+ **atasm** doesn't provide a way to force word addressing, when the operand
+ of a store instruction will allow zero page addressing to be used. You'll
+ run into this a lot in Atari 2600 code, or any other 6502 code that has to
+ maintain sync with an external piece of hardware: using word addressing
+ causes the 6502 to use an extra CPU cycle, which is a commonly used
+ method of adding a 1-cycle delay.
+
+ **dasm2atasm** will convert any such instructions into **.byte**
+ pseudo-ops that will generate the correct code. Example::
+
+ ;;;;; dasm2atasm: was `sta.w COLUPF', using .byte to generate opcode
+ .byte $8d, <COLUPF, >COLUPF
+
+ **dasm** actually supports the *.w* and *.b* extensions to
+ most instructions and a few pseudo-ops. **dasm2atasm** doesn't handle
+ this in the general case.
+
+*. (dot) as program counter*
+
+ **dasm** allows the use of either **.** or ***** for the current
+ program counter. **atasm** only allows *****. **dasm2atasm** doesn't
+ attempt to translate this, as it doesn't include a full expression parser.
+
+*( ) (parentheses)*
+
+ **dasm** allows parentheses or square brackets in expressions:
+ *(1+2)\*3* and *[1+2]\*3* are equivalent. **atasm**
+ only allows square brackets. **dasm2atasm** does not attempt to
+ translate this currently, though a future version may.
+
+*macro arguments*
+
+ **dasm** uses **{1}**, **{2}**, etc. to refer to macro arguments
+ within a macro definition. **atasm** uses **$1**, **$2**, etc.
+ **dasm2atasm** makes no attempt to translate these.
+
+CA65 NOTES
+==========
+
+**ca65** output is actually the same as the **atasm** output, with
+the addition of **.FEATURE pc_assignment** and
+**.FEATURE labels_without_colons** at the beginning of the source.
+
+Most Atari source written with **dasm** is intended to be assembled
+with the **-f3** option (raw output). The equvalent option for
+**atasm** is **-r**, and for **ca65** (and its linker,
+**ld65**) it is **-t none**.
+
+However, **ca65**'s linker (**ld65**) will not correctly handle
+files with multiple **ORG** directives, when using **-t none**. Example::
+
+ start ORG $0600 ; or *= $0600 in atasm
+ ; 5 bytes of code here
+ LDA #1 ; example code, doesn't do anything useful
+ STA 0
+ RTS
+
+ ; dasm or atasm will include 251 bytes of filler here
+ ; (dasm fills with $00 by default; atasm fills with $ff)
+
+ ORG $0700 ; or *= $0700 in atasm
+ ; 5 more bytes of code here
+ LDA #2
+ STA 1
+ RTS
+
+With **dasm -f3** or **atasm -r**, the output will be 261 bytes of
+object code. With **ca65 -t none** and **ld65 -t none**, the filler
+bytes will not be included, and the output will be only 10 bytes long.
+The correct solution to this would be to rewrite the code so that it
+doesn't include any Atari-specific header information (e.g. binary
+load headers as data bytes), then use **-t atari** to have **ca65**
+generate the binary load headers (though as far as the author knows,
+**ca65** doesn't know how to generate other records such as Atari
+boot disk or cartridge headers).
+
+.. include:: manftr.rst
diff --git a/equates.inc b/equates.inc
new file mode 100644
index 0000000..4fd60c8
--- /dev/null
+++ b/equates.inc
@@ -0,0 +1,1384 @@
+;
+; ATARI 800 EQUATE LISTING
+;
+; (version 20070530_bkw)
+;
+; This is a heavily modified copy of Appendix A of the Atari System
+; Reference Manual (with much info added from Appendix B, and from
+; Mapping the Atari and other sources)
+;
+;
+;This listing is based on the original release of Operating System,
+;version A. The vectors shown here were not changed in version B.
+;New equates for XL and XE models are included and noted. Changes
+;from version B to XL/XE are also noted.
+;
+;Most of the equate names given below are the official Atari
+;names. They are in common use but are not mandatory.
+
+; This file can be included in your assembly source, but it's also
+; got a lot of useful human-readable comments. It's meant to serve as
+; a "quick reference" to Atari programmers, particularly ones who use
+; a cross-assembler on a UNIX-ish platform and a text editor that can
+; use "ctags":
+
+; $ ctags equates.inc
+; $ vim mystuff.dasm
+
+; While in vim, press ^] (control-right-bracket) while sitting on a label,
+; to jump to that label's definition in this file. (Also, you can type
+; :tag labelname). You can also do completion on the labels in vim by
+; typing part of a label and pressing ^N (or Tab, if you use the
+; CleverTab script from vimhelp.org)
+
+; GNU Emacs and XEmacs also support ctags, but I've never used them, so
+; I dunno how to do it. I do know that you need to use "Exuberant" ctags,
+; not the ctags that comes with emacs (which doesn't grok 6502 asm).
+
+; If you're using an Atari assembler instead of a cross-assembler, you
+; don't want to use this as-is: all the extra comments make it huge, and
+; it'll be either too large for the Atari's memory, or at least will take
+; a long time to assemble. You can make a comment-less, Atari-compatible
+; version like so:
+
+; perl -ne 's/;.*//; s/\s*$//; print if $_' < equates.inc > small.inc
+
+; ...then use a8eol to convert it to ATASCII format.
+
+; 20061028 bkw: Originally downloaded from:
+
+; http://atrey.karlin.mff.cuni.cz/~pavel/atari/atrb.html
+
+; ...and converted to DASM/ATASM/CA65 format. dasm, atasm, and ca65 use
+; similar enough syntax that this file can be used as-is with any of
+; them. Unfortunately, this means I can't do conditional assembly in this
+; file, since the two assemblers use different semantics... macros are
+; even less compatible :(
+
+; If you use ca65, you need this line in your source:
+
+;; .FEATURE labels_without_colons
+
+; before including this file, or else run ca65 with
+; "--feature labels_without_colons").
+
+; 20070529 bkw: updated, added missing GTIA/POKEY/ANTIC equates,
+; documented where the shadows are for those GTIA/POKEY/ANTIC/PIA
+; registers that have them. Also added a list of error messages and
+; explanation of the cassette buffer layout, and organized the C_*
+; CIO constants. Made minor modifications to get this file to assemble
+; with ATasm as well as DASM.
+
+; I have added a few missing equates: this file only
+; contained OS ROM locations when I got it.
+; I added a few from FMS/DOS as well (e.g. RUNAD and INITAD).
+
+; XL-specific locations in the original file were duplicate labels
+; (e.g. PTIMOT was defined as both $1C and $314), which keeps DASM
+; from being able to assemble the file. I prefixed the XL/XL versions
+; with "XL_"
+
+; Also, I've prefixed the CIO command and AUX1 constants with C_, since some
+; of them conflicted with other labels in the original version.
+
+; Areas listed as "unmapped" are literally not connected to anything.
+; Trying to read from unmapped address space results in reading whatever
+; garbage was on the data bus when the read happened. On my 1200XL, this
+; generally results in all 1's ($FF or 255).
+; In a 400, 600XL or other Atari with less than 48K of RAM, the missing
+; RAM address space is also unmapped.
+
+; Still TODO:
+; - Rest of the DOS/FMS equates
+; - Mark 800-ony locations with OSB_
+; - ifdef code, so the user can set the machine type (OSB or XL),
+; then refer to e.g. PTIMOT and get either OSB_PTIMOT or XL_PTIMOT
+; - Split into separate files? I'd rather not (it's only about 1000 lines)
+
+; This file's mostly intended for new development. It could also be
+; useful for porting old ASM/ED or Mac65 code to DASM, but such code
+; may need work... you can always assemble it with ATasm, in that case,
+; since it's 99.999% source code compatible with Mac65.
+
+; References to "APPENDIX C" and such are referring to the Atari System
+; Reference Manual, a version of which can be found at:
+
+; http://atrey.karlin.mff.cuni.cz/~pavel/atari/atrtblc.html
+
+; References to "Mapping" refer to "Mapping the Atari, Revised Edition",
+; which can be found at:
+
+; http://www.atariarchives.org/mapping/index.php
+
+; I've pasted a few quotes from Mapping into this file; I consider them
+; small enough to be covered under the "fair use" provisions of copyright law
+; (I am not a lawyer, though).
+
+;
+;
+; DEVICE NAMES
+;
+;
+;SCREDT = "E" SCREEN EDITOR
+;KBD = "K" KEYBOARD
+;DISPLY = "S" DISPLAY
+;PRINTR = "P" PRINTER
+;CASSET = "C" CASSETTE
+;DISK = "D" DISK DRIVE
+;
+;
+;
+; STATUS CODES
+;
+;
+
+; 20070529 bkw: These are returned as error codes, though various DOSes
+; also define their own codes (usually in the range 160-255).
+; Errors 2-21 are defined by BASIC.
+; Errors 150-154 are defined by the R: (850 or compatible, RS-232) handler.
+SUCCES = $01 ; 1
+BRKABT = $80 ; 128 BREAK KEY ABORT
+PRVOPN = $82 ; 130 IOCB ALREADY OPEN
+NONDEV = $82 ; 130 NONEXISTANT DEVICE
+WRONLY = $83 ; 131 OPENED FOR WRITE ONLY
+NVALID = $84 ; 132 INVALID COMMAND
+NOTOPN = $85 ; 133 DEVICE OR FILE NOT OPEN
+BADIOC = $86 ; 134 INVALID IOCB NUMBER
+RDONLY = $87 ; 135 OPENED FOR READ ONLY
+EOFERR = $88 ; 136 END OF FILE
+TRNRCD = $89 ; 137 TRUNCATED RECORD
+TIMOUT = $8A ; 138 PERIPHERAL TIME OUT
+DNACK = $8B ; 139 DEVICE DOES NOT ACKNOWLEDGE
+FRMERR = $8C ; 140 SERIAL BUS FRAMING ERROR
+CRSROR = $8D ; 141 CURSOR OUT OF RANGE
+OVRRUN = $8E ; 142 SERIAL BUS DATA OVERRUN
+CHKERR = $8F ; 143 SERIAL BUS CHECKSUM ERROR
+DERROR = $90 ; 144 PERIPHERAL DEVICE ERROR
+BADMOD = $91 ; 145 NON EXISTANT SCREEN MODE
+FNCNOT = $92 ; 146 FUNCTION NOT IMPLEMENTED
+SCRMEM = $93 ; 147 NOT ENOUGH MEMORY FOR SCREEN MODE
+
+; BASIC error codes (also used by e.g. Basic XL/XE and Turbo BASIC):
+;; 2: Insufficient Memory
+;; 3: Value Error
+;; 4: Too Many Variables
+;; 5: String Length Error
+;; 6: Out of Data Error
+;; 7: Number Greater than 32767
+;; 8: Input Statement Error
+;; 9: Array or String DIM Error
+;; 10: Argument Stack Overflow
+;; 11: Floating Point Overflow or Underflow Error
+;; 12: Line Not Found
+;; 13: No Matching FOR Statement
+;; 14: Line Too Long
+;; 15: GOSUB or FOR Line Deleted
+;; 16: RETURN Error
+;; 17: Garbage Error
+;; 18: Invalid String Character
+;; 19: LOAD Program Too Long
+;; 20: Bad Channel Number
+;; 21: LOAD File Error
+
+; 850/R: error codes:
+;; 150: Serial Port Already Open
+;; 151: Concurrent Mode Not Enabled
+;; 152: Illegal User-Supplied Buffer
+;; 153: Active Concurrent Mode Error
+;; 154: Concurrent Mode Not Active
+
+; DOS error codes (DOS 2.0S only; other DOSes may define other errors)
+;; 160: Device Number Error
+;; 161: Too Many OPEN Files
+;; 162: Disk Full
+;; 163: Fatal System Error
+;; 164: File Number Mismatch
+;; 165: Bad File Name
+;; 166: POINT Data Length Error
+;; 167: File Locked
+;; 168: Invalid XIO Command
+;; 169: Directory Full
+;; 170: File Not Found
+;; 171: POINT Invalid
+;; 172: DOS 1 File
+;; 173: Bad Sector
+;; 255: FORMATTING Error (DOS 2.5)
+
+;
+;
+;
+;
+; COMMAND CODES FOR CIO
+;
+;
+
+; Command byte goes in ICCOM,x
+
+;; General-purpose commands:
+C_OPEN = $03 ; 3 OPEN (BASIC OPEN)
+C_GETREC = $05 ; 5 GET RECORD
+C_GETCHR = $07 ; 7 GET BYTE
+C_PUTREC = $09 ; 9 WRITE RECORD
+C_PUTCHR = $0B ; 11 PUT-BYTE
+C_CLOSE = $0C ; 12
+C_STATUS = $0D ; 13
+C_SPECIL = $0E ; 14 BEGINNING OF SPECIAL COMMANDS (aka XIO)
+;; Commands for S: device:
+C_DRAWLN = $11 ; 17 SCREEN DRAW (BASIC DRAWTO)
+C_FILLIN = $12 ; 18 SCREEN FILL
+;; Commands for D: device (only when DOS is loaded):
+C_RENAME = $20 ; 32
+C_DELETE = $21 ; 33
+C_LOCK = $23 ; 35
+C_UNLOCK = $24 ; 36
+C_POINT = $25 ; 37
+C_NOTE = $26 ; 38
+
+; AUX1 modes (ICAX1,x or 2nd parameter of BASIC OPEN command):
+C_OPREAD = $04 ; 4 OPEN FOR INPUT
+C_OWRITE = $08 ; 8 OPEN FOR OUTPUT
+C_APPEND = $09 ; 9 OPEN TO APPEND TO END OF DISK FILE
+C_OUPDAT = $0C ; 12 OPEN FOR INPUT AND OUTPUT AT THE SAME TIME
+;; D: (DOS) only:
+C_OPDIR = $06 ; 6 OPEN TO DISK DIRECTORY
+;; S: only:
+C_MXDMOD = $10 ; 16 OPEN TO SPLIT SCREEN (MIXED MODE)
+C_INSCLR = $20 ; 32 OPEN TO SCREEN BUT DON'T ERASE
+;; C: only:
+C_NOIRG = $80 ; 128 NO GAP CASSETTE MODE
+
+;; Command bytes (ICCOM) for the RS-232 (R:) device:
+;;
+;; Output partial block 32 $20
+;; Control RTS,XMT,DTR 34 $22
+;; Baud, stop bits, word size 36 $24
+;; Translation mode 38 $26
+;; Concurrent mode 40 $28
+;;
+;; (see the 850 Interface Manual for details)
+
+
+; SIO command bytes (not part of CIO):
+S_DFRMAT = $21 ; 33 FORMAT DISK (RESIDENT DISK HANDLER (RDH))
+S_PTSECT = $50 ; 80 RDH PUT SECTOR
+S_GTSECT = $52 ; 82 RDH GET SECTOR
+S_DSTAT = $53 ; 83 RDH GET STATUS
+S_PSECTV = $57 ; 87 RDH PUT SECTOR AND VERIFY
+; Various other SIO commands are supported by different drives
+
+; 20061028 bkw: CR/EOL not really part of CIO, but useful:
+CR = $9B ; 155 CARRIAGE RETURN (EOL)
+EOL = CR ; defined in SYSEQU.ASM
+
+;
+IOCBSZ = $10 ; 16 IOCB SIZE
+MAXIOC = $80 ; 128 MAX IOCB BLOCK SIZE
+IOCBF = $FF ; 255 IOCB FREE
+;
+LEDGE = $02 ; 2 DEFAULT LEFT MARGIN
+REDGE = $27 ; 39 DEFAULT RIGHT MARGIN
+
+; OS VARIABLES
+;
+; PAGE 0
+;
+LINZBS = $00 ; 0 (800) FOR ORIGINAL DEBUGGER
+; $00 0 (XL) RESERVED
+NGFLAG = $01 ; 1 (XL) FOR POWER-UP SELF TEST
+CASINI = $02 ; 2
+RAMLO = $04 ; 4 POINTER FOR SELF TEST
+TRAMSZ = $06 ; 6 TEMPORARY RAM SIZE
+TSTDAT = $07 ; 7 TEST DATA
+WARMST = $08 ; 8
+BOOTQ = $09 ; 9 SUCCESSFUL BOOT FLAG
+; aka BOOT? in the OS source, but some assemblers don't support ? in labels
+DOSVEC = $0A ; 10 PROGRAM RUN VECTOR
+DOSINI = $0C ; 12 PROGRAM INITIALIZATION
+APPMHI = $0E ; 14 DISPLAY LOW LIMIT
+POKMSK = $10 ; 16 IRQ ENABLE FLAGS (shadow for IRQEN)
+BRKKEY = $11 ; 17 FLAG
+RTCLOK = $12 ; 18 3 BYTES, MSB FIRST
+BUFADR = $15 ; 21 INDIRECT BUFFER ADDRESS
+ICCOMT = $17 ; 23 COMMAND FOR VECTOR
+DSKFMS = $18 ; 24 DISK FILE MANAGER POINTER
+DSKUTL = $1A ; 26 DISK UTILITY POINTER (DUP.SYS)
+PTIMOT = $1C ; 28 (800) PRINTER TIME OUT REGISTER
+ABUFPT = $1C ; 28 (XL) RESERVED
+PBPNT = $1D ; 29 (800) PRINTER BUFFER POINTER
+; $1D ; 29 (XL) RESERVED
+PBUFSZ = $1E ; 30 (800) PRINTER BUFFER SIZE
+; $1E ; 30 (XL) RESERVED
+PTEMP = $1F ; 31 (800) TEMPORARY REGISTER (PTEMP deleted in XL OS)
+; $1F ; 31 (XL) RESERVED
+ZIOCB = $20 ; 32 ZERO PAGE IOCB
+ICHIDZ = $20 ; 32 HANDLER INDEX NUMBER (ID)
+ICDNOZ = $21 ; 33 DEVICE NUMBER
+ICCOMZ = $22 ; 34 COMMAND
+ICSTAZ = $23 ; 35 STATUS
+ICBALZ = $24 ; 36 BUFFER POINTER LOW BYTE
+ICBAHZ = $25 ; 37 BUFFER POINTER HIGH BYTE
+ICPTLZ = $26 ; 38 PUT ROUTINE POINTER LOW
+ICPTHZ = $27 ; 39 PUT ROUTINE POINTER HIGH
+ICBLLZ = $28 ; 40 BUFFER LENGTH LOW
+ICBLHZ = $29 ; 41
+ICAX1Z = $2A ; 42 AUXILIARY INFORMATION BYTE 1
+ICAX2Z = $2B ; 43
+ICSPRZ = $2C ; 44 TWO SPARE BYTES (CIO USE)
+ICIDNO = $2E ; 46 IOCB NUMBER X 16
+CIOCHR = $2F ; 47 CHARACTER BYTE FOR CURRENT OPERATION
+;
+STATUS = $30 ; 48 STATUS STORAGE
+CHKSUM = $31 ; 49 SUM WITH CARRY ADDED BACK
+BUFRLO = $32 ; 50 DATA BUFFER LOW BYTE
+BUFRHI = $33 ; 51
+BFENLO = $34 ; 52 ADDRESS OF LAST BUFFER BYTE +1 (LOW)
+BFENHI = $35 ; 53
+CRETRY = $36 ; 54 (800) NUMBER OF COMMAND FRAME RETRIES
+XL_LTEMP = $36 ; 54 (XL) LOADER TEMPORARY STORAGE, 2 BYTES
+DRETRY = $37 ; 55 (800) DEVICE RETRIES
+BUFRFL = $38 ; 56 BUFFER FULL FLAG
+RECVDN = $39 ; 57 RECEIVE DONE FLAG
+XMTDON = $3A ; 58 TRANSMISSION DONE FLAG
+CHKSNT = $3B ; 59 CHECKSUM-SENT FLAG
+NOCKSM = $3C ; 60 CHECKSUM-DOES-NOT-FOLLOW-DATA FLAG
+BPTR = $3D ; 61
+FTYPE = $3E ; 62
+FEOF = $3F ; 63
+FREQ = $40 ; 64
+;
+SOUNDR = $41 ; 65 0=QUIET I/O
+CRITIC = $42 ; 66 CRITICAL FUNCTION FLAG, NO DEFFERED VBI
+FMSZPG = $43 ; 67 DOS ZERO PAGE, 7 BYTES
+CKEY = $4A ; 74 (800) START KEY FLAG
+XL_ZCHAIN = $4A ; 74 (XL) HANDLER LOADER TEMP, 2 BYTES
+CASSBT = $4B ; 75 (800) CASSETTE BOOT FLAG
+DSTAT = $4C ; 76 DISPLAY STATUS
+;
+ATRACT = $4D ; 77
+DRKMSK = $4E ; 78 ATTRACT MASK
+COLRSH = $4F ; 79 ATTRACT COLOR SHIFTER (EORed WITH GRAPHICS)
+;
+TMPCHR = $50 ; 80
+HOLD1 = $51 ; 81
+LMARGN = $52 ; 82 SCREEN LEFT MARGIN REGISTER
+RMARGN = $53 ; 83 SCREEN RIGHT MARGIN
+ROWCRS = $54 ; 84 CURSOR ROW
+COLCRS = $55 ; 85 CURSOR COLUMN, 2 BYTES
+DINDEX = $57 ; 87 DISPLAY MODE
+SAVMSC = $58 ; 88 SCREEN ADDRESS
+OLDROW = $5A ; 90 CURSOR BEFORE DRAW OR FILL
+OLDCOL = $5B ; 91
+OLDCHR = $5D ; 93 DATA UNDER CURSOR
+OLDADR = $5E ; 94 CURSOR ADDRESS
+XL_FKDEF = $60 ; 96 (XL) FUNCTION KEY DEFINITION POINTER (LSB/MSB)
+NEWROW = $60 ; 96 (800) DRAWTO DESTINATION
+NEWCOL = $61 ; 97 (800) DRAWTO DESTINATION, 2 BYTES
+XL_PALNTS = $62 ; 98 (XL) EUROPE/NORTH AMERICA TV FLAG
+LOGCOL = $63 ; 99 LOGICAL LINE COLUMN POINTER
+MLTTMP = $66 ; 102
+OPNTMP = $66 ; 102 TEMPORARY STORAGE FOR CHANNEL OPEN
+SAVADR = $68 ; 104
+RAMTOP = $6A ; 106 START OF ROM (END OF RAM + 1), HIGH BYTE ONLY
+BUFCNT = $6B ; 107 BUFFER COUNT
+BUFSTR = $6C ; 108 POINTER USED BY EDITOR
+BITMSK = $6E ; 110 POINTER USED BY EDITOR
+SHFAMT = $6F ; 111
+ROWAC = $70 ; 112
+COLAC = $72 ; 114
+ENDPT = $74 ; 116
+DELTAR = $76 ; 118
+DELTAC = $77 ; 119
+ROWINC = $79 ; 121 (800)
+XL_KEYDEF = $79 ; 121 (XL) KEY DEFINITION POINTER, 2 BYTES
+COLINC = $7A ; 122 (800)
+SWPFLG = $7B ; 123 NON 0 IF TEXT AND REGULAR RAM IS SWAPPED
+HOLDCH = $7C ; 124 CH MOVED HERE BEFORE CTRL AND SHIFT
+INSDAT = $7D ; 125 used by S: handler, tmp for char under cursor
+COUNTR = $7E ; 126 used by XIO DRAW command (2 bytes)
+
+; $80 to $FF are free if BASIC and floating point are not used.
+; If BASIC is not used, but FP is, $80 to $D0 are still free.
+; There is no way to use BASIC without constantly using FP, as all BASIC
+; numbers are FP (even "integers" such as line numbers).
+ZROFRE = $80 ; 128 FREE ZERO PAGE, 84 BYTES
+
+; BASIC zero page variables:
+LOMEM = $80 ; 128 LSB, BASIC start-of-memory pointer
+; $81 ; 129 MSB, LOMEM (not to be confused with the OS's MEMLO!)
+VNTP = $82 ; 130 LSB, BASIC start of Variable Name Table pointer
+; $83 ; 131 MSB, VNTP
+VNTD = $84 ; 132 LSB, BASIC end of Variable Name Table pointer (+1 byte)
+; $85 ; 133 MSB, VNTP
+VVTP = $86 ; 134 LSB, BASIC start of Variable Value Table pointer
+; $87 ; 135 MSB, VVTP
+STMTAB = $88 ; 136 LSB, BASIC start of Statement Table pointer
+; $89 ; 137 MSB, STMTAB
+STMCUR = $8A ; 138 LSB, BASIC current statement pointer
+; $8B ; 139 MSB, STMCUR
+STARP = $8C ; 140 LSB, BASIC current string/array table pointer
+; $8D ; 141 MSB, STARP (also points to end of BASIC program)
+RUNSTK = $8E ; 142 LSB, BASIC runtime stack pointer
+; $8F ; 143 MSG, RUNSTK
+; BASIC and the OS both use the name MEMTOP; I've renamed the BASIC one.
+BAS_MEMTOP = $90 ; 144 LSB, pointer to top of BASIC memory
+; $91 ; 145 MSB, BAS_MEMTOP
+MEOLFLG = $92 ; 146 "modified EOL flag register", whatever that is
+; $93 ; 147 listed as "spare" by Mapping's Errata
+;COX = $94 ; 148 current output index (?)
+POKADR = $95 ; 149 LSB, address of last POKE location
+; ; 150 MSB, POKADR
+
+; Locations $96 to $B5 are used for various purposes by BASIC,
+; and most of them are of little or no interest, even for someone
+; writing assembly code meant to run as a USR() routine, so I haven't
+; bothered listing them all here. See Compute! Books' "Atari BASIC Sourcebook"
+; for the gory details. In fact, you can see it here:
+
+; http://users.telenet.be/kim1-6502/6502/absb.html
+
+; It's fascinating (at least it is to me)... includes full source code
+; to Atari BASIC!
+
+; DATAD and DATALN are reset to 0 by BASIC RESTORE command.
+DATAD = $B6 ; 182 the data element being read (e.g. 10 for 10th item
+ ; in a DATA statement)
+DATALN = $B7 ; 183 LSB current DATA statement line number
+; $B8 ; 184 MSB, DATALN
+;ERRNUM = $B9 ; 185 Most recent error number. Gets cleared before you
+ ; can PEEK it; use ERRSAVE instead.
+STOPLN = $BA ; 186 LSB, line where a program stopped by STOP/break/error
+; $BB ; 187 MSB, STOPLN
+; what are $BC and $BD for?
+SAVCUR = $BE ; 190 Saves the current line address (LSB?)
+; $BF ; 191 presumably, the MSB of SAVCUR?
+IOCMD = $C0 ; 192, I/O Command (Mapping Errata)
+IODVC = $C1 ; 193, I/O Device (Mapping Errata)
+PROMPT = $C2 ; 194, Prompt character (Mapping Errata, presumably INPUT?)
+ERRSAVE = $C3 ; 195 Error code that caused a stop or TRAP
+;TEMPA = $C4 ; 196 a 2-byte temp
+;ZTEMP2 = $C6 ; 198 a 2-byte temp
+COLOR = $C8 ; 200 Stores color from COLOR command
+PTABW = $C9 ; 201 Number of columns between tab stops
+ ; (for PRINT with commas, not the TAB key)
+LOADFLG = $CA ; 202 Load in progress flag. I can tell you from bitter
+ ; experience that BASIC clears this often.
+
+; $CB - $CF are unused by BASIC or the ASM/ED cart.
+; $D0 and $D1 are unused by BASIC (does that mean they *are* used by ASM/ED?)
+
+; $D2 and $D3 are used by BASIC. Mapping Errata calls them the "BASIC
+; floating-point work area". They get cleared to 0 by BASIC, probably
+; every time a FP number is used (e.g. "POKE 210,1:? PEEK(210)" prints 0).
+; The BASIC source code labels $D2 as TVTYPE and VTYPE, and $D3 as
+; TVNUM and VNUM.
+
+; Floating point zero page variables:
+FPZRO = $D4 ; 212 FLOATING POINT RAM, 43 BYTES
+ ; (20070530 bkw: pretty sure that comment is wrong, and
+ ; should read 44 bytes; see $FF below)
+FR0 = $D4 ; 212 FP REGISTER 0 (also used by BASIC for USR() return val)
+ ; (FR0/FRE/FR1/FR2 are each 6 bytes long)
+FRE = $DA ; 218
+FR1 = $E0 ; 224 FP REGISTER 1
+FR2 = $E6 ; 230 FP REGISTER 2
+FRX = $EC ; 236 SPARE
+EEXP = $ED ; 237 VALUE OF E
+NSIGN = $ED ; 237 SIGN OF FP NUMBER
+ESIGN = $EF ; 239 SIGN OF FP EXPONENT
+FCHFLG = $F0 ; 240 FIRST CHARACTER FLAG
+DIGRT = $F1 ; 241 NUMBER OF DIGITS RIGHT OF DECIMAL POINT
+CIX = $F2 ; 242 INPUT INDEX
+INBUFF = $F3 ; 243 POINTER TO ASCII FP NUMBER
+ZTEMP1 = $F5 ; 245
+ZTEMP4 = $F7 ; 247
+ZTEMP3 = $F9 ; 249
+DEGFLG = $FB ; 251
+RADFLG = $FB ; 251 0=RADIANS, 6=DEGREES
+FLPTR = $FC ; 252 POINTER TO BCD FP NUMBER (2 bytes)
+FPTR2 = $FE ; 254 maybe a 2nd pointer to an FP number? (2 bytes)
+; $FF ; 255 This *definitely* is used by the FP package
+ ; Try: POKE 255,0:? SIN(1):? PEEK(255)
+
+;
+; PAGE 1
+;
+; 65O2 STACK
+;
+;
+
+;
+;
+; PAGE 2
+;
+;
+; 20070529 bkw: Bytes listed as "spare" should NOT be used for your own
+; purposes. They may not really be unused (just undocumented), and/or they
+; may be unused on the 800 but not the XL (or vice versa).
+INTABS = $0200 ; 512 INTERRUPT RAM
+VDSLST = $0200 ; 512 NMI VECTOR
+VPRCED = $0202 ; 514 PROCEED LINE IRQ VECTOR
+VINTER = $0204 ; 516 INTERRUPT LINE IRQ VECTOR
+VBREAK = $0206 ; 518 break key IRQ vector (not in OS rev. A)
+VKEYBD = $0208 ; 520 keyboard IRQ vector (not break/console keys)
+VSERIN = $020A ; 522 SERIAL INPUT READY IRQ
+VSEROR = $020C ; 524 SERIAL OUTPUT READY IRQ
+VSEROC = $020E ; 526 SERIAL OUTPUT COMPLETE IRQ
+VTIMR1 = $0210 ; 528 TIMER 1 IRQ vector
+VTIMR2 = $0212 ; 530 TIMER 2 IRQ vector
+VTIMR4 = $0214 ; 532 TIMER 4 IRQ vector
+VIMIRQ = $0216 ; 534 IRQ VECTOR
+CDTMV1 = $0218 ; 536 COUNTDOWN TIMER 1 vector
+CDTMV2 = $021A ; 538 COUNTDOWN TIMER 2 vector
+CDTMV3 = $021C ; 540 COUNTDOWN TIMER 3 vector
+CDTMV4 = $021E ; 542 COUNTDOWN TIMER 4 vector
+CDTMV5 = $0220 ; 544 COUNTDOWN TIMER 5 vector
+VVBLKI = $0222 ; 546 immediate VBLANK vector
+VVBLKD = $0224 ; 548 deferred VBLANK vector (ignore if CRITIC != 0)
+CDTMA1 = $0226 ; 550 COUNTDOWN TIMER 1 JSR ADDRESS
+CDTMA2 = $0228 ; 552 COUNTDOWN TIMER 2 JSR ADDRESS
+CDTMF3 = $022A ; 554 COUNTDOWN TIMER 3 FLAG
+SRTIMR = $022B ; 555 REPEAT TIMER
+CDTMF4 = $022C ; 556 COUNTDOWN TIMER 4 FLAG
+INTEMP = $022D ; 557 IAN'S TEMP (used by SETVBL routine)
+CDTMF5 = $022E ; 558 COUNTDOWN TIMER FLAG 5
+SDMCTL = $022F ; 559 DMACTL SHADOW
+SDLSTL = $0230 ; 560 DISPLAY LIST POINTER, LSB (shadow for DLISTL)
+SDLSTH = $0231 ; 561 display list pointer, MSB (shadow for DLISTH)
+SSKCTL = $0232 ; 562 SKCTL SHADOW
+; $0233 ; 563 (800) UNLISTED (Mapping calls this SPARE)
+XL_LCOUNT = $0233 ; 563 (XL) LOADER TEMP
+LPENH = $0234 ; 564 LIGHT PEN HORIZONTAL (shadow for PENH)
+LPENV = $0235 ; 565 LIGHT PEN VERTICAL (shadow for PENV)
+; $0236 ; 566 2 SPARE BYTES
+; $0238 ; 568 (800) SPARE, 2 BYTES
+;XL_RELADR = $0238 ; 568 (XL) relocatable loader relative addr, 1200XL only!
+XL_VPIRQ = $0238 ; 568 (XL) PBI IRQ vector (not on 1200XL!)
+CDEVIC = $023A ; 570 DEVICE COMMAND FRAME BUFFER
+CAUX1 = $023C ; 572 DEVICE COMMAND AUX 1
+CAUX2 = $023D ; 573 DEVICE COMMAND AUX 2
+TEMP = $023E ; 574 TEMPORARY STORAGE
+ERRFLG = $023F ; 575 DEVICE ERROR FLAG (EXCEPT TIMEOUT)
+DFLAGS = $0240 ; 576 FLAGS FROM DISK SECTOR 1
+DBSECT = $0241 ; 577 NUMBER OF BOOT DISK SECTORS
+BOOTAD = $0242 ; 578 BOOT LOAD ADDRESS POINTER
+COLDST = $0244 ; 580 COLD START FLAG, 1 = COLD START IN PROGRESS
+; $0245 ; 581 (800) SPARE
+XL_RECLEN = $0245 ; 581 (XL) LOADER
+DSKTIM = $0246 ; 582 (800) DISK TIME OUT REGISTER
+; $0246 ; 582 (XL) RESERVED, 39 BYTES
+LINBUF = $0247 ; 583 (800) CHARACTER LINE BUFFER, 40 BYTES
+ ; LINBUF was deleted from the XL OS and replaced with:
+
+; $0247 - $024D are "reserved" on the 1200XL. On other XL's they are:
+XL_PDVMSK = $0247 ; 583 shadow for PBI device selection register @ $D1FF
+XL_SHPDVS = $0248 ; 584 shadow for PBI register (where??)
+XL_PDMSK = $0249 ; 585 PBI interrupt mask
+XL_RELADR = $024A ; 586 (XL) LSB, relocatable loader relative addr (NOT 1200XL)
+; $024B ; 587 MSB, XL_RELADR
+XL_PPTMPA = $024C ; 588 temporaries for relocatable loader
+XL_PPTMPX = $024D ; 589 "
+
+; $024E - $026A are "spare" on all XL/XE's
+
+; More XL stuff:
+XL_CHSALT = $026B ; 619 (XL) CHARACTER SET POINTER (ctrl-F4 on 1200XL)
+XL_VSFLAG = $026C ; 620 (XL) FINE SCROLL TEMPORARY
+XL_KEYDIS = $026D ; 621 (XL) KEYBOARD DISABLE (ctrl-F1 on 1200XL)
+XL_FINE = $026E ; 622 (XL) FINE SCROLL FLAG (POKE 622,255:GR.0)
+
+GPRIOR = $026F ; 623 P/M PRIORITY AND GTIA MODES (shadow for PRIOR)
+;GTIA = $026F ; 623 ; 20070529 bkw: does anyone define this?
+
+; Game controller shadows (joysticks/paddles)
+; Joystick directions and paddle triggers (buttons) are wired to the PIA.
+; Joystick triggers (fire buttons) and the actual paddle potentiometers
+; are wired to the GTIA.
+; If this seems a little odd, that's because it is :)
+
+; Paddles (potentiometers):
+PADDL0 = $0270 ; 624 (XL) 3 MORE PADDLES, (800) 7 MORE PADDLES
+PADDL1 = $0271 ; 625 (these are read in BASIC with PADDLE(x)
+PADDL2 = $0272 ; 626 (PADDL0-7 are shadows for POT0-7)
+PADDL3 = $0273 ; 627
+PADDL4 = $0274 ; 628 (PADDL4-7 are copies of PADDL0-3 on the XL)
+PADDL5 = $0275 ; 629
+PADDL6 = $0276 ; 630
+PADDL7 = $0277 ; 631
+
+; Joysticks (directions only)
+STICK0 = $0278 ; 632 (XL) 1 MORE STICK, (800) 3 MORE STICKS
+STICK1 = $0279 ; 633 (these are read in BASIC with STICK(x)
+STICK2 = $027A ; 634 (STICK0/1 are shadows for PORTA; STICK2/3 shadows PORTB)
+STICK3 = $027B ; 635
+; STICK0 is a shadow for bits 4-7 of PORTA (shifted 4 bits right)
+; STICK1 is a shadow for bits 0-3 of PORTA
+
+; On the 800:
+; STICK2 is a shadow for bits 4-7 of PORTB (shifted 4 bits right)
+; STICK3 is a shadow for bits 0-3 of PORTB
+
+; On the XL/XE series:
+; STICK2 and STICK3 are copies of STICK0 and STICK1, respectively.
+
+; In the XL/XE machines, there are only 2 joystick ports, and PORTB
+; (formerly joystick ports) is now used to control the MMU.
+
+; joystick directions are active low (1=not pressed) and decode as:
+
+; bit direction
+; 0 or 4 up
+; 1 or 5 down
+; 2 or 6 left
+; 3 or 7 right
+
+; A value of $0F in a STICKx register means no direction is being pressed.
+; When a direction is pressed, its bit becomes a logic 0, so e.g. $0E means
+; someone's moving the joystick up.
+
+; (bits 4-7 are only used when reading directly from the HW registers,
+; PORTA and PORTB).
+
+; Paddle triggers (buttons)
+PTRIG0 = $027C ; 636 (XL) 3 MORE PADDLE TRIGGERS, (800) 7 MORE
+PTRIG1 = $027D ; 637 (these are read in BASIC with PTRIG(x))
+PTRIG2 = $027E ; 638 (PTRIG0-3 are shadows for PORTA)
+PTRIG3 = $027F ; 639
+PTRIG4 = $0280 ; 640 (PTRIG4-7 are shadows for PORTB on the 800)
+PTRIG5 = $0281 ; 641 (they are copies of PTRIG0-3 on the XL)
+PTRIG6 = $0282 ; 642
+PTRIG7 = $0283 ; 643
+; In case someone doesn't already know this: The paddle triggers are wired
+; to the same pins on the joystick port as the left/right joystick directions.
+; Each pair of paddles uses left for the first paddle's trigger and right
+; for the second (so PTRIG0/1 are also the left/right bits in STICK0,
+; PTRIG2/3 are STICK1, etc).
+
+; Joystick triggers (buttons)
+STRIG0 = $0284 ; 644 (XL) 1 MORE STICK TRIGGER, (800) 3 MORE
+STRIG1 = $0285 ; 645 (these are read in BASIC with STRIG(x))
+STRIG2 = $0286 ; 646 (STRIG0-3 are shadows for TRIG0-3)
+STRIG3 = $0287 ; 647
+
+; C: handler variables:
+CSTAT = $0288 ; 648 (800) Cassette status register
+; note that CSTAT was deleted from the XL OS, and replaced with:
+XL_HIBYTE = $0288 ; 648 (XL) used by relocatable loader
+WMODE = $0289 ; 649 used by C: handler (0=read, 128-write)
+BLIM = $028A ; 650 cassette buffer data record size
+; $028B ; 651 (800) 5 SPARE BYTES (to $028F)
+XL_IMASK = $028B ; 651 (XL) used by relocatable loader
+XL_JVECK = $028C ; 652 (XL) (Mapping says it's unused)
+ ; 653 (XL) Presumably the MSB of JVECK (unused?)
+XL_NEWADR = $028E ; 654 (XL) LOADER RAM (2 bytes)
+
+; Misc. S: and/or E: handler variables:
+TXTROW = $0290 ; 656
+TXTCOL = $0291 ; 657
+TINDEX = $0293 ; 659 TEXT INDEX
+TXTMSC = $0294 ; 660
+TXTOLD = $0296 ; 662 OLD ROW AND OLD COL FOR TEXT, 2 BYTES
+; $0298 ; 664 4 SPARE BYTES
+TMPX1 = $029C ; 668 (800)
+; note that TMPX1 was deleted from the XL OS, and replaced with:
+XL_CRETRY = $029C ; 668 (XL) NUMBER OF COMMAND FRAME RETRIES
+ ; (moved from CRETRY on 800)
+SUBTMP = $029E ; 670
+HOLD2 = $029F ; 671
+DMASK = $02A0 ; 672
+TMPLBT = $02A1 ; 673
+ESCFLG = $02A2 ; 674
+TABMAP = $02A3 ; 675 15 BYTE BIT MAP FOR TAB SETTINGS
+LOGMAP = $02B2 ; 690 4 BYTE LOGICAL LINE START BIT MAP
+INVFLG = $02B6 ; 694 mask for inverse video ($80=inverse, 0=normal)
+FILFLG = $02B7 ; 695 FILL DURING DRAW FLAG
+TMPROW = $02B8 ; 696
+TMPCOL = $02B9 ; 697
+SCRFLG = $02BB ; 699 SCROLL FLAG
+HOLD4 = $02BC ; 700
+HOLD5 = $02BD ; 701 (800)
+; note that HOLD5 was deleted from the XL OS, and replaced with:
+XL_DRETRY = $02BD ; 701 (XL) NUMBER OF DEVICE RETRIES
+ ; (moved from DRETRY on 800)
+SHFLOC = $02BE ; 702
+BOTSCR = $02BF ; 703 24 NORM, 4 SPLIT
+
+; Color register shadows (HW registers are in GTIA)
+PCOLR0 = $02C0 ; 704 3 MORE PLAYER COLOR REGISTERS (shadows for COLPM0-3)
+PCOLR1 = $02C1 ; 705 (missiles use same color regs as same-numbered players!)
+PCOLR2 = $02C2 ; 706
+PCOLR3 = $02C3 ; 707
+COLOR0 = $02C4 ; 708 4 MORE GRAPHICS COLOR REGISTERS (shadows for COLPF0-3)
+COLOR1 = $02C5 ; 709 (text luminance in GR.0)
+COLOR2 = $02C6 ; 710 (text background and chroma in GR.0)
+COLOR3 = $02C7 ; 711
+COLOR4 = $02C8 ; 712 (background, shadow for COLBK)
+; On boot, system reset, or any time S:/E: devices are opened:
+; PCOLR0-3 are initialzed to 0 ($00, black)
+; COLOR0 is initialized to 40 ($28, orange)
+; COLOR1 is initialized to 202 ($CA, green)
+; COLOR2 is initialized to 148 ($94, blue)
+; COLOR3 is initialized to 70 ($46, red)
+; COLOR4 is initialized to 0 ($00, black)
+
+; $02C9 713 (800) 23 SPARE BYTES
+; XL relocatable handler and other variables:
+XL_RUNADR = $02C9 ; 713 (XL) LOADER VECTOR
+XL_HIUSED = $02CB ; 715 (XL) LOADER VECTOR
+XL_ZHIUSE = $02CD ; 717 (XL) LOADER VECTOR
+XL_GBYTEA = $02CF ; 719 (XL) LOADER VECTOR
+XL_LOADAD = $02D1 ; 721 (XL) LOADER VECTOR
+XL_ZLOADA = $02D3 ; 723 (XL) LOADER VECTOR
+XL_DSCTLN = $02D5 ; 725 (XL) DISK SECTOR SIZ
+XL_ACMISR = $02D7 ; 727 (XL) RESERVED
+XL_KRPDER = $02D9 ; 729 (XL) KEY AUTO REPEAT DELAY
+XL_KEYREP = $02DA ; 730 (XL) KEY AUTO REPEAT RATE
+XL_NOCLIK = $02DB ; 731 (XL) KEY CLICK DISABLE (ctrl-F3 on 1200XL)
+XL_HELPFG = $02DC ; 732 (XL) HELP KEY FLAG
+XL_DMASAV = $02DD ; 733 (XL) SDMCTL (DMA) SAVE (ctrl-F2 on 1200XL)
+XL_PBPNT = $02DE ; 734 (XL) PRINTER BUFFER POINTER (moved from PBPNT on 800)
+XL_PBUFSZ = $02DF ; 735 (XL) PRINTER BUFFER SIZE (moved from PBUFSZ on 800)
+; note that PTEMP was deleted from the XL OS
+
+; DOS/FMS variables:
+GLBABS = $02E0 ; 736 GLOBAL VARIABLES, 4 SPARE BYTES (if DOS not loaded)
+ ; If DOS/FMS is loaded:
+RUNAD = $02E0 ; 736 (DOS) Run address for binary file (LSB/MSB)
+INITAD = $02E2 ; 736 (DOS) Init address for binary file (LSB/MSB)
+
+; SYSEQU.ASM defines these:
+GOADR = RUNAD
+INITADR = INITAD
+
+; OS variables:
+RAMSIZ = $02E4 ; 740 PERMANENT START OF ROM POINTER
+MEMTOP = $02E5 ; 741 END OF FREE RAM
+MEMLO = $02E7 ; 743 LSB, points to bottom of free memory ($0700 if DOS
+ ; not booted). Not to be confused with BASIC's LOMEM!
+; $02E8 ; 744 MSB of MEMLO
+
+; $02E9 ; 745 (800) SPARE
+XL_HNDLOD = $02E9 ; 745 (XL) HANDLER LOADER FLAG
+
+DVSTAT = $02EA ; 746 DEVICE STATUS BUFFER, 4 BYTES
+CBAUDL = $02EE ; 750 CASSETTE BAUD RATE, 2 BYTES
+CRSINH = $02F0 ; 752 1 = INHIBIT CURSOR
+KEYDEL = $02F1 ; 753 KEY DELAY AND RATE (aka debounce counter)
+CH1 = $02F2 ; 754 prior keyboard character code
+CHACT = $02F3 ; 755 (shadow for CHACTL)
+CHBAS = $02F4 ; 756 CHARACTER SET POINTER (shadow for CHBASE)
+
+; These next 4 are located elsewhere on the 800 OS:
+XL_NEWROW = $02F5 ; 757 (XL) DRAW DESTINATION
+XL_NEWCOL = $02F6 ; 758 (XL) DRAW DESTINATION
+XL_ROWINC = $02F8 ; 760 (XL)
+XL_COLINC = $02F9 ; 761 (XL)
+; $02F5 - $02F9 are "spare" on the 800.
+
+CHAR = $02FA ; 762 most recent character read/written (screen code)
+ATACHR = $02FB ; 763 ATASCII CHARACTER FOR CIO
+CH = $02FC ; 764 last key pressed (internal scan code)
+FILDAT = $02FC ; 764 COLOR FOR SCREEN FILL
+DSPFLG = $02FE ; 766 DISPLAY CONTROL CHARACTERS FLAG
+SSFLAG = $02FF ; 767 DISPLAY START/STOP FLAFG
+
+;
+; PAGE 3
+;
+;
+; RESIDENT DISK HANDLER/SIO INTERFACE
+;
+; The DCB is used for SIO (serial I/O).
+DCB = $0300 ; 768 DEVICE CONTROL BLOCK
+DDEVIC = $0300 ; 768 device ID ($31-$38 for D1:-D8:)
+DUNIT = $0301 ; 769 disk/device unit numder
+DCOMND = $0302 ; 770 device command
+DSTATS = $0303 ; 771 status code (set by OS)
+DBUFLO = $0304 ; 772 data buffer LSB (set by user)
+DBUFHI = $0305 ; 773 data buffer MSB (set by user)
+DTIMLO = $0306 ; 774 timeout (set by user, units of 60/64 seconds)
+DUNUSE = $0307 ; 775 unused
+DBYTLO = $0308 ; 776 number of bytes to transfer, LSB
+DBYTHI = $0309 ; 777 number of bytes to transfer, MSB
+DAUX1 = $030A ; 778 LSB of sector number (for disk) (set by user)
+DAUX2 = $030B ; 779 MSB of sector number (for disk)
+TIMER1 = $030C ; 780 INITIAL TIMER VALUE
+ADDCOR = $030E ; 782 (800) ADDITION CORRECTION
+; note that ADDCOR was deleted from the XL OS, and replaced with:
+XL_JMPERS = $030E ; 782 (XL) OPTION JUMPERS
+CASFLG = $030F ; 783 CASSETTE MODE WHEN SET
+TIMER2 = $0310 ; 784 FINAL VALUE, TIMERS 1 & 2 DETERMINE BAUD RATE
+TEMP1 = $0312 ; 786
+XL_TEMP2 = $0313 ; 787 (XL)
+TEMP2 = $0314 ; 788 (800)
+XL_PTIMOT = $0314 ; 788 (XL) PRINTER TIME OUT
+TEMP3 = $0315 ; 789
+SAVIO = $0316 ; 790 SAVE SERIAL IN DATA PORT
+TIMFLG = $0317 ; 791 TIME OUT FLAG FOR BAUD RATE CORRECTION
+STACKP = $0318 ; 792 SIO STACK POINTER SAVE
+TSTAT = $0319 ; 793 TEMPORARY STATUS HOLDER
+HATABS = $031A ; 794 HANDLER ADDRESS TABLE, 38 BYTES
+MAXDEV = $0321 ; 801 MAXIMUM HANDLER ADDRESS INDEX
+XL_PUPBT1 = $033D ; 829 (XL) POWER-UP/RESET
+XL_PUPBT2 = $033E ; 830 (XL) POWER-UP/RESET
+XL_PUPBT3 = $033F ; 831 (XL) POWER-UP/RESET
+
+; IOCB's, 8 of them, 16 bytes each.
+; Set X register to (IOCB number * 16), and use e.g. ICCOM,x
+;
+IOCB = $0340 ; 832 ; IOCB base address
+ICHID = $0340 ; 832 ; Handler ID (set by OS)
+ICDNO = $0341 ; 833 ; Device number (set by OS)
+ICCOM = $0342 ; 834 ; Command byte (see C_* constants) (set by user)
+ICCMD = ICCOM ; ; alternate name for ICCOM, according to Mapping.
+ICSTA = $0343 ; 835 ; Status (set by OS)
+ICBAL = $0344 ; 836 ; Buffer address, LSB (set by user)
+ICBAH = $0345 ; 837 ; Buffer address, MSB (set by user)
+ICPTL = $0346 ; 838 ; Put-one-byte address minus one, LSB (set by OS)
+ICPTH = $0347 ; 839 ; Put-one-byte address minus one, MSB (set by OS)
+ICBLL = $0348 ; 840 ; Buffer length, LSB (set by user)
+ICBLH = $0349 ; 841 ; Buffer length, MSB (set by user)
+ICAX1 = $034A ; 842 ; AUX1 byte (2nd param in BASIC OPEN) (set by user)
+ICAX2 = $034B ; 843 ; AUX2 byte (4rd param in BASIC OPEN) (set by user)
+ICAX3 = $034C ; 844 ; AUX3 byte (used by NOTE/POINT) (set by user)
+ICAX4 = $034D ; 845 ; AUX4 byte (used by NOTE/POINT) (set by user)
+ICAX5 = $034E ; 846 ; AUX5 byte (used by NOTE/POINT) (set by user)
+ICAX6 = $034F ; 847 ; Spare aux byte
+; OTHER IOCB's, 112 BYTES ($300 + $10 * channel)
+
+IOCBLEN = *-IOCB ; length of one IOCB (from SYSEQU.ASM)
+
+; Alternative names for the above. I found these in SYSEQU.ASM, as
+; distributed with the disk version of Mac65.
+ICBADR = ICBAL
+ICPUT = ICPTL
+ICBLEN = ICBLL
+ICAUX1 = ICAX1
+ICAUX2 = ICAX2
+ICAUX3 = ICAX3
+ICAUX4 = ICAX4
+ICAUX5 = ICAX5
+ICAUX6 = ICAX6
+
+PRNBUF = $03C0 ; 960 PRINTER BUFFER, 40 BYTES
+; $03E8 ; 1000 (800) 21 SPARE BYTES
+XL_SUPERF = $03E8 ; 1000 (XL) SCREEN EDITOR
+XL_CKEY = $03E9 ; 1001 (XL) START KEY FLAG
+XL_CASSBT = $03EA ; 1002 (XL) CASSETTE BOOT FLAG
+XL_CARTCK = $03EB ; 1003 (XL) CARTRIDGE CHECKSUM
+XL_ACMVAR = $03ED ; 1005 (XL) RESERVED, 10 BYTES (to $03F7)
+XL_BASICF = $03F8 ; 1006 (XL) 0 if ROM-BASIC enabled, 1 if not
+XL_MINTLK = $03F9 ; 1017 (XL) RESERVED
+XL_GINTLK = $03FA ; 1018 (XL) CARTRIDGE INTERLOCK
+XL_CHLINK = $03FB ; 1019 (XL) HANDLER CHAIN, 2 BYTES
+CASBUF = $03FD ; 1021 CASSETTE BUFFER, 131 BYTES TO $047F
+
+; Layout of the cassette buffer after a cassette block is read:
+
+; Baud correction ($55 $55) bytes are located at offsets 0 and 1
+; Control byte is at offset 2 ($03FF):
+; Actual data (128 bytes) runs from offset 3 ($0400) to $047F.
+; Each cassette frame has a 1 byte checksum after the 128 data bytes, but
+; the checksum is NOT stored anywhere in the cassette buffer!
+
+; CONTROL BYTE VALUES
+; Value Meaning
+; 250 ($FA) Partial record follows. The actual number of bytes is stored
+; in the last byte of the record (CASBUF+130, or $047F).
+; 252 ($FC) Record full; 128 bytes follow.
+; 254 ($FE) End of File (EOF) record; followed by 128 zero bytes.
+
+; Boot tapes normally don't have partial or EOF records, but BASIC
+; CLOAD/LOAD/LIST and data file tapes do.
+
+; Mapping the Atari says the first disk boot sector is read into CASBUF also.
+
+;
+;
+; PAGE 4
+;
+;
+USAREA = $0480 ; 1152 128 SPARE BYTES (but used by BASIC)
+;
+; SEE APPENDIX C FOR PAGES 4 AND 5 USAGE
+
+;
+;
+;
+;
+; PAGE 5
+;
+PAGE5 = $0500 ; 1280 127 FREE BYTES
+; $057E 1406 129 FREE BYTES IF FLOATING POINT ROUTINES NOT USED
+;
+;FLOATING POINT NON-ZERO PAGE RAM, NEEDED ONLY IF FP IS USED
+; (20070529 bkw: BASIC constantly uses FP! Also, it uses some of these
+; addresses for its own purposes.)
+;
+LBPR1 = $057E ; 1406 LBUFF PREFIX 1
+LBPR2 = $05FE ; 1534 LBUFF PREFIX 2
+LBUFF = $0580 ; 1408 LINE BUFFER
+PLYARG = $05E0 ; 1504 POLYNOMIAL ARGUMENTS
+FPSCR = $05E6 ; 1510 PLYARG+FPREC
+FPSCR1 = $05EC ; 1516 FPSCR+FPREC
+FSCR = $05E6 ; 1510 =FPSCR
+FSCR1 = $05EC ; 1516 =FPSCR1
+LBFEND = $05FF ; 1535 END OF LBUFF
+
+;
+; PAGE 6
+;
+;
+PAGE6 = $0600 ; 1536 256 FREE BYTES
+
+;
+;
+; PAGE 7
+;
+;
+BOOTRG = $0700 ; 1792 PROGRAM AREA
+; Boot disks (including DOS) are generally loaded here. Also, BASIC RAM
+; (variables and program) starts here, if BASIC is booted without DOS.
+
+; Page 80 (XL): Self-test (aka diagnostic) ROM is mapped at $5000,
+; if enabled with bit 7 of PORTB. Normally only happens if you boot without
+; BASIC, cartridge, tape, or disk... or if the OS detects a memory error
+; during boot.
+
+;
+;
+; UPPER ADDRESSES
+;
+;
+RITCAR = $8000 ;32768 RAM IF NO CARTRIDGE (extends to $9FFF)
+LFTCAR = $A000 ;40960 RAM IF NO CARTRIDGE (extends to $BFFF)
+
+; These 2 are from the Atari System Reference Manual, chapter 12:
+CARTA = LFTCAR
+CARTB = RITCAR
+
+CARTLOC = $BFFA ;49146 cartridge run address (from SYSEQU.ASM)
+
+; Carts were originally 8K only when the 400/800 were first released.
+; There were plans to release 16K programs on two cartridges, but this
+; never happened (the price of 16K ROMs came down, I guess). 16K cartridges
+; go in the left slot, but they actually use the address space for both
+; the right and left slots.
+
+; Mapping the Atari has this to say about cartridges:
+;; Byte Purpose
+;; Left (A) Right(B)
+;; 49146 ($BFFA) 40954 ($9FFA) Cartridge start address (low byte)
+;;
+;; 49147 ($BFFB) 40955 ($9FFB) Cartridge start address (high byte)
+;;
+;; 49148 ($BFFC) 40956 ($9FFC) Reads zero if a cartridge is
+;; inserted, non-zero when no cartridge is present. This information
+;; is passed down to the page zero RAM: if the A cartridge is plugged
+;; in, then location 6 will read one; if the B cartridge is plugged in,
+;; then location 7 will read one; otherwise they will read zero.
+;;
+;; 49149 ($BFFD) 40957 ($9FFD) Option byte. If BIT 0 equals one,
+;; then boot the disk (else there is no disk boot). If BIT 2 equals one,
+;; then initialize and start the cartridge (else initialize but do not
+;; start). If BIT 7 equals one, then the cartridge is a diagnostic
+;; cartridge which will take control, but not initialize the OS (else
+;; non-diagnostic cartridge). Diagnostic cartridges were used by
+;; Atari in the development of the system and are not available to the
+;; public.
+;;
+;; 49150 ($BFFE) 40958 ($9FFE) Cartridge initialization address
+;; low byte.
+;;
+;; 49151 ($BFFF) 40959 ($9FFF) Cartridge initialization address
+;; high byte. This is the address to which the OS will jump during all
+;; powerup and RESETs.
+;;
+;; The OS makes temporary use of locations 36876 to 36896 ($900C to
+;; $9020) to set up vectors for the interrupt handler. See the OS
+;; listings pages 31 and 81. This code was only used in the
+;; development system used to design the Atari.
+
+
+; Page 192
+
+C0PAGE = $C000 ;49152 (800) EMPTY, 4K BYTES
+ ; 20070529 bkw: unmapped address space.
+ ; Mapping the Atari erroneously lists this as "unused ROM".
+ ; There are upgrades to the 800 to give 4K of RAM here
+ ; (for a total of 52K of RAM), or ROM (Omnimon?).
+ ; Also, there is RAM here if you boot the Translator
+ ; disk on an XL.
+
+; (XL) $C000 also contains info about the ROM revision. From Mapping:
+
+;Bytes 49152-49163 ($C000-$C00B) are used to identify the computer
+;and the ROM in the $C000-$DFFF block:
+;
+;Byte Use
+;49152-3/C000-1 Checksum (LSB/MSB) of all the bytes
+; in ROM except the checksum bytes
+; themselves.
+;49154/C002 Revision date, stored in the form
+; DDMMYY. This is DD, day, usually $10.
+;49155/C003 Revision date, month; usually $05.
+;49156/C004 Revision date, year; usually $83.
+;49157/C005 Reserved option byte; reads zero for
+; the 1200, 800XL, and 130XE.
+;49158/C006 Part number in the form AANNNNNN;
+; AA is an ASCII character and
+; NNNNNN is a four-bit BCD digit. This is
+; byte A1.
+;49159-62/C007-A Part number, bytes A2, N1-N6 (each
+; byte has two N values of four bits
+; each).
+;49163/C00B Revision number. Mapping author's 800XL and 130XE say 2.
+
+;C0PAGE = $C000 ;49152 (XL) OS ROM, mostly interrupt handlers
+; $C800 51200 (XL) START OF OS ROM
+CHORG2 = $CC00 ;52224 (XL) INTERNATIONAL CHARACTER SET
+
+
+
+;
+;
+; HARDWARE REGISTERS
+;
+;
+; SEE REGISTER LIST FOR MORE INFORMATION
+;
+;
+
+; GTIA
+GTIA = $D000
+HPOSP0 = $D000 ;53248 (W) ; P/M positions (no shadows)
+HPOSP1 = $D001 ;53249 (W)
+HPOSP2 = $D002 ;53250 (W)
+HPOSP3 = $D003 ;53251 (W)
+HPOSM0 = $D004 ;53252 (W)
+HPOSM1 = $D005 ;53253 (W)
+HPOSM2 = $D006 ;53254 (W)
+HPOSM3 = $D007 ;53255 (W)
+SIZEP0 = $D008 ;53256 (W) ; P/M size regs (no shadows)
+SIZEP1 = $D009 ;53257 (W)
+SIZEP2 = $D00A ;53258 (W)
+SIZEP3 = $D00B ;53259 (W)
+SIZEM = $D00C ;53260 (W)
+M0PF = $D000 ;53248 (R) ; collision regs (no shadows)
+M1PF = $D001 ;53249 (R)
+M2PF = $D002 ;53250 (R)
+M3PF = $D003 ;53251 (R)
+P0PF = $D004 ;53252 (R)
+P1PF = $D005 ;53253 (R)
+P2PF = $D006 ;53254 (R)
+P3PF = $D007 ;53255 (R)
+M0PL = $D008 ;53256 (R)
+M1PL = $D009 ;53257 (R)
+M2PL = $D00A ;53258 (R)
+M3PL = $D00B ;53259 (R)
+P0PL = $D00C ;53260 (R)
+P1PL = $D00D ;53261 (R)
+P2PL = $D00E ;53262 (R)
+P3PL = $D00F ;53263 (R)
+GRAFP0 = $D00D ;53261 (W) ; direct (non-DMA) P/M graphics regs (no shadows)
+GRAFP1 = $D00E ;53262 (W)
+GRAFP2 = $D00F ;53263 (W)
+GRAFP3 = $D010 ;53264 (W)
+GRAFM = $D011 ;53265 (W)
+TRIG0 = $D010 ;53264 (R) ; Joystick triggers (shadows @ STRIG0-3)
+TRIG1 = $D011 ;53265 (R)
+TRIG2 = $D012 ;53266 (R)
+TRIG3 = $D013 ;53267 (R)
+PAL = $D014 ;53268 (R) ; PAL/NTSC detect (no shadow)
+ ; PAL supposedly moved to XL_PALNTS on XL; what was it
+ ; replaced with?
+COLPM0 = $D012 ;53266 (W) ; P/M colors (shadows @ PCOLR0-3)
+COLPM1 = $D013 ;53267 (W)
+COLPM2 = $D014 ;53268 (W)
+COLPM3 = $D015 ;53269 (W)
+COLPF0 = $D016 ;53270 (W) ; Playfield colors (shadows @ COLOR0-3)
+COLPF1 = $D017 ;53271 (W)
+COLPF2 = $D018 ;53272 (W)
+COLPF3 = $D019 ;53273 (W)
+COLBK = $D01A ;53274 (W) ; Background color (shadow @ COLOR4)
+PRIOR = $D01B ;53275 (W) ; GTIA priority (shadow @ GPRIOR)
+GTIAR = $D01B ;53275 (R?)
+VDELAY = $D01C ;53276 (W)
+GRACTL = $D01D ;53277 (W)
+HITCLR = $D01E ;53278 (W), latch
+CONSOL = $D01F ;53279 (W=keyclick spkr, R=console keys)
+
+; $D020 - $D0FF are mirrors of GTIA address space
+; $D100 - $D1FF are supposed to be unused (unmapped) on the 800
+; On the XL, $D100 - $D1FF is switched to device memory during PBI I/O
+
+; POKEY
+POKEY = $D200
+; no shadows for AUDC/AUDF
+AUDF1 = $D200 ;53760 (W) ; Audio frequency 1
+AUDC1 = $D201 ;53761 (W) ; Audio control 1 (distortion/volume)
+AUDF2 = $D202 ;53762 (W)
+AUDC2 = $D203 ;53763 (W)
+AUDF3 = $D204 ;53764 (W)
+AUDC3 = $D205 ;53765 (W)
+AUDF4 = $D206 ;53766 (W)
+AUDC4 = $D207 ;53767 (W)
+
+; POT0-7 shadows at PADDL0-7
+POT0 = $D200 ;53760 (R) ; Paddle positions
+POT1 = $D201 ;53761 (R)
+POT2 = $D202 ;53762 (R)
+POT3 = $D203 ;53763 (R)
+POT4 = $D204 ;53764 (R) ; pots 3-7 don't exist on XL/XE
+POT5 = $D205 ;53765 (R)
+POT6 = $D206 ;53766 (R)
+POT7 = $D207 ;53767 (R)
+
+AUDCTL = $D208 ;53768 (W) ; Audio control (no shadow)
+ALLPOT = $D208 ;53768 (R) (no shadow)
+STIMER = $D209 ;53769 (W) (no shadow)
+KBCODE = $D209 ;53769 (R) (shadow @ CH)
+SKREST = $D20A ;53770 (W) (latch)
+RANDOM = $D20A ;53770 (R) (no shadow)
+POTGO = $D20B ;53771 (W) (latch)
+; $D20C (53772) is unused
+SEROUT = $D20D ;53773 (W) (no shadow)
+SERIN = $D20D ;53773 (R) (no shadow)
+IRQEN = $D20E ;53774 (W) (shadow @ POKMSK)
+IRQST = $D20E ;53774 (R)
+SKCTL = $D20F ;53775 (W) (shadow @ SSKCTL)
+SKSTAT = $D20F ;53775 (R)
+
+; $D210 - $D2FF are mirrors of POKEY address space. The "stereo POKEY"
+; modification adds a second POKEY chip, usually addressed at $D210.
+
+; PIA
+; No shadow regs for PIA regs
+PIA = $D300
+PORTA = $D300 ;54016
+PORTB = $D301 ;54017
+PACTL = $D302 ;54018
+PBCTL = $D303 ;54019
+
+; $D304 - $D3FF are mirrors of PIA address space
+
+; ANTIC
+ANTIC = $D400
+DMACTL = $D400 ;54272 (W) (shadow @ SDMCTL)
+CHACTL = $D401 ;54273 (W) (shadow @ CHACT)
+DLISTL = $D402 ;54274 (W) (shadow @ SDLSTL)
+DLISTH = $D403 ;54275 (W) (shadow @ SDLSTH)
+HSCROL = $D404 ;54276 (W) (no shadow)
+VSCROL = $D405 ;54277 (W) (no shadow)
+; $D406 (54278) is unused
+PMBASE = $D407 ;54279 (W) (no shadow)
+; $D408 (54280) is unused
+CHBASE = $D409 ;54281 (W) (shadow @ CHBAS)
+WSYNC = $D40A ;54282 (W), latch (data written doesn't matter)
+VCOUNT = $D40B ;54283 (R) (no shadow)
+PENH = $D40C ;54284 (R) (shadow @ LPENH)
+PENV = $D40D ;54285 (R) (shadow @ LPENV)
+NMIEN = $D40E ;54286 (W) (no shadow)
+NMIRES = $D40F ;54287 (W), latch?
+NMIST = $D40F ;54287 (R) (no shadow)
+
+; $D410 - $D4FF are mirrors of ANTIC address space
+
+CCNTL = $D500 ;54528 Cartridge control (sometimes used for bankswitching)
+; $D500 - $D5FF is supposed to be all be mapped to CCNTL
+
+; $D600 - $D7FF is unmapped? used by PBI on XL? seems to read all $FF
+
+;
+; FLOATING POINT MATH ROUTINES
+;
+; From Mapping:
+; These entry points are the same on 400/800 and XL OS, though the
+; routines themselves are different (bugfixed/optimized for XL)
+; Also, on the XL, the $D800 area is bankswitched to PBI device ROM,
+; during PBI I/O. Not sure if all of $D800 - $DFFF is switched out
+; or just part of it.
+AFP = $D800 ;55296 ASCII to Floating Point (FP) conversion.
+FASC = $D8E6 ;55526 FP value to ASCII conversion.
+IFP = $D9AA ;55722 Integer to FP conversion
+FPI = $D9D2 ;55762 FP to Integer conversion
+ZFR0 = $DA44 ;55876 Clear FR0 (set all bytes to 0)
+ZF1 = $DA46 ;55878 Clear FR1 (set all bytes to 0) (aka AF1 (De Re))
+FSUB = $DA60 ;55904 FP subtract: FR0 = FR0 - FR1
+FADD = $DA66 ;55910 FP add: FR0 = FR0 + FR1
+FMUL = $DADB ;56027 FP multiply: FR0 = FR0 * FR1
+FDIV = $DB28 ;56104 FP divide: FR0 = FR0 / FR1
+PLYEVL = $DD40 ;56640 FP polynomial evaluation
+FLD0R = $DD89 ;56713 Load FP number into FR0 from 6502 X/Y registers
+FLD0P = $DD8D ;56717 Load FP number into FR0 from FLPTR
+FLD1R = $DD98 ;56728 Load FP number into FR1 from 6502 X/Y registers
+FLD1P = $DD9C ;56732 Load FP number into FR1 from FLPTR
+FST0R = $DDA7 ;56743 Store FP number into 6502 X/Y regs from FR0
+FST0P = $DDAB ;56747 Store FP number from FR0, using FLPTR
+FMOVE = $DDB6 ;56758 Move FP number from FR0 into FR1 (FR1 = FR0)
+EXP = $DDC0 ;56768 FP base e exponentiation
+EXP10 = $DDCC ;56780 FP base 10 exponentiation
+LOG = $DECD ;57037 FP natural logarithm
+LOG10 = $DED1 ;57041 FP base 10 logarithm
+
+;
+;
+; OPERATING SYSTEM
+;
+;
+; MODULE ORIGIN TABLE
+;
+CHORG = $E000 ;57344 CHARACTER SET, 1K
+VECTBL = $E400 ;58368 VECTOR TABLE
+VCTABL = $E480 ;58496 RAM VECTOR INITIAL VALUE TABLE
+CIOORG = $E4A6 ;58534 CIO HANDLER
+INTORG = $E6D5 ;59093 INTERRUPT HANDLER
+SIOORG = $E944 ;59716 SIO DRIVER
+DSKORT = $EDEA ;60906 DISK HANDLER
+PRNORG = $EE78 ;61048 PRINTER HANDLER
+CASORG = $EE78 ;61048 CASSETTE HANDLER
+MONORG = $F0E3 ;61667 MONITOR/POWER UP MODULE
+KBDORG = $F3E4 ;62436 KEYBOARD/DISPLAY HANDLER
+;
+;
+; VECTOR TABLE, CONTAINS ADDRESSES OF CIO ROUTINES IN THE
+; FOLLOWING ORDER. THE ADDRESSES IN THE TABLE ARE TRUE ADDRESSES-1
+;
+; ADDRESS + 0 OPEN
+; + 2 CLOSE
+; + 4 GET
+; + 6 PUT
+; + 8 STATUS
+; + A SPECIAL
+; + C JMP TO INITIALIZATION
+; + F NOT USED
+;
+;
+
+; 20070529 bkw: why are they address minus one? because they are called
+; via RTS: a JSR actually pushes the return address minus one, and RTS
+; increments the address on the stack after popping it. The Atari OS
+; "pretends" to have done a JSR by pushing the address-1 on the stack,
+; then executes RTS, which "returns" to the correct address.
+
+EDITRV = $E400 ;58368 EDITOR
+SCRENV = $E410 ;58384 SCREEN
+KEYBDV = $E420 ;58400 KEYBOARD
+PRINTV = $E430 ;58416 PRINTER
+CASETV = $E440 ;58432 CASSETTE
+;
+; ROM VECTORS
+;
+; 20070529 bkw: These consist of a JMP xxxx instruction in the ROM.
+DSKINV = $E453 ;58451
+CIOV = $E456 ;58454 ; Main CIO entry point!
+SIOV = $E459 ;58457 ; Main SIO entry point!
+SETVBV = $E45C ;58460
+SYSVBV = $E45F ;58463
+VBIVAL = $E460 ;58464 ADR AT VVBLKI (operand of JMP @ $E45F)
+XITVBV = $E462 ;58466 EXIT VBI
+VBIXVL = $E463 ;58467 ADR AT VVBLKD (operand of JMP @ $E462)
+SIOINV = $E465 ;58469
+SENDEV = $E468 ;58472
+INTINV = $E46B ;58475
+CIOINV = $E46E ;58478
+BLKBDV = $E471 ;58481 MEMO PAD MODE (self-test in XL)
+WARMSV = $E474 ;58484 ; warmstart (RESET key jumps here)
+COLDSV = $E477 ;58487 ; coldstart (reboot) the Atari
+RBLOKV = $E47A ;58490
+CSOPIV = $E47D ;58493
+
+; SYSEQU.ASM defines this:
+CIO = CIOV
+
+; XL-only entry points:
+XL_SELFSV = BLKBDV ; self-test (same entry point as 800 memo pad)
+XL_SELFTST = BLKBDV ; alt. name (Mapping)
+XL_PUPDIV = $E480 ;58496 (XL) Power-up ATARI logo (1200XL only), or self-test
+XL_SLFTSV = $E483 ;58499 (XL) Self-test vector (points to $5000)
+XL_PENTV = $E486 ;58502 (XL) Entry to the handler uploaded from peripheral
+ ; or disk (is this for the PBI?)
+XL_PHUNLV = $E489 ;58505 (XL) Entry to uploaded handler unlink (PBI?)
+XL_PHINIV = $E48C ;58508 (XL) Entry to uploaded handler init (PBI?)
+XL_GPDVV = $E48F ;58511 (XL) General-purpose parallel device handler
+ ; (copy to HATABS to use)
+
+;;;;; Here endeth the list of official mnemonics
+
+; Mapping has this to say about the XL ROMs:
+;Byte Use
+;65518/FFEE Revision date D1 and D2 (four-bit BCD)
+;65519/FFEF Revision date M1 and M2
+;65520/FFF0 Revision date Y1 and Y2
+;65521/FFF1 Option byte; should read 1 for the
+; 1200XL (Mapping author's 800XL reads 2)
+;65522-26/FFF2-6 Part number in the form AANNNNNN
+;65527/FFF7 Revision number (again, mine reads 2)
+;65528-9/FFF8-9 Checksum, bytes (LSB/MSB)
+; There don't seem to be any known mnemonics for the above...
+
+; 20061120 bkw: display list stuff. These are not official Atari mnemonics,
+; but they *are* somewhat based on the "Checkers Demo" by Carol Shaw,
+; in the Atari Hardware Manual (she didn't define all these, and she didn't
+; use the "DL_" prefix, probably because her assembler was limited to
+; 6-character labels and/or didn't support the underscore).
+
+; blank lines, 1-8 scanlines high
+DL_BLANK1 = $00
+DL_BLANK2 = $10
+DL_BLANK3 = $20
+DL_BLANK4 = $30
+DL_BLANK5 = $40
+DL_BLANK6 = $50
+DL_BLANK7 = $60
+DL_BLANK8 = $70
+
+; modifier bits..
+DL_VSCROLL = $10
+DL_HSCROLL = $20
+DL_LMS = $40
+DL_DLI = $80
+
+; graphics modes (these are the BASIC modes)
+; If you're more familiar with the ANTIC modes, nobody's forcing you
+; to use these :)
+DL_GR0 = $02
+DL_GR1 = $06
+DL_GR2 = $07
+DL_GR3 = $08
+DL_GR4 = $09
+DL_GR5 = $0A
+DL_GR6 = $0B
+DL_GR7 = $0D
+DL_GR8 = $0F
+DL_GR12 = $04 ; GR. 12-15 only supported by GRAPHICS command on XL/XE,
+DL_GR13 = $05 ; but they exist on all ANTIC revisions
+DL_GR14 = $0C
+DL_GR15 = $0E ; AKA "graphics 7.5"
+; No GRAPHICS mode for ANTIC $03 (true descender) mode
+
+; jump instructions
+DL_JMP = $01 ; jump without vertical blank (used to skip over 1K boundary)
+DL_JVB = $41 ; jump & wait for VBLANK (end of display list)
+
+; How to use the above: here's a sample display list for GR.0, with a DLI
+; on screen line 10.
+
+; dlist:
+; ; 4*8 = 32 blank lines at start of display
+; byte DL_BLANK8, DL_BLANK8, DL_BLANK8, DL_BLANK8
+;
+; byte DL_GR0 | DL_LMS ; display GR.0 line, and load screen memory address..
+; word screen_ram ; ...from our screen memory (declared elsewhere)
+;
+; ; 8 more GR.0 lines
+; byte DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0
+;
+; byte DL_GR0 | DL_DLI ; another GR.0 line, with the DLI bit enabled
+;
+; ; lines 11-24 (14 more GR.0 bytes)
+; byte DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0
+; byte DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0
+;
+; ; that's 24 lines, so finish with a VBLANK
+; byte DL_JVB ; jump (and wait), to...
+; word dlist ; ...the beginning.
diff --git a/fenders.1 b/fenders.1
new file mode 100644
index 0000000..aa6d5fc
--- /dev/null
+++ b/fenders.1
@@ -0,0 +1,282 @@
+.\" 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 "FENDERS" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py fenders.rst > fenders.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fIfenders\fP [\fI\-hrcsiv\fP] [\-t \fItitle\fP] \fIinfile.atr\fP [\fIoutfile.atr\fP]
+.SH DESCRIPTION
+.sp
+\fBfenders\fP replaces the boot sectors of an ATR image with a menu\-driven
+"game dos" binary loader.
+.sp
+When the disk is booted, a disk directory menu is displayed on the
+screen. To load a binary (XEX/COM/BIN/etc) file, press the letter
+shown for that file. Other file types may not be loaded (and will cause
+the Atari to crash).
+.sp
+The installed bootloader overwrites sectors 1\-3 of the image
+(and sector 720, for double\-density images). This is the same code
+installed by the Atari\-based \fBFenders 3\-sector\fP loader installer
+utility.
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \-h
+Print this help message.
+.TP
+.B \-r
+DON\(aqT reboot (coldstart) the Atari if Reset is pressed. The default is to coldstart.
+.TP
+.B \-c
+Rotate colors during load. May cause problems with some games.
+.TP
+.B \-s
+Screen off after load. May cause problems with some games, but
+may fix graphics corruption for other games. YMMV.
+.TP
+.B \-i
+In\-place update. The input file is renamed to end in \fB~\fP (tilde),
+and the output is written to the original filename. May not be
+used when reading from standard input.
+.TP
+.B \-v
+Set inverse video bit in title. This causes the title text to
+appear in red and/or blue on the Atari, instead of the default
+orange and green colors.
+.TP
+.B \-t
+Set the menu title. May up to 20 characters; default is \fIatari
+arcade\fP\&. Will be truncated to 20 characters if a longer title is
+given. See MENU TITLE, below. Note that you\(aqll have to quote the
+title if it contains spaces or other characters that have meaning to your shell.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B infile
+The ATR image to read from. It must be either single\-density
+and at least 368 sectors long, or double\-density and at least
+720 sectors long. You may use \fB\-\fP for \fIinfile\fP to read from
+standard input (unless \fB\-i\fP is used).
+.TP
+.B outfile
+The ATR image to create, which will contain all the files
+from the input image, plus the Fenders boot loader. You may
+omit \fIoutfile\fP or use \fB\-\fP to write to standard output. \fIoutfile\fP is
+ignored when the \fB\-i\fP option is used.
+.UNINDENT
+.sp
+If both \fIinfile\fP and \fIoutfile\fP are omitted, the default is to read from
+standard input and write to standard output.
+.SH NOTES
+.sp
+\fBfenders\fP will abort, if asked to write to standard output when standard
+output is a terminal. This is to avoid spewing binary garbage to your
+terminal.
+.sp
+The \fB\-r\fP, \fB\-c\fP, \fB\-s\fP, and \fB\-t\fP options actually modify the \fBfenders\fP 6502 object
+code before writing it to the ATR image.
+.sp
+The Atari \fBfenders\fP installer\(aqs default is to not coldstart, which
+tends to cause the Atari to lock up when Reset is pressed. The
+author believes that rebooting the Atari is more useful than locking
+it up, so \fBfenders\fP causes the coldstart by default.
+.sp
+The Atari \fBfenders\fP installer contains a bug: the meanings of the "Rotate
+color" and "Screen off after load" options are reversed. \fBfenders\fP does
+not duplicate this bug.
+.sp
+The disk density doesn\(aqt have to be specified, because \fBfenders\fP reads it
+from the ATR header. The loader\(aqs 6502 object code is different, for
+single\- and double\-density disks.
+.sp
+The double\-density version of the loader isn\(aqt actually a 3\-sector
+loader. The first 384 bytes of its code are stored in the 3 boot sectors, and the last 256 bytes of code are actually read from sector 720.
+When installing the boot loader on a double\-density disk, sector 720 is
+\fBoverwritten\fP\&. Any data that may have been there is lost. On most disks,
+sector 720 is either unusable by DOS, or not used unless the disk is
+completely full, so this is less of a problem than you might think.
+.sp
+\fBfenders\fP and the \fBfenders\fP boot\-loader code will work with DOS
+2.5 "enhanced density" formatted floppies, but only partially: files
+that use sectors above 720 will not appear in the menu (these are the
+same files that DOS 2.5 lists with <> around the filename).
+.sp
+\fBfenders\fP only works on Atari DOS 2.x and compatible (MyDOS, DOS XL, et
+al) single\-sided disk images, either single\-density (720 sectors, 90K),
+double\-density (720 sectors, 180K), or "1050 enhanced" density (1040
+sectors, 130K, although 1050 enhanced density images must be in DOS 2.5
+format, \fInot\fP MyDOS, and see \fBLIMITATIONS\fP below). Other non\-standard DOS
+formats such as SpartaDOS or Atari DOS 3.0 and 4.0 are not supported.
+Atari DOS 1.0 may or may not work (untested).
+.sp
+The \fBfenders\fP boot loader source code distributed with \fBfenders\fP
+is not the original source code (which has never been
+released). The author of \fBfenders\fP spent a couple of days with a
+disassembly of the object code and reverse engineered (labelled,
+commented) it so that humans can read it, provided they are humans who
+speak 6502 assembly. The \fIfenders.dasm\fP and \fIfendersdbl.dasm\fP files
+can be assembled with the DASM cross assembler. The resulting object
+code is identical with the original \fBfenders\fP object code (which
+the author read from a disk image in the first place).
+.sp
+Contrary to what you may have read, the Fenders boot loader code
+doesn\(aqt have to be rewritten to a disk if you add/delete files. It
+reads the disk directory, and can cope with changes just fine. Even a
+heavily fragmented filesystem won\(aqt cause any problems.
+.SH MENU TITLE
+.sp
+The menu title (set with \fB\-t\fP) is stored in sector 3 for a
+single\-density disk or sector 720 of a double\-density disk, in Atari
+internal screen codes (which are not the same as either ASCII or
+ATASCII). fenders converts the title into internal codes, so the user
+doesn\(aqt have to worry about this.
+.sp
+The menu title is displayed in "GRAPHICS 2" mode, which can only
+display 64 different characters, including uppercase, numbers, and
+(most) punctuation, but NOT including lowercase or inverse video.
+.sp
+However, lowercase letters are displayed as different\-colored
+uppercase letters, and inverse characters are also displayed with
+different colors. The boot loader uses the default Atari
+colors, as set by the Atari OS.
+.sp
+The title is limited to 20 characters because that\(aqs the width of a
+GRAPHICS 2 line of text on the Atari.
+.sp
+The character set used by GRAPHICS 2 consists of:
+.INDENT 0.0
+.IP \(bu 2
+The space character.
+.IP \(bu 2
+The letters \fBA\-Z\fP\&.
+.IP \(bu 2
+The numbers \fB0\-9\fP\&.
+.IP \(bu 2
+Punctuation: \fB!\fP \fB"\fP \fB#\fP \fB$\fP \fB%\fP \fB&\fP \fB\(aq\fP \fB(\fP \fB)\fP \fB*\fP \fB+\fP \fB,\fP \fB\-\fP \fB\&.\fP \fB/\fP \fB:\fP \fB;\fP \fB<\fP \fB=\fP \fB>\fP \fB?\fP \fB@\fP \fB[\fP \fB\e\fP \fB]\fP \fB^\fP \fB_\fP
+.UNINDENT
+.sp
+Uppercase letters, numbers, and punctuation are displayed in orange (or
+blue, if \fB\-v\fP is used).
+.sp
+Lowercase letters are displayed in green (or red, with \fB\-v\fP).
+.sp
+The characters \(ga { | } ~ are displayed as green (or red) versions of
+@ [ ] ^, respectively.
+.sp
+Currently, it\(aqs not possible to mix normal (orange/green) characters
+with inverse (blue/red) with fenders, unless your terminal provides a
+way for you to enter ASCII characters with their high bits set. Some
+versions of xterm(1x) allow this with the Alt or Meta key.
+.SH LIMITATIONS
+.sp
+\fBfenders\fP should warn if the disk image contains more than 20 files,
+since they won\(aqt all be displayed in the menu.
+.sp
+Double\-density images less than 720 sectors long are not handled,
+although they could be with a little more work.
+.sp
+For double\-density disks, the VTOC and sector link bytes should
+be checked to see if sector 720 is in use, rather than just blindly
+overwriting it. On a single\-density DOS 2.0S disk, sector 720 is
+marked "in use" in the VTOC when the disk is formatted, but will never
+be used for file storage.
+.sp
+No checking for non\-standard formats (SpartaDOS, DOS 3, MyDOS with
+subdirectories, dedicated bootdisks, etc) is done.
+.sp
+If an "enhanced" 1050 density disk image has the bootloader installed,
+it won\(aqt display the files using sectors above 720 (the ones which
+would appear as \fI<filename.ext>\fP in the DOS 2.5 directory).
+.sp
+Actually, the above limitations are a direct result of the fact that
+\fBfenders\fP deals with the disk image at the "sector" level, and contains
+no code that understands the files, directory, or VTOC on the image.
+The original Atari\-based Fenders installer shares the same limitations,
+so the author doesn\(aqt really consider them to be bugs, although they
+may be addressed in a future version of \fBfenders\fP\&.
+.sp
+Note that the bootloader only displays up to 20 files on the screen.
+This shouldn\(aqt be a real problem (how many games can you fit on a 180K
+floppy?), but no warning or error is given if there are too many files.
+.sp
+The bootloader only supports standard SIO disk speed (19200bps). The
+only way to make it support high\-speed SIO is to use the APE Warp OS
+(or some other high\-speed patched OS) on the Atari.
+.SH BUGS
+.sp
+There should be an option to delete DOS.SYS and DUP.SYS from the image,
+but there isn\(aqt. The original Fenders installer has this option.
+.sp
+When used with an image whose size according to the ATR header
+doesn\(aqt match the actual image file size, \fBfenders\fP may produce
+a broken or zero\-length output file. If in doubt, use \fBatrcheck\fP to
+validate the image before use.
+.sp
+There should be a way to install an arbitrary 3\-sector (384\-byte)
+binary file in the boot sectors of an image. This can be done with
+\fBdd\fP, of course, but its syntax is difficult to remember, and typos are
+prone to wipe out the file you\(aqre working with.
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/fenders.bin b/fenders.bin
new file mode 100644
index 0000000..ee8146c
--- /dev/null
+++ b/fenders.bin
Binary files differ
diff --git a/fenders.c b/fenders.c
new file mode 100644
index 0000000..55ad709
--- /dev/null
+++ b/fenders.c
@@ -0,0 +1,418 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "fenders_bin.h"
+#include "fenders_offsets.h"
+#include "fendersdbl_bin.h"
+#include "fendersdbl_offsets.h"
+
+extern char *optarg;
+extern int optind, opterr, optopt;
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "fenders"
+#define BANNER SELF " v" VERSION " by B. Watson (WTFPL)\n"
+#define DEFAULT_TITLE "atari arcade"
+#define OPTIONS "hrscit:v"
+
+char *usage =
+ BANNER
+ "Install Fenders 3-sector loader in boot sectors of an ATR image\n"
+ "Usage: " SELF " -[hrcsiv] [-t title] infile.atr [outfile.atr]\n"
+ " -h Print this help message\n"
+ " -r DON'T reboot (coldstart) the Atari if Reset is pressed\n"
+ " -c Rotate colors during load\n"
+ " -s Screen off after load\n"
+ " -i In-place update (original renamed to end in ~)\n"
+ " -v Set inverse video bit in title (blue/red text)\n"
+ " -t title Set title (up to 20 chars, default: '" DEFAULT_TITLE "')\n";
+
+typedef enum { SD, DD } density;
+
+void set_title(char *title, density dens, int inverse) {
+ int i;
+ int offset;
+ unsigned char *bin;
+ int len = strlen(title);
+
+ if(dens == SD) {
+ offset = OFFSET_TITLE;
+ bin = fenders_bin;
+ } else {
+ offset = OFFSET_TITLE_DD;
+ bin = fendersdbl_bin;
+ }
+
+ /* zero out the title area first (zeroes are Atari spaces BTW) */
+ memset(&bin[offset], 0, 20);
+
+ if(len > 20) {
+ len = 20;
+ fprintf(stderr, SELF ": Truncating title to 20 characters\n");
+ } else if(!len) {
+ return;
+ }
+
+ /* convert ASCII to Atari screen codes (not the same as ATASCII)
+ charset in GR.2 is space plus:
+ !"#$%'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
+
+ Punctuation comes out orange (blue with -v)
+ Digits and uppercase letters are orange/blue
+ Lowercase letters are green/red
+ ` = green/red @
+ { = green/red [
+ | = green/red ^
+ } = green/red ]
+ ~ = green/red ^
+ */
+ for(i=0; i<len; i++) {
+ char c = title[i];
+
+ if(c >= 32 && c <= 95)
+ c -= 32;
+
+ if(inverse)
+ c |= 128;
+
+ title[i] = c;
+ }
+
+ /* Center title in 20-byte title area */
+ memcpy(&bin[offset + 10 - len / 2], title, len);
+
+ /*
+ fprintf(stderr, SELF ": Set title to \"");
+ for(i=0; i<20; i++) {
+ char c = bin[offset + i];
+ if(!c) c = ' ';
+ if(!isprint(c)) c = '.';
+ putc(c, stderr);
+ }
+
+ putc('"', stderr);
+ putc('\n', stderr);
+ */
+}
+
+/* for set_coldstart(), set_rot_color(), set_screen_off(),
+ see fenders.dasm (and fendersdbl.dasm) to understand what's going on.
+ Search for the OFFSET_* strings. */
+
+void set_coldstart(density dens) {
+ if(dens == SD) {
+ fenders_bin[OFFSET_COLDST_1] = 1; /* operand for LDY # (replaces 0) */
+ fenders_bin[OFFSET_COLDST_2] = 0xea; /* NOP (replaces INY) */
+ } else {
+ fendersdbl_bin[OFFSET_COLDST_1_DD] = 1;
+ fendersdbl_bin[OFFSET_COLDST_2_DD] = 0xea;
+ }
+}
+
+void set_rot_color(density dens) {
+ if(dens == SD)
+ fenders_bin[OFFSET_ROTCOLOR] = 0x8d; /* STA abs (replace LDA abs) */
+ else
+ fendersdbl_bin[OFFSET_ROTCOLOR_DD] = 0x8d;
+}
+
+void set_screen_off(density dens) {
+ if(dens == SD)
+ fenders_bin[OFFSET_SCREENOFF] = 0x8d; /* STA abs (replace LDA abs) */
+ else
+ fendersdbl_bin[OFFSET_SCREENOFF_DD] = 0x8d;
+}
+
+int main(int argc, char **argv) {
+ int coldstart = 1, rot_color = 0, screen_off = 0;
+ int c, res, size, inverse = 0;
+ char title[21];
+ int in_place = 0;
+ char rename_to[4096];
+ unsigned char buf[384], *bin;
+ char *infile = "-", *outfile = "-";
+ FILE *in = stdin, *out = stdout;
+ density dens;
+
+ /* initialize title (may be changed by -t option) */
+ strcpy(title, DEFAULT_TITLE);
+
+ /* parse options */
+ while( (c = getopt(argc, argv, OPTIONS)) != -1) {
+ switch(c) {
+ case 'h':
+ printf(usage);
+ exit(0);
+ break;
+
+ case 'r':
+ coldstart = 0;
+ break;
+
+ case 'c':
+ rot_color = 1;
+ break;
+
+ case 's':
+ screen_off = 1;
+ break;
+
+ case 't':
+ strcpy(title, optarg);
+ break;
+
+ case 'i':
+ in_place = 1;
+ break;
+
+ case 'v':
+ inverse = 1;
+ break;
+
+ default:
+ fprintf(stderr, usage);
+ exit(1);
+ break;
+ }
+ }
+
+ /* get input filename if present */
+ if(optind < argc)
+ infile = argv[optind++];
+
+ /* get output filename if present */
+ if(!in_place && optind < argc)
+ outfile = argv[optind++];
+
+ if(optind < argc) {
+ fprintf(stderr, SELF
+ ": Ignoring extra junk '%s ...' on command line.\n",
+ argv[optind]);
+ }
+
+ if(in_place) {
+ /* rename infile to infile~, set outfile to old infile */
+ int len = strlen(infile);
+
+ if(strcmp(infile, "-") == 0) {
+ fprintf(stderr, SELF
+ ": Can't use in-place mode with standard input.\n");
+ exit(1);
+ }
+
+ strcpy(rename_to, infile);
+ rename_to[len] = '~';
+ rename_to[len + 1] = '\0';
+
+ fprintf(stderr, SELF ": Backing up %s to %s\n", infile, rename_to);
+ if(link(infile, rename_to)) {
+ perror("link()");
+ exit(1);
+ }
+
+ if(unlink(infile)) {
+ perror("unlink()");
+ exit(1);
+ }
+
+ outfile = infile;
+ infile = rename_to;
+ }
+
+ /* open input and output files, if not stdin/stdout */
+ if(strcmp(infile, "-") != 0) {
+ in = fopen(infile, "rb");
+ if(!in) {
+ perror(infile);
+ exit(1);
+ }
+ }
+
+ /* read ATR header */
+ res = fread(buf, 1, 16, in);
+ if(res < 16) {
+ perror(infile);
+ exit(1);
+ }
+
+ /* make sure it's an ATR image */
+ if( !(buf[0] == 0x96 && buf[1] == 0x02) ) {
+ fprintf(stderr, SELF
+ ": %s not an ATR file (no NICKATARI signature)!\n"
+ "If this is an XFD file, try xfd2atr\n",
+ infile);
+ exit(2);
+ }
+
+ /* get sector size. The single- and double-density versions of
+ the loader are totally different, so pick the one we need. */
+ if( (buf[4] == 0x80 && buf[5] == 0x00) ) {
+ dens = SD;
+ bin = fenders_bin;
+ } else if( (buf[4] == 0x00 && buf[5] == 0x01) ) {
+ dens = DD;
+ bin = fendersdbl_bin;
+ } else {
+ fprintf(stderr, SELF ": ATR image must have 128 or 256 byte sectors\n");
+ exit(2);
+ }
+
+ /* modify the loader according to the user's options */
+ set_title(title, dens, inverse);
+ if(coldstart) set_coldstart(dens);
+ if(rot_color) set_rot_color(dens);
+ if(screen_off) set_screen_off(dens);
+
+ /* size of ATR image in bytes (minus the header). We don't support
+ DD images less than 720 sectors. */
+ size = (buf[2] + (buf[3] << 8) + (buf[6] << 16)) * 16;
+ if(dens == SD && size < (128 * 369)) {
+ fprintf(stderr, SELF
+ ": ATR is single density < 369 sectors, not supported\n"
+ "Use atrsize to grow the image.\n");
+ exit(2);
+ } else if(dens == SD && size > (128 * 720)) {
+ fprintf(stderr, SELF
+ ": ATR is single density > 720 sectors; "
+ "some files may not appear in menu.\n");
+ } else if(dens == DD && size < (128 * 3 + 256 * 717)) {
+ fprintf(stderr, SELF
+ ": ATR is double density < 720 sectors, not supported\n"
+ "Use atrsize to grow the image.\n");
+ exit(2);
+ } else if(dens == DD && size > (128 * 3 + 256 * 717)) {
+ /* 20071005 bkw: whoops, we were truncating large images to 180K.
+ The bootloader doesn't work with MyDOS >180K formats anyway, so
+ don't try.
+ fprintf(stderr, SELF
+ ": ATR file is double density > 720 sectors; some files may not"
+ "appear in the menu or load correctly\n");
+ */
+
+ /* Abort instead */
+ fprintf(stderr, SELF
+ ": ATR is double density > 720 sectors, not supported "
+ "by bootloader (try MyPicoDOS)\n");
+ exit(2);
+ }
+
+ /* Input looks OK, open the output... */
+ if(strcmp(outfile, "-") != 0) {
+ out = fopen(outfile, "wb");
+ if(!out) {
+ fclose(in);
+ perror(outfile);
+ exit(1);
+ }
+ } else if(isatty(fileno(stdout))) {
+ /* don't scare the n00bs! */
+ fprintf(stderr,
+ SELF ": Standard output is a terminal, not writing binary data.\n"
+ "Either redirect to a file or set the output filename.\n");
+ exit(1);
+ }
+
+ /* write ATR header */
+ res = fwrite(buf, 1, 16, out);
+ if(res < 16) {
+ perror(outfile);
+ exit(1);
+ }
+
+ /* read (and ignore) first 3 sectors. A DD image still uses SD
+ sectors for the first 3 (boot) sectors on the disk. */
+ res = fread(buf, 1, 384, in);
+ if(res < 384) {
+ perror(infile);
+ exit(1);
+ }
+
+ /* Write the loader. For SD disks, this is the whole thing.
+ For DD disks, this is the first 384 bytes (3 boot sectors),
+ and we'll have to write the rest of the object code to sector 720 */
+ res = fwrite(bin, 1, 384, out);
+ if(res < 384) {
+ perror(outfile);
+ exit(1);
+ }
+
+ if(dens == SD) {
+ /* single density can just use a simple copy loop */
+ while( (c = getc(in)) != EOF )
+ putc(c, out);
+ } else {
+ /* double density: copy sectors 4-719 as-is... */
+ for(c=4; c<720; c++) {
+ if(fread(buf, 1, 256, in) < 256) {
+ if(feof(in)) {
+ fprintf(stderr, SELF
+ ": got premature EOF (bad/truncated ATR image).\n");
+ } else {
+ perror(infile);
+ }
+ exit(1);
+ }
+
+ if(fwrite(buf, 1, 256, out) < 256) {
+ perror(outfile);
+ exit(1);
+ }
+ }
+
+ /* TODO: check the VTOC and/or look for non-zero data in sector
+ 720, warn the user if the sector was in use. */
+
+ /* TODO: fix bootloader to work with MyDOS-style sector link bytes.
+ Also, store last part of bootloader somewhere not used by MyDOS,
+ maybe sector 369 (last directory sector, unused on disks with
+ less than 56 files... and 56 files is way too many to fit on
+ screen in GR.1). This should happen on both single and double
+ density images. */
+
+ /* TODO: examine directory sectors, look for:
+ - DOS 2.5 extended files. Either warn about them, or clear the
+ extended flag (which causes the bootloader to load them just fine).
+ - MyDOS subdirectories. It's probably best to abort in that case.
+ - Non-DOS-compatible disk formats (boot disks, SpartaDOS, Atari
+ DOS 3 or 4).
+ */
+
+ /* TODO: add option to delete DOS.SYS and DUP.SYS (or more likely,
+ delete them by default, and add option to allow user to keep them).
+ */
+
+ /* TODO: option that creates a new, blank image, with bootloader
+ already on it? There's already "atrsize -b" for creating a blank
+ image... */
+
+ /* write the rest of the loader code code to sector 720.
+ The code in the boot sectors will load sector 720. */
+ if(fwrite(bin+384, 1, 256, out) < 256) {
+ perror(outfile);
+ exit(1);
+ }
+ }
+
+ /* set return value: 0 for success, 1 for failure */
+ c = 0;
+
+ if(ferror(in)) {
+ perror(infile);
+ c = 1;
+ }
+
+ if(ferror(out)) {
+ perror(outfile);
+ c = 1;
+ }
+
+ /* ...and I'm spent! */
+ return c;
+}
diff --git a/fenders.dasm b/fenders.dasm
new file mode 100644
index 0000000..7bec9bc
--- /dev/null
+++ b/fenders.dasm
@@ -0,0 +1,570 @@
+
+; Fender's 3-sector loader (boot code only)
+
+; A "game DOS" binary file loader. Install it on a disk full of binaries,
+; it replaces the DOS boot loader. You don't need DOS.SYS or DUP.SYS on
+; such a disk, so there's more room for games. When you boot, the 3-sector
+; loader reads the directory and presents you with a menu of up to 20
+; files to load with a single keypress.
+
+; presumably written by someone named Fender, in the early 1980s
+
+; I have no idea what the licensing is (shareware, public domain, ???),
+; but after all this time, I don't think anyone's going to sue me.
+
+; Disassembled with dis6502
+; Commented, labelled, and reformatted for DASM use by B. Watson, 20061105
+
+; Can be assembled to a 384-byte object file with a command like:
+
+; dasm fenders.dasm -ofenders.obj -f3
+
+; The object code can be installed on an existing single-density ATR
+; floppy image on UN*X like so:
+
+; dd if=fenders.obj of=disk.atr bs=1 count=384 seek=16 conv=notrunc
+
+; (for an XFD image, leave off the "seek=16")
+
+; This is a standalone source file: it doesn't require any "system equates"
+; include file.
+
+; Intended for use with DASM, but doesn't use macros or other fancy
+; features, so it'll probably assemble on most 6502 assemblers.
+; Your assembler may use a slightly different syntax for conditional
+; assembly, if so search the file for ".if" and change as needed.
+; Also, remove the "processor" line if you aren't using DASM:
+
+ processor 6502
+
+; This disassembly only contains the boot loader code, not the installer.
+; The installer is 30 sectors long (10x the size of the loader), and
+; pretty straightforward.
+
+; The installer allows you to customize the loader somewhat. It does
+; this by changing the boot loader code before writing it. The options
+; in the installer are:
+
+; Option | Label
+; A. Cause COLDSTART on RESET | COLDSTART_ON_RESET
+; B. Rotate color during load | SCREEN_OFF [*]
+; C. Screen off after load | ROTATE_COLOR [*]
+; D. Title | (see "screen" label near end of this file)
+; F. Change density | DENSITY (TODO: support this)
+; (the other options don't change the boot code)
+
+; [*] B and C options are mislabelled in the installer, selecting
+; "Rotate color" actually toggles "Screen off", and vice versa.
+
+; Options A, B, and C cause the installer to modify the in-memory copy
+; of the boot code before writing it to disk. I've used conditional
+; assembly to simulate this:
+
+COLDSTART_ON_RESET .equ 0 ; 0=false, non-zero=true
+ROTATE_COLOR .equ 0 ; 0=false, non-zero=true
+SCREEN_OFF .equ 0 ; 0=false, non-zero=true
+
+; TODO: support conditional assembly for double density.
+; Unlike the first 3 options, double density changes a lot of code.
+; The original author doesn't change the code at runtime to support
+; double-density; the installer contains complete copies of the
+; single and double density boot code.
+;DENSITY .equ 1 ; 1=single, 2=double
+
+
+;;; OS equates
+
+;; OS ROM entry points
+
+; Question for you Atari SIO and OS experts: why does "Mappping the
+; Atari" state that you're supposed to call $E543 for the disk,
+; instead of $E459, but the author of this program used $E459 anyway?
+SIOV .equ $e459 ; Serial I/O
+
+COLDSV .equ $e477 ; Cold Start (reboot)
+KEYBDV .equ $e420 ; K: handler device table
+keyb_get_lo .equ KEYBDV+4 ; pointer to "get byte" routine, minus 1
+keyb_get_hi .equ KEYBDV+5 ; (used by get_key)
+
+
+;; OS zero page
+BOOTQ .equ $09 ; Tells OS what to do when RESET is pressed:
+ ; (0 = reboot, 1 = warmstart, JSR through DOSVEC)
+
+;; OS and FMS page 2 RAM variables
+SDMCTL .equ $022f ; used to turn off the screen when done
+SDLSTL .equ $0230 ; display list start pointer
+COLDST .equ $0244 ; "coldstart in progress" flag
+RUNAD .equ $02e0 ; The run address of the loaded file
+INITAD .equ $02e2 ; The init address of the loaded file (can be >1 per file)
+
+;; DCB, used for sector I/O parameters by SIOV (called by read_sector)
+DDEVIC .equ $0300 ; Device serial bus ID (set to $31 by OS disk boot)
+DUNIT .equ $0301 ; Drive number (we always use 1)
+DCOMND .equ $0302 ; SIO command (set to $52, "Read sector", by OS)
+DSTATS .equ $0303 ; Data direction register: $40 for "read"
+DBUFLO .equ $0304 ; I/O buffer, lo byte
+DBUFHI .equ $0305 ; I/O buffer, hi byte
+DTIMLO .equ $0306 ; SIO timeout
+DBYTLO .equ $0308 ; number of bytes to transfer (AKA sector size), lo byte
+DBYTHI .equ $0309 ; ...hi byte
+DAUX1 .equ $030a ; For "Read" command, the sector number (lo byte)
+DAUX2 .equ $030b ; ...hi byte
+
+;; Hardware registers
+COLPF1 .equ $d017 ; GTIA, used by read_sector to rotate the title color
+
+;; Local variables (zero page)
+dest_ptr .equ $43 ; (AKA FMSZPG) 2 bytes, init to load address
+end_address .equ $45 ; 2 bytes
+save_pos .equ $49 ; 1 byte: saves X register while loaded init routine runs
+menu_counter .equ $b0 ; tracks how many entries (filenames) are in the menu
+dir_sector_lo .equ $b1 ; lo byte of current directory sector
+ ; (range $69-??, high byte is always 1)
+menu_ptr .equ $b2 ; points to screen RAM, for printing filenames
+
+; Starting sector tables. For each file numbered N on the disk,
+; do_dirent saves the low byte of its starting sector at
+; start_sector_lo_tbl+N and its high byte at start_sector_hi_tbl+N
+; Each table is only 32 bytes, but that's fine as we stop reading the
+; directory after 20 files are found (all that will fit on the screen).
+start_sector_lo_tbl .equ $c0
+start_sector_hi_tbl .equ $e0
+
+;;; Local variables (non zero page)
+buffer .equ $0b00
+sector_link_hi .equ $0b7d ; buffer + $7d
+sector_link_lo .equ $0b7e ; buffer + $7e
+sector_byte_count .equ $0b7f ; buffer + $7f
+
+;;; Bootable disk image starts here
+ .org $0700
+
+;;; Standard Atari boot disk header (6 bytes)
+boot_record:
+ .byte $00 ; ignored
+ .byte $03 ; number of sectors to read
+ .word boot_record ; load address
+ .word COLDSV ; init address, don't think this gets used
+
+;;; Actual code starts here
+boot_continuation: ; OS starts running our code here.
+OFFSET_COLDST_1 equ *-boot_record+1
+OFFSET_COLDST_2 equ *-boot_record+5
+ .if COLDSTART_ON_RESET ; installer option A
+ LDY #$01 ; tell OS to reboot if RESET pressed
+ STY COLDST ; (coldstart in progress = true)
+ NOP
+ .else
+ LDY #$00 ; tell OS not to reboot if RESET pressed
+ STY COLDST ; (coldstart in progress = false)
+ INY
+ .endif
+
+ ; either way, Y is now 1
+ STY BOOTQ ; tell OS we booted successfully
+ STY DUNIT ; set drive #1 for later SIO use
+
+ DEC DTIMLO ; decrease default disk I/O timeout
+
+ ; set up our custom display list
+ LDA #<display_list ; 74 J
+ STA SDLSTL
+ LDA #>display_list ; 8 .
+ STA SDLSTL+1
+
+ ; set up to start reading the directory
+ LDA #<menu
+ STA menu_ptr
+ LDA #>menu
+ STA menu_ptr+1
+ LDA #$69 ; Start loading directory at sector 361 ($0169)
+ STA dir_sector_lo
+
+read_dir_sector:
+ LDA dir_sector_lo
+ STA DAUX1
+ LDA #$01 ; The directory is located at sectors 361-368,
+ STA DAUX2 ; so the high byte is always 1
+ JSR read_sector
+ INC dir_sector_lo
+ DEX
+
+ ; X reg is the pointer into the sector buffer, keep that in mind
+do_dirent:
+ LDA buffer,X ; look at status byte
+ BEQ wait_for_input ; if it's 0, there are no more files, so we're done
+ BMI next_dirent ; if it's deleted (bit 7 true), skip it
+ AND #$01 ; open for write if bit 0 set
+ BNE next_dirent ; If it's open for writing, skip it
+ INC menu_counter
+ LDY menu_counter
+
+ ; grab and save starting sector; we're building a table in RAM that
+ ; holds the starting sector for every file on the disk
+ LDA buffer+3,X
+ STA start_sector_lo_tbl,Y
+ LDA buffer+4,X
+ STA start_sector_hi_tbl,Y
+ TYA
+
+ ; draw "A.", "B.", etc (menu choice letters) in the menu
+ ; (menu_ptr) points to column 0 of the current menu line, and gets
+ ; 20 added to it each time through the loop (we're using GR.1, which
+ ; has 20 bytes per screen line).
+ ; Y is the offset into the current line, AKA the column number
+ CLC
+ ADC #$a0 ; GR.2 offset into color 2 alphabet (128+32=160)
+ LDY #$03 ; Menu letter goes in column 3 of the current menu line
+ STA (menu_ptr),Y
+ INY
+ LDA #$8e ; blue period (no, really, a period that's blue)
+ STA (menu_ptr),Y ; Store in column 4
+ INY
+
+next_char:
+ INY
+ LDA buffer+5,X ; buffer+5 holds the first ATASCII char of the filename
+ INX
+ SEC
+ SBC #$20 ; subtracting 32 makes them come out in color 1
+ STA (menu_ptr),Y
+ CPY #$10 ; done with 16-byte dirent?
+ BNE next_char ; if not, do next character
+
+ ; Set up pointer for next screen line
+ CLC
+ LDA menu_ptr
+ ADC #$14 ; skip to next GR.1 line ($14 = 20 bytes)
+ STA menu_ptr
+ BCC skip_ptr_hi
+ INC menu_ptr+1 ; inc hi byte if necessary
+skip_ptr_hi:
+ LDA menu_counter
+ CMP #$14 ; see if the screen is full (max entries = 20)
+ BEQ wait_for_input ; yep, stop adding entries, or...
+
+next_dirent:
+ ; set X to point to the offset of the next dir entry
+ ; they occur every 16 bytes, so: X = (X mod 16) + 16
+ TXA
+ AND #$f0
+ CLC
+ ADC #$10
+ TAX
+
+ ; dirents only take up the first 128 bytes of a dir sector, even
+ ; on a double density disk, so:
+ ASL ; see if the high bit it set (A >= 128)...
+ BCC do_dirent ; no, we're still in the same dir sector, do next entry, or..
+ BCS read_dir_sector ; yes, we need to load the next sector
+
+wait_for_input:
+ JSR get_key ; get_key waits for the user to press a key and returns its
+ ; ATASCII value in A
+ SEC
+ SBC #$40 ; Convert from ATASCII A-Z to numbers 1-26
+ CMP menu_counter ; See if it's one of our valid menu entries
+
+ ; do a "branch if less than or equal" to load_file:
+ BEQ load_file ; if equal, branch
+ BCS wait_for_input ; else if greater than, ignore the keypress and
+ ; wait for the user to press another key
+ ; else if less than, fall through to load_file
+
+load_file: ; A register has the file number (1 indexed; there's no file #0)
+ TAX
+ INC menu_dlist,X ; change selected entry from GR.1 to GR.2, so the user
+ ; can see which file he'd selected
+
+ ; get starting sector of this file
+ LDA start_sector_lo_tbl,X
+ STA DAUX1
+ LDA start_sector_hi_tbl,X
+ STA DAUX2
+
+ JSR try_read ; read the first sector...
+ DEX ; point X at first byte we just read
+
+; main loop of the program (see "Atari Binary Load Format", near end of file)
+read_segment:
+ ; get the segment load address and save in dest_ptr
+ JSR get_next_byte
+ STA dest_ptr
+ JSR get_next_byte
+ STA dest_ptr+1
+ AND dest_ptr
+ CMP #$ff ; If we just read two $FF bytes, throw them out
+ BEQ read_segment ; ...and read the next 2 bytes instead
+
+ ; get the segment end address and save in end_address
+ JSR get_next_byte
+ STA end_address
+ JSR get_next_byte
+ STA end_address+1
+
+load_byte:
+ JSR get_next_byte ; get next loaded data byte
+ STA (dest_ptr),Y ; store in destination RAM
+ INC dest_ptr ; bump destination pointer: lo byte
+ BNE check_seg_done ; skip the hi byte if the lo byte didn't wrap around
+ INC dest_ptr+1 ; bump hi byte, if needed
+ BEQ check_for_init ; stop loading this seg if address wraps $FFFF -> $0000
+ ; else fall through to check_seg_done
+
+check_seg_done:
+ LDA end_address ; double-byte double compare, to see if we've
+ CMP dest_ptr ; finished loading all the data in the segment
+ LDA end_address+1
+ SBC dest_ptr+1 ; (SBC instead of CMP because we care about the carry here)
+ BCS load_byte ; if we're not done, load the next byte of data
+
+check_for_init:
+ LDA INITAD
+ ORA INITAD+1
+ BEQ read_segment ; if init addr is 0, we don't have one, go do next seg
+ STX save_pos ; else save byte position (X reg) and do the init
+ JSR do_init
+ LDX save_pos ; init routine returns here, reload byte position
+ LDY #$00 ; zero out init address so it doesn't run again next time
+ STY INITAD ; (unless of course the next segment(s) load at INITAD)
+ STY INITAD+1
+ BEQ read_segment ; unconditional branch, go read next segment
+
+ ; JSR do_init simulates indirect JSR
+do_init:
+ JMP (INITAD)
+
+; WARNING: self-modifying code. read_sector modifies the operand
+; of the CPX, setting it to the number of valid data bytes in the
+; sector just read. The initial value of $7d represents the number of
+; data bytes in a sector that's full of data (125 bytes of actual data).
+get_next_byte:
+ CPX #$7d ; are we done with all the bytes in this sector?
+ BNE return_next_byte ; if not, go do the next one
+
+ LDA DAUX1 ; have we read the last sector?
+ ORA DAUX2 ; (if so, the "next sector" stored at DAUX1/2 will be 0)
+ BNE try_read ; if not, go read the next sector
+OFFSET_SCREENOFF equ *-boot_record
+ .if SCREEN_OFF ; installer option C, "turn off screen after loading"
+ STA SDMCTL ; turn off the screen (er, why?)...
+ .else
+ LDA SDMCTL ; don't turn off the screen
+ .endif
+ JMP (RUNAD) ; ...we're done loading, go run the binary!
+
+read_sector:
+ LDA #>buffer
+ STA DBUFHI ; set sector buffer (lo byte presumably defaults to 0?)
+ LDA #$80 ; sector size for SIO: 128 bytes for single density
+ STA DBYTLO
+
+try_read:
+ LDA #$40 ; data direction = read (bit 6 set, bit 7 clear)...
+ STA DSTATS ; DSTATS is where we store the data direction
+ JSR SIOV ; all set up, so call SIO
+ BMI try_read ; on error, just retry (forever, if need be)
+
+ ; set up SIO sector number for next time:
+ LDA sector_link_hi ; The high 2 bits of the sector link (AKA next sector in
+ ; file) are stored in the LOW 2 bits of sector_link_hi,
+ ; along with the file number in bits 2-7
+ AND #$03 ; mask off file number
+ STA DAUX2
+ LDA sector_link_lo ; sector link, low 8 bits
+ STA DAUX1
+
+ ; rotate PF color
+OFFSET_ROTCOLOR equ *-boot_record
+ .if ROTATE_COLOR ; installer option B
+ STA COLPF1
+ .else
+ LDA COLPF1
+ .endif
+
+ ; save number of data bytes in sector
+ LDA sector_byte_count
+ AND #$7f ; hmm, is this really necessary?
+ STA get_next_byte+1 ; WARNING: self-modifying code (see above)
+
+ ; init source and destination indices
+ LDY #$00
+ LDX #$00
+
+return_next_byte:
+ LDA buffer,X
+ INX
+ RTS
+
+; Simulate JSR through keyb_get_lo. This is one of those Atari
+; "entry point minus one" vectors. To call it, you push it onto the
+; stack and do an RTS (works because a real JSR saves the PC minus 1 on
+; the stack).
+; Becauses it gets the vector from ROM instead of hard-coding it in
+; the get_key routine, this code will work on all Atari models (400/800,
+; XL/XE). It's a compromise between hardcoding the address and doing
+; a full-blown IOCB setup and CIO call.
+get_key:
+ LDA keyb_get_hi
+ PHA
+ LDA keyb_get_lo
+ PHA
+ RTS
+
+;;; Data
+display_list: ; display list, $084A - $0867
+ ; 3 "blank 8 scans", a GR.2 line with LMS, then screen RAM address
+ .byte $70,$70,$70,$47,<screen,>screen ; "pppGh."
+menu_dlist:
+ ; blank 8 scans, then 20 GR.1 lines, then a jump back to the
+ ; start of the display list.
+ .byte $70,$06,$06,$06,$06,$06,$06,$06 ; "p......."
+ .byte $06,$06,$06,$06,$06,$06,$06,$06 ; "........"
+ .byte $06,$06,$06,$06,$06,$41,<display_list,>display_list ; ".....AJ."
+
+ ; screen RAM, $0868 - $087F and beyond
+OFFSET_TITLE equ *-boot_record
+screen:
+ ; $00 is a space, lowercase letters come out as caps (color reg 1)
+ .byte $00,$00,$00,$00 ; 4 spaces for centering
+ .byte "atari", $00, "arcade" ; "ATARI ARCADE" in green
+ .byte $00,$00,$00,$00 ; rest of the GR.2 line (20 bytes total)
+menu: ; the filenames start getting stored here, 2nd line of the display
+
+ ; 4 filler bytes, so we come out at an even 3 sectors (384 bytes)
+ .byte $00,$00,$00,$00
+
+;;; End of code. Rest of file is boring documentation :)
+
+; Atari Binary Load File Format - A Primer
+
+; The binary load format was first defined for Atari DOS 1.0, and supported
+; by all DOSes for the Atari. There's no formal standard that I'm aware of;
+; the closest thing is probably the binary load implementation in Atari
+; DOS 2.0S or 2.5 (the most widely-used DOSes from Atari), but that's
+; copyrighted code, so it couldn't be used as a base to build on or as a
+; module for another DOS.
+
+; Sadly, Atari didn't add a CIO SPECIAL (aka XIO from BASIC) command to load
+; binary files as part of their DOS's public API... so anyone who wanted to
+; write a game-loader or menu that runs under DOS would have to write their
+; own loader (for compatibility with many DOSes), or just assume they'll
+; be running under DOS 2 and JSR to the unpublished entry point within DOS
+; (which changed between DOS 2 and 2.5). Some third-party DOSes (SpartaDOS,
+; MyDOS) included a CIO/XIO call for loading and/or running binary files,
+; which meant a menu program was easy to write in just a few lines of
+; BASIC, but it wouldn't work on standard Atari DOS, and might not even
+; be portable from Sparta to MyDOS, if they didn't choose the same CIO
+; command for binary loading.
+
+; None of this DOS-related discussion is relevant to Fenders, though.
+; The whole point of Fenders is to do away with the overhead of DOS, so:
+
+; - You don't have to keep copies of DOS.SYS (39 sectors) or DUP.SYS (42
+; sectors) on your disk. Together these take up over 10% of the available
+; storage on a DOS 2 formatted disk. With Fenders you get all 707 sectors
+; to use for game storage.
+
+; - You don't have to wait for a full-blown DOS boot. It takes a while
+; to load DOS.SYS and DUP.SYS (81 sectors, taken together), which are a
+; rather complete file management system and menu interface that you don't
+; need for playing games. With Fenders, you get your menu almost instantly.
+
+; - Once DOS and DUP are loaded, you have to type "A Return Return" to
+; get a disk directory, then "L Return FILENAME Return" to load a file.
+; With Fenders, you type one letter.
+
+; There is no support in the OS ROM for the binary load format, or for
+; the disk device at all at the CIO level (the ROM does have SIO routines
+; for reading/writing raw sectors, which is what Fenders uses). There are
+; many binary loader implementations on the Atari. Each third-party DOS
+; generally had to roll its own, and there were numerous "No DOS" and
+; "Game DOS" utilities (like Fenders) that had to implement read-only
+; support for the DOS 2 filesystem along with a binary loader. Fenders
+; does this in 330 bytes of code (including the user interface).
+
+; An Atari binary load file consists of one or more segments, each with
+; its own load address and end address.
+
+; A segment is:
+
+; FFFF leader (bytes 0 and 1) - A two-byte leader: $FF, $FF
+; Required for 1st segment, optional for others
+; Load address (bytes 2 and 3 in standard 6502 LSB/MSB)
+; End address (bytes 4 and 5)
+; (Load address - End address + 1) bytes of data to be loaded
+
+; Fenders actually doesn't require the FFFF leader even for the first
+; segment, and could deal with multiple FFFF leaders if present: it
+; just skips over them. I haven't checked to see if Atari DOS does
+; the same thing or not.
+
+; A well-formed binary load file must have an end address higher than the
+; start address for every segment. It's undefined what happens otherwise
+; (Fenders quits loading if the current address ever wraps around from
+; $FFFF to 0000, I don't know what it does if the end address is the same
+; as the start address).
+
+; There's no rule against having overlapping segments, but it's kind
+; of an odd thing to do.
+
+; After each segment is loaded, a loader must check INITAD (a 16-bit
+; vector at location $02E2) to see if the segment loaded anything at
+; that address. If so, a JSR through the vector is performed, with the
+; expectation that the loaded program's initialization routine will run
+; and then do an RTS to return control to the loader. This is often used
+; (or abused) by crackers to load a screen with their name/logo. In a
+; real DOS, IOCB #1 is left open, so theoretically the init routine could
+; read data from the file, located between segments as it were. Fenders
+; doesn't do this, as it doesn't implement a CIO handler at all. Any program
+; that relies on this behavior won't work with Fenders, which isn't much
+; of a limitation (I doubt this capability has ever been used in the
+; entire history of the Atari. It might be useful for something like an
+; auto-relocating load?). Use of the init vector is optional. Since it's
+; checked after every segment, there can be more than one init routine
+; run this way.
+
+; After all segments have been loaded, a loader must check RUNAD (16-bit
+; vector located at $2E0) to see if any segment loaded anything at that
+; address. If so, a JSR through the vector is performed to run the main
+; routine of the just-loaded program, which may or may not do an RTS to
+; return control to the caller. In a real DOS, the RTS will generally redraw
+; the DOS menu or print a new command prompt (whatever makes sense). Fenders
+; does a JMP (RUNAD), not expecting the loaded program to return (most
+; games just run forever).
+
+; If RUNAD never gets loaded, a real DOS will return to its menu or prompt
+; without trying to run anything. You could use this to just load some
+; data (a font, maybe) into memory. Fenders will crash if RUNAD never gets
+; loaded, since it's set to 0 by the OS (a JMP (0) happens, transferring
+; control off to never-never-land). Again, not much of a limitation, since
+; it's intended for running games, not general-purpose data-file loading.
+
+; There is ABSOLUTELY NO protection against a binary file trying to
+; load on top of Fenders while it's running (or the memory-mapped
+; I/O registers, or zero page, or anywhere it wants to). There is also
+; no protection against a loaded init routine messing up the zero page
+; pointers or disk buffer. In practice, this doesn't happen often
+; (and a "rude" file that did this would probably have problems loading
+; under a standard DOS, too).
+
+; This information comes from various sources. I probably gleaned most of
+; it from "Mapping the Atari" or the Compute! magazine "Insight: Atari"
+; column, way back when. Both of these are available on the web now:
+
+; Mapping the Atari, De Re Atari, and a lot of other books can be
+; found at http://www.atariarchives.org
+
+; Most of the old Compute! and Antic magazines have been scanned and
+; archived at http://www.atarimagazines.com/
+
+; There's also a "Digital ANALOG Project" that's got a lot of Analog
+; magazine issues archived: http://www.cyberroach.com/analog/
+
+; For a compact reference to the binary load format, see:
+; http://www.atarimax.com/jindroush.atari.org/afmtexe.html
+
+; TODO: I want to add high speed I/O to Fenders, for use with the SIO2PC
+; and AtariSIO (or APE).
+
diff --git a/fenders.rst b/fenders.rst
new file mode 100644
index 0000000..9c4f1c7
--- /dev/null
+++ b/fenders.rst
@@ -0,0 +1,239 @@
+.. RST source for fenders(1) man page. Convert with:
+.. rst2man.py fenders.rst > fenders.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+=======
+fenders
+=======
+
+---------------------------------------------------------------
+Install Fenders 3-sector loader in boot sectors of an ATR image
+---------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+*fenders* [*-hrcsiv*] [-t *title*] *infile.atr* [*outfile.atr*]
+
+DESCRIPTION
+===========
+
+**fenders** replaces the boot sectors of an ATR image with a menu-driven
+"game dos" binary loader.
+
+When the disk is booted, a disk directory menu is displayed on the
+screen. To load a binary (XEX/COM/BIN/etc) file, press the letter
+shown for that file. Other file types may not be loaded (and will cause
+the Atari to crash).
+
+The installed bootloader overwrites sectors 1-3 of the image
+(and sector 720, for double-density images). This is the same code
+installed by the Atari-based **Fenders 3-sector** loader installer
+utility.
+
+
+OPTIONS
+=======
+
+-h
+ Print this help message.
+
+-r
+ DON'T reboot (coldstart) the Atari if Reset is pressed. The default is to coldstart.
+
+-c
+ Rotate colors during load. May cause problems with some games.
+
+-s
+ Screen off after load. May cause problems with some games, but
+ may fix graphics corruption for other games. YMMV.
+
+-i
+ In-place update. The input file is renamed to end in **~** (tilde),
+ and the output is written to the original filename. May not be
+ used when reading from standard input.
+
+-v
+ Set inverse video bit in title. This causes the title text to
+ appear in red and/or blue on the Atari, instead of the default
+ orange and green colors.
+
+-t
+ Set the menu title. May up to 20 characters; default is *atari
+ arcade*. Will be truncated to 20 characters if a longer title is
+ given. See MENU TITLE, below. Note that you'll have to quote the
+ title if it contains spaces or other characters that have meaning to your shell.
+
+infile
+ The ATR image to read from. It must be either single-density
+ and at least 368 sectors long, or double-density and at least
+ 720 sectors long. You may use **-** for *infile* to read from
+ standard input (unless **-i** is used).
+
+outfile
+ The ATR image to create, which will contain all the files
+ from the input image, plus the Fenders boot loader. You may
+ omit *outfile* or use **-** to write to standard output. *outfile* is
+ ignored when the **-i** option is used.
+
+If both *infile* and *outfile* are omitted, the default is to read from
+standard input and write to standard output.
+
+NOTES
+=====
+
+**fenders** will abort, if asked to write to standard output when standard
+output is a terminal. This is to avoid spewing binary garbage to your
+terminal.
+
+The **-r**, **-c**, **-s**, and **-t** options actually modify the **fenders** 6502 object
+code before writing it to the ATR image.
+
+The Atari **fenders** installer's default is to not coldstart, which
+tends to cause the Atari to lock up when Reset is pressed. The
+author believes that rebooting the Atari is more useful than locking
+it up, so **fenders** causes the coldstart by default.
+
+The Atari **fenders** installer contains a bug: the meanings of the "Rotate
+color" and "Screen off after load" options are reversed. **fenders** does
+not duplicate this bug.
+
+The disk density doesn't have to be specified, because **fenders** reads it
+from the ATR header. The loader's 6502 object code is different, for
+single- and double-density disks.
+
+The double-density version of the loader isn't actually a 3-sector
+loader. The first 384 bytes of its code are stored in the 3 boot sectors, and the last 256 bytes of code are actually read from sector 720.
+When installing the boot loader on a double-density disk, sector 720 is
+**overwritten**. Any data that may have been there is lost. On most disks,
+sector 720 is either unusable by DOS, or not used unless the disk is
+completely full, so this is less of a problem than you might think.
+
+**fenders** and the **fenders** boot-loader code will work with DOS
+2.5 "enhanced density" formatted floppies, but only partially: files
+that use sectors above 720 will not appear in the menu (these are the
+same files that DOS 2.5 lists with <> around the filename).
+
+**fenders** only works on Atari DOS 2.x and compatible (MyDOS, DOS XL, et
+al) single-sided disk images, either single-density (720 sectors, 90K),
+double-density (720 sectors, 180K), or "1050 enhanced" density (1040
+sectors, 130K, although 1050 enhanced density images must be in DOS 2.5
+format, *not* MyDOS, and see **LIMITATIONS** below). Other non-standard DOS
+formats such as SpartaDOS or Atari DOS 3.0 and 4.0 are not supported.
+Atari DOS 1.0 may or may not work (untested).
+
+The **fenders** boot loader source code distributed with **fenders**
+is not the original source code (which has never been
+released). The author of **fenders** spent a couple of days with a
+disassembly of the object code and reverse engineered (labelled,
+commented) it so that humans can read it, provided they are humans who
+speak 6502 assembly. The *fenders.dasm* and *fendersdbl.dasm* files
+can be assembled with the DASM cross assembler. The resulting object
+code is identical with the original **fenders** object code (which
+the author read from a disk image in the first place).
+
+Contrary to what you may have read, the Fenders boot loader code
+doesn't have to be rewritten to a disk if you add/delete files. It
+reads the disk directory, and can cope with changes just fine. Even a
+heavily fragmented filesystem won't cause any problems.
+
+MENU TITLE
+==========
+
+The menu title (set with **-t**) is stored in sector 3 for a
+single-density disk or sector 720 of a double-density disk, in Atari
+internal screen codes (which are not the same as either ASCII or
+ATASCII). fenders converts the title into internal codes, so the user
+doesn't have to worry about this.
+
+The menu title is displayed in "GRAPHICS 2" mode, which can only
+display 64 different characters, including uppercase, numbers, and
+(most) punctuation, but NOT including lowercase or inverse video.
+
+However, lowercase letters are displayed as different-colored
+uppercase letters, and inverse characters are also displayed with
+different colors. The boot loader uses the default Atari
+colors, as set by the Atari OS.
+
+The title is limited to 20 characters because that's the width of a
+GRAPHICS 2 line of text on the Atari.
+
+The character set used by GRAPHICS 2 consists of:
+
+- The space character.
+
+- The letters **A-Z**.
+
+- The numbers **0-9**.
+
+- Punctuation: **!** **"** **#** **$** **%** **&** **'** **(** **)** **\*** **+** **,** **-** **.** **/** **:** **;** **<** **=** **>** **?** **@** **[** **\\** **]** **^** **_**
+
+Uppercase letters, numbers, and punctuation are displayed in orange (or
+blue, if **-v** is used).
+
+Lowercase letters are displayed in green (or red, with **-v**).
+
+The characters ` { | } ~ are displayed as green (or red) versions of
+@ [ \ ] ^, respectively.
+
+Currently, it's not possible to mix normal (orange/green) characters
+with inverse (blue/red) with fenders, unless your terminal provides a
+way for you to enter ASCII characters with their high bits set. Some
+versions of xterm(1x) allow this with the Alt or Meta key.
+
+LIMITATIONS
+===========
+
+**fenders** should warn if the disk image contains more than 20 files,
+since they won't all be displayed in the menu.
+
+Double-density images less than 720 sectors long are not handled,
+although they could be with a little more work.
+
+For double-density disks, the VTOC and sector link bytes should
+be checked to see if sector 720 is in use, rather than just blindly
+overwriting it. On a single-density DOS 2.0S disk, sector 720 is
+marked "in use" in the VTOC when the disk is formatted, but will never
+be used for file storage.
+
+No checking for non-standard formats (SpartaDOS, DOS 3, MyDOS with
+subdirectories, dedicated bootdisks, etc) is done.
+
+If an "enhanced" 1050 density disk image has the bootloader installed,
+it won't display the files using sectors above 720 (the ones which
+would appear as *<filename.ext>* in the DOS 2.5 directory).
+
+Actually, the above limitations are a direct result of the fact that
+**fenders** deals with the disk image at the "sector" level, and contains
+no code that understands the files, directory, or VTOC on the image.
+The original Atari-based Fenders installer shares the same limitations,
+so the author doesn't really consider them to be bugs, although they
+may be addressed in a future version of **fenders**.
+
+Note that the bootloader only displays up to 20 files on the screen.
+This shouldn't be a real problem (how many games can you fit on a 180K
+floppy?), but no warning or error is given if there are too many files.
+
+The bootloader only supports standard SIO disk speed (19200bps). The
+only way to make it support high-speed SIO is to use the APE Warp OS
+(or some other high-speed patched OS) on the Atari.
+
+BUGS
+====
+
+There should be an option to delete DOS.SYS and DUP.SYS from the image,
+but there isn't. The original Fenders installer has this option.
+
+When used with an image whose size according to the ATR header
+doesn't match the actual image file size, **fenders** may produce
+a broken or zero-length output file. If in doubt, use **atrcheck** to
+validate the image before use.
+
+There should be a way to install an arbitrary 3-sector (384-byte)
+binary file in the boot sectors of an image. This can be done with
+**dd**, of course, but its syntax is difficult to remember, and typos are
+prone to wipe out the file you're working with.
+
+.. include:: manftr.rst
diff --git a/fenders_bin.c b/fenders_bin.c
new file mode 100644
index 0000000..8256bb8
--- /dev/null
+++ b/fenders_bin.c
@@ -0,0 +1,54 @@
+/* C source created by blob2c from input file fenders.bin */
+
+unsigned char fenders_bin[] = {
+ /* 0 */ 0x00,0x03,0x00,0x07,0x77,0xe4,0xa0,0x00, /* ....w... */
+ /* 8 */ 0x8c,0x44,0x02,0xc8,0x84,0x09,0x8c,0x01, /* .D...... */
+ /* 16 */ 0x03,0xce,0x06,0x03,0xa9,0x4a,0x8d,0x30, /* .....J.0 */
+ /* 24 */ 0x02,0xa9,0x08,0x8d,0x31,0x02,0xa9,0x7c, /* ....1..| */
+ /* 32 */ 0x85,0xb2,0xa9,0x08,0x85,0xb3,0xa9,0x69, /* .......i */
+ /* 40 */ 0x85,0xb1,0xa5,0xb1,0x8d,0x0a,0x03,0xa9, /* ........ */
+ /* 48 */ 0x01,0x8d,0x0b,0x03,0x20,0x0b,0x08,0xe6, /* .... ... */
+ /* 56 */ 0xb1,0xca,0xbd,0x00,0x0b,0xf0,0x4f,0x30, /* ......O0 */
+ /* 64 */ 0x41,0x29,0x01,0xd0,0x3d,0xe6,0xb0,0xa4, /* A)..=... */
+ /* 72 */ 0xb0,0xbd,0x03,0x0b,0x99,0xc0,0x00,0xbd, /* ........ */
+ /* 80 */ 0x04,0x0b,0x99,0xe0,0x00,0x98,0x18,0x69, /* .......i */
+ /* 88 */ 0xa0,0xa0,0x03,0x91,0xb2,0xc8,0xa9,0x8e, /* ........ */
+ /* 96 */ 0x91,0xb2,0xc8,0xc8,0xbd,0x05,0x0b,0xe8, /* ........ */
+ /* 104 */ 0x38,0xe9,0x20,0x91,0xb2,0xc0,0x10,0xd0, /* 8. ..... */
+ /* 112 */ 0xf2,0x18,0xa5,0xb2,0x69,0x14,0x85,0xb2, /* ....i... */
+ /* 120 */ 0x90,0x02,0xe6,0xb3,0xa5,0xb0,0xc9,0x14, /* ........ */
+ /* 128 */ 0xf0,0x0c,0x8a,0x29,0xf0,0x18,0x69,0x10, /* ...)..i. */
+ /* 136 */ 0xaa,0x0a,0x90,0xae,0xb0,0x9c,0x20,0x41, /* ...... A */
+ /* 144 */ 0x08,0x38,0xe9,0x40,0xc5,0xb0,0xf0,0x02, /* .8.@.... */
+ /* 152 */ 0xb0,0xf4,0xaa,0xfe,0x50,0x08,0xb5,0xc0, /* ....P... */
+ /* 160 */ 0x8d,0x0a,0x03,0xb5,0xe0,0x8d,0x0b,0x03, /* ........ */
+ /* 168 */ 0x20,0x15,0x08,0xca,0x20,0xf9,0x07,0x85, /* ... ... */
+ /* 176 */ 0x43,0x20,0xf9,0x07,0x85,0x44,0x25,0x43, /* C ...D%C */
+ /* 184 */ 0xc9,0xff,0xf0,0xf0,0x20,0xf9,0x07,0x85, /* .... ... */
+ /* 192 */ 0x45,0x20,0xf9,0x07,0x85,0x46,0x20,0xf9, /* E ...F . */
+ /* 200 */ 0x07,0x91,0x43,0xe6,0x43,0xd0,0x04,0xe6, /* ..C.C... */
+ /* 208 */ 0x44,0xf0,0x0a,0xa5,0x45,0xc5,0x43,0xa5, /* D...E.C. */
+ /* 216 */ 0x46,0xe5,0x44,0xb0,0xe9,0xad,0xe2,0x02, /* F.D..... */
+ /* 224 */ 0x0d,0xe3,0x02,0xf0,0xc7,0x86,0x49,0x20, /* ......I */
+ /* 232 */ 0xf6,0x07,0xa6,0x49,0xa0,0x00,0x8c,0xe2, /* ...I.... */
+ /* 240 */ 0x02,0x8c,0xe3,0x02,0xf0,0xb6,0x6c,0xe2, /* ......l. */
+ /* 248 */ 0x02,0xe0,0x7d,0xd0,0x3f,0xad,0x0a,0x03, /* ..}.?... */
+ /* 256 */ 0x0d,0x0b,0x03,0xd0,0x10,0xad,0x2f,0x02, /* ....../. */
+ /* 264 */ 0x6c,0xe0,0x02,0xa9,0x0b,0x8d,0x05,0x03, /* l....... */
+ /* 272 */ 0xa9,0x80,0x8d,0x08,0x03,0xa9,0x40,0x8d, /* ......@. */
+ /* 280 */ 0x03,0x03,0x20,0x59,0xe4,0x30,0xf6,0xad, /* .. Y.0.. */
+ /* 288 */ 0x7d,0x0b,0x29,0x03,0x8d,0x0b,0x03,0xad, /* }.)..... */
+ /* 296 */ 0x7e,0x0b,0x8d,0x0a,0x03,0xad,0x17,0xd0, /* ~....... */
+ /* 304 */ 0xad,0x7f,0x0b,0x29,0x7f,0x8d,0xfa,0x07, /* ...).... */
+ /* 312 */ 0xa0,0x00,0xa2,0x00,0xbd,0x00,0x0b,0xe8, /* ........ */
+ /* 320 */ 0x60,0xad,0x25,0xe4,0x48,0xad,0x24,0xe4, /* `.%.H.$. */
+ /* 328 */ 0x48,0x60,0x70,0x70,0x70,0x47,0x68,0x08, /* H`pppGh. */
+ /* 336 */ 0x70,0x06,0x06,0x06,0x06,0x06,0x06,0x06, /* p....... */
+ /* 344 */ 0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06, /* ........ */
+ /* 352 */ 0x06,0x06,0x06,0x06,0x06,0x41,0x4a,0x08, /* .....AJ. */
+ /* 360 */ 0x00,0x00,0x00,0x00,0x61,0x74,0x61,0x72, /* ....atar */
+ /* 368 */ 0x69,0x00,0x61,0x72,0x63,0x61,0x64,0x65, /* i.arcade */
+ /* 376 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 /* ........ */
+}; /* fenders_bin */
+
+int fenders_bin_len = 384;
diff --git a/fenders_bin.h b/fenders_bin.h
new file mode 100644
index 0000000..3482b5b
--- /dev/null
+++ b/fenders_bin.h
@@ -0,0 +1,9 @@
+/* C header created by blob2c from input file fenders.bin */
+
+#ifndef fenders_bin_H
+#define fenders_bin_H
+
+extern unsigned char fenders_bin[];
+extern int fenders_bin_len;
+
+#endif /* fenders_bin_H */
diff --git a/fenders_offsets.h b/fenders_offsets.h
new file mode 100644
index 0000000..bf3431b
--- /dev/null
+++ b/fenders_offsets.h
@@ -0,0 +1,5 @@
+#define OFFSET_COLDST_1 0x0007
+#define OFFSET_COLDST_2 0x000b
+#define OFFSET_ROTCOLOR 0x012d
+#define OFFSET_SCREENOFF 0x0105
+#define OFFSET_TITLE 0x0168
diff --git a/fenders_offsets.pl b/fenders_offsets.pl
new file mode 100644
index 0000000..8ecce64
--- /dev/null
+++ b/fenders_offsets.pl
@@ -0,0 +1,6 @@
+#!/usr/bin/perl -w
+
+while(<>) {
+ chomp;
+ print "#define $1 0x$2\n" while(/(OFFSET_\w+):?\s+([0-9a-f]+)/g);
+}
diff --git a/fendersdbl.bin b/fendersdbl.bin
new file mode 100644
index 0000000..94c4545
--- /dev/null
+++ b/fendersdbl.bin
Binary files differ
diff --git a/fendersdbl.dasm b/fendersdbl.dasm
new file mode 100644
index 0000000..cb6ea3f
--- /dev/null
+++ b/fendersdbl.dasm
@@ -0,0 +1,411 @@
+; Fenders "3-sector" loader disassembly, 20070526 bkw
+; Double-density version
+
+; Note: the double-density loader doesn't actually fit in 3 sectors.
+; It uses sectors 1-3 and 720.
+; First 3 sectors of a DD disk are still only 128 bytes/sector.
+
+; At boot, the OS boot code loads the first 3 sectors and jumps to
+; the loaded code... which then loads sector 720 (a proper 256-byte DD
+; sector), which contains the rest of the code.
+
+; I haven't done a very thorough job of reverse-engineering the DD
+; version of the loader. Its structure is similar to that of the SD
+; loader (fenders.dasm).
+
+ processor 6502
+
+;;; Equates:
+
+;; OS ROM entry points
+SIOV .equ $e459
+COLDSV .equ $e477
+KEYBDV .equ $e420 ; K: handler device table
+keyb_get_lo .equ KEYBDV+4 ; pointer to "get byte" routine, minus 1
+keyb_get_hi .equ KEYBDV+5 ; (used by get_key)
+
+;; OS zero page
+BOOTQ .equ $09
+SAVMSC .equ $58
+ZROFRE .equ $80
+
+;; OS and FMS page 2 RAM variables
+COLDST .equ $0244
+SDMCTL .equ $022f
+SDLSTL .equ $0230
+SDLSTH .equ $0231
+RUNAD .equ $02e0
+INITAD .equ $02e2
+
+;; DCB, used for sector I/O parameters by SIOV (called by read_sector)
+DDEVIC .equ $0300
+DUNIT .equ $0301
+DCOMND .equ $0302
+DSTATS .equ $0303
+DBUFLO .equ $0304
+DBUFHI .equ $0305
+DTIMLO .equ $0306
+DBYTLO .equ $0308
+DBYTHI .equ $0309
+DAUX1 .equ $030a
+DAUX2 .equ $030b
+
+;; Hardware registers
+COLPF1 .equ $d017
+VCOUNT .equ $d40b
+
+;; Local variables (zero page)
+end_address .equ $45
+save_pos .equ $49
+menu_counter .equ $b0
+dir_sector_lo .equ $b1
+tmp_dlistl .equ $b2
+tmp_dlisth .equ $b3
+menu_ptr_lo .equ $b4
+menu_ptr_hi .equ $b5
+dest_ptr .equ $43
+start_sector_lo_tbl .equ $c0
+start_sector_hi_tbl .equ $e0
+
+;; Local variables (non zero page)
+buffer .equ $0b10
+sector_link_hi .equ $0c0d ; buffer + $fd
+sector_link_lo .equ $0c0e ; buffer + $fe
+sector_byte_count .equ $0c0f ; buffer + $ff
+
+;;; Bootable disk image starts here:
+ .org $0700
+
+;;; Standard Atari boot disk header (6 bytes)
+boot_record:
+ .byte $00 ; ignored
+ .byte $03 ; number of sectors to read
+ .word boot_record ; load address
+ .word COLDSV ; init address, don't think this gets used
+
+;;; Actual code starts here:
+boot_continuation:
+OFFSET_COLDST_1_DD .equ *-boot_record+1
+OFFSET_COLDST_2_DD .equ *-boot_record+5
+ LDY #$00 ; 0 .
+ STY COLDST
+ INY
+ STY BOOTQ
+ STY DUNIT
+ DEC DTIMLO
+set_dbl_density:
+ LDA #$4e ; 78 N
+ STA DCOMND
+ LDA #$40 ; 64 @
+ STA DSTATS
+ LDA #$0c ; 12 .
+ STA DBYTLO
+ LDA #$00 ; 0 .
+ STA DBYTHI
+ LDA #<buffer
+ STA DBUFLO
+ LDA #>buffer
+ STA DBUFHI
+ JSR SIOV
+ BMI set_dbl_density
+ LDA #$04 ; 4 .
+ STA buffer+5
+ LDA #$01 ; 1 .
+ STA buffer+6
+ LDA #$00 ; 0 .
+ STA buffer+7
+ LDA #$4f ; 79 O
+ STA DCOMND
+ LDA #$80 ; 128 .
+ STA DSTATS
+ JSR SIOV
+ BMI set_dbl_density
+
+; The bootloader code is 640 bytes long. First 384 bytes were loaded
+; from the 3 boot sectors already; the rest lives in sector 720, which
+; we have to load before running it:
+read_sec_720:
+ LDA #$52 ; 82 R
+ STA DCOMND
+ LDA #$40 ; 64 @
+ STA DSTATS
+ LDA #$80 ; 128 .
+ STA DBUFLO
+ LDA #$08 ; 8 .
+ STA DBUFHI
+ LDA #$00 ; 0 .
+ STA DBYTLO
+ LDA #$01 ; 1 .
+ STA DBYTHI
+ LDA #$d0 ; 208 .
+ STA DAUX1
+ LDA #$02 ; 2 .
+ STA DAUX2
+ JSR SIOV
+ BMI read_sec_720
+
+ ; setup display list
+ LDA SDLSTL
+ STA tmp_dlistl
+ LDA SDLSTH
+ STA tmp_dlisth
+ LDA #$00 ; 0 .
+ STA SDMCTL
+ LDA #<display_list ; 58 :
+ STA SDLSTL
+ LDA #>display_list ; 9 .
+ STA SDLSTH
+
+ ; init menu
+ LDA #$6c ; 108 l
+ STA menu_ptr_lo
+ LDA #$09 ; 9 .
+ STA menu_ptr_hi
+ LDA #$69 ; 105 i
+ STA dir_sector_lo
+
+read_dir_sector:
+ LDA dir_sector_lo
+ STA DAUX1
+ LDA #$01 ; 1 .
+ STA DAUX2
+ JSR read_sector
+ INC dir_sector_lo
+ DEX
+
+do_dirent:
+ LDA buffer,X
+ BEQ dir_done
+ BMI next_dirent
+ AND #$01 ; 1 .
+ BNE next_dirent
+ INC menu_counter
+ LDY menu_counter
+ LDA buffer+3,X
+ STA start_sector_lo_tbl,Y
+ LDA buffer+4,X
+ STA start_sector_hi_tbl,Y
+ TYA
+ CLC
+ ADC #$a0 ; 160 .
+ LDY #$03 ; 3 .
+ STA (menu_ptr_lo),Y
+ INY
+ LDA #$8e ; 142 .
+ STA (menu_ptr_lo),Y
+ INY
+next_char:
+ INY
+ LDA buffer+5,X
+ INX
+ SEC
+ SBC #$20 ; 32
+ STA (menu_ptr_lo),Y
+ CPY #$10 ; 16 .
+ BNE next_char
+ CLC
+ LDA menu_ptr_lo
+ ADC #$14 ; 20 .
+ STA menu_ptr_lo
+ BCC skip_ptr_hi
+ INC menu_ptr_hi
+skip_ptr_hi:
+ LDA menu_counter
+ CMP #$14 ; 20 .
+ BEQ dir_done
+next_dirent:
+ TXA
+ AND #$f0 ; 240 .
+ CLC
+ ADC #$10 ; 16 .
+ TAX
+ ASL
+ BCC do_dirent
+ BCS read_dir_sector
+dir_done:
+ LDA #$22 ; 34 "
+ STA SDMCTL
+wait_vcount_0:
+ LDA VCOUNT
+ BNE wait_vcount_0
+wait_for_input:
+ JSR get_key
+ SEC
+ SBC #$40 ; 64 @
+ CMP menu_counter
+ BEQ load_file
+ BCS wait_for_input
+load_file:
+ TAX
+ LDA start_sector_lo_tbl,X
+ STA DAUX1
+ LDA start_sector_hi_tbl,X
+ STA DAUX2
+ LDA #$68 ; 104 h
+ STA menu_ptr_lo
+ LDA #$09 ; 9 .
+ STA menu_ptr_hi
+L0834:
+ DEX
+ BEQ print_loading_msg
+ CLC
+ LDA menu_ptr_lo
+ ADC #$14 ; 20 .
+ STA menu_ptr_lo
+ BCC L0834
+ INC menu_ptr_hi
+ BNE L0834
+print_loading_msg:
+ LDY #$00 ; 0 .
+next_msg_byte:
+ LDA loading_msg,Y
+ STA (SAVMSC),Y
+ INY
+ CPY #$09 ; 9 .
+ BNE next_msg_byte
+print_filename:
+ LDA (menu_ptr_lo),Y
+ STA (SAVMSC),Y
+ INY
+ CPY #$15 ; 21 .
+ BNE print_filename
+ LDA #$00 ; 0 .
+ STA SDMCTL
+ LDA tmp_dlistl
+ STA SDLSTL
+ LDA tmp_dlisth
+ STA SDLSTH
+ LDA #$22 ; 34 "
+ STA SDMCTL
+wait_vcount_again:
+ LDA VCOUNT
+ BNE wait_vcount_again
+ LDY #$00 ; 0 .
+ TYA
+clear_zp:
+ STA ZROFRE,Y
+ INY
+ BPL clear_zp
+
+; the "JSR try_read" below MUST be located at $087f.
+; The JSR opcode is the last byte loaded in the 3-sector boot loader,
+; and its operand is the first 2 bytes loaded from sector 720!
+ .if *<>$087f
+ .echo "Code offsets have changed, fix me (", *, "should be $087f)"
+ .err
+ .endif
+ JSR try_read ; cut here!
+
+ DEX
+
+read_segment:
+ JSR get_next_byte
+ STA dest_ptr
+ JSR get_next_byte
+ STA dest_ptr+1
+ AND dest_ptr
+ CMP #$ff ; 255 .
+ BEQ read_segment
+ JSR get_next_byte
+ STA end_address
+ JSR get_next_byte
+ STA end_address+1
+load_byte:
+ JSR get_next_byte
+ STA (dest_ptr),Y
+ INC dest_ptr
+ BNE check_seg_done
+ INC dest_ptr+1
+ BEQ check_for_init
+check_seg_done:
+ LDA end_address
+ CMP dest_ptr
+ LDA end_address+1
+ SBC dest_ptr+1
+ BCS load_byte
+check_for_init:
+ LDA INITAD
+ ORA INITAD+1
+ BEQ read_segment
+ STX save_pos
+ JSR do_init
+ LDX save_pos
+ LDY #$00 ; 0 .
+ STY INITAD
+ STY INITAD+1
+ BEQ read_segment
+
+do_init:
+ JMP (INITAD)
+
+get_next_byte:
+ ; self-modifying code changes immediate CPX operand
+ CPX #$fd ; 253 .
+ BNE return_next_byte
+ LDA DAUX1
+ ORA DAUX2
+ BNE try_read
+OFFSET_SCREENOFF_DD .equ *-boot_record
+ LDA SDMCTL
+ JMP (RUNAD)
+
+read_sector:
+ LDA #$31 ; 49 1
+ STA DDEVIC
+ LDA #$52 ; 82 R
+ STA DCOMND
+ LDA #<buffer ; 16 .
+ STA DBUFLO
+ LDA #>buffer ; 11 .
+ STA DBUFHI
+ LDA #$00 ; 0 .
+ STA DBYTLO
+ LDA #$01 ; 1 .
+ STA DBYTHI
+
+try_read:
+ LDA #$40 ; 64 @
+ STA DSTATS
+ JSR SIOV
+ BMI try_read
+ LDA sector_link_hi
+ AND #$03 ; 3 .
+ STA DAUX2
+ LDA sector_link_lo
+ STA DAUX1
+OFFSET_ROTCOLOR_DD .equ *-boot_record
+ LDA COLPF1
+ LDA sector_byte_count
+ STA get_next_byte+1
+ LDY #$00 ; 0 .
+ LDX #$00 ; 0 .
+return_next_byte:
+ LDA buffer,X
+ INX
+ RTS
+
+get_key:
+ LDA keyb_get_hi
+ PHA
+ LDA keyb_get_lo
+ PHA
+ RTS
+
+loading_msg:
+ .byte $00,$00,$2c,$6f,$61,$64,$69,$6e ; "..,oadin"
+ .byte $67,$00,$00 ; "g.."
+display_list:
+ .byte $70,$70,$70,$47 ; "pppG"
+ .byte <screen,>screen
+ .byte $70,$06,$06,$06,$06,$06,$06 ; "p......"
+ .byte $06,$06,$06,$06,$06,$06,$06,$06 ; "........"
+ .byte $06,$06,$06,$06,$06,$06,$41 ; "......A"
+ .byte <display_list,>display_list
+OFFSET_TITLE_DD .equ *-boot_record
+screen:
+ .byte $00,$00,$00,$00,$61,$74,$61 ; ".....ata"
+ .byte $72,$69,$00,$61,$72,$63,$61,$64 ; "ri.arcad"
+ .byte $65,$00,$00,$00,$00,$00,$00,$00 ; "e......."
+ .byte $00,$00,$00,$00,$00,$00,$00,$00 ; "........"
+ .byte $00,$00,$00,$00,$00,$00,$00,$00 ; "........"
+ .byte $00 ; "."
diff --git a/fendersdbl_bin.c b/fendersdbl_bin.c
new file mode 100644
index 0000000..e3fc5cd
--- /dev/null
+++ b/fendersdbl_bin.c
@@ -0,0 +1,86 @@
+/* C source created by blob2c from input file fendersdbl.bin */
+
+unsigned char fendersdbl_bin[] = {
+ /* 0 */ 0x00,0x03,0x00,0x07,0x77,0xe4,0xa0,0x00, /* ....w... */
+ /* 8 */ 0x8c,0x44,0x02,0xc8,0x84,0x09,0x8c,0x01, /* .D...... */
+ /* 16 */ 0x03,0xce,0x06,0x03,0xa9,0x4e,0x8d,0x02, /* .....N.. */
+ /* 24 */ 0x03,0xa9,0x40,0x8d,0x03,0x03,0xa9,0x0c, /* ..@..... */
+ /* 32 */ 0x8d,0x08,0x03,0xa9,0x00,0x8d,0x09,0x03, /* ........ */
+ /* 40 */ 0xa9,0x10,0x8d,0x04,0x03,0xa9,0x0b,0x8d, /* ........ */
+ /* 48 */ 0x05,0x03,0x20,0x59,0xe4,0x30,0xdd,0xa9, /* .. Y.0.. */
+ /* 56 */ 0x04,0x8d,0x15,0x0b,0xa9,0x01,0x8d,0x16, /* ........ */
+ /* 64 */ 0x0b,0xa9,0x00,0x8d,0x17,0x0b,0xa9,0x4f, /* .......O */
+ /* 72 */ 0x8d,0x02,0x03,0xa9,0x80,0x8d,0x03,0x03, /* ........ */
+ /* 80 */ 0x20,0x59,0xe4,0x30,0xbf,0xa9,0x52,0x8d, /* Y.0..R. */
+ /* 88 */ 0x02,0x03,0xa9,0x40,0x8d,0x03,0x03,0xa9, /* ...@.... */
+ /* 96 */ 0x80,0x8d,0x04,0x03,0xa9,0x08,0x8d,0x05, /* ........ */
+ /* 104 */ 0x03,0xa9,0x00,0x8d,0x08,0x03,0xa9,0x01, /* ........ */
+ /* 112 */ 0x8d,0x09,0x03,0xa9,0xd0,0x8d,0x0a,0x03, /* ........ */
+ /* 120 */ 0xa9,0x02,0x8d,0x0b,0x03,0x20,0x59,0xe4, /* ..... Y. */
+ /* 128 */ 0x30,0xd3,0xad,0x30,0x02,0x85,0xb2,0xad, /* 0..0.... */
+ /* 136 */ 0x31,0x02,0x85,0xb3,0xa9,0x00,0x8d,0x2f, /* 1....../ */
+ /* 144 */ 0x02,0xa9,0x3a,0x8d,0x30,0x02,0xa9,0x09, /* ..:.0... */
+ /* 152 */ 0x8d,0x31,0x02,0xa9,0x6c,0x85,0xb4,0xa9, /* .1..l... */
+ /* 160 */ 0x09,0x85,0xb5,0xa9,0x69,0x85,0xb1,0xa5, /* ....i... */
+ /* 168 */ 0xb1,0x8d,0x0a,0x03,0xa9,0x01,0x8d,0x0b, /* ........ */
+ /* 176 */ 0x03,0x20,0xde,0x08,0xe6,0xb1,0xca,0xbd, /* . ...... */
+ /* 184 */ 0x10,0x0b,0xf0,0x4f,0x30,0x41,0x29,0x01, /* ...O0A). */
+ /* 192 */ 0xd0,0x3d,0xe6,0xb0,0xa4,0xb0,0xbd,0x13, /* .=...... */
+ /* 200 */ 0x0b,0x99,0xc0,0x00,0xbd,0x14,0x0b,0x99, /* ........ */
+ /* 208 */ 0xe0,0x00,0x98,0x18,0x69,0xa0,0xa0,0x03, /* ....i... */
+ /* 216 */ 0x91,0xb4,0xc8,0xa9,0x8e,0x91,0xb4,0xc8, /* ........ */
+ /* 224 */ 0xc8,0xbd,0x15,0x0b,0xe8,0x38,0xe9,0x20, /* .....8. */
+ /* 232 */ 0x91,0xb4,0xc0,0x10,0xd0,0xf2,0x18,0xa5, /* ........ */
+ /* 240 */ 0xb4,0x69,0x14,0x85,0xb4,0x90,0x02,0xe6, /* .i...... */
+ /* 248 */ 0xb5,0xa5,0xb0,0xc9,0x14,0xf0,0x0c,0x8a, /* ........ */
+ /* 256 */ 0x29,0xf0,0x18,0x69,0x10,0xaa,0x0a,0x90, /* )..i.... */
+ /* 264 */ 0xae,0xb0,0x9c,0xa9,0x22,0x8d,0x2f,0x02, /* ...."./. */
+ /* 272 */ 0xad,0x0b,0xd4,0xd0,0xfb,0x20,0x26,0x09, /* ..... &. */
+ /* 280 */ 0x38,0xe9,0x40,0xc5,0xb0,0xf0,0x02,0xb0, /* 8.@..... */
+ /* 288 */ 0xf4,0xaa,0xb5,0xc0,0x8d,0x0a,0x03,0xb5, /* ........ */
+ /* 296 */ 0xe0,0x8d,0x0b,0x03,0xa9,0x68,0x85,0xb4, /* .....h.. */
+ /* 304 */ 0xa9,0x09,0x85,0xb5,0xca,0xf0,0x0d,0x18, /* ........ */
+ /* 312 */ 0xa5,0xb4,0x69,0x14,0x85,0xb4,0x90,0xf4, /* ..i..... */
+ /* 320 */ 0xe6,0xb5,0xd0,0xf0,0xa0,0x00,0xb9,0x2f, /* ......./ */
+ /* 328 */ 0x09,0x91,0x58,0xc8,0xc0,0x09,0xd0,0xf6, /* ..X..... */
+ /* 336 */ 0xb1,0xb4,0x91,0x58,0xc8,0xc0,0x15,0xd0, /* ...X.... */
+ /* 344 */ 0xf7,0xa9,0x00,0x8d,0x2f,0x02,0xa5,0xb2, /* ..../... */
+ /* 352 */ 0x8d,0x30,0x02,0xa5,0xb3,0x8d,0x31,0x02, /* .0....1. */
+ /* 360 */ 0xa9,0x22,0x8d,0x2f,0x02,0xad,0x0b,0xd4, /* ."./.... */
+ /* 368 */ 0xd0,0xfb,0xa0,0x00,0x98,0x99,0x80,0x00, /* ........ */
+ /* 376 */ 0xc8,0x10,0xfa,0x20,0xfc,0x08,0xca,0x20, /* ... ... */
+ /* 384 */ 0xcc,0x08,0x85,0x43,0x20,0xcc,0x08,0x85, /* ...C ... */
+ /* 392 */ 0x44,0x25,0x43,0xc9,0xff,0xf0,0xf0,0x20, /* D%C.... */
+ /* 400 */ 0xcc,0x08,0x85,0x45,0x20,0xcc,0x08,0x85, /* ...E ... */
+ /* 408 */ 0x46,0x20,0xcc,0x08,0x91,0x43,0xe6,0x43, /* F ...C.C */
+ /* 416 */ 0xd0,0x04,0xe6,0x44,0xf0,0x0a,0xa5,0x45, /* ...D...E */
+ /* 424 */ 0xc5,0x43,0xa5,0x46,0xe5,0x44,0xb0,0xe9, /* .C.F.D.. */
+ /* 432 */ 0xad,0xe2,0x02,0x0d,0xe3,0x02,0xf0,0xc7, /* ........ */
+ /* 440 */ 0x86,0x49,0x20,0xc9,0x08,0xa6,0x49,0xa0, /* .I ...I. */
+ /* 448 */ 0x00,0x8c,0xe2,0x02,0x8c,0xe3,0x02,0xf0, /* ........ */
+ /* 456 */ 0xb6,0x6c,0xe2,0x02,0xe0,0xfd,0xd0,0x51, /* .l.....Q */
+ /* 464 */ 0xad,0x0a,0x03,0x0d,0x0b,0x03,0xd0,0x24, /* .......$ */
+ /* 472 */ 0xad,0x2f,0x02,0x6c,0xe0,0x02,0xa9,0x31, /* ./.l...1 */
+ /* 480 */ 0x8d,0x00,0x03,0xa9,0x52,0x8d,0x02,0x03, /* ....R... */
+ /* 488 */ 0xa9,0x10,0x8d,0x04,0x03,0xa9,0x0b,0x8d, /* ........ */
+ /* 496 */ 0x05,0x03,0xa9,0x00,0x8d,0x08,0x03,0xa9, /* ........ */
+ /* 504 */ 0x01,0x8d,0x09,0x03,0xa9,0x40,0x8d,0x03, /* .....@.. */
+ /* 512 */ 0x03,0x20,0x59,0xe4,0x30,0xf6,0xad,0x0d, /* . Y.0... */
+ /* 520 */ 0x0c,0x29,0x03,0x8d,0x0b,0x03,0xad,0x0e, /* .)...... */
+ /* 528 */ 0x0c,0x8d,0x0a,0x03,0xad,0x17,0xd0,0xad, /* ........ */
+ /* 536 */ 0x0f,0x0c,0x8d,0xcd,0x08,0xa0,0x00,0xa2, /* ........ */
+ /* 544 */ 0x00,0xbd,0x10,0x0b,0xe8,0x60,0xad,0x25, /* .....`.% */
+ /* 552 */ 0xe4,0x48,0xad,0x24,0xe4,0x48,0x60,0x00, /* .H.$.H`. */
+ /* 560 */ 0x00,0x2c,0x6f,0x61,0x64,0x69,0x6e,0x67, /* .,oading */
+ /* 568 */ 0x00,0x00,0x70,0x70,0x70,0x47,0x58,0x09, /* ..pppGX. */
+ /* 576 */ 0x70,0x06,0x06,0x06,0x06,0x06,0x06,0x06, /* p....... */
+ /* 584 */ 0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06, /* ........ */
+ /* 592 */ 0x06,0x06,0x06,0x06,0x06,0x41,0x3a,0x09, /* .....A:. */
+ /* 600 */ 0x00,0x00,0x00,0x00,0x61,0x74,0x61,0x72, /* ....atar */
+ /* 608 */ 0x69,0x00,0x61,0x72,0x63,0x61,0x64,0x65, /* i.arcade */
+ /* 616 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ........ */
+ /* 624 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ........ */
+ /* 632 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 /* ........ */
+}; /* fendersdbl_bin */
+
+int fendersdbl_bin_len = 640;
diff --git a/fendersdbl_bin.h b/fendersdbl_bin.h
new file mode 100644
index 0000000..b27302c
--- /dev/null
+++ b/fendersdbl_bin.h
@@ -0,0 +1,9 @@
+/* C header created by blob2c from input file fendersdbl.bin */
+
+#ifndef fendersdbl_bin_H
+#define fendersdbl_bin_H
+
+extern unsigned char fendersdbl_bin[];
+extern int fendersdbl_bin_len;
+
+#endif /* fendersdbl_bin_H */
diff --git a/fendersdbl_offsets.h b/fendersdbl_offsets.h
new file mode 100644
index 0000000..af60bcd
--- /dev/null
+++ b/fendersdbl_offsets.h
@@ -0,0 +1,5 @@
+#define OFFSET_COLDST_1_DD 0x0007
+#define OFFSET_COLDST_2_DD 0x000b
+#define OFFSET_ROTCOLOR_DD 0x0214
+#define OFFSET_SCREENOFF_DD 0x01d8
+#define OFFSET_TITLE_DD 0x0258
diff --git a/get_address.c b/get_address.c
new file mode 100644
index 0000000..cc503ad
--- /dev/null
+++ b/get_address.c
@@ -0,0 +1,22 @@
+#include <stdio.h>
+
+int get_address(char *self, char *arg) {
+ unsigned int got;
+
+ if(sscanf(arg, "0x%x", &got) != 1)
+ if(sscanf(arg, "$%x", &got) != 1)
+ if(sscanf(arg, "%d", &got) != 1) {
+ fprintf(stderr, "Invalid address '%s'\n", arg);
+ return -1;
+ }
+
+ if(got >= 0x10000) {
+ if(self) fprintf(stderr, "%s: ", self);
+ fprintf(stderr, "Address '%s' not in range $0000-$FFFF\n", arg);
+ return -1;
+ }
+
+ return (int)got;
+}
+
+
diff --git a/get_address.h b/get_address.h
new file mode 100644
index 0000000..27488fe
--- /dev/null
+++ b/get_address.h
@@ -0,0 +1,2 @@
+int get_address(char *self, char *arg);
+
diff --git a/loadscreen.bin b/loadscreen.bin
new file mode 100644
index 0000000..7ae3e00
--- /dev/null
+++ b/loadscreen.bin
Binary files differ
diff --git a/loadscreen.dasm b/loadscreen.dasm
new file mode 100644
index 0000000..e623d09
--- /dev/null
+++ b/loadscreen.dasm
@@ -0,0 +1,153 @@
+
+; 20070524 bkw: DASM source for GR.2 "LOADING..." screen.
+; Object code is intended to be prepended to some other object
+; file, so it uses INITAD rather than RUNAD.
+
+; WARNING: if you modify this file such that the object code changes,
+; make sure you edit cart2bin.c and correct the offset for the title
+; screen text! As long as you don't change anything after the "loading"
+; label, this won't be a problem.
+
+ processor 6502
+
+ include "equates.inc"
+
+ seg.u ZP
+ org $80
+
+ if * > $ff
+ echo "Zero page vars have overrun into the stack! * =", *
+ err
+ else
+ echo *-$80, "bytes of zero page used,", $ff-*, "remain"
+ endif
+
+ seg CODE
+
+LOAD_ADDR = $6000 ; load at 24K
+origin = LOAD_ADDR-6;
+ org origin ; make room for 6-byte header
+
+; 2-byte Atari executable header:
+ byte $FF, $FF ; let DOS know it's a binary load file
+
+; start of first segment
+
+; 4-byte segment header:
+ word LOAD_ADDR ; Load address
+ word endbin-1 ; Last byte to load
+
+; Rest of segment contains code and data
+pagesize byte 0 ; cart2bin will fill this in with the size of
+ ; the cart image code, in pages.
+main:
+
+; set RAMTOP to 28K, call GR.2, store "LOADING..." in screen RAM,
+; then set RAMTOP back to original value.
+
+ lda RAMTOP
+ cmp #$C0 ; do we have 48K?
+ bcs ram_ok
+
+ ldx #0 ; no! print error message...
+ lda #9
+ sta ICCOM
+ lda #<ram_msg
+ sta ICBAL
+ lda #>ram_msg
+ sta ICBAH
+ lda #ram_msg_len
+ sta ICBLL
+ stx ICBLH
+ jsr CIOV
+hang bcs hang ; spin forever
+
+ram_msg byte "Need at least 48K to run this.", $9B
+ram_msg_len = *-ram_msg+1
+
+ram_ok
+ ;sec ; carry already set by cmp above!
+ sbc pagesize ; adjust RAMTOP to make room for the converted cart image
+ pha ; save old RAMTOP value on stack
+
+ lda #$70 ; set RAMTOP to $7000 (28K). Later on we do GRAPHICS 2,
+ sta RAMTOP ; which will cause the DL and screen RAM to go here.
+
+ ldx #$60 ; CLOSE #6 first
+ lda #12 ; Command 12=CLOSE
+ sta ICCOM,x
+ jsr CIOV ; call CIO, ignore any error
+
+ ;ldx #$60 ; set up IOCB #6 for CIO GRAPHICS command
+ lda #3 ; Command 3=OPEN
+ sta ICCOM,x
+ lda #12 ; 12=R/W access
+ sta ICAX1,x
+ lda #2 ; GR. mode 2
+ sta ICAX2,x
+ lda #<s_dev ; S: device
+ sta ICBAL,x
+ lda #>s_dev
+ sta ICBAH,x
+ lda #s_dev_len
+ sta ICBLL,x
+ lda #0
+ sta ICBLH,x
+ jsr CIOV ; call CIO
+
+ lda #5 ; POSITION 5,5
+ sta ROWCRS
+ sta COLCRS
+ lda #0
+ sta COLCRS+1
+
+ sta ICBLH,x ; PRINT #6;"LOADING...
+; ldx #$60
+ lda #9 ; Command 9 = CIO "put record"
+ sta ICCOM,x
+ lda #<loading
+ sta ICBAL,x
+ lda #>loading
+ sta ICBAH,x
+ lda #loading_len
+ sta ICBLL,x
+ jsr CIOV ; call CIO
+
+; ldx #$60 ; CLOSE #6
+ lda #12 ; Command 12=CLOSE
+ sta ICCOM,x
+ jsr CIOV ; call CIO, ignore any error
+
+ pla ; get adjusted RAMTOP value
+ sta RAMTOP
+
+ rts ; end of init routine, return control to DOS
+
+s_dev byte "S:"
+s_dev_len equ *-s_dev+1
+
+; WARNING: if you change any code/data below this line, you'll probably need
+; to change the offset in cart2bin.c as well.
+loading byte " LOADING"
+ ds 6
+title ds 20 ; cart2bin will fill in these 20 bytes with the filename
+ byte $9B ; (or title, if -t is used)
+loading_len equ *-loading+1
+
+endbin ; end of first segment
+
+ echo "Code is", *-origin+1, "bytes"
+
+; start of second segment (which loads at INITAD, to tell DOS
+; where to start running the init code loaded in segment 1)
+
+ ; 4-byte segment header:
+ word INITAD ; load address (loading into INITAD lets our code run)
+ word INITAD+1 ; Last byte to load
+
+ ; Segment data contains 2 bytes (the actual run address)
+ word main ; the 2 bytes to stuff into RUNAD
+
+ ; That's all, folks!
+ echo "Title offset: loadscreen_bin_len - ", *-title
+
diff --git a/loadscreen_bin.c b/loadscreen_bin.c
new file mode 100644
index 0000000..5c4d9bd
--- /dev/null
+++ b/loadscreen_bin.c
@@ -0,0 +1,34 @@
+/* C source created by blob2c from input file loadscreen.bin */
+
+unsigned char loadscreen_bin[] = {
+ /* 0 */ 0xff,0xff,0x00,0x60,0xd1,0x60,0x00,0xa5, /* ...`.`.. */
+ /* 8 */ 0x6a,0xc9,0xc0,0xb0,0x3d,0xa2,0x00,0xa9, /* j...=... */
+ /* 16 */ 0x09,0x8d,0x42,0x03,0xa9,0x25,0x8d,0x44, /* ..B..%.D */
+ /* 24 */ 0x03,0xa9,0x60,0x8d,0x45,0x03,0xa9,0x20, /* ..`.E.. */
+ /* 32 */ 0x8d,0x48,0x03,0x8e,0x49,0x03,0x20,0x56, /* .H..I. V */
+ /* 40 */ 0xe4,0xb0,0xfe,0x4e,0x65,0x65,0x64,0x20, /* ...Need */
+ /* 48 */ 0x61,0x74,0x20,0x6c,0x65,0x61,0x73,0x74, /* at least */
+ /* 56 */ 0x20,0x34,0x38,0x4b,0x20,0x74,0x6f,0x20, /* 48K to */
+ /* 64 */ 0x72,0x75,0x6e,0x20,0x74,0x68,0x69,0x73, /* run this */
+ /* 72 */ 0x2e,0x9b,0xed,0x00,0x60,0x48,0xa9,0x70, /* ....`H.p */
+ /* 80 */ 0x85,0x6a,0xa2,0x60,0xa9,0x0c,0x9d,0x42, /* .j.`...B */
+ /* 88 */ 0x03,0x20,0x56,0xe4,0xa9,0x03,0x9d,0x42, /* . V....B */
+ /* 96 */ 0x03,0xa9,0x0c,0x9d,0x4a,0x03,0xa9,0x02, /* ....J... */
+ /* 104 */ 0x9d,0x4b,0x03,0xa9,0xac,0x9d,0x44,0x03, /* .K....D. */
+ /* 112 */ 0xa9,0x60,0x9d,0x45,0x03,0xa9,0x03,0x9d, /* .`.E.... */
+ /* 120 */ 0x48,0x03,0xa9,0x00,0x9d,0x49,0x03,0x20, /* H....I. */
+ /* 128 */ 0x56,0xe4,0xa9,0x05,0x85,0x54,0x85,0x55, /* V....T.U */
+ /* 136 */ 0xa9,0x00,0x85,0x56,0x9d,0x49,0x03,0xa9, /* ...V.I.. */
+ /* 144 */ 0x09,0x9d,0x42,0x03,0xa9,0xae,0x9d,0x44, /* ..B....D */
+ /* 152 */ 0x03,0xa9,0x60,0x9d,0x45,0x03,0xa9,0x25, /* ..`.E..% */
+ /* 160 */ 0x9d,0x48,0x03,0x20,0x56,0xe4,0xa9,0x0c, /* .H. V... */
+ /* 168 */ 0x9d,0x42,0x03,0x20,0x56,0xe4,0x68,0x85, /* .B. V.h. */
+ /* 176 */ 0x6a,0x60,0x53,0x3a,0x20,0x20,0x4c,0x4f, /* j`S: LO */
+ /* 184 */ 0x41,0x44,0x49,0x4e,0x47,0x00,0x00,0x00, /* ADING... */
+ /* 192 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ........ */
+ /* 200 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ........ */
+ /* 208 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x9b, /* ........ */
+ /* 216 */ 0xe2,0x02,0xe3,0x02,0x01,0x60 /* .....` */
+}; /* loadscreen_bin */
+
+int loadscreen_bin_len = 222;
diff --git a/loadscreen_bin.h b/loadscreen_bin.h
new file mode 100644
index 0000000..80cf77f
--- /dev/null
+++ b/loadscreen_bin.h
@@ -0,0 +1,9 @@
+/* C header created by blob2c from input file loadscreen.bin */
+
+#ifndef loadscreen_bin_H
+#define loadscreen_bin_H
+
+extern unsigned char loadscreen_bin[];
+extern int loadscreen_bin_len;
+
+#endif /* loadscreen_bin_H */
diff --git a/manftr.rst b/manftr.rst
new file mode 100644
index 0000000..e26d352
--- /dev/null
+++ b/manftr.rst
@@ -0,0 +1,30 @@
+COPYRIGHT
+=========
+
+WTFPL. See http://www.wtfpl.net/txt/copying/ for details.
+
+AUTHOR
+======
+
+B. Watson <urchlay@slackware.uk>; Urchlay on irc.libera.chat *##atari*.
+
+SEE ALSO
+========
+
+**a8eol**\(1),
+**a8utf8**\(1),
+**atr2xfd**\(1),
+**atrsize**\(1),
+**axe**\(1),
+**blob2c**\(1),
+**cart2xex**\(1),
+**dasm2atasm**\(1),
+**fenders**\(1),
+**rom2cart**\(1),
+**unmac65**\(1),
+**xexcat**\(1),
+**xexsplit**\(1),
+**xfd2atr**\(1).
+
+Any good Atari 8-bit book: *De Re Atari*, *The Atari BASIC Reference
+Manual*, the *OS Users' Guide*, *Mapping the Atari*, etc.
diff --git a/manhdr.rst b/manhdr.rst
new file mode 100644
index 0000000..bd5b0f1
--- /dev/null
+++ b/manhdr.rst
@@ -0,0 +1,7 @@
+.. include:: ver.rst
+.. |date| date::
+
+:Manual section: 1
+:Manual group: Urchlay's Atari 8-bit Tools
+:Date: |date|
+:Version: |version|
diff --git a/rom2cart.1 b/rom2cart.1
new file mode 100644
index 0000000..d339b02
--- /dev/null
+++ b/rom2cart.1
@@ -0,0 +1,244 @@
+.\" 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 "ROM2CART" 1 "2022-08-29" "0.2.0" "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:
+.
+.\" rst2man.py rom2cart.rst > rom2cart.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fBrom2cart\fP [\fI\-chlnrv\fP] [\-m \fImachine\fP] [\-t \fItype\fP] [\-T \fIforced\-type\fP] [\-C \fIforced\-checksum\fP] [\-U \fIunused\-data\fP] [\fIinfile\fP] [\fIoutfile\fP]
+.sp
+\fBcart2rom\fP [\fIinfile\fP] [\fIoutfile\fP]
+.SH DESCRIPTION
+.sp
+\fBrom2cart\fP converts between raw ROM dumps and Atari800 \fB\&.CAR\fP
+images. Despite the name, conversion can be done in either direction.
+\fBcart2rom\fP is equivalent to \fBrom2cart \-r\fP\&.
+.sp
+Input ROM files may be either raw dumps or Atari800 .CAR format.
+Output will be a \fB\&.CAR\fP file, or (with \fB\-r\fP) a raw dump.
+.sp
+When reading a raw dump, \fBrom2cart\fP attempts to determine
+the correct image type based on the file size, machine type
+(specified via \fB\-m\fP, or guessed by looking at the ROM
+content), and (optional) user\-supplied type number or name (\fB\-t\fP).
+If \fBrom2cart\fP is unable to narrow the selection down to one
+image type, it will "guess" by choosing the lowest\-numbered type
+that matches the given parameters (unless \fB\-n\fP is given to prevent
+this behavior).
+.sp
+When writing a \fB\&.CAR\fP file, \fBrom2cart\fP will calculate the checksum
+automatically and store it in the \fBCART\fP header (unless \fB\-C\fP is
+used to force the checksum).
+.SH OPTIONS
+.sp
+Standard options:
+.INDENT 0.0
+.TP
+.B \-c
+Check ROM and print info only; do not create any output.
+.TP
+.B \-h
+Print help (usage) message and exit.
+.TP
+.B \-l
+Print a complete list of known image types and exit.
+.TP
+.BI \-m \ machine
+Sets the machine type for the image. Valid machine values are
+\fB5200\fP and \fB8bit\fP (may be abbreviated as \fB5\fP and \fB8\fP). This is used as
+a hint by the type\-guessing algorithm to narrow down the search.
+May be used in combination with \fB\-t\fP\&. Without \fB\-t\fP, only the machine
+type and file size are used to guess the image type. This option
+has no effect with \fB\-T\fP\&.
+.TP
+.B \-n
+Do not guess image type, if unable to determine it exactly from the
+file size and any supplied \fB\-t\fP/\fB\-T\fP/\fB\-m\fP arguments.
+The type\-matching is still done, and if only one type matches,
+it will be used. This option only has an effect if type\-matching
+results in two or more possible matches. This option has no effect
+with \fB\-T\fP\&.
+.TP
+.B \-r
+Output a raw image dump, rather than a \fB\&.CAR\fP image. Most useful
+when input is a \fB\&.CAR\fP image, but may be used with raw input (in
+which case, the output file will be a copy of the input, but it
+will only be created if \fBrom2cart\fP thinks the input is a valid raw
+dump). If output filename not specified, it will be derived from
+the input filename, and will end in \fI\&.rom\fP\&.
+.TP
+.BI \-t \ type
+Set the image type. type may be either a valid numeric type or
+a name. If a numeric type is given, it must be a valid type, the
+file size must be correct for the type, and if given, the machine
+type (\fB\-m\fP option) must match the type. If a name is given, it is used
+to search the list of known types; only names that match will be
+considered as possible types. This is a case\-insensitive substring
+match.
+.TP
+.B \-v
+Verbose operation. May be given twice, for extra verbosity.
+.UNINDENT
+.sp
+Advanced options:
+.sp
+The "advanced" options are considered advanced because they\(aqre capable
+of creating a bogus \fB\&.CAR\fP file that Atari800 won\(aqt accept. They\(aqre also
+useful for debugging \fBrom2cart\fP or Atari800 itself.
+.INDENT 0.0
+.TP
+.BI \-T \ type
+Force the image type. Unlike \fB\-t\fP, the \fItype\fP given must be numeric,
+and is not required to be a known type. When using this option,
+the file size is not checked. \fB\-T\fP is intended to be used for image
+types that were not yet in existence when this version of \fBrom2cart\fP
+was written.
+.TP
+.BI \-C \ sum
+Force the checksum field in the output \fB\&.CAR\fP image to \fIsum\fP\&.
+Intended for debugging purposes. Atari800 will refuse to load
+\fB\&.CAR\fP images with invalid checksums. \fIsum\fP is a 32\-bit unsigned
+value, and may be given in hex (prefixed with $ or 0x) or decimal
+(no prefix). This option has no effect if the output is a raw dump
+(\fB\-r\fP option).
+.TP
+.BI \-U \ data
+Set the unused bytes (offsets 12\-15) in the \fBCART\fP header to \fIdata\fP\&.
+Currently, these bytes are unused by Atari800, but future versions
+may define a use for them. Normally, \fBrom2cart\fP sets them to all
+zeroes. \fIdata\fP is a 32\-bit unsigned value, and may be given in hex
+(prefixed with $ or 0x) or decimal (no prefix). This option has no
+effect if the output is a raw dump (\fB\-r\fP option).
+.UNINDENT
+.SH NOTES
+.sp
+\fIinfile\fP may be \(aq\-\(aq to read from standard input. \fIoutfile\fP may be \(aq\-\(aq to
+write to standard output. \fBrom2cart\fP will refuse to write binary data to
+a terminal.
+.sp
+If \fIoutfile\fP is omitted, but \fIinfile\fP is provided, the output filename will
+be constructed from \fIinfile\fP by replacing the filename extension with
+\fI\&.car\fP (or \fI\&.rom\fP if \fB\-r\fP is given), or by appending \fI\&.car\fP (or \fI\&.rom\fP) if there
+is no extension.
+.sp
+\fBrom2cart\fP contains an internal database of image types. The current
+version uses the list from Atari800 v2.0.3 (types 1\-43). If you
+need to create a \fB\&.CAR\fP image of a type not supported in this
+version of \fBrom2cart\fP, you can use the \fB\-T\fP option. Alternatively, look
+for a newer version of \fBrom2cart\fP\&. If no new version exists, bug the
+author until he releases one!
+.sp
+The \fB\-T\fP option should be used with caution. It disables all the
+checks that are normally done, and can be used to create \fB\&.CAR\fP files
+with arbitrary types and data sizes (e.g. a 16K image with its
+type set to "Standard 8K", or an image whose type isn\(aqt recognized
+by Atari800 at all). With \fB\-T\fP, it\(aqs up to you to ensure that the image
+type and size is correct.
+.SH CART FORMAT
+.sp
+The \fB\&.CAR\fP format is fully documented in \fIcart.txt\fP, supplied
+with the Atari800 source distribution. The following is an abbreviated
+description.
+.sp
+A \fB\&.CAR\fP image consists of a 16\-byte header followed by the ROM data.
+.sp
+The first 4 bytes contain \(aqC\(aq \(aqA\(aq \(aqR\(aq \(aqT\(aq in ASCII.
+.sp
+The next 4 bytes contain the cartridge type in MSB (aka
+\fIbig\-endian\fP) format.
+.sp
+The next 4 bytes contain cartridge checksum in MSB format (ROM only).
+.sp
+The next 4 bytes are currently unused (zero).
+.sp
+The rest of the file contains the ROM data: 4, 8, 16, 32, 40, 64, 128,
+256, 512 or 1024 kilobytes.
+.SH HEURISTICS
+.sp
+If none of the \fB\-m\fP, \fB\-n\fP, \fB\-T\fP options are given, the machine type is
+guessed according to these rules:
+.sp
+First, examine the option byte (3rd\-to\-last in the ROM image). If
+it\(aqs \fI$FF\fP or in the range \fI$50\-$59\fP, assume 5200. If it\(aqs \fI$04\fP, \fI$05\fP, or
+\fI$80\fP, assume 8\-bit computer.
+.sp
+If the option byte doesn\(aqt help, and if the ROM is 32K or larger
+in size, the cartridge init address (last two bytes of ROM) is
+checked. If it falls in the range \fI$4000\-$7FFF\fP, it must be a 5200 ROM
+(because cartridge ROM starts at \fI$8000\fP on the 8\-bit).
+.sp
+If the ROM is less than 32K, and/or its init address is
+>=$8000, rom2cart searches the first 8K of ROM data, looking for
+6502 machine code that writes to \fI$E8xx\fP (5200 POKEY) or \fI$D2xx\fP
+(8\-bit POKEY). If there are 3 or more "5200 POKEY" writes and zero or
+one "8\-bit POKEY) writes, assume 5200. If 3 or more "8\-bit POKEY"
+writes and zero or one "5200 POKEY" writes, assume 8\-bit.
+.sp
+If the machine type is still unknown, \fBrom2cart\fP will choose
+the lowest\-numbered cartridge type that matches the ROM size,
+regardless of machine type.
+.SH EXIT STATUS
+.sp
+Exit status is zero for success, non\-zero 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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/rom2cart.c b/rom2cart.c
new file mode 100644
index 0000000..f6f8754
--- /dev/null
+++ b/rom2cart.c
@@ -0,0 +1,641 @@
+/* Convert a raw cart dump into a .CAR
+
+ Either the user sets the type, in which case we make sure the file size
+ matches, or else he doesn't, and we try to guess from the file size.
+ Also give options to force the type in case our cart_types is out of
+ data, and to force the checksum for debugging purposes.
+
+ Type guessing algorithm... we have some or all of these:
+
+ File size - always have. Eliminate all types of any other size.
+
+ Machine type - optional, via -m. If we have it, eliminate all types of
+ the wrong machine type.
+
+ Type name partial string - optional, via -t. If we have it, eliminate
+ all non-matching types.
+
+ Type number - optional, via -t. If we have it, eliminate all but the
+ given type number.
+
+ If list is now empty, give error message.
+ If list is only one element long, use it as-is.
+ Otherwise... there are >1 elements in the list.
+
+ If the user has disabled guessing, show the him list, ask him to be
+ more specific.
+
+ (optional heuristics:)
+ - Look at the option byte (3rd to last byte of the ROM)
+ - If the list contains both machine types, try to narrow it down by
+ looking at the init address ($4000-7FFF is definitely 5200).
+ - Also try looking for 3 or more STA $E8xx instructions: on the 5200,
+ these are writes to POKEY; on the 8bit, they're writes to ROM (unlikely).
+
+ If list is only one element long, use it as-is.
+
+ If he hasn't disabled guessing, then guess! Take lowest-numbered type...
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "cart.h"
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "rom2cart"
+
+#define BANNER \
+ SELF " v" VERSION " by B. Watson (WTFPL)\n"
+
+char *usage =
+ "\nUsage: " SELF " [-chl] [-t type] [-T type] [-C sum] [infile] [outfile]\n"
+ " cart2rom [infile] [outfile] (equivalent to " SELF " -r)\n"
+ " -h Print this help.\n"
+ " -v Verbose operation (-vv for 'very verbose').\n"
+ " -c Check only; do not create output file (enables -vv).\n"
+ " -t type Set cartridge type. [type] may be a numeric type or a\n"
+ " partial name to match.\n"
+ " -l List all known cartridge types.\n"
+ " -m mach Cart is for given machine, either '8bit' or '5200'.\n"
+ " -n Do not guess cart type, if unable to narrow down to one.\n"
+ " -r Write a raw ROM image as output, rather than a CART image.\n"
+ " -T type Like -t, but forces the type. Numeric type only.\n"
+ " -C sum Force the checksum to sum. Not very useful.\n"
+ " -U data Set 'unused' bytes in CART header to data.\n"
+ "\n"
+ "infile may be either a raw dump or a CART image, and may be '-' to read\n"
+ "from standard input. outfile may be omitted, in which case output will\n"
+ "be written to a file named after infile, with its extension changed to\n"
+ ".car (or .rom, if -r given). outfile may be '-' to write to standard\n"
+ "output.\n";
+
+#define OPTIONS "vchlm:nT:t:C:rU:"
+
+void print_type_table(int *set) {
+ /* print a nicely-formatted type table. If set is NULL, print all types,
+ otherwise only print types whose bit in set is true. */
+ int i;
+ fprintf(stderr, "%5s | %8s | %6s | %s\n",
+ "Type", "Machine", "SizeKB", "Name");
+ fprintf(stderr, "------|----------|--------|------------------------\n");
+ for(i=1; i<=MAX_CART_TYPE; i++) {
+ if(set == NULL || set[i])
+ fprintf(stderr, "%5d | %8s | %6d | %s\n",
+ i,
+ (cart_types[i].machine == M_5200 ? "5200" : "8-bit"),
+ cart_types[i].size,
+ cart_types[i].name);
+ }
+}
+
+/* read_file() reads and mallocs this many bytes at a time. There should
+ never be a reason to change this. */
+#define READ_CHUNK_SIZE 4096
+
+int read_file(FILE *f, unsigned char **bufp) {
+ /* read an entire file, malloc'ing as we go. caller must free(*bufp).
+ return value is number of bytes read. */
+ int bytes_read = 0, total_read = 0;
+
+ *bufp = malloc(READ_CHUNK_SIZE);
+ if(!*bufp) {
+ fclose(f);
+ fputs("Out of memory (initial malloc failed)\n", stderr);
+ exit(1);
+ }
+
+ while((bytes_read = fread(*bufp + total_read, 1, READ_CHUNK_SIZE, f)) > 0) {
+ unsigned char *newbuf;
+
+ total_read += bytes_read;
+ newbuf = realloc(*bufp, total_read + READ_CHUNK_SIZE);
+
+ if(!newbuf) {
+ fclose(f);
+ free(*bufp);
+ fputs("Out of memory (realloc failed)\n", stderr);
+ exit(1);
+ }
+
+ *bufp = newbuf;
+ }
+
+ return total_read;
+}
+
+/* case-insensitive strstr()-like search */
+char *string_match(char *haystack, char *needle) {
+ while(*haystack) {
+ char *htmp = haystack;
+ char *ntmp = needle;
+
+ if(tolower(*ntmp) == tolower(*haystack)) {
+ while(tolower(*ntmp) == tolower(*htmp)) {
+ ntmp++;
+ htmp++;
+
+ if(!*ntmp)
+ return haystack;
+
+ if(!*htmp)
+ break;
+ }
+ }
+
+ haystack++;
+ }
+
+ return NULL;
+}
+
+machine_t code_heuristics(unsigned char *rom, int size, int verbose) {
+ /* Look at the first 8K (minus 6-byte header), count possible
+ occurences of POKEY writes. The POKEY is at $E800 on the 5200 and
+ $D200 on the 8-bit. Return value is the (probable) machine type,
+ which may be M_INVALID if the test is inconclusive. */
+ int i, a8 = 0, a52 = 0;
+ int machine = M_INVALID;
+
+ for(i=0; i < (size < 8192 ? (size-6) : (8192-6) ); i++) {
+ unsigned char opcode = rom[i];
+ unsigned char page = rom[i+2];
+
+ /* STA abs STX abs STA abs,X */
+ if(opcode == 0x8d || opcode == 0x8e || opcode == 0x9d) {
+ if(page == 0xe8)
+ a52++;
+ else if(page == 0xd2)
+ a8++;
+ }
+ }
+
+ if(verbose > 1) {
+ fprintf(stderr,
+ SELF ": found %d probable 8-bit POKEY writes\n", a8);
+ fprintf(stderr,
+ SELF ": found %d probable 5200 POKEY writes\n", a52);
+ }
+
+ if(a8 >= 3 && a52 <= 1)
+ machine = M_ATARI8;
+ else if(a52 >= 3 && a8 <= 1)
+ machine = M_5200;
+
+ return machine;
+}
+
+machine_t guess_machine_type(unsigned char *rom, int size, int verbose) {
+ /* Try to guess the machine type based on the option byte and/or
+ init address. If that fails, call code_heuristics().
+ Return value is the guessed machine type, which may be M_INVALID if
+ we can't figure it out. */
+ machine_t machine = M_INVALID;
+ unsigned char option = rom[size - 3];
+ int init = rom[size - 2] | (rom[size - 1] << 8);
+
+ if(verbose)
+ fprintf(stderr,
+ SELF ": using heuristics to guess machine type "
+ "(-n or -m to inhibit)\n");
+
+ if(option == 0xff || (option >= 0x50 && option <= 0x59)) {
+ /* 0xff means diagnostic cart on the 5200. 0x50 thru 0x59 are the
+ digits 0-9 for non-diagnostic carts (the 2nd digit of the
+ copyright year) */
+ machine = M_5200;
+ if(verbose > 1)
+ fprintf(stderr, SELF ": machine type is 5200 based on option byte "
+ "$%02X\n", option);
+ } else if(option == 4 || option == 5 || option == 0x80) {
+ /* These are the most common option bytes for A8. */
+ machine = M_ATARI8;
+ if(verbose > 1)
+ fprintf(stderr, SELF ": machine type is 8-bit based on option byte "
+ "$%02X\n", option);
+ } else if(init >= 0x4000 && init < 0x8000 && size >= 32768) {
+ /* 32K and up 5200 carts may have an init address below $8000. On
+ the 8-bit, addresses below $8000 are RAM, not part of the cart
+ address window. */
+ machine = M_5200;
+ if(verbose > 1)
+ fprintf(stderr, SELF ": machine type is 5200 based on init address "
+ "$%04X\n", init);
+ } else {
+ /* If all else fails, look for STA $E8xx (5200 POKEY writes),
+ or STA $D2xx (A8 POKEY writes). Of course, data tables might
+ happen to contain these sequences of bytes... */
+ machine = code_heuristics(rom, size, verbose);
+ }
+
+ if(verbose) {
+ if(machine == M_INVALID)
+ fprintf(stderr, SELF ": heuristics couldn't guess machine type\n");
+ else
+ fprintf(stderr, SELF ": heuristics guessed machine type: %s\n",
+ (machine == M_ATARI8 ? "8-bit" : "5200"));
+ }
+
+ return machine;
+}
+
+int determine_type(
+ int verbose,
+ int allow_guess,
+ int type_override,
+ char *type_param,
+ machine_t machine,
+ int size,
+ unsigned char *rom)
+{
+ /* Figure out the cartridge type (ID), based on the image size, machine
+ type (optional), and/or some heuristics that look at the ROM itself.
+ Return value is the type, which may be 0 if we can't guess.
+ candidates[] has one "bit" (int) per cart type, and is initialized to
+ all zeroes. As we find possible type matches, we turn on the bit(s) in
+ candidates[] (or turn them off, as types are eliminated).
+
+ All this ugly table-searching code would be much prettier as an
+ SQL select, but it'd be overkill to require a Real Database for a
+ small niche-purpose utility like this...
+ */
+ int type = 0, i, match, both, candidates[MAX_CART_TYPE + 1];
+
+ /* process -t/-T first */
+ if(type_param && *type_param >= '1' && *type_param <= '9') {
+ type = atoi(type_param);
+ if(type_override) {
+ if(type)
+ return type; /* valid numeric -T */
+ else
+ return 0; /* bogus -T */
+ } else {
+ if(!type || type > MAX_CART_TYPE) {
+ fprintf(stderr, "Invalid numeric type '%d' for -t (valid types "
+ "are 1 to %d; use -l for list)\n", type, MAX_CART_TYPE);
+ return 0;
+ }
+ }
+ }
+
+ /* no -T, so do a search */
+ for(i=0; i<=MAX_CART_TYPE; i++)
+ candidates[i] = 0;
+
+ if(type) {
+ /* -t numeric type */
+ if(size != (cart_types[type].size * 1024)) {
+ fprintf(stderr, SELF ": ROM size does not match, type %d "
+ "should be %dK, but input is %dK\n",
+ type, cart_types[type].size, size/1024);
+ return 0;
+ }
+
+ candidates[type] = 1;
+ machine = cart_types[type].machine; /* bypass machine-type guessing */
+ } else if(type_param) {
+ /* -t string match */
+ match = 0;
+ if(verbose)
+ fprintf(stderr, SELF ": trying to match '%s'\n", type_param);
+
+ for(i=1; i<=MAX_CART_TYPE; i++) {
+ if(string_match(cart_types[i].name, type_param)) {
+ if(verbose > 1)
+ fprintf(stderr, SELF ": matched type %d (%s)\n",
+ i, cart_types[i].name);
+ candidates[i] = 1;
+ match++;
+ }
+ }
+
+ if(!match) {
+ fprintf(stderr, SELF ": no types match '%s'\n", type_param);
+ return 0;
+ }
+ } else {
+ /* no -t param at all, all types are potential matches */
+ for(i=0; i<=MAX_CART_TYPE; i++)
+ candidates[i] = 1;
+ }
+
+ /* now eliminate all wrong-sized types */
+ match = 0;
+ for(i=1; i<=MAX_CART_TYPE; i++) {
+ if(cart_types[i].size * 1024 == size)
+ match++;
+ else
+ candidates[i] = 0;
+ }
+
+ /* size doesn't match anything in the table! Whoops! */
+ if(!match) {
+ fprintf(stderr, SELF ": no known types match size %.1fKB\n",
+ (double)size / 1024.0);
+ return 0;
+ }
+
+ /* see if both machine types are in the list */
+ match = both = 0;
+ if(machine == M_INVALID) {
+ for(i=1; i<=MAX_CART_TYPE; i++) {
+ if(!candidates[i])
+ continue;
+
+ machine_t mt = cart_types[i].machine;
+ if(match) {
+ if(mt != match) {
+ both = 1;
+ break;
+ }
+ } else {
+ match = mt;
+ }
+ }
+
+ if(!both) machine = match;
+ }
+
+ if((verbose > 1) && both)
+ fprintf(stderr, SELF ": candidate list contains both machine types\n");
+
+ /* if no machine type override, try some heuristics to determine the
+ machine type. */
+ if(allow_guess && (machine == M_INVALID))
+ machine = guess_machine_type(rom, size, verbose);
+
+ /* if -m given or guessed, eliminate all types of wrong machine type */
+ if(machine != M_INVALID) {
+ for(i=1; i<=MAX_CART_TYPE; i++) {
+ if(cart_types[i].machine != machine)
+ candidates[i] = 0;
+ }
+ }
+
+ /* now see whether we have 0, 1, or >1 matches */
+ type = match = 0;
+ for(i=1; i<=MAX_CART_TYPE; i++) {
+ if(candidates[i]) {
+ match++;
+ if(!type) type = i; /* take the lowest-numbered type that matches */
+ }
+ }
+
+ if(!match) {
+ /* d'oh! Nothing matched... */
+ return 0;
+ } else if(match == 1) {
+ /* Exact match, use it */
+ if(verbose) {
+ fprintf(stderr, SELF ": exact match:\n");
+ print_type_table(candidates);
+ }
+
+ return type;
+ } else {
+ /* >1 match... */
+ if(allow_guess) {
+ if(verbose) {
+ fprintf(stderr, SELF ": %d types match:\n", match);
+ print_type_table(candidates);
+ }
+ fprintf(stderr, SELF ": guessing type %d (%s)\n",
+ type, cart_types[type].name);
+ return type;
+ } else {
+ fprintf(stderr, SELF ": %d types match, not guessing:\n", match);
+ print_type_table(candidates);
+ return 0;
+ }
+ }
+}
+
+/* helper for -C and -U options */
+unsigned int get_u32_value(char opt, char *arg) {
+ unsigned int got;
+
+ if(sscanf(arg, "0x%x", &got) != 1)
+ if(sscanf(arg, "$%x", &got) != 1)
+ if(sscanf(arg, "%u", &got) != 1) {
+ fprintf(stderr, "Invalid value for -%c: '%s'\n", opt, arg);
+ exit(1);
+ }
+
+ return got;
+}
+
+int main(int argc, char **argv) {
+ char *infile = "-", outfile[4096] = "-";
+ FILE *in = stdin, *out = stdout;
+ unsigned char *rom;
+ char *type_param = NULL;
+ int check_only = 0, type = 0, type_override = 0, allow_guess = 1,
+ verbose = 0, raw_output = 0;
+ machine_t machine = M_INVALID;
+ int c, size;
+ unsigned int cksum = 0, cksum_override = 0, unused_data = 0;
+ unsigned char *buffer;
+ unsigned char **bufp = &buffer;
+ unsigned char cart[16];
+
+ if(strstr(argv[0], "cart2rom") != NULL) {
+ raw_output = 1;
+ }
+
+ while( (c = getopt(argc, argv, OPTIONS)) != -1 ) {
+ switch(c) {
+ case 'v':
+ verbose++;
+ break;
+
+ case 'c':
+ check_only++;
+ verbose = 2;
+ break;
+
+ case 'h':
+ fputs(BANNER, stderr);
+ fprintf(stderr, usage);
+ exit(0);
+ break;
+
+ case 'l':
+ print_type_table(NULL);
+ exit(0);
+ break;
+
+ case 'T':
+ type_override++;
+ /* fall through */
+
+ case 't':
+ type_param = optarg;
+ break;
+
+ case 'C':
+ cksum = get_u32_value('C', optarg);
+ cksum_override++;
+ break;
+
+ case 'U':
+ unused_data = get_u32_value('U', optarg);
+ break;
+
+ case 'n':
+ allow_guess = 0;
+ break;
+
+ case 'm':
+ if(optarg[0] == '5')
+ machine = M_5200;
+ else if(optarg[0] == 'a' || optarg[0] == '8')
+ machine = M_ATARI8;
+ else {
+ fprintf(stderr, SELF ": Invalid machine type (-m)\n"
+ "Valid types are '8bit' and '5200' (or '8' and '5')\n");
+ exit(1);
+ }
+ break;
+
+ case 'r':
+ raw_output++;
+ break;
+
+ default:
+ fputs(BANNER, stderr);
+ fprintf(stderr, usage);
+ exit(1);
+ break;
+ }
+ }
+
+ if(verbose) fputs(BANNER, stderr);
+
+ if(optind < argc) {
+ infile = argv[optind];
+ if(strcmp(infile, "-") == 0) {
+ in = stdin;
+ if(verbose) fprintf(stderr, SELF ": reading from standard input\n");
+ } else {
+ char *p;
+ strcpy(outfile, infile);
+ p = strrchr(outfile, '.');
+ if(!p) p = outfile + strlen(outfile);
+ *p = '\0';
+ if(raw_output)
+ strcat(outfile, ".rom");
+ else
+ strcat(outfile, ".car");
+
+ if( !(in = fopen(infile, "rb")) ) {
+ if(verbose)
+ fprintf(stderr, SELF ": (fatal) %s: %s\n",
+ infile, strerror(errno));
+ exit(1);
+ }
+ }
+ optind++;
+ }
+
+ size = read_file(in, bufp);
+ fclose(in);
+
+ if(has_cart_signature(buffer)) {
+ rom = buffer + 16;
+ size -= 16;
+ type = get_cart_type(buffer);
+ if(verbose) fprintf(stderr, SELF ": input is CART image, type %d\n", type);
+ } else {
+ rom = buffer;
+ if(verbose) fprintf(stderr, SELF ": input is raw dump, %d bytes\n", size);
+ }
+
+ if(size <= 0) {
+ fprintf(stderr, SELF ": input file is empty!\n");
+ exit(1);
+ } else if(size % 4096) {
+ fprintf(stderr, SELF ": %s: input size not a multiple of 4KB!\n",
+ (type_override ? "warning" : "fatal"));
+ if(!type_override) exit(1);
+ }
+
+ if(!type) {
+ type = determine_type(
+ verbose, allow_guess, type_override, type_param, machine, size, rom);
+ }
+
+ if(!type) {
+ fprintf(stderr, SELF ": "
+ "Can't determine image type from supplied info and ROM size.\n"
+ "Please re-run with -t, -T, and/or -m options.\n");
+ free(buffer);
+ exit(1);
+ }
+
+ /* Built cart header. Don't use create_cart_header() because it assumes
+ the ROM size always matches the expected ROM size for the type. This
+ may NOT be the case, if someone uses -T to e.g. set the type to 16K
+ when there's only 8K of actual ROM. */
+ memcpy(cart, CART_SIGNATURE, 4);
+ set_cart_type(cart, type);
+ if(!cksum_override)
+ cksum = calc_rom_checksum(buffer, size);
+ set_cart_checksum(cart, cksum);
+ set_cart_unused(cart, unused_data);
+
+ if(verbose > 1) cart_dump_header(cart, 0);
+
+ if(!check_only) {
+ if(optind < argc) {
+ strcpy(outfile, argv[optind]);
+ optind++;
+ }
+
+ if(strcmp(outfile, "-") == 0) {
+ out = stdout;
+ if(verbose) fprintf(stderr, SELF ": writing to standard output\n");
+ } else {
+ if(strcmp(infile, outfile) == 0) {
+ fprintf(stderr, SELF ": Input and output filenames are the same, "
+ "aborting!\n");
+ exit(1);
+ }
+ if(verbose) fprintf(stderr, SELF ": output file is '%s'\n", outfile);
+
+ if( !(out = fopen(outfile, "wb")) ) {
+ fprintf(stderr, SELF ": (fatal) %s: %s\n", outfile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ if(isatty(fileno(out))) {
+ fprintf(stderr, SELF ": Standard output is a terminal, "
+ "not writing binary data.\n"
+ "Either redirect to a file or set the output filename.\n");
+ exit(1);
+ }
+ }
+
+ if(optind < argc) {
+ fprintf(stderr, SELF ": ignoring trailing junk on command line: "
+ "'%s' ...\n",
+ argv[optind]);
+ }
+
+ if(!check_only) {
+ if(!raw_output) fwrite(cart, 16, 1, out);
+ fwrite(rom, size, 1, out);
+ fclose(out);
+ }
+
+ free(buffer);
+
+ exit(0);
+}
diff --git a/rom2cart.rst b/rom2cart.rst
new file mode 100644
index 0000000..0edc57d
--- /dev/null
+++ b/rom2cart.rst
@@ -0,0 +1,202 @@
+.. RST source for rom2cart(1) man page. Convert with:
+.. rst2man.py rom2cart.rst > rom2cart.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+========
+rom2cart
+========
+
+----------------------------------------------------------------
+Convert a raw ROM image to an Atari800 CART image, or vice versa
+----------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+**rom2cart** [*-chlnrv*] [-m *machine*] [-t *type*] [-T *forced-type*] [-C *forced-checksum*] [-U *unused-data*] [*infile*] [*outfile*]
+
+**cart2rom** [*infile*] [*outfile*]
+
+DESCRIPTION
+===========
+
+**rom2cart** converts between raw ROM dumps and Atari800 **.CAR**
+images. Despite the name, conversion can be done in either direction.
+**cart2rom** is equivalent to **rom2cart -r**.
+
+Input ROM files may be either raw dumps or Atari800 .CAR format.
+Output will be a **.CAR** file, or (with **-r**) a raw dump.
+
+When reading a raw dump, **rom2cart** attempts to determine
+the correct image type based on the file size, machine type
+(specified via **-m**, or guessed by looking at the ROM
+content), and (optional) user-supplied type number or name (**-t**).
+If **rom2cart** is unable to narrow the selection down to one
+image type, it will "guess" by choosing the lowest-numbered type
+that matches the given parameters (unless **-n** is given to prevent
+this behavior).
+
+When writing a **.CAR** file, **rom2cart** will calculate the checksum
+automatically and store it in the **CART** header (unless **-C** is
+used to force the checksum).
+
+OPTIONS
+=======
+
+Standard options:
+
+-c
+ Check ROM and print info only; do not create any output.
+
+-h
+ Print help (usage) message and exit.
+
+-l
+ Print a complete list of known image types and exit.
+
+-m machine
+ Sets the machine type for the image. Valid machine values are
+ **5200** and **8bit** (may be abbreviated as **5** and **8**). This is used as
+ a hint by the type-guessing algorithm to narrow down the search.
+ May be used in combination with **-t**. Without **-t**, only the machine
+ type and file size are used to guess the image type. This option
+ has no effect with **-T**.
+
+-n
+ Do not guess image type, if unable to determine it exactly from the
+ file size and any supplied **-t**/**-T**/**-m** arguments.
+ The type-matching is still done, and if only one type matches,
+ it will be used. This option only has an effect if type-matching
+ results in two or more possible matches. This option has no effect
+ with **-T**.
+
+-r
+ Output a raw image dump, rather than a **.CAR** image. Most useful
+ when input is a **.CAR** image, but may be used with raw input (in
+ which case, the output file will be a copy of the input, but it
+ will only be created if **rom2cart** thinks the input is a valid raw
+ dump). If output filename not specified, it will be derived from
+ the input filename, and will end in *.rom*.
+
+-t type
+ Set the image type. type may be either a valid numeric type or
+ a name. If a numeric type is given, it must be a valid type, the
+ file size must be correct for the type, and if given, the machine
+ type (**-m** option) must match the type. If a name is given, it is used
+ to search the list of known types; only names that match will be
+ considered as possible types. This is a case-insensitive substring
+ match.
+
+-v
+ Verbose operation. May be given twice, for extra verbosity.
+
+Advanced options:
+
+The "advanced" options are considered advanced because they're capable
+of creating a bogus **.CAR** file that Atari800 won't accept. They're also
+useful for debugging **rom2cart** or Atari800 itself.
+
+-T type
+ Force the image type. Unlike **-t**, the *type* given must be numeric,
+ and is not required to be a known type. When using this option,
+ the file size is not checked. **-T** is intended to be used for image
+ types that were not yet in existence when this version of **rom2cart**
+ was written.
+
+-C sum
+ Force the checksum field in the output **.CAR** image to *sum*.
+ Intended for debugging purposes. Atari800 will refuse to load
+ **.CAR** images with invalid checksums. *sum* is a 32-bit unsigned
+ value, and may be given in hex (prefixed with $ or 0x) or decimal
+ (no prefix). This option has no effect if the output is a raw dump
+ (**-r** option).
+
+-U data
+ Set the unused bytes (offsets 12-15) in the **CART** header to *data*.
+ Currently, these bytes are unused by Atari800, but future versions
+ may define a use for them. Normally, **rom2cart** sets them to all
+ zeroes. *data* is a 32-bit unsigned value, and may be given in hex
+ (prefixed with $ or 0x) or decimal (no prefix). This option has no
+ effect if the output is a raw dump (**-r** option).
+
+NOTES
+=====
+
+*infile* may be '-' to read from standard input. *outfile* may be '-' to
+write to standard output. **rom2cart** will refuse to write binary data to
+a terminal.
+
+If *outfile* is omitted, but *infile* is provided, the output filename will
+be constructed from *infile* by replacing the filename extension with
+*.car* (or *.rom* if **-r** is given), or by appending *.car* (or *.rom*) if there
+is no extension.
+
+**rom2cart** contains an internal database of image types. The current
+version uses the list from Atari800 v2.0.3 (types 1-43). If you
+need to create a **.CAR** image of a type not supported in this
+version of **rom2cart**, you can use the **-T** option. Alternatively, look
+for a newer version of **rom2cart**. If no new version exists, bug the
+author until he releases one!
+
+The **-T** option should be used with caution. It disables all the
+checks that are normally done, and can be used to create **.CAR** files
+with arbitrary types and data sizes (e.g. a 16K image with its
+type set to "Standard 8K", or an image whose type isn't recognized
+by Atari800 at all). With **-T**, it's up to you to ensure that the image
+type and size is correct.
+
+CART FORMAT
+===========
+
+The **.CAR** format is fully documented in *cart.txt*, supplied
+with the Atari800 source distribution. The following is an abbreviated
+description.
+
+A **.CAR** image consists of a 16-byte header followed by the ROM data.
+
+The first 4 bytes contain 'C' 'A' 'R' 'T' in ASCII.
+
+The next 4 bytes contain the cartridge type in MSB (aka
+*big-endian*) format.
+
+The next 4 bytes contain cartridge checksum in MSB format (ROM only).
+
+The next 4 bytes are currently unused (zero).
+
+The rest of the file contains the ROM data: 4, 8, 16, 32, 40, 64, 128,
+256, 512 or 1024 kilobytes.
+
+HEURISTICS
+==========
+
+If none of the **-m**, **-n**, **-T** options are given, the machine type is
+guessed according to these rules:
+
+First, examine the option byte (3rd-to-last in the ROM image). If
+it's *$FF* or in the range *$50-$59*, assume 5200. If it's *$04*, *$05*, or
+*$80*, assume 8-bit computer.
+
+If the option byte doesn't help, and if the ROM is 32K or larger
+in size, the cartridge init address (last two bytes of ROM) is
+checked. If it falls in the range *$4000-$7FFF*, it must be a 5200 ROM
+(because cartridge ROM starts at *$8000* on the 8-bit).
+
+If the ROM is less than 32K, and/or its init address is
+>=$8000, rom2cart searches the first 8K of ROM data, looking for
+6502 machine code that writes to *$E8xx* (5200 POKEY) or *$D2xx*
+(8-bit POKEY). If there are 3 or more "5200 POKEY" writes and zero or
+one "8-bit POKEY) writes, assume 5200. If 3 or more "8-bit POKEY"
+writes and zero or one "5200 POKEY" writes, assume 8-bit.
+
+If the machine type is still unknown, **rom2cart** will choose
+the lowest-numbered cartridge type that matches the ROM size,
+regardless of machine type.
+
+EXIT STATUS
+===========
+
+Exit status is zero for success, non-zero for failure.
+
+.. include:: manftr.rst
diff --git a/rstman.rst b/rstman.rst
new file mode 100644
index 0000000..361ade5
--- /dev/null
+++ b/rstman.rst
@@ -0,0 +1,58 @@
+.. RST source for PRGNAM(1) man page. Convert with:
+.. rst2man.py PRGNAM.rst > PRGNAM.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+======
+PRGNAM
+======
+
+-------------------------------
+description of whatever this is
+-------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+*PRGNAM* [*-options*]
+
+DESCRIPTION
+===========
+
+Description of PRGNAM
+
+OPTIONS
+=======
+
+--version Output version number
+
+-?, -h, --help
+ Output usage string
+
+**-foo** *BAR*
+ Use *BAR* for fooing.
+
+**-baz**
+ Stuff with baz.
+
+-v Verbose mode
+
+.. other sections we might want, uncomment as needed.
+
+.. FILES
+.. =====
+
+.. ENVIRONMENT
+.. ===========
+
+.. EXIT STATUS
+.. ===========
+
+.. BUGS
+.. ====
+
+.. EXAMPLES
+.. ========
+
+.. include:: manftr.rst
diff --git a/test2 b/test2
new file mode 100644
index 0000000..00ef866
--- /dev/null
+++ b/test2
@@ -0,0 +1,2 @@
+Writing this file to a disk image should result in only one sector being used in the image, because I fixed the bug in axe..
+Writing this file to a disk image should result in only one sector being used in the image, because I fixed the bug in axe..
diff --git a/testdata b/testdata
new file mode 100644
index 0000000..54729a2
--- /dev/null
+++ b/testdata
@@ -0,0 +1 @@
+Writing this file to a disk image should result in only one sector being used in the image, because I fixed the bug in axe..
diff --git a/testdata.orig b/testdata.orig
new file mode 100644
index 0000000..54729a2
--- /dev/null
+++ b/testdata.orig
@@ -0,0 +1 @@
+Writing this file to a disk image should result in only one sector being used in the image, because I fixed the bug in axe..
diff --git a/unmac65.1 b/unmac65.1
new file mode 100644
index 0000000..71c29c9
--- /dev/null
+++ b/unmac65.1
@@ -0,0 +1,392 @@
+.\" 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 "UNMAC65" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py unmac65.rst > unmac65.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fBunmac65\fP [\fI\-options\fP] \fIfile.m65\fP
+.SH DESCRIPTION
+.sp
+\fBunmac65\fP reads files created with Mac/65\(aqs SAVE command (usually called
+\&.M65 files) and converts them back to plain text assembly source.
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \-a
+Use ATASCII EOLs. This option is not available in Atari version,
+since it already uses ATASCII.
+.TP
+.B \-c
+Convert non\-printable characters constants to hex bytes.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \fB\-cc\fP
+Convert all character constants to hex bytes.
+.TP
+.B \-e nnn[,iii]
+Renumber the program, starting at line \fInnn\fP, with increment \fIiii\fP\&.
+\fInnn\fP must be present, and must be an integer greater than or equal
+to zero. \fIiii\fP is optional, must be positive (non\-zero), and
+defaults to 10 if not given.
+.sp
+Mac/65\(aqs maximum line number is 65535. unmac65 will happily renumber
+lines with no upper bound (other than unsigned int overflow), so pay
+attention.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \-h
+Show command\-line help.
+.TP
+.B \-i
+Convert inverse video (in comments and strings) to standard ASCII.
+Lines that were converted will get a comment "\fI; XXX inverse\fP" at
+the end. This option also enables the \-c option.
+.sp
+If the program contained any inverse\-video strings, the resulting
+output will \fInot\fP reassemble correctly; you\(aqll have to edit it to
+e.g. change the formerly inverse video strings to a list of hex
+bytes.
+.sp
+This option is not available in the Atari version.
+.TP
+.B \-l
+Lowercase mnemonics and hex constants (but not labels or comments).
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \fB\-la\fP
+Lowercase mnemonics, hex constants, labels, and comments (but not
+strings or character constants).
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \-n
+No line numbers in output.
+.TP
+.BI \-o \ file
+Output to \fIfile\fP (default = standard output).
+.TP
+.B \-q
+Add closing single\-quote to character constants. Changes this:
+.INDENT 7.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+LDA #\(aqA
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+\&...to this:
+.INDENT 7.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+LDA #\(aqA\(aq
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.TP
+.B \-p
+Omit leading . (period) from pseudo\-ops (e.g. print BYTE for .BYTE).
+.TP
+.B \-t
+Replace leading spaces with tabs.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \fB\-ta\fP
+Replace spaces between all fields with tabs.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \-v
+Verbose output (dump tokens in hex). Useful for examining damaged
+\&.M65 files, or debugging unmac65 itself.
+.UNINDENT
+.SS Human\-readable Output Options
+.sp
+The \-m, \-r, and \-u options are not available for the Atari, and may or
+may not be useful on non\-Linux OSes.
+.INDENT 0.0
+.TP
+.B \-m
+Print inverse video as pseudo\-underlined, using backspace and
+underscore. Useful for piping to more(1) or less(1). Can be combined
+with \-u.
+.TP
+.B \-r
+Print inverse video as reverse video using xterm/ANSI compatible
+escape sequences. Can be combined with \-u. Useful for piping to
+less(1) provided its \-r or \-R option is used.
+.TP
+.B \-u
+Print ATASCII control characters as their nearest Unicode
+equivalents (encoded in UTF\-8). Depending on your terminal,
+combining this option with \-r may not work properly. Also, depending
+on the font(s) your terminal is using, you may see boxes instead of
+control characters. If this happens, try a different font, or a
+different terminal (the author recommends rxvt\-unicode).
+.UNINDENT
+.sp
+Options may not be bundled (use "\-p \-t", not "\-pt").
+.sp
+Unlike most UNIX\-flavored programs, the CLI options are
+case\-insensitive. This is to make life easier for users of the Atari
+version, where uppercase is the normal way of doing things.
+.sp
+The \-c, \-cc, \-l, \-la, \-n, \-p, \-t, \-ta options are provided to assist in
+porting Mac/65 programs to other assemblers, such as ca65 or dasm.
+unmac65\(aqs output with none of these options (or with \-n only) is
+acceptable as input for the atasm assembler. This is true even if there
+are inverse video strings: they look funny when viewing the file, but
+atasm handles them correctly.
+.sp
+The \-v option prints the hex bytes for each line (preceded by ";;" and
+the line number) after that line\(aqs detokenized listing.
+.sp
+Note that the output from \-m, \-r, \-u is intended for humans to read.
+They\(aqre not very useful if you\(aqre trying to port Mac/65 code to a
+different assembler, as none of them know what to do with the
+underlines, ANSI codes, or pseudo\-ATASCII Unicode characters.
+.SH FILE FORMAT
+.sp
+A tokenized Mac/65 file consists of:
+.sp
+Header: 2 byte $FE $FE signature, followed by the 2 byte program length
+in LSB/MSB format. Length doesn\(aqt include the 4 header bytes.
+.sp
+The rest of the file consists of lines of code. Each line is:
+.sp
+Line number, 2 bytes (LSB/MSB format)
+.sp
+Line length, 1 byte. Total length, including the line number and length
+bytes.
+.sp
+Tokens. Length minus 3 bytes of tokens. If the line is labelled, the
+label will appear first, as a tokenized string (see below).
+.sp
+Whether or not there\(aqs a label, the next byte is the token for a
+mnemonic (or pseudo\-op). Lines containing only a comment will have a
+special token meaning "no mnemonic".
+.sp
+After the mnemonic token, 0 or more bytes of operands. Quoted strings or
+labels as operands are stored as a tokenized string. Hex or decimal
+constants are preceded by a token indicating the length (one or two
+bytes) and the type (hex or decimal).
+.sp
+If there is a comment, the last byte of the operand field will be an
+ASCII semicolon. The remaining bytes on the line are the comment in
+ASCII form.
+.sp
+Tokenized strings can occur in the label or operand parts of the line.
+The first byte is the length of the string in bytes, with the high bit
+set (e.g. $84 for a 4\-byte string), followed by that number of ASCII
+bytes. The length doesn\(aqt include the first byte, so e.g. the string
+"ABC" is stored as $83, $41, $42, $43. Mac/65 doesn\(aqt allow empty
+strings, so a zero\-length string (length byte $80) is an error.
+.sp
+There are separate sets of tokens for mnemonics/pseudo\-ops and operands.
+Mnemonic/pseudo\-op tokens run from 0 to $5F, and operand tokens run from
+0 to $4D (with 0\-$09 being "special", and a few invalid tokens in the
+range $0A\-$4D). See the C source for the full list (or a hex/ascii dump
+of the Mac/65 ROM, which is where I got the lists to put them in the C
+source). Also, you can run unmac65 with the \-v option to get a
+line\-by\-line hex dump of the tokens.
+.SH DIAGNOSTICS
+.sp
+unmac65: line XX contains NN non\-printable ATASCII characters <= $1F
+.sp
+Self\-explanatory. Depending on what you\(aqre going to use the converted
+file for, this may or may not be a problem. Non\-fatal. This warning
+doesn\(aqt occur in the Atari version of unmac65. Also, it doesn\(aqt occur if
+the \-u option is in use.
+.sp
+unmac65: line XX contains NN inverse ATASCII characters >= $80
+.sp
+Self\-explanatory. Depending on what you\(aqre going to use the converted
+file for, this may or may not be a problem. Non\-fatal. Use the \-i option
+to convert inverse video to normal. This warning doesn\(aqt occur in the
+Atari version of unmac65. Also, it doesn\(aqt occur if any of the \-i, \-m,
+or \-r options are in use.
+.sp
+unmac65: not a mac/65 file (missing $FEFE header)
+.sp
+Self\-explanatory. Fatal error.
+.sp
+unmac65: corrupt or truncated file?
+.sp
+The length of the last line in the file is longer that the number of
+bytes remaining in the file (according to the file header). Fatal error.
+.sp
+unmac65: unexpected EOF?
+.sp
+The file is shorter than the file header\(aqs program length. Probably the
+file is truncated; less probably, the length header got scrambled
+somehow. Fatal error.
+.sp
+unmac65: file is too short (N bytes)
+.sp
+The minimum length for a Mac/65 file is 4 bytes (which would be an empty
+program containing no lines). The input file was shorter than 4 bytes.
+Fatal error. (Actually, Mac/65 will never produce a 4\-byte file, it\(aqs
+just the theoretical minimum)
+.sp
+unmac65: file is valid but contains no lines of code
+.sp
+Self\-explanatory. Mac/65 creates a file like this if the SAVE command is
+given before entering any code (at startup or after a NEW). The SAVEd
+file will be 5 bytes in length, and utterly useless. Non\-fatal warning.
+.sp
+unmac65: line #lll <= prev line #mmm
+.sp
+The line numbers in the file are supposed to be stored in ascending
+order. Somehow this file has the line numbers out of order. Non\-fatal,
+but probably the rest of the file will be garbage.
+.sp
+unmac65: internal error, state n
+.sp
+This is a "this should never happen" error. It indicates a bug in the
+program. If you ever see this error, please notify the author, and send
+a copy of the Mac/65 program that caused it. This is a non\-fatal error,
+but the output might be garbage.
+.sp
+[$nn?] or <$nn?> in the output (where nn is 2 hex digits)
+.sp
+These indicate unknown/invalid tokens. Either the file is damaged, or
+there is a bug in the program. These are non\-fatal errors. If you ever
+see them, please contact the author, and send a copy of the Mac/65
+program that caused them.
+.sp
+unmac65: ignoring extra junk at EOF
+.sp
+The file contains more bytes than the program length header says it
+should. This usually means the file was stored on an old DOS disk or
+transferred with a broken XMODEM implementation, and was padded to the
+sector/block size. Alternately, the header bytes got corrupted somehow
+(this is highly unlikely, especially if there are no other
+errors/warnings). Non\-fatal error.
+.sp
+Other errors are possible (e.g. disk full, I/O error reading input), but
+they\(aqre not specific to unmac65; no need to list them all there.
+.sp
+Fatal errors result in unmac65 terminating. A non\-fatal error can
+usually be recovered from, though the line that caused it will probably
+be printed strangely.
+.sp
+It\(aqs probably worth mentioning also that Mac/65 source files can contain
+ATASCII graphics or escape codes, although it\(aqs not a very common
+practice. If you see strange stuff and/or your terminal misbehaves when
+writing to standard output, try writing to a file (\-o option) instead.
+See also the \-m, \-r, \-u options for human\-readable output. The Atari
+version will render ATASCII graphics just fine, of course.
+.SH EXIT STATUS
+.sp
+unmac65 will normally exit with a zero (success) status upon completion.
+A non\-zero status indicates a fatal error.
+.SH LIMITATIONS
+.sp
+The main difference between Mac/65\(aqs LIST output and unmac65\(aqs output is
+that Mac/65 lines up the label, mnemonic, and comment fields (if the
+field contents are short enough to fit in the allotted width), while
+unmac65 makes no attempt to do so. If the field alignment is important
+to you, try the \-t or \-ta options (which insert hard tabs, unlike
+mac65\(aqs spaces). A future version of unmac65 may correct this minor
+flaw, but as it stands, I\(aqve tested quite a few .M65 files by running
+them through unmac65, then ENTERing unmac65\(aqs listed file in Mac/65 and
+reSAVEing them... In all cases, the newly created tokenized files
+compare identically to the original .M65 file. If you come across a file
+that doesn\(aqt do this, yet is valid (can be assembled without error by
+Mac/65), please send it to me, so I can fix unmac65!
+.sp
+Although a few helpful options have been added for porting Mac/65
+sources to other assemblers, unmac65 doesn\(aqt completely automate the
+process. Depending on the assembler you\(aqre using, you may still have a
+lot of manual edits to make. A future version of unmac65 may add a few
+more options, but some hypothetical complex porting functions (like
+"convert to ca65 format") would require implementing a much more complex
+parser (such as a yacc\-based recursive descent parser). Most likely this
+will never happen, if only because I want the program to be usable on
+the Atari itself, with its limited memory and C compiler support.
+.sp
+There are a few ways that an invalid file can sneak past unmac65\(aqs error
+checking. The same files wouldn\(aqt load correctly in Mac/65 either, but
+generally don\(aqt cause any errors in Mac/65 (just silent failure). It\(aqd
+be nice if unmac65 could act as a "M65 lint" for Mac/65 users.
+.sp
+unmac65 is mostly developed/tested against the OSS Mac/65 v1.01
+cartridge. The various disk versions appear to use the same tokenized
+format, but haven\(aqt been well tested. If you have problems, please
+contact the author.
+.sp
+(A further limitation is that the documentation isn\(aqt very concise.
+Sorry about that.)
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/unmac65.c b/unmac65.c
new file mode 100644
index 0000000..42875bb
--- /dev/null
+++ b/unmac65.c
@@ -0,0 +1,1041 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#ifndef VERSION
+# define VERSION "???"
+#endif
+
+#ifndef TAG
+# define TAG ""
+#endif
+
+#define SELF "unmac65"
+#define BANNER SELF " v" VERSION " by B. Watson (WTFPL)\n\n"
+
+#ifdef __CC65__
+# ifdef __ATARI__
+# define ATARI8
+# else
+# error "This program only supports Atari 8-bit when built with cc65"
+/* Feel free to add support for other systems, if you need to. The main
+ differences will be in fix_filename() and atari8_get_opts(). */
+# endif
+#else
+# undef ATARI8
+#endif
+
+#define lsbmsb(lo, hi) ( (lo) | (hi << 8) )
+
+/* use static filename buffers on A8, since we (probably) won't be
+ getting them passed with argv. FIXME: I really ought to do bounds
+ checking... */
+#ifdef ATARI8
+char infile[128], outfile[128];
+#else
+char *infile = NULL, *outfile = NULL;
+#endif
+
+FILE *input = NULL, *output = NULL;
+int using_stdout = 1;
+
+/* see handle_cli_opts() and/or atari8_get_opts() for these: */
+char nl = '\n';
+char no_numbers = 0;
+char leading_tabs = 0;
+char all_tabs = 0;
+char omit_dots = 0;
+char lcase_opcodes = 0;
+char lcase_all = 0;
+char dump_tokens = 0;
+char add_quote = 0;
+
+unsigned int renum_start, renum_line;
+int renum_incr = 0;
+
+typedef enum { CC_NONE, CC_UNPRINT, CC_ALL } chconst_opt_t;
+chconst_opt_t chconsts_hex = 0;
+
+/* options only available in the non-Atari8 ports */
+#ifndef ATARI8
+char deinverse = 0;
+char found_inverse = 0;
+char found_unprint = 0;
+char inv_underscore = 0;
+char inv_ansi = 0;
+char unicode = 0;
+#endif
+
+/* dumpbuf[] really should be local to parse_one_line(), but
+ cc65 won't let us make this a local var, it's too big. 1000 bytes
+ is plenty (max line length is 256 bytes, we dump them in hex at
+ 3 chars each, plus 10-12 chars worth of formatting) */
+char dumpbuf[1000];
+
+/* number of bytes left to read (initialized from 4-byte m65 header,
+ decremented by next_byte()). If this ever reaches 0 while there's
+ more input, or if we get EOF while prog_bytes != 0, it's an error. */
+unsigned int prog_bytes;
+
+/* Guess what these are for? */
+int line_number, old_line_number = -1;
+
+char *opcode_tokens[] = {
+ "ERROR -", /* 0x00 */
+ ".IF",
+ ".ELSE",
+ ".ENDIF",
+ ".MACRO",
+ ".ENDM",
+ ".TITLE",
+ " ",
+ ".PAGE",
+ ".WORD",
+ ".ERROR",
+ ".BYTE",
+ ".SBYTE",
+ ".DBYTE",
+ ".END",
+ ".OPT",
+ ".TAB", /* 0x10 */
+ ".INCLUDE",
+ ".DS",
+ ".ORG",
+ ".EQU",
+ "BRA",
+ "TRB",
+ "TSB",
+ ".FLOAT",
+ ".CBYTE",
+ ";",
+ ".LOCAL",
+ ".SET",
+ "*=",
+ "=",
+ ".=",
+ "JSR", /* 0x20 */
+ "JMP",
+ "DEC",
+ "INC",
+ "LDX",
+ "LDY",
+ "STX",
+ "STY",
+ "CPX",
+ "CPY",
+ "BIT",
+ "BRK",
+ "CLC",
+ "CLD",
+ "CLI",
+ "CLV",
+ "DEX", /* 0x30 */
+ "DEY",
+ "INX",
+ "INY",
+ "NOP",
+ "PHA",
+ "PHP",
+ "PLA",
+ "PLP",
+ "RTI",
+ "RTS",
+ "SEC",
+ "SED",
+ "SEI",
+ "TAX",
+ "TAY",
+ "TSX", /* 0x40 */
+ "TXA",
+ "TXS",
+ "TYA",
+ "BCC",
+ "BCS",
+ "BEQ",
+ "BMI",
+ "BNE",
+ "BPL",
+ "BVC",
+ "BVS",
+ "ORA",
+ "AND",
+ "EOR",
+ "ADC",
+ "STA", /* 0x50 */
+ "LDA",
+ "CMP",
+ "SBC",
+ "ASL",
+ "ROL",
+ "LSR",
+ "ROR",
+ "", /* 0x58 - the null opcode */
+ "STZ",
+ "DEA",
+ "INA",
+ "PHX",
+ "PHY",
+ "PLX",
+ "PLY" /* 0x5f */
+};
+
+/* Special opcodes */
+#define MAX_OPCODE 0x5f
+#define NO_OPCODE 0x58
+
+char *operand_tokens[] = {
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL, /* actually "'", special handling tho */ /* 0x0a, 10 decimal */
+ "%$",
+ "%",
+ "*",
+ " ",
+ " ",
+ "a", /* 0x10 */
+ "q",
+ "+",
+ "-",
+ "*", /* 0x14, 20 decimal */
+ "/",
+ "&",
+ ".DEF",
+ "=",
+ "<=",
+ ">=",
+ "<>",
+ ">",
+ "<",
+ "-", /* 0x1e, 30 dec */
+ "[",
+ "]", /* 0x20 */
+ ".OR",
+ ".AND",
+ ".NOT",
+ "!",
+ "^",
+ ".REF",
+ "\\",
+ NULL, /* 0x28, 40 dec */
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ ".REF",
+ ".DEF ", /* 0x30 */
+ ".NOT ",
+ " .AND ", /* 0x32, 50 dec */
+ " .OR ",
+ " <",
+ " >",
+ ",X)",
+ "),Y",
+ ",Y",
+ ",X",
+ ")",
+ ",", /* 0x3b, 59, the null operand */
+ "\x1b", /* 0x3c, 60, ASCII escape, chr$(27)? */
+ ",",
+ "#",
+ "A",
+ "(", /* 0x40, 64 dec */
+ "\"",
+ "$",
+ "Q",
+ "NO",
+ "NO ",
+ "OBJ", /* 0x46, 70 dec */
+ "ERR",
+ "EJECT",
+ "LIST",
+ "XREF",
+ "MLIST",
+ "CLIST",
+ "NUM", /* 0x4d, 77 dec */
+ /* "M", */ /* maybe? I think not... */
+};
+
+/* Special operands */
+#define MAX_OPERAND 0x4d
+#define NO_OPERAND 0x3b
+
+#define HEXWORD_PREFIX 5
+#define HEXBYTE_PREFIX 6
+
+#define DECWORD_PREFIX 7
+#define DECBYTE_PREFIX 8
+
+#define CHAR_CONST_PREFIX 0x0a
+
+/* Functions */
+void print_label_byte(unsigned char byte, FILE *output) {
+ putc((lcase_all ? tolower(byte) : byte), output);
+}
+
+#ifdef ATARI8
+void print_string_byte(unsigned char byte, FILE *output) {
+ putc(byte, output);
+}
+#else
+char *unicode_table[] = {
+ "♥", "┣", "┃", "┛", "┫", "┓", "╱", "╲", "◢", "▗", "◣", "▝", "▘", "▔", "▁", "▖",
+ "♣", "┏", "━", "╋", "⚫ ", "▄", "▎", "┳", "┻", "▌", "┗", "␛", "↑", "↓", "←", "→",
+ " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/",
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?",
+ "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
+ "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_",
+ "◆", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
+ "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "♠", "|", "↰", "◀", "▶"
+};
+
+/* This is probably more complex than it needs to be. */
+void start_inverse(FILE *output) {
+ if(inv_underscore) {
+ fputs("_\x08", output);
+ } else if(inv_ansi) {
+ fputs("\x1b[7m", output);
+ }
+}
+
+void end_inverse(FILE *output) {
+ if(inv_underscore) {
+ /* do nothing */;
+ } else if(inv_ansi) {
+ fputs("\x1b[0m", output);
+ }
+}
+
+void print_string_byte(unsigned char byte, FILE *output) {
+ int inverse;
+
+ inverse = (byte >= 0x80);
+ byte &= 0x7f;
+ if(inverse && deinverse) {
+ found_inverse++;
+ inverse = 0;
+ }
+
+ if(inverse) start_inverse(output);
+ if(unicode) {
+ fputs(unicode_table[byte], output);
+ } else {
+ if(inv_underscore || inv_ansi) {
+ putc(byte, output);
+ } else {
+ putc(inverse ? (byte | 0x80) : byte, output);
+ found_inverse += inverse;
+ }
+ if(byte < 0x20) found_unprint++;
+ }
+ if(inverse) end_inverse(output);
+}
+
+#endif
+
+#define print_comment_byte(x, y) print_string_byte((lcase_all ? tolower(x) : x), y)
+
+void print_hex_byte(unsigned char byte, FILE *output) {
+ fprintf(output, (lcase_opcodes ? "$%02x" : "$%02X"), byte);
+}
+
+void print_hex_word(int word, FILE *output) {
+ fprintf(output, (lcase_opcodes ? "$%04x" : "$%04X"), word);
+}
+
+
+void closeall() {
+ if(output && !using_stdout) {
+ fclose(output);
+ output = NULL;
+ }
+
+ if(input) {
+ fclose(input);
+ input = NULL;
+ }
+}
+
+void exit_cleanly(int status) {
+#ifdef ATARI8
+ if(status) {
+ puts("Press Return to exit:");
+ fflush(stdout);
+ fgets(infile, 80, stdin);
+ }
+#endif
+
+ closeall();
+ exit(status);
+}
+
+unsigned char next_byte() {
+ int c;
+
+ if(prog_bytes <= 0) {
+ fprintf(stderr, SELF ": corrupt or truncated file?\n");
+ exit_cleanly(1);
+ }
+
+ --prog_bytes;
+ c = getc(input);
+ if(c == EOF && prog_bytes != 0) {
+ fprintf(stderr, SELF ": unexpected EOF\n");
+ exit_cleanly(1);
+ }
+
+ if(prog_bytes == 0 && getc(input) != EOF) {
+ fprintf(stderr, SELF ": ignoring extra junk at EOF\n");
+ }
+
+ return (unsigned char)c;
+}
+
+void parse_header(void) {
+ unsigned char inbuf[4];
+ int bytes = fread(inbuf, 1, 4, input);
+ if(bytes < 4) {
+ if(ferror(input)) {
+ perror(infile);
+ } else {
+ fprintf(stderr, SELF ": file is too short (%d bytes)\n", bytes);
+ }
+ exit_cleanly(1);
+ }
+
+ if(inbuf[0] != 0xfe || inbuf[1] != 0xfe) {
+ fprintf(stderr, SELF ": not a mac/65 file (missing $FEFE header)\n");
+ exit_cleanly(1);
+ }
+
+ prog_bytes = lsbmsb(inbuf[2], inbuf[3]);
+ if(!prog_bytes) fprintf(stderr, SELF ": file is valid but contains no lines of code\n");
+
+ if(dump_tokens) {
+ fprintf(output, ";; Mac/65 header: ");
+ for(bytes = 0; bytes < 4; bytes++)
+ fprintf(output, "%02X ", inbuf[bytes]);
+ fprintf(output, "length %d\n", prog_bytes);
+ }
+}
+
+void print_opcode(unsigned char byte) {
+ char *opc = opcode_tokens[byte];
+
+ putc((leading_tabs ? '\t' : ' '), output);
+ if(omit_dots && opc[0] == '.' && isalpha(opc[1]))
+ ++opc;
+
+ if(lcase_opcodes) {
+ char buf[10];
+ char *p = buf;
+ strcpy(buf, opc);
+ while(*p) {
+ *p = tolower(*p);
+ ++p;
+ }
+ opc = buf;
+ }
+
+ fputs(opc, output);
+ putc((all_tabs ? '\t' : ' '), output);
+}
+
+/* Parser states. The parser is hand-rolled and kind of ugly. */
+#define ST_NEED_OPCODE 1
+#define ST_IN_LABEL 2
+#define ST_IN_OPERAND 3
+#define ST_IN_OPSTRING 4
+#define ST_IN_COMMENT 5
+#define ST_ERROR 6
+#define ST_IN_HEXBYTE 7
+#define ST_IN_HEXWORD_LSB 8
+#define ST_IN_HEXWORD_MSB 9
+#define ST_IN_DECBYTE 10
+#define ST_IN_DECWORD_LSB 11
+#define ST_IN_DECWORD_MSB 12
+#define ST_IN_CHAR_CONST 13
+
+int handle_operand(unsigned char byte) {
+ char *operand = NULL;
+
+ if(byte > 0x80)
+ return ST_IN_OPSTRING;
+
+ if(byte == NO_OPERAND) {
+ putc((all_tabs ? '\t' : ' '), output);
+ return ST_IN_COMMENT;
+ }
+
+ if(byte > MAX_OPERAND)
+ return ST_ERROR;
+
+ operand = operand_tokens[byte];
+ if(operand) {
+ fputs(operand, output);
+ return ST_IN_OPERAND;
+ }
+
+ switch(byte) {
+ case HEXWORD_PREFIX:
+ return ST_IN_HEXWORD_LSB;
+
+ case HEXBYTE_PREFIX:
+ return ST_IN_HEXBYTE;
+
+ case DECWORD_PREFIX:
+ return ST_IN_DECWORD_LSB;
+
+ case DECBYTE_PREFIX:
+ return ST_IN_DECBYTE;
+
+ case CHAR_CONST_PREFIX:
+ return ST_IN_CHAR_CONST;
+
+ /* TODO: find out if any other specials exist */
+
+ default:
+ fprintf(output, "[$%02X?]", byte);
+ return ST_IN_OPERAND;
+ }
+}
+
+void parse_one_line() {
+ int state = ST_NEED_OPCODE;
+ int string_len = 0;
+ int line_bytes;
+ int lsb = 0;
+ unsigned char byte, linenum_lo, linenum_hi;
+ char hexbuf[20];
+
+#ifndef ATARI8
+ found_inverse = found_unprint = 0;
+#endif
+
+ linenum_lo = next_byte();
+ linenum_hi = next_byte();
+
+ if(renum_incr) {
+ line_number = renum_line;
+ renum_line += renum_incr;
+ } else {
+ line_number = lsbmsb(linenum_lo, linenum_hi);
+ }
+
+ if(line_number <= old_line_number) {
+ fprintf(stderr, SELF ": line #%d <= prev line #%d\n",
+ line_number, old_line_number);
+ }
+
+ line_bytes = next_byte() - 3;
+
+ if(dump_tokens)
+ sprintf(dumpbuf, ";; %d (%02X %02X, len %02X):",
+ line_number, linenum_lo, linenum_hi, line_bytes + 3);
+
+ if(!no_numbers) {
+ char *format = "%06d ";
+
+ /* duplicate mac65's weird line number formatting */
+ if(line_number < 100)
+ format = "%02d ";
+ else if(line_number < 10000)
+ format = "%04d ";
+
+ fprintf(output, format, line_number);
+ }
+
+ while(line_bytes) {
+ byte = next_byte();
+ --line_bytes;
+
+ if(dump_tokens) {
+ sprintf(hexbuf, " %02X", byte);
+ strcat(dumpbuf, hexbuf);
+ }
+
+ switch(state) {
+ case ST_NEED_OPCODE:
+ if(byte > 0x80) {
+ string_len = byte & 0x7f;
+ state = ST_IN_LABEL;
+ } else if(byte == NO_OPCODE) {
+ state = ST_IN_COMMENT;
+ } else if(byte <= MAX_OPCODE) {
+ print_opcode(byte);
+ state = ST_IN_OPERAND;
+ } else {
+ state = ST_ERROR;
+ }
+ break;
+
+ case ST_IN_LABEL:
+ print_label_byte(byte, output);
+ if(--string_len == 0)
+ state = ST_NEED_OPCODE; /* TODO: 2 labels is error, detect */
+ break;
+
+ case ST_IN_OPERAND:
+ state = handle_operand(byte);
+ if(state == ST_IN_OPSTRING) string_len = byte & 0x7f;
+ break;
+
+ case ST_IN_OPSTRING:
+ print_string_byte(byte, output);
+ if(--string_len == 0)
+ state = ST_IN_OPERAND;
+ break;
+
+ case ST_IN_COMMENT:
+ print_comment_byte(byte, output);
+ break;
+
+ case ST_ERROR:
+ fprintf(output, "<$%02X?>", byte);
+ break;
+
+ case ST_IN_HEXBYTE:
+ print_hex_byte(byte, output);
+ state = ST_IN_OPERAND;
+ break;
+
+ case ST_IN_HEXWORD_LSB:
+ lsb = byte;
+ state = ST_IN_HEXWORD_MSB;
+ break;
+
+ case ST_IN_HEXWORD_MSB:
+ print_hex_word(lsbmsb(lsb, byte), output);
+ state = ST_IN_OPERAND;
+ break;
+
+ case ST_IN_DECBYTE:
+ fprintf(output, "%d", byte);
+ state = ST_IN_OPERAND;
+ break;
+
+ case ST_IN_DECWORD_LSB:
+ lsb = byte;
+ state = ST_IN_DECWORD_MSB;
+ break;
+
+ case ST_IN_DECWORD_MSB:
+ fprintf(output, "%d", lsbmsb(lsb, byte));
+ state = ST_IN_OPERAND;
+ break;
+
+ case ST_IN_CHAR_CONST:
+#ifndef ATARI8
+ if(
+ chconsts_hex == CC_ALL ||
+ (chconsts_hex == CC_UNPRINT && (byte < 0x20 || byte > 0x7f))
+ )
+ {
+ print_hex_byte(byte, output);
+ state = ST_IN_OPERAND;
+ break;
+ }
+#endif
+ putc('\'', output);
+ print_string_byte(byte, output);
+ if(add_quote)
+ putc('\'', output);
+ state = ST_IN_OPERAND;
+ break;
+
+ default:
+ fprintf(stderr, SELF ": internal error, state %d\n", state);
+ state = ST_ERROR;
+ break;
+ }
+ }
+
+#ifndef ATARI8
+ if(found_inverse) {
+ fprintf(stderr, SELF ": line %d contains %d inverse ATASCII characters >= $80\n", line_number, found_inverse);
+ if(deinverse) printf("; XXX inverse (%d chars)", found_inverse);
+ }
+ if(found_unprint) fprintf(stderr, SELF ": line %d contains %d non-printable ATASCII characters <= $1F\n", line_number, found_unprint);
+#endif
+
+#ifdef CYGWIN_NEWLINE_HACK
+ if(nl == '\n') putc('\r', output);
+#endif
+ putc(nl, output);
+
+ if(dump_tokens) {
+ fputs(dumpbuf, output);
+#ifdef CYGWIN_NEWLINE_HACK
+ if(nl == '\n') putc('\r', output);
+#endif
+ putc(nl, output);
+ }
+
+ if(ferror(output)) {
+ perror(SELF);
+ exit_cleanly(1);
+ }
+}
+
+void parse_lines() {
+ old_line_number = -1;
+
+ while(prog_bytes)
+ parse_one_line();
+
+ closeall();
+}
+
+#ifdef ATARI8
+/* get rid of trailing newline, make uppercase... */
+void cleanstring(char *s) {
+ while(*s) {
+ *s = toupper(*s);
+ if(*s == '\n') *s = 0;
+ ++s;
+ }
+}
+
+/* add .M65 if no extension entered, add leading D: if
+ no device name. */
+void fix_filename(char *src, char *dst, char *ext) {
+ if(!strrchr(src, '.'))
+ strcat(src, ext);
+
+ dst[0] = '\0';
+
+ if(!strrchr(src, ':'))
+ strcat(dst, "D:");
+
+ strcat(dst, src);
+}
+
+void prompt_for_opt(char *prompt, char *opt) {
+ char buffer[10];
+
+ printf("%s [%c/%c]?",
+ prompt,
+ (*opt ? 'Y' : 'y'),
+ (*opt ? 'n' : 'N'));
+
+ fflush(stdout);
+ fgets(buffer, 10, stdin);
+ cleanstring(buffer);
+
+ if(buffer[0] == 'Y' || buffer[0] == 'y')
+ *opt = 1;
+ else if(buffer[0] == 'N' || buffer[0] == 'n')
+ *opt = 0;
+ /* else leave unchanged */
+}
+
+void prompt_for_str(char *prompt, char *buf) {
+ fputs(prompt, stdout);
+ putc('?', stdout);
+ fflush(stdout);
+ fgets(buf, 120, stdin);
+ cleanstring(buf);
+}
+
+void atari8_get_opts() {
+ char buffer[128];
+ char other = 0;
+
+ prompt_for_str("M65 file or Return to quit", buffer);
+
+ if(!buffer[0]) {
+ prompt_for_opt("Really quit", &other);
+ if(other)
+ exit_cleanly(0);
+ else
+ other = 0;
+ }
+
+ fix_filename(buffer, infile, ".M65");
+
+ prompt_for_str("Output file (Return for E:)", buffer);
+
+ if(!buffer[0]) {
+ using_stdout = 1;
+ } else {
+ using_stdout = 0;
+ fix_filename(buffer, outfile, ".TXT");
+ }
+
+ prompt_for_opt("Set other options", &other);
+ if(other) {
+ prompt_for_opt("Omit line numbers", &no_numbers);
+ if(!no_numbers) {
+ prompt_for_opt("Renumber lines", &other);
+ if(other) {
+ prompt_for_str("Starting line number", buffer);
+ renum_start = atoi(buffer);
+ prompt_for_str("Line num increment (Return = 10)", buffer);
+ renum_incr = atoi(buffer);
+ if(renum_incr < 1) renum_incr = 10;
+ } else {
+ renum_incr = 0;
+ }
+ }
+ prompt_for_opt("Omit . (dots) from pseudo-ops", &omit_dots);
+
+ prompt_for_opt("Lowercase everything", &lcase_all);
+ if(!lcase_all)
+ prompt_for_opt("Lowercase mnemonics, hex", &lcase_opcodes);
+
+ prompt_for_opt("Replace leading spaces w/tabs", &leading_tabs);
+ if(!leading_tabs)
+ prompt_for_opt("Replace lead+inner spaces w/tabs", &all_tabs);
+
+ other = (chconsts_hex == CC_UNPRINT);
+ prompt_for_opt("Unprintable char consts to hex", &other);
+ if(other) {
+ chconsts_hex = CC_UNPRINT;
+ } else {
+ other = (chconsts_hex == CC_ALL);
+ prompt_for_opt("All char consts to hex", &other);
+ if(other)
+ chconsts_hex = CC_ALL;
+ else
+ chconsts_hex = CC_NONE;
+ }
+
+ if(chconsts_hex != CC_ALL)
+ prompt_for_opt("Close quote ' for char consts", &add_quote);
+
+ prompt_for_opt("Dump tokens in hex", &dump_tokens);
+ }
+
+ fflush(stdout);
+}
+#endif
+
+void usage() {
+ fprintf(stderr, "usage: " SELF " [options] inputfile\n\n");
+ fprintf(stderr, "options:\n");
+#ifndef ATARI8
+ fprintf(stderr, " -a Use ATASCII EOLs\n");
+ fprintf(stderr, " -c Convert non-printable char constants to hex\n");
+ fprintf(stderr, "-cc Convert all char constants to hex\n");
+#endif
+ fprintf(stderr, " -e nnn[,i] Renumber starting with nnn [increment i]\n");
+ fprintf(stderr, " -h Help (this text)\n");
+#ifndef ATARI8
+ fprintf(stderr, " -i Convert inverse video to normal\n");
+#endif
+ fprintf(stderr, " -l Lowercase mnemonics, hex constants\n");
+ fprintf(stderr, "-la Lowercase all, including comments\n");
+ fprintf(stderr, " -n No line numbers in output\n");
+ fprintf(stderr, " -o [file] Output to file (default = stdout)\n");
+ fprintf(stderr, " -p Omit leading . (period) from pseudo-ops\n");
+ fprintf(stderr, " -q Add closing quote (') to character constants\n");
+ fprintf(stderr, " -t Replace leading spaces with tabs\n");
+ fprintf(stderr, "-ta Replace spaces between all fields with tabs\n");
+ fprintf(stderr, " -v Verbose output (dump tokens)\n");
+#ifndef ATARI8
+ fprintf(stderr, " -m Print inverse video as underlined\n");
+ fprintf(stderr, " -r Print inverse video as ANSI reverse video\n");
+ fprintf(stderr, " -u Print ATASCII as Unicode/UTF-8\n");
+#endif
+ exit(1);
+}
+
+void get_renum_args(char *arg) {
+ renum_incr = 10;
+ /* atoi() doesn't detect errors, so: */
+ if(!arg || arg[0] > '9' || arg[0] < '0')
+ usage();
+ renum_start = atoi(arg);
+ arg = strchr(arg, ',');
+ if(arg) renum_incr = atoi(++arg);
+ if(!renum_incr || renum_incr < 0) usage();
+}
+
+/* TODO: support a few more -options
+ A lot of the fancier options I wanted to add, would require
+ a full parser for the grammar. I've avoided this partly because
+ it's more work, and partly because I dunno how well yacc/bison would
+ play with cc65...
+*/
+void handle_cli_opts(int argc, char **argv) {
+#ifdef ATARI8
+ infile[0] = '\0';
+#endif
+
+ while(++argv, --argc) {
+ if(argv[0][0] == '-') {
+ switch(tolower(argv[0][1])) {
+#ifndef ATARI8
+ case 'a':
+ nl = 0x9b;
+ if(argv[0][2]) usage();
+ break;
+
+ case 'i':
+ chconsts_hex = CC_UNPRINT;
+ deinverse = 1;
+ if(argv[0][2]) usage();
+ break;
+
+ case 'm':
+ inv_underscore = 1;
+ if(argv[0][2]) usage();
+ break;
+
+ case 'r':
+ inv_ansi = 1;
+ if(argv[0][2]) usage();
+ break;
+
+ case 'u':
+ unicode = 1;
+ if(argv[0][2]) usage();
+ break;
+#endif
+ case 'c':
+ chconsts_hex = CC_UNPRINT;
+ if(argv[0][2] == 'C' || argv[0][2] == 'c')
+ chconsts_hex = CC_ALL;
+ else if(argv[0][2]) usage();
+ break;
+
+ case 'e':
+ if(argv[0][2]) usage();
+ if(!argv[1]) usage();
+ get_renum_args(argv[1]);
+ argv++, argc--;
+ break;
+
+ case 'n':
+ no_numbers = 1;
+ if(argv[0][2]) usage();
+ break;
+
+ case 'h':
+ usage();
+ break;
+
+ case 'l':
+ lcase_opcodes = 1;
+ if(argv[0][2] == 'A' || argv[0][2] == 'a')
+ lcase_all = 1;
+ else if(argv[0][2]) usage();
+ break;
+
+ case 'p':
+ omit_dots = 1;
+ if(argv[0][2]) usage();
+ break;
+
+ case 'q':
+ add_quote = 1;
+ if(argv[0][2]) usage();
+ break;
+
+ case 't':
+ leading_tabs = 1;
+ if(argv[0][2] == 'A' || argv[0][2] == 'a')
+ all_tabs = 1;
+ else if(argv[0][2]) usage();
+ break;
+
+ case 'v':
+ dump_tokens = 1;
+ if(argv[0][2]) usage();
+ break;
+
+ case 'o':
+ if(argv[0][2]) {
+#ifdef ATARI8
+ strcpy(outfile, &argv[0][2]);
+#else
+ outfile = &argv[0][2];
+#endif
+ } else if(argc == 1) {
+ usage();
+ } else {
+ ++argv, --argc;
+#ifdef ATARI8
+ strcpy(outfile, argv[0]);
+#else
+ outfile = argv[0];
+#endif
+ }
+ using_stdout = 0;
+ break;
+
+ default:
+ usage();
+ break;
+ }
+ } else {
+#ifdef ATARI8
+ if(infile[0])
+ usage();
+ else
+ strcpy(infile, argv[0]);
+#else
+ if(infile)
+ usage();
+ else
+ infile = argv[0];
+#endif
+ }
+ }
+
+ if(!infile) usage();
+}
+
+int main(int argc, char **argv) {
+ fputs(BANNER, stderr);
+
+#ifdef ATARI8
+ while(1) {
+ if(argc < 2) {
+ atari8_get_opts();
+ } else {
+ handle_cli_opts(argc, argv);
+ argc = 1;
+ }
+#else
+ handle_cli_opts(argc, argv);
+#endif
+
+ input = fopen(infile, "rb");
+ if(!input) {
+ perror(infile);
+ exit_cleanly(1);
+ }
+
+ if(using_stdout) {
+ output = stdout;
+ } else {
+ output = fopen(outfile, "w");
+ if(!output) {
+ perror(outfile);
+ exit_cleanly(1);
+ }
+ }
+
+ if(renum_incr) renum_line = renum_start;
+ parse_header();
+ parse_lines();
+
+#ifdef ATARI8
+ }
+#endif
+
+ exit_cleanly(0);
+ return 0; /* to shut gcc up... */
+}
diff --git a/unmac65.rst b/unmac65.rst
new file mode 100644
index 0000000..5330828
--- /dev/null
+++ b/unmac65.rst
@@ -0,0 +1,324 @@
+.. RST source for unmac65(1) man page. Convert with:
+.. rst2man.py unmac65.rst > unmac65.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+=======
+unmac65
+=======
+
+------------------------------------------
+Detokenize Atari 8-bit Mac/65 SAVEd files.
+------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+**unmac65** [*-options*] *file.m65*
+
+DESCRIPTION
+===========
+
+**unmac65** reads files created with Mac/65's SAVE command (usually called
+.M65 files) and converts them back to plain text assembly source.
+
+OPTIONS
+=======
+
+-a
+ Use ATASCII EOLs. This option is not available in Atari version,
+ since it already uses ATASCII.
+
+-c
+ Convert non-printable characters constants to hex bytes.
+
+**-cc**
+ Convert all character constants to hex bytes.
+
+-e nnn[,iii]
+ Renumber the program, starting at line *nnn*, with increment *iii*.
+ *nnn* must be present, and must be an integer greater than or equal
+ to zero. *iii* is optional, must be positive (non-zero), and
+ defaults to 10 if not given.
+
+ Mac/65's maximum line number is 65535. unmac65 will happily renumber
+ lines with no upper bound (other than unsigned int overflow), so pay
+ attention.
+
+-h
+ Show command-line help.
+
+-i
+ Convert inverse video (in comments and strings) to standard ASCII.
+ Lines that were converted will get a comment "*; XXX inverse*" at
+ the end. This option also enables the -c option.
+
+ If the program contained any inverse-video strings, the resulting
+ output will *not* reassemble correctly; you'll have to edit it to
+ e.g. change the formerly inverse video strings to a list of hex
+ bytes.
+
+ This option is not available in the Atari version.
+
+-l
+ Lowercase mnemonics and hex constants (but not labels or comments).
+
+**-la**
+ Lowercase mnemonics, hex constants, labels, and comments (but not
+ strings or character constants).
+
+-n
+ No line numbers in output.
+
+-o file
+ Output to *file* (default = standard output).
+
+-q
+ Add closing single-quote to character constants. Changes this::
+
+ LDA #'A
+
+ ...to this::
+
+ LDA #'A'
+
+-p
+ Omit leading . (period) from pseudo-ops (e.g. print BYTE for .BYTE).
+
+-t
+ Replace leading spaces with tabs.
+
+**-ta**
+ Replace spaces between all fields with tabs.
+
+-v
+ Verbose output (dump tokens in hex). Useful for examining damaged
+ .M65 files, or debugging unmac65 itself.
+
+Human-readable Output Options
+-----------------------------
+
+The -m, -r, and -u options are not available for the Atari, and may or
+may not be useful on non-Linux OSes.
+
+-m
+ Print inverse video as pseudo-underlined, using backspace and
+ underscore. Useful for piping to more(1) or less(1). Can be combined
+ with -u.
+
+-r
+ Print inverse video as reverse video using xterm/ANSI compatible
+ escape sequences. Can be combined with -u. Useful for piping to
+ less(1) provided its -r or -R option is used.
+
+-u
+ Print ATASCII control characters as their nearest Unicode
+ equivalents (encoded in UTF-8). Depending on your terminal,
+ combining this option with -r may not work properly. Also, depending
+ on the font(s) your terminal is using, you may see boxes instead of
+ control characters. If this happens, try a different font, or a
+ different terminal (the author recommends rxvt-unicode).
+
+Options may not be bundled (use "-p -t", not "-pt").
+
+Unlike most UNIX-flavored programs, the CLI options are
+case-insensitive. This is to make life easier for users of the Atari
+version, where uppercase is the normal way of doing things.
+
+The -c, -cc, -l, -la, -n, -p, -t, -ta options are provided to assist in
+porting Mac/65 programs to other assemblers, such as ca65 or dasm.
+unmac65's output with none of these options (or with -n only) is
+acceptable as input for the atasm assembler. This is true even if there
+are inverse video strings: they look funny when viewing the file, but
+atasm handles them correctly.
+
+The -v option prints the hex bytes for each line (preceded by ";;" and
+the line number) after that line's detokenized listing.
+
+Note that the output from -m, -r, -u is intended for humans to read.
+They're not very useful if you're trying to port Mac/65 code to a
+different assembler, as none of them know what to do with the
+underlines, ANSI codes, or pseudo-ATASCII Unicode characters.
+
+FILE FORMAT
+===========
+
+A tokenized Mac/65 file consists of:
+
+Header: 2 byte $FE $FE signature, followed by the 2 byte program length
+in LSB/MSB format. Length doesn't include the 4 header bytes.
+
+The rest of the file consists of lines of code. Each line is:
+
+Line number, 2 bytes (LSB/MSB format)
+
+Line length, 1 byte. Total length, including the line number and length
+bytes.
+
+Tokens. Length minus 3 bytes of tokens. If the line is labelled, the
+label will appear first, as a tokenized string (see below).
+
+Whether or not there's a label, the next byte is the token for a
+mnemonic (or pseudo-op). Lines containing only a comment will have a
+special token meaning "no mnemonic".
+
+After the mnemonic token, 0 or more bytes of operands. Quoted strings or
+labels as operands are stored as a tokenized string. Hex or decimal
+constants are preceded by a token indicating the length (one or two
+bytes) and the type (hex or decimal).
+
+If there is a comment, the last byte of the operand field will be an
+ASCII semicolon. The remaining bytes on the line are the comment in
+ASCII form.
+
+Tokenized strings can occur in the label or operand parts of the line.
+The first byte is the length of the string in bytes, with the high bit
+set (e.g. $84 for a 4-byte string), followed by that number of ASCII
+bytes. The length doesn't include the first byte, so e.g. the string
+"ABC" is stored as $83, $41, $42, $43. Mac/65 doesn't allow empty
+strings, so a zero-length string (length byte $80) is an error.
+
+There are separate sets of tokens for mnemonics/pseudo-ops and operands.
+Mnemonic/pseudo-op tokens run from 0 to $5F, and operand tokens run from
+0 to $4D (with 0-$09 being "special", and a few invalid tokens in the
+range $0A-$4D). See the C source for the full list (or a hex/ascii dump
+of the Mac/65 ROM, which is where I got the lists to put them in the C
+source). Also, you can run unmac65 with the -v option to get a
+line-by-line hex dump of the tokens.
+
+DIAGNOSTICS
+===========
+
+unmac65: line XX contains NN non-printable ATASCII characters <= $1F
+
+Self-explanatory. Depending on what you're going to use the converted
+file for, this may or may not be a problem. Non-fatal. This warning
+doesn't occur in the Atari version of unmac65. Also, it doesn't occur if
+the -u option is in use.
+
+unmac65: line XX contains NN inverse ATASCII characters >= $80
+
+Self-explanatory. Depending on what you're going to use the converted
+file for, this may or may not be a problem. Non-fatal. Use the -i option
+to convert inverse video to normal. This warning doesn't occur in the
+Atari version of unmac65. Also, it doesn't occur if any of the -i, -m,
+or -r options are in use.
+
+unmac65: not a mac/65 file (missing $FEFE header)
+
+Self-explanatory. Fatal error.
+
+unmac65: corrupt or truncated file?
+
+The length of the last line in the file is longer that the number of
+bytes remaining in the file (according to the file header). Fatal error.
+
+unmac65: unexpected EOF?
+
+The file is shorter than the file header's program length. Probably the
+file is truncated; less probably, the length header got scrambled
+somehow. Fatal error.
+
+unmac65: file is too short (N bytes)
+
+The minimum length for a Mac/65 file is 4 bytes (which would be an empty
+program containing no lines). The input file was shorter than 4 bytes.
+Fatal error. (Actually, Mac/65 will never produce a 4-byte file, it's
+just the theoretical minimum)
+
+unmac65: file is valid but contains no lines of code
+
+Self-explanatory. Mac/65 creates a file like this if the SAVE command is
+given before entering any code (at startup or after a NEW). The SAVEd
+file will be 5 bytes in length, and utterly useless. Non-fatal warning.
+
+unmac65: line #lll <= prev line #mmm
+
+The line numbers in the file are supposed to be stored in ascending
+order. Somehow this file has the line numbers out of order. Non-fatal,
+but probably the rest of the file will be garbage.
+
+unmac65: internal error, state n
+
+This is a "this should never happen" error. It indicates a bug in the
+program. If you ever see this error, please notify the author, and send
+a copy of the Mac/65 program that caused it. This is a non-fatal error,
+but the output might be garbage.
+
+[$nn?] or <$nn?> in the output (where nn is 2 hex digits)
+
+These indicate unknown/invalid tokens. Either the file is damaged, or
+there is a bug in the program. These are non-fatal errors. If you ever
+see them, please contact the author, and send a copy of the Mac/65
+program that caused them.
+
+unmac65: ignoring extra junk at EOF
+
+The file contains more bytes than the program length header says it
+should. This usually means the file was stored on an old DOS disk or
+transferred with a broken XMODEM implementation, and was padded to the
+sector/block size. Alternately, the header bytes got corrupted somehow
+(this is highly unlikely, especially if there are no other
+errors/warnings). Non-fatal error.
+
+Other errors are possible (e.g. disk full, I/O error reading input), but
+they're not specific to unmac65; no need to list them all there.
+
+Fatal errors result in unmac65 terminating. A non-fatal error can
+usually be recovered from, though the line that caused it will probably
+be printed strangely.
+
+It's probably worth mentioning also that Mac/65 source files can contain
+ATASCII graphics or escape codes, although it's not a very common
+practice. If you see strange stuff and/or your terminal misbehaves when
+writing to standard output, try writing to a file (-o option) instead.
+See also the -m, -r, -u options for human-readable output. The Atari
+version will render ATASCII graphics just fine, of course.
+
+EXIT STATUS
+===========
+
+unmac65 will normally exit with a zero (success) status upon completion.
+A non-zero status indicates a fatal error.
+
+LIMITATIONS
+===========
+
+The main difference between Mac/65's LIST output and unmac65's output is
+that Mac/65 lines up the label, mnemonic, and comment fields (if the
+field contents are short enough to fit in the allotted width), while
+unmac65 makes no attempt to do so. If the field alignment is important
+to you, try the -t or -ta options (which insert hard tabs, unlike
+mac65's spaces). A future version of unmac65 may correct this minor
+flaw, but as it stands, I've tested quite a few .M65 files by running
+them through unmac65, then ENTERing unmac65's listed file in Mac/65 and
+reSAVEing them... In all cases, the newly created tokenized files
+compare identically to the original .M65 file. If you come across a file
+that doesn't do this, yet is valid (can be assembled without error by
+Mac/65), please send it to me, so I can fix unmac65!
+
+Although a few helpful options have been added for porting Mac/65
+sources to other assemblers, unmac65 doesn't completely automate the
+process. Depending on the assembler you're using, you may still have a
+lot of manual edits to make. A future version of unmac65 may add a few
+more options, but some hypothetical complex porting functions (like
+"convert to ca65 format") would require implementing a much more complex
+parser (such as a yacc-based recursive descent parser). Most likely this
+will never happen, if only because I want the program to be usable on
+the Atari itself, with its limited memory and C compiler support.
+
+There are a few ways that an invalid file can sneak past unmac65's error
+checking. The same files wouldn't load correctly in Mac/65 either, but
+generally don't cause any errors in Mac/65 (just silent failure). It'd
+be nice if unmac65 could act as a "M65 lint" for Mac/65 users.
+
+unmac65 is mostly developed/tested against the OSS Mac/65 v1.01
+cartridge. The various disk versions appear to use the same tokenized
+format, but haven't been well tested. If you have problems, please
+contact the author.
+
+(A further limitation is that the documentation isn't very concise.
+Sorry about that.)
+
+.. include:: manftr.rst
diff --git a/ver.rst b/ver.rst
new file mode 100644
index 0000000..6f54bd4
--- /dev/null
+++ b/ver.rst
@@ -0,0 +1 @@
+.. |version| replace:: 0.2.1
diff --git a/xex.c b/xex.c
new file mode 100644
index 0000000..e4b66ce
--- /dev/null
+++ b/xex.c
@@ -0,0 +1,313 @@
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+
+#include "xex.h"
+
+int xex_errno;
+static int xex_sys_errno;
+
+int xex_verbose = 0;
+
+static char *errors[] = {
+ "OK", /* XERR_NONE 0 */
+ "Failed system call", /* XERR_SYSCALL 1 */
+ "End address < start address", /* XERR_REVERSED 2 */
+ "Truncated segment", /* XERR_TRUNCATED 3 */
+ "No data", /* XERR_NULLDATA 4 */
+ "Unknown/Invalid XEX error code", /* XERR_MAXERR 5 */
+};
+
+
+int xex_new_seg(xex_segment *seg, unsigned char *data) {
+ int offset = 0;
+
+ if(data[0] == 0xff && data[1] == 0xff) {
+ offset = 2;
+ seg->has_ff_header = 1;
+ } else {
+ seg->has_ff_header = 0;
+ }
+
+ seg->start_addr = data[offset] + (data[offset + 1] << 8);
+ offset += 2;
+
+ seg->end_addr = data[offset] + (data[offset + 1] << 8);
+ offset += 2;
+
+ seg->object = &data[offset];
+
+ seg->len = seg->end_addr - seg->start_addr + 1;
+
+ return xex_check_seg(seg);
+}
+
+int xex_init_seg(xex_segment *seg, unsigned char *object, unsigned short addr) {
+ object[0] = XEX_LSB(addr);
+ object[1] = XEX_MSB(addr);
+
+ seg->start_addr = XEX_INITAD;
+ seg->end_addr = XEX_INITAD + 1;
+ seg->object = object;
+ seg->len = 2;
+ seg->has_ff_header = 0;
+
+ if(xex_verbose) {
+ fprintf(stderr, "xex_init_seg(): created init segment @ %04X:\n", addr);
+ xex_print_seg_info(seg);
+ }
+
+ return 1;
+}
+
+int xex_run_seg(xex_segment *seg, unsigned char *object, unsigned short addr) {
+ object[0] = XEX_LSB(addr);
+ object[1] = XEX_MSB(addr);
+
+ seg->start_addr = XEX_RUNAD;
+ seg->end_addr = XEX_RUNAD + 1;
+ seg->object = object;
+ seg->len = 2;
+ seg->has_ff_header = 0;
+
+ if(xex_verbose) {
+ fprintf(stderr, "xex_run(): created run segment @ %04X:\n", addr);
+ xex_print_seg_info(seg);
+ }
+
+ return 1;
+}
+
+static int read_char(FILE *file) {
+ int c;
+
+ c = getc(file);
+ if(c == EOF) {
+ if(ferror(file)) {
+ xex_sys_errno = errno;
+ xex_errno = XERR_SYSCALL;
+ } else {
+ xex_errno = XERR_TRUNCATED;
+ }
+ }
+
+ return c;
+}
+
+int xex_fread_seg_header(xex_segment *seg, FILE *file) {
+ int c, d;
+ unsigned short addr;
+
+ xex_errno = XERR_NONE;
+
+ seg->has_ff_header = 0;
+ seg->object = NULL;
+
+ c = read_char(file);
+ if(c == EOF) {
+ if(feof(file))
+ xex_errno = XERR_NONE;
+
+ return 0;
+ }
+ d = read_char(file);
+ if(d == EOF) return 0;
+
+ addr = XEX_ADDR(c, d);
+ if(addr == 0xffff) {
+ seg->has_ff_header = 1;
+ c = read_char(file);
+ if(c == EOF) return 0;
+ d = read_char(file);
+ if(d == EOF) return 0;
+ addr = XEX_ADDR(c, d);
+ }
+
+ if(addr == 0xffff) {
+ xex_errno = XERR_REVERSED;
+ return 0;
+ }
+
+ seg->start_addr = addr;
+
+ c = read_char(file);
+ if(c == EOF) return 0;
+ d = read_char(file);
+ if(d == EOF) return 0;
+ addr = XEX_ADDR(c, d);
+ seg->end_addr = addr;
+
+ seg->len = seg->end_addr - seg->start_addr + 1;
+
+ if(seg->end_addr < seg->start_addr) {
+ xex_errno = XERR_REVERSED;
+ return 0;
+ }
+
+ if(xex_verbose) {
+ fprintf(stderr, "xex_fread_seg_header(): read header:\n");
+ xex_print_seg_info(seg);
+ }
+
+ return 1;
+}
+
+int xex_fread_seg_data(xex_segment *seg, FILE *file) {
+ int res;
+
+ xex_errno = XERR_NONE;
+
+ res = fread(seg->object, 1, seg->len, file);
+ xex_sys_errno = errno;
+
+ if(xex_verbose) {
+ fprintf(stderr, "xex_fread_seg_data(): read data:\n");
+ xex_print_seg_info(seg);
+ }
+
+ if(res == seg->len)
+ return 1;
+
+ if(ferror(file)) {
+ xex_errno = XERR_SYSCALL;
+ } else {
+ /* EOF or short read */
+ xex_errno = XERR_TRUNCATED;
+ }
+
+ return xex_check_seg(seg);
+}
+
+int xex_fread_seg(xex_segment *seg, FILE *file) {
+ unsigned char *tmp = seg->object;
+ int res;
+
+ xex_errno = XERR_NONE;
+
+ if(tmp == NULL) {
+ if(xex_verbose)
+ fprintf(stderr, "xex_fread_seg(): seg->object == NULL\n");
+ xex_errno = XERR_NULLDATA;
+ return 0;
+ }
+
+ res = xex_fread_seg_header(seg, file);
+ seg->object = tmp;
+
+ if(!res)
+ return 0;
+
+ if(!xex_fread_seg_data(seg, file))
+ return 0;
+
+ return 1;
+}
+
+int xex_fwrite_seg(xex_segment *seg, FILE *file) {
+ int res;
+
+ xex_errno = XERR_NONE;
+
+ if(xex_verbose) {
+ fprintf(stderr, "xex_fwrite_seg(): about to write:\n");
+ xex_print_seg_info(seg);
+ }
+
+ if(!xex_check_seg(seg))
+ return 0;
+
+ if(seg->has_ff_header) {
+ putc(0xff, file);
+ putc(0xff, file);
+ }
+
+ putc(XEX_LSB(seg->start_addr), file);
+ putc(XEX_MSB(seg->start_addr), file);
+ putc(XEX_LSB(seg->end_addr), file);
+ putc(XEX_MSB(seg->end_addr), file);
+
+ res = fwrite(seg->object, 1, seg->len, file);
+ if(res == seg->len)
+ return 1;
+
+ if(ferror(file)) {
+ xex_errno = XERR_SYSCALL;
+ xex_sys_errno = errno;
+ } else {
+ xex_errno = XERR_TRUNCATED;
+ }
+
+ return 0;
+}
+
+int xex_get_object(xex_segment *seg, unsigned char *data) {
+ int offset = 0;
+
+ if(!xex_check_seg(seg))
+ return 0;
+
+ if(seg->has_ff_header) {
+ data[0] = data[1] = 0xff;
+ offset = 2;
+ }
+
+ data[offset] = XEX_LSB(seg->start_addr);
+ data[offset + 1] = XEX_MSB(seg->start_addr);
+ offset += 2;
+ data[offset] = XEX_LSB(seg->end_addr);
+ data[offset + 1] = XEX_MSB(seg->end_addr);
+ offset += 2;
+
+ memcpy(&data[offset], seg->object, seg->len);
+
+ return 1;
+}
+
+int xex_check_seg(xex_segment *seg) {
+ int ret = 1;
+
+ xex_errno = XERR_NONE;
+
+ if(seg->end_addr < seg->start_addr) {
+ xex_errno = XERR_REVERSED;
+ ret = 0;
+ }
+
+ if(seg->len != (seg->end_addr - seg->start_addr) + 1) {
+ xex_errno = XERR_TRUNCATED;
+ ret = 0;
+ }
+
+ if(seg->object == NULL) {
+ xex_errno = XERR_NULLDATA;
+ ret = 0;
+ }
+
+ if(xex_verbose && !ret && xex_errno != XERR_NULLDATA)
+ fprintf(stderr, "xex_check_seg() FAILED: %s\n", xex_strerror(xex_errno));
+
+ return ret;
+}
+
+char *xex_strerror(int err) {
+ if(err < 0 || err >= XERR_MAXERR)
+ err = XERR_MAXERR;
+
+ if(err == XERR_SYSCALL)
+ return strerror(xex_sys_errno);
+ else
+ return errors[err];
+}
+
+void xex_perror(char *msg) {
+ fprintf(stderr, "%s: %s\n", msg, xex_strerror(xex_errno));
+}
+
+void xex_print_seg_info(xex_segment *seg) {
+ xex_check_seg(seg);
+ fprintf(stderr,
+ "has_ff_header==%d start==%04X end==%04X "
+ "len==%04X status==%s\n",
+ seg->has_ff_header, seg->start_addr, seg->end_addr,
+ seg->len, xex_strerror(xex_errno));
+}
diff --git a/xex.h b/xex.h
new file mode 100644
index 0000000..2300481
--- /dev/null
+++ b/xex.h
@@ -0,0 +1,117 @@
+#ifndef XEX_H
+#define XEX_H
+
+/* A xex_segment represents one segment of a XEX file. */
+typedef struct {
+ unsigned short start_addr;
+ unsigned short end_addr;
+ unsigned char *object;
+ int len;
+ unsigned char has_ff_header;
+} xex_segment;
+
+/* object points to an array containing ONLY the data bytes, not the
+ header bytes! Use xex_get_object() to reconstitute the full
+ segment with headers.
+
+ (Note that I'm using the word "object" in the sense of "object code",
+ not in its OOP sense)
+ */
+
+#define XEX_RUNAD 0x2e0
+#define XEX_INITAD 0x2e2
+
+#define XEX_LSB(x) (x & 0xff)
+#define XEX_MSB(x) ((x >> 8) & 0xff)
+#define XEX_ADDR(x, y) ((unsigned char)x | ((unsigned char)y << 8))
+
+/* All int functions return true for success or false for failure.
+ On failure, the variable xex_errno will be set to one of the
+ XERR_* constants. Caller may use xex_strerror(xex_errno) to
+ get a human-readable error message (or xex_perror()).
+
+ Note that XERR_NONE == 0, so you can use the construct:
+ if(xex_errno) {
+ // handle error here
+ }
+ */
+
+#define XERR_NONE 0
+#define XERR_SYSCALL 1
+#define XERR_REVERSED 2
+#define XERR_TRUNCATED 3
+#define XERR_NULLDATA 4
+#define XERR_MAXERR 5
+
+extern int xex_errno;
+extern int xex_verbose;
+
+/* Initialize a xex_segment from the data pointed to by segment, which must
+ not be null. segment must be a complete segment, including start/end
+ addresses and possible $FFFF header. seg->object will point within the
+ data, so don't free() it until you're done with seg! */
+int xex_new_seg(xex_segment *seg, unsigned char *segment);
+
+/* Set up seg as an init address segment. object must have room for at
+ least 2 characters. A copy of the object pointer is stored in
+ seg->object, so don't free(object) until you're done with seg. */
+int xex_init_seg(xex_segment *seg, unsigned char *object, unsigned short addr);
+
+/* Set up seg as an run address segment. object must have room for at
+ least 2 characters. A copy of the object pointer is stored in seg->object,
+ so don't free(object) until you're done with seg.*/
+int xex_run_seg(xex_segment *seg, unsigned char *object, unsigned short addr);
+
+/* Since this library does NOT allocate any memory, reading a segment must
+ be done in two pieces. xex_fread_seg_header() reads the 4- or 6-byte
+ header and sets seg->len to the size of the segment. xex_fread_seg_data()
+ then reads this number of bytes from the file, into seg->object.
+
+ Example usage:
+
+ xex_segment seg;
+
+ xex_fread_seg_header(&seg, my_file);
+ seg.object = malloc(seg->len);
+ xex_fread_seg_data(&seg, my_file);
+
+ // ...later, the caller must free(seg.object) when done with it.
+
+ xex_fread_seg_header() returns 0 on EOF, with xex_errno set to
+ XERR_NONE. It also returns 0 on failure, but xex_errno will be
+ something other than XERR_NONE.
+*/
+int xex_fread_seg_header(xex_segment *seg, FILE *file);
+int xex_fread_seg_data(xex_segment *seg, FILE *file);
+
+/* If you're dealing with xex files sequentially, and want to use a
+ static buffer instead of malloc(), you can use xex_fread_seg() instead.
+ seg->object should point to a buffer large enough to hold any segment's
+ object code (64K, to be on the safe side), or you WILL get segfaults. */
+int xex_fread_seg(xex_segment *seg, FILE *file);
+
+/* xex_fwrite_seg() writes a segment to a file, which must be opened
+ for output. */
+int xex_fwrite_seg(xex_segment *seg, FILE *file);
+
+/* Extract the object code from a segment. data must have room for len+6
+ bytes (or len+4 if has_ff_header is false). data will be the raw
+ segment data, suitable for writing to a XEX file. */
+int xex_get_object(xex_segment *seg, unsigned char *data);
+
+/* Sanity check a segment. Make sure start_addr is less than end_addr,
+ that it doesn't wrap around the 6502 address space, and so on.
+ Returns true if segment is OK. */
+int xex_check_seg(xex_segment *seg);
+
+/* Get human-readable error message. If xex_errno is XSYSCALL, the system's
+ strerror() is called. */
+char *xex_strerror(int err);
+
+/* Shortcut for xex_strerror(), like system's perror() */
+void xex_perror(char *msg);
+
+/* Debugging aid */
+void xex_print_seg_info(xex_segment *seg);
+
+#endif /* XEX_H */
diff --git a/xexcat.1 b/xexcat.1
new file mode 100644
index 0000000..5cb659c
--- /dev/null
+++ b/xexcat.1
@@ -0,0 +1,181 @@
+.\" 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 "XEXCAT" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py xexcat.rst > xexcat.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fIxexcat\fP [\fI\-hvc\fP] [\-l \fIaddress\fP [\-r \fIaddress\fP] [\-i \fIaddress\fP] [\-o \fIoutfile.xex\fP] [\fIinfile.xex\fP] [\fIinfile.xex ...\fP]
+.SH DESCRIPTION
+.sp
+\fBxexcat\fP reads one or more Atari executables (XEX/BIN/COM/etc)
+from the given filenames, and writes a single Atari executable
+containing all the segments from all the input files to \fIoutfile\fP\&.
+.sp
+To read from standard input, \fIinfile\fP may be omitted, or given as
+\fB\-\fP\&. To write to standard output, \fB\-o\fP \fIoutfile\fP may be omitted,
+or given as \fB\-o\-\fP\&.
+.sp
+The output file is a valid Atari executable, including the
+required \fI$FFFF\fP header for the first segment. If there are multiple
+segments, the second and subsequent segments will not have the
+optional \fI$FFFF\fP header.
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \-h
+Print a short help message and exit.
+.TP
+.B \-v
+Verbose operation. Each segment\(aqs information is printed to
+standard error, including start/end address and length.
+.TP
+.B \-c
+Check only; no output file is written. Equivalent to \fB\-v \-o /dev/null\fP\&.
+.TP
+.BI \-o \ outfile
+Write output xex file to outfile. Default is to write to standard output.
+.TP
+.BI \-l \ address
+Force the output file\(aqs load address to address. This only
+affects the first segment of the output file.
+.TP
+.BI \-i \ address
+Force the output file\(aqs first init address (if present) to
+\fIaddress\fP\&. This \fIonly\fP affects the \fBfirst\fP init address segment of the
+output file. Further init address segments in the input will be
+left unmodified. If \fIaddress\fP is 0, the first init segment will
+be removed (0 means "none", not "init at address 0"). This option
+does nothing if none of the input files contain init address
+segments.
+.TP
+.BI \-r \ address
+Force the output file\(aqs run address to \fIaddress\fP\&. If \fIaddress\fP
+is not 0, all run address segments from all input files will be
+ignored, and a new run address segment will be constructed
+with the given \fIaddress\fP and appended to the output. If \fIaddress\fP
+is 0, all run addresses from all input files are ignored,
+and the output file will not contain a run address segment
+at all. Such a file can still be loaded from DOS, but it will
+not execute (user will be returned to the DOS menu).
+.UNINDENT
+.SH NOTES
+.sp
+It is possible to join multiple Atari executables together with
+the standard \fBcat\fP(1) command. However, \fBxexcat\fP is always guaranteed to
+produce a valid Atari binary load file (or an empty file, if all input
+files are invalid), which is not the case for \fBcat\fP\&.
+.sp
+When writing to standard output, \fBxexcat\fP will refuse to write
+binary data to the user\(aqs terminal.
+.sp
+The Atari binary load format requires the \fI$FFFF\fP header only for
+the first segment in a file. The second and subsequent segments
+may also have a \fI$FFFF\fP header, but it\(aqs optional. \fBxexcat\fP\(aqs output file
+will always have the \fI$FFFF\fP header for the first segment, and no
+\fI$FFFF\fP header for further segments, regardless of whether the segments
+in the input files had it or not (in fact, \fBxexcat\fP can handle
+an invalid XEX file which is missing the initial $FFFF header for the
+first segment).
+.sp
+Some Atari executables contain raw blocks of data, which are meant
+to be read into memory by the init routine. These blocks do not
+have start/end address headers, so \fBxexcat\fP is unable to handle
+them. Raw data blocks usually occur in files created with "packer"
+or "compressor" programs, or occasionally in other large programs
+(Turbo BASIC is an example). Raw data blocks are generally found just
+after an init address segment. If you have an executable that loads
+just fine on a real Atari or emulator, but fails with \fBxexcat\fP,
+a raw data block is usually the reason why.
+.sp
+The terms "Atari executable", "binary load file", and "XEX file"
+all refer to the same thing. Also, there is no difference between
+Atari executables named with "XEX", "COM", "BIN", "EXE", etc. The
+Atari and its DOS don\(aqt care about the names, only the contents.
+.SH EXIT STATUS
+.sp
+Exit status is zero for success, non\-zero for failure.
+.\" other sections we might want, uncomment as needed.
+.
+.\" FILES
+.
+.\" =====
+.
+.\" ENVIRONMENT
+.
+.\" ===========
+.
+.\" EXIT STATUS
+.
+.\" ===========
+.
+.\" BUGS
+.
+.\" ====
+.
+.\" EXAMPLES
+.
+.\" ========
+.
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/xexcat.c b/xexcat.c
new file mode 100644
index 0000000..22d8276
--- /dev/null
+++ b/xexcat.c
@@ -0,0 +1,248 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+
+#include "get_address.h"
+#include "xex.h"
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "xexcat"
+#define OPTIONS "hvo:l:r:i:c"
+
+char *usage =
+ SELF " v" VERSION " - by B. Watson (WTFPL)\n"
+ "Join one or more Atari 8-bit executables into one multi-segment file.\n"
+ "usage: " SELF " -[hvc] [-l address] [-i address] [-r address]\n"
+ " [-o outfile.xex] [infile1.xex] [infile2.xex ...]\n"
+ " -h Print this help\n"
+ " -o outfile.xex Output file (default: standard output)\n"
+ " -v Verbose operation\n"
+ " -c Check only; no output (same as -v -o/dev/null)\n"
+ " -l address Force first load address (decimal, $hex, or 0xhex)\n"
+ " -i address Force first init address\n"
+ " -r address Force run address\n";
+
+int main(int argc, char **argv) {
+ xex_segment seg;
+ char *outfile = "-";
+ FILE *in, *out = stdout;
+ int count = 1, c, errcount = 0;
+ unsigned char buffer[65536]; /* be lazy: statically allocate large buffer */
+ int force_load = -1, force_run = -1, force_init = -1;
+ int read_stdin = 0;
+
+ /* parse args */
+ while( (c = getopt(argc, argv, OPTIONS)) > 0) {
+ switch(c) {
+ case 'h':
+ printf(usage);
+ exit(0);
+ break;
+
+ case 'v':
+ xex_verbose = 1;
+ break;
+
+ case 'o':
+ outfile = optarg;
+ break;
+
+ case 'c':
+ xex_verbose = 1;
+ outfile = "/dev/null";
+ break;
+
+ case 'l':
+ if( (force_load = get_address(SELF, optarg)) < 0 )
+ exit(1);
+ break;
+
+ case 'r':
+ if( (force_run = get_address(SELF, optarg)) < 0 )
+ exit(1);
+ break;
+
+ case 'i':
+ if( (force_init = get_address(SELF, optarg)) < 0 )
+ exit(1);
+ break;
+
+ default:
+ fprintf(stderr, usage);
+ exit(1);
+ }
+ }
+
+ /* special case if there are no input filenames */
+ if(argc <= optind) {
+ read_stdin = 1;
+ }
+
+ /* open outfile */
+ if(strcmp(outfile, "-") != 0) {
+ if( !(out = fopen(outfile, "wb")) ) {
+ fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno));
+ exit(1);
+ }
+ } else if(isatty(fileno(out))) {
+ /* be polite... */
+ fprintf(stderr,
+ SELF ": Standard output is a terminal; not writing binary data\n"
+ "Run '" SELF " -h' for help\n");
+ exit(1);
+ } else {
+ outfile = "(standard output)";
+ }
+
+ /* only have to set seg.object once... */
+ seg.object = buffer;
+
+ /* process each input file on the command line */
+ while(read_stdin || (optind < argc)) {
+ char *infile = argv[optind++];
+ int filecount = 1;
+
+ if(read_stdin || strcmp(infile, "-") == 0) {
+ read_stdin = 0;
+ in = stdin;
+ infile = "(standard input)";
+ } else if( !(in = fopen(infile, "rb")) ) {
+ /* failure to open is NOT fatal (just skip it and move on) */
+ fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno));
+ errcount++;
+ continue;
+ }
+
+ if(xex_verbose)
+ fprintf(stderr, SELF ": reading from file %s\n", infile);
+
+ /* process every segment in current input file */
+ while(xex_fread_seg(&seg, in)) {
+ if(filecount++ == 1 && !seg.has_ff_header) {
+ fprintf(stderr, SELF ": warning: '%s' first segment "
+ "lacks $FFFF header (bad/partial XEX file?)\n",
+ infile);
+ }
+
+ /* normalize the $FFFF headers: only the first segment needs one */
+ seg.has_ff_header = (count == 1);
+
+ /* process -l option */
+ if(count == 1 && force_load > -1) {
+ if(xex_verbose)
+ fprintf(stderr,
+ SELF ": %s: setting first load address to %04X "
+ "due to -l option\n",
+ infile, force_load);
+ seg.start_addr = force_load;
+ seg.end_addr = force_load + seg.len - 1;
+ force_load = -1;
+ }
+
+ count++;
+
+ /* process -i option */
+ if(seg.start_addr == XEX_INITAD && seg.len == 2) {
+ if(force_init == 0) {
+ if(xex_verbose)
+ fprintf(stderr,
+ SELF ": %s: "
+ "skipping first init address segment due to -i0\n",
+ infile);
+ force_init = -1;
+ continue;
+ } else if(force_init > 0) {
+ if(xex_verbose)
+ fprintf(stderr,
+ SELF ": %s: "
+ "setting first init address to %04X due to -i option\n",
+ infile, force_init);
+ seg.object[0] = XEX_LSB(force_init);
+ seg.object[1] = XEX_MSB(force_init);
+ force_init = -1;
+ }
+
+ if(xex_verbose)
+ fprintf(stderr,
+ SELF ": %s: init address: %04X\n",
+ infile, XEX_ADDR(seg.object[0], seg.object[1]));
+ }
+
+ /* process -r option */
+ if(seg.start_addr == XEX_RUNAD && seg.len == 2) {
+ if(force_run > -1) {
+ if(xex_verbose)
+ fprintf(stderr,
+ SELF ": %s: "
+ "skipping run address segment due to -r option\n",
+ infile);
+ continue;
+ } else {
+ if(xex_verbose)
+ fprintf(stderr,
+ SELF ": %s: "
+ "run address: %04X\n",
+ infile, XEX_ADDR(seg.object[0], seg.object[1]));
+ }
+ }
+
+ /* write (possibly modified) segment to output */
+ if(!xex_fwrite_seg(&seg, out)) {
+ fprintf(stderr, SELF ": %s: %s\n",
+ outfile, xex_strerror(xex_errno));
+ xex_errno = 0;
+ errcount++;
+ break;
+ }
+ }
+
+ /* xex_errno will be 0 for XERR_NONE (meaning "no error") */
+ if(xex_errno) {
+ fprintf(stderr, SELF ": %s: %s\n",
+ infile, xex_strerror(xex_errno));
+ errcount++;
+ } else if(filecount == 1) {
+ fprintf(stderr, SELF ": warning: %s: file is empty.\n", infile);
+ }
+
+ if(xex_verbose)
+ fprintf(stderr, SELF ": done reading file %s\n", infile);
+
+ fclose(in);
+ }
+
+ /* if -r given, all run addresses in all files were omitted from the
+ output file. Here we add a single run address to the output. */
+ if(force_run > 0) {
+ xex_run_seg(&seg, buffer, force_run);
+ if(!xex_fwrite_seg(&seg, out)) {
+ fprintf(stderr, SELF ": %s: %s\n",
+ outfile, xex_strerror(xex_errno));
+ errcount++;
+ }
+ count++;
+ } else if(count == 1) {
+ fprintf(stderr, SELF ": warning: %s: file is empty.\n", outfile);
+ }
+
+ if(xex_verbose)
+ fprintf(stderr, SELF ": wrote %d segment%s to %s\n",
+ (count - 1),
+ (count == 2 ? "" : "s"),
+ outfile);
+
+ fclose(out);
+
+ if(xex_verbose || errcount) {
+ fprintf(stderr,
+ SELF ": %d error%s.\n", errcount, (errcount == 1 ? "" : "s"));
+ return errcount;
+ }
+
+ return 0;
+}
diff --git a/xexcat.rst b/xexcat.rst
new file mode 100644
index 0000000..a7ce20b
--- /dev/null
+++ b/xexcat.rst
@@ -0,0 +1,132 @@
+.. RST source for xexcat(1) man page. Convert with:
+.. rst2man.py xexcat.rst > xexcat.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+======
+xexcat
+======
+
+-----------------------------------------------------------------
+Concatenate Atari 8-bit executables (XEX) into a single XEX file.
+-----------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+*xexcat* [*-hvc*] [-l *address* [-r *address*] [-i *address*] [-o *outfile.xex*] [*infile.xex*] [*infile.xex ...*]
+
+DESCRIPTION
+===========
+
+**xexcat** reads one or more Atari executables (XEX/BIN/COM/etc)
+from the given filenames, and writes a single Atari executable
+containing all the segments from all the input files to *outfile*.
+
+To read from standard input, *infile* may be omitted, or given as
+**-**. To write to standard output, **-o** *outfile* may be omitted,
+or given as **-o-**.
+
+The output file is a valid Atari executable, including the
+required *$FFFF* header for the first segment. If there are multiple
+segments, the second and subsequent segments will not have the
+optional *$FFFF* header.
+
+OPTIONS
+=======
+
+-h
+ Print a short help message and exit.
+
+-v
+ Verbose operation. Each segment's information is printed to
+ standard error, including start/end address and length.
+
+-c
+ Check only; no output file is written. Equivalent to **-v -o /dev/null**.
+
+-o outfile
+ Write output xex file to outfile. Default is to write to standard output.
+
+-l address
+ Force the output file's load address to address. This only
+ affects the first segment of the output file.
+
+-i address
+ Force the output file's first init address (if present) to
+ *address*. This *only* affects the **first** init address segment of the
+ output file. Further init address segments in the input will be
+ left unmodified. If *address* is 0, the first init segment will
+ be removed (0 means "none", not "init at address 0"). This option
+ does nothing if none of the input files contain init address
+ segments.
+
+-r address
+ Force the output file's run address to *address*. If *address*
+ is not 0, all run address segments from all input files will be
+ ignored, and a new run address segment will be constructed
+ with the given *address* and appended to the output. If *address*
+ is 0, all run addresses from all input files are ignored,
+ and the output file will not contain a run address segment
+ at all. Such a file can still be loaded from DOS, but it will
+ not execute (user will be returned to the DOS menu).
+
+NOTES
+=====
+
+It is possible to join multiple Atari executables together with
+the standard **cat**\(1) command. However, **xexcat** is always guaranteed to
+produce a valid Atari binary load file (or an empty file, if all input
+files are invalid), which is not the case for **cat**.
+
+When writing to standard output, **xexcat** will refuse to write
+binary data to the user's terminal.
+
+The Atari binary load format requires the *$FFFF* header only for
+the first segment in a file. The second and subsequent segments
+may also have a *$FFFF* header, but it's optional. **xexcat**'s output file
+will always have the *$FFFF* header for the first segment, and no
+*$FFFF* header for further segments, regardless of whether the segments
+in the input files had it or not (in fact, **xexcat** can handle
+an invalid XEX file which is missing the initial $FFFF header for the
+first segment).
+
+Some Atari executables contain raw blocks of data, which are meant
+to be read into memory by the init routine. These blocks do not
+have start/end address headers, so **xexcat** is unable to handle
+them. Raw data blocks usually occur in files created with "packer"
+or "compressor" programs, or occasionally in other large programs
+(Turbo BASIC is an example). Raw data blocks are generally found just
+after an init address segment. If you have an executable that loads
+just fine on a real Atari or emulator, but fails with **xexcat**,
+a raw data block is usually the reason why.
+
+The terms "Atari executable", "binary load file", and "XEX file"
+all refer to the same thing. Also, there is no difference between
+Atari executables named with "XEX", "COM", "BIN", "EXE", etc. The
+Atari and its DOS don't care about the names, only the contents.
+
+EXIT STATUS
+===========
+
+Exit status is zero for success, non-zero for failure.
+
+.. other sections we might want, uncomment as needed.
+
+.. FILES
+.. =====
+
+.. ENVIRONMENT
+.. ===========
+
+.. EXIT STATUS
+.. ===========
+
+.. BUGS
+.. ====
+
+.. EXAMPLES
+.. ========
+
+.. include:: manftr.rst
diff --git a/xexsplit.1 b/xexsplit.1
new file mode 100644
index 0000000..c3a88e6
--- /dev/null
+++ b/xexsplit.1
@@ -0,0 +1,195 @@
+.\" 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 "XEXSPLIT" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py xexsplit.rst > xexsplit.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.\" TODO: add -n option (same as -v, but no output)?
+.
+.SH SYNOPSIS
+.sp
+\fIxexsplit\fP [\fI\-hv\fP] \fIinfile.xex\fP [\fIoutfile\-prefix\fP]
+.SH DESCRIPTION
+.sp
+xexsplit reads an Atari executable (XEX/BIN/COM/etc) from \fIinfile\fP and
+writes each segment to a separate file. To read from standard input,
+infile may be omitted, or given as \fB\-\fP\&.
+.sp
+Output files are named \fBoutfile\-prefix.NNN.SSSS.EEEE\fP, where \fBNNN\fP is
+the segment number (decimal, 3 digits, with leading zeroes), \fBSSSS\fP
+is the start address of the segment (4 digits, hex), and \fBEEEE\fP is the
+end address of the segment (4 digits, hex). If \fIoutfile\-prefix\fP is
+omitted, the default prefix is \fBxexsplit.xex\fP\&.
+.sp
+Each output file is a valid single\-segment Atari executable, including
+the required \fI$FFFF\fP header. They may be joined back together (possibly
+after being modified) with xexcat or plain old \fBcat\fP(1).
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \-h
+Print a short help message and exit.
+.TP
+.B \-v
+Verbose operation. Each segment\(aqs information is printed to
+standard error, including start/end address and length.
+.UNINDENT
+.SH NOTES
+.sp
+The terms "Atari executable", "binary load file", and "XEX file"
+all refer to the same thing. Also, there is no difference between
+Atari executables named with "XEX", "COM", "BIN", "EXE", etc. The
+Atari and its DOS don\(aqt care about the names, only the contents.
+.sp
+It is not possible to use \fB\-\fP to write to standard output. This makes
+sense, because there\(aqs no way to write multiple, separate files to stdout.
+.sp
+The output filenames were chosen so they will sort in the order they
+were created, when used with the shell\(aqs globbing. This means that you
+can join them back into a single valid XEX file with a command like:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+cat prefix.* > joined.xex
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+or use \fBxexcat\fP(1) instead of \fBcat\fP:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+xexcat \-o joined.xex prefix.*
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+The Atari binary load format requires the \fI$FFFF\fP header only for
+the first segment in a file. The second and subsequent segments may
+also have a \fI$FFFF\fP header, but it\(aqs optional. \fBxexsplit\fP\(aqs output
+files will always have the \fI$FFFF\fP header, regardless of whether
+the segments in the original file had it or not (in fact, \fBxexsplit\fP
+can handle an invalid XEX file which is missing the initial $FFFF
+header for the first seg‐ ment).
+.sp
+A segment starting at address \fB$02E2\fP contains an init address.
+During loading, Atari DOSes JSR to the init address immediately
+after its segment has loaded. Init addresses are optinal; there\(aqs no
+rule that says that every file has to have one. It\(aqs also normal
+and fairly common for there to be several init addresses in a
+multi\-segment file. An init ad‐ dress segment is normally located
+right after the code segment it\(aqs in‐ tended to run, but this is not
+a requirement.
+.sp
+A segment starting at address \fB$02E0\fP contains a run address.
+After all segments have been loaded, Atari DOSes JSR to the run
+address to run the program just loaded. The run address is optional;
+a file containing no run address segment will simply be loaded into
+memory and not executed (user will be returned to the DOS menu).
+It\(aqs legal for a multi\-segment file to contain multiple run
+address segments, but only the last run address will actually be
+used. Normally, there is only one run address; if there are more than
+one, only the last one is used. The run address is normally the last
+segment in the file, but this is not a requirement.
+.sp
+Attempting to run a single\-segment file consisting of a run or init
+address only, will generally crash the Atari.
+.sp
+Some Atari executables contain raw blocks of data, which are meant
+to be read into memory by the init routine. These blocks do not
+have start/end address headers, so \fBxexsplit\fP is unable to handle
+them. Raw data blocks usually occur in files created with "packer"
+or "compressor" programs, or occasionally in other large programs
+(Turbo BASIC is an example). Raw data blocks are generally found just
+after an init address segment. If you have an executable that loads
+just fine on a real Atari or emulator, but fails with \fBxexsplit\fP,
+a raw data block is usually the reason why.
+.\" other sections we might want, uncomment as needed.
+.
+.\" FILES
+.
+.\" =====
+.
+.\" ENVIRONMENT
+.
+.\" ===========
+.
+.\" EXIT STATUS
+.
+.\" ===========
+.
+.\" BUGS
+.
+.\" ====
+.
+.\" EXAMPLES
+.
+.\" ========
+.
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/xexsplit.c b/xexsplit.c
new file mode 100644
index 0000000..4df51f3
--- /dev/null
+++ b/xexsplit.c
@@ -0,0 +1,123 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+
+#include "xex.h"
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "xexsplit"
+#define OPTIONS "hv"
+
+char *usage =
+ SELF " v" VERSION " by B. Watson (WTFPL)\n"
+ "Split a multi-segment Atari binary load file into multiple files\n"
+ "usage: " SELF " -[hv] [infile.xex] [outfile-prefix]\n"
+ " -h Print this help\n"
+ " -v Verbose operation\n";
+
+int main(int argc, char **argv) {
+ xex_segment seg;
+ char *infile = "-";
+ FILE *in = stdin;
+ char outfile[4096];
+ unsigned char buffer[65536];
+ FILE *out = NULL;
+ int count = 1, outlen, c;
+
+ strcpy(outfile, "xexsplit");
+
+ while( (c = getopt(argc, argv, OPTIONS)) > 0) {
+ switch(c) {
+ case 'h':
+ printf(usage);
+ exit(0);
+ break;
+
+ case 'v':
+ xex_verbose = 1;
+ break;
+
+ default:
+ fprintf(stderr, usage);
+ exit(1);
+ }
+ }
+
+ if(argc > optind) {
+ infile = argv[optind];
+ optind++;
+ }
+
+ if(argc > optind) {
+ strcpy(outfile, argv[optind]);
+ optind++;
+
+ if(argc > optind)
+ fprintf(stderr, SELF ": "
+ "ignoring extra junk on command line: '%s'.\n", argv[optind]);
+ } else if(strcmp(infile, "-") != 0) {
+ strcpy(outfile, infile);
+ }
+
+ if(strcmp(outfile, "-") == 0) {
+ fprintf(stderr, SELF ": can't write to standard output.\n");
+ exit(1);
+ }
+
+ outlen = strlen(outfile);
+
+ if( strcmp(infile, "-") != 0 && !(in = fopen(infile, "rb")) ) {
+ fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno));
+ exit(1);
+ }
+
+ seg.object = buffer;
+ while(xex_fread_seg(&seg, in)) {
+ if(count == 1 && !seg.has_ff_header) {
+ fprintf(stderr, SELF ": warning: first segment lacks $FFFF header "
+ "(bad XEX file?)\n");
+ }
+
+ seg.has_ff_header = 1;
+
+ sprintf(outfile + outlen,
+ ".%03d.%04X.%04X",
+ count, seg.start_addr, seg.end_addr);
+
+ if( !(out = fopen(outfile, "wb")) ) {
+ fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno));
+ fclose(in);
+ exit(1);
+ }
+
+ if(!xex_fwrite_seg(&seg, out))
+ break;
+
+ fclose(out);
+ out = NULL;
+
+ fprintf(stderr, SELF ": Wrote file %s\n", outfile);
+
+ count++;
+ }
+
+ fclose(in);
+ if(out) fclose(out);
+
+ if(xex_errno) {
+ xex_perror(SELF);
+ return 1;
+ }
+
+ if(count == 1) {
+ fprintf(stderr, SELF ": input file was empty!\n");
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/xexsplit.rst b/xexsplit.rst
new file mode 100644
index 0000000..3a304c3
--- /dev/null
+++ b/xexsplit.rst
@@ -0,0 +1,128 @@
+.. RST source for xexsplit(1) man page. Convert with:
+.. rst2man.py xexsplit.rst > xexsplit.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+.. TODO: add -n option (same as -v, but no output)?
+
+========
+xexsplit
+========
+
+--------------------------------------------------------------------------------------
+Split a multi-segment Atari 8-bit executable (XEX) into multiple single-segment files.
+--------------------------------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+*xexsplit* [*-hv*] *infile.xex* [*outfile-prefix*]
+
+DESCRIPTION
+===========
+
+xexsplit reads an Atari executable (XEX/BIN/COM/etc) from *infile* and
+writes each segment to a separate file. To read from standard input,
+infile may be omitted, or given as **-**.
+
+Output files are named **outfile-prefix.NNN.SSSS.EEEE**, where **NNN** is
+the segment number (decimal, 3 digits, with leading zeroes), **SSSS**
+is the start address of the segment (4 digits, hex), and **EEEE** is the
+end address of the segment (4 digits, hex). If *outfile-prefix* is
+omitted, the default prefix is **xexsplit.xex**.
+
+Each output file is a valid single-segment Atari executable, including
+the required *$FFFF* header. They may be joined back together (possibly
+after being modified) with xexcat or plain old **cat**\(1).
+
+OPTIONS
+=======
+
+-h
+ Print a short help message and exit.
+
+-v
+ Verbose operation. Each segment's information is printed to
+ standard error, including start/end address and length.
+
+NOTES
+=====
+
+The terms "Atari executable", "binary load file", and "XEX file"
+all refer to the same thing. Also, there is no difference between
+Atari executables named with "XEX", "COM", "BIN", "EXE", etc. The
+Atari and its DOS don't care about the names, only the contents.
+
+It is not possible to use **-** to write to standard output. This makes
+sense, because there's no way to write multiple, separate files to stdout.
+
+The output filenames were chosen so they will sort in the order they
+were created, when used with the shell's globbing. This means that you
+can join them back into a single valid XEX file with a command like::
+
+ cat prefix.* > joined.xex
+
+or use **xexcat**\(1) instead of **cat**::
+
+ xexcat -o joined.xex prefix.*
+
+The Atari binary load format requires the *$FFFF* header only for
+the first segment in a file. The second and subsequent segments may
+also have a *$FFFF* header, but it's optional. **xexsplit**'s output
+files will always have the *$FFFF* header, regardless of whether
+the segments in the original file had it or not (in fact, **xexsplit**
+can handle an invalid XEX file which is missing the initial $FFFF
+header for the first seg‐ ment).
+
+A segment starting at address **$02E2** contains an init address.
+During loading, Atari DOSes JSR to the init address immediately
+after its segment has loaded. Init addresses are optinal; there's no
+rule that says that every file has to have one. It's also normal
+and fairly common for there to be several init addresses in a
+multi-segment file. An init ad‐ dress segment is normally located
+right after the code segment it's in‐ tended to run, but this is not
+a requirement.
+
+A segment starting at address **$02E0** contains a run address.
+After all segments have been loaded, Atari DOSes JSR to the run
+address to run the program just loaded. The run address is optional;
+a file containing no run address segment will simply be loaded into
+memory and not executed (user will be returned to the DOS menu).
+It's legal for a multi-segment file to contain multiple run
+address segments, but only the last run address will actually be
+used. Normally, there is only one run address; if there are more than
+one, only the last one is used. The run address is normally the last
+segment in the file, but this is not a requirement.
+
+Attempting to run a single-segment file consisting of a run or init
+address only, will generally crash the Atari.
+
+Some Atari executables contain raw blocks of data, which are meant
+to be read into memory by the init routine. These blocks do not
+have start/end address headers, so **xexsplit** is unable to handle
+them. Raw data blocks usually occur in files created with "packer"
+or "compressor" programs, or occasionally in other large programs
+(Turbo BASIC is an example). Raw data blocks are generally found just
+after an init address segment. If you have an executable that loads
+just fine on a real Atari or emulator, but fails with **xexsplit**,
+a raw data block is usually the reason why.
+
+.. other sections we might want, uncomment as needed.
+
+.. FILES
+.. =====
+
+.. ENVIRONMENT
+.. ===========
+
+.. EXIT STATUS
+.. ===========
+
+.. BUGS
+.. ====
+
+.. EXAMPLES
+.. ========
+
+.. include:: manftr.rst
diff --git a/xextest.c b/xextest.c
new file mode 100644
index 0000000..06614ab
--- /dev/null
+++ b/xextest.c
@@ -0,0 +1,87 @@
+#include <stdio.h>
+#include "xex.h"
+
+/* This is a minimal program illustrating the use of the xex library.
+
+ It expects to read one or more Atari executables from standard
+ input, and writes one (possibly multi-segment) Atari executable
+ to standard output.
+
+ This code is provided an an example only; for a useful real-world
+ tool, use xexcat.
+
+ To compile: "make xextest" if you have the full bw_atari8_utils
+ source distribution. Otherwise, (assuming you at least have xex.c
+ and xex.h): gcc -o xextest xextest.c xex.c (should work with other
+ compilers too).
+ */
+
+int main(int argc, char **argv) {
+ xex_segment seg;
+ unsigned char buffer[64 * 1024]; /* 64K buffer is guaranteed big enough */
+ int count = 1;
+
+ /* tell xex library to emit debugging trace to stderr. Default value
+ is 0 (no trace). */
+ xex_verbose = 1;
+
+ /* xex_fread_seg_header() returns false on error or at EOF. */
+ while(xex_fread_seg_header(&seg, stdin)) {
+ seg.object = buffer;
+
+ /* We're using a static buffer here. If we were using dynamically
+ allocated buffers, we'd say "seg.object = malloc(seg.len)" above,
+ and put a "free(seg.object)" somewhere after the xex_fwrite_seg().
+
+ Note that the xex lib NEVER calls malloc() or free() itself.
+
+ (Also, it never calls any of the standard I/O functions except
+ fread(), fwrite(), feof(), or ferror(); you have to fopen() and
+ fclose() in the calling code).
+ */
+
+ /* xex library doesn't care if the first segment is missing the
+ required Atari $FFFF header, so we handle it ourselves. */
+ if(count == 1 && !seg.has_ff_header)
+ fprintf(stderr, "missing initial $FFFF header (bad XEX file?)\n");
+
+ /* Force the first segment to have a $FFFF header, and remove the
+ (optional) $FFFF header from subsequent segments. */
+ seg.has_ff_header = (count == 1);
+
+ /* Read the segment data. xex_fread_seg_data() returns false for
+ failure (with xex_errno set to indicate the reason). An EOF in the
+ middle of a segment is an error (means the file was truncated). */
+ if(!xex_fread_seg_data(&seg, stdin))
+ break;
+
+ /* xex_fwrite_seg() returns true for success, or false for failure
+ (with xex_errno set). Unless you've diddled with seg's fields,
+ xex_errno will always be XERR_SYSCALL when xex_fwrite_seg()
+ fails (and xex_perror() or xex_strerror() will call the real
+ strerror() to get the system's error message). */
+ if(!xex_fwrite_seg(&seg, stdout))
+ break;
+
+ /* If we weren't using xex_verbose mode, we might want to call
+ xex_print_seg_info() ourselves to print some info about the
+ segment, like so: */
+
+ /* xex_print_seg_info(&seg); */
+
+ fprintf(stderr, "segment #%d done\n\n", count);
+ count++;
+ }
+
+ /* If xex_fread_seg_header() returned false due to EOF, xex_errno will
+ be zero (aka XERR_NONE). Otherwise, xex_fread_seg_header() didn't like
+ the header, or else xex_fread_seg_data() returned false (probably
+ due to a premature EOF in the middle of the segment data), so we print
+ an error message about it. */
+ if(xex_errno)
+ xex_perror("error");
+
+ /* xex_errno will be 0 for error or non-zero otherwise, so it's suitable
+ for use as a standard UNIX exit status. */
+ return xex_errno;
+}
diff --git a/xfd2atr.1 b/xfd2atr.1
new file mode 100644
index 0000000..9e5c541
--- /dev/null
+++ b/xfd2atr.1
@@ -0,0 +1,132 @@
+.\" 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 "XFD2ATR" 1 "2022-08-27" "0.2.0" "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:
+.
+.\" rst2man.py xfd2atr.rst > xfd2atr.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fIxfd2atr\fP [\fI\-sd\fP] \fIinfile.xfd\fP [\fIoutfile.atr\fP]
+.SH DESCRIPTION
+.sp
+\fBxfd2atr\fP generates and adds a 16\-byte ATR header to an XFD
+image. If no \fB\-s\fP or \fB\-d\fP options are given, xfd2atr tries to
+guess the density based on the file size.
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \-s
+Assume the image uses single density (128\-byte) sectors, instead
+of trying to guess the density from the file size.
+.TP
+.B \-d
+Assume the image uses double density (256\-byte) sectors, instead
+of trying to guess the density from the file size.
+.UNINDENT
+.SH NOTES
+.sp
+You may use \fB\-\fP for \fIinfile\fP to read from standard input and/or
+\fB\-\fP for \fIoutfile\fP to write to standard output. If a filename is
+supplied for \fIoutfile\fP, it will always be used as\-is (no \fI\&.atr\fP
+extension will be appended).
+.sp
+If \fIoutfile\fP is omitted, it is constructed like so:
+.INDENT 0.0
+.INDENT 3.5
+.INDENT 0.0
+.IP \(bu 2
+If reading from standard input, write to standard output.
+.IP \(bu 2
+If reading from a file whose name ends with an \fI\&.xfd\fP or \fI\&.XFD\fP
+extension, replace the extension with \fI\&.atr\fP\&.
+.IP \(bu 2
+Otherwise, append \fI\&.atr\fP to the input filename.
+.UNINDENT
+.UNINDENT
+.UNINDENT
+.sp
+Since XFD images are raw dumps with no header or structure, it\(aqs
+impossible to know the correct density (bytes/sector) for a given
+image for certain.
+.sp
+However, no known Atari\-compatible disk format uses other than 128
+or 256 bytes per sector (or possibly 512, for some hard disk images,
+but \fBxfd2atr\fP doesn\(aqt support these). This means file that isn\(aqt a
+multiple of 128 bytes in size will be rejected.
+.sp
+Likewise, no known format uses an odd number of sectors, and
+it\(aqs assumed that all double\-density images will begin with 3
+single\-density boot sectors (true of all floppy images you\(aqre
+ever likely to run across; may not be true of hard disk images).
+.sp
+Given these assumptions, \fBxfd2atr\fP is able to make an educated
+guess about the correct sector size and count to use for the ATR
+header it generates. If it guesses wrong, the resulting ATR image
+will be unusable; if this happens, re\-run \fBxfd2atr\fP and force the
+density with \fB\-s\fP or \fB\-d\fP\&.
+.SH EXIT STATUS
+.sp
+Exit status is zero for success, non\-zero for failure. Further,
+exit status will be 1 for errors involving file I/O (file not found,
+permissions, etc), and 2 for structural errors in the XFD file.
+.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),
+\fBcart2xex\fP(1),
+\fBdasm2atasm\fP(1),
+\fBfenders\fP(1),
+\fBrom2cart\fP(1),
+\fBunmac65\fP(1),
+\fBxexcat\fP(1),
+\fBxexsplit\fP(1),
+\fBxfd2atr\fP(1).
+.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/xfd2atr.c b/xfd2atr.c
new file mode 100644
index 0000000..3fad55e
--- /dev/null
+++ b/xfd2atr.c
@@ -0,0 +1,227 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "xfd2atr"
+
+#define USAGE \
+ SELF " v" VERSION " by B. Watson (WTFPL)\n" \
+ "Usage: " SELF " -[sd] input.xfd [output.atr]\n"
+
+int main(int argc, char **argv) {
+ struct stat st;
+ char infile[4096], outfile[4096];
+ FILE *in, *out;
+ int i, paras, secsize = 0, seccount;
+
+ if(argc < 2 || argc > 4) {
+ fprintf(stderr, USAGE);
+ exit(1);
+ }
+
+ if(argv[1][0] == '-') {
+ switch(argv[1][1]) {
+ case 's':
+ secsize = 128;
+ break;
+
+ case 'd':
+ secsize = 256;
+ break;
+
+ default:
+ fprintf(stderr,
+ SELF ": invalid option -'%c'\n" USAGE, argv[1][1]);
+ }
+
+ argv++; argc--;
+ }
+
+ strcpy(infile, argv[1]);
+ if(argc == 3) {
+ strcpy(outfile, argv[2]);
+ } else if(strcmp(infile, "-") == 0) {
+ strcpy(outfile, "-");
+ } else {
+ char *p;
+ strcpy(outfile, argv[1]);
+
+ p = strstr(outfile, ".xfd");
+ if(!p) p = strstr(outfile, ".XFD");
+ if(!p) p = outfile + strlen(outfile);
+ strcpy(p, ".atr");
+ }
+
+ fprintf(stderr, SELF ": input '%s', output '%s'\n", infile, outfile);
+
+ if(strcmp(infile, "-") == 0) {
+ in = stdin;
+ } else {
+ if( !(in = fopen(infile, "rb")) ) {
+ fprintf(stderr, SELF ": (fatal) can't read %s: %s\n",
+ infile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ if(fstat(fileno(in), &st)) {
+ fprintf(stderr, SELF ": (fatal) can't stat %s: %s\n",
+ infile, strerror(errno));
+ exit(1);
+ }
+
+ /* A few sanity checks... */
+ if(st.st_size < 384) {
+ fprintf(stderr,
+ SELF ": (fatal) %s too small to be an XFD image (<384 bytes)\n",
+ infile);
+ exit(2);
+ }
+
+ if(st.st_size % 128 == 16) {
+ fprintf(stderr,
+ SELF ": (fatal) %s looks like an ATR image, not an XFD\n",
+ infile);
+ exit(2);
+ }
+
+ if(st.st_size % 128 != 0) {
+ fprintf(stderr,
+ SELF ": (fatal) %s not a valid XFD image (not an even number "
+ "of sectors)\n", infile);
+ exit(2);
+ }
+
+ if(st.st_size > (65535 * 256)) {
+ fprintf(stderr, SELF ": (fatal) %s too large to be an XFD image (>16M)\n",
+ infile);
+ exit(2);
+ }
+
+ if(!secsize) {
+ char *type;
+ fprintf(stderr, SELF ": guessing type; use -s or -d to set.\n");
+
+ /* Automagically figure out the sector size and count */
+ if(st.st_size == 720 * 128) {
+ type = "90K SS/SD image";
+ secsize = 128;
+ } else if(st.st_size == 1040 * 128) {
+ type = "130K SS/ED image";
+ secsize = 128;
+ } else if(st.st_size == 720 * 256 - 128 * 3) {
+ type = "180K SS/DD image";
+ secsize = 256;
+ } else if(st.st_size == 1440 * 256 - 128 * 3) {
+ type = "360K DS/DD image";
+ secsize = 256;
+ } else if(st.st_size < 720 * 128) {
+ type = "<90K image, assuming SD";
+ secsize = 128;
+ } else if(st.st_size > 720 * 128 && st.st_size < 1040 * 128) {
+ type = ">90K, <130K image, assuming SD sectors";
+ secsize = 128;
+ } else if(st.st_size % 256 == 0) {
+ fprintf(stderr, SELF ": Non-standard %dK image, assuming SD sectors\n",
+ (int)st.st_size / 1024);
+ type = "Large floppy or hard disk, SD";
+ secsize = 128;
+ } else {
+ fprintf(stderr, SELF ": Non-standard %dK image, assuming DD sectors\n",
+ (int)st.st_size / 1024);
+ type = "Large floppy or hard disk, DD";
+ secsize = 256;
+ }
+
+ fprintf(stderr, SELF ": guessed type: %s\n", type);
+ }
+
+ if(secsize == 128) {
+ seccount = st.st_size / secsize;
+ if(seccount & 1)
+ fprintf(stderr, SELF ": odd number of sectors in SD image, might "
+ "actually be DD (try with -d?)\n");
+ } else {
+ seccount = (st.st_size - 384) / secsize + 3;
+ if(st.st_size % 256 != 128)
+ fprintf(stderr, SELF ": partial sector at end of DD image, might "
+ "actually be SD (try with -s?)\n");
+ }
+
+ /* One last sanity check */
+ if(seccount > 65535) {
+ fprintf(stderr,
+ SELF ": (fatal) %s too large to be an XFD image "
+ "at current density (>65535 sectors)\n",
+ infile);
+ if(secsize == 128)
+ fprintf(stderr, SELF ": Try forcing double density with -d\n");
+ exit(2);
+ }
+
+ fprintf(stderr, SELF ": sectors: %d, sector size: %d bytes",
+ seccount, secsize);
+ if(secsize == 256)
+ fprintf(stderr, " (first 3 sectors are 128 bytes)");
+ fputc('\n', stderr);
+
+ paras = st.st_size / 16;
+ fprintf(stderr, SELF ": %d 16-byte paragraphs\n", paras);
+
+ /* Only open the output file after the XFD is known to be good */
+ if(strcmp(outfile, "-") == 0) {
+ out = stdout;
+ } else {
+ if( !(out = fopen(outfile, "wb")) ) {
+ fprintf(stderr, SELF ": (fatal) can't write %s: %s\n",
+ outfile, strerror(errno));
+ exit(1);
+ }
+ }
+
+ /* output ATR header: */
+ fputc(0x96, out); /* NICKATARI cksum, lo byte */
+ fputc(0x02, out); /* NICKATARI cksum, hi byte */
+ fputc(paras & 0xff, out); /* paragraphs, lo byte */
+ fputc((paras >> 8) & 0xff, out); /* paragraphs, mid byte */
+ fputc(secsize & 0xff, out); /* sector size, lo byte */
+ fputc(secsize >> 8, out); /* sector size, hi byte */
+ fputc((paras >> 16) & 0xff, out); /* paragraphs, hi byte */
+
+ /* unused ATR header bytes */
+ for(i=0; i<9; i++)
+ fputc('\0', out);
+
+ /* copy the data */
+ while( (i = fgetc(in)) != EOF )
+ fputc(i, out);
+
+ /* fgetc() returns EOF on error *or* EOF;
+ check for I/O errors, return 1 if so */
+ i = 0;
+
+ if(ferror(in)) {
+ i = 1;
+ fprintf(stderr,
+ SELF ": error reading %s: %s\n", infile, strerror(errno));
+ }
+
+ if(ferror(out)) {
+ i = 1;
+ fprintf(stderr,
+ SELF ": error writing %s: %s\n", outfile, strerror(errno));
+ }
+
+ fclose(in);
+ fclose(out);
+
+ return i;
+}
diff --git a/xfd2atr.rst b/xfd2atr.rst
new file mode 100644
index 0000000..f28b19c
--- /dev/null
+++ b/xfd2atr.rst
@@ -0,0 +1,82 @@
+.. RST source for xfd2atr(1) man page. Convert with:
+.. rst2man.py xfd2atr.rst > xfd2atr.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+=======
+xfd2atr
+=======
+
+------------------------------------------------------------
+Convert an Atari 8-bit XFD (raw) disk image to an ATR image.
+------------------------------------------------------------
+
+.. include:: manhdr.rst
+
+SYNOPSIS
+========
+
+*xfd2atr* [*-sd*] *infile.xfd* [*outfile.atr*]
+
+DESCRIPTION
+===========
+
+**xfd2atr** generates and adds a 16-byte ATR header to an XFD
+image. If no **-s** or **-d** options are given, xfd2atr tries to
+guess the density based on the file size.
+
+OPTIONS
+=======
+
+-s
+ Assume the image uses single density (128-byte) sectors, instead
+ of trying to guess the density from the file size.
+
+-d
+ Assume the image uses double density (256-byte) sectors, instead
+ of trying to guess the density from the file size.
+
+NOTES
+=====
+
+You may use **-** for *infile* to read from standard input and/or
+**-** for *outfile* to write to standard output. If a filename is
+supplied for *outfile*, it will always be used as-is (no *.atr*
+extension will be appended).
+
+If *outfile* is omitted, it is constructed like so:
+
+ - If reading from standard input, write to standard output.
+
+ - If reading from a file whose name ends with an *.xfd* or *.XFD*
+ extension, replace the extension with *.atr*.
+
+ - Otherwise, append *.atr* to the input filename.
+
+Since XFD images are raw dumps with no header or structure, it's
+impossible to know the correct density (bytes/sector) for a given
+image for certain.
+
+However, no known Atari-compatible disk format uses other than 128
+or 256 bytes per sector (or possibly 512, for some hard disk images,
+but **xfd2atr** doesn't support these). This means file that isn't a
+multiple of 128 bytes in size will be rejected.
+
+Likewise, no known format uses an odd number of sectors, and
+it's assumed that all double-density images will begin with 3
+single-density boot sectors (true of all floppy images you're
+ever likely to run across; may not be true of hard disk images).
+
+Given these assumptions, **xfd2atr** is able to make an educated
+guess about the correct sector size and count to use for the ATR
+header it generates. If it guesses wrong, the resulting ATR image
+will be unusable; if this happens, re-run **xfd2atr** and force the
+density with **-s** or **-d**.
+
+EXIT STATUS
+===========
+
+Exit status is zero for success, non-zero for failure. Further,
+exit status will be 1 for errors involving file I/O (file not found,
+permissions, etc), and 2 for structural errors in the XFD file.
+
+.. include:: manftr.rst