/***************************************************************************
 *                                                                         *
 *   FTFKERM.C                                                             *
 *                                                                         *
 *   Copyright (c) 1992-1997 Galacticomm, Inc.      All Rights Reserved.   *
 *                                                                         *
 *   File Transfer Software for Kermit protocol                            *
 *                                                                         *
 *   With all operating-system-specific functions removed (I/O, disk,      *
 *   memory, time), the pure file transfer algorithm can be isolated,      *
 *                                                                         *
 *                                               - R. Stein  3/03/92       *
 *                                                                         *
 ***************************************************************************/

#include "gcomm.h"
#include "ftf.h"
#include "ftfkerm.h"

/*

Physical Kermit packet:

     MARK  LEN   SEQ   TYPE  <data>  <checksum>  EOL
    :     :     :          :       :           :
    :     :                :       :           :
     \_______KHEAD________/        :           :
          .     .                  :           :
          :                        :           :
           \_included in checksum_/            :
                .                              :
                :                              :
                 \_____included in length_____/
                  (KPKLEN is physical maximum)
                (krmscb->maxl is logical maximum)

     MARK  always '\x01'

     LEN   length of SEQ,TYPE,<data>,<checksum> fields in actual bytes plus ' '
           logically 0 to 94 (KPKLEN),
           physically ' ' to '~'

     SEQ   64-state sequence code, wrapping from ' ' to '_' ('\x20' to '\x5F')

     TYPE  packet type, such as:  S Y N Z D E B F A

     <data>  0 to 91 bytes for 1-byte checksum
               to 90           2-byte
               to 89           3-byte

           krmscb->numdat is actual number of bytes in <data> field

           These are physical bytes.  They may represent fewer or more bytes
           due to various escaping, prefixing, and compression methods.

     <checksum>  1 to 3 bytes

           krmscb->chkt is '1', '2', or '3', correspondingly

     EOL   usually '\r'


Some assumptions about the other party, and about the file routines:

     Assumes that ftfxrb() is capable of returning EOF multiple times
         after the first time it returns EOF

     Assumes that after ftfxrb()'ing N bytes and then ftfxrb()'ing EOF
         (in other words, the file has N bytes), that you can ftfxsk() to
         N and then ftfxrb() will obediently return EOF right away.

     May send a final 0-length D(ata) packet if the last bytes of the
         file neatly fit into a full packet.

*/

/*-- KERMIT constants --- */

#define KPROOM 6   /* safety room for each new element in transmited packet */
         /* (max bytes that reccod() could add to packet at krmscb->packet) */

#define KMARK '\001'                        /* mark field of Kermit packets */
#define KHEAD 4            /* number of header characters in Kermit packets */
#define CXMAX 5                  /* number of control-X's to abort transfer */
#define CTRLX '\030'            /* manual Control-X's abort Kermit transfer */
#define KRTEND 2                  /* Number of retries for final 'B' packet */

#define CAPATT 0x08           /* CAPAS field: can accept attribute packets? */
#define CAPWIN 0x04              /* CAPAS field: can handle sliding windows */

/* timeouts (timer ticks) */

#define KABWAIT (1*16)      /* seconds to pause after kermit transfer abort */
#define KDSWAIT (5*16)   /* timeout retry for init transmit (infin retries) */
#define KPAWAIT (5*16)    /* seconds of patience for 'E' packet to transmit */
#define KUSWAIT (5*16)    /* timeout retry for init receive (infin retries) */
#define KWEND   (5*16)       /* timeout at end waiting for output to finish */
#define KNQUIET     8     /* 1/2 sec silence timeout after "noise" b4 retry */

CHAR *wscan;              /* received packet scan for kmwpak() and dechar() */
CHAR *atrptr;               /* file attribute encode pointer (for encatr()) */
CHAR *capptr;         /* global return of inieat(), points after CAPAS bits */
CHAR rejtyp;            /* global return of kmdrpl() - rejected packet type */
static CHAR initst[]="~* @-#Y3~,";   /* 'S' init stg, or 'Y' reply init stg */
static CHAR defini[]="~  @-#N1  "; /* defaults for incoming S/Y init string */


/*--- KERMIT utilities ---*/

static VOID
kmchk(                      /* compute and store checksum for kermit packet */
CHAR chktyp,                                            /* '1', '2', or '3' */
CHAR *data)                           /* len-seq-type-data fields of packet */
      /* appends 1-3 character checksum onto end of data[] array, by XORing */
        /* for transmit, zero first; for receive, check for zero afterwards */
{
     CHAR sum;                                         /* 1-character check */
     UINT isum;                                        /* 2-character check */
     UINT c,q,ucrc;                                    /* 3-character check */
     LONG crc,factor;
     CHAR *pchk;                      /* pointer to where check gets stored */
     INT ckslen;               /* number of bytes to which checksum applies */

     ckslen=data[0]-' '-(chktyp-'0')+1;
     ckslen=max(0,ckslen);
     pchk=data+ckslen;
     switch(chktyp) {
     case '1':
          sum=0;
          while (ckslen-- > 0) {
            sum+=*data++;
          }
          pchk[0]^=((sum+((sum&0xC0)>>6))&0x3F)+' ';
          break;
     case '2':
          isum=0;
          while (ckslen-- > 0) {
               isum+=*data++;
          }
          pchk[0]^=((isum>>6)&0x3F)+' ';
          pchk[1]^=(isum&0x3F)+' ';
          break;
     case '3':
          crc=0L;
          while (ckslen-- > 0) {
               c=*data++;
               q=(UINT)((crc^c)&017);
               factor=(q*010201);
               factor=factor&0x0000FFFFL;  /* avd -B-U-G- in Borland C++ v3 */
               crc=(crc>>4)^factor;
               c>>=4;     /* c pre-shifted to aVOID -B-U-G- in Turbo-C "-Z" */
               q=(UINT)((crc^c)&017);
               factor=(q*010201);
               factor=factor&0x0000FFFFL;  /* avd -B-U-G- in Borland C++ v3 */
               crc=(crc>>4)^factor;
          }
          ucrc=(UINT)crc;
          pchk[0]^=((ucrc>>12)&0x0F)+' ';
          pchk[1]^=((ucrc>>6)&0x3F)+' ';
          pchk[2]^=(ucrc&0x3F)+' ';
          break;
     }
}

