aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile25
-rw-r--r--src/addrs.h86
-rw-r--r--src/alfsum.175
-rw-r--r--src/alfsum.c53
-rw-r--r--src/alfsum.rst65
-rw-r--r--src/extract.c732
-rw-r--r--src/io.c92
-rw-r--r--src/listalf.c118
-rw-r--r--src/unalf.1136
-rw-r--r--src/unalf.c66
-rw-r--r--src/unalf.h40
-rw-r--r--src/unalf.rst124
12 files changed, 1612 insertions, 0 deletions
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..56951f2
--- /dev/null
+++ b/src/Makefile
@@ -0,0 +1,25 @@
+COPT=-O3
+CFLAGS=-Wall -Wno-unused-label -I../f65 $(COPT)
+
+all: unalf unalf.1 alfsum alfsum.1
+
+unalf: unalf.o io.o listalf.o extract.o ../f65/f65.o
+
+unalf.o: unalf.c unalf.h ../f65/f65.h
+
+io.o: io.c unalf.h addrs.h ../f65/f65.h
+
+listalf.o: listalf.c addrs.h unalf.h ../f65/f65.h
+
+extract.o: extract.c addrs.h unalf.h ../f65/f65.h
+
+../f65/f65.o: ../f65/f65.c ../f65/f65.h
+
+unalf.1: unalf.rst
+ rst2man unalf.rst > unalf.1
+
+alfsum.1: alfsum.rst
+ rst2man alfsum.rst > alfsum.1
+
+clean:
+ rm -f *.o unalf alfsum ../f65/f65.o
diff --git a/src/addrs.h b/src/addrs.h
new file mode 100644
index 0000000..47e0e32
--- /dev/null
+++ b/src/addrs.h
@@ -0,0 +1,86 @@
+
+#define buf_adr_l 0x7A7B
+#define buf_adr_h 0x7A7C
+#define buf_len_l 0x7A7D
+#define buf_len_h 0x7A7E
+#define zp_b0 0x00B0
+#define zp_b1 0x00B1
+#define acc16_l 0x00B2
+#define acc16_h 0x00B3
+#define zp_b4 0x00B4
+#define zp_b5 0x00B5
+#define stackptr_l 0x00B6
+#define stackptr_h 0x00B7
+#define zp_b8 0x00B8
+#define zp_b9 0x00B9
+#define outbuf_ptr_l 0x00BA
+#define outbuf_ptr_h 0x00BB
+#define zp_bc 0x00BC
+#define zp_bd 0x00BD
+#define zp_be 0x00BE
+#define zp_bf 0x00BF
+#define MEMTOP 0x02E5
+#define MEMTOP_lo 0x02E5
+#define MEMTOP_hi 0x02E6
+#define MEMLO 0x02E7
+#define MEMLO_hi 0x02E8
+#define input_file 0x7000
+#define output_dir 0x7050
+#define outfile_l 0x70A0
+#define outfile_h 0x70A1
+#define linbuf 0x7120
+#define L7174 0x7174
+#define L7175 0x7175
+#define L7176 0x7176
+#define L7177 0x7177
+#define L7178 0x7178
+#define L7179 0x7179
+#define L717A 0x717A
+#define L717B 0x717B
+#define L717C 0x717C
+#define L717D 0x717D
+#define L717E 0x717E
+#define cksum_l 0x717F
+#define cksum_h 0x7180
+#define L7181 0x7181
+#define L7182 0x7182
+#define inbuf_len_l 0x7183
+#define inbuf_len_h 0x7184
+#define inbuf_adr_l 0x7185
+#define inbuf_adr_h 0x7186
+#define outbuf_adr_l 0x7187
+#define outbuf_adr_h 0x7188
+#define L7189 0x7189
+#define L718A 0x718A
+#define L718B 0x718B
+#define L718C 0x718C
+#define alf_header 0x718D
+#define alf_hdr_sig 0x718E
+#define alf_hdr_filename 0x718F
+#define alf_hdr_compsize0 0x719C
+#define alf_hdr_compsize1 0x719D
+#define alf_hdr_compsize2 0x719E
+#define alf_hdr_compsize3 0x719F
+#define alf_hdr_date0 0x71A0
+#define alf_hdr_date1 0x71A1
+#define alf_hdr_time0 0x71A2
+#define alf_hdr_time1 0x71A3
+#define alf_hdr_cksum_l 0x71A4
+#define alf_hdr_cksum_h 0x71A5
+#define alf_hdr_origsize0 0x71A6
+#define alf_hdr_origsize1 0x71A7
+#define alf_hdr_origsize2 0x71A8
+#define alf_hdr_origsize3 0x71A9
+#define L71AA 0x71AA
+#define L71AB 0x71AB
+#define shift_counter 0x71AC
+#define L71AD 0x71AD
+#define L71AE 0x71AE
+#define L71AF 0x71AF
+#define L71B0 0x71B0
+#define outbuf_len_l 0x71B1
+#define outbuf_len_h 0x71B2
+#define L71B4 0x71B4
+#define L71B5 0x71B5
+#define L71B6 0x71B6
+#define L71B7 0x71B7
diff --git a/src/alfsum.1 b/src/alfsum.1
new file mode 100644
index 0000000..1f4ec5b
--- /dev/null
+++ b/src/alfsum.1
@@ -0,0 +1,75 @@
+.\" 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 "ALFSUM" 1 "2025-11-04" "0.0.0" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+alfsum \- calculate ALF checksums
+.\" RST source for alfsum(1) man page. Convert with:
+.
+.\" rst2man.py alfsum.rst > alfsum.1
+.
+.SH SYNOPSIS
+.sp
+alfsum \fBfile\fP [\fBfile\fP ...]
+.SH DESCRIPTION
+.sp
+\fBalfsum\fP calculates the checksums used by the \fBALF\fP compression
+utility on the Atari 8\-bit platform.
+.sp
+There are no options. Use \fB\-\fP to read from standard input. Use
+\fB\&./\-file\fP to read a file whose name begins with \fB\-\fP\&.
+.SH NOTES
+.sp
+The checksum algorithm is very simple: all the bytes in the file are
+added together, and the low 16 bits of the result are the checksum.
+.SH EXIT STATUS
+.INDENT 0.0
+.TP
+.B 0
+Success.
+.TP
+.B 1 to 254
+File I/O error count. If there are more than 254 I/O errors, 254 is returned.
+.TP
+.B 255
+Error in command\-line arguments.
+.UNINDENT
+.SH COPYRIGHT
+.sp
+\fBunalf\fP is released under the WTPFL: Do WTF you want with this.
+.SH AUTHORS
+.INDENT 0.0
+.IP B. 3
+Watson <\fI\%urchlay@slackware.uk\fP>
+.UNINDENT
+.SH SEE ALSO
+.sp
+TODO
+.\" Generated by docutils manpage writer.
+.
diff --git a/src/alfsum.c b/src/alfsum.c
new file mode 100644
index 0000000..7f30c7c
--- /dev/null
+++ b/src/alfsum.c
@@ -0,0 +1,53 @@
+#include <stdio.h>
+#include <unistd.h>
+
+/* 20251104 bkw: implement the simple checksum used by ALF.
+ Dumbest possible algo: all the bytes are added together and
+ the bottom 16 bits of the result is the checksum. */
+
+char *self;
+
+void alfsum(const char *file, FILE *f) {
+ int c;
+ unsigned long sum = 0;
+
+ while((c = fgetc(f)) != EOF)
+ sum += c;
+
+ printf("%8s\t%04x\n", file, (unsigned int)(sum & 0xffff));
+}
+
+int main(int argc, char **argv) {
+ int errs = 0;
+ char *file;
+ FILE *f;
+
+ self = argv[0];
+
+ /* if the first arg is a - followed by anything at all, assume --help */
+ if(argc < 2 || (argc == 2 && argv[1][0] == '-' && argv[1][1])) {
+ fprintf(stderr,
+ "usage: %s filename [filename ...]\n"
+ "\t(use - to read from standard input)\n",
+ self);
+ return -1;
+ }
+
+ while((file = *++argv)) {
+ if(argv[0][0] == '-' && !argv[0][1]) {
+ if(isatty(0))
+ fprintf(stderr, "%s: reading from stdin...\n", self);
+ f = stdin;
+ file = " (stdin)";
+ } else if(!(f = fopen(file, "rb"))) {
+ fprintf(stderr, "%s: ", self);
+ perror(file);
+ errs++;
+ continue;
+ }
+ alfsum(file, f);
+ fclose(f);
+ }
+
+ return errs > 254 ? 254 : errs;
+}
diff --git a/src/alfsum.rst b/src/alfsum.rst
new file mode 100644
index 0000000..1d1ee87
--- /dev/null
+++ b/src/alfsum.rst
@@ -0,0 +1,65 @@
+.. RST source for alfsum(1) man page. Convert with:
+.. rst2man.py alfsum.rst > alfsum.1
+
+.. |version| replace:: 0.0.0
+.. |date| date::
+
+======
+alfsum
+======
+
+-----------------------
+calculate ALF checksums
+-----------------------
+
+:Manual section: 1
+:Manual group: Urchlay's Atari 8-bit Tools
+:Date: |date|
+:Version: |version|
+
+SYNOPSIS
+========
+
+alfsum **file** [**file** ...]
+
+DESCRIPTION
+===========
+
+**alfsum** calculates the checksums used by the **ALF** compression
+utility on the Atari 8-bit platform.
+
+There are no options. Use **-** to read from standard input. Use
+**./-file** to read a file whose name begins with **-**.
+
+NOTES
+=====
+
+The checksum algorithm is very simple: all the bytes in the file are
+added together, and the low 16 bits of the result are the checksum.
+
+EXIT STATUS
+===========
+
+0
+ Success.
+
+1 to 254
+ File I/O error count. If there are more than 254 I/O errors, 254 is returned.
+
+255
+ Error in command-line arguments.
+
+COPYRIGHT
+=========
+
+**unalf** is released under the WTPFL: Do WTF you want with this.
+
+AUTHORS
+=======
+
+B. Watson <urchlay@slackware.uk>
+
+SEE ALSO
+========
+
+TODO
diff --git a/src/extract.c b/src/extract.c
new file mode 100644
index 0000000..079fe7f
--- /dev/null
+++ b/src/extract.c
@@ -0,0 +1,732 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <f65.h>
+#include "unalf.h"
+#include "addrs.h"
+
+void dpoke(int addr, u16 value) {
+ mem[addr] = value & 0xff;
+ mem[addr + 1] = value >> 8;
+}
+
+u16 dpeek(int addr) {
+ return mem[addr] | (mem[addr + 1] << 8);
+}
+
+void extract_alf(void) {
+ char *filename;
+
+ /* get ready to call fake 6502 stuff. set up memory like the Atari. */
+ dpoke(MEMTOP, 0xbc1f);
+
+ while(read_alf_header()) {
+ filename = (char *)(mem + alf_hdr_filename);
+ printf("Uncrunching %s\n", filename);
+
+ if(!(out_file = fopen(filename, "wb"))) {
+ fprintf(stderr, "%s: ", self);
+ perror(filename);
+ exit(1);
+ }
+
+ uncrunch_file();
+ fclose(out_file);
+ }
+}
+
+static void chksum_err(void) {
+ fprintf(stderr, "%s: checksum error on file %s\n", self, in_filename);
+}
+
+void uncrunch_file(void) {
+ lda_i(0x00);
+ sta(L71AF);
+ sta(L71B0);
+ sta(outbuf_len_l);
+ sta(outbuf_len_h);
+ sta(L71B6);
+ sta(L71B7);
+ sta(L718C);
+ sta(cksum_l);
+ sta(cksum_h);
+ lda_i(0x09);
+ sta(shift_counter);
+ lda_i(0x00);
+ sta(L71AD);
+ lda_i(0x02);
+ sta(L71AE);
+ lda_i(0x00);
+ sta(stackptr_l);
+ lda_i(0x60);
+ sta(stackptr_h);
+ jsr(setup_io_bufs);
+ ldx_i(0x10);
+ lda(inbuf_adr_l);
+ sta(buf_adr_l);
+ lda(inbuf_adr_h);
+ sta(buf_adr_h);
+ lda(inbuf_len_l);
+ sta(buf_len_l);
+ lda(inbuf_len_h);
+ sta(buf_len_h);
+ jsr(L7A19);
+ sty(L718C);
+ jsr(L79E7);
+ lda(buf_len_l);
+ ora(buf_len_h);
+ bne(L75B1);
+ rts();
+
+L75B1:
+ jsr(L76D0);
+ lda(acc16_h);
+ cmp_i(0x01);
+ bne(uncrunch_blk);
+ lda(acc16_l);
+ cmp_i(0x01);
+ bne(uncrunch_blk);
+ jsr(write_output);
+ lda(alf_hdr_cksum_l);
+ cmp(cksum_l);
+ bne(print_emsg_checksum);
+ lda(alf_hdr_cksum_h);
+ cmp(cksum_h);
+ beq(cksum_ok);
+print_emsg_checksum:
+ chksum_err();
+cksum_ok:
+ rts();
+
+uncrunch_blk:
+ lda(acc16_l);
+ cmp_i(0x00);
+ bne(L760B);
+ lda(acc16_h);
+ cmp_i(0x01);
+ bne(L760B);
+ jsr(init_counters);
+ jsr(L76D0);
+ lda(acc16_l);
+ sta(L717D);
+ sta(L7179);
+ lda(acc16_h);
+ sta(L717E);
+ sta(L717A);
+ lda(acc16_l);
+ sta(L7177);
+ sta(L7178);
+ jsr(store_outbyte);
+ jmp(L75B1);
+
+L760B:
+ lda(acc16_l);
+ sta(L717D);
+ sta(L7175);
+ lda(acc16_h);
+ sta(L717E);
+ sta(L7176);
+ lda(acc16_h);
+ cmp(L717C);
+ bcc(L7641);
+ lda(acc16_l);
+ cmp(L717B);
+ bcc(L7641);
+ lda(L7179);
+ sta(acc16_l);
+ sta(L717D);
+ lda(L717A);
+ sta(acc16_h);
+ sta(L717E);
+ lda(L7178);
+ sta(acc16_l);
+ jsr(push_acc16);
+
+L7641:
+ lda(L717E);
+ beq(L7670);
+ lda(L717D);
+ sta(zp_b4);
+ lda(L717E);
+ sta(zp_b5);
+ jsr(L7899);
+ ldy_i(0x02);
+ lda_ind_y(zp_b0);
+ sta(acc16_l);
+ jsr(push_acc16);
+ ldy_i(0x00);
+ lda_ind_y(zp_b0);
+ sta(acc16_l);
+ sta(L717D);
+ iny();
+ lda_ind_y(zp_b0);
+ sta(acc16_h);
+ sta(L717E);
+ jmp(L7641);
+
+L7670:
+ lda(L717D);
+ sta(acc16_l);
+ sta(L7178);
+ sta(L7177);
+ lda(L717E);
+ sta(acc16_h);
+ jsr(push_acc16);
+L7683:
+ lda(L71AF);
+ ora(L71B0);
+ beq(L7694);
+ jsr(pop_acc16);
+ jsr(store_outbyte);
+ jmp(L7683);
+
+L7694:
+ jsr(L78C2);
+ lda(L7175);
+ sta(acc16_l);
+ sta(L7179);
+ lda(L7176);
+ sta(acc16_h);
+ sta(L717A);
+ lda(L717B);
+ sta(zp_b4);
+ lda(L717C);
+ sta(zp_b5);
+ cmp(L71AE);
+ bcc(L76CD);
+ lda(zp_b4);
+ cmp(L71AD);
+ bcc(L76CD);
+ lda(shift_counter);
+ cmp_i(0x0C);
+ beq(L76CD);
+ inc(shift_counter);
+ asl(L71AD);
+ rol(L71AE);
+L76CD:
+ jmp(L75B1);
+}
+
+void L76D0(void) {
+L76D0:
+ lda(L71B6);
+ sta(zp_b8);
+ lda(L71B7);
+ sta(zp_b9);
+ ldx_i(0x02);
+L76DC:
+ lsr(zp_b9);
+ ror(zp_b8);
+ dex();
+ bpl(L76DC);
+ lda(zp_b8);
+ sta(L71AA);
+ lda(zp_b9);
+ sta(L71AB);
+ lda(inbuf_len_l);
+ sec();
+ sbc(zp_b8);
+ sta(zp_bc);
+ lda(inbuf_len_h);
+ sbc(zp_b9);
+ sta(zp_bd);
+ lda(zp_bd);
+ bne(L770F);
+ lda(zp_bc);
+ cmp_i(0x03);
+ bcs(L770F);
+ ldx(L718C);
+ bpl(L771C);
+ cmp_i(0x02);
+ bcc(L7712);
+L770F:
+ jmp(L779B);
+/* ---------------------------------------------------------------------------- */
+L7712:
+ #if 0
+ ldx_i(0x0A);
+ ldy_i(0x0A);
+ jsr(printstr);
+ jmp(cleanup_and_exit);
+ #endif
+ fprintf(stderr, "%s: unknown error (L7712)\n", self);
+ exit(1);
+/* ---------------------------------------------------------------------------- */
+L771C:
+ tay();
+ dey();
+ ldx(inbuf_adr_l);
+ stx(zp_be);
+ ldx(inbuf_adr_h);
+ stx(zp_bf);
+ lda(inbuf_adr_l);
+ clc();
+ adc(zp_b8);
+ sta(zp_b8);
+ lda(inbuf_adr_h);
+ adc(zp_b9);
+ sta(zp_b9);
+L7737:
+ lda_ind_y(zp_b8);
+ sta_ind_y(zp_be);
+ dey();
+ bpl(L7737);
+ lda(inbuf_len_l);
+ sec();
+ sbc(zp_bc);
+ sta(buf_len_l);
+ lda(inbuf_len_h);
+ sbc_i(0x00);
+ sta(buf_len_h);
+ lda(inbuf_adr_l);
+ clc();
+ adc(zp_bc);
+ sta(buf_adr_l);
+ lda(inbuf_adr_h);
+ adc_i(0x00);
+ sta(buf_adr_h);
+ ldx_i(0x10);
+ jsr(L7A19);
+ sty(L718C);
+ bpl(L7771);
+ cpy_i(0x88);
+ beq(L7771);
+ // jmp(cleanup_and_exit);
+ exit(0);
+/* ---------------------------------------------------------------------------- */
+L7771:
+ jsr(L79E7);
+ lda(L71AA);
+ sta(zp_b8);
+ lda(L71AB);
+ sta(zp_b9);
+ ldx_i(0x02);
+L7780:
+ asl(zp_b8);
+ rol(zp_b9);
+ dex();
+ bpl(L7780);
+ lda(L71B6);
+ sec();
+ sbc(zp_b8);
+ sta(L71B6);
+ lda(L71B7);
+ sbc(zp_b9);
+ sta(L71B7);
+ jmp(L76D0);
+/* ---------------------------------------------------------------------------- */
+L779B:
+ lda(zp_b8);
+ sta(zp_bc);
+ clc();
+ adc(inbuf_adr_l);
+ sta(zp_b8);
+ lda(zp_b9);
+ sta(zp_bd);
+ adc(inbuf_adr_h);
+ sta(zp_b9);
+ ldy_i(0x00);
+ lda(L71B6);
+ and_i(0x07);
+ bne(L77E1);
+ lda_ind_y(zp_b8);
+ sta(acc16_h);
+ iny();
+ lda_ind_y(zp_b8);
+ sta(acc16_l);
+L77C0:
+ lda_i(0x0F);
+ sec();
+ sbc(shift_counter);
+ tax();
+L77C7:
+ lsr(acc16_h);
+ ror(acc16_l);
+ dex();
+ bpl(L77C7);
+ lda(shift_counter);
+ clc();
+ adc(L71B6);
+ sta(L71B6);
+ lda_i(0x00);
+ adc(L71B7);
+ sta(L71B7);
+ rts();
+
+/* ---------------------------------------------------------------------------- */
+L77E1:
+ ldx_i(0x02);
+L77E3:
+ lda_ind_y(zp_b8);
+ sta_abs_x(L7189);
+ iny();
+ dex();
+ bpl(L77E3);
+ lda(L71B6);
+ and_i(0x07);
+ tax();
+ dex();
+L77F3:
+ asl(L7189);
+ rol(L718A);
+ rol(L718B);
+ dex();
+ bpl(L77F3);
+ lda(L718A);
+ sta(acc16_l);
+ lda(L718B);
+ sta(acc16_h);
+ jmp(L77C0);
+}
+
+void L7A19(void) {
+L7A19:
+ lda(alf_hdr_compsize2);
+ ora(alf_hdr_compsize3);
+ beq(L7A28);
+L7A21:
+ jsr(readblock);
+ Y = 1; /* CIO would set this */
+ jsr(L7A5D);
+ rts();
+L7A28:
+ lda(alf_hdr_compsize1);
+ cmp(buf_len_h);
+ bcc(L7A40);
+ beq(L7A34);
+ bcs(L7A21);
+L7A34:
+ lda(alf_hdr_compsize0);
+ cmp(buf_len_l);
+ bcc(L7A40);
+ beq(L7A40);
+ bcs(L7A21);
+L7A40:
+ lda(alf_hdr_compsize0);
+ sta(buf_len_l);
+ lda(alf_hdr_compsize1);
+ sta(buf_len_h);
+ lda(buf_len_l);
+ ora(buf_len_h);
+ beq(L7A57);
+ jsr(readblock);
+ Y = 1; /* CIO would set this */
+L7A57:
+ ldy_i(0x88);
+ jsr(L7A5D);
+ rts();
+}
+
+void L7A5D(void) {
+L7A5D:
+ lda(alf_hdr_compsize0);
+ sec();
+ sbc(buf_len_l);
+ sta(alf_hdr_compsize0);
+ lda(alf_hdr_compsize1);
+ sbc(buf_len_h);
+ sta(alf_hdr_compsize1);
+ lda(alf_hdr_compsize2);
+ sbc_i(0x00);
+ sta(alf_hdr_compsize2);
+ rts();
+}
+
+void setup_io_bufs(void) {
+setup_io_bufs:
+ lda(MEMTOP_lo);
+ sec();
+ sbc_i(0xDC);
+ sta(inbuf_len_l);
+ lda(MEMTOP_hi);
+ sbc_i(0x7F);
+ sta(inbuf_len_h);
+ lsr(inbuf_len_h);
+ ror(inbuf_len_l);
+ lda(inbuf_len_h);
+ cmp_i(0x1F);
+ bcc(L79A0);
+ lda_i(0x00);
+ sta(inbuf_len_l);
+ lda_i(0x1F);
+ sta(inbuf_len_h);
+L79A0:
+ lda_i(0xDC);
+ sta(inbuf_adr_l);
+ lda_i(0x7F);
+ sta(inbuf_adr_h);
+ lda_i(0xDC);
+ clc();
+ adc(inbuf_len_l);
+ sta(outbuf_adr_l);
+ sta(outbuf_ptr_l);
+ lda_i(0x7F);
+ adc(inbuf_len_h);
+ sta(outbuf_adr_h);
+ sta(outbuf_ptr_h);
+ rts();
+}
+
+void init_counters(void) {
+init_counters:
+ lda_i(0x09);
+ sta(shift_counter);
+ lda_i(0x00);
+ sta(L71AD);
+ lda_i(0x02);
+ sta(L71AE);
+ lda_i(0x02);
+ sta(L717B);
+ lda_i(0x01);
+ sta(L717C);
+ rts();
+}
+
+/* save decrunched byte in outbuf, update checksum, write outbuf if full */
+void store_outbyte(void) {
+ ldy_i(0x00);
+ lda(acc16_l);
+ sta_ind_y(outbuf_ptr_l);
+ clc();
+ adc(cksum_l);
+ sta(cksum_l);
+ lda_i(0x00);
+ adc(cksum_h);
+ sta(cksum_h);
+ inc(outbuf_ptr_l);
+ bne(out_ptr_hi_ok);
+ inc(outbuf_ptr_h);
+out_ptr_hi_ok:
+ inc(outbuf_len_l);
+ bne(out_len_hi_ok);
+ inc(outbuf_len_h);
+out_len_hi_ok:
+ lda(outbuf_len_h);
+ cmp(inbuf_len_h);
+ bcc(outbuf_not_full);
+ lda(outbuf_len_l);
+ cmp(inbuf_len_l);
+ bcc(outbuf_not_full);
+ lda(outbuf_adr_l);
+ sta(buf_adr_l);
+ lda(outbuf_adr_h);
+ sta(buf_adr_h);
+ lda(outbuf_len_l);
+ sta(buf_len_l);
+ lda(outbuf_len_h);
+ sta(buf_len_h);
+ /* TODO: call C writeblock() */
+ ldx_i(0x30);
+ jsr(writeblock);
+ bpl(init_outbuf);
+ #if 0
+/* ldx #<emsg_checksum : ldy #>emsg_checksum */
+print_emsg_write_output_2:
+ ldx_i(0xD5);
+ ldy_i(0x72);
+ jsr(printstr);
+cleanup_and_exit:
+ pla();
+ pla();
+ pla();
+ pla();
+ jmp(exit);
+ #endif
+ chksum_err();
+ exit(1);
+/* ---------------------------------------------------------------------------- */
+init_outbuf:
+ lda(outbuf_adr_l);
+ sta(outbuf_ptr_l);
+ lda(outbuf_adr_h);
+ sta(outbuf_ptr_h);
+ lda_i(0x00);
+ sta(outbuf_len_l);
+ sta(outbuf_len_h);
+outbuf_not_full:
+ rts();
+}
+
+
+/* ---------------------------------------------------------------------------- */
+/* push 2 byte 'register' to software stack */
+void push_acc16(void) {
+push_acc16:
+ ldy_i(0x00);
+ lda(acc16_l);
+ sta_ind_y(stackptr_l);
+ iny();
+ lda(acc16_h);
+ sta_ind_y(stackptr_l);
+ lda(stackptr_l);
+ clc();
+ adc_i(0x02);
+ sta(stackptr_l);
+ bcc(L790C);
+ inc(stackptr_h);
+L790C:
+ lda(stackptr_h);
+ cmp_i(0x70);
+ bcc(L791C);
+ #if 0
+/* ldx #<emsg_stk_overrun : ldy #>emsg_stk_overrun */
+print_emsg_stk_overrun:
+ ldx_i(0x25);
+ ldy_i(0x79);
+ jsr(printstr);
+ jmp(cleanup_and_exit);
+ #endif
+ fprintf(stderr, "%s: stack overrun\n", self);
+ exit(1);
+/* ---------------------------------------------------------------------------- */
+L791C:
+ inc(L71AF);
+ bne(L7924);
+ inc(L71B0);
+L7924:
+ rts();
+}
+
+void pop_acc16(void) {
+/* pop 2 byte 'register' from software stack */
+pop_acc16:
+ lda(stackptr_l);
+ sec();
+ sbc_i(0x02);
+ sta(stackptr_l);
+ lda(stackptr_h);
+ sbc_i(0x00);
+ sta(stackptr_h);
+ ldy_i(0x00);
+ lda_ind_y(stackptr_l);
+ sta(acc16_l);
+ iny();
+ lda_ind_y(stackptr_l);
+ sta(acc16_h);
+ lda(stackptr_h);
+ cmp_i(0x60);
+ bcs(L796C);
+ #if 0
+/* ldx #<emsg_stk_underrun : ldy #>emsg_stk_underrun */
+print_emsg_stk_underrun:
+ ldx_i(0x34);
+ ldy_i(0x79);
+ jsr(printstr);
+ jmp(cleanup_and_exit);
+ #endif
+ fprintf(stderr, "%s: stack underrun\n", self);
+ exit(1);
+/* ---------------------------------------------------------------------------- */
+L796C:
+ lda(L71AF);
+ bne(L7974);
+ dec(L71B0);
+L7974:
+ dec(L71AF);
+ rts();
+}
+
+void L79E7(void) {
+L79E7:
+ lda(buf_adr_l);
+ clc();
+ adc(buf_len_l);
+ sta(zp_be);
+ lda(buf_adr_h);
+ adc(buf_len_h);
+ sta(zp_bf);
+ ldx_i(0x02);
+ bne(L7A0A);
+L79FC:
+ ldy_i(0x00);
+ tya();
+ sta_ind_y(zp_be);
+ inc(zp_be);
+ bne(L7A07);
+ inc(zp_bf);
+L7A07:
+ dex();
+ bmi(L7A18);
+L7A0A:
+ lda(zp_bf);
+ cmp(outbuf_adr_h);
+ bcc(L79FC);
+ lda(zp_be);
+ cmp(outbuf_adr_l);
+ bcc(L79FC);
+L7A18:
+ rts();
+}
+
+void L7899(void) {
+L7899:
+ lda(zp_b4);
+ sta(zp_b0);
+ lda(zp_b5);
+ sta(zp_b1);
+ asl(zp_b0);
+ rol(zp_b1);
+ lda(zp_b0);
+ clc();
+ adc(zp_b4);
+ sta(zp_b0);
+ lda(zp_b1);
+ adc(zp_b5);
+ sta(zp_b1);
+ lda(zp_b0);
+ clc();
+ adc(L7181);
+ sta(zp_b0);
+ lda(zp_b1);
+ adc(L7182);
+ sta(zp_b1);
+ rts();
+}
+
+void L78C2(void) {
+L78C2:
+ lda(L717B);
+ sta(zp_b4);
+ lda(L717C);
+ sta(zp_b5);
+ jsr(L7899);
+ lda(L7177);
+ sta(acc16_l);
+ ldy_i(0x02);
+ sta_ind_y(zp_b0);
+ lda(L7179);
+ sta(acc16_l);
+ lda(L717A);
+ sta(acc16_h);
+ ldy_i(0x00);
+ lda(acc16_l);
+ sta_ind_y(zp_b0);
+ iny();
+ lda(acc16_h);
+ sta_ind_y(zp_b0);
+ inc(L717B);
+ bne(L78F5);
+ inc(L717C);
+L78F5:
+ rts();
+}
+
+void write_output(void) {
+write_output:
+ lda(outbuf_len_l);
+ ora(outbuf_len_h);
+ bne(have_output);
+ rts();
+/* ---------------------------------------------------------------------------- */
+have_output:
+ lda(outbuf_adr_l);
+ sta(buf_adr_l);
+ lda(outbuf_adr_h);
+ sta(buf_adr_h);
+ lda(outbuf_len_l);
+ sta(buf_len_l);
+ lda(outbuf_len_h);
+ sta(buf_len_h);
+ ldx_i(0x30);
+ jsr(writeblock);
+ rts();
+}
diff --git a/src/io.c b/src/io.c
new file mode 100644
index 0000000..9b8e40e
--- /dev/null
+++ b/src/io.c
@@ -0,0 +1,92 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <f65.h>
+#include "unalf.h"
+#include "addrs.h"
+
+static int headers_read = 0;
+
+static void die_arc(void) {
+ fprintf(stderr, "%s: this is an ARC file, not ALF\n", self);
+ exit(1);
+}
+
+static void die_not_alf(void) {
+ fprintf(stderr, "%s: not an ALF file\n", self);
+ exit(1);
+}
+
+static void eof_junk(void) {
+ fprintf(stderr, "%s: junk at EOF (ignoring)\n", self);
+}
+
+/* return 1 if a header is read, 0 if not */
+int read_alf_header(void) {
+ u8 h1, h2;
+ int bytes;
+
+ bytes = fread(mem + alf_header, 1, 29, in_file);
+
+ if(!bytes) {
+ if(headers_read)
+ return 0;
+ else
+ die_not_alf();
+ } else if(bytes < 29) {
+ if(headers_read) {
+ eof_junk();
+ return 0;
+ } else {
+ die_not_alf();
+ }
+ }
+
+ h1 = mem[alf_header];
+ h2 = mem[alf_hdr_sig];
+
+ if(h1 == 0x1a) {
+ if(h2 < 0x0f) die_arc();
+ if(h2 == 0x0f) {
+ headers_read++;
+ return 1; /* signature matches */
+ }
+ }
+
+ if(headers_read)
+ eof_junk();
+ else
+ die_not_alf();
+
+ return 0;
+}
+
+/* read buf_len_l/h bytes into buf_adr_l/h, then store the number
+ of bytes actually read in buf_len_l/h. TODO: what about EOF? */
+void readblock(void) {
+ int bytes, len, bufadr;
+ u8 *buf;
+
+ bufadr = dpeek(buf_adr_l);
+ buf = mem + bufadr;
+ len = dpeek(buf_len_l);
+
+ // fprintf(stderr, "readblock, bufadr = $%04x, len = $%04x\n", bufadr, len);
+
+ bytes = fread(buf, 1, len, in_file);
+ dpoke(buf_len_l, bytes);
+}
+
+/* mirror of readblock() */
+void writeblock(void) {
+ int bytes, len, bufadr;
+ u8 *buf;
+
+ bufadr = dpeek(buf_adr_l);
+ buf = mem + bufadr;
+ len = dpeek(buf_len_l);
+
+ // fprintf(stderr, "writeblock, bufadr = $%04x, len = $%04x\n", bufadr, len);
+
+ bytes = fwrite(buf, 1, len, out_file);
+ dpoke(buf_len_l, bytes);
+}
diff --git a/src/listalf.c b/src/listalf.c
new file mode 100644
index 0000000..051f2fe
--- /dev/null
+++ b/src/listalf.c
@@ -0,0 +1,118 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <f65.h>
+#include "unalf.h"
+#include "addrs.h"
+
+static const char *monthnames[] = {
+ "???",
+ "Jan", "Feb", "Mar", "Apr",
+ "May", "Jun", "Jul", "Aug",
+ "Sep", "Oct", "Nov", "Dec",
+ "???", "???", "???",
+};
+
+u16 getword(int offs) {
+ return mem[offs] | (mem[offs + 1] << 8);
+}
+
+unsigned int getquad(int offs) {
+ return getword(offs) | ((getword(offs + 2) << 16));
+}
+
+/* see Arcinfo for details */
+void format_msdos_time(char *buf) {
+ u16 t, hour, min;
+ char ampm;
+
+ t = getword(alf_hdr_time0);
+
+ /* don't bother with seconds (arc doesn't print them either) */
+ hour = t >> 11;
+ min = (t >> 5) & 0x3f;
+
+ /* midnight prints as 12pm, not 12am... but 0:01 (1 min past
+ midnight) prints as 12:01am */
+ if(hour == 0 && min == 0) hour = 24;
+
+ ampm = (hour > 11 ? 'p' : 'a');
+ hour %= 12;
+
+ /* midnight and noon print as 12, not 0 */
+ if(hour == 0) hour = 12;
+
+ sprintf(buf, "%2d:%02d%c", hour, min, ampm);
+}
+
+/* see Arcinfo for details */
+void format_msdos_date(char *buf) {
+ u16 d, year, month, day;
+
+ d = getword(alf_hdr_date0);
+
+ /* year actually ranges 1980 to 2107... */
+ year = (d >> 9) + 1980;
+ year %= 100; /* ...but only print last 2 digits */
+
+ /* valid months range 1 to 12. values 0, 13, 14, 15 are possible
+ but invalid (print as ???) */
+ month = (d >> 5) && 0x0f;
+
+ /* valid day range is 1 to 31. 0 is invalid, print as-is. */
+ day = d & 0x1f;
+
+ sprintf(buf, "%2d %3s %02d", day, monthnames[month], year);
+}
+
+/* small files may be "compressed" larger than the original, so this
+ has to return a signed type. */
+static int comp_percent(unsigned int orig, unsigned int comp) {
+ if(orig == 0) return 0; /* no division by zero please */
+ return 100 - (int)((float)comp / (float)orig * 100.0);
+}
+
+/* output similar to "arc v", except we omit the compression type column
+ since there's only one type. also, don't call the checksum a CRC, it
+ isn't. */
+void list_alf(void) {
+ unsigned int c = 0, orig_size, comp_size, total_osize = 0, total_csize = 0;
+ char buf[100];
+ char *filename;
+
+ puts("Name Length Size now Comp Date Time CkSum");
+ puts("============ ======== ======== ==== ========= ====== =====");
+
+ while(read_alf_header()) {
+ c++;
+ orig_size = getquad(alf_hdr_origsize0);
+ comp_size = getquad(alf_hdr_compsize0);
+ filename = (char *)(mem + alf_hdr_filename);
+
+ total_osize += orig_size;
+ total_csize += comp_size;
+
+ printf("%-12s ", filename);
+ printf("%8d ", orig_size);
+ printf("%8d ", comp_size);
+ printf("%3d%% ", comp_percent(orig_size, comp_size));
+ format_msdos_date(buf);
+ printf("%9s ", buf);
+ format_msdos_time(buf);
+ printf("%6s ", buf);
+ printf(" %04x", getword(alf_hdr_cksum_l));
+ putchar('\n');
+
+ if(fseek(in_file, comp_size, SEEK_CUR) != 0) {
+ fputs(self, stderr);
+ perror(": fseek");
+ exit(1);
+ }
+ }
+
+ fputs(" ==== ======== ======== ====\nTotal ", stdout);
+ printf("%4d ", c);
+ printf("%8d ", total_osize);
+ printf("%8d ", total_csize);
+ printf("%3d%% ", comp_percent(total_osize, total_csize));
+ putchar('\n');
+}
diff --git a/src/unalf.1 b/src/unalf.1
new file mode 100644
index 0000000..3ed769b
--- /dev/null
+++ b/src/unalf.1
@@ -0,0 +1,136 @@
+.\" 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 "UNALF" 1 "2025-11-12" "0.0.0" "Urchlay's Atari 8-bit Tools"
+.SH NAME
+unalf \- extract Atari 8-bit ALF archives
+.\" RST source for unalf(1) man page. Convert with:
+.
+.\" rst2man.py unalf.rst > unalf.1
+.
+.SH SYNOPSIS
+.sp
+unalf [\fB\-l\fP] \fBalf\-file\fP
+.SH DESCRIPTION
+.sp
+\fBunalf\fP lists or extracts the contents of an \fIALF\fP archive.
+.sp
+\fIALF\fP is a compressed archive format similar to \fBarc\fP(1), though
+not compatible with it. It was used on the Atari 8\-bit platform
+beginning in the late 1980s.
+.sp
+Extracted files are written to the current directory. Existing files
+are overwritten without warning.
+.SH OPTIONS
+.sp
+For compatibility with \fBarc\fP(1) usage, the \fB\-\fP in front of
+option letters is optional.
+.INDENT 0.0
+.TP
+.B \-l
+List contents of archive, but do not extract. The output format
+is similar to that of \fBarc \-v\fP, minus the \fIStowage\fP column, since
+\fIALF\fP doesn\(aqt support multiple compression types. The date and time
+are displayed, but in most .alf files these are the default values
+of "8 Jan 82 12:24a".
+.TP
+.B \-v
+Same as \fB\-l\fP\&.
+.UNINDENT
+.INDENT 0.0
+.TP
+.B \fB\-e\fP, \fB\-x\fP
+Extract files. This is actually a no\-op, as extraction is the
+default action.
+.UNINDENT
+.SH EXIT STATUS
+.sp
+\fB0\fP for success, \fB1\fP for failure.
+.SH DIAGNOSTICS
+.sp
+Besides the standard error messages such as "no such file or directory":
+.INDENT 0.0
+.TP
+.B \fBunalf: this is an ARC file, not ALF\fP
+Self\-explanatory. Use the \fBarc\fP(1) utility for this file.
+.TP
+.B \fBunalf: not an ALF file\fP
+Self\-explanatory. Either the file is too small (less than 29 bytes)
+or its first two bytes don\(aqt match the \fIALF\fP signature \fB0x1a\fP \fB0x0f\fP\&.
+.TP
+.B \fBunalf: junk at EOF (ignored)\fP
+Usually this is caused by the .alf file being stored on a CP/M disk
+at some time, or by a dumb file transfer protocol. Either way, the
+file gets padded to the block size of the filesystem or protocol.
+Usually, the padding characters are \fB0x1a\fP, aka ASCII control\-Z.
+.sp
+If you see this message, you can ignore it. It\(aqs intended to let
+you know that this .alf file can\(aqt be appended to by the \fBALF.COM\fP
+aka \fBLZ.COM\fP Atari utility.
+.TP
+.B \fBunalf: checksum error on\fP \fI<file>\fP
+The archive is corrupt. If \fI<file>\fP is a text file, it may be
+partially readable. If it\(aqs an executable or other binary file, it\(aqs
+probably unrecoverable.
+.UNINDENT
+.SH NOTES
+.sp
+This \fBunalf\fP is 100% compatible with the original Atari \fBUNALF.COM\fP
+aka \fBDZ.COM\fP, with the following differences:
+.INDENT 0.0
+.IP \(bu 2
+There is no interactive mode. The file to extract must be given as
+a command\-line argument.
+.IP \(bu 2
+Files are always extracted to the current directory. The original
+\fBUNALF.COM\fP accepted a 2nd argument or prompted for the output directory.
+.IP \(bu 2
+This \fBunalf\fP is capable of listing the contents of an archive
+without extracting it.
+.IP \(bu 2
+Turning the screen off for speed makes no sense on modern operating
+systems, so there\(aqs no option for that.
+.UNINDENT
+.sp
+Neither this \fBunalf\fP nor \fBUNALF.COM\fP actually use the dates/times
+stored in the archive. Extracted files will have their timestamps set
+to the current date/time.
+.SH COPYRIGHT
+.sp
+\fBunalf\fP is released under the WTPFL: Do WTF you want with this.
+.SH AUTHORS
+.INDENT 0.0
+.IP B. 3
+Watson <\fI\%urchlay@slackware.uk\fP>
+.UNINDENT
+.SH SEE ALSO
+.sp
+TODO
+.\" Generated by docutils manpage writer.
+.
diff --git a/src/unalf.c b/src/unalf.c
new file mode 100644
index 0000000..ba6405e
--- /dev/null
+++ b/src/unalf.c
@@ -0,0 +1,66 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <f65.h>
+#include "unalf.h"
+
+FILE *in_file, *out_file;
+char *in_filename, *self;
+
+static void usage(void) {
+ printf("usage: %s [-l] file.alf\n", self);
+ exit(1);
+}
+
+static void set_self(char *argv0) {
+ char *p;
+
+ self = argv0;
+ p = strrchr(self, '/');
+ if(p) self = p + 1;
+}
+
+int main(int argc, char **argv) {
+ char opt = 0;
+ int listonly = 0;
+
+ set_self(argv[0]);
+
+ if(argc == 1) {
+ fprintf(stderr, "%s: missing required argument(s)\n", self);
+ usage();
+ } else if(argc > 3) {
+ fprintf(stderr, "%s: too many arguments\n", self);
+ usage();
+ } else if(argc == 2) {
+ in_filename = argv[1];
+ } else { /* argc == 3 */
+ in_filename = argv[2];
+ if(argv[1][0] == '-')
+ opt = argv[1][1];
+ else
+ opt = argv[1][0];
+ if(opt == 'l' || opt == 'v') {
+ listonly = 1;
+ } else if(opt == 'x' || opt == 'e') {
+ /* NOP */
+ } else {
+ fprintf(stderr, "%s: unknown option '-%c'\n", self, opt);
+ usage();
+ }
+ }
+
+ if(!(in_file = fopen(in_filename, "rb"))) {
+ fprintf(stderr, "%s: ", self);
+ perror(in_filename);
+ exit(1);
+ }
+
+ if(listonly)
+ list_alf();
+ else
+ extract_alf();
+
+ exit(0);
+}
diff --git a/src/unalf.h b/src/unalf.h
new file mode 100644
index 0000000..855ca80
--- /dev/null
+++ b/src/unalf.h
@@ -0,0 +1,40 @@
+/* converted from asm */
+void uncrunch_file(void);
+void setup_io_bufs(void);
+void L7A19(void);
+void L79E7(void);
+void L76D0(void);
+void write_output(void);
+void init_counters(void);
+void store_outbyte(void);
+void push_acc16(void);
+void L7899(void);
+void pop_acc16(void);
+void L78C2(void);
+void L7A19(void);
+void L79E7(void);
+void writeblock(void);
+void L7A5D(void);
+
+/* io.c - asm rewritten in C */
+int read_alf_header(void);
+void readblock(void);
+void writeblock(void);
+void write_output(void);
+
+/* io.c - just C */
+void open_output(void);
+
+/* listalf.c */
+void list_alf(void);
+
+/* extract.c */
+void extract_alf(void);
+void dpoke(int addr, u16 value);
+u16 dpeek(int addr);
+
+/* unalf.c */
+extern char *self;
+extern FILE *out_file;
+extern FILE *in_file;
+extern char *in_filename;
diff --git a/src/unalf.rst b/src/unalf.rst
new file mode 100644
index 0000000..774ddf2
--- /dev/null
+++ b/src/unalf.rst
@@ -0,0 +1,124 @@
+.. RST source for unalf(1) man page. Convert with:
+.. rst2man.py unalf.rst > unalf.1
+
+.. |version| replace:: 0.0.0
+.. |date| date::
+
+=====
+unalf
+=====
+
+--------------------------------
+extract Atari 8-bit ALF archives
+--------------------------------
+
+:Manual section: 1
+:Manual group: Urchlay's Atari 8-bit Tools
+:Date: |date|
+:Version: |version|
+
+SYNOPSIS
+========
+
+unalf [**-l**] **alf-file**
+
+DESCRIPTION
+===========
+
+**unalf** lists or extracts the contents of an *ALF* archive.
+
+*ALF* is a compressed archive format similar to **arc**\(1), though
+not compatible with it. It was used on the Atari 8-bit platform
+beginning in the late 1980s.
+
+Extracted files are written to the current directory. Existing files
+are overwritten without warning.
+
+OPTIONS
+=======
+
+For compatibility with **arc**\(1) usage, the **-** in front of
+option letters is optional.
+
+-l
+ List contents of archive, but do not extract. The output format
+ is similar to that of **arc -v**, minus the *Stowage* column, since
+ *ALF* doesn't support multiple compression types. The date and time
+ are displayed, but in most .alf files these are the default values
+ of "8 Jan 82 12:24a".
+
+-v
+ Same as **-l**.
+
+**-e**, **-x**
+ Extract files. This is actually a no-op, as extraction is the
+ default action.
+
+EXIT STATUS
+===========
+
+**0** for success, **1** for failure.
+
+DIAGNOSTICS
+===========
+
+Besides the standard error messages such as "no such file or directory":
+
+**unalf: this is an ARC file, not ALF**
+ Self-explanatory. Use the **arc**\(1) utility for this file.
+
+**unalf: not an ALF file**
+ Self-explanatory. Either the file is too small (less than 29 bytes)
+ or its first two bytes don't match the *ALF* signature **0x1a** **0x0f**\.
+
+**unalf: junk at EOF (ignored)**
+ Usually this is caused by the .alf file being stored on a CP/M disk
+ at some time, or by a dumb file transfer protocol. Either way, the
+ file gets padded to the block size of the filesystem or protocol.
+ Usually, the padding characters are **0x1a**, aka ASCII control-Z.
+
+ If you see this message, you can ignore it. It's intended to let
+ you know that this .alf file can't be appended to by the **ALF.COM**
+ aka **LZ.COM** Atari utility.
+
+**unalf: checksum error on** *<file>*
+ The archive is corrupt. If *<file>* is a text file, it may be
+ partially readable. If it's an executable or other binary file, it's
+ probably unrecoverable.
+
+NOTES
+=====
+
+This **unalf** is 100% compatible with the original Atari **UNALF.COM**
+aka **DZ.COM**, with the following differences:
+
+- There is no interactive mode. The file to extract must be given as
+ a command-line argument.
+
+- Files are always extracted to the current directory. The original
+ **UNALF.COM** accepted a 2nd argument or prompted for the output directory.
+
+- This **unalf** is capable of listing the contents of an archive
+ without extracting it.
+
+- Turning the screen off for speed makes no sense on modern operating
+ systems, so there's no option for that.
+
+Neither this **unalf** nor **UNALF.COM** actually use the dates/times
+stored in the archive. Extracted files will have their timestamps set
+to the current date/time.
+
+COPYRIGHT
+=========
+
+**unalf** is released under the WTPFL: Do WTF you want with this.
+
+AUTHORS
+=======
+
+B. Watson <urchlay@slackware.uk>
+
+SEE ALSO
+========
+
+TODO