[net.sources.mac] new version of xbin.shar

ddj@brunix.UUCP (Dave Johnson) (03/25/85)

#!/bin/sh-----cut here-----cut here-----cut here-----cut here-----
# shar:	Shell Archiver
#	Run the following text with /bin/sh to create:
#	README #	xbin.c #	xbin.l
echo shar: extracting README
cat - << \SHAR_EOF > README
This version of "xbin" can handle all three BinHex formats
(so far).  Thanks to Darin Adler at TMQ Software for providing
the code to compute and check the CRC values for all three formats.

Other new features include "list" and "verbose" modes, the
ability to convert several binhex files at one time, the ability
to read standard input, somewhat better error handling, and a
manual page.

Any extraneous mail or news headers are ignored, but xbin relies
on finding a line which starts with "(This file" to know when
the header ends and the good stuff begins.  You can add one
of these by hand if it's been lost.

To compile it on USG systems, type:
	cc -o xbin xbin.c

or on Berkeley systems:
	cc -o xbin xbin.c -DBSD

As usual, please report any problems, suggestions, or
improvements to me.

	Dave Johnson
	Brown University Computer Science
	ddj%brown@csnet-relay.ARPA
	{ihnp4,decvax,allegra,ulysses,linus}!brunix!ddj
SHAR_EOF
echo shar: extracting xbin.c
cat - << \SHAR_EOF > xbin.c
#ifndef lint
static char version[] = "xbin.c Version 2.0 03/24/85";
#endif lint

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/dir.h>

#ifdef MAXNAMLEN	/* 4.2 BSD */
#define FNAMELEN MAXNAMLEN
#else
#define FNAMELEN DIRSIZ
#endif

#ifdef BSD
#include <sys/timeb.h>
#define search_last rindex
extern char *rindex();
#else
extern long timezone;
#define search_last strrch
extern char *strrch();
#endif

/* Mac time of 00:00:00 GMT, Jan 1, 1970 */
#define TIMEDIFF 0x7c25b080

#define DATABYTES 128

#define BYTEMASK 0xff
#define BYTEBIT 0x100
#define WORDMASK 0xffff
#define WORDBIT 0x10000

#define NAMEBYTES 63
#define H_NLENOFF 1
#define H_NAMEOFF 2

/* 65 <-> 80 is the FInfo structure */
#define H_TYPEOFF 65
#define H_AUTHOFF 69
#define H_FLAGOFF 73

#define H_LOCKOFF 81
#define H_DLENOFF 83
#define H_RLENOFF 87
#define H_CTIMOFF 91
#define H_MTIMOFF 95

#define H_OLD_DLENOFF 81
#define H_OLD_RLENOFF 85

#define F_BUNDLE 0x2000
#define F_LOCKED 0x8000

struct macheader {
	char m_name[NAMEBYTES+1];
	char m_type[4];
	char m_author[4];
	short m_flags;
	long m_datalen;
	long m_rsrclen;
	long m_createtime;
	long m_modifytime;
} mh;

struct filenames {
	char f_info[256];
	char f_data[256];
	char f_rsrc[256];
} files;

FILE *ifp;
int pre_beta;
int compressed;
int qformat;
int listmode;
int verbose;

/*
 * xbin -- unpack BinHex format file into suitable
 * format for downloading with macput
 * Dave Johnson, Brown University Computer Science
 *
 * checksum code by Darin Adler, TMQ Software
 *
 * (c) 1984 Brown University
 * may be used but not sold without permission
 *
 * created ddj 12/16/84
 * revised ddj 03/10/85 -- version 4.0 compatibility, other minor mods
 * revised ddj 03/11/85 -- strip LOCKED bit from m_flags
 * revised ahm 03/12/85 -- System V compatibility
 * revised dba 03/16/85 -- 4.0 EOF fixed, 4.0 checksum added
 * revised ddj 03/17/85 -- extend new features to older formats: -l, stdin
 * revised ddj 03/24/85 -- check for filename truncation, allow multiple files
 */
char usage[] = "usage: \"xbin [-v] [-l] [-o] [-n name] [-] filename\"\n";