static CHAR
ibgood(                              /* initialization prefix byte is good? */
CHAR pfx)                                                    /* prefix byte */
                                              /* returns 0=bad, or pfx=good */
{
     if ((pfx < 33 || pfx > 62) && (pfx < 96 || pfx > 126)) {
          pfx=0;
     }
     return(pfx);
}

static VOID
inieat(VOID)                  /* process other-party's incoming init string */
                 /* capptr is global return value - points after CAPAS bits */
{
     INT n;
     CHAR ch;

     if ((n=strlen(krmscb->inpack+KHEAD)) < strlen(defini)) {
          strcpy(krmscb->inpack+KHEAD+n,defini+n);
     }
     krmscb->maxl=min(KPKLEN,krmscb->inpack[KHEAD+0]-' ');
     if ((ch=krmscb->inpack[KHEAD+1]) <= ' ') {
          krmscb->time=65535U;
     }
     else {
          krmscb->time=((ch-' ')<<4);
     }
     krmscb->npad=krmscb->inpack[KHEAD+2]-' ';
     krmscb->padc=krmscb->inpack[KHEAD+3]^64;
     krmscb->eol=krmscb->inpack[KHEAD+4]-' ';
     krmscb->qctl=krmscb->inpack[KHEAD+5];
     krmscb->qbin=ibgood(krmscb->inpack[KHEAD+6]);
     if ((krmscb->chkt=krmscb->inpack[KHEAD+7]) < '1' || krmscb->chkt > '3') {
          krmscb->chkt='1';
     }
     krmscb->rept=ibgood(krmscb->inpack[KHEAD+8]);
     krmscb->capas=krmscb->inpack[KHEAD+9]-' ';
     for (capptr=krmscb->inpack+KHEAD+9 ; (*capptr&1) != 0 ; capptr++) {
     }                                                  /* skip capas bytes */
     if (!(krmscb->capas&CAPWIN) || (krmscb->windo=capptr[1]-' ') < 1) {
          krmscb->windo=1;
     }
}

static VOID
seqnxt(VOID)                                /* compute next sequence number */
{
     krmscb->seq=(krmscb->seq+1)&63;
}


/*--- KERMIT packet transmit routines ---*/

static VOID
kmxpak(                               /* transmit a formatted kermit packet */
CHAR type,                             /* type of packet ('S','F','D',etc.) */
CHAR seq,                                           /* sequence code (0-63) */
CHAR chktyp)                              /* type of checksum ('1','2','3') */
                               /* data field starts at krmscb->packet+KHEAD */
                                        /* and is krmscb->numdat bytes long */
{
     INT i;
     CHAR *cp;

     for (i=0 ; i < krmscb->npad ; i++) {
          ftfout(&krmscb->padc,1);
     }
     krmscb->packet[0]=KMARK;
     krmscb->packet[1]=2+krmscb->numdat+(chktyp-'0')+' ';
     krmscb->packet[2]=seq+' ';
     krmscb->packet[3]=type;
     setmem(cp=krmscb->packet+KHEAD+krmscb->numdat,chktyp-'0',0);
     kmchk(chktyp,krmscb->packet+1);
     cp[chktyp-'0']=krmscb->eol;
     ftfout(krmscb->packet,KHEAD+krmscb->numdat+chktyp-'0'+1);
     krmscb->flags&=~KNOISE;
}

static VOID
eccod(        /* Eight-bit/Control encoding for transmitting Kermit packets */
CHAR khar)                             /* max encoded length:  3 characters */
{
     CHAR ctlpfx=0;
     CHAR khar7;

     khar7=khar&0x7F;
     if ((khar&0x60) == 0 || khar7 == 127) {
          ctlpfx='#';
          khar^=0x40;
     }
     else if (khar7 == '#' ||
              (khar7 == krmscb->qbin && krmscb->qbin != 0) ||
              (khar7 == krmscb->rept && krmscb->rept != 0)) {
          ctlpfx='#';
     }
     if ((khar&0x80) && krmscb->qbin) {
          krmscb->packet[KHEAD+krmscb->numdat++]=krmscb->qbin;
          khar&=0x7F;
     }
     if (ctlpfx) {
          krmscb->packet[KHEAD+krmscb->numdat++]=ctlpfx;
     }
     krmscb->packet[KHEAD+krmscb->numdat++]=khar;
}

static INT
reccod(VOID)  /* RLE/Eighth-bit/Control encoding for xmiting Kermit packets */
                              /* max encoded length:  6 characters (KPROOM) */
/* returns number of data bytes encoded (may differ from bytes transmitted) */
{
     INT khar,nextch;
     INT runlen;

     if (krmscb->flags&KDUNGOT) {
          khar=krmscb->ungot;
          krmscb->flags&=~KDUNGOT;
     }
     else if ((khar=ftfxrb()) == EOF) {
          return(0);
     }
     if (krmscb->rept == 0) {
          eccod(khar);
          return(1);
     }
     for (runlen=1 ; (nextch=ftfxrb()) == khar && runlen < 94 ; runlen++) {
     }
     if (nextch != EOF) {
          krmscb->ungot=nextch;
          krmscb->flags|=KDUNGOT;
     }
     switch(runlen) {
     case 3:
          eccod(khar);
     case 2:
          eccod(khar);
     case 1:
          eccod(khar);
          break;
     default:
          krmscb->packet[KHEAD+krmscb->numdat++]=krmscb->rept;
          krmscb->packet[KHEAD+krmscb->numdat++]=runlen+' ';
          eccod(khar);
          break;
     }
     return(runlen);
}

static INT
kmfpak(VOID)                /* format a data packet for kermit transmission */
{               /* encodes data from file into data field of krmscb->packet */
     INT numenc=0;                       /* returns number of bytes encoded */
     INT n;

     krmscb->numdat=0;
     while (krmscb->numdat <= -2+krmscb->maxl-(krmscb->chkt-'0')-KPROOM) {
          if ((n=reccod()) == 0) {
               krmscb->flags|=KDFEOF;
               break;
          }
          numenc+=n;
     }
     return(numenc);
}

