#ifndef lint
static char rcsid[] =
	"$Xukc: z88link.c,v 1.11 91/06/08 14:11:32 rlh2 Rel $";
#endif  /* !lint */

/* I give lots of software away and here is the standard copyright
 * notice I use ..
 */

/*
 * Copyright 1991 Richard Hesketh / rlh2@ukc.ac.uk
 *
 * Permission to use, copy, modify and distribute this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of Richard Hesketh not be used
 * in advertising or publicity pertaining to distribution of the software
 * without specific, written prior permission.  Richard Hesketh makes no
 * representations about the suitability of this software for any purpose.
 * It is provided "as is" without express or implied warranty.
 *
 * Richard Hesketh DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL Richard Hesketh
 * BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
 * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
 * OF THIS SOFTWARE.
 *
 * Author:  Richard Hesketh / rlh2@ukc.ac.uk, 
 */

/*
 *  File transfer ([]X Import/Export) for the Cambridge Computer
 *  Z88 Laptop.
 *
 * This is a very simple file transfer program for UNIX machines
 * via /dev/tty?  (where ? is normally "a" or "b" on a SPARCstation for
 * example). It recognises when it is connected to a pipe or file and so
 * is not restricted to a /dev serial device.
 *
 * It uses the Z88's Import/Export protocol encoding for the actual
 * transfer of data and also converts between Newline representations.
 *
 * No conversion to or from PipeDream files is attempted .. you
 * need a separate script for that.  Something like a lex or perl script
 * that converts the %blah% constructs to something should do (I don't
 * have one yet).
 *
 * Usage details are presented at the end of this file.
 *
 * WARNING: Whilst I have taken every care to trap errors, it is still possible
 *	    during a transfer to bung up the tty serial line on the UNIX
 *	    end.  Typing Control-C may restore the serial line back into
 *	    a sane state, however it can also leave it unusable.  If this
 *	    happens the only thing that cures it is a machine reboot.
 *	    However, at 2400 baud this should not happen to you!
 *	    I hope to up the rate to at least 9600 when I figure out the
 *	    the tty driver 8-).
 *
 * Richard Hesketh
 * Computing Lab., University of Kent at Canterbury,
 * Canterbury, Kent, CT2 7NF, United Kingdom.
 *       Tel: +44 227 764000 ext 7620/7590      Fax: +44 227 762811
 * Email: [Internet] rlh2@ukc.ac.uk
 *	  [JANET]    rlh2@uk.ac.ukc
 */

#define VERSION 1.1

#include <stdio.h>
#include <errno.h>	/* names for errno */

#ifndef clipper
/* pure BSD 4.3 machines have open(2) mode defined in <sys/file.h> */
#include <fcntl.h>	/* open(2) modes */
#endif /* clipper */

#include <sgtty.h>	/* ioctls for terminal interface */
#include <sys/types.h>
#include <sys/time.h>	/* for timeout on select */
#include <sys/file.h>	/* for access(2) modes */
#include <signal.h>

#define CONTROL(x)	((x) & (~ 0x40))	/* converts 'A' to '^A' */

/* Z88 Import/Export protocol codes */
#define XON '\021'
#define XOFF '\023'
#define BIN "\033B"
#define ESC '\033'
#define ZFILENAME_START "\033N"
#define ZFILENAME_END "\033F"
#define ZFILE_END "\033E"
#define ZBATCH_END "\033Z"

#define IN_BAUD_RATE B2400
#define OUT_BAUD_RATE B2400
#define TRUE 1
#define FALSE 0

#define FILENAME_SIZE 300

static void close_connection();

static char is_pipe = FALSE;
static int exit_status = 0;
static int confd;		/* fd of outgoing connection */
static struct sgttyb save_serial;	/* leave it as we found it */

struct sgttyb rsmode = {
	IN_BAUD_RATE,
	OUT_BAUD_RATE,
	-1, -1,
	RAW | ODDP | EVENP | TANDEM
};

static char port[10];
static char *prog_name = "z88link";


/*  return sys_errlist[errno] if in range
 */
static char *
reason()
{
        extern char *sys_errlist[];
        extern int errno, sys_nerr;

        return((errno > 0 && errno < sys_nerr) ? sys_errlist[errno]
                                                        : "unknown reason");
}