main(ac, av)
char **av;
{
	char *filename, *macname;

	filename = ""; macname = "";
	ac--; av++;
	while (ac) {
		if (av[0][0] == '-') {
			switch (av[0][1]) {
			case '\0':
				filename = "-";
				break;
			case 'v':
				verbose++;
				break;
			case 'l':
				listmode++;
				break;
			case 'o':
				pre_beta++;
				break;
			case 'n':
				if (ac > 1) {
					ac--; av++;
					macname = av[0];
					filename = "";
					break;
				}
				else
					goto bad_usage;
			default:
				goto bad_usage;
			}
		}
		else
			filename = av[0];
		if (filename[0] != '\0') {
			setup_files(filename, macname);
			if (listmode) {
				print_header();
			}
			else {
				process_forks();
				/* now that we know the size of the forks */
				forge_info();
			}
			macname = "";
		}
		ac--; av++;
	}
	if (*filename == '\0') {
bad_usage:
		fprintf(stderr, usage);
		exit(1);
	}
}

static char *extensions[] = {
	".hqx",
	".hcx",
	".hex",
	"",
	NULL
};

setup_files(filename, macname)
char *filename;		/* input file name -- extension optional */
char *macname;		/* name to use on the mac side of things */
{
	char namebuf[256], *np;
	char **ep;
	int n;
	struct stat stbuf;
	long curtime;

	if (filename[0] == '-') {
		ifp = stdin;
		filename = "stdin";
	}
	else {
		/* find input file and open it */
		for (ep = extensions; *ep != NULL; ep++) {
			sprintf(namebuf, "%s%s", filename, *ep);
			if (stat(namebuf, &stbuf) == 0)
				break;
		}
		if (*ep == NULL) {
			perror(namebuf);
			exit(-1);
		}
		ifp = fopen(namebuf, "r");
		if (ifp == NULL) {
			perror(namebuf);
			exit(-1);
		}
	}
	if (ifp == stdin) {
		curtime = time(0);
		mh.m_createtime = curtime;
		mh.m_modifytime = curtime;
	}
	else {
		mh.m_createtime = stbuf.st_mtime;
		mh.m_modifytime = stbuf.st_mtime;
	}

	qformat = find_header(); /* eat mailer header &cetera, intuit format */

	if (qformat)
		do_q_header(macname);
	else
		do_o_header(macname, filename);

	/* make sure host file name doesn't get truncated beyond recognition */
	n = strlen(mh.m_name);
	if (n > FNAMELEN - 2)
		n = FNAMELEN - 2;
	strncpy(namebuf, mh.m_name, n);
	namebuf[n] = '\0';

	/* get rid of troublesome characters */
	for (np = namebuf; *np; np++)
		if (*np == ' ' || *np == '/')
			*np = '_';

	sprintf(files.f_data, "%s.data", namebuf);
	sprintf(files.f_rsrc, "%s.rsrc", namebuf);
	sprintf(files.f_info, "%s.info", namebuf);
	if (listmode || verbose) {
		printf("%s %s%s",
			listmode ? "\nListing" : "Converting",
			filename, listmode ? ":\n" : " ");
		if (!listmode)
			printf("==> %s.{info,data,rsrc}\n", namebuf);
	}
}

/* print out header information in human-readable format */
print_header()
{
	char *ctime();

	printf("macname: %s\n", mh.m_name);
	printf("filetype: %.4s, ", mh.m_type);
	printf("author: %.4s, ", mh.m_author);
	printf("flags: 0x%x\n", mh.m_flags);
	if (qformat) {
		printf("data length: %d, ", mh.m_datalen);
		printf("rsrc length: %d\n", mh.m_rsrclen);
	}
	if (!pre_beta) {
		printf("create time: %s", ctime(&mh.m_createtime));
	}
}

process_forks()
{
	if (qformat) {
		/* read data and resource forks of .hqx file */
		do_q_fork(files.f_data, mh.m_datalen);
		do_q_fork(files.f_rsrc, mh.m_rsrclen);
	}
	else
		do_o_forks();
}