static VOID
kdabaut(            /* abort transmit, notify user's program via 'E' packet */
CHAR *message)
{
     strcpy(krmscb->packet+KHEAD,message);
     krmscb->numdat=strlen(message);
     kmxpak('E',krmscb->seq,krmscb->chkt);
     ftfabt(message);
}


/*--- KERMIT packet receive routines ---*/

static INT
kmrck(              /* check on received packet (st with KMARK) in inpack[] */
INT chktyp)                                     /* check type ('1','2','3') */
{                  /* returns 1=packet OK (& checksum byte(s) become '\0's) */
               /* checks that length field in range and checksum is correct */
     INT len;
     CHAR *cp;

     len=krmscb->inpack[1]-' ';
     if (len < 0 || KPKLEN < len) {
          return(0);
     }
     kmchk(chktyp,krmscb->inpack+1);
     for (cp=krmscb->inpack+2+len ; chktyp > '0' ; chktyp--) {
          if (*--cp != 0) {
               return(0);
          }
     }
     return(1);
}

static CHAR
dechar(VOID)                          /* decode received character sequence */
{                                         /* handles QBIN and QCTL prefixes */
     CHAR orf=0,andf=0xFF;
     CHAR khar;

     if (krmscb->qbin) {
          andf=0x7F;
          if (*wscan == krmscb->qbin) {
               wscan++;
               orf=0x80;
          }
     }
     if ((khar=*wscan++) == krmscb->qctl) {
          if (!ibgood((khar=*wscan++)&0x7F)) {
               khar^=0x40;
          }
     }
     return((khar&andf)|orf);
}

static INT
kmwpak(                                    /* write received packet to disk */
CHAR *packet,                              /* formated data field of packet */
INT nchars)                                        /* # bytes in data field */
{                                                 /* returns 1=good 0=error */
     INT nrept;
     CHAR khar;
     CHAR *wsend;

     for (wscan=packet,wsend=packet+nchars ; wscan < wsend ; ) {
          if (*wscan == krmscb->rept) {
               wscan++;
               nrept=(*wscan++)-' ';
               khar=dechar();
               while (nrept-- > 0) {
                    if (ftfrwb(khar) == 0) {
                         kdabaut("Disk full.");
                         return(0);
                    }
               }
               ftfscb->actbyt+=nrept;
          }
          else {
               if (ftfrwb(dechar()) == 0) {
                    kdabaut("Disk full.");
                    return(0);
               }
               ftfscb->actbyt++;
          }
     }
     if (!ftfcbl()) {
          kdabaut(ftfscb->abwhy);
          return(0);
     }
     ftfnwp();
     return(1);
}


/*--- KERMIT transmit routines ---*/

static VOID
kmxini(VOID)                               /* Initialize KERMIT transmitter */
{
     ftpkmx.scblen=sizeof(struct krmdat)+(INT)ftpkmx.window*KDWSIZ;
}

static VOID
kdwini(VOID)                /* initialize Kermit transmit window structures */
{
     INT i;

     for (i=0 ; i < krmscb->windo ; i++) {
          kdwtmp[i].chances=-1;
     }
}

static VOID
kdfnxt(VOID)                    /* check for end of file, load up next file */
{
     INT i;

     if ((krmscb->flags&KDFEOF) &&            /* wait until no more of file */
         ftfoba() == ftfomt) {                 /* (wait until output empty) */
          for (i=0 ; i < krmscb->windo ; i++) {
               if (kdwtmp[i].chances > 0) {
                    return;
               }
          }                          /* wait until all packets acknowledged */
          krmscb->numdat=0;
          kmxpak('Z',krmscb->seq,krmscb->chkt);          /* then send Z-eof */
          krmscb->retry=ftfpsp->retrys;
          ftfnew(KDEOF);
     }
}

static VOID
knewout(                                              /* transmit new packet */
INT index)                        /* available entry in window array to use */
{
     LONG fpos;

     fpos=ftfscb->actbyt;
     ftfscb->actbyt+=kmfpak();                 /* format a brand new packet */
     ftfnwp();
     if (krmscb->numdat > 0) {          /* if any bytes to transmit at all: */
          kdwtmp[index].chances=ftfpsp->retrys;                   /* record */
          kdwtmp[index].fpos=fpos;                                /* window */
          kdwtmp[index].seq=krmscb->seq;                            /* info */
          kmxpak('D',krmscb->seq,krmscb->chkt);              /* send packet */
          seqnxt();
          ftfnew(KDDATA);
     }
}

static VOID
kdwnxt(VOID)                      /* transmit new packet, if room in window */
{
     INT i;                               /* scan through packets in window */
     CHAR ago;                        /* how long ago was this packet sent? */
     CHAR seqago;                   /* how long ago was OLDEST packet sent? */
     INT ilow;                                    /* index of oldest packet */

     if ((krmscb->flags&KDFEOF) ||                 /* check for end of file */
         ftfoba() < krmscb->npad+2+KPKLEN+1) {  /* ck room in output buffer */
          return;
     }
     seqago=0;
     for (i=0 ; i < krmscb->windo ; i++) {
          if (kdwtmp[i].chances == -1) {    /* use never-used window entry: */
               knewout(i);
               return;
          }
          if ((ago=(krmscb->seq-kdwtmp[i].seq)&63) > seqago) {
               seqago=ago;
               ilow=i;
          }
     }
     if (seqago > 0 && kdwtmp[ilow].chances == 0) {    /* use oldest window */
          knewout(ilow);                      /* entry, if it has been ACK'd */
     }
}

static VOID
kdwrty(             /* retry sending a packet fm inside the transmit window */
INT index)                                     /* index into kdwtmp[] array */
{
     if (kdwtmp[index].chances == 0) {
          return;                             /* ignore NAK of ACK'd packet */
     }
     if (--kdwtmp[index].chances == 0) {
          kdabaut("Too many retries.");
          return;
     }
     ftfxsk(kdwtmp[index].fpos);
     krmscb->flags&=~KDUNGOT;
     kmfpak();
     kmxpak('D',kdwtmp[index].seq,krmscb->chkt);
     ftfnew(KDDATA);
     ftfxsk(ftfscb->actbyt);
     krmscb->flags&=~KDUNGOT;
}

