/* dlink.c - x/ymodem transfer routines for bmodem */
/*
	by David Betz, BYTE Magazine/BIX
*/

#include <stdio.h>
#include "dlink.h"

/* useful definitions */
#define TRUE	1
#define FALSE	0

/* protocol characters */
#define SOH	0x01	/* start of a 128 byte block */
#define STX	0x02	/* start of a 1024 byte block */
#define EOT	0x04	/* end of a complete file */
#define ACK	0x06	/* positive acknowledgement */
#define NAK	0x15	/* negative acknowledgement */
#define CAN	0x18	/* cancel transfer */
#define CRC	'C'	/* CRC request (instead of NAK to start a transfer) */

/* useful constants */
#define DT_TIME	-1	/* return value for timeout */
#define RETRY	10	/* number of times to retry */

/* timeout values (in seconds) */
#define STIME	6	/* timeout between characters of a packet */
#define ITIME	5	/* timeout sending initial NAK or 'C' */
#define XTIME	60	/* timeout waiting for initial NAK */
#define LTIME	20	/* timeout waiting for an ACK/NAK */
#define FTIME	1	/* timeout for flushing the line */

/* error messages */
char *errstring[] = {
    "OK",
    "INIT",	/* failed during initialization */
    "DATA",	/* failed sending/receiving a data packet */
    "EOT",	/* failed sending/receiving EOT */
    "CANCEL",	/* transfer canceled by other side */
    "EOF",	/* reached end of file */
    "DONE",	/* done with batch receive */
    "HEAD"	/* bad header */
};

/* global variables */
int blknum;		/* current block number */

/* local variables */
static int crcmode;	/* TRUE for CRC mode, FALSE for checksum */
static int timeout;	/* current receive timeout */
static int needack;	/* need an ACK for the previous block */
static int nak;		/* NAK character on receive */