/*
 *  recvc()
 *  Receive a character from the serial port.  Returns the character,
 *  or -1 if no character ready.
 */
static int
recvc()
{
	char ch;
	int s;

	errno = 0;
	do {
		if ((s = read(confd, &ch, 1)) == 1)
			return (ch & 0xff);
	} while (s < 0);

	if (is_pipe) {
		/* special case for input coming from a data file or | */
		close_connection();
	}

	return (-1);
}


/*
 *  rawsendc()
 *  Send a character to the serial port.
 *  Should only be called after open_connection().
 */
static void
rawsendc(c)
int c;
{
	char ch;

	ch = c & 0xff;
	write(confd, &ch, 1);
}


/*
 *  rawsends()
 *  Send a string to the serial port using rawsendc().
 */
static void
rawsends(s)
char *s;
{
	char *p;

	for (p = s; *p != '\0'; ++p)
		rawsendc(*p);
}


/*
 *  hex()
 *  Converts a 4-bit number into the appropriate hexdecimal representation
 */
static unsigned char
hex(b)
unsigned char b;
{
	return (b > 9 ? ('A' + b - 10) : ('0' + b));
}


/*
 *  binsendc()
 *  Sends a unprintable or 8-bit character using an ESC B prefix.
 */
static void
binsendc(ch)
int ch;
{
	char out[5];
	unsigned char c = (unsigned)(ch & 0xff);

	if (c == '\012')
		(void)sprintf(out, "%s0D", BIN);
	else
		(void)sprintf(out, "%s%c%c", BIN,
				hex((c & 0xf0) >> 4), hex(c & 0x0f));
	rawsends(out);
}


/*
 *  sendc()
 *  Send a character to the serial port.
 *  Should only be called after open_connection().
 *  Characters out of the printable 7-bit ascii range (0x20 to 0x7E)
 *  are sent using binsendc().
 */
static void
sendc(c)
int c;
{
	char ch;

	ch = c & 0xFF;

	if (ch < 0x20 || ch > 0x7e) {
		/* send using the BIN protocol prefix */
		binsendc(ch);
		return;
	} else
		rawsendc(ch);
}


/*
 *  sends()
 *  Send a string to the serial port using sendc().
 */
static void
sends(s)
char *s;
{
	char *p;

	for (p = s; *p != '\0'; ++p)
		sendc(*p);
}


/*
 *  sendfilename()
 *  Send a filename to the Z88 in its Import/Export protocol.
 */
static void
sendfilename(name)
char *name;
{
	char filename[FILENAME_SIZE];

	(void)sprintf(filename, "%s%s%s", ZFILENAME_START, name, ZFILENAME_END);
	rawsends(filename);
}


/*
 *  sendEOF()
 *  Send the EOF marker out .. sends out End-Of-Batch if "true"
 */
static void
sendEOF(last)
int last;
{
	if (last)
		rawsends(ZBATCH_END);
	else
		rawsends(ZFILE_END);
}


static int
blocking_read()
{
	int ch;

	while ((ch = recvc()) == -1);
	return (ch);
}


static int
readfile(fp, size)
FILE *fp;
int *size;
{
	int end_of_batch = 1, eof = 0, ch, num_bytes_read = 0;
	char found_escape = 0, found_binary = 0;
	unsigned char bin1;

	*size = 0;
	while (!eof) {
		ch = blocking_read();

		if (found_binary == 1) {
			found_binary = 2;
			bin1 = ch > '9' ? ch - 'A' + 10 : ch - '0';
			continue;
		} else if (found_binary == 2) {
			/* got the second part of binary character
			 * so convert it to a real ascii and output that.
			 */
			found_binary = 0;
			bin1 *= 16;
			bin1 += ch > '9' ? ch - 'A' + 10 : ch - '0';
			ch = bin1;

			if (ch == 0x0D)
				/* convert into correct form of newlines */
				ch = '\012';
		} else if (ch == ESC) {
			found_escape = 1;
			continue;
		} else if (found_escape) {
			if (ch == 'Z' || ch == 'E') {
				end_of_batch = (ch == 'Z');
				eof = 1;
				continue;
			}
			if (ch == 'B') {
				/* is this a binary number? */
				found_escape = 0;
				found_binary = 1;
				continue;
			}
		} else if (ch == 13)
			/* convert into correct form of newlines */
			ch = '\012';
		else if (ch == XON || ch == XOFF)
			/* ignore these? */
			continue;

		if (putc(ch, fp) < 0) {
			fprintf(stderr, "%s: failed to save file (%s)\n",
				prog_name, reason());
			exit_status = 2;
			close_connection();
			/* NOTREACHED */
		} else {
			num_bytes_read++;
			if (num_bytes_read % 1024 == 0) {
				putc('.', stderr);
				fflush(stderr);
			}
		}
	}
	*size = num_bytes_read;
	return (end_of_batch);
}