static INT
kdoldest(VOID)              /* find oldest unack'd entry in transmit window */
                                                      /* returns -1 if none */
{
     CHAR ago;                        /* how long ago was this packet sent? */
     CHAR seqago=0;                 /* how long ago was OLDEST packet sent? */
     INT i,ilow=-1;                               /* index of oldest packet */

     for (i=0 ; i < krmscb->windo ; i++) {
          if (kdwtmp[i].chances > 0 &&
              (ago=(krmscb->seq-kdwtmp[i].seq)&63) > seqago) {
               seqago=ago;
               ilow=i;
          }
     }
     return(ilow);
}

static VOID
kdgo(VOID)                               /* begin Kermit transmit of a file */
{
     strcpy(krmscb->packet+KHEAD,ftfscb->fname);
     krmscb->numdat=strlen(krmscb->packet+KHEAD);
     kmxpak('F',krmscb->seq,krmscb->chkt);
     krmscb->retry=ftfpsp->retrys;
     krmscb->flags&=~(KDFEOF+KDUNGOT);
     ftfnew(KDFILE);
}

static VOID
kdstop(VOID)                              /* end Kermit transmit of file(s) */
{
     krmscb->numdat=0;
     kmxpak('B',krmscb->seq,krmscb->chkt);
     krmscb->retry=KRTEND;
     ftfnew(KDFIN);
}

static VOID
knext(VOID)                            /* find next Kermit file to transmit */
{
     if (ftfxop() == 0) {
          ftfsrt();
          kdgo();
     }
     else {
          kdstop();
     }
}

static VOID
khsack(VOID)         /* handle received acknowledge to Kermit Start message */
{
     inieat();
     if (krmscb->windo > ftfscb->window) {
          krmscb->windo=(INT)ftfscb->window;
     }
}

static VOID
seqlst(VOID)                            /* back up the sequence number once */
{
     krmscb->seq=(krmscb->seq-1)&63;
}

static VOID
kretry(                                           /* retry sending a packet */
CHAR pktype)
{
     if (krmscb->retry-- == 0) {
          kdabaut("Too many retries.");
     }
     else {
          kmxpak(pktype,krmscb->seq,krmscb->chkt);
          ftfscb->tckstt=ftftck;
     }
}

static VOID
encatr(                                /* encode file attribute at "atrptr" */
INT type,                                        /* type code for attribute */
CHAR *data)                                          /* value for attribute */
{
     strcpy(atrptr+2,data);
     atrptr[0]=type;
     atrptr[1]=strlen(data)+' ';
     atrptr+=2+strlen(data);
}

static VOID
sndatrs(VOID)               /* transmit file attributes in attribute packet */
{
     CHAR buff[40];
     USHORT dat,tim;

     atrptr=krmscb->packet+KHEAD;
     encatr('1',spr("%ld",ftfscb->estbyt));
     dat=ftfscb->dosdat;
     tim=ftfscb->dostim;
     sprintf(buff,"%04d%02d%02d %02d:%02d:%02d",ddyear(dat),
          ddmon(dat),ddday(dat),dthour(tim),dtmin(tim),dtsec(tim));
     encatr('#',buff);
     krmscb->numdat=strlen(krmscb->packet+KHEAD);
     kmxpak('A',krmscb->seq,krmscb->chkt);
}

static INT
kmdrpl(                       /* is packet of proper type, seq, & checksum? */
CHAR pktype,                                  /* expecting this packet type */
CHAR chkt)                                /* using this type of check field */
                                     /* returns 1 if good chk & seq & type  */
            /* if so, packet (sans check field) is stored at krmscb->inpack */
                   /* strlen(krmscb->inpack+KHEAD) is length of data field, */
                                        /* starting at krmscb->inpack+KHEAD */
                 /* if packet rejected due only to wrong type or wrong seq, */
         /* then global "rejtyp" is set to rejected type (else rejtyp == 0) */
{
     rejtyp=0;
     if (!kmrck(chkt)) {
          krmscb->flags|=KNOISE;          /* (bad cksum makes us impatient) */
     }
     else if (krmscb->inpack[2] != krmscb->seq+' '              /* bad seq? */
           || krmscb->inpack[3] != pktype) {             /* or bad type? */
          krmscb->flags|=KNOISE;                  /* (then reply very soon) */
          rejtyp=krmscb->inpack[3]; /* or maybe there's another valid type? */
     }
     else {
          return(1);                                        /* good packet! */
     }
     return(0);
}

static VOID
kmxsrt(VOID)                               /* Begin KERMIT transmit session */
{
     setmem(krmscb,ftfpsp->scblen,0);
     ftfscb->paksiz=ftfpsp->paksiz;
     ftfscb->window=ftfpsp->window;
     if (ftfxop() == 0) {
          ftfsrt();
          ftfnew(KDINIT);
     }
     else {
          ftfnew(KDEND);
     }
}

static INT
waiting(                 /* waiting too long?  or quiet after recent noise? */
INT ticks)
{
     if (ftftck-ftfscb->tckstt > ticks
      || (krmscb->flags&KNOISE && ftftck-ftfscb->tckbyt > KNQUIET)) {
          krmscb->flags&=~KNOISE;
          ftfscb->tckbyt=ftftck;
          krmscb->inplen=0;                    /* start krminc() over again */
          if (krmscb->xcnt > 0) {
               krmscb->xcnt--;         /* decay Ctrl-X (op-abort) detection */
          }
          ftfnew(ftfscb->state);
          return(1);
     }
     return (0);
}