/* crctab calculated by Mark G. Mendel, Network Systems Corporation */
static unsigned short crctab[256] = {
    0x0000,  0x1021,  0x2042,  0x3063,  0x4084,  0x50a5,  0x60c6,  0x70e7,
    0x8108,  0x9129,  0xa14a,  0xb16b,  0xc18c,  0xd1ad,  0xe1ce,  0xf1ef,
    0x1231,  0x0210,  0x3273,  0x2252,  0x52b5,  0x4294,  0x72f7,  0x62d6,
    0x9339,  0x8318,  0xb37b,  0xa35a,  0xd3bd,  0xc39c,  0xf3ff,  0xe3de,
    0x2462,  0x3443,  0x0420,  0x1401,  0x64e6,  0x74c7,  0x44a4,  0x5485,
    0xa56a,  0xb54b,  0x8528,  0x9509,  0xe5ee,  0xf5cf,  0xc5ac,  0xd58d,
    0x3653,  0x2672,  0x1611,  0x0630,  0x76d7,  0x66f6,  0x5695,  0x46b4,
    0xb75b,  0xa77a,  0x9719,  0x8738,  0xf7df,  0xe7fe,  0xd79d,  0xc7bc,
    0x48c4,  0x58e5,  0x6886,  0x78a7,  0x0840,  0x1861,  0x2802,  0x3823,
    0xc9cc,  0xd9ed,  0xe98e,  0xf9af,  0x8948,  0x9969,  0xa90a,  0xb92b,
    0x5af5,  0x4ad4,  0x7ab7,  0x6a96,  0x1a71,  0x0a50,  0x3a33,  0x2a12,
    0xdbfd,  0xcbdc,  0xfbbf,  0xeb9e,  0x9b79,  0x8b58,  0xbb3b,  0xab1a,
    0x6ca6,  0x7c87,  0x4ce4,  0x5cc5,  0x2c22,  0x3c03,  0x0c60,  0x1c41,
    0xedae,  0xfd8f,  0xcdec,  0xddcd,  0xad2a,  0xbd0b,  0x8d68,  0x9d49,
    0x7e97,  0x6eb6,  0x5ed5,  0x4ef4,  0x3e13,  0x2e32,  0x1e51,  0x0e70,
    0xff9f,  0xefbe,  0xdfdd,  0xcffc,  0xbf1b,  0xaf3a,  0x9f59,  0x8f78,
    0x9188,  0x81a9,  0xb1ca,  0xa1eb,  0xd10c,  0xc12d,  0xf14e,  0xe16f,
    0x1080,  0x00a1,  0x30c2,  0x20e3,  0x5004,  0x4025,  0x7046,  0x6067,
    0x83b9,  0x9398,  0xa3fb,  0xb3da,  0xc33d,  0xd31c,  0xe37f,  0xf35e,
    0x02b1,  0x1290,  0x22f3,  0x32d2,  0x4235,  0x5214,  0x6277,  0x7256,
    0xb5ea,  0xa5cb,  0x95a8,  0x8589,  0xf56e,  0xe54f,  0xd52c,  0xc50d,
    0x34e2,  0x24c3,  0x14a0,  0x0481,  0x7466,  0x6447,  0x5424,  0x4405,
    0xa7db,  0xb7fa,  0x8799,  0x97b8,  0xe75f,  0xf77e,  0xc71d,  0xd73c,
    0x26d3,  0x36f2,  0x0691,  0x16b0,  0x6657,  0x7676,  0x4615,  0x5634,
    0xd94c,  0xc96d,  0xf90e,  0xe92f,  0x99c8,  0x89e9,  0xb98a,  0xa9ab,
    0x5844,  0x4865,  0x7806,  0x6827,  0x18c0,  0x08e1,  0x3882,  0x28a3,
    0xcb7d,  0xdb5c,  0xeb3f,  0xfb1e,  0x8bf9,  0x9bd8,  0xabbb,  0xbb9a,
    0x4a75,  0x5a54,  0x6a37,  0x7a16,  0x0af1,  0x1ad0,  0x2ab3,  0x3a92,
    0xfd2e,  0xed0f,  0xdd6c,  0xcd4d,  0xbdaa,  0xad8b,  0x9de8,  0x8dc9,
    0x7c26,  0x6c07,  0x5c64,  0x4c45,  0x3ca2,  0x2c83,  0x1ce0,  0x0cc1,
    0xef1f,  0xff3e,  0xcf5d,  0xdf7c,  0xaf9b,  0xbfba,  0x8fd9,  0x9ff8,
    0x6e17,  0x7e36,  0x4e55,  0x5e74,  0x2e93,  0x3eb2,  0x0ed1,  0x1ef0
};

/*
 * updcrc macro derived from article Copyright (C) 1986 Stephen Satchell. 
 *  NOTE: First argument is referenced twice.
 *        Second argument must be in range 0 to 255.
 * 
 * Programmers may incorporate any or all code into their programs, 
 * giving proper credit within the source. Publication of the 
 * source routines is permitted so long as proper credit is given 
 * to Stephen Satchell, Satchell Evaluations and Chuck Forsberg, 
 * Omen Technology.
 */

#define updcrc(crc,ch) (crctab[((crc >> 8) & 0xFF)] ^ (crc << 8) ^ ch)

/* xy_sinit - initialize to send data using x/ymodem */
int xy_sinit()
{
    blknum = 0;
    return (startxfr());
}

/* xy_sndhdr - send a ymodem batch header */
int xy_sndhdr(name,size)
  char *name;		/* filename */
  long size;		/* file size (in bytes) */
{
    int sts;
    if ((sts = xy_sinit()) == ER_OK)
	sts = sendhdr(name,size);
    return (sts);
}

/* xy_sndblk - send a data block using x/ymodem */
int xy_sndblk(buf,len)
  unsigned char *buf;	/* data buffer */
  int len;		/* data length (must be either 128 or 1024) */
{
    return (sndblk(++blknum,buf,len));
}

/* xy_sndeot - end a data transfer using x/ymodem */
int xy_sndeot()
{
    unsigned char buf[1];
    buf[0] = EOT;		/* make an EOT packet */
    return (sndpkt(buf,1));	/* send it */
}