/*
 *  get_filename()
 *  Look for a filename in the Z88 protocol format and return it or NULL
 */
static char *
get_filename()
{
	int ch, i, escape_found = 0;
	static char bigbuf[FILENAME_SIZE];

	i = 1;
	/* find the start of the filename .. performing some
	 * re-synchronization upon error.
	 */
	while (i) {
		ch = blocking_read();

		if (ch == ESC) {
			/* this must be a batch control character? */
			ch = blocking_read();
			if (ch == 'Z')
				return (char *)1;
			if (ch != 'N')
				return NULL;
			i = 0;
		}
	}

	/* we have found ZFILENAME_START so read filename until
	 * ZFILENAME_END.
	 */
	i = 0;
	while(1) {
		ch = blocking_read();
		if (ch == ESC)
			escape_found = 1;
		else if (escape_found) {
			if (ch == 'F') {
				/* end of the filename */
				bigbuf[i] = '\0';
				break;
			}
			bigbuf[i++] = ESC;
			bigbuf[i++] = ch;
		} else {
			escape_found = 0;
			bigbuf[i++] = ch;
		}
	};
	/* when we have got here we have read the ZFILENAME_START, the
	 * whole filename and ZFILENAME_END.
	 */
	return (bigbuf);
}


static void
sane_tty(fd)
int fd;
{
        struct sgttyb basic;
        struct tchars chars;
        struct ltchars local;
        int mode;

	mode = NTTYDISC;

        if (ioctl(fd, TIOCSETD, (char *) &mode) != 0)
                perror("failed to set line discipline");

        basic.sg_ispeed = B9600;
        basic.sg_ospeed = B9600;
        basic.sg_erase = 0x7f;
        basic.sg_kill = CONTROL('U');
        basic.sg_flags = CRMOD | ECHO;

        if (ioctl(fd, TIOCSETP, (char *) &basic) != 0)
                perror("failed to set basic terminal parameters");

        
        chars.t_intrc = CONTROL('C');
        chars.t_quitc = CONTROL('\\');
        chars.t_startc = CONTROL('Q');
        chars.t_stopc = CONTROL('S');
        chars.t_eofc = CONTROL('D');
        chars.t_brkc = -1;

        if (ioctl(fd, TIOCSETC, (char *) &chars) != 0)
                perror("failed to set terminal special characters");


        mode = LCRTERA | LCRTKIL | LPASS8 | LDECCTQ;

        if (ioctl(fd, TIOCLSET, (char *) &mode) != 0)
                perror("failed to set terminal local mode word");


        local.t_suspc = CONTROL('Z');
        local.t_dsuspc = CONTROL('Y');
        local.t_rprntc = CONTROL('R');
        local.t_flushc = CONTROL('O');
        local.t_werasc = CONTROL('W');
        local.t_lnextc = CONTROL('V');

        if (ioctl(fd, TIOCSLTC, (char *) &local) != 0)
                perror("failed to set terminal local characters");
}


/*
 *  close_connection()
 *  Restores the state of the serial port and closes the file
 *  associated with it.
 */
static void
close_connection()
{
	if (!is_pipe) {
		ioctl(confd, TIOCSETP, &save_serial);
		sane_tty(confd);
		(void)close(confd);
	}
	exit(exit_status); 
}


/*
 *  open_connection()
 *  Opens the serial line and starts a connection to the given host.
 */