/* write out .info file from information in the mh structure */
forge_info()
{
	static char buf[DATABYTES];
	char *np;
	FILE *fp;
	int n, tdiff;
	struct tm *tp;
#ifdef BSD
	struct timeb tbuf;
#else
	long bs;
#endif

	for (np = mh.m_name; *np; np++)
		if (*np == '_') *np = ' ';

	buf[H_NLENOFF] = n = np - mh.m_name;
	strncpy(buf + H_NAMEOFF, mh.m_name, n);
	strncpy(buf + H_TYPEOFF, mh.m_type, 4);
	strncpy(buf + H_AUTHOFF, mh.m_author, 4);
	put2(buf + H_FLAGOFF, mh.m_flags & ~F_LOCKED);
	if (pre_beta) {
		put4(buf + H_OLD_DLENOFF, mh.m_datalen);
		put4(buf + H_OLD_RLENOFF, mh.m_rsrclen);
	}
	else {
		put4(buf + H_DLENOFF, mh.m_datalen);
		put4(buf + H_RLENOFF, mh.m_rsrclen);

		/* convert unix file time to mac time format */
#ifdef BSD
		ftime(&tbuf);
		tp = localtime(&tbuf.time);
		tdiff = TIMEDIFF - tbuf.timezone * 60;
		if (tp->tm_isdst)
			tdiff += 60 * 60;
#else
		/* I hope this is right! -andy */
		time(&bs);
		tp = localtime(&bs);
		tdiff = TIMEDIFF - timezone;
		if (tp->tm_isdst)
			tdiff += 60 * 60;
#endif
		put4(buf + H_CTIMOFF, mh.m_createtime + tdiff);
		put4(buf + H_MTIMOFF, mh.m_modifytime + tdiff);
	}
	fp = fopen(files.f_info, "w");
	if (fp == NULL) {
		perror("info file");
		exit(-1);
	}
	fwrite(buf, 1, DATABYTES, fp);
	fclose(fp);
}

/* eat characters until header detected, return which format */
find_header()
{
	int c, n, at_bol;
	char ibuf[BUFSIZ];

	/* look for "(This file ...)" line */
	while (fgets(ibuf, BUFSIZ, ifp) != NULL) {
		if (strncmp(ibuf, "(This file", 10) == 0)
			break;
	}
	at_bol = 1;
	while ((c = getc(ifp)) != EOF) {
		switch (c) {
		case '\n':
		case '\r':
			at_bol = 1;
			break;
		case ':':
			if (at_bol)	/* q format */
				return 1;
			break;
		case '#':
			if (at_bol) {	/* old format */
				ungetc(c, ifp);
				return 0;
			}
			break;
		default:
			at_bol = 0;
			break;
		}
	}

	fprintf(stderr, "unexpected EOF\n");
	exit(2);
}

static unsigned int crc;

short get2q();
long get4q();

/* read header of .hqx file */
do_q_header(macname)
char *macname;
{
	char namebuf[256];		/* big enough for both att & bsd */
	int n;
	int calc_crc, file_crc;

	crc = 0;			/* compute a crc for the header */

	n = getq();			/* namelength */
	n++;				/* must read trailing null also */
	getqbuf(namebuf, n);		/* read name */
	if (macname[0] == '\0')
		macname = namebuf;

	n = strlen(macname);
	if (n > NAMEBYTES)
		n = NAMEBYTES;
	strncpy(mh.m_name, macname, n);
	mh.m_name[n] = '\0';

	getqbuf(mh.m_type, 4);
	getqbuf(mh.m_author, 4);
	mh.m_flags = get2q();
	mh.m_datalen = get4q();
	mh.m_rsrclen = get4q();

	compute_crcq(0);
	compute_crcq(0);
	calc_crc = crc;
	file_crc = get2q();
	verify_crc(calc_crc, file_crc);
}

do_q_fork(fname, len)
char *fname;
register int len;
{
	FILE *outf;
	register int c, i;
	int calc_crc, file_crc;

	outf = fopen(fname, "w");
	if (outf == NULL) {
		perror(fname);
		exit(-1);
	}

	crc = 0;	/* compute a crc for a fork */

	if (len)
		for (i = 0; i < len; i++) {
			if ((c = getq()) == EOF) {
				fprintf(stderr, "unexpected EOF\n");
				exit(2);
			}
			putc(c, outf);
		}

	compute_crcq(0);
	compute_crcq(0);
	calc_crc = crc;
	file_crc = get2q();
	verify_crc(calc_crc, file_crc);
}