/* xy_sndend - send an empty header to end a batch send */
int xy_sndend()
{
    return (xy_sndhdr("",-1L));
}

/* xy_rinit - initialize to receive data using x/ymodem */
int xy_rinit(usecrc)
  int usecrc;		/* TRUE for CRC transfers, FALSE for checksum */
{
    timeout = ITIME;		/* use shorter initial timeout */
    nak = (usecrc ? CRC : NAK);	/* initial NAK sets crc/checksum mode */
    crcmode = usecrc;		/* remember if crc is in use */
    blknum = 0;			/* start at block zero (in case of header) */
    needack = FALSE;		/* don't need an ACK the first time */
    md_put(nak);		/* send the initial NAK (or CRC) */
    md_iflush(0);
    return (ER_OK);		/* return successfully */
}

/* xy_rcvhdr - receive a ymodem batch header */
int xy_rcvhdr(usecrc,name,psize)
  int usecrc;		/* TRUE for CRC transfers, FALSE for checksum */
  char *name;		/* file name (output) */
  long *psize;		/* file size (output) */
{
    unsigned char buf[LBLKLEN];
    int bc,sts;
    if ((sts = xy_rinit(usecrc)) != ER_OK)	/* initialize */
	return (sts);
    if ((sts = rcvblk(blknum,buf,&bc)) != ER_OK)/* receive the header */
	return (sts);
    return (processhdr(buf,bc,name,psize));	/* process the header */
}

/* xy_rcvblk - receive a data block using x/ymodem */
int xy_rcvblk(buf,plen)
  unsigned char *buf;	/* data buffer (output) */
  int *plen;		/* data length (output) */
{
    if (needack)			/* send ACK for previous block */
	xy_sndack();
    return (rcvblk(++blknum,buf,plen));	/* receive the next block */
}

/* xy_sndack - send an ack for a header packet */
xy_sndack()
{
    md_put(ACK);	/* send an ACK */
    md_iflush(0);	/* flush the input buffer */
}

/* xy_cancel - cancel a transfer */
xy_cancel()
{
    md_iflush(1);	/* flush the input buffer */
    md_put(CAN);	/* cancel the transfer */
    md_put(CAN); md_put(CAN); md_put(CAN);
}

/* startxfr - wait for the initial NAK or 'C' to start transfer */
static int startxfr()
{
    int lastch,ch;

    /* wait for the initial NAK or 'C' */
    for (lastch = -1; ; lastch = ch) {
	ch = md_get(XTIME);		/* get the next character */
	if (ch == DT_TIME) {		/* check for timeout */
	    log("timeout waiting for initial NAK");
	    return (ER_INIT);
	}
	else if (ch == NAK) {		/* start of checksum transfer */
	    crcmode = FALSE;
	    return (ER_OK);
	}
	else if (ch == CRC) {		/* start of CRC transfer */
	    crcmode = TRUE;
	    return (ER_OK);
	}
	else if (ch == CAN) {		/* abort transfer on CAN/CAN */
	    if (lastch == CAN)
		return (ER_CANCEL);
	}
	else				/* garbage character */
	    log("expecting initial NAK, ch=%02x",ch);
    }
}

/* sendhdr - send block zero header */
static int sendhdr(name,size)
  char *name; long size;
{
    unsigned char buf[SBLKLEN],*bp;
    int bc;

    /* initialize the block */
    for (bp = buf, bc = SBLKLEN; --bc >= 0; )
	*bp++ = '\0';
    blknum = 0;

    /* insert the filename and size */
    if (size >= 0)
	sprintf(buf,"%s%c%ld",name,'\0',size);
    else
	strcpy(buf,name);

    /* send the block */
    return (sndblk(blknum,buf,SBLKLEN));
}