static int
open_connection(port, receiving_files)
char *port;
char receiving_files;
{
	struct sgttyb serp;
	int flags, attempts, ch, ttyfd;

	if (!isatty(confd = fileno(receiving_files ? stdin : stdout))) {
		/* Input is being redirected to/from a pipe or file.
		 * So use stdin/stdout instead of the tty as the serial line.
		 */
		is_pipe = TRUE;
		return TRUE;
	}

	/*
	 *  Open the serial port, and set it to the required baud rate.
	 *  Save the parameters so we can reset it again when it's closed.
	 */
	if ((confd = open(port, O_RDWR)) == -1) {
		perror(port);
		return FALSE;
	}
	ioctl(confd, TIOCEXCL, 0); /* get exclusive use */

	(void)signal(SIGINT, close_connection);
	(void)signal(SIGHUP, close_connection);
	(void)signal(SIGQUIT, close_connection);

	ioctl(confd, TIOCGETP, &save_serial);
	ioctl(confd, TIOCSETP, &rsmode);
	return TRUE;
}


static void
usage()
{
	fprintf(stderr, "usage: %s [a|b] [-r | [file1 ..]]\n", prog_name);
	fprintf(stderr, "\ta = /dev/ttya .. b = /dev/ttyb\n");
	fprintf(stderr, "\t-r = receive files\n");
	fprintf(stderr, "\tfile1 .. = files to send\n");
	exit(1);
}



/******************************************************************/

static int
make_directories(filename)
char *filename;
{
	extern char *index();
	char *slash, name[FILENAME_SIZE];
	int offset = 0, need_new_directories = 0;

	(void)strcpy(name, filename);

	while ((slash = index(name + offset, '/')) != NULL) {
		if (slash == name) {
			/* we have a filename starting from root! */
			offset++;
			continue;
		}
		*slash = '\0';
		offset = slash - name + 1;
		errno = 0;
		if (access(name, W_OK|X_OK) < 0 && errno == ENOENT) {
			/* last directory in current pathname does not exist */
			*slash = '/';
			need_new_directories = 1;
			break;
		} else if (errno > 0) {
			/* some other error occurred */
			return (0);
		}
		*slash = '/';
	}

	if (!need_new_directories)
		return (1);	/* no directories to make */

	/* we must now make all the directories from the "name + offset"
	 * downwards .. only stopping on a permissions or file system full
	 * error.
	 */
	while (*slash != '\0') {
		*slash = '\0';
		printf("Making directory: %s", name);
		if (mkdir(name, 0755) < 0) {
			printf(" ... Failed! (%s)\n", reason());
			return (0);
		} else
			printf("\n");
		*slash = '/';
		slash++;
		while (*slash != '\0' && *slash != '/') slash++;
	}
	return (1);
}


static void
receive_files()
{
	int end_of_batch = 0;
	char *filename;
	FILE *fp;
	int size;

	if (open_connection(port, TRUE) < 0) {
		fprintf(stderr, "%s: failed to open %s\n", prog_name, port);
		exit(2);
	}

	printf("Waiting for incoming files ...\n");
	do {
		if ((filename = get_filename()) == NULL) {
			fprintf(stderr, "%s: garbled/missing filename in receive\n",
				prog_name);
			exit_status = 2;
			close_connection();
			/* NOTREACHED */
		}
		if (filename == (char *)1) /* End of batch found */
			break;

		if (!make_directories(filename) ||
		    (fp = fopen(filename, "w")) == NULL) {
			fprintf(stderr, "%s: cannot upload file into %s (%s)\n",
				prog_name, filename, reason());
			exit_status = 2;
			close_connection();
			/* NOTREACHED */
		}
		printf("Starting reception of %s (one dot equals 1k)\n",
			filename);
		size = 0;
		if ((end_of_batch = readfile(fp, &size)) < 0) {
			fprintf(stderr, "%s: failed to upload %s\n",
				prog_name, filename);
			exit_status = 2;
			close_connection();
			/* NOTREACHED */
		}
		if (fclose(fp) < 0) {
			fprintf(stderr, "%s : failed to close uploaded file %s (%s)\n",
				prog_name, filename, reason());
			exit_status = 2;
			close_connection();
			/* NOTREACHED */
		}
		if (size >= 1024)
			putc('\n', stderr);
		fprintf(stderr, "End of successful reception of %s (%d bytes)\n",
			filename, size);
	} while (end_of_batch == 0);

	close_connection();
}