/* verify_crc(calc_crc, file_crc); -- check if crc's check out */
verify_crc(calc_crc, file_crc)
unsigned int calc_crc, file_crc;
{
	calc_crc &= WORDMASK;
	file_crc &= WORDMASK;

	if (calc_crc != file_crc) {
		fprintf(stderr, "CRC error\n---------\n");
		fprintf(stderr, "CRC in file:    0x%x\n", file_crc);
		fprintf(stderr, "calculated CRC: 0x%x\n", calc_crc);
		exit(3);
	}
}

/* get2q(); q format -- read 2 bytes from input, return short */
short
get2q()
{
	register int c;
	short value = 0;

	c = getq();
	value = (c & BYTEMASK) << 8;
	c = getq();
	value |= (c & BYTEMASK);

	return value;
}

/* get4q(); q format -- read 4 bytes from input, return long */
long
get4q()
{
	register int c, i;
	long value = 0;

	for (i = 0; i < 4; i++) {
		c = getq();
		value <<= 8;
		value |= (c & BYTEMASK);
	}
	return value;
}

/* getqbuf(); q format -- read n characters from input into buf */
/*		All or nothing -- no partial buffer allowed */
getqbuf(buf, n)
register char *buf;
register int n;
{
	register int c, i;

	for (i = 0; i < n; i++) {
		if ((c = getq()) == EOF)
			return EOF;
		*buf++ = c;
	}
	return 0;
}

#define RUNCHAR 0x90

/* q format -- return one byte per call, keeping track of run codes */
getq()
{
	register int c;
	if ((c = getq_nocrc()) == EOF)
		return EOF;
	compute_crcq(c);
	return c;
}

getq_nocrc()
{
	static int rep, lastc;
	int c;

	if (rep) {
		rep--;
		return lastc;
	}
	if ((c = getq_raw()) == EOF) {
		return EOF;
		}
	if (c == RUNCHAR) {
		if ((rep = getq_raw()) == EOF)
			return EOF;
		if (rep == 0) {
			return RUNCHAR;
		}
		else {
			/* already returned one, about to return another */
			rep -= 2;
			return lastc;
		}
	}
	else {
		lastc = c;
		return c;
	}
}

/* q format -- return next 8 bits from file without interpreting run codes */
getq_raw()
{
	char ibuf[4];
	register char *ip = ibuf, *iend = ibuf + sizeof ibuf;
	static char obuf[3];
	static char *op = obuf, *oend = obuf + sizeof obuf;
	static int eof = 0;
	int c;

	if (op == obuf) {
		for (ip = ibuf; ip < iend; ip++) {
			if ((c = get6bits()) == EOF)
				if (ip <= &ibuf[1])
					return EOF;
				else if (ip == &ibuf[2])
					eof = 1;
				else
					eof = 2;
			*ip = c;
		}
		obuf[0] = (ibuf[0] << 2 | ibuf[1] >> 4);
		obuf[1] = (ibuf[1] << 4 | ibuf[2] >> 2);
		obuf[2] = (ibuf[2] << 6 | ibuf[3]);
	}
	if ((eof) & (op >= &obuf[eof]))
		return EOF;
	c = *op++;
	if (op >= oend)
		op = obuf;
	return (c & 0xff);
}

char tr[] = "!\"#$%&'()*+,-012345689@ABCDEFGHIJKLMNPQRSTUVXYZ[`abcdefhijklmpqr";

/* q format -- decode one byte into 6 bit binary */
get6bits()
{
	register int c;
	register char *where;

	while (1) {
		c = getc(ifp);
		switch (c) {
		case '\n':
		case '\r':
			continue;
		case ':':
		case EOF:
			return EOF;
		default:
			where = tr;
			while (*where != '\0' && *where != c)
				where++;
			if (*where == c)
				return (where - tr);
			else {
				fprintf(stderr, "bad char\n");
				return EOF;
			}
		}
	}
}

#define CRCCONSTANT 0x1021

compute_crcq(c)
register unsigned int c;
{
	register int i;
	register unsigned int temp = crc;

	for (i=0; i<8; i++) {
		c <<= 1;
		if ((temp <<= 1) & WORDBIT)
			temp = (temp & WORDMASK) ^ CRCCONSTANT;
		temp ^= (c >> 8);
		c &= BYTEMASK;
	}
	crc = temp;
}

