home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Oakland CPM Archive
/
oakcpm.iso
/
cpm
/
arc-lbr
/
unarc16s.ark
/
UNARC.Z80
< prev
Wrap
Text File
|
1987-03-27
|
126KB
|
4,126 lines
TITLE UNARC CP/M Archive File Extractor
IDENT MACRO
DB 'UNARC 1.6 27 Mar 87'
ENDM
; (Remember to update version/date here and maintain history log below)
SELF MACRO ; Self-unpacking archive file name
DB 'UNARC16'
ENDM
COPR MACRO
DB 'Copyright (C) 1986, 1987 by Robert A. Freed'
ENDM
.COMMENT |
NOTICE: This program is the copyrighted property of its author -- it
is NOT in the public domain. HOWEVER... Free use, distribution, and
modification of this program is permitted (and encouraged), subject to
the following conditions:
(1) Such use or distribution must be for non-profit purposes only.
(2) The author's copyright notice may not be altered or removed.
(3) Modifications to this program may not be distributed without
notification of and approval by the author.
(4) The source program code may not be used, in whole or in part,
in any other publicly-distributed or derivative work without
similar notification and approval.
No fee is requested or expected for the use and distribution of this
program subject to the above conditions. The author reserves the right
to modify these conditions for any future revisions of this program.
Questions, comments, suggestions, commercial inquiries, and bug reports
or fixes are welcomed by the author:
Bob Freed
62 Miller Rd.
Newton Centre, MA 02159
Telephone (617) 332-3533
|
PAGE
SUBTTL Modification History
.COMMENT |
1.6 27 Mar 87 (RAF)
- Murphy's Law strikes again: Within hours after the release of version
1.5, a bug was discovered. Incorrect CRC error messages are generated
during file extraction in some situations. This was caused by failure
to clear carry before a 16-bit subtract (SBC HL,DE), which we changed
inadvertantly in 1.42. (So much for Beta-testing!) Such faulty error
messages occur only for disk file extraction, not when the 'C' command
option is used to check an archive. Furthermore, the bug occurs only
when (1) a file contains an odd number of 128-byte records and (2) the
BDOS returns from the last write-record call with carry set. [Note of
interest: The CP/M 2.2 BDOS returns with carry set only if the output
drive is different than the current default drive. This assumes, of
course, that no RSX-type system extensions are in place to intercept
BDOS calls: We would have caught this bug, but for such a system
extension which always clears carry before returning from BDOS calls.]
Our thanks to Tom Brady for reporting this one.
- Zero-fills last record of .COM file. (Not needed with Z80ASM and/or
SLRNK, but provided so that M80/L80 will generate identical output to
that produced by the SLR Systems' tools.)
1.5 24 Mar 87 (RAF)
- UNARC is now distributed as a self-unpacking archive, UNARC15.ARK.
This requires: (1) the non-z80 version (UNARCA.COM) must be the FIRST
file in the archive, (2) UNARCA.COM must be stored in UNPACKED form
using compression version 1, (3) the header for UNARCA.COM must be
preceded by the SINGLE byte, 0C3H (opcode for unconditional jump),
and (4) the archive must be copied or renamed to UNARCxx.COM on the
current disk drive (xx = current version, i.e. UNARC15.COM for this
release). Then, the file is executed with a single optional parameter
specifying the disk drive to use for extracting all files (defaults to
current drive). For example, assuming UNARC15.ARK is on drive B:
A>B: ; Set current drive for UNARC15.ARK
B>REN UNARC15.COM=UNARC15.ARK ; Rename it to UNARC15.COM
B>UNARC15 [d:] ; Extract files to current drive [or d:]
- Corrects non-Z80 version emulation of the Z80 16-bit add and subtract
instructions (ADC_HL and SBC_HL macros), to properly set the Z(ero)
condition flag. Previously, Z reflected only the upper byte of the
16-bit result and was incorrect for non-zero results less than 256.
This caused a serious bug (in the non-Z80 version, UNARCA.COM, only):
Failure to output the last 1-255 bytes of an extracted file in cases
where the final output buffer size was less than 256 bytes. (In
particular, ALL files less than 256 bytes in length could not be
extracted.) Thanks to Barry Kaufman (Multipath, Inc., P.O. Box 395,
Montville, NJ 07045) for bringing this to our attention. [This
tends to confirm our opinion regarding the prevalence of non-Z80
systems, since this bug has been present but unreported since the
release of UNARC 1.2.]
- Alters the interpretation of the USELUX definition in the UNARCOVL.ASM
overlay file. USELUX = YES now restricts file typeout buffering to
one page (equivalent to TYPGS = 1) instead of altering the upper TPA
limit (CCPSV value). This eliminates the LUXSIZ definition (which
specified the size of the LUX RSX-type resident code) and avoids the
confusion introduced by recent multiple new versions of LUX from
different authors.
- Corrects CP/M 2.2 tab alignment for the first displayed line of file
typeout after continuing from a screen pause ([more] message).
- Adds explicit check for CTRL-S (suspend output) in CABORT, to handle
cases where standard CP/M 2.2 BDOS misses these. (Also masks console
input characters to 7 bits, in case this is not done, as it should be,
by BIOS. This is an attempt to solve reports of failure to recognize
CTRL-C and CTRL-S on some systems.)
- Allows 0-length "crunched" files (i.e. with no code size byte). [The
various MS-DOS ARC utilities differ in their handling of 0-length
files. SEA's ARC generates unpacked (version 2), which we feel is
esthetically best, and ARCA generates packed (version 3). But PKARC
generates crunched (version 8), which was regurgitated by earlier
UNARC versions due to the absence of the code size byte.]
- Minor code improvements for version 1.42 changes.
- Eliminates DS directives at end of file to avoid wasted space when
linked with L80 (as opposed to SLRNK, which handles trailing
uninitialized data intelligently). This also permits overlaying of
the self-unpacking initialization code by data in the non-Z80 version.
1.42 07 Jan 87 (RAF)
Interim Beta-test release:
- Supports 'squashed' files (compression version 9) generated by PKARC
version 2.0, as defined by Phil Katz' document file SQSHINFO.DOC,
dated 12/27/86. (Katz is certainly doing his best to make life
interesting for us.) Note: We've made an educated guess that Katz'
handling of bypassed output codes after adaptive reset is identical to
that of crunched (version 8) files. (Since there is no requirement
for ARC512 compatibility here, he could have handled this in a less
brain-damaged manner. However, on the basis of two very limited test
examples, our assumption appears to be true.) This compression method
requires a minimum TPA size of 30K (Z80) or 31K (8080) for extraction
(worst case yet).
- Lists total of CRC values (mod 64K), as per suggestion of Steven
Greenberg. This provides a simple single checksum value for comparing
files created by different archive programs. (Since the CRC is
computed over the UNcompressed files, this value should be the same
for all archives created from the same set of input files, independent
of any particular variations in file order or compression methods.)
- Adds trailing command line option 'C' to check the validity of one or
more (or all, via *.*) archive members (i.e. to extract them for
purposes of CRC and length checking, without storing them as disk
files). This is a quick hack, in response to a suggestion by Keith
Petersen. This option is currently allowed only if the wheel byte is
non-zero. I.e., it is ignored in restricted RCP/M versions (although
there is no reason why this could not be allowed, subject to a Sysop-
definable patch byte). Also, the limited command line syntax prevents
the simultaneous use of the 'N' option for non-paged typeout (i.e.
screen pauses will always occur). Both of these limitations will be
eliminated with addition of enhanced command line processing
(including du: user area syntax) in a future release.
- Disallows use of 'P' option for printing files in restricted (RCP/M)
versions. (We inadvertantly failed to implement this as intended in
the 1.41 release. Hopefully, the recipients of that release will
honor our limited-distribution request!) Note that the statement
accompanying the 1.41 release is slightly incorrect: Both 'P' and 'C'
options are processed ONLY if the wheel byte is non-zero (and in the
absence of an output file drive, which always causes extraction to a
disk file); a zero HODRV byte does not, in itself, inhibit these.
- Makes .ARK the preferred default archive filetype. I.e., first open
attempt uses .ARK; second attempt tries .ARC if first is unsuccessful.
- Expands help message usage examples a bit (now that 4K limit is not a
concern).
Note: The additions in 1.41 and 1.42 have pushed the size of the Z80
version UNARC.COM file above 4K (which means 6K or 8K disk space on most
systems). Such is life (and progress): We've resisted this for a long
time, but it now seems unavoidable. The UNARCOVL.ASM overlay file
distributed with UNARC 1.4 remains applicable for these releases.
1.41 14 Dec 86 (RAF)
Special limited-distribution release:
- Adds trailing command line option 'P' to allow printing of an archive
member file on CP/M list device. This is a quick hack, in response to
a user request (Craig Arno, Seattle), to allow direct printing of
highly-compressed binary plot images (e.g. 1+ MB files which crunch
to < 5% of their original size). Accordingly, ALL data is passed to
the printer in 8-bit form, with no filtering by UNARC (including ^Z).
This option is allowed under the same conditions as disk extraction
(non-zero HODRV and wheel byte), and the files which may be printed
are subject to the filetype exclusion table for typeout.
- Defers initializing listing totals until after CHECK is called.
(This moved in 1.4 to accomodate LPS, without realizing it might
cause a problem, albeit with an insignificant probability. LPS
is now allocated in code and cleared by CHECK.)
1.4 21 Nov 86 (RAF)
We had hoped NOT to release another update of this program, but to
replace it entirely by three new programs with enhanced functionality
(UNARK, ADIR, and ATYPE), in conjunction with the upcoming release of
the CP/M archive file builder (NOAH). However, (sigh).....
Corrects bug (exhibited with .ARC's created by version 1.1 or later of
Phil Katz' PKARC program for MS-DOS) which caused files to be extracted
incorrectly (with output file length and CRC warnings) due to string
table reset codes appearing early in crunched files (i.e. before the
output code length reaches 12 bits). Thanks to Keith Petersen for
identifying and notifying us of this problem.
And, while we're at it.....
Adds paging of all displayed output, controlled by non-zero patch byte
specifying screen lines between pauses (TYLPS, default value = 23).
This is essentially the feature added by 'Larry Smith' (see version 1.3
below), but we've been able to do it (with enhancements) and still keep
the (Z80 version) .COM file below 4K (just!). Causes '[more]' message
to appear at bottom of screen. Space bar scrolls one more line, ^C
aborts, anything else scrolls one more screenful. (LINE FEED may be
used to avoid overprinting the '[more]' line.) May be defeated for
continuous typeout by trailing 'N' (after a blank) on command line.
Also:
- If archive filetype omitted, and the default .ARC filetype not found,
tries .ARK as an alternate default. (Anticipates NOAH, and compatible
with Irv Hoff's KMD22.)
- Incorporates option to bypass BDOS function 31 call (Get DPB Address),
for non-std CP/M clones such as Cromemco CDOS and CP/M-68K emulator
for 8080 CP/M 2.2. (Eliminates UNARC12 patch notice, UNARC-P1.NOT.)
- Allows program name to be patched (at start of USAGE message).
Affects all help screen references and abort message. (E.g., RCP/M
sysops may prefer 'ADIR' to 'UNARC'.)
- Corrects count of bytes skipped due to invalid header when processing
'self-unpacking' archives with more than 3 preliminary bytes.
- Enhances recovery processing for invalid archive headers, and merges
'invalid format' and 'unexpected eof' errors. This change tends to
cause display of a garbage directory entry (before abort) for non-ARC
files, but it does allow processing of certain new self-unpacking
archives, such as Phil Katz' PKX32A11.COM.
- Changes the replacement for an invalid filename char from '_' to '$'
(since underline is not allowed as a filename char by CP/M CCP, and
'$' usually carries a 'temporary' significance in CP/M).
- Reduces directory listing width by one column (78 now), to allow
one more char without extra blank line on terminals which autowrap
after column 80 (e.g. allows leading semicolon generated by MDM7 and
IMP during disk file capture of terminal output).
- Adds a few bells to warning and fatal messages, along with a patch
byte to disable these (for those who prefer solitude).
- Allows ^K in addition to ^C for program abort requests. (For certain
ancient RCP/M systems which never pass ^C back to user programs.)
- Adds .ARK and .?Z? (CP/M CRUNCH or MS-DOS ZOO 'Z format' files) to
list of excluded typeout extensions, and eliminates .CMD (since that
might be a readable dBASE command file instead of CP/M-86 binary).
- Simplifies the Z80 CPU check and removes the 'Z80 Version' message
in the help display, to save a few bytes in that version. (Alternate
version, UNARCA.COM, now displays '8080 Version'.)
- Adds 8080 version message recommending Z80 version, when run on a Z80.
1.3 --none-- (RAF)
This version bypassed due to appearance of several unauthorized updates
with the name UNARC13 (and not because of superstition). Most notably,
these include Steve Sanders' unnecessary addition of ^S and ^C checking
during file typeout (because TurboDOS does not properly emulate CP/M's
handling of these in BDOS function 2 calls), and the addition of paged
typeout by 'Larry Smith' (whoever he is; a worthwhile enhancement, but
the release was deficient in several other respects). WHY CAN'T THESE
'CONTRIBUTORS' SIMPLY CONTACT THE AUTHOR BEFORE RELEASING THEIR CHANGES
TO THE PUBLIC?!
1.2 24 Jun 86 (RAF)
Modified to allow assembly of a version which will execute on 8080/8085
CPU's. (We resisted this initially but have been made to realize that
this is necessary to achieve true acceptance of UNARC by the full CP/M
user community. Non-Z80 users, particularly RCP/M sysops, still exert
considerable influence in the world of public domain software. This,
we believe, is out of proportion to their numbers, since almost all
CP/M systems sold in the last five years are Z80-based. Nevertheless,
we've accommodated the needs of these users by extensive use of macros
which serve to emulate Z80 instructions on non-Z80 machines.) However,
no attempt has been made to optimize for either size or speed in the
non-Z80 version (which is 1K larger and 50% or more slower than its
Z80-only counterpart).
Also:
- Implements a "wheel" byte to simplify use and installation on RCP/M's.
- Lines up file types in directory listing.
- Permits processing of "self-unpacking" archives such as the MS-DOS
ARC51.COM file (anticipates a future scheme for distributing UNARC).
- Attempts to recover from bad archive headers by skipping extra bytes.
- Eliminates archaic "T:" syntax completely for file typeout.
1.1 24 May 86 (RAF)
Minor change to allow file typeout without the "T:" syntax (which
didn't work with almost ANY CCP replacement)... File will be typed if
it: (1) has no disk drive name, (2) is a single (UNambiguous) file,
and (3) is not an excluded filetype. (Else, file will simply be listed
with no error message.) This change was suggested by Irv Hoff's mod to
UNARC10, which he called ADIR. (Previous "T:" method can still be
enabled, but it is now undocumented since we will probably drop it
altogether in future.)
Also shortened on-line help message, so that COM file size is now
reduced to 4K. (For RCP/M systems, if HODRV = 0 and/or TYFLG = 0, the
help information relating to disk extraction and/or file typeout,
respectively, is automatically removed.)
1.0 03 May 86 (RAF)
First public release. Supports file formats generated by all versions
of MS-DOS ARC through (at least) version 5.12 dated February 7, 1986.
0.0 01 Mar 86 (RAF)
I undertook writing this program to satisfy my curiosity about software
developments in the MS-DOS/PC-DOS world. The ARC "freeware" program
(copyright by System Enhancement Associates) has been around for over a
year now and has achieved enormous popularity in the 16-bit community.
Unfortunately, the lack of a compatible equivalent for CP/M systems
renders a large amount of public domain software inaccessible to 8-bit
users such as myself. (Note that 16-bit software can indeed be usable
on 8-bit systems, e.g. Pascal and C language programs.) Also, an
increasing number of RCP/M systems are catering to both 8-bit and
16-bit users, and it is my hope that UNARC may find a welcome home on
such systems.
Note that I was not (initially) a fan of the sequential .ARC file
format, which is less flexible and slower to process (though certainly
more compact) than the random-access format which Novosielski .LBR
libraries have provided for years. Therefore, I stopped short of
producing a complete ARC program equivalent which includes creation of
.ARC files. The LZW "crunching" algorithm is impressive though (see my
editorial comments preceeding the UCR routine), and I now believe there
is a place for .ARC files in the CP/M world (particularly on RCP/M's,
where the name of the game is reducing upload/download time). But
that's the domain of another program (i.e. my next project: NARC).
- Bob Freed
Credits:
Primary credit is due to System Enhancement Associates' ARC author
Thom Henderson for his fine utility program (even if it's not for
CP/M). Of course without ARC, UNARC would have no reason to exist.
But special thanks are due SEA for making publicly available the C
language source code, without which we could never have begun. |
PAGE
SUBTTL Z80/8080 Version Definitions
.Z80 ; Sorry, if you're an Intel fan
.COMMENT |
This source program uses Zilog mnemonics (author's preference) and may
be assembled with the M80 ((C) Microsoft) or Z80ASM ((C) SLR Systems)
macro assemblers. (Relocatable code features have been avoided, so
conversion to other assembler formats should be straightforward but
may require manual expansion of the macros defined here.)
The following macro definitions enable conditional assembly of a
version which will execute on 8080/8085 CPU's. Our intent is to
provide a non-Z80 version without imposing a limitation on any
Z80-specific capabilities in the source. (I.e., in specific cases the
chosen emulation of Z80 opcodes does not necessarily produce the
optimal 8080/8085 implementation, in terms of either size or speed.
This approach allows us to offer a non-Z80 version without worrying too
much about its efficiency.) |
NO EQU 0
YES EQU NOT NO
; For Z80ASM only, the following may be left undefined to allow
; interactive definition at assembly time. For M80 (which does not
; support the .ACCEPT directive), the leading semicolon must be removed
; in order to generate the non-Z80 version.
;Z80 EQU NO ; YES for Z80 version, NO for 8080/8085
IFNDEF Z80 ; If not defined above (and pass 1),
N EQU NO ; (Allows short
Y EQU YES ; responses)
.ACCEPT Z80 ; Ask user for definition (Z80ASM only)
IFNDEF Z80 ; If still not defined (must be M80),
Z80 EQU YES ; Generate the Z80 version
ENDIF
ENDIF
PAGE
IF Z80
; Macros for Z80 version (to simplify our effort for the 8080 version)
EX_AF MACRO
EX AF,AF'
ENDM
LD_DE MACRO AA
LD DE,AA
ENDM
STO_DE MACRO AA
LD (AA),DE
ENDM
STO_BC MACRO AA
LD (AA),BC
ENDM
ADC_HL MACRO AA
ADC HL,AA
ENDM
SBC_HL MACRO AA
SBC HL,AA
ENDM
LD_IX MACRO AA
LD IX,AA
ENDM
STO_IX MACRO AA
LD (AA),IX
ENDM
PUSH_IX MACRO
PUSH IX
ENDM
POP_IX MACRO
POP IX
ENDM
INC_IX MACRO
INC IX
ENDM
ADD_IX MACRO AA
ADD IX,AA
ENDM
LD_A_IX MACRO
LD A,(IX)
ENDM
ENDIF ; Z80
IF NOT Z80
; Macros for 8080 version (to emulate Z80-only opcodes)
; Note: Many of these emulations of Z80 instructions do not correctly
; implement the setting of the condition flags (e.g. DJNZ should not
; alter the Z flag). In all such cases, we have been careful to ensure
; that an exact emulation is not required anywhere in the code, but
; extreme vigilance is needed when making future program changes.
; (Exact emulation is always possible if necessary, so avoid trying to
; code around the differences: Our goal should be to always produce the
; best possible Z80 version!)
JR MACRO AA,BB
IF NUL BB
JP AA
ELSE
JP AA,BB
ENDIF
ENDM
DJNZ MACRO AA ; Destroys SF, ZF
DEC B
JP NZ,AA
ENDM
EX_AF MACRO
PUSH HL
PUSH AF
LD HL,(AFSAV)
EX (SP),HL
LD (AFSAV),HL
POP AF
POP HL
ENDM
EXX MACRO ; Long enough to warrant subroutine
CALL EXX
ENDM
LD_DE MACRO AA
EX DE,HL
LD HL,AA
EX DE,HL
ENDM
STO_DE MACRO AA
EX DE,HL
LD (AA),HL
EX DE,HL
ENDM
STO_BC MACRO AA
PUSH HL
LD H,B
LD L,C
LD (AA),HL
POP HL
ENDM
ADC_HL MACRO AA
ADSBHL AA,ADC
ENDM
SBC_HL MACRO AA
ADSBHL AA,SBC
ENDM
ADSBHL MACRO AA,BB
PUSH AF
LD A,L
CC DEFL NO
IRPC DD,AA
IF CC
BB A,DD
ENDIF
CC DEFL YES
ENDM
LD L,A
LD A,H
IRPC DD,AA
BB A,DD
EXITM
ENDM
LD H,A
JP NZ,$+5 ;; Test both bytes for zero,
INC L ;; without disturbing carry
DEC L ;; (added in UNARC 1.5)
EX (SP),HL
LD A,H
POP HL
ENDM
LD_IX MACRO AA
PUSH HL
LD HL,AA
LD (IXSAV),HL
POP HL
ENDM
STO_IX MACRO AA
PUSH HL
LD HL,(IXSAV)
LD (AA),HL
POP HL
ENDM
PUSH_IX MACRO
PUSH HL
LD HL,(IXSAV)
EX (SP),HL
ENDM
POP_IX MACRO
EX (SP),HL
LD (IXSAV),HL
POP HL
ENDM
INC_IX MACRO
PUSH HL
LD HL,(IXSAV)
INC HL
LD (IXSAV),HL
POP HL
ENDM
ADD_IX MACRO AA
PUSH HL
LD HL,(IXSAV)
IFIDN <AA>,<IX>
ADD HL,HL
ELSE
ADD HL,AA
ENDIF
LD (IXSAV),HL
POP HL
ENDM
LD_A_IX MACRO
PUSH HL
LD HL,(IXSAV)
LD A,(HL)
POP HL
ENDM
LDI MACRO ; Does not handle P/V
PUSH AF
LD A,(HL)
LD (DE),A
INC HL
INC DE
DEC BC
POP AF
ENDM
LDIR MACRO ; Destroys CF
CALL LDIR
ENDM
CPIR MACRO ; Destroys CF, does not handle P/V
CALL CPIR
ENDM
RLD MACRO ; Not a true RLD, but suffices for us
LD A,(HL)
RLCA
RLCA
RLCA
RLCA
ENDM
SRL MACRO AA
OR A
SHIFT AA,RRA
ENDM
SRA MACRO AA
SHIFT AA,<RLCA,RRCA,RRA>
ENDM
RR MACRO AA
SHIFT AA,RRA
ENDM
RRC MACRO AA
SHIFT AA,RRCA
ENDM
SHIFT MACRO AA,BB
IFDIF <AA>,<A>
PUSH AF
LD A,AA
ENDIF
IRP CC,<BB>
CC
ENDM
INC A ;; Set flags without
DEC A ;; changing carry
IFDIF <AA>,<A>
LD AA,A
EX (SP),HL
LD A,H
POP HL
ENDIF
ENDM
BIT MACRO AA,BB ; Destroys CF, SF
PUSH AF
IFDIF <BB>,<A>
LD A,BB
ENDIF
AND 1 SHL AA
BITMSK DEFL $-1 ;; For squashed files (c.f. STRADD)
EX (SP),HL
LD A,H
POP HL
ENDM
SET MACRO AA,BB
SETRES AA,BB,OR
ENDM
RES MACRO AA,BB
SETRES AA,BB,<AND NOT>
ENDM
SETRES MACRO AA,BB,CC ; Destroys flags if register A
IFDIF <BB>,<A>
PUSH AF
LD A,BB
ENDIF
CC (1 SHL AA)
IFDIF <BB>,<A>
LD BB,A
POP AF
ENDIF
ENDM
ENDIF ; NOT Z80
PAGE
SUBTTL Definitions
; ARC file parameters
ARCMARK EQU 26 ; Archive header marker byte
; Note: The following three definitions should not be changed lightly.
; These are hard-wired into the code at numerous places!
ARCVER EQU 9 ; Max. header vers. supported for output
CRBITS EQU 12 ; Max. bits in crunched file input codes
CQBITS EQU 13 ; Max. bits in squashed file input codes
; CP/M system equates
BOOT EQU 0000H ; Base of system page / warm boot return
BDOS EQU BOOT+005H ; BDOS entry
MEMTOP EQU BDOS+1 ; Contains base of BDOS / top of TPA
DFCB EQU BOOT+05CH ; Command line tail default FCB
SFCB EQU BOOT+06CH ; Command line tail secondary FCB
DBUF EQU BOOT+080H ; Default DMA buffer
TBASE EQU BOOT+100H ; Base of TPA
; BDOS function codes
$CONIN EQU 1 ; Console input
$CONOUT EQU 2 ; Console output
$LIST EQU 5 ; Listing output
$PRTSTR EQU 9 ; Print (console) string
$CONST EQU 11 ; Get console status
$VERSN EQU 12 ; Get CP/M version no.
$SELECT EQU 14 ; Select disk
$OPEN EQU 15 ; Open file
$CLOSE EQU 16 ; Close file
$FIND EQU 17 ; Find file
$DELETE EQU 19 ; Delete file
$READ EQU 20 ; Read sequential record
$WRITE EQU 21 ; Write sequential record
$MAKE EQU 22 ; Make file
$DISK EQU 25 ; Get current disk
$SETDMA EQU 26 ; Set DMA address
$GETDPB EQU 31 ; Get disk parameter block address
$READR EQU 33 ; Read random record
$RECORD EQU 36 ; Set random record no.
; FCB offsets
@DR EQU 0 ; Drive code
@FN EQU 1 ; File name
@FT EQU 9 ; File type
@CR EQU 32 ; Current record
@RN EQU 33 ; Random record no. (optional)
@FCBSZ EQU 33 ; FCB size for sequential I/O
@FCBSX EQU @FCBSZ+3 ; Extended FCB size for random I/O
PAGE
; ASCII control codes
CTLC EQU 'C'-'@' ; Control-C (console abort)
CTLK EQU 'K'-'@' ; Control-K (alternate abort)
BEL EQU 'G'-'@' ; Bell
HT EQU 'I'-'@' ; Horizontal tab
LF EQU 'J'-'@' ; Line feed
CR EQU 'M'-'@' ; Carriage return
CTLS EQU 'S'-'@' ; Control-S (suspend output)
CTLZ EQU 'Z'-'@' ; Control-Z (CP/M end-of-file)
DEL EQU 7FH ; Delete/rubout
REP EQU 'P'-'@'+80H ; Repeated byte flag (DLE with msb set)
PAGE
SUBTTL Patchable Options
; Useful options here at start of file to simplify patching
ASEG ; This simplifies page alignment at end
ORG TBASE ; .COM file starts here
JP BEGIN ; Skip over this stuff on program entry
; The default values of all of these options are suitable for standard
; CP/M 2.2 systems. In each case an alternate setting is illustrated,
; but these are primarily of interest to RCP/M sysops or users with
; non-standard (or very small) systems. Options followed by ";*" are
; automatically affected by the wheel byte setting (see below).
CCPSV: DB 8 ; No. high memory pages to save (8 = 2K)
;CCPSV: DB 0 ; This to clobber CCP and force reboot
;BLKSZ: DB 1 ; Default disk allocation block size (K)
BLKSZ: DB 0 ;*This to use default drive's block size
; for listing, when no output drive
HIDRV: DB 'P'-'@' ; Highest input file drive (A=1,B=2,...)
;HIDRV: DB 0 ; This restricts input to default drive
HODRV: DB 'P'-'@' ;*Highest output file drive no.
;HODRV: DB 0 ; RCP/M's use this for no disk output
; (if no wheel byte implemented)
; Note: As of UNARC 1.2, the following byte serves only as a flag.
; I.e., it no longer defines a pseudo typeout "drive".
TYFLG: DB 0FFH ; This enables single file typeout
;TYFLG: DB 0 ;*RCP/M's use this for no file typeout
TYPGS: DB 0 ;*No. buffer pages for typeout (0=max)
;TYPGS: DB 1 ; This minimizes viewing waits, but may
; cause excess floppy motor stop/start
TYLIM: DB 0 ; No line limit for file typeout
;TYLIM: DB 80 ;*RCP/M's may prefer non-zero line limit
; Following added in UNARC 1.2 to simplify use by RCP/M sysops. If byte
; addressed by WHEEL is zero, no file output allowed (as if HODRV = 0).
; Also BLKSZ and/or TYPGS are assumed = 1, if these are zero by default.
; If byte addressed by WHEEL is non-zero (indicates a privileged user),
; TYFLG and TYLIM are not enforced (unlimited typeout allowed). The
; default wheel byte address defined here (HODRV) provides compatibility
; with previous releases of UNARC for systems which do not implement a
; wheel byte. (ZCPR3 users should set this word to the address of their
; Z3WHL byte, as determined by running SHOW.COM.)
WHEEL: DW HODRV ; Address of "wheel" byte (this if none)
;WHEEL: DW BOOT+03EH ; E.g. if wheel byte stored in base page
PAGE
; Following added in UNARC 1.4:
TYLPS: DB 23 ; No. lines between typeout pauses
;TYLPS: DB 0 ; Forces continuous typeout always
DBLSZ: DB 0 ; Use DPB for disk allocation block size
;DBLSZ: DB 1 ; Assumed block size (K) if BDOS 31 call
; not supported (e.g. CP/M-68K)
BELLS: DB 0FFH ; Allow bells in warning/error messages
;BELLS: DB 0 ; This for solitude
; Table of file types which are disallowed for typeout
NOTYP: DB 'COM' ; CP/M-80 or MS-DOS binary object
DB 'CM','D'+80H ; CP/M-86 binary object (or dBASE file)
DB 'EXE' ; MS-DOS executable
DB 'OBJ' ; Renamed COM
DB 'OV?' ; Binary overlay
DB 'REL' ; Relocatable object
DB '?RL' ; Other relocatables (PRL, CRL, etc.)
DB 'INT' ; Intermediate compiler code
DB 'SYS' ; System file
DB 'BAD' ; Bad disk block
DB 'LBR' ; Library
DB 'ARC' ; Archive (unlikely in an ARC)
DB 'ARK' ; Alternate archive (ditto)
DB '?Q?' ; Any SQueezed file (ditto)
DB '?Z?' ; Any CRUNCHed (or ZOO'd) file (ditto)
; Note: Additional types may be added below. To remove one of the above
; types without replacing it, simply set the msb in any byte (as
; shown above for .CMD, since that can be a readable dBASE command
; file).
REPT 5 ; Room for more types (20 total)
DB 0,0,0
ENDM
DB 0 ; End of table
PAGE
SUBTTL Program Usage
; Following displays if no command line parameters
; (Also on attempts to type the .COM file)
; Note: All program name output is obtained from the first chars of the
; usage message below (up to and including the first blank), and
; is generated by a byte value 1 in any typeout string.
USAGE: IDENT ; Program version identification first
DB CR,LF
DB 'CP/M Archive File Extractor'
IF NOT Z80
USEA: DB ' (8080 Version)'
ENDIF
DB CR,LF,LF,'Usage: ',1,'[d:]arcfile[.typ] '
USE1: DB '[d:]'
USE1L EQU $-USE1 ; Above cleared if HODRV=0 or non-wheel
DB '[afn] [N'
USE4: DB '|P|C'
USE4L EQU $-USE4 ; Above cleared if non-wheel
DB ']',CR,LF,LF
DB 'Examples:',CR,LF
DB 'B>',1,'A:SAVE.ARK *.* '
DB '; List all files in CP/M archive SAVE on drive A',CR,LF
DB 'B>',1,'A:SAVE.ARC *.* '
DB '; List all files in MS-DOS archive SAVE on drive A',CR,LF
DB 'A>',1,'SAVE '
DB '; Same as either of above',CR,LF
DB 'A>',1,'SAVE *.* N '
DB '; Same as above (no screen pauses)',CR,LF
DB 'A>',1,'SAVE *.DOC '
DB '; List just .DOC files',CR,LF
USE2: DB 'A>',1,'SAVE READ.ME '
DB '; Typeout the file READ.ME',CR,LF
DB 'A>',1,'SAVE READ.ME N '
DB '; Typeout the file READ.ME (no screen pauses)',CR,LF
USE2L EQU $-USE2 ; Above cleared if TYFLG=0 and non-wheel
USE3: DB 'A>',1,'SAVE A: '
DB '; Extract all files to drive A',CR,LF
DB 'A>',1,'SAVE B:*.DOC '
DB '; Extract .DOC files to drive B',CR,LF
DB 'A>',1,'SAVE C:READ.ME '
DB '; Extract file READ.ME to drive C',CR,LF
USE3L EQU $-USE3 ; Above cleared if HODRV=0 or non-wheel
USE5: DB 'A>',1,'SAVE PRN.DAT P '
DB '; Print the file PRN.DAT (no formatting)',CR,LF
DB 'A>',1,'SAVE *.* C '
DB '; Check validity of all files in archive'
USEC: DB CR,LF
USE5L EQU $-USE5 ; Above cleared if non-wheel
DB LF
COPR ; Copyright notice last
; (We'd like to be unobtrusive, but please don't remove or patch out)
USEB: DB 0 ; End of message marker
DB CTLZ ; Stop attempted .COM file typeout here
PAGE
SUBTTL Beginnings and Endings
IF NOT Z80
; Special entry for self-unpacking archive (non-Z80 version only)
; Note: This works because the initial file (UNARCA.COM) in a self-
; unpacking archive is offset 26 bytes in memory (due to the
; initial JP opcode plus 25-byte version 1 header). I.e., the
; first three bytes of such a file are 0C3H, 1AH, 01H = JP 11AH.
; Location 11AH contains the instruction normally found at the
; base address (100H) of UNARCA.COM, i.e. JP BEGIN. But because
; of the offset, that will jump here instead of to BEGIN.
JP SELFUP ; Go setup for self-unpacking
REPT 5 ; Pad out for 26-byte offset...
DB 0,0,0,0
ENDM
JP BOOT ; (Should never reach this!)
ENDIF
; Program begins
; Note: The program is self-initializing. Once loaded, it may be
; re-executed multiple times (e.g. by a zero-length COM file,
; or the ZCPR GO command).
BEGIN:
;;; XOR A ; \ This sets Z80 P/V = 0 (no overflow),
;;; DEC A ; / or 8080/8085 P/V = 1 (even parity)
SUB A ; (More elegant, saves a byte: v1.4)
LD C,$PRTSTR ; Setup to print message by BDOS
IF Z80
LD DE,NOTZ80 ; Must be a Z80, or forget all else
JP PE,BDOS ; If not, just print message and abort
LD (SPSAV),SP ; Save CCP stack (better be a Z80 now!)
ELSE
LD DE,USEZ80 ; Should be an 8080/8085
CALL PO,BDOS ; If not, tell user about Z80 version
BEGIN1: LD HL,0 ; Entry after self-unpacking relocation
ADD HL,SP ; Save CCP stack (8080 or Z80)
LD (SPSAV),HL
ENDIF
CALL CHECK ; Check if we can proceed
LD SP,STACK ; Now setup local stack
LD HL,TOTS ; Zero all listing totals
LD BC,TOTC*256+0
CALL FILL
CALL INIT ; Process command line, open ARC file
CALL OUTSET ; Check output drive, setup for output
; Find first archive header
; Note: As of UNARC 1.2, up to three additional bytes are tolerated
; before first header mark, with no error or warning messages
; (for "self-unpacking" archives).
LD HL,3 ; Assume will skip at least 3 bytes
LD B,L ; Setup count of allowed extra bytes
FIRST: CALL GET ; Get next byte
CP ARCMARK ; Is it header marker?
JR Z,NEXT ; Yes, skip
DJNZ FIRST ; Else loop for no. allowed extras
PAGE
; File processing loop
LOOP: CALL GET ; Get next byte
CP ARCMARK ; Is it archive header marker?
JR NZ,BAD ; No, it's a bad header
; Process next file
NEXT: CALL GET ; Get header version
OR A ; If zero, that's logical end of file,
JR Z,DONE ; and we're done
NEXT1: CALL GETHDR ; Read archive header
CALL GETNAM ; Does file name match test pattern?
JR NZ,SKIP ; No, skip this file
CALL LIST ; List file info
CALL OUTPUT ; Output the file (possibly)
CALL TAMBIG ; Ambiguous output file selection?
JR NZ,EXIT ; No, quit early
; Skip to next file
SKIP: LD HL,SIZE ; Get two-word remaining file size
CALL LGET ; (will be 0 if output was completed)
CALL SEEK ; Seek past it
LD HL,0 ; Reinit count of bytes skipped
JR LOOP ; Loop for next file
; Done with all files
DONE: LD HL,(TFILES) ; Get no. files processed
LD A,H
OR A
JR NZ,DONE1 ; Skip if many
OR L ; No files found?
LD DE,NOFILS ; Yes, setup error message
JR Z,PABORT ; and abort
DEC A ; Test if just one file
DONE1: CALL NZ,LISTT ; If more than one, list totals
; Exit program
EXIT: CALL ICLOSE ; Close input and output files (if open)
LD A,(CCPSV) ; Possibly overlaid CCP?
OR A
JP Z,BOOT ; Yes, reboot CP/M
LD SP,0 ; Restore CCP stack
SPSAV EQU $-2 ; (Original stack ptr saved here)
RET ; Return to CCP
PAGE
; Bad archive file header
; Note: This added in UNARC 1.2 (mostly compatible with MS-DOS ARC
; 5.12) and modified somewhat in UNARC 1.4. It's a bit kludgy
; now, but it does permit processing of Phil Katz' self-unpacking
; archive, PKX32A11.COM (with a warning message), as well as
; SEA's ARC51.COM (with no warning). (Although success with
; PKX32A11 hinges on the fact that no ARCMARK's are followed
; by valid non-zero versions in that file, which is probably
; coincidental.)
BAD: CALL BADCNT ; Count bad header byte
CALL GET ; Read byte (unless end of file abort)
BAD1: CP ARCMARK ; Found a header marker?
JR NZ,BAD ; No, repeat attempt to re-synchronize
CALL GET ; Ok, found another (possible) header
PUSH AF ; Save header version
DEC A ; But ignore archive eof here
CP ARCVER ; Is it a valid version?
JR NC,BAD2 ; No, skip
EX DE,HL ; Get count of bytes skipped
LD HL,HDRSKP ; Store in message
LD BC,0
CALL WTOD
LD (HL),0
LD DE,HDRERR ; Print warning message
CALL PRINTX
POP AF ; Recover version
JR NEXT1 ; Go process (assumed valid) next file
BAD2: CALL BADCNT ; Count bad header byte (1st of 2 seen)
POP AF ; Restore vesion
JR BAD1 ; Go check if 2 consecutive header marks
PAGE
; Preliminary checks
; Note: Following is called before local stack is setup. Primary
; caution here is that PRINT (called by PABORT and PEXIT) uses no
; more than 5 stack levels. (Assumes program called from CCP with
; 7 stack levels available, and that at most one of these must be
; reserved for interrupts.)
CHECK: XOR A ; Clear flags in case early abort:
LD (IFLAG),A ; Input file open flag
LD (OFLAG),A ; Output file open flag
LD (LPS),A ; Prevent any screen pauses yet
LD C,$VERSN ; Must be CP/M 2.0 or above, since we
CALL BDOS ; use random disk reads
CP 20H
LD DE,CPMERR ; (With a bit of work, this limitation
JR C,EABORT ; could be eliminated in future)
LD A,(MEMTOP+1) ; Get base page of BDOS
LD HL,CCPSV ; Subtract no. pages reserved for CCP
SUB (HL) ; (if any)
LD (HIPAGE),A ; Save highest usable page (+1)
LD A,HIGH MINMEM ; Ensure enough memory to do anything
; Check for enough memory
CKMEM: CP 0 ; Page address to check in A
HIPAGE EQU $-1 ; Must be lower than this
RET C ; Return if ok
LD DE,NOROOM ; Else, abort due to no room
; Early abort during preliminary checks
EABORT: POP HL ; Reclaim stack level for extra safety
; Print error message and abort
PABORT: CALL PRINT
; Abort program
ABORT: LD DE,ABOMSG ; Print general abort message
; Print message and exit
; Note: We call PRINT+CRLF, instead of PRINTX, to save a stack level
PEXIT: CALL PRINT
CALL CRLF
JR EXIT
PAGE
; Validate command line parameters and open input file
INIT: LD HL,DBUF ; Point to command line buffer
LD E,(HL) ; Fetch its length
LD D,0
ADD HL,DE ; Point to the last byte
DEC HL ; Point to second-to-last char
LD A,(HL) ; Is it a blank?
CP ' '
JR NZ,INIT1 ; No, skip (no option)
INC HL ; Point to option letter
LD A,(HL) ; Is it 'N' ?
CP 'N'
JR Z,INIT2 ; Yes, skip (no paging)
CP 'P' ; Is it 'P' ?
JR NZ,INIT0
LD (PROUTF),A ; Yes, set printer output flag
INIT0: CP 'C' ; Is it 'C' ?
JR NZ,INIT1 ; No, go enstate paging limit
LD (CHECKF),A ; Yes, set check archive flag
INIT1: LD A,(TYLPS) ; Fetch default lines between pauses
LD (LPS),A ; Set lines per screen (enables pauses)
LD (LPSCT),A ; Init count of lines until next pause
INIT2: LD A,' ' ; Setup blank for (several) tests
LD HL,SFCB ; Point to second parameter FCB
LD DE,OFCB ; Point to file output FCB
LDI ; Save output drive, point to file name
LD DE,TNAME ; Set to save test pattern
LD BC,11 ; Setup count for file name and type
CP (HL) ; Output file name specified?
JR NZ,INIT3 ; Yes, go move it
LD H,D ; No, default to "*.*"
LD L,E
LD (HL),'?' ; (I.e. all "?" chars)
INC DE
DEC BC
INIT3: LDIR ; Save test name pattern
LD HL,IFCB+@FT ; Point to ARC file type
CP (HL) ; Omitted?
JR NZ,INIT4 ; Skip if not
LD (HL),'A' ; Yes, set default file type (.ARK)
INC HL
LD (HL),'R'
INC HL
LD (HL),'K'
LD (ARKFLG),A ; Set flag for alternate (.ARC) next
INIT4: LD HL,IFCB+@FN ; Any ARC file name?
CP (HL)
JR Z,HELP ; No, go show on-line help
PUSH HL ; Save name ptr for message generation
CALL FAMBIG ; Ambiguous ARC file name?
LD DE,NAMERR ; Yes, report error
INIT5: JR Z,PABORT ; and abort
POP DE ; Recover ptr to FCB name
LD HL,ARCNAM ; Unparse name for message
LD C,' ' ; (with no blanks)
CALL LNAME
XOR A ; Cleanup end of message string
LD (HL),A
DEC A ; Set to read a new record next
LD (GETPTR),A ; (initializes GET)
LD HL,IFCB ; Point to ARC file FCB
LD A,(HIDRV) ; Get highest allowed drive no.
CP (HL) ; Is ARC file drive in range?
LD DE,BADIDR ; No, report bad input drive
JP C,PABORT ; and abort
; Open archive file
EX DE,HL ; Recover FCB address
LD C,$OPEN ; Open ARC file
CALL FDOS ; File found?
JR NZ,INIT6 ; Yes, skip
LD HL,ARKFLG ; No, but can we retry with alternate
OR (HL) ; default file type?
LD DE,OPNERR ; No, report error
JR Z,INIT5 ; and abort (via branch aid)
LD (HL),0 ; Clear retry flag for next time
LD HL,IFCB+@FT+2 ; Point to last char of file type
LD (HL),'C' ; Change from .ARK to .ARC
JR INIT4 ; Go attempt open one more time
INIT6: LD (IFLAG),A ; Set input file open flag
LD DE,ARCMSG ; Show ARC file name
CALL PRINTX
LD A,(BLKSZ) ; Get default disk block size
OR A ; Explicit default?
CALL Z,WHLCK ; Or non-wheel if none? (i.e. forces 1K)
JR NZ,SAVBLS ; Yes, skip
; Get current disk's allocation block size for listing
GETBLS: LD A,(DBLSZ) ; Any default disk block size?
OR A ; (e.g. if $GETDPB not supported)
JR NZ,SAVBLS ; Yes, bypass the $GETDPB call
LD C,$GETDPB ; Get DPB address
CALL BDOS
INC HL ; Point to block mask
INC HL
INC HL
LD A,(HL) ; Fetch block mask
INC A ; Compute block size / 1K bytes
RRCA
RRCA
RRCA
SAVBLS: LD (LBLKSZ),A ; Save block size for listing
RET ; Return
; Display program usage help message
HELP: CALL WHLCK ; Check wheel byte
PUSH AF ; Save it
DEC A ; Privileged user?
JR Z,HELP1 ; No, skip (extraction never allowed)
LD A,(HODRV) ; File extraction allowed?
OR A
HELP1: LD HL,USE1 ; Setup to clear out usage examples
LD BC,256*USE1L+80H
CALL Z,FILL ; Do it if not allowed
LD HL,USE3
LD B,USE3L
CALL Z,FILL ; (Two places)
POP AF ; Was wheel byte set?
JR Z,HELP2 ; Yes, skip (typeout etc always allowed)
LD HL,USE4 ; Clear out print/check option examples
LD B,USE4L
CALL FILL
LD HL,USE5 ; (Two places)
LD B,USE5L
CALL FILL
LD A,(TYFLG) ; File typeout allowed?
OR A
LD HL,USE2
LD B,USE2L
CALL Z,FILL ; No, clear out usage example
HELP2: LD DE,USAGE ; Just print usage message
JP PEXIT ; and exit
; Check wheel byte
WHLCK: PUSH HL ; Save register
LD HL,(WHEEL) ; Get wheel byte address
LD A,(HL) ; Fetch wheel byte
POP HL ; Restore reg
OR A ; Check wheel byte
JR NZ,WHLCK1
INC A ; If zero, user is not privileged
RET ; Return A=1 (NZ)
WHLCK1: XOR A ; If non-zero, he's a big wheel
RET ; Return A=0 (Z)
PAGE
; Close input and output files (called at program exit)
ICLOSE: LD DE,IFCB ; Setup ARC file FCB
LD A,0 ; Get input open flag
IFLAG EQU $-1 ; (stored here)
CALL CLOSE ; Close input file first (e.g. for MP/M)
; Close output file
OCLOSE: LD DE,OFCB ; Setup output file FCB
LD A,0 ; Get output open flag
OFLAG EQU $-1 ; (stored here)
; Close a file if open
CLOSE: OR A ; File is open?
LD C,$CLOSE ; Yes, close it
CALL NZ,BDOS
INC A ; Check return code
RET ; Return to caller (Z set if error)
; BDOS file functions for output file
OFDOS: LD DE,OFCB ; Setup output file FCB
; BDOS file functions
FDOS: CALL BDOS ; Perform function
INC A ; Test directory code
RET ; Return (Z set if file not found)
; Set DMA address for file input/output
SETDMA: LD C,$SETDMA ; DMA address in DE
CALL BDOS ; This is always a good place to...
; Check for CTRL-C abort (and/or read console char if any)
CABORT: LD C,$CONST ; Get console status
CALL BDOS
OR A ; Character ready?
RET Z ; Return (Z set) if not
LD C,$CONIN ; Input console char (echo if printable)
CALL BDOS
; Note: Following added in UNARC 1.5 to handle any ^S input which is not
; detected by CP/M 2.2 BDOS.
AND 7FH ; Mask to 7 bits
CP CTLS ; Is it CTRL-S (suspend output)?
LD C,$CONIN
CALL Z,BDOS ; Yes, wait for another char
AND 7FH ; Mask to 7 bits
CP CTLC ; Is it CTRL-C?
JR Z,GABORT ; Yes, go abort
CP CTLK ; Or is it CTRL-K (RCP/M alternate ^C)?
RET NZ ; No, return char (and NZ) to caller
GABORT: JP ABORT ; Go abort program
PAGE
SUBTTL Archive File Input Routines
; Get counted byte from archive subfile (saves alternate register set)
; The alternate register set normally contains values for the low-level
; output routines (see PUTSET). This entry to GETC saves these and
; returns with them enstated (for PUT, PUTUP, etc.). Caller must issue
; EXX after call to return these to the alternate set, and must save and
; restore any needed values from the original register set.
; Note: At first glance, all this might seem unnecessary, since BDOS
; (might be called by GETREC) does not use the Z80 alternate
; register set (at least with Digital Research CP/M). But some
; CBIOS implementations (e.g. Osborne's) assume these are fair
; game, so we are extra cautious here.
GETCX: EXX ; Swap in alt regs (GETC saves them)
; Get counted byte from component file of archive
; GETC returns with carry set (and a zero byte) upon reaching the
; logical end of the current subfile. (This relies on the GET routine
; NOT returning with carry set.)
GETC: PUSH BC ; Save registers
PUSH DE
PUSH HL
LD HL,SIZE ; Point to remaining bytes in subfile
LD B,4 ; Setup for long (4-byte) size
GETC1: LD A,(HL) ; Get size
DEC (HL) ; Count it down
OR A ; But was it zero? (clears carry)
JR NZ,GET1 ; No, go get byte (must not set carry!)
INC HL ; Point to next byte of size
DJNZ GETC1 ; Loop for multi-precision decrement
LD B,4 ; Size was zero, now it's -1
GETC2: DEC HL ; Reset size to zero...
LD (HL),A ; (SIZE must contain valid bytes to skip
DJNZ GETC2 ; to get to next subfile in archive)
SCF ; Set carry to indicate end of subfile
JR GET2 ; Go restore registers and return zero
PAGE
; Get next sequential byte from archive file
; Note: GET and SEEK rely on the fact that the default DMA buffer
; used for file input (DBUF) begins on a half-page boundary.
; I.e. DBUF address = nn80H (nn = 00 for standard CP/M).
GET: PUSH BC ; Save registers
PUSH DE
PUSH HL
GET1: LD HL,(GETPTR) ; Point to last byte read
INC L ; At end of buffer?
CALL Z,GETNXT ; Yes, read next record and reset ptr
LD (GETPTR),HL ; Save new buffer ptr
LD A,(HL) ; Fetch byte from there
GET2: POP HL ; Restore registers
POP DE
POP BC
RET ; Return
; Get next sequential record from archive file
GETNXT: LD C,$READ ; Setup read-sequential function code
; Get record (sequential or random) from archive file
GETREC: LD DE,DBUF ; Point to default buffer
PUSH DE ; Save ptr
PUSH BC ; Save read function code
CALL SETDMA ; Set DMA address
LD DE,IFCB ; Setup FCB address
POP BC ; Restore read function
CALL BDOS ; Do it
POP HL ; Restore buffer ptr
OR A ; End of file?
RET Z ; Return if not
; Unexpected end of file
EOF: LD DE,FMTERR ; Print bad format message and abort
JP PABORT ; (not much else we can do)
; Count bytes skipped while processing bad archive header
BADCNT: INC HL ; Bump bad byte count
LD A,H ; But 64K bytes is enough!
OR L
RET NZ ; Return if not reached limit
JR EOF ; Else, report bad format and abort
PAGE
; Seek to new random position in file (relative to current position)
; (BCDE = 32-bit byte offset)
SEEK: LD A,B ; Most CP/M (2.2) can handle is 23 bits
OR A ; So highest bits of offset must be 0
JR NZ,EOF ; Else, that's certainly past eof!
LD A,E ; Get low bits of offset in A
LD L,D ; Get middle bits in HL
LD H,C
ADD A,A ; LSB of record offset -> carry
ADC_HL HL ; Record offset -> HL
JR C,EOF ; If too big, report unexpected eof
RRA ; Get byte offset
EX DE,HL ; Save record offset
LD HL,GETPTR ; Point to offset (+80H) of last byte in
ADD A,(HL) ; Add byte offsets
LD (HL),A ; Update buffer ptr for new position
INC A ; But does it overflow current record?
JP P,SEEK1 ; Yes, skip
LD A,D ; Check record offset
OR E
RET Z ; Return if none (still in same record)
DEC DE ; Get offset from next record
JR SEEK2 ; Go compute new record no.
SEEK1: ADD A,7FH ; Get proper byte offset in DMA page
LD (HL),A ; Save new buffer pointer
SEEK2: PUSH DE ; Save record offset
LD DE,IFCB
LD C,$RECORD ; Compute current "random" record no.
CALL BDOS ; (I.e. next sequential record to read)
LD HL,(IFCB+@RN) ; Get result
POP DE ; Restore record offset
ADD HL,DE ; Compute new record no.
JR C,EOF ; If >64k, it's past largest (8 Mb) file
LD (IFCB+@RN),HL ; Save new record no.
LD C,$READR ; Read the random record
CALL GETREC
LD HL,IFCB+@CR ; Point to current record in extent
INC (HL) ; Bump for subsequent sequential read
RET ; Return
PAGE
; Get archive file header
GETHDR: LD DE,HDRBUF ; Set to fill header buffer
LD B,HDRSIZ ; Setup normal header size
CP 1 ; But test if version 1
PUSH AF ; Save test result
JR NZ,GETHD2 ; Skip if not version 1
LD B,HDRSIZ-4 ; Else, header is 4 bytes less
JR GETHD2 ; Go to store loop
GETHD1: CALL GET ; Get header byte
GETHD2: LD (DE),A ; Store in buffer
INC DE
DJNZ GETHD1 ; Loop for all bytes
POP AF ; Version 1?
RET NZ ; No, all done
LD HL,SIZE ; Yes, point to compressed size
LD C,4 ; It's 4 bytes
LDIR ; Move to uncompressed length
RET ; Return
PAGE
; Get, save, and test file name from archive header
GETNAM: LD DE,NAME ; Point to name in header
LD HL,OFCB+@FN ; Point to output file name
LD_IX TNAME ; Point to test pattern
LD B,11 ; Set count for name and type
GETN1: LD A,(DE) ; Get next name char
AND 7FH ; Ensure no flags, is it end of name?
JR Z,GETN4 ; Yes, go store blank
INC DE ; Bump name ptr
CP ' '+1 ; Is it legal char for file name?
JR C,GETN2 ; No, if blank or non-printing,
CP DEL ; or this
JR NZ,GETN3 ; Skip if ok
GETN2: LD A,'$' ; Else, change to something legal
GETN3: CALL UPCASE ; Ensure it's upper case
CP '.' ; But is it type separator?
JR NZ,GETN5 ; No, go store name char
LD A,B ; Get count of chars left
CP 4 ; Reached type yet?
JR C,GETN1 ; Yes, bypass the separator
DEC DE ; Backup to re-read separator
GETN4: LD A,' ' ; Set to store a blank
GETN5: LD (HL),A ; Store char in output name
LD_A_IX ; Get pattern char
INC_IX ; Bump pattern ptr
CP '?' ; Pattern matches any char?
JR Z,GETN6 ; Yes, skip
CP (HL) ; Matches this char?
RET NZ ; Return (NZ) if not
GETN6: INC HL ; Bump store ptr
DJNZ GETN1 ; Loop until FCB name filled
LD BC,256*(@FCBSZ-@FN-11)+0
JP FILL ; Zero rest of FCB, return (Z still set)
PAGE
SUBTTL File Output Routines
; Check output drive and setup for file output
OUTSET: LD A,(HODRV) ; Get highest allowed output drive
LD B,A ; Save for later test
LD HL,CHECKF ; Point to check-only flag
CALL WHLCK ; Check wheel byte
DEC A ; Is user privileged?
JR NZ,OUTS1 ; Yes, skip
LD B,A ; Else, no output drive allowed
LD (HL),A ; No checking allowed
LD (PROUTF),A ; No printing allowed
LD A,(TYFLG) ; Fetch flag for typeout allowed
OUTS1: LD C,A ; Save typeout flag (always if wheel)
LD A,(OFCB) ; Any output drive?
OR A
JR NZ,OUTS2 ; Yes, skip to check it
OR (HL) ; Just checking files?
JR Z,CKTYP ; No, go see if typeout permitted
LD DE,CHKMSG ; Yes, show 'Checking...' message
CALL PRINTL
LD A,0FEH ; Set dummy drive in output FCB
LD (OFCB),A
JR CRCINI ; Skip to init CRC computations
OUTS2: DEC A ; Get zero-relative drive no.
CP B ; In range of allowed drives?
LD DE,BADODR ; No, report bad output drive
JP NC,PABORT ; and abort
LD E,A ; Save output drive
PUSH DE
ADD A,'A' ; Convert to ASCII
LD (OUTDRV),A ; Store drive letter for message
LD DE,OUTMSG ; Show output drive
CALL PRINTL
LD C,$DISK ; Get default drive
CALL BDOS
POP DE ; Recover output drive
CP E ; Test if same as default
PUSH AF ; Save default drive (and test result)
LD C,$SELECT ; Select output drive
CALL NZ,BDOS ; (if different than default)
CALL GETBLS ; Get its block size for listing
POP AF ; Restore original default drive
LD E,A
LD C,$SELECT ; Reselect it
CALL NZ,BDOS ; (if changed)
PAGE
; Initialize lookup table for CRC generation
; Note: For maximum speed, the CRC routines rely on the fact that the
; lookup table (CRCTAB) is page-aligned.
X16 EQU 0 ; x^16 (implied)
X15 EQU 1 SHL (15-15) ; x^15
X2 EQU 1 SHL (15-2) ; x^2
X0 EQU 1 SHL (15-0) ; x^0 = 1
POLY EQU X16+X15+X2+X0 ; Polynomial (CRC-16)
CRCINI: LD HL,CRCTAB+256 ; Point to 2nd page of lookup table
LD A,H ; Check enough memory to store it
CALL CKMEM
LD DE,POLY ; Setup polynomial
; Loop to compute CRC for each possible byte value from 0 to 255
CRCIN1: LD A,L ; Init low CRC byte to table index
LD BC,256*8 ; Setup bit count, clear high CRC byte
; Loop to include each bit of byte in CRC
CRCIN2: SRL C ; Shift CRC right 1 bit (high byte)
RRA ; (low byte)
JR NC,CRCIN3 ; Skip if 0 shifted out
EX_AF ; Save lower CRC byte
LD A,C ; Update upper CRC byte
XOR D ; with upper polynomial byte
LD C,A
EX_AF ; Recover lower CRC byte
XOR E ; Update with lower polynomial byte
CRCIN3: DJNZ CRCIN2 ; Loop for 8 bits
LD (HL),C ; Store upper CRC byte (2nd table page)
DEC H
LD (HL),A ; Store lower CRC byte (1st table page)
INC H
INC L ; Bump table index
JR NZ,CRCIN1 ; Loop for 256 table entries
RET
PAGE
; Check for valid file name for typeout (or printing)
CKTYP: OR C ; Typeout not allowed?
CALL NZ,TAMBIG ; Or ambiguous output file name?
RET Z ; Yes, return (will just list file)
LD DE,NOTYP ; Point to table of excluded types
CKTYP1: LD HL,TNAME+8 ; Point to type of selected file
LD B,3 ; Setup count for 3 chars
CKTYP2: LD A,(DE) ; Fetch next table char
OR A ; End of table?
JR Z,CKTYP5 ; Yes, go set flag to allow typeout
CP '?' ; Matches any char?
JR Z,CKTYP3 ; Yes, skip
CP (HL) ; Matches this char?
CKTYP3: INC DE ; Bump table ptr
JR Z,CKTYP4 ; Matched?
DJNZ CKTYP3 ; No, just advance to next table entry
JR CKTYP1 ; Then loop to try again
CKTYP4: INC HL ; Char matched, point to next
DJNZ CKTYP2 ; Loop for all chars in file type
RET ; If all matched, return (no typeout)
CKTYP5: DEC A ; If no match, file name is valid
LD (OFCB),A ; Set dummy drive (0FFH) in output FCB
RET ; Return
; Test for ambiguous output file selection
TAMBIG: LD HL,TNAME ; Point to test pattern
; Check for ambiguous file name (HL = ptr to FCB-type name)
FAMBIG: LD BC,11 ; Setup count for file name and type
LD A,'?' ; Any "?" chars?
CPIR ; Yes, return with Z set
RET ; No, return NZ
PAGE
; Extract file for disk or console output
OUTPUT: LD A,(OFCB) ; Any output drive (or typing files)?
OR A
RET Z ; No, there's nothing to do here
LD B,A ; Save output drive
LD A,(VER) ; Get header version
CP ARCVER+1 ; Supported for output?
LD DE,BADVER ; No, report unknown version
JP NC,PABORT ; and abort
LD L,A ; Copy version
LD H,0
LD DE,OBUFT-1 ; Use to index table of starting
ADD HL,DE ; output buffer pages
LD A,(HL) ; Get starting page of buffer
CALL CKMEM ; Ensure enough memory
LD HL,BUFPAG ; Point to buffer start page
LD (HL),A ; Save it
LD C,A ; (also for typeout buffer check)
INC HL ; Point to buffer limit (BUFLIM)
LD A,(HIPAGE) ; Get memory limit page
LD (HL),A ; Assume max possible output buffer
INC B ; Typing files?
JR NZ,OUTDSK ; No, go extract to disk
; Setup for console (or printer) output
LD A,(TYPGS) ; Get max. pages to buffer typeout
OR A ; No limit?
CALL Z,WHLCK ; And is this privileged user?
JR Z,OUTCON ; Yes, skip (use 1 page if no privilege)
ADD A,C ; Compute desired limit page
JR C,OUTCON ; But skip if exceeds (physical) memory
CP (HL)
JR NC,OUTCON ; Also if exceeds available memory
LD (HL),A ; If ok, set lower buffer limit
OUTCON: LD A,(PROUTF) ; Printing file?
OR A
JR NZ,OUTBEG ; Yes, skip the separator
LD HL,LINE ; Fill listing line with dashes
LD BC,256*LINLEN+'-'
CALL FILL
CALL LISTL ; Print separating line first
JR OUTBEG ; Go extract file for typeout
PAGE
; Setup for disk file (or black hole) output
OUTDSK: INC B ; Just checking file?
JR Z,OUTBEG ; Yes, skip
LD DE,BUFF ; Set DMA address to a safe place
CALL SETDMA
LD C,$FIND ; Find file
CALL OFDOS ; Already exists?
JR Z,OUTD2 ; No, skip
LD DE,EXISTS ; Inform user and ask:
CALL PRINTS ; Should we overwrite existing file?
OUTD1: CALL CABORT ; Wait for response (or CTRL-C abort)
JR Z,OUTD1
LD E,A ; Save response
CALL CRLF ; Start a new line after prompt
LD A,E ; Get response char
CALL UPCASE ; Upper and lower case are the same
CP 'Y' ; Answer was yes?
RET NZ ; No, return (skip file output)
LD C,$DELETE ; Yes, delete existing file
CALL OFDOS
OUTD2: LD C,$MAKE ; Create a new file
CALL OFDOS ; But directory full?
LD DE,DIRFUL ; Yes, report error
JP Z,PABORT ; and abort
LD (OFLAG),A ; Set flag for output file open
PAGE
; All set to output file
OUTBEG: LD A,(VER) ; Check compression type
CP 4
JR NC,USQ ; Skip if squeezed or crunched/squashed
CALL PUTSET ; Else (simple cases), setup output regs
CP 3 ; Packed?
JR Z,UPK ; Yes, skip
; Uncompressed file
UNC: CALL GETC ; Just copy input to output
JR C,OUTEND ; until end of file
CALL PUT
JR UNC
; Packed file
UPK1: CALL PUTUP ; Output with repeated byte expansion
UPK: CALL GETC ; Get input byte
JR NC,UPK1 ; Loop until end of file
; End of output file
OUTEND: CALL PUTBUF ; Flush final buffer (if any)
LD A,(OFCB) ; Typing (or printing) file?
INC A
RET Z ; Yes, all done (no CRC check)
; Note: Following instruction added in UNARC 1.6, since the preceding
; test (altered in 1.42) no longer clears carry.
OR A ; Clear carry for 16-bit subtract
EX DE,HL ; Save computed CRC
LD HL,(CRC) ; Get CRC recorded in archive header
SBC_HL DE ; Do they match?
LD DE,CRCERR ; If not,
CALL NZ,OWARN ; print warning message
LD HL,LEN ; Point to remaining (output) length
CALL LGET ; Fetch length (it's 4 bytes)
LD A,B ; All should be zero...
OR C
OR D
OR E
LD DE,LENERR ; If not,
CALL NZ,OWARN ; print incorrect length warning
CALL OCLOSE ; Close output file (if open)
LD HL,OFLAG ; Clear file open flag
LD (HL),0
RET NZ ; Return unless error closing file
LD DE,CLSERR ; Else, report close failure
JP PABORT ; and abort
PAGE
; Unsqueeze (Huffman-coded) file
.COMMENT |
Note: Although numerous assembly-language implementations of Richard
Greenlaw's pioneer USQ (C language) program have appeared, all of the
coding here is original. At risk of being accused of "re-inventing
the wheel," we do this primarily for personal satisfaction (not to
mention protection of our copyright).
We were tempted to use the super-fast algorithm suggested by Steven
Greenberg's recent public contribution, UF (aka USQFST, nee FU).
(After all, we require a Z80, so why not take advantage of the latest
technology?) However, some of the speed benefit of Greenberg's method
is necessarily lost, since we do not buffer the input file and must
count each input byte against the file size recorded in the archive
header. (Input buffering is not advantageous, since we must have
random access to the archive file.) Also, the occurence of squeezed
files in archives is relatively rare, since the "crunching" method
produces better compression in most cases. Thus we use a more
classical approach, albeit at the expense of the ultimate in
performance, but with a substantial savings in code complexity and
memory requirements.
Note also that many authors go to elaborate pains to check the validity
of the binary decoding tree. Such checks include: (1) the node count
(can be at most 256, although some people mistakenly think it can be
greater -- c.f. Knuth, vol. 1, 2nd ed., sec. 2.3.4.5, pp. 399-405); (2)
all node links in the tree must be in the range specified by the node
count; (3) no infinite loops in the tree (this one's not so easy to
test); and (4) premature end-of-file in the tree or data. Instead, we
take a KISS approach which assumes the tree is valid and relies upon
the final output file CRC and length checks to warn of any possible
errors: (1) the tree is initially cleared (all links point to the root
node); (2) at most 256 nodes are stored; and (3) decoding terminates
upon detecting the special end-of-file code in the data (the normal
case), the physical end-of-file (as determined by the size recorded in
the archive header), or a tree link to the root node (which indicates a
diseased tree). |
PAGE
; Start unsqueezing
USQ: JR NZ,UCR ; But skip if crunched/squashed file
; First clear the decoding tree
LD BC,TREESZ-1 ; Setup bytes to clear - 1
CALL TREECL ; (Leaves DE pointing past end of tree)
; Read in the tree
; Note: The end-of-file condition may be safely ignored while reading
; the node count and tree, since GETC will repeatedly return
; zero bytes in this case.
CALL GETC ; Get node count, low byte
LD C,A ; Save for loop
CALL GETC ; Get high byte (can be ignored)
OR C ; But is it zero nodes?
JR Z,USQ3 ; Yes (very unlikely), it's empty file
USQ1: LD B,4 ; Setup count for 4 bytes in node
LD A,D ; Each byte will be stored in a separate
SUB B ; page (tree is page-aligned), so
LD D,A ; point back to the first page
USQ2: CALL GETC ; Get next byte
LD (DE),A ; Store in tree
INC D ; Point to next page
DJNZ USQ2 ; Loop for all bytes in node
INC E ; Bump tree index
DEC C ; Reduce node count
JR NZ,USQ1 ; Loop for all nodes
USQ3: CALL PUTSET ; Done with tree, setup output regs
PUSH HL ; Reset current input byte (on stack)
; Start of decoding loop for next output byte
USQ4: EXX ; Save output registers
XOR A ; Reset node index to root of tree
; Top of loop for next input bit
USQ5: LD L,A ; Setup index of next tree node
POP AF ; Get current input byte
SRL A ; Shift out next input bit
JR NZ,USQ6 ; Skip unless need a new byte
PAGE
; Read next input byte
PUSH HL ; Save tree index
CALL GETCX ; Get next input byte
EXX ; Save output regs
JR C,USQEND ; But go stop if reached end of input
POP HL ; Restore tree index
SCF ; Set flag for end-of-byte detection
RRA ; Shift out first bit of new byte
; Process next input bit
USQ6: PUSH AF ; Save input byte
LD H,HIGH TREE ; Point to start of current node
JR NC,USQ7 ; Skip if new bit is 0
INC H ; Bit is 1, point to 2nd word of node
INC H ; (3rd tree page)
USQ7: LD A,(HL) ; Get low byte of node word
INC H
LD B,(HL) ; Get high byte (from next tree page)
INC B
JR NZ,USQ8 ; Skip if high byte not -1
CPL ; We've got output byte (complemented)
EXX ; Restore regs for output
CALL PUTUP ; Output with repeated byte expansion
JR USQ4 ; Loop for next byte
USQ8: DJNZ USQEND ; If high byte not 0, it's special EOF
OR A ; If high byte was 0, its new node link
JR NZ,USQ5 ; Loop for new node (but can't be root)
; End of squeezed file (physical, logical, or due to Dutch elm disease)
USQEND: POP HL ; Cleanup stack
; End of unsqueezed or uncrunched file output
UCREND: EXX ; Restore output regs
JP OUTEND ; Go end output
; Clear squeezed file decoding tree (or crunched file string table)
TREECL: LD HL,TREE ; Point to tree (also string table)
STRTCL: ; (Entry for partial string table clear)
LD (HL),L ; Clear first byte (it's page-aligned)
LD D,H ; Copy pointer to first byte
LD E,L
INC DE ; Propogate it thru second byte, etc.
LDIR ; (called with BC = byte count - 1)
RET ; Return
PAGE
; Uncrunch (LZW-coded) file
.COMMENT |
The Lempel-Ziv-Welch (so-called "LZW") data compression algorithm is
the most impressive benefit of ARC files. It performs better than
Huffman coding in many cases, often achieving 50% or better compression
of ASCII text files and 15%-40% compression of binary object files.
The algorithm is named after its inventors: A. Lempel and J. Ziv
provided the original theoretical groundwork, while Terry A. Welch
published an elegant practical implementation of their procedure. (The
definitive article is Welch's "A Technique for High-Performance Data
Compression", in the June 1984 issue of IEEE Computer magazine.)
The Huffman algorithm encoded each input byte by a variable-length bit
string (up to 16 bits in Greenlaw's implementation), with bit length
(approximately) inversely proportional to the frequency of occurrence
of the encoded byte. This has the disadvantages of requiring (1) two
passes over the input file for encoding and (2) the inclusion of the
decoding information along with the output file (a binary tree of up to
1026 bytes in Greenlaw's implementation). In comparison, LZW is a one-
pass procedure which encodes variable-length strings of bytes by a
fixed-length code (12 bits in this implementation), without additional
overhead in the output file. In essence, the procedure adapts itself
dynamically to the redundancy present in the input data. There is one
drawback: LZW requires substantially more memory than the Huffman
algorithm for both encoding and decoding. (A 12K-byte string table is
required in this program; the MS-DOS ARC program uses even more. Of
course, 12K is not that much these days: I don't think they're even
selling IBM-PC's or MAC's with less than 512K anymore. But some of us
in the CP/M world are still concerned with efficiency of memory use.)
The MS-DOS ARC program by System Enhancement Associates has (to date)
employed four different variations on the LZW scheme, differentiated by
the version byte in the archive file header:
Version 5: LZW applied to original input file
Version 6: LZW applied to file after packing repeated bytes
Version 7: Same as version 6 with a new (faster) hash code
Version 8: Completely new (much improved) implementation
The MS-DOS program PKARC 2.0 introduced another variation ("squashing"):
Version 9: Same as version 8 with 13-bit codes and no pre-packing
Version 8 (and 9) varies the output code width from 9 to 12 (13) bits
as the string table grows (benefits small files), performs an adaptive
reset of the string table after it becomes full if the compression
ratio drops (benefits large files), and eliminates the need for hash
computations by the decoder (reduces decoding time and space; in this
program, an extra 8K-byte table is eliminated). Although the latest
release of the ARC program uses only this last version for encoding,
we, like ARC (PKXARC), support all four (five) versions for
compatibility with files encoded by earlier releases. |
PAGE
; Setup for uncrunching (or unsquashing)
; We've been able to isolate all of the differences between the five
; versions of LZW into just three routines -- input, output, and hash
; function. These are disposed of first, by inserting appropriate
; vectors into common coding and initializing version-dependent data.
; Note: Introduction of squashed files in UNARC 1.42 has added some
; extra kludges here.
UCR: LD HL,STRBIT ; All but version 9 use 4K string table
LD (HL),BIT4H ; entries, so setup STRADD bit test
CP 8 ; Version 8 or 9?
JR NC,UCR1 ; Yes, skip
LD DE,OGETCR ; Old versions get fixed 12-bit codes
LD BC,STRSZ+HSHSZ-1; and need extra table for hashing
LD HL,OHASH ; Assume old hash function
CP 6 ; Test version
LD A,55H ; Setup initial flags for OGETCR
JR Z,UCR6 ; All set if version 6
JR C,UCR5 ; Skip if version 5
LD HL,FHASH ; Version 7 uses faster hash function
JR UCR6 ; (but we've never seen one of these!)
UCR1: JR Z,UCR2 ; Skip if version 8
LD (HL),BIT5H ; Version 9 allows 13-bit codes
LD BC,STQSZ-1 ; and has larger string table
LD A,8192/256 ; with 8K entries (less buffer space)
JR UCR4 ; Join common code for versions 8 and 9
; Note: This is the only place that we reference the code size for
; crunched files (CRBITS) symbolically. Currently, a value of
; 12 bits is required and it is assumed throughout the program.
UCR2: CALL GETC ; Read code size used to crunch file
JR C,UCR3 ; But skip if none (PKARC 0-length file)
CP CRBITS ; Same as what we expect?
LD DE,UCRERR ; No, report incompatible format
JP NZ,PABORT ; and abort
UCR3: LD BC,STRSZ-1 ; Version 8 provides more buffer space
LD A,4096/256 ; and only 4K string table entries
UCR4: LD (STRMAX),A ; Setup NHASH table-full test
LD HL,0 ; Clear code residue and count to init
LD (CODES),HL ; NGETCR input (BITSAV and CODES)
LD DE,NGETCR ; New version has variable-length codes
LD HL,NHASH ; and has a very simple "hash"
LD A,9 ; Setup initial code size for NGETCR
JR Z,UCR6 ; Skip if version 8
UCR5: LD_IX PUT ; Versions 5 and 9 don't unpack
JR UCR7
UCR6: LD_IX PUTUP ; Versions 6-8 unpack repeated bytes
UCR7: STO_IX PUTCRP ; Save ptr to output routine
LD (HASHP),HL ; Save ptr to hash function
STO_DE GETCRP ; Save ptr to input routine
LD (BITS),A ; Initialize input routine
LD A,B ; Get string table pages to clear (-1)
SUB 3 ; Less 3 for atomic strings
LD (STRCSZ),A ; Setup for reset clear in NGETCR
PAGE
; Start uncrunching
; (All version-dependent differences are handled now)
CALL TREECL ; Clear string (and hash) table(s)
STO_BC STRCT ; Set no entries in string table
DEC BC ; Get code for no prefix string (-1)
PUSH BC ; Save as first-time flag
XOR A ; Init table with one-byte strings...
GCR0: POP BC ; Set for no prefix string
PUSH BC ; (Resave first-time flag)
PUSH AF ; Save byte value
CALL STRADD ; Add to table
POP AF ; Recover byte
INC A ; Done all 256 bytes?
JR NZ,GCR0 ; No, loop for next
CALL PUTSET ; Setup output registers
; Top of loop for next input code (top of stack holds previous code)
GCR: EXX ; Save output regs first
GETCR: CALL 0 ; Get next input code
GETCRP EQU $-2 ; (ptr to NGETCR or OGETCR stored here)
POP BC ; Recover previous input code (or -1)
JP C,UCREND ; But all done if end of input
PUSH HL ; Save new code for next loop
CALL STRPTR ; Point to string table entry for code
INC B ; Is this the first one in file?
JR NZ,GCR2 ; No, skip
INC HL ; Yes,
LD A,(HL) ; Get first output byte
GCR1: CALL PUTCR ; Output final byte for this code
JR GCR ; Loop for next input code
GCR2: DEC B ; Correct prev code (stays in BC awhile)
LD A,(HL) ; Is new code in table?
OR A
PUSH AF ; (Save test result for later)
JR NZ,GCR3 ; Yes, skip
LD H,B ; Else (special case), setup previous
LD L,C ; code (it prefixes the new one)
CALL STRPTR ; Point to its table entry instead
PAGE
; At this point, we have the table ptr for the new output string (except
; possibly its final byte, which is a special case to be handled later).
; Unfortunately, the table entries are linked in reverse order. I.e.,
; we are pointing to the last byte to be output. Therefore, we trace
; through the table to find the first byte of the string, reversing the
; link order as we go. When done, we can output the string in forward
; order and restore the original link order. (This is, we think, an
; innovative approach: it saves allocation of an extra 4K-byte stack,
; as in the MS-DOS ARC program, or an enormous program stack, as needed
; for the recursive algorithm of Steve Greenberg's UNCRunch program.)
; Careful: The following value must be non-zero, so that the old-style
; hash (invoked by STRADD below) will not think a re-linked entry is
; unused! (In a development version, we used zero; this worked fine for
; newer crunched files, but proved a difficult bug to squash when the
; old-style de-crunching failed randomly.)
GCR3: LD D,1 ; Init previous entry ptr (01xxH = none)
GCR4: LD A,(HL) ; Test this entry
CP HIGH STRT ; Any prefix string?
JR C,GCR5 ; No, we've reached the first byte
LD (HL),D ; Relink this entry
LD D,A ; (i.e. swap prev ptr with prefix ptr)
DEC HL
LD A,(HL)
LD (HL),E
LD E,A
INC HL
EX DE,HL ; Swap current ptr with prefix ptr
JR GCR4 ; Loop for next entry
; HL points to table entry for first byte of output string. We can now
; add the table entry for the string which the encoder placed in his
; table before sending us the current code. (It's the previous code's
; string concatenated with the first byte of the new string). Note that
; BC has been holding the previous code all this time.
GCR5: INC HL ; Point to byte
POP AF ; Recover special-case flag
LD A,(HL) ; Fetch byte
PUSH AF ; Re-save flag along with byte
DEC HL ; Restore table ptr
PUSH DE ; Save ptr to prev entry
PUSH HL ; Save ptr to this entry
CALL STRADD ; Add new code to table (for BC and A)
POP HL ; Setup table ptr for output loop
PAGE
; Top of string output loop
; HL points to table entry for byte to output.
; Top of stack contains pointer to next table entry (or 01xxH).
GCR6: INC HL ; Point to byte
LD A,(HL) ; Fetch it
PUSH HL ; Save table ptr
CALL PUTCR ; Output the byte (finally!)
EXX ; Save output regs
POP DE ; Recover ptr to this byte
POP HL ; Recover ptr to next byte's entry
DEC H ; Reached end of string?
JR Z,GCR7 ; Yes, skip out of loop
INC H ; Correct next entry ptr from above test
DEC DE ; Restore ptr to this entry's mid byte
LD A,(HL) ; Relink the next entry
LD (HL),D ; (i.e. swap its "prefix" ptr with
LD D,A ; ptr to this entry)
DEC HL
LD A,(HL)
LD (HL),E
LD E,A
INC HL
PUSH DE ; Save ptr to 2nd next entry
JR GCR6 ; Loop to output next byte
; End of uncrunching loop
; All bytes of new string have been output, except possibly the final
; byte (which is the same as the first byte in this special case).
GCR7: POP AF ; Recover special-case flag and byte
JR NZ,GETCR ; If not set, loop for next input code
JR GCR1 ; Else, go output final byte first
PAGE
; Add entry to string table
; This routine receives a 12-bit prefix string code in BC and a suffix
; byte in A. It then adds an entry to the string table (unless it's
; full) for the new string obtained by concatenating these. Nothing
; is (or need be) returned to the caller.
.COMMENT |
String table format:
The table (STRT) contains 4096 three-byte entries, each of which is
identified by a 12-bit code (table index). The third byte (highest
address) of each entry contains the suffix byte for the string. The
first two bytes contain a pointer (low-byte first) to the middle byte
of the table entry for the prefix string. The null string (prefix to
the one-byte strings) is represented by a (16-bit) code value -1, which
yields a non-zero pointer below the base address of the table. An
empty table entry contains a zero prefix pointer.
Our choice to represent prefix strings by pointers rather than codes
speeds up almost everything we do. The high byte of the prefix pointer
(middle byte of an entry) may be tested for non-zero to determine if an
entry is occupied, and (since the table is page-aligned) it may be
further tested against the page address of the table's base (HIGH STRT)
to decide if it represents the null string.
Note that the entry for code 256 is not used in the newer version of
crunching. This is reserved for a special signal to reset the string
table (handled by the hash and input routines, NHASH and NGETCR). |
STRADD: LD HL,(STRCT) ; Get count of strings in table
BIT 4,H ; Is it the full 4K?
; Note: Above test complicated by introduction of squashed files (which
; allow 13-bit codes and 8K string table entries) and the non-Z80
; emulation of the BIT instruction. Following definitions handle
; this.
IF Z80
STRBIT EQU $-1 ; Byte to modify BIT instruction
BIT4H EQU 64H ; High byte of BIT 4,H
BIT5H EQU 6CH ; High byte of BIT 5,H
ELSE
STRBIT EQU BITMSK ; Byte to modify emulated BIT
BIT4H EQU 1 SHL 4 ; Mask to test bit 4
BIT5H EQU 1 SHL 5 ; Mask to test bit 5
ENDIF
RET NZ ; Yes, forget it
INC HL ; Bump count for one more
LD (STRCT),HL ; Save new string count
PUSH AF ; Save suffix byte
PUSH BC ; Save prefix code
CALL 0 ; Hash them to get pointer to new entry
HASHP EQU $-2 ; (ptr to xHASH routine stored here)
EX (SP),HL ; Save result, recover prefix code
CALL STRPTR ; Get pointer to prefix entry
EX DE,HL ; Save it
POP HL ; Recover new entry pointer
DEC HL ; Point to low byte of entry
LD (HL),E ; Store prefix ptr in entry
INC HL ; (low byte first)
LD (HL),D ; (then high byte, in mid entry byte)
INC HL ; Point to high byte of new entry
POP AF ; Recover suffix byte
LD (HL),A ; Store
RET ; All done
PAGE
; Hash function for (new-style) crunched files
; Note: "Hash" is of course a misnomer here, since strings are simply
; added to the table sequentially with the newer crunch method.
; This routine's main responsibility is to update the bit-length
; for expected input codes, and to bypass the table entry for
; code 256 (reserved for adaptive reset), at appropriate times.
NHASH: LD A,L ; Copy low byte of string count in HL
DEC L ; Get table offset for new entry
OR A ; But is count a multiple of 256?
JR NZ,STRPTR ; No, just return the table pointer
LD A,H ; Copy high byte of count
DEC H ; Complete double-register decrement
LD DE,STRCT ; Set to bump string count (bypasses
JR Z,NHASH1 ; next entry) if exactly 256
CP 4096/256 ; Else, is count the full 4K?
STRMAX EQU $-1 ; (Byte to modify max string count test)
JR Z,STRPTR ; Yes (last table entry), skip
; Note the following cute test. (It's mentioned in K & R, ex. 2-9.)
AND H ; Is count a power-of-two?
JR NZ,STRPTR ; No, skip
LD DE,BITS ; Yes, next input code is one bit longer
; Note: By definition, there can be no input code residue at this point.
; I.e. (BITSAV) = 0, since we have read a power-of-two (> 256) no.
; of codes at the old length (total no. of bits divisible by 8).
; By the same argument, (CODES) = 0 modulo 8 (see NGETCR).
NHASH1: EX DE,HL ; Swap in address value to increment
INC (HL) ; Bump the value (STRCT or BITS)
EX DE,HL ; Recover table offset
; Get pointer to string table entry
; This routine is input a 12-bit code in HL (or -1 for the null string).
; It returns a pointer in HL to the middle byte of the string table
; entry for that code (STRT-2 for the null string). Destroys DE only.
STRPTR: LD D,H ; Copy code
LD E,L
ADD HL,HL ; Get 2 * code
ADD HL,DE ; Get 3 * code
LD DE,STRT+1 ; Point to table base entry (2nd byte)
ADD HL,DE ; Compute pointer
RET ; Return
PAGE
; Get variable-length code from (new-style) crunched file
.COMMENT |
These codes are packed in right-to-left order (lsb first). The code
length (stored in BITS) begins at 9 bits and increases up to a maximum
of 12 bits (13 bits for squashed files) as the string table grows
(maintained by NHASH). Location BITSAV holds residue bits remaining in
the last input byte after each call (must be initialized to 0, code
assumes BITSAV = BITS-1).
In comparison, the MS-DOS ARC program buffers 8 codes at a time (i.e.
n bytes, where n = bits/code) and flushes this buffer whenever the code
length changes (so that first code at new length begins on an even byte
boundary). By coincidence (see NHASH) this buffer is always empty when
the code length increases as a result of normal string table growth.
Thus the only time this added bufferring affects us is when the code
length is reset back to 9 bits upon receipt of the special clear
request (code 256), at which time we must possibly bypass up to 10
input bytes (worst case = 7 codes at 1.5 bytes/code). This is handled
by a simple down-counter in location CODES, whose mod-8 value indicates
the no. of codes which should be skipped (must be initialized to 0,
code assumes that CODES = BITSAV-1). |
; Note: This can probably be made a lot faster (e.g. by unfolding into
; 8 separate cases and using a co-routine return), but that's a
; lot of work. For now, we KISS ("keep it short and simple").
NGETCR: LD HL,CODES ; First update code counter
DEC (HL) ; for clear code processing
INC HL ; Point to BITSAV
LD A,(HL) ; Get saved residue bits
INC HL ; Point to BITS
LD B,(HL) ; Setup bit counter for new code
LD HL,7FFFH ; Init code (msb reset for end detect)
; Top of loop for next input bit
NGETC1: SRL A ; Shift out next input bit
JR Z,NGETC7 ; But skip out if new byte needed
NGETC2: RR H ; Shift bit into high end of code word
RR L ; (double-register shift)
DJNZ NGETC1 ; Loop until have all bits needed
; Input complete, cleanup code word
NGETC3: SRL H ; Shift code down,
RR L ; to right-justify it in HL
JR C,NGETC3 ; Loop until end flag shifted out
LD (BITSAV),A ; Save input residue for next call
LD A,H ; But is it code 256?
DEC A ; (i.e. adaptive reset request)
OR L
RET NZ ; No, return (carry clear)
; Special handling to reset string table upon receipt of clear code
LD HL,BITS ; Point to BITS
LD C,(HL) ; Fetch current code length
LD (HL),9 ; Go back to 9-bit codes
DEC HL ; Point to BITSAV
LD (HL),A ; Empty the residue buffer
DEC HL ; Point to CODES
LD A,(HL) ; Get code counter
AND 7 ; Modulo 8 is no. codes to flush
JR Z,NGETC6 ; Skip if none
; Note: It's a shame we have to do this at all. With a minor change in
; its implementation, the MS-DOS ARC program could have simply
; shuffled down its buffer and avoided wasting up to 10 bytes in
; the crunched file (not to mention a lot of unnecessary effort).
; Note: Prior to UNARC 1.4, the following coding was simplified by the
; (incorrect) assumption that 12-bit codes are being generated at
; this point. While true for .ARC files created by ARC 5.12 or
; earlier, this is not necessarily the case for files created by
; PKARC 1.1 or later. Hence, some added effort here now...
LD B,A ; Save no. codes to flush
XOR A ; Reset no. bits to flush
LD (HL),A ; Reset code counter to 0 for next time
NGETC4: ADD A,C ; Add no. bits per code
DJNZ NGETC4 ; Loop to compute total bits to flush
RRA ; Divide by 8
RRA
RRA
AND 0FH ; Max possible result 10 (11 squashed)
LD B,A ; Obtain no. input bytes to bypass
NGETC5: PUSH BC ; Loop to flush the (encoder's) buffer
CALL GETCX
EXX ; (No need to test for end-of-file
POP BC ; here, we'll pick it up later if
DJNZ NGETC5 ; it happens)
NGETC6: LD HL,STRT+(3*256) ; Clear out (all but one-byte) strings
LD BC,STRSZ-(3*256)-1
STRCSZ EQU $-1 ; (Byte to modify string tbl clear size)
CALL STRTCL
LD HL,257 ; Reset count for just one-byte strings
LD (STRCT),HL ; plus the unused entry
; Kludge: We rely here on the fact that the previous input code is at
; top of caller's stack, where -1 indicates none. This should
; properly be done by the caller, but doing it here preserves
; commonality of coding for old-style crunched files (i.e. caller
; never knows this happened).
POP HL ; Get return address
EX (SP),HL ; Exchange with top of (caller's) stack
LD HL,-1 ; Set no previous code
EX (SP),HL ; Replace on stack
PUSH HL ; Restore return
JR NGETCR ; Go again for next input code
; Read next input byte
NGETC7: PUSH BC ; Save bit count
PUSH HL ; Save partial code
CALL GETCX ; Get next input byte
EXX ; Save output regs
POP HL ; Restore code
POP BC ; Restore count
RET C ; But stop if reached end of file
; Special test to speed things up a bit...
; (If need the whole byte, might as well save some bit fiddling)
BIT 3,B ; At least 8 more bits needed?
JR NZ,NGETC8 ; Yes, go do it faster
SCF ; Else, set flag for end-of-byte detect
RRA ; Shift out first bit of new byte
JR NGETC2 ; Go back to bit-shifting loop
; Update code by (entire) new byte
NGETC8: LD L,H ; Shift code down 8 bits
LD H,A ; Insert new byte into code
LD A,B ; Get bit count
SUB 8 ; Reduce by 8
LD B,A ; Update remaining count
JR NZ,NGETC7 ; Get another byte if still more needed
JR NGETC3 ; Else, go exit early (note A=0)
PAGE
; Hash functions for (old-style) crunched files
; This stuff exists for the sole purpose of processing files which were
; created by older releases of MS-DOS ARC (pre-version 5.0). To quote
; that program's author: "Please note how much trouble it can be to
; maintain upwards compatibility." Amen!
; Note: The multiplications required by the two hash function versions
; are sufficiently specialized that we've hand-coded each of them
; separately, for speed, rather than use a common multiply
; subroutine.
; Versions 5 and 6...
; Compute hash key = upper 12 of lower 18 bits of unsigned square of:
; (prefix code + suffix byte) OR 800H
; Note: I'm sure there's a faster way to do this, but I didn't want to
; exert myself unduly for an obsolete crunching method.
OHASH: LD DE,0 ; Clear product
LD L,A ; Extend suffix byte
LD H,D ; to 16 bits
ADD HL,BC ; Sum with prefix code
SET 3,H ; Or in 800H
; We now have a 13-bit number which is to be squared, but we are only
; interested in the lower 18 bits of the 26-bit product. The following
; reduces this to a 12-bit multiply which yields the correct product
; shifted right 2 bits. This is acceptable (we discard the low 6 bits
; anyway) and allows us to compute desired result in a 16-bit register.
; For the algebraically inclined...
; If n is even (n = 2m + 0): n * n = 4(m * m)
; If n is odd (n = 2m + 1): n * n = 4(m * (m+1)) + 1
SRA H ; Divide number by 2 (i.e. "m")
RR L ; HL will be multiplicand (m or m+1)
LD C,H ; Copy to multiplier in C (high byte)
LD A,L ; and A (low byte)
ADC_HL DE ; If was odd, add 1 to multiplicand
; Note there is one anomalous case: The first one-byte string (with
; prefix = -1 = 0FFFFH and suffix = 0) generates the 16-bit sum 0FFFFH,
; which should hash to 800H (not 0). The following test handles this.
JR C,OHASH3 ; Skip if special case (will get 800H)
LD B,12 ; Setup count for 12 bits in multiplier
; Top of multiply loop (vanilla shift-and-add)
OHASH1: SRL C ; Shift out next multiplier bit
RRA
JR NC,OHASH2 ; Skip if 0
EX DE,HL ; Else, swap in product
ADD HL,DE ; Add multiplicand (carries ignored)
EX DE,HL ; Reswap
OHASH2: ADD HL,HL ; Shift multiplicand
DJNZ OHASH1 ; Loop until done all multiplier bits
; Now have the desired hash key in upper 12 bits of the 16-bit product
EX DE,HL ; Obtain product in HL
ADD HL,HL ; Shift high bit into carry
OHASH3: RLA ; Shift up 4 bits into A...
ADD HL,HL
RLA
ADD HL,HL
RLA
ADD HL,HL
RLA
LD L,H ; Move down low 8 bits of final result
JR HASH ; Join common code to mask high 4 bits
; Version 7 (faster)...
; Compute hash key = lower 12 bits of unsigned product:
; (prefix code + suffix byte) * 15073
FHASH: LD L,A ; Extend suffix byte
LD H,0 ; to 16 bits
ADD HL,BC ; Sum with prefix code
; Note: 15073 = 2785 mod 4096, so we need only multiply by 2785.
LD D,H ; Copy sum, and compute in HL:
LD E,L ; 1 * sum
ADD HL,HL ; 2 * sum
ADD HL,HL ; 4 * sum
ADD HL,DE ; 5 * sum
ADD HL,HL ; 10 * sum
ADD HL,HL ; 20 * sum
ADD HL,DE ; 21 * sum
ADD HL,HL ; 42 * sum
ADD HL,DE ; 43 * sum
ADD HL,HL ; 86 * sum
ADD HL,DE ; 87 * sum
ADD HL,HL ; 174 * sum
ADD HL,HL ; 348 * sum
ADD HL,HL ; 696 * sum
ADD HL,HL ; 1392 * sum
ADD HL,HL ; 2784 * sum
ADD HL,DE ; 2785 * sum
LD A,H ; Setup high byte of result
; Common code for old-style hashing
HASH: AND 0FH ; Mask hash key to 12 bits
LD H,A
PUSH HL ; Save key as trial string table index
CALL STRPTR ; Point to string table entry
POP DE ; Restore its index
LD A,(HL) ; Is table entry used?
OR A
RET Z ; No (that was easy), return table ptr
; Hash collision occurred. Trace down list of entries with duplicate
; keys (in auxilliary table HSHT) until the last duplicate is found.
LD BC,HSHT ; Setup collision table base
PUSH HL ; Create dummy stack level
HASH1: POP HL ; Discard last index
EX DE,HL ; Get next trial index
PUSH HL ; Save it
ADD HL,HL ; Get ptr to collision table entry
ADD HL,BC
LD E,(HL) ; Fetch entry
INC HL
LD D,(HL)
LD A,D ; Is it zero?
OR E
JR NZ,HASH1 ; No, loop for next in chain
; We now have the index (top of stack) and pointer (HL) for the last
; entry in the duplicate key list. In order to find an empty spot for
; the new string, we search the string table sequentially starting 101
; (circular) entries past that of the last duplicate.
EX (SP),HL ; Save collision ptr, swap its index
LD E,101 ; Move 101 entries past it
ADD HL,DE
HASH2: RES 4,H ; Mask table index to 12 bits
PUSH HL ; Save index
CALL STRPTR ; Point to string table entry
POP DE ; Restore its index
LD A,(HL) ; Fetch byte from entry
OR A ; Is it empty?
JR Z,HASH3 ; Yes, found a spot in table
EX DE,HL ; Else,
INC HL ; Bump index to next entry
JR HASH2 ; Loop until we find one free
; We now have the index (DE) and pointer (HL) for an available entry
; in the string table. We just need to add the index to the chain of
; duplicates for this hash key, and then return the pointer to caller.
HASH3: EX (SP),HL ; Swap ptr to last duplicate key entry
LD (HL),D ; Add this index to duplicate chain
DEC HL
LD (HL),E
POP HL ; Recover string table ptr
RET ; Return it to caller
PAGE
; Get fixed-length code from (old-style) crunched file
; These codes are packed in left-to-right order (msb first). Two codes
; fit in three bytes, so we alternate processing every other call based
; on a rotating flag word in BITS (initialized to 55H). Location BITSAV
; holds the middle byte between calls (coding assumes BITSAV = BITS-1).
OGETCR: CALL GETCX ; Get next input byte
EXX ; Save output regs
RET C ; Return (carry set) if end of file
LD E,A ; Copy byte (high or low part of code)
LD HL,BITS ; Point to rotating bit pattern
RRC (HL) ; Rotate it
JR C,OGETC1 ; Skip if this is high part of code
DEC HL ; Point to saved byte from last call
LD A,(HL) ; Fetch saved byte
AND 0FH ; Mask low nibble (high 4 bits of code)
EX DE,HL ; Get new byte in L (low 8 bits of code)
LD H,A ; Form 12-bit code in HL
RET ; Return (carry clear from mask)
OGETC1: PUSH DE ; Save byte just read (high 8 code bits)
CALL GETCX ; Get next byte
EXX ; Save output regs
POP HL ; Restore previous byte in L
RET C ; But return if eof
LD (BITSAV),A ; Save new byte for next call
AND 0F0H ; Mask high nibble (low 4 bits of code)
RLA ; Rotate once through carry
LD H,A ; Set for circular rotate of HL & carry
REPT 4
ADC_HL HL ;;Form the 12-bit code
ENDM
RET ; Return (carry clear after last rotate)
; Output next byte decoded from crunched file
PUTCR: EXX ; Swap in output registers
JP 0 ; Vector to the appropriate routine
PUTCRP EQU $-2 ; (ptr to PUT or PUTUP stored here)
PAGE
; Low-level output routines
; Register usage (once things get going):
;
; B = Flag for repeated byte expansion (1 = repeat count expected)
; C = Last byte output (saved for repeat expansion)
; DE = Output buffer pointer
; HL = CRC value
; Setup registers for output (preserves AF)
PUTSET: LD HL,(BUFPAG-1) ; Get buffer start address
LD L,0 ; (It's always page aligned)
EX DE,HL
LD H,E ; Clear the CRC
LD L,E
LD B,E ; Clear repeat flag
RET ; Return
; Table of starting output buffer pages
; (No. of entries must match ARCVER)
OBUFT: ; Header version:
DB HIGH BUFF ; 1 - Uncompressed (obsolete)
DB HIGH BUFF ; 2 - Uncompressed
DB HIGH BUFF ; 3 - Packed
DB HIGH BUFFSQ ; 4 - Squeezed
DB HIGH BUFFCX ; 5 - Crunched (unpacked) (old)
DB HIGH BUFFCX ; 6 - Crunched (packed) (old)
DB HIGH BUFFCX ; 7 - Crunched (packed, faster) (old)
DB HIGH BUFFCR ; 8 - Crunched (new)
DB HIGH BUFFCQ ; 9 - Squashed
PAGE
; Unpack and output packed byte
PUTUP: DJNZ PUTUP4 ; Expecting a repeat count?
LD B,A ; Yes ("byte REP count"), save count
OR A ; But is it zero?
JR NZ,PUTUP2 ; No, enter expand loop (did one before)
LD A,REP ; Else ("REP 0"),
JR PUT ; Go output REP code as data
PUTUP1: LD A,C ; Get repeated byte
CALL PUT ; Output it
PUTUP2: DJNZ PUTUP1 ; Loop until repeat count exhausted
RET ; Return when done
PUTUP3: INC B ; Set flag for repeat count next
RET ; Return (must wait for next call)
PUTUP4: INC B ; Normal byte, reset repeat flag
CP REP ; But is it the special flag code (REP)?
JR Z,PUTUP3 ; Yes, go wait for next byte
LD C,A ; Save output byte for later repeat
; Output byte (and update CRC)
PUT: LD (DE),A ; Store byte in buffer
XOR L ; Include byte in lower CRC
LD L,A ; to get lookup table index
LD A,H ; Save high (becomes new low) CRC byte
LD H,HIGH CRCTAB ; Point to table value low byte
XOR (HL) ; Include in CRC
INC H ; Point to table value high byte
LD H,(HL) ; Fetch to get new high CRC byte
LD L,A ; Copy new low CRC byte
INC E ; Now that CRC updated, bump buffer ptr
RET NZ ; Return if not end of page
INC D ; Point to next buffer page
LD A,(BUFLIM) ; Get buffer limit page
CP D ; Buffer full?
RET NZ ; No, return
PAGE
; Output buffer
PUTBUF: PUSH HL ; Save register (i.e. CRC)
LD HL,(BUFPAG-1) ; Get buffer start address
XOR A ; (it's always page-aligned)
LD L,A
EX DE,HL ; Swap with buffer end ptr
SBC_HL DE ; Compute buffer length
JR Z,PUTB2 ; But skip all the work if it's empty
PUSH BC ; Save register (i.e. repeat flag/byte)
LD B,H ; Copy buffer length
LD C,L
LD HL,(LEN) ; Get (remaining) output file length
SBC_HL BC ; Subtract size of buffer
LD (LEN),HL ; (Should be zero when we're all done)
JR NC,PUTB1 ; Skip if double-precision not needed
LD HL,(LEN+2) ; Update upper word of length
DEC HL
LD (LEN+2),HL
PUTB1: PUSH DE ; Save buffer start
CALL WRTBUF ; Write the buffer
POP DE ; Reset output ptr for next refill
POP BC ; Restore register
PUTB2: POP HL ; Restore register
RET ; Return to caller
PAGE
; Write buffer to disk
WRTBUF: LD A,(OFLAG) ; Output file open?
OR A
JR Z,TYPBUF ; No, go typeout buffer instead
LD H,D ; Get buffer end ptr
LD L,E
ADD HL,BC
JR WRTB2 ; Enter loop
WRTB1: LD (HL),CTLZ ; Fill last record with CP/M EOF...
INC HL
INC BC
WRTB2: LD A,L ; Buffer ends on a CP/M record boundary?
AND 7FH
JR NZ,WRTB1 ; No, loop until it does
OR B ; At least one page to write?
JR Z,WRTB4 ; Skip if not
WRTB3: PUSH BC ; Save remaining byte count
CALL WRTREC ; Output 2 records to disk (i.e. 1 page)
CALL WRTREC ; (Note returns A=0 as expected below)
POP BC ; Restore count
DJNZ WRTB3 ; Loop for all (full) pages in buffer
WRTB4: OR C ; Half-page left?
RET Z ; No, return
; Write record to disk
WRTREC: LD HL,128 ; Get CP/M record length
ADD HL,DE ; Add buffer ptr
PUSH HL ; Save next record start
CALL SETDMA ; Set to write from buffer ptr
LD C,$WRITE ; Write a record to output file
CALL OFDOS
POP DE ; Restore ptr for next call
DEC A ; Write error?
RET Z ; No, return
LD DE,DSKFUL ; Disk is full, report error
JP PABORT ; and abort
PAGE
; Typeout buffer
TYPBUF: LD A,(CHECKF) ; Just checking file?
OR A
RET NZ ; Yes, ignore buffer
LD A,(PROUTF) ; Printer output enabled?
OR A
JR NZ,PRTBUF ; Yes, go print buffer instead
; Note: The file typeout facility was originally added to this program
; as an afterthought. The primitive nature of this facility has
; been enhanced considerably with the addition of screen pauses in
; UNARC 1.4. Areas for future improvement include intelligent
; handling of screen width and terminal characteristics.
TYPB0: LD A,(DE) ; Fetch next byte from buffer
CP CTLZ ; Is it CP/M end-of-file?
JP Z,EXIT ; Yes, exit program early
PUSH BC ; Save remaining byte count
INC A ; Bump ASCII code (simplifies DEL test)
AND 7FH ; Mask to 7 bits
CP ' '+1 ; Is it a printable char?
DEC A ; (Restore code)
JR C,TYPB3 ; Skip if non-printable
TYPB1: CALL PCHAR ; Type char
TYPB2: INC DE ; Bump ptr to next byte
POP BC ; Restore byte count
DEC BC ; Reduce count
LD A,B ; Done all bytes?
OR C
JR NZ,TYPB0 ; No, loop for next
RET ; Yes, return to caller
TYPB3: CP HT ; Is (non-printing) char a tab?
JR Z,TYPB1 ; Yes, go type it
JR C,TYPB2 ; But ignore if low control char
CP CR ; Does char generate a new line?
JR NC,TYPB2 ; No, ignore control char (incl. CR)
CALL CRLF ; Yes (LF/VT/FF), start a new line
PUSH DE ; Save buffer ptr
CALL CABORT ; Good place to check for CTRL-C abort
POP DE ; Restore ptr
LD HL,LINCT ; Point to line count
INC (HL) ; Bump for one more line
JR Z,TYPB2 ; But skip if 256 (must be no limit)
LD A,(TYLIM) ; Get max allowed lines
CP (HL) ; Reached limit (e.g. for RCP/M)?
JR NZ,TYPB2 ; No, go back to typeout loop
CALL WHLCK ; But is wheel byte set?
JR Z,TYPB2 ; Yes, do not enforce limit
LD DE,TYPERR ; Else, report too many lines
JP PABORT ; and abort
PAGE
; Print buffer
; This added in UNARC 1.41 as a quick hack to allow printing of
; highly-compressed binary plot images. It may not be suitable for
; general text file listing. (In particular, CTRL-Z is not treated
; as a file terminator.)
PRTBUF: EX DE,HL ; Buffer ptr -> HL
PRTB1: LD E,(HL) ; Fetch next byte from buffer
PUSH HL ; Save buffer ptr
PUSH BC ; Save remaining byte count
LD C,$LIST ; Print byte (on listing device)
CALL BDOS
CALL CABORT ; Check for CTRL-C abort
POP BC ; Restore byte count
POP HL ; Restore ptr
INC HL ; Bump to next byte in buffer
DEC BC ; Reduce count
LD A,B ; Done all bytes?
OR C
JR NZ,PRTB1 ; No, loop for next
RET ; Yes, return to caller
PAGE
SUBTTL Listing Routines
; List file information
LIST: LD HL,(TFILES) ; Get total files so far
LD A,H ; Test if this is first file
OR L
INC HL ; Add one more
LD (TFILES),HL ; Update total files
CALL Z,LTITLE ; If first file, list column titles
LD DE,SIZE ; Point to compressed file size
PUSH DE ; Save for later
LD HL,TSIZE ; Update total compressed size
CALL LADD
LD DE,LEN ; Point to uncompressed length
PUSH DE ; Save for later
LD HL,TLEN ; Update total length
CALL LADD
LD HL,LINE ; Setup listing line pointer
LD DE,OFCB+@FN ; List file name from output FCB
LD C,0 ; (with blank fill)
CALL LNAME
POP DE ; Recover file length ptr
PUSH DE ; Save again for factor calculation
CALL LTODA ; List file length
CALL LDISK ; Compute and list disk space
CALL LSTOW ; List stowage method and version
POP BC ; Restore uncompressed length ptr
POP DE ; Restore compressed size ptr
CALL LSIZE ; List size and compression factor
LD A,(DATE) ; Check for valid file date
OR A ; (This anticipates no-date CP/M files)
JR NZ,LIST1 ; Skip if valid
LD B,18 ; Else, clear out date and time fields
CALL FILLB
JR LIST2 ; Skip
LIST1: CALL LDATE ; List file date
CALL LTIME ; List file time
LIST2: CALL LCRC ; List CRC value
PAGE
; Terminate and print listing line
LISTL: LD DE,LINE ; Setup listing line ptr
JR LIST3 ; Go finish up and list it
; List file totals
LISTT: LD HL,LINE ; Setup listing line ptr
LD_DE (TFILES) ; List total files
CALL WTODA
LD DE,TLEN ; List total file length
PUSH DE ; and save ptr for factor calculation
CALL LTODA
LD_DE (TDISK) ; List total disk space
CALL LDISK1
LD B,13 ; Fill next columns with blanks
CALL FILLB
POP BC ; Recover total uncompressed length ptr
LD DE,TSIZE ; Get total compressed size ptr
CALL LSIZE ; List overall size, compression factor
LD B,20 ; Fill next columns with blanks
CALL FILLB
LD_DE (TCRC) ; List sum of all CRC values
CALL WHEX
LD DE,TOTALS ; Point to totals string (precedes line)
LIST3: LD (HL),0 ; Terminate listing line
JR PRINTL ; Go print it, followed by new line
; Print character
PCHAR: CP BEL ; Is it a noisy one?
JR NZ,PCHAR1 ; No, skip
LD HL,BELLS ; Yes, is silence desired?
AND (HL)
RET Z ; Yes, keep quiet
PCHAR1: PUSH DE ; Save register
PCHAR2: LD E,A ; Setup char
DEC A ; But is it special program name marker?
JR Z,PNAME ; Yes, go insert name
LD C,$CONOUT ; Send to BDOS console output
CALL BDOS
POP DE ; Restore register
RET ; Return
; Print program name string, followed by blank
PNAME: LD DE,USAGE ; Point to name string in help message
PNAME1: LD A,(DE) ; Reached trailing blank?
CP ' '
JR Z,PCHAR2 ; Yes, back to PCHAR to print it
CALL PCHAR ; Print name char
INC DE ; Point to next
JR PNAME1 ; Loop until blank delimiter
; Print string on new line, then start another
PRINTX: CALL CRLF
; Print string, then start new line
PRINTL: CALL PRINTS
; Start new line
; Note: Must preserve DE
CRLF: LD A,CR
CALL PCHAR
LD A,LF
CALL PCHAR
LD HL,LPSCT ; Reached end of screen?
DEC (HL)
RET NZ ; No, return
LD A,0 ; But are screen pauses enabled?
LPS EQU $-1 ; (lines per screen = 0 if not)
OR A
RET Z ; No, return
LD (HL),A ; Reset count of lines left
PUSH DE ; Save register
LD DE,MORE ; Print '[more]' on the new line
CALL PRINTS
CRLF1: CALL CABORT ; Wait for char (or ^C abort)
JR Z,CRLF1
PUSH AF ; Save input response
LD DE,NOMORE ; Blank out the '[more]' line
CALL PRINTS
POP AF ; Restore response
POP DE ; Restore register
XOR ' ' ; Was response the space bar?
RET NZ ; Anything else scrolls another screen
INC A ; Yes, set to pause after one more line
LD (LPSCT),A
RET ; Return
PAGE
; Print string on new line
; Note: Restricted to at most 5 stack levels (c.f. CHECK). CRLF will
; not perform page pause during this restriction, but PCHAR will
; execute PNAME (during ABOMSG print), so we're now at the limit!
PRINT: CALL CRLF
; Print NUL-terminated string
PRINTS: LD A,(DE)
OR A
RET Z
CALL P,PCHAR ; (Ignore help msg chars with MSB set)
INC DE
JR PRINTS
; Output warning message about extracted file
OWARN: PUSH DE
LD DE,WARN
CALL PRINTS
POP DE
JR PRINTL
PAGE
; List column titles
; Note: This saves some much-needed space, by using the same template
; to generate the title line and the 'equal signs' separator line.
LTITLE: CALL CRLF
LD DE,TITLES
PUSH DE
LD A,(DE)
LTITL1: CP '=' ; For titles, convert '=' to blank
JR NZ,LTITL2
LD A,' '
LTITL2: CALL PCHAR
INC DE
LD A,(DE)
OR A
JR NZ,LTITL1
POP DE
CALL CRLF
LTITL3: LD A,(DE)
OR A
JR Z,CRLF
CP ' ' ; Separator converts non-blank to '='
JR Z,LTITL4
LD A,'='
LTITL4: CALL PCHAR
INC DE
JR LTITL3
PAGE
; List file name
; Note: We use name in output file FCB, rather than original name in
; archive header (illegal chars already filtered by GETNAM).
; This routine also called by INIT to unparse ARC file name.
LNAME: LD B,12 ; Setup count for name, '.', and type
LNAME1: LD A,B ; Get count
CP 4 ; At end of name?
LD A,'.'
JR Z,LNAME2 ; Yes, go store separator
LD A,(DE) ; Get next char
INC DE
CP C ; Ignore blanks (possibly)
JR Z,LNAME3
LNAME2: LD (HL),A ; Store char
INC HL
LNAME3: DJNZ LNAME1 ; Loop for all chars in name and type
RET ; Return to caller
PAGE
; Compute and list disk space for uncompressed file
LDISK: PUSH HL ; Save line ptr
LD HL,(LEN) ; Convert file length to 1k disk space
LD A,(LEN+2) ; (Most we can handle here is 16 Mb)
LD DE,1023 ; First, round up to next 1k
ADD HL,DE
ADC A,0
RRA ; Now, shift to divide by 1k
RR H
RRA
RR H
AND 3FH
LD L,H ; Result -> HL
LD H,A
LD A,(LBLKSZ) ; Get disk block size
DEC A ; Round up result accordingly
LD E,A
LD D,0
ADD HL,DE
CPL ; Form mask for lower bits
AND L
LD E,A ; Final result -> DE
LD D,H
LD HL,(TDISK) ; Update total disk space used
ADD HL,DE
LD (TDISK),HL
POP HL ; Restore line ptr
LDISK1: CALL WTODA ; List result
LD (HL),'k'
INC HL
RET
PAGE
; List stowage method and version
LSTOW: CALL FILL2B ; Blanks first
EX DE,HL
LD HL,STOWTX ; Point to stowage text table
LD A,(VER) ; Get header version no.
PUSH AF ; Save for next column
LD BC,8 ; Use to get correct text ptr
CP 3
JR C,LSTOW1
ADD HL,BC
JR Z,LSTOW1
ADD HL,BC
CP 4
JR Z,LSTOW1
ADD HL,BC
CP 9
JR C,LSTOW1
ADD HL,BC
JR Z,LSTOW1
ADD HL,BC
LSTOW1: LDIR ; List stowage text
EX DE,HL ; Restore line ptr
POP AF ; Recover version no.
LSTOW2: LD B,3 ; List in 3 cols, blank-filled
JP BTODB ; and return
PAGE
; List compressed file size and compression factor
LSIZE: PUSH DE ; Save compressed size ptr
PUSH BC ; Save uncompressed length ptr
CALL LTODA ; List compressed size
POP DE ; Recover length ptr
EX (SP),HL ; Save line ptr, recover size ptr
; Compute compression factor = 100 - [100*size/length]
; (HL = ptr to size, DE = ptr to length, A = result)
PUSH DE ; Save length ptr
CALL LGET ; Get BCDE = size
LD H,B ; Compute 100*size
LD L,C ; in HLIX:
PUSH DE
POP_IX ; size
ADD_IX IX
ADC_HL HL ; 2*size
ADD_IX DE
ADC_HL BC ; 3*size
ADD_IX IX
ADC_HL HL ; 6*size
ADD_IX IX
ADC_HL HL ; 12*size
ADD_IX IX
ADC_HL HL ; 24*size
ADD_IX DE
ADC_HL BC ; 25*size
ADD_IX IX
ADC_HL HL ; 50*size
ADD_IX IX
ADC_HL HL ; 100*size
EX (SP),HL ; Swap back length ptr, save upper
CALL LGET ; Get BCDE = length
PUSH_IX
POP HL ; Now have (SP),HL = 100*size
LD A,B ; Length = 0?
OR C ; (Unlikely, but possible)
OR D
OR E
JR Z,LSIZE2 ; Yes, go return result = 0
LD A,101 ; Initialize down counter for result
LSIZE1: DEC A ; Divide by successive subtractions
SBC_HL DE
EX (SP),HL
SBC_HL BC
EX (SP),HL
JR NC,LSIZE1 ; Loop until remainder < length
LSIZE2: POP HL ; Clean stack
POP HL ; Restore line ptr
CALL BTODA ; List the factor
LD (HL),'%'
INC HL
RET ; Return
PAGE
; List file creation date
; ARC files use MS-DOS 16-bit date format:
;
; Bits [15:9] = year - 1980
; Bits [8:5] = month of year
; Bits [4:0] = day of month
;
; (All zero means no date, checked before call to this routine)
LDATE: LD A,(DATE) ; Get date
AND 1FH ; List day
CALL BTODA
LD (HL),' ' ; Then a blank
INC HL
EX DE,HL ; Save listing line ptr
LD HL,(DATE) ; Get date again
PUSH HL ; Save for listing year (in upper byte)
ADD HL,HL ; Shift month into upper byte
ADD HL,HL
ADD HL,HL
LD A,H ; Get month
AND 0FH
CP 13 ; Make sure it's valid
JR C,LDATE1
XOR A ; (Else will show as "???")
LDATE1: LD C,A ; Use to index to 3-byte string table
LD B,0
LD HL,MONTX
ADD HL,BC
ADD HL,BC
ADD HL,BC
LD C,3
LDIR ; Move month text into listing line
EX DE,HL ; Restore line ptr
LD (HL),' ' ; Then a blank
INC HL
POP AF ; Recover high byte of date
SRL A ; Get 1980-relative year
ADD A,80 ; Get true year in century
LDATE2: LD BC,256*2+'0' ; Setup for 2 digits with high-zero fill
JR BTOD ; and convert binary to decimal ASCII
PAGE
; List file creation time
; ARC files use MS-DOS 16-bit time format:
;
; Bits [15:11] = hour
; Bits [10:5] = minute
; Bits [4:0] = second/2 (not shown here)
LTIME: EX DE,HL ; Save listing line ptr
LD HL,(TIME) ; Fetch time
LD A,H ; Copy high byte
RRA ; Get hour
RRA
RRA
AND 1FH
LD B,'a' ; Assume am
JR Z,LTIME1 ; Skip if 0 (12 midnight)
CP 12 ; Is it 1-11 am?
JR C,LTIME2 ; Yes, skip
LD B,'p' ; Else, it's pm
SUB 12 ; Convert to 12-hour clock
JR NZ,LTIME2 ; Skip if not 12 noon
LTIME1: LD A,12 ; Convert 0 to 12
LTIME2: PUSH BC ; Save am/pm indicator
ADD HL,HL ; Shift minutes up to high byte
ADD HL,HL
ADD HL,HL
PUSH HL ; Save minutes
EX DE,HL ; Recover listing line ptr
CALL LSTOW2 ; List hour
LD (HL),':' ; Then ":"
INC HL
POP AF ; Restore and list minutes
AND 3FH
CALL LDATE2
POP AF ; Restore and list am/pm letter
LD (HL),A
INC HL
RET ; Return
PAGE
; List hex CRC value
LCRC: CALL FILL2B
LD_DE (CRC)
PUSH HL
LD HL,(TCRC) ; Update CRC total
ADD HL,DE
LD (TCRC),HL
POP HL
; List hex word in DE
WHEX: CALL DHEX
LD D,E
; List hex byte in D
DHEX: LD (HL),D
RLD
CALL AHEX
LD A,D
; List hex nibble in A
AHEX: OR 0F0H
DAA
CP 60H
SBC A,1FH
LD (HL),A
INC HL
RET
; A few decimal ASCII conversion callers, for convenience
WTODA: LD B,5 ; List blank-filled word in 5 cols
WTODB: LD C,' ' ; List blank-filled word in B cols
JR WTOD ; List C-filled word in B cols
BTODA: LD B,4 ; List blank-filled byte in 4 cols
BTODB: LD C,' ' ; List blank-filled byte in B cols
JR BTOD ; List C-filled byte in B cols
LTODA: LD BC,9*256+' ' ; List blank-filled long in 9 cols
; JR LTOD
PAGE
; Convert Long (or Word or Byte) Binary to Decimal ASCII
; R. A. Freed
; 2.0 15 Mar 85
; Entry: A = Unsigned 8-bit byte value (BTOD)
; DE = Unsigned 16-bit word value (WTOD)
; DE = Pointer to low byte of 32-bit long value (LTOD)
; B = Max. string length (0 implies 256, i.e. no limit)
; C = High-zero fill (0 to suppress high-zero digits)
; HL = Address to store ASCII byte string
;
; Return: HL = Adress of next byte after last stored
;
; Stack: n+1 levels, where n = no. significant digits in output
;
; Notes: If B > n, (B-n) leading fill chars (C non-zero) stored.
; If B < n, high-order (n-B) digits are suppressed.
; If only word or byte values need be converted, use the
; shorter version of this routine (WTOD or BTOD) instead.
RADIX EQU 10 ; (Will work with any radix <= 10)
LTOD: PUSH DE ; Entry for 32-bit long pointed to by DE
EXX ; Save caller's regs, swap in alt set
POP HL ; Get pointer and fetch value to HADE
LD E,(HL)
INC HL
LD D,(HL)
INC HL
LD A,(HL)
INC HL
LD H,(HL)
EX DE,HL ; Value now in DAHL
JR LTOD1 ; Join common code
BTOD: LD E,A ; Entry for 8-bit byte in A
LD D,0 ; Copy to 16-bit word in DE
WTOD: PUSH DE ; Entry for 16-bit word in DE, save it
EXX ; Swap in alt regs for local use
POP HL ; Recover value in HL
XOR A ; Set to clear upper bits in DE
LD D,A
; Common code for all entries
LTOD1: LD E,A ; Now have 32-bit value in DEHL
LD C,RADIX ; Setup radix for divides
SCF ; Set first-time flag
PUSH AF ; Save for stack emptier when done
PAGE
; Top of conversion loop
; Method: Generate output digits on stack in reverse order. Each loop
; divides the value by the radix. Remainder is the next output digit,
; quotient becomes the dividend for the next loop. Stop when get zero
; quotient or no. of digits = max. string length. (Always generates at
; least one digit, i.e. zero value has one "significant" digit.)
LTOD2: CALL DIVLB ; Divide to get next digit
OR '0' ; Convert to ASCII (clears carry)
EXX ; Swap in caller's regs
DJNZ LTOD5 ; Skip if still more room in string
; All done (value fills string), this is the output loop
LTOD3: LD (HL),A ; Store digit in string
INC HL ; Bump string ptr
LTOD4: POP AF ; Unstack next digit
JR NC,LTOD3 ; Loop if any
RET ; Return to caller
; Still more room in string, test if more significant digits
LTOD5: PUSH AF ; Stack this digit
EXX ; Swap back local regs
LD A,H ; Last quotient = 0?
OR L
OR D
OR E
JR NZ,LTOD2 ; No, loop for next digit
; Can stop early (no more digits), handle leading zero-fill (if any)
EXX ; Swap back caller's regs
OR C ; Any leading fill wanted?
JR Z,LTOD4 ; No, go to output loop
LTOD6: LD (HL),A ; Store leading fill
INC HL ; Bump string ptr
DJNZ LTOD6 ; Repeat until fill finished
JR LTOD4 ; Then go store the digits
PAGE
SUBTTL Miscellaneous Support Routines
; Note: The following general-purpose routine is currently used in this
; program only to divide longs by 10 (by decimal convertor, LTOD).
; Thus, a few unneeded code locations have been commented out.
; (May be restored if program requirements change.)
; Unsigned Integer Division of Long (or Word or Byte) by Byte
; R. A. Freed
; Divisor in C, dividend in (A)DEHL or (A)HL or L (depends on call used)
; Quotient returned in DEHL (or just HL), remainder in A
;DIVXLB:OR A ; 40-bit dividend in ADEHL (A < C)
; JR NZ,DIVLB1 ; Skip if have more than 32 bits
DIVLB: LD A,D ; 32-bit dividend in DEHL
OR E ; But is it really only 16 bits?
JR Z,DIVWB ; Yes, skip (speeds things up a lot)
XOR A ; Clear high quotient for first divide
DIVLB1: CALL DIVLB2 ; Get upper quotient first, then swap:
DIVLB2: EX DE,HL ; Upper quotient in DE, lower in HL
DIVXWB: OR A ; 24-bit dividend in AHL (A < C)
JR NZ,DIVWB1 ; Skip if have more than 16 bits
DIVWB: LD A,H ; 16-bit dividend in HL
CP C ; Will quotient be less than 8 bits?
JR C,DIVBB1 ; Yes, skip (small dividend speed-up)
XOR A ; Clear high quotient
DIVWB1: LD B,16 ; Setup count for 16-bit divide
JR DIVB ; Skip to divide loop
;DIVBB: XOR A ; 8-bit dividend in L
DIVBB1: LD H,L ; For very small nos., pre-shift 8 bits
LD L,0 ; High byte of quotient will be zero
LD B,8 ; Setup count for 8-bit divide
; Top of divide loop (vanilla in-place shift-and-subtract)
DIVB: ADD HL,HL ; Divide AHL (B=16) or AH (B=8) by C
RLA ; Shift out next remainder bit
; JR C,DIVB1 ; (This needed only for divsors > 128)
CP C ; Greater than divisor?
JR C,DIVB2 ; No, skip (next quotient bit is 0)
DIVB1: SUB C ; Yes, reduce remainder
INC L ; and set quotient bit to 1
DIVB2: DJNZ DIVB ; Loop for no. bits in quotient
RET ; Done (quotient in HL, remainder in A)
PAGE
; Fetch a long (4-byte) value
LGET: LD E,(HL) ; Fetch BCDE from (HL)
INC HL
LD D,(HL)
INC HL
LD C,(HL)
INC HL
LD B,(HL)
RET
; Add two longs
LADD: LD B,4 ; (DE) + (HL) -> (HL)
OR A
LADD1: LD A,(DE)
ADC A,(HL)
LD (HL),A
INC HL
INC DE
DJNZ LADD1
RET
; Fill routines
FILL2B: LD B,2 ; Fill 2 blanks
FILLB: LD C,' ' ; Fill B blanks
FILL: LD (HL),C ; Fill B bytes with char in C
INC HL
DJNZ FILL
RET
; Convert character to upper case
UPCASE: CP 'a'
RET C
CP 'z'+1
RET NC
ADD A,'A'-'a'
RET
PAGE
IF NOT Z80
; EXX instruction emulator
EXX:
IRP AA,<HL,DE,BC>
PUSH AA
LD HL,(AA&SAV)
EX (SP),HL
LD (AA&SAV),HL
ENDM
POP BC
POP DE
POP HL
RET
; LDIR instruction emulator
LDIR: PUSH AF
LDIR1: LD A,(HL)
LD (DE),A
INC HL
INC DE
DEC BC
LD A,B
OR C
JP NZ,LDIR1
POP AF
RET
; CPIR instruction emulator
CPIR1: POP AF
CPIR: CP (HL)
INC HL
DEC BC
RET Z
PUSH AF
LD A,B
OR C
JP NZ,CPIR1
POP AF
RET
ENDIF
PAGE
SUBTTL Messages and Initialized Data
IF Z80
NOTZ80: DB BEL,'Z80 required!$'
ELSE
USEZ80: DB 'NOTE: The Z80 version is smaller and faster!',CR,LF,'$'
ENDIF
ABOMSG: DB BEL,1,'aborted!',0
CPMERR: DB 'CP/M version 2 or higher required',0
NOROOM: DB 'Not enough memory',0
NAMERR: DB 'Ambiguous archive file name',0
OPNERR: DB 'Cannot find archive file',0
FMTERR: DB 'Invalid archive file format',0
HDRERR: DB BEL,'Warning: Bad archive file header, bytes skipped = '
HDRSKP: DB '00000',0
NOFILS: DB 'No matching file(s) in archive',0
BADIDR: DB 'Invalid archive file drive',0
BADODR: DB 'Invalid output drive',0
ARCMSG: DB 'Archive File = '
ARCNAM: DB 'FILENAME.ARC',0
OUTMSG: DB 'Output Drive = '
OUTDRV: DB 'A:',0
CHKMSG: DB 'Checking archive...',0
BADVER: DB 'Cannot extract file (need newer version of UNARC?)',0
EXISTS: DB BEL,'Replace existing output file (y/n)? ',0
DSKFUL: DB 'Disk full',0
DIRFUL: DB 'Directory full',0
CLSERR: DB 'Cannot close output file',0
UCRERR: DB 'Incompatible crunched file format',0
TYPERR: DB 'Typeout line limit exceeded',0
WARN: DB BEL,'Warning: Extracted file has incorrect ',0
CRCERR: DB 'CRC',0
LENERR: DB 'length',0
MORE: DB '[more]',0
NOMORE: DB CR,' ',HT,CR,0
; Note: Tab (HT) added above in UNARC 1.5 for proper following tab
; expansion (since CP/M 2.2 BDOS does not reset its column
; position after raw CR output). The blanks are still generated
; in case of BDOS implementations which do not expand tabs.
MONTX: DB '???JanFebMarAprMayJunJulAugSepOctNovDec'
STOWTX: DB 'Unpacked'
DB ' Packed '
DB 'Squeezed'
DB 'Crunched'
DB 'Squashed'
DB 'Unknown!'
TITLES: DB 'Name======== =Length Disk =Method= Ver =Stored Save'
DB 'd ===Date== =Time= CRC='
LINLEN EQU $-TITLES
DB 0
TOTALS: DB ' ==== ======= ==== ======= ==='
DB ' ===='
DB CR,LF
DB 'Total ' ; (LINE must follow!)
; .COM file ends here (except for non-Z80 self-unpacking startup code)
COMLEN EQU $-TBASE ; Length of initialized code and data
PAGE
SUBTTL Data Storage
; Unitialized data last (does not contribute to .COM file size)
; Note: Following macro introduced in UNARC 1.5 to avoid use of the
; assembler DS directive, which generates unneeded records in the
; .COM file when linked with L80 (unlike SLRNK). (Also preserves
; location counter for self-unpacking initialization code in the
; non-Z80 version.)
DSS MACRO SYM,BYTES
SYM EQU $D
$D DEFL $D+(BYTES)
ENDM
$D DEFL $ ; Start of data storage (pseudo PC)
DSS LINE,LINLEN+1 ; Listing line buffer (follow TOTALS!)
$D DEFL $D+(25*2) ; Program stack (25 levels)
STACK EQU $D ; (Too small will only garbage listing)
TOTS EQU $D ; Start of listing totals
DSS TFILES,2 ; Total files processed
DSS TLEN,4 ; Total uncompressed bytes
DSS TDISK,2 ; Total 1K disk blocks
DSS TSIZE,4 ; Total compressed bytes
DSS TCRC,2 ; Total of all CRC values
DSS LINCT,1 ; Line count for file typeout
DSS ARKFLG,1 ; Default file type flag (allows .ARC)
DSS PROUTF,1 ; Printer output flag
DSS CHECKF,1 ; Check archive validity flag
TOTC EQU $D-TOTS ; Count of bytes to clear
DSS GETPTR,2 ; Input buffer pointer
DSS LPSCT,1 ; Lines per screen counter
DSS LBLKSZ,1 ; Disk allocation block size for listing
DSS TNAME,11 ; Test pattern for selecting file names
DSS OFCB,@FCBSZ ; Output file FCB
; DSS IFCB,@FCBSX ; Input file FCB
IFCB EQU DFCB ; (Currently using default FCB instead)
HDRBUF EQU $D ; Archive file header buffer...
DSS VER,1 ; Header version no. (stowage type)
DSS NAME,13 ; Name string (NUL-terminated)
DSS SIZE,4 ; Compressed bytes
DSS DATE,2 ; Creation date
DSS TIME,2 ; Creation time
DSS CRC,2 ; Cyclic check of uncompressed file
DSS LEN,4 ; Uncompressed bytes (version > 1)
HDRSIZ EQU $D-HDRBUF ; Header size (4 less if version = 1)
IF NOT Z80
; Data for Z80 instruction emulation
DSS HLSAV,2 ; HL'
DSS DESAV,2 ; DE'
DSS BCSAV,2 ; BC'
DSS AFSAV,2 ; AF'
DSS IXSAV,2 ; IX
ENDIF
MINMEM EQU $D-1 ; Min memory limit (no file output)
PAGE
; Data for file output processing only
; Following order required:
DSS BUFPAG,1 ; Output buffer start page
DSS BUFLIM,1 ; Output buffer limit page
; Following order required:
DSS CODES,1 ; Code count for crunched input
DSS BITSAV,1 ; Bits save for crunched input
DSS BITS,1 ; Bit count for crunched input
DSS STRCT,2 ; No. entries in crunched string table
; Tables and buffers for file output
; (All of the following must be page-aligned)
$D DEFL ($D+255) AND 0FF00H ; Align to page boundary
DSS CRCTAB,256*2 ; CRC lookup table (256 2-byte values)
BUFF EQU $D ; Output buff for non-squeezed/crunched
; or:
TREE EQU $D ; Decoding tree for squeezed files
TREESZ EQU 256*4 ; (256 4-byte nodes)
BUFFSQ EQU TREE+TREESZ ; Output buffer for squeezed files
; or:
STRT EQU $D ; String table for crunched files
STRSZ EQU 4096*3 ; (4K 3-byte entries)
BUFFCR EQU STRT+STRSZ ; Output buffer for newer crunched files
; plus (for old-style crunched files):
HSHT EQU BUFFCR ; Extra table for hash code chaining
HSHSZ EQU 4096*2 ; (4K 2-byte entries)
BUFFCX EQU HSHT+HSHSZ ; Output buffer for older crunched files
; or (for squashed files):
STQSZ EQU 8192*3 ; (8K 3-byte string table entries)
BUFFCQ EQU STRT+STQSZ ; Output buffer for squashed files
PAGE
IF NOT Z80
; Initialization for self-unpacking archive file (non-Z80 version only)
; Note: Following is needed only when UNARCA.COM is executed from a
; self-unpacking archive file. It is subsequently overlayed by
; data during program execution, so the only additional run-time
; overhead for self-unpacking support is the 26 bytes immediately
; preceding BEGIN. (The added disk space for this code is also
; minimal, and none of this is included in the Z80-only version,
; UNARC.COM, which applies to the majority of users.)
.PHASE $+26 ; This code is offset 26 bytes in memory
SELFUP: LD C,$DISK ; Get current default disk drive no.
CALL BDOS ; (archive file drive)
LD B,A ; Save default for extracted files
ADD A,'A' ; Get ASCII drive letter
LD (SELFMD),A ; Store in archive file name message
LD DE,DFCB ; Point to default FCB
LD A,(DE) ; Disk drive specified on command line?
OR A
JP NZ,SELFU1 ; Yes, skip to use it
LD A,B ; Recover default disk no.
INC A ; Convert to drive code
SELFU1: LD (SELFXD),A ; Store drive code for extracted files
ADD A,'A'-1 ; Get ASCII drive letter
LD (SELFCD),A ; Store in command line
LD HL,SELFCB ; Point to fixed internal FCB
LD BC,SELFSZ ; Get no. bytes to move to system page
CALL SELFMV ; Move down fixed command parameters
LD DE,TBASE ; Setup normal .COM file base
LD HL,TBASE+26 ; Setup current (offset) base in memory
LD BC,COMLEN ; Setup .COM file length
CALL SELFMV ; Relocate .COM file to its proper place
LD (CCPSV),A ; Force reboot later (and max. buffer)
INC A ; Set default disk block size to 1K
LD (DBLSZ),A ; (e.g., might be running CP/M-68K)
LD A,'$' ; Patch usage message
LD (USEA),A ; for program identification
LD (USEB),A ; and copyright displays only
LD DE,SELFCR ; Start with a blank display line
CALL SELFPR
LD DE,USAGE ; Show program id
CALL SELFPR
LD DE,USEC ; Show copyright
CALL SELFPR
LD DE,SELFMS ; Show archive file name (new user aid)
CALL SELFPR
JP BEGIN1 ; Go begin (skip Z80 warning note)
; Brute force memory mover (can't use LDIR emulation yet)
SELFMV: LD A,(HL)
LD (DE),A
INC HL
INC DE
DEC BC
LD A,B
OR C
JP NZ,SELFMV
RET ; Return with A = 0
; Print message via BDOS (can't use internal print routines yet)
SELFPR: LD C,$PRTSTR
JP BDOS
; Fixed FCB's and command line for self-unpacking file extraction
SELFCB: DB 0 ; Archive file drive (default always)
SELF ; Archive file name
REPT SELFCB+9-$ ; (pad with blanks to 8 chars)
DB ' '
ENDM
DB 'COM' ; Archive file type (always .COM)
DB 0,0,0,0 ; Extent descriptor bytes
SELFXD: DB 0 ; Drive code for file extraction
DB ' ' ; Files to extract (defaults to *.*)
DB 0,0,0,0 ; Extent descriptor bytes
DB 0,0,0,0 ; Current and random record nos.
DB SELFCE-SELFCL ; Command line length (moves to DBUF)
SELFCL: DB ' ' ; Command line tail...
SELF
DB '.COM ' ; (e.g. ' UNARC15.COM A: N')
SELFCD: DB 'A: N' ; (extract all files, no screen pauses)
SELFCE: DB 0 ; (end of command line)
SELFSZ EQU $-SELFCB ; Size of fixed command data to move
; Message naming self-unpacking archive file
SELFMS: DB CR,LF,LF,'(Self-unpacking file '
SELFMD: DB 'A:'
SELF
DB '.COM)'
SELFCR: DB CR,LF,'$'
.DEPHASE
; End of special self-unpacking code for non-Z80 version
ENDIF
; That's all, folks!
IF ($ AND 7FH) NE 0
; Clear out final record of the .COM file
; (Needed only for precise M80/L80 compatibility with Z80ASM/SLRNK)
REPT 128-($ AND 7FH)
DB 0
ENDM
ENDIF
END BEGIN
type (always .COM)
DB 0,0,0,0 ; Exte