/* sndblk - send a data packet */
static int sndblk(n,buf,len)
  int n;		/* block number */
  unsigned char *buf;	/* data buffer */
  int len;		/* data length (either SBLKLEN or LBLKLEN) */
{
    unsigned char packet[LBLKLEN+5],*p;
    unsigned int chk;

    /* setup the packet header */
    p = packet;
    *p++ = (len == SBLKLEN ? SOH : STX);
    *p++ = n;
    *p++ = ~n;

    /* compute the block check code */
    if (crcmode) {			/* compute the CRC */
	for (chk = 0; --len >= 0; )
	    chk = updcrc(chk,(*p++ = *buf++));
	chk = updcrc(chk,'\0');
	chk = updcrc(chk,'\0');
	*p++ = chk >> 8;
	*p++ = chk;
    }
    else {				/* compute the checksum */
	for (chk = 0; --len >= 0; )
	    chk += (*p++ = *buf++);
	*p++ = chk;
    }

    /* send the packet */
    return (sndpkt(packet,p-packet));
}

/* sndpkt - send a packet and wait for a response */
static int sndpkt(packet,plength)
  unsigned char *packet; int plength;
{
    int retries,lastch,ch;

    /* send data and wait for an ACK */
    for (retries = RETRY; retries > 0; --retries) {

	/* send the data */
	md_write(packet,plength);
	md_iflush(0);

	/* wait for ACK, NAK or CAN/CAN */
	for (lastch = -1; ; lastch = ch) {
	    ch = md_get(XTIME);		/* get the next character */
	    if (ch == DT_TIME) {	/* check for timeout */
		log("timeout waiting for ACK/NAK/CAN");
		break;
	    }
	    else if (ch == ACK)		/* return on ACK */
		return (ER_OK);
	    else if (ch == NAK) {	/* send packet again on NAK */
		log("received NAK");
		break;
	    }
	    else if (ch == CAN) {	/* cancel transfer on CAN/CAN */
		if (lastch == CAN)
		    return (ER_CANCEL);
	    }
	    else			/* garbage */
		log("expecting ACK/NAK/CAN, ch=%02x",ch);
	}
    }

    /* retry count ran out */
    return (plength == 1 ? ER_EOT : ER_DATA);
}

/* processhdr - process a received header */
static int processhdr(buf,len,name,psize)
  char *buf;		/* header */
  int len;		/* header length */
  char *name;		/* file name (output) */
  long *psize;		/* file size (output) */
{
    extern long atol();
    int i,j;

    /* check for end of batch */
    if (buf[0] == '\0') {
	xy_sndack();
	return (ER_DONE);
    }

    /* find the filename */
    for (i = 0; i < len; )
	if ((*name++ = buf[i++]) == '\0')
	    break;
    if (i >= len)
	return (ER_HEAD);

    /* find the file size */
    for (j = i; j < len; ++j)
	if (buf[j] < '0' || buf[j] > '9')
	    break;
    *psize = (i < j ? atol(&buf[i]) : -1L);

    /* return successfully */
    return (ER_OK);
}

/* rcvblk - receive a data block */
static int rcvblk(n,buf,plen)
  int n;		/* block number */
  unsigned char *buf;	/* data buffer */
  int *plen;		/* data length (output) */
{
    int nn,sts;

    /* receive a packet */
    while ((sts = rcvpkt(&nn,buf,plen)) == ER_OK) {

	/* check the block number */
	if (nn == (n & 0xFF)) {
	    timeout = LTIME;	/* use the longer timeout now */
	    nak = NAK;		/* and the normal NAK character */
	    needack = TRUE;	/* need an ACK for this block */
	    return (ER_OK);
	}
	else if (nn == ((n - 1) & 0xFF)) {
	    log("duplicate block");
	    xy_sndack();
	}
	else {
	    log("sequence error, n=%d",n);
	    sts = ER_DATA;
	    xy_cancel();
	    break;
	}
    }

    /* return failure status */
    return (sts);
}