static INT
kmxctn(VOID)                         /* continue processing KERMIT transmit */
{                                        /* returns 1=still working, 0=done */
     INT i;

     switch(ftfscb->state) {                   /* constant service tasks... */
     case KDINIT:
          strcpy(krmscb->packet+KHEAD,initst);
          krmscb->packet[KHEAD+1]=ftfpsp->paktmo+' ';
          krmscb->packet[KHEAD+10]=(CHAR)(ftfscb->window+' ');
          krmscb->numdat=11;       /* includes 1 CAPAS byte plus WINDO byte */
          krmscb->eol='\r';
          kmxpak('S',krmscb->seq,'1');
          ftfnew(KDSTART);
          break;
     case KDSTART:
          if (waiting(KDSWAIT)) {
               kmxpak('S',krmscb->seq,'1');
          }
          break;
     case KDFILE:
          if (waiting(krmscb->time)) {
               kretry('F');
          }
          break;
     case KDATTR:
          if (waiting(krmscb->time)) {
               kretry('A');
          }
          break;
     case KDDATA:
          if (waiting(krmscb->time)) {
               if ((i=kdoldest()) >= 0) {        /* use oldest window entry */
                    kdwrty(i);                    /* that hasn't been ACK'd */
               }
          }
          else {
               kdwnxt();   /* if nothing else to do, chk for room in window */
               kdfnxt();         /* also check for end of file transmission */
          }
          break;
     case KDFSKIP:
     case KDFTERM:
     case KDEOF:
          if (waiting(krmscb->time)) {
               kretry('Z');
          }
          break;
     case KDFIN:
          if (waiting(krmscb->time)) {
               if (krmscb->retry == 0) {
                    ftfnew(KDEND);                        /* (give up easy) */
               }
               else {
                    kretry('B');
               }
          }
          break;
     case FTFABORT:
          ftfcli();
          if (ftfscb->isopen) {
               ftfxcl(0);
               ftfscb->isopen=0;
          }
          ftfnew(FTFABWAIT);
          break;
     case FTFABWAIT:
          if (ftftck-ftfscb->tckstt > KABWAIT
           && (ftftck-ftfscb->tckstt > KPAWAIT || ftfoba() == ftfomt)) {
               ftfclo();
               return(-1);
          }
          break;
     case KDEND:
          if (ftftck-ftfscb->tckstt > KWEND || ftfoba() == ftfomt) {
               return(0);
          }
          break;
     }
     return(1);
}


/*--- KERMIT receive routines ---*/

static VOID
kmrini(VOID)                                  /* Initialize KERMIT receiver */
{
     ftpkmr.scblen=sizeof(struct krmdat)+(INT)ftpkmr.window*KUWSIZ;
}

static VOID
kmrsrt(VOID)                                /* Begin KERMIT receive session */
{
     setmem(krmscb,ftfpsp->scblen,0);
     ftfscb->paksiz=ftfpsp->paksiz;
     ftfscb->window=ftfpsp->window;
     krmscb->eol='\r';
     ftfstf();
     ftfnew(KUSTART);
}

static VOID
khattr(VOID)                           /* handle attribute of incoming file */
{
     CHAR *ap;
     LONG year;
     INT hh,mm,ss;
     INT mn,dd;
     CHAR buff[41];                 /* buffer for decoding attribute fields */
     INT len;

     for (ap=krmscb->inpack+KHEAD ; *ap != '\0' ; ap+=ap[1]-' '+2) {
          len=min(ap[1]-' ',40);
          movmem(ap+2,buff,len);
          buff[len]='\0';
          switch(*ap) {
          case '#':
               year=19800000L;
               hh=mm=ss=0;
               sscanf(buff,"%ld %d:%d:%d",&year,&hh,&mm,&ss);
               if (year < 19000000L) year+=19000000L;
               if (year < 19800000L) year+= 1000000L;
               dd=(INT)(year%100L);
               year/=100L;
               mn=(INT)(year%100L);
               year/=100L;
               ftfscb->dosdat=dddate(mn,dd,(USHORT)year);
               ftfscb->dostim=dttime(hh,mm,ss);
               break;
          case '!':
               sscanf(buff,"%ld",&ftfscb->estbyt);
               ftfscb->estbyt<<=10;
               ftfscb->estbyt+=512;
               break;
          case '1':
               sscanf(buff,"%ld",&ftfscb->estbyt);
               break;
          }
     }
}

static VOID
kuwini(VOID)
{
     INT i;

     for (i=0 ; i < krmscb->windo ; i++) {
          kuwtmp[i].chances=-1;
     }
}

static VOID
kuwrty(       /* retry sending a packet from inside the receive window */
INT index)                                     /* index into kuwtmp[] array */
{
     if (kuwtmp[index].chances == 0) {
          return;                              /* don't NAK an ACK'd packet */
     }
     if (--kuwtmp[index].chances == 0) {           /* give him one more try */
          kdabaut("Too many retries.");
          return;
     }
     krmscb->numdat=0;
     kmxpak('N',kuwtmp[index].seq,krmscb->chkt);                     /* NAK */
     ftfnew(KUDATA);
}

static INT
kuoldest(VOID)               /* find oldest unack'd entry in receive window */
                                                      /* returns -1 if none */
{
     CHAR ago;                        /* how long ago was this packet sent? */
     CHAR seqago=0;                 /* how long ago was OLDEST packet sent? */
     INT i,ilow=-1;                               /* index of oldest packet */

     for (i=0 ; i < krmscb->windo ; i++) {
          if (kuwtmp[i].chances > 0 &&
              (ago=(krmscb->seq-kuwtmp[i].seq)&63) >= seqago) {
               seqago=ago;
               ilow=i;
          }
     }
     return(ilow);
}

static VOID
knewin(                        /* store new incoming packet in window table */
INT index)                              /* index in table, 0..krmscb->windo */
{
     movmem(krmscb->inpack+KHEAD,kuwtmp[index].packet,
            kuwtmp[index].numdat=min(KPKLEN,strlen(krmscb->inpack+KHEAD)));
     kuwtmp[index].chances=0;
     kuwtmp[index].seq=krmscb->inpack[2]-' ';
     ftfnew(KUDATA);
}