/* old format -- process .hex and .hcx files */
do_o_header(macname, filename)
char *macname, *filename;
{
	char namebuf[256];		/* big enough for both att & bsd */
	char ibuf[BUFSIZ];
	int n;

	/* set up name for output files */
	if (macname[0] == '\0') {
		strcpy(namebuf, filename);

		/* strip directories */
		macname = search_last(namebuf, '/');
		if (macname == NULL)
			macname = namebuf;
		else
			macname++;

		/* strip extension */
		n = strlen(macname);
		if (n > 4) {
		    n -= 4;
		    if (macname[n] == '.' && macname[n+1] == 'h'
					    && macname[n+3] == 'x')
			    macname[n] = '\0';
		}
	}
	n = strlen(macname);
	if (n > NAMEBYTES)
		n = NAMEBYTES;
	strncpy(mh.m_name, macname, n);
	mh.m_name[n] = '\0';

	/* read "#TYPEAUTH$flag"  line */
	if (fgets(ibuf, BUFSIZ, ifp) == NULL) {
		fprintf(stderr, "unexpected EOF\n");
		exit(2);
	}
	n = strlen(ibuf);
	if (n >= 7 && ibuf[0] == '#' && ibuf[n-6] == '$') {
		if (n >= 11)
			strncpy(mh.m_type, &ibuf[1], 4);
		if (n >= 15)
			strncpy(mh.m_author, &ibuf[5], 4);
		sscanf(&ibuf[n-5], "%4hx", &mh.m_flags);
	}
}

do_o_forks()
{
	char ibuf[BUFSIZ];
	int forks = 0, found_crc = 0;
	int calc_crc, file_crc;
	int n;

	crc = 0;	/* calculate a crc for both forks */

	/* create empty files ahead of time */
	close(creat(files.f_data, 0666));
	close(creat(files.f_rsrc, 0666));

	while (!found_crc && fgets(ibuf, BUFSIZ, ifp) != NULL) {
		if (forks == 0 && strncmp(ibuf, "***COMPRESSED", 13) == 0) {
			compressed++;
			continue;
		}
		if (strncmp(ibuf, "***DATA", 7) == 0) {
			mh.m_datalen = make_file(files.f_data, compressed);
			forks++;
			continue;
		}
		if (strncmp(ibuf, "***RESOURCE", 11) == 0) {
			mh.m_rsrclen = make_file(files.f_rsrc, compressed);
			forks++;
			continue;
		}
		if (compressed && strncmp(ibuf, "***CRC:", 7) == 0) {
			found_crc++;
			calc_crc = crc;
			sscanf(&ibuf[7], "%x", &file_crc);
			break;
		}
		if (!compressed && strncmp(ibuf, "***CHECKSUM:", 12) == 0) {
			found_crc++;
			calc_crc = crc & BYTEMASK;
			sscanf(&ibuf[12], "%x", &file_crc);
			file_crc &= BYTEMASK;
			break;
		}
	}

	if (found_crc)
		verify_crc(calc_crc, file_crc);
	else {
		fprintf(stderr, "missing CRC\n");
		exit(3);
	}
}

make_file(fname, compressed)
char *fname;
int compressed;
{
	register int n;
	char ibuf[BUFSIZ];
	FILE *outf;
	int nbytes = 0;

	outf = fopen(fname, "w");
	if (outf == NULL) {
		perror(fname);
		exit(-1);
	}

	while (fgets(ibuf, BUFSIZ, ifp) != NULL) {
		if (strncmp(ibuf, "***END", 6) == 0)
			break;
		if (compressed)
			nbytes += comp_to_bin(ibuf, outf);
		else
			nbytes += hex_to_bin(ibuf, outf);
	}

	return nbytes;
}

compute_crcc(c)
unsigned char c;
{
	crc = (crc + c) & WORDMASK;
	crc = ((crc << 3) & WORDMASK) | (crc >> 13);
}

compute_crce(c)
unsigned char c;
{
	crc += c;
}

#define SIXB(c) (((c)-0x20) & 0x3f)