/* rcvpkt - receive a data packet */
static int rcvpkt(pn,buf,plen)
  int *pn;		/* block number (output) */
  unsigned char *buf;	/* data buffer */
  int *plen;		/* data length (output) */
{
    int bc,ch,retries,sts;
    unsigned char *bp;

    /* receive data */
    for (retries = RETRY; retries > 0; --retries) {

	/* receive the packet header */
	switch (sts = rcvhdr(pn,plen)) {
	case ER_EOF:
	case ER_CANCEL:
	    return (sts);
	case ER_DATA: /* really timeout */
	    if (crcmode && blknum == 1 && retries == 6) {
		log("switching to checksum mode");
		crcmode = FALSE;
		nak = NAK;
	    }
	    md_put(nak);
	    continue;
	}

	/* receive the data */
	for (bp = buf, bc = *plen; --bc >= 0; *bp++ = ch)
	    if ((ch = md_get(STIME)) == DT_TIME)
		break;
	if (bc >= 0) {
	    log("timeout within packet");
	    md_put(nak);
	    continue;
	}

	/* check the block check code */
	if (rcvchk(buf,*plen) != ER_OK) {
	    md_put(nak);
	    continue;
	}

	/* return successfully */
	return (ER_OK);
    }

    /* retry count ran out */
    return (ER_DATA);
}

/* rcvhdr - receive a packet header */
static int rcvhdr(pn,plen)
  int *pn;	/* block number (output) */
  int *plen;	/* packet length (output) */
{
    int ch,lastch;

    /* wait for next packet header */
    for (lastch = -1; ; lastch = ch) {

	/* wait for SOH, STX or CAN/CAN */
	ch = md_get(timeout);	/* get next character */
	if (ch == DT_TIME) {	/* check for timeout */
	    log("timeout waiting for SOH/STX");
	    return (ER_DATA);
	}
	else if (ch == EOT) {	/* end of transfer */
	    xy_sndack();
	    return (ER_EOF);
	}
	else if (ch == SOH)	/* start of a short packet */
	    *plen = SBLKLEN;
	else if (ch == STX)	/* start of a long packet */
	    *plen = LBLKLEN;
	else if (ch == CAN) {	/* two cancels abort transfer */
	    if (lastch == CAN)
		return (ER_CANCEL);
	    continue;
	}
	else {	/* garbage */
	    log("expecting SOH/STX, ch=%02x",ch);
	    md_iflush(1);
	    continue;
	}

	/* get the block numbers */
	if ((*pn = md_get(STIME)) == DT_TIME
	||  (ch = md_get(STIME)) == DT_TIME) {
	    log("timeout within packet");
	    return (ER_DATA);
	}

	/* check the block numbers */
	if (*pn + ch != 0xFF) {
	    log("block check failed, n1=%02x, n2=%02x",*pn,ch);
	    continue;
	}

	/* got a valid packet header */
	return (ER_OK);
    }
}

/* rcvchk - receive and check the checksum or CRC */
static int rcvchk(buf,len)
  unsigned char *buf;	/* data buffer */
  int len;		/* data length */
{
    unsigned int chk;
    int lastch,ch;

    /* receive and check the checksum or CRC */
    if (crcmode) {				/* CRC */
	if ((lastch = md_get(STIME)) == DT_TIME	/* two bytes */
	||  (ch = md_get(STIME)) == DT_TIME) {
	    log("timeout within packet");
	    return (ER_DATA);
	}
	for (chk = 0; --len >= 0; )
	    chk = updcrc(chk,*buf++);
	chk = updcrc(chk,lastch);
	if (updcrc(chk,ch) & 0xFFFF) {
	    log("CRC error");
	    return (ER_DATA);
	}
    }
    else {					/* checksum */
	if ((ch = md_get(STIME)) == DT_TIME) {	/* one byte */
	    log("timeout within packet");
	    return (ER_DATA);
	}
	for (chk = 0; --len >= 0; )
	    chk += *buf++;
	if ((chk & 0xFF) != ch) {
	    log("checksum error");
	    return (ER_DATA);
	}
    }

    /* return successfully */
    return (ER_OK);
}