static INT
kuwdump(VOID)                  /* dump an entry in the receive window table */
                         /* returns index of now free entry, or -1 if error */
{
     INT i,ilow=-1;
     CHAR ago,seqago=0;

     for (i=0 ; i < krmscb->windo ; i++) {
          switch(kuwtmp[i].chances) {
          case -1:                       /* use never-used window entry: */
               return(i);
          case 0:                         /* (or use oldest ACK'd entry) */
          default:                  /* (oops, check for oldest NAK'd entry) */
               if ((ago=(krmscb->seq-kuwtmp[i].seq)&63) > seqago) {
                    seqago=ago;
                    ilow=i;
               }
               break;
          }
     }
     if (ilow >= 0 && kuwtmp[ilow].chances == 0) {     /* use oldest window */
          if (!kmwpak(kuwtmp[ilow].packet,         /* entry, if it has been */
                      kuwtmp[ilow].numdat)) {      /* ACK'd, and write this */
               return(-1);                            /* old record to disk */
          }
          kuwtmp[ilow].chances=-1;
          return(ilow);                    /* and store the NEW record here */
     }
     else {
          kdabaut("Sender busted its window.");
          return(-1);
     }
}

static INT
kuwmty(VOID)           /* transmitter sent an EOF, empty the receive window */
                  /* returns 1=window empty, 0=still have packets, -1=error */
{
     INT i;
     CHAR ago,seqago=0;
     INT ilow;

     for (i=0 ; i < krmscb->windo ; i++) {
          switch(kuwtmp[i].chances) {
          case -1:
               break;
          case 0:                        /* looking for oldest ACK'd packet */
               if ((ago=(krmscb->seq-kuwtmp[i].seq)&63) > seqago) {
                    seqago=ago;
                    ilow=i;
               }
               break;
          default:             /* any packets still not ACK'd? total bedlam */
               kdabaut("Sender declared EOF before all packets acknowledged.");
               return(-1);
          }
     }
     if (seqago > 0 && kuwtmp[ilow].chances == 0) {    /* use oldest window */
          if (!kmwpak(kuwtmp[ilow].packet,kuwtmp[ilow].numdat)) {
               return(-1);
          }
          kuwtmp[ilow].chances=-1;
          return(0);
     }
     return(1);
}

static INT
kuwnxt(VOID)                           /* handle a new incoming data packet */
{                           /* returns 1=handled, ACK it, 0=error or ignore */
     CHAR future,oldseq;
     INT i,j;

     for (i=0 ; i < krmscb->windo ; i++) {
          if (kuwtmp[i].seq == krmscb->inpack[2]-' ') {
               switch (kuwtmp[i].chances) {
               case 0:
                    return(1);                       /* ACK this old packet */
               case -1:
                    break;                  /* (ignore unused window entry) */
               default:
                    knewin(i);            /* accept previously NAK'd packet */
                    return(1);
               }
          }
     }
     if ((future=(krmscb->inpack[2]-' '-krmscb->seq)&63) <= krmscb->windo-1) {
          oldseq=krmscb->seq;
          krmscb->seq=krmscb->inpack[2]-' '; /* jump to this new seq number */
          seqnxt();                     /* (then expect the one after that) */
          for (i=0 ; i < future ; i++) {          /* NAK any missed packets */
               if ((j=kuwdump()) < 0) {
                    return(0);                      /* (window error abort) */
               }
               kuwtmp[j].chances=ftfpsp->retrys;
               krmscb->numdat=0;
               kmxpak('N',kuwtmp[j].seq=(oldseq+i)&63,krmscb->chkt);
          }
          if ((j=kuwdump()) < 0) {
               return(0);                           /* (window error abort) */
          }
          knewin(j);                         /* ACK the new received packet */
          return(1);
     }
     return(0);                  /* ignore anything outside receiver window */
}

static INT
seqchk(VOID)                                    /* check on sequence number */
{
     if (krmscb->inpack[2] != krmscb->seq+' ') {                /* bad seq? */
          krmscb->flags|=KNOISE;              /* then force immediate reply */
          return(0);
     }
     return(1);
}

static VOID
ansold(VOID)    /* answer an old left over packet, that xmitter is retrying */
{
     seqlst();
     if (seqchk()) {
          kretry('Y');                    /* yes, yes, we heard you already */
     }
     seqnxt();
}

static INT
kmrctn(VOID)                          /* continue processing KERMIT receive */
{                                        /* returns 1=still working, 0=done */
     INT i;

     switch(ftfscb->state) {
     case KUSTART:
          if (waiting(KUSWAIT)) {
               krmscb->numdat=0;
               kmxpak('N',krmscb->seq,'1');
          }
          break;
     case KUFILE:
          if (waiting(krmscb->time)) {
               kretry('N');
          }
          break;
     case KUDATA:
          if (waiting(krmscb->time)) {
               if ((i=kuoldest()) >= 0) {        /* use oldest window entry */
                    kuwrty(i);                    /* that hasn't been ACK'd */
               }
               else {                                         /* or if none */
                    kretry('N');            /* ask for next expected packet */
               }
          }
          break;
     case FTFABORT:
          if (ftfscb->tckstt-ftftck)
          ftfcli();
          if (ftfscb->isopen) {
               ftfrcl(0);
               ftfscb->isopen=0;
          }
          ftfnew(FTFABWAIT);
          break;
     case FTFABWAIT:
          if (ftftck-ftfscb->tckstt > KABWAIT
          && (ftftck-ftfscb->tckstt > KPAWAIT || ftfoba() == ftfomt)) {
               ftfclo();
               return(-1);
          }
          break;
     case KUEND:
          if (ftftck-ftfscb->tckstt > KWEND || ftfoba() == ftfomt) {
               return(0);
          }
          break;
     }
     return(1);
}

/*--- KERMIT character input handling ---*/

static VOID
hdldpk(VOID)                                 /* handle received data packet */
{
     if (kuwnxt()) {                               /* was what we got good? */
          krmscb->numdat=0;                         /* then ACK whatever it */
          kmxpak('Y',krmscb->inpack[2]-' ',krmscb->chkt);     /* was we got */
          krmscb->retry=ftfpsp->retrys;
     }
}