static int
send_file(fp, size)
FILE *fp;
int *size;
{
	int ch;

	while ((ch = fgetc(fp)) != EOF) {
		(*size)++;
		sendc(ch);
		if (*size % 1024 == 0) {
			putc('.', stderr);
			fflush(stderr);
		}
	}
	return 0;
}


static void
send_files(files, count)
char **files;
int count;
{
	FILE *fp;
	int i, size;

	if (open_connection(port, FALSE) < 0) {
		fprintf(stderr, "%s: failed to open %s\n", prog_name, port);
		exit(2);
	}
	for (i = 0; i < count; i++) {
		if ((fp = fopen(files[i], "r")) == NULL) {
			fprintf(stderr, "%s: unable to send %s (%s)\n",
				prog_name, files[i], reason());
			continue;
		}
		fprintf(stderr, "Starting transmission of %s (one dot equals 1k)\n",
			files[i]);
		sendfilename(files[i]);
		size = 0;
		if (send_file(fp, &size) < 0) {
			fprintf(stderr, "%s: transmission of %s somehow failed .. file may be corrupted\n",
				prog_name, files[i]);
		}
		if (size >= 1024)
			putc('\n', stderr);
		fprintf(stderr, "Finished transmission of %s (%d bytes)\n",
			files[i], size);
		sendEOF(i == (count - 1));
	}
	close_connection();
}


/* Main routine */

/*
 * This program is a sort of filter for an RS232 tty port on a UNIX box
 * connected to a Cambridge Z88 laptop.  It is used for sending or receiving
 * files via the Z88's built-in Import/Export tool.
 *
 * Compile with ..
 *
 *	cc -o z88link z88link.c
 *
 *	(it has been built and tested on a SPARCstation 1 running SunOS 4.1.1
 *	 and has also just been built (but not tested) on a DECstation 3100,
 *	 VAXstation 3200, Sun 3, Sun 386i and Orion Clipper [a what?])
 *
 * Setup for Z88 ..
 *
 *	go into the control panel ([]s) and set the following ..
 *
 *		Transmit baud rate = 2400
 *		Receive baud rate = 2400
 *		Parity = None
 *		Xon/Xoff = Yes
 *
 * Invoked with ..
 *
 *	z88link [a|b] -r
 *		Reads files from the Z88 until an END-OF-BATCH is received.
 *		We are sent the name of the file etc and do any processing
 *		necessary.
 *
 *	z88link [a|b] file1 file2 ...
 *		Sends the named files to the Z88 as a batch of files.
 *		We send the filenames and do any conversion necessary.
 *
 * I use it for doing daily backups of all the files, in which case I use ..
 *
 *	(on UNIX)   % z88link a -r <RETURN>
 *
 *	(on z88)    invoke the Import/Export tool ([]x)
 *		    enter "s" to send a file
 *		    and enter ":RAM.?//*" to send all the files to UNIX
 *
 * z88link creates any missing directories when needed and is very chatty
 * about what it is doing 8-).
 *
 * z88link can also take advantage of pipes and file redirection. This
 * means that you are not stuck with having to use /dev/tty?, you can
 * use any communications link available.  Reception and Transmission is
 * still done using the Import/Export protocol.  For example if you have
 * a file "erik.bas" which is in the Import/Export protocol format you can
 * unpack on to a UNIX machine using file redirection ..
 *
 *	% z88link a -r < erik.bas
 *
 * or using a pipe ...
 *
 *	% cat erik.bas | z88link a -r
 *
 * Of course, you can also encode files in the Import/Export protocol as
 * well ..
 *
 *	% z88link a erik.bas > IE_erik.bas
 *
 */
main(argc, argv)
int argc;
char *argv[];
{
	prog_name = argv[0];

	if (argc < 3)
		usage();

	(void)sprintf(port, "/dev/tty%c", *argv[1]);

	if (strcmp(argv[2], "-r") == 0) {
		if (argc != 3)
			usage();
		receive_files();
	} else {
		argv++;
		argv++;
		argc--;
		argc--;
		send_files(argv, argc);
	}
	return (0);
}