comp_to_bin(ibuf, outf)
char ibuf[];
FILE *outf;
{
	char obuf[BUFSIZ];
	register char *ip = ibuf;
	register char *op = obuf;
	register int n, outcount;
	int numread, incount;

	numread = strlen(ibuf);
	ip[numread-1] = ' ';		/* zap out the newline */
	outcount = (SIXB(ip[0]) << 2) | (SIXB(ip[1]) >> 4);
	incount = ((outcount / 3) + 1) * 4;
	for (n = numread; n < incount; n++)	/* restore lost spaces */
		ibuf[n] = ' ';

	n = 0;
	while (n <= outcount) {
		*op++ = SIXB(ip[0]) << 2 | SIXB(ip[1]) >> 4;
		*op++ = SIXB(ip[1]) << 4 | SIXB(ip[2]) >> 2;
		*op++ = SIXB(ip[2]) << 6 | SIXB(ip[3]);
		ip += 4;
		n += 3;
	}

	for (n=1; n <= outcount; n++)
		compute_crcc(obuf[n]);

	fwrite(obuf+1, 1, outcount, outf);
	return outcount;
}

hex_to_bin(ibuf, outf)
char ibuf[];
FILE *outf;
{
	register char *ip = ibuf;
	register int n, outcount;
	int c;

	n = strlen(ibuf) - 1;
	outcount = n / 2;
	for (n = 0; n < outcount; n++) {
		c = hexit(*ip++);
		compute_crce(c = (c << 4) | hexit(*ip++));
		fputc(c, outf);
	}
	return outcount;
}

hexit(c)
int c;
{
	if ('0' <= c && c <= '9')
		return c - '0';
	if ('A' <= c && c <= 'F')
		return c - 'A' + 10;

	fprintf(stderr, "illegal hex digit: %c", c);
	exit(4);
}

put2(bp, value)
char *bp;
short value;
{
	*bp++ = (value >> 8) & BYTEMASK;
	*bp++ = value & BYTEMASK;
}

put4(bp, value)
char *bp;
long value;
{
	register int i, c;

	for (i = 0; i < 4; i++) {
		c = (value >> 24) & BYTEMASK;
		value <<= 8;
		*bp++ = c;
	}
}
SHAR_EOF
echo shar: extracting xbin.l
cat - << \SHAR_EOF > xbin.l
.TH XBIN local "24 Mar 1985"
.UC 4
.SH NAME
xbin \- convert mailable format BinHex file into binary before downloading
to MacTerminal
.SH SYNOPSIS
.B xbin
[
.B \-o
]
[
.B \-v
]
[
.B \-l
]
[[
.B \-n
name
] file] ...
.SH DESCRIPTION
.I Xbin
converts a file created by BinHex (usually
named with one of the extensions ".hex", ".hcx", or ".hqx")
into three host-system files suitable for downloading to a
Macintosh via macput.
This program is designed for use with the 1.1 Release
version of MacTerminal, but includes a compatibility option for the
old -0.15X Almost-Alpha version.
.PP
The
.B -l
(list) option reads the header information and
prints out all the useful information there,
without creating any converted output files.
.PP
The
.B -v
(verbose) option prints a line for each file to be converted, indicating
the input and output file names.
.PP
The
.B -n
name
option allows the user to specify the name to use when creating
the host files and the eventual name to use on the mac.
This option must precede the input file name it is to affect.
.PP
If this option is not used, the names will be derived from
either the input file name (.hex or .hcx files),
or the name encoded in the header information (.hqx files).
Spaces and slashes will be converted to underscores, and
the .h?x extension will be deleted, if one is included in the
input file name.
.PP
A file name of "-" indicates that the input should be taken from stdin.
If no mac file name is specified, the default name (for .hex or .hcx files)
is "stdin".
.PP
Mail or news headers and signatures need not be manually
stripped -- xbin will ignore pretty much anything
it doesn't need.
.PP
.I xbin
creates three host-system files from each input file:
.IB name .info ,
.IB name .data ,
and
.IB name .rsrc .
.PP
The
.B \-o
flag specifies "old" (version -0.15X) MacTerminal compatibility mode.
.SH BUGS
The "LOCKED" bit in the flags cannot be set by xbin.
This is due to a bug in MacTerminal, which sets the flags
when the file is created, rather than after it has been
transfered, resulting in it not being able to write the
file.
.PP
Input files must contain a line starting with "(This file"
to detect the beginning of the BinHex information.
.SH SEE ALSO
macput(1), macget(1)
.SH AUTHOR
Dave Johnson, Brown 12/16/84;
CRC handling code by Darin Adler, TMQ Software 3/16/85
SHAR_EOF