static VOID
hdlzpk(VOID)                       /* handle misc received packets, Z, F, A */
{
     INT rc;

     if (krmscb->inpack[KHEAD] == 'D') {    /* Z(D) - discard received file */
          ftfrcl(0);
     }
     else {                      /* 'Z' packet rcvd, prepare to log in file */
          while ((rc=kuwmty()) == 0) {
          }
          if (rc == -1) {
               return;
          }
          ftfrcl(1);
          ftfscb->actfil++;
     }
     ftfscb->isopen=0;
     krmscb->numdat=0;
     ftfcli();
     kmxpak('Y',krmscb->seq,krmscb->chkt);
     seqnxt();
     krmscb->retry=ftfpsp->retrys;
     ftfnew(KUFILE);
}

static VOID
hdlrpk(VOID)                                      /* handle received packet */
{
     CHAR c;
     INT i;

     if (krmscb->inpack[3] == 'E' && kmrck(krmscb->chkt)) {
          if (ftfscb->state >= 0) {
               ftfabt(krmscb->inpack+KHEAD);
          }              /* ignore Telix's blank 'E' reply to an 'E' packet */
          return;
     }
     switch(ftfscb->state) {
     case KDSTART:                      /* expect ack to Start(init) string */
          if (kmdrpl('Y','1')) {
               seqnxt();
               khsack();
               kdgo();                                    /* send file name */
          }
          break;
     case KDFILE:                                /* expect ack to file name */
          if (kmdrpl('Y',krmscb->chkt)) {
               seqnxt();
               if (krmscb->capas&CAPATT) {                   /* if capable: */
                    sndatrs();                                /* send attrs */
                    krmscb->retry=ftfpsp->retrys;
                    ftfnew(KDATTR);
               }
               else {                                         /* otherwise: */
                    kdwini();
                    ftfnew(KDDATA);
                    kdwnxt();                   /* transmit 1st data packet */
               }
          }
          break;
     case KDATTR:                          /* expect ack to file attributes */
          if (kmdrpl('Y',krmscb->chkt)) {
               seqnxt();
               kdwini();
               ftfnew(KDDATA);
               kdwnxt();                        /* transmit 1st data packet */
          }
          break;
     case KDDATA:
          if (kmrck(krmscb->chkt)) {
               if (krmscb->inpack[3] == 'Y') {                  /* got ACK, */
                    if ((c=krmscb->inpack[KHEAD]) == KDFSKIP || c == KDFTERM) {
                         krmscb->numdat=1;            /* skip or terminate? */
                         krmscb->packet[KHEAD]='D';        /* then Z(D) for */
                         kmxpak('Z',krmscb->seq,krmscb->chkt);   /* discard */
                         krmscb->retry=ftfpsp->retrys;
                         ftfnew(c);
                         break;
                    }                                    /* no, normal ACK: */
                    for (i=0 ; i < krmscb->windo ; i++) {
                         if (kdwtmp[i].seq == krmscb->inpack[2]-' ') {
                              kdwtmp[i].chances=0;             /* in window */
                              kdwnxt();              /* so mark and proceed */
                              break;
                         }
                    }                          /* ignore ACK outside window */
               }
               else if (krmscb->inpack[3] == 'N') {              /* got NAK */
                    for (i=0 ; i < krmscb->windo ; i++) {
                         if (kdwtmp[i].seq == krmscb->inpack[2]-' ') {
                              kdwrty(i);                       /* in window */
                              break;                            /* so retry */
                         }
                    }                          /* ignore NAK outside window */
               }
          }
          else {
               krmscb->flags|=KNOISE;           /* trash makes us impatient */
          }
          break;
     case KDEOF:                      /* transmitted Z eof, waiting for ACK */
          if (kmdrpl('Y',krmscb->chkt)) {
               seqnxt();
               ftfxcl(1);
               ftfscb->isopen=0;
               ftfscb->actfil++;
               knext();
          }
          break;
     case KDFSKIP:   /* got ACK(X), xmitted Z eof (D=discard), look for ACK */
          if (kmdrpl('Y',krmscb->chkt)) {
               seqnxt();
               ftfxsq("file skipped by receiver");
               ftfxcl(0);
               ftfscb->isopen=0;
               knext();
          }
          break;
     case KDFTERM:   /* got ACK(Z), xmitted Z eof (D=discard), look for ACK */
          if (kmdrpl('Y',krmscb->chkt)) {
               seqnxt();
               ftfxsq("session terminated by receiver");
               ftfxcl(0);
               ftfscb->isopen=0;
               kdstop();
          }
          break;
     case KDFIN: /* transmitted B terminator, waiting a short while for ACK */
          if (kmdrpl('Y',krmscb->chkt)) {
               ftfnew(KDEND);
          }
          break;
     case KUSTART:
          if (kmdrpl('S','1')) {
               inieat();
               if (krmscb->windo > ftfscb->window) {
                    krmscb->windo=(INT)ftfscb->window;
               }
               strcpy(krmscb->packet+KHEAD,initst);       /* chkt reflected */
               krmscb->packet[KHEAD+7]=krmscb->chkt;          /* ala PCPlus */
               krmscb->packet[KHEAD+10]=(CHAR)(ftfscb->window+' ');
               krmscb->numdat=11;  /* includes 1 CAPAS byte plus WINDO byte */
               ftfcli();
               kmxpak('Y',krmscb->seq,'1');
               seqnxt();
               krmscb->retry=ftfpsp->retrys;
               ftfnew(KUFILE);
          }
          break;
     case KUFILE:                   /* waiting for incoming 'F' file packet */
          if (krmscb->inpack[3] == 'S') {       /* retry of 'S' packet, eh? */
               if (kmrck('1')) {
                    seqlst();
                    if (krmscb->retry-- == 0) {
                         kdabaut("Too many retries.");
                    }
                    else {                /* yes, yes, we heard you already */
                         ftfcli();
                         kmxpak('Y',krmscb->seq,'1');
                    }                           /* retry Y with init string */
                    seqnxt();
               }
               else {
                    krmscb->flags|=KNOISE;       /* bad cksum => impatience */
               }
          }
          else if (!kmrck(krmscb->chkt)) {
               krmscb->flags|=KNOISE;       /* bad cksum makes us impatient */
          }
          else {
               switch(krmscb->inpack[3]) {
               case 'F':                          /* here's that 'F' packet */
                    if (seqchk()) {
                         stzcpy(ftfscb->fname,krmscb->inpack+KHEAD,8+1+3+1);
                         krmscb->numdat=0;
                         ftfcli();
                         kmxpak('Y',krmscb->seq,krmscb->chkt);
                         seqnxt();
                         krmscb->retry=ftfpsp->retrys;
                         ftfscb->dosdat=0;
                         ftfscb->dostim=0;
                         ftfscb->estbyt=0L;
                         ftfnew(KUATTR);
                    }
                    break;
               case 'B':                  /* 'B' packet means no more files */
                    if (seqchk()) {
                         krmscb->numdat=0;                        /* ACK it */
                         ftfcli();
                         kmxpak('Y',krmscb->seq,krmscb->chkt);
                         seqnxt();
                         ftfnew(KUEND);
                    }
                    break;
               case 'Z':      /* hmm, got here a retry of an old 'Z' packet */
                    ansold();
                    break;
               }
          }
          break;
     case KUATTR:             /* waiting for incoming 'A'ttribute packet(s) */
          if (kmdrpl('A',krmscb->chkt)) {
               khattr();
               krmscb->numdat=0;
               ftfcli();
               kmxpak('Y',krmscb->seq,krmscb->chkt);
               seqnxt();
               ftfnew(KUATTR);
          }
          else if (rejtyp == 'D' && seqchk()) {
               if (ftfrop(0,0,0) == 0) {
                    ftfsrt();
                    kuwini();
                    ftfnew(KUDATA);   /* first incoming data packet of file */
                    hdldpk();
               }
               else {
                    kdabaut(ftfscb->abwhy);
               }
          }
          else if (rejtyp == 'Z' && seqchk()) {
               hdlzpk();
          }
          else if (rejtyp == 'F' ||   /* what? must be duplicate 'F' packet */
                   rejtyp == 'A') {          /* or a retry of an 'A' packet */
               ansold();
          }
          break;
     case KUDATA:        /* waiting for incoming 'D' packet (or 'A' or 'Z') */
          if (kmdrpl('Z',krmscb->chkt)) {                /* eof packet yet? */
               hdlzpk();
          }
          else if (rejtyp == 'D') {          /* nope, still data packets... */
               hdldpk();
          }
          break;
     }
}

static VOID
krminc(                          /* handle incoming byte for KERMIT receive */
CHAR c)
{
     switch(c) {
     case KMARK:
          krmscb->inplen=1;
          break;
     case CTRLX:
          if (++krmscb->xcnt >= CXMAX) {
               ftfabt("operator CTRL-X abort");
          }
          break;
     default:
          switch (krmscb->inplen) {
          case 0:
               break;
          case 1:
               if (' ' <= c && c <= KPKLEN+' ') {
                    krmscb->inpack[(SHORT)krmscb->inplen++]=c;
               }
               else {
                    krmscb->inplen=0;
               }
               break;
          default:
               krmscb->inpack[(SHORT)krmscb->inplen++]=c;
               if (krmscb->inplen >= 2+(krmscb->inpack[1]-' ')+1) {
                    hdlrpk();
                    krmscb->inplen=0;
                    if (krmscb->xcnt > 0) {
                         krmscb->xcnt--;   /* decay ^X (op-abort) detection */
                    }
               }
               break;
          }
          break;
     }
}

struct ftfpsp ftpkmr={                                  /* KERMIT receiving */
     NULL,
     "K",                                  /* 1-3 code letters for protocol */
     "Kermit / Super Kermit",                           /* name of protocol */
     FTFMUL+FTF7BT+FTFXTD,                     /* protocol capability flags */
     sizeof(struct krmdat)+8*KUWSIZ,
                                   /* total length of session control block */
     0,         /* .byttmo                             default byte timeout */
     5*16,      /* .paktmo                           default packet timeout */
     10,        /* .retrys                              default max retries */
     8,         /* .window                        max window size (packets) */
     1024,      /* .paksiz                        packet size 0=auto-figure */
     kmrini,    /* .initze()    Initialize this protocol (recompute scblen) */
     kmrsrt,    /* .start()                                Start a transfer */
     kmrctn,    /* .contin()              Continuously call, 1=more, 0=done */
     krminc,    /* .hdlinc()                       Handle one incoming byte */
     NULL,      /* .hdlins()                   Handle incoming line of text */
     ftfabt,    /* .term()        Initiate graceful termination of transfer */
     ftfrca,    /* .abort()  Immediately unconditionally abort the transfer */
                /* .hdlinb()              Handle an array of incoming bytes */
     (VOID *)ftfinbc,
     "",        /* .secur                App-specific security of some kind */
     {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};

struct ftfpsp ftpkmx={                               /* KERMIT transmitting */
     NULL,
     "K",                                  /* 1-3 code letters for protocol */
     "Kermit / Super Kermit",                           /* name of protocol */
     FTFXMT+FTFMUL+FTF7BT+FTFXTD,              /* protocol capability flags */
     sizeof(struct krmdat)+8*KDWSIZ,
                                   /* total length of session control block */
     0,         /* .byttmo                             default byte timeout */
     5*16,      /* .paktmo                           default packet timeout */
     10,        /* .retrys                              default max retries */
     8,         /* .window                        max window size (packets) */
     1024,      /* .paksiz                        packet size 0=auto-figure */
     kmxini,    /* .initze()    Initialize this protocol (recompute scblen) */
     kmxsrt,    /* .start()                                Start a transfer */
     kmxctn,    /* .contin()              Continuously call, 1=more, 0=done */
     krminc,    /* .hdlinc()                       Handle one incoming byte */
     NULL,      /* .hdlins()                   Handle incoming line of text */
     ftfabt,    /* .term()        Initiate graceful termination of transfer */
     ftfxca,    /* .abort()  Immediately unconditionally abort the transfer */
                /* .hdlinb()              Handle an array of incoming bytes */
     (VOID *)ftfinbc,
     "",        /* .secur                App-specific security of some kind */
     {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
