/** @(#)spook.c 99/03/28 UNIX/Win32s Ciphergram Solver Engine - K L Dunn
*/
/**
    Ciphergram Solver for Windows v1.0
    Copyright (C) 1997  K. L. Dunn

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    Author: K. L. Dunn    email: k.l.dunn@ieee.org
*/
/**
 * Uses a word list file, sorted by word size, smallest first
 * Accepts a short ciphergram with spacing and punctuation in place
 * Attempts to find a simple alphabetic substution key
 * Returns message deciphered as far as possible
 *
 * Ciphergram must obey simple rules:
 *   The cipher is simple substution, upper case only
 *   No cipher letter may stand for itself in clear text
 *   The substution alphabet must be a 1-1 map of the clear alphabet
 *   Spacing, spelling, and punctuation must be accurate
 *
 * Revision history:
 *   10 May 97	Initial
 *   11 May 97	Debug getkey function
 *   12 May 97	Improve getkey algorithm
 *   13 May 97	Try and abandon smallest-word-first search
 *   14 May 97	Try to improve final key
 *   17 May 97	Improve primary search strategy (getkey)
 *   18 May 97	More attempts at improvement
 *   19 May 97	Still more...
 *   20 May 97  Use a size-weighted score for goodness of fit
 *   21 May 97	Use size-weighted score only if WEIGHTED is defined
 *   23 May 97	Troubleshoot failure to search more possibilities
 *   24 May 97	Use a recursive scheme to do searches
 *   25 May 97	Debug
 *   26 May 97	Debug
 *   27 May 97	Debug
 *   28 May 97	Deny fit if subkey keeps words of three letters or less
		  from fitting
 *   29 May 97	Debug
 *   03 Jun 97	Use linear size weighting if WEIGHTED and LINEAR defined
 *   11 Jun 97	Start modifying input routine for url encoded arguments
 *   12 Jun 97	... more ...
 *   13 Jun 97	... more ...
 *   14 Jun 97	... more, graceful exit if execute too long
 *   12 Jul 97	Add arg: starting key, normally 26 blanks
 *   03 Aug 97	Provide for Win32s if __BORLANDC__ is defined
 *   04 Aug 97	Debug
 *   14 Aug 97	Provide a yield for other processes
 *   22 Nov 97	Fix apostrophe bug; extend time to one day
 *   30 Nov 97	Prevent core dump on empty input
 *   28 Mar 99	Add selfok switch to spook
 *   15 Aug 00	Put under GNU General Public License v2
 */

#ifdef __BORLANDC__
#include <windows.h>
#else
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/stat.h>
#include <time.h>

#define MAXTIME 30		/* Execution seconds not to exceed */

#endif

#ifndef WIN32
#define CGI			/* Output html fragment */
#endif

/* #define WEIGHTED */		/* Weighted fit score */
#define	LINEAR			/* Linear (instead of squared) size weights */

#define MAXWORDS 32		/* Maximum word length */
#define MAXGRAM 2048		/* Maximum ciphergram length */
#define MAXATTR	256		/* Maximum attribution length */

/* typedefs for portability */
#ifdef CGI
typedef unsigned char Byte;
typedef unsigned char *Bptr;
#else
typedef char Byte;
typedef char *Bptr;
#endif

/* typdefs for data */
typedef struct {
  long    index;		/* Ordinal of word matched in word list */
  int     size;			/* Length of word */
  Byte   *bptr;			/* Pointer to a black word */
}       bword;
typedef bword *bwparray;

/* Function prototypes */
#ifdef CGI
int     hexnib(char);
void    urlxlate(char *, char *);
#endif
int     splitstring(char **, char *);
int     storecgram(int, char **);
void    storeranks(void);
void    getkey(long *, Byte *);
void    rfit(int, long *, Byte *, Byte *);
#ifdef CGI
void    showkey(Byte *);
void    showgram(Byte *, Byte *);
#endif
int     searchfit(long *, int, Byte *, Byte *, Byte *);
int     readlist(char *);
#ifdef WIN32
VOID    ErrorBox(char *szCaption, char *szFormat, ...);	/* in wspook.c */
VOID    Throttle(void);                                 /* in wspook.c */
VOID    OutputKey(LPSTR lpszResult, LPSTR lpszKey);
VOID    ApplyKey(LPSTR lpszResult, LPSTR key, LPSTR ciphertext);
LPSTR   NextLine(LPSTR lpszLine, int n, HANDLE hFile);
#else
char   *strupr(char *);
#endif

/* Globals */
#ifdef WIN32
extern char szAppName[];	/* in wspook.c */
HGLOBAL hWordList;
DWORD dwLeftInBuf;
#endif
static char *what_text = "@(#)spook.c 990328 K L Dunn";
/* Cipher text version of url-encoded input ciphergram, null-separated */
Byte    ciphergram[MAXGRAM + 1];
/* Cipher text version of url-encoded input ciphergram, no nulls */
Byte    ciphertext[MAXGRAM + 1];
/* Cipher text version of url-encoded input attribution */
Byte    attribution[MAXATTR + 1];
/* List of pointers to null-terminated words making up the ciphergram */
char   *bwlist[MAXGRAM / 2];
/* The null-terminated words */
Byte    nullwords[MAXGRAM + 1];
/* Array of structures that holds the ciphergram */
bwparray blackword;
/* Array of indeces into blackword, ordered by size, longest word first */
int    *rank;
/* Index into rank where words of three letters or less begin */
int     rank3;
/* Number of words in ciphergram == number of structures in blackword[] */
int     nw;
/* Pointer to word list in heap */
Bptr    wlist;
/* Pointers to sized subsets of word list */
Bptr    wlists[MAXWORDS + 1];
/* Maximum current goodness-of-fit score */
long    maxscore;
/* Best score so far */
long	bestscore;
/* Busy flag */
int     trying;
/* Index into rank of longest word not abandoned */
int	highrank;
/* Key to start with, from args */
Byte    startkey[27];
/* Best key ever found */
Byte    bestkey[27];
#ifdef CGI
/* Execution time limit variables */
time_t  timezero,	/* time at start of execution (seconds since 1/1/70) */
        scum;		/* holder for junk */
#endif
/* selfok switch: 1 = a cipher letter may represent itself */
int	gselfok;

#ifdef CGI
/* Main program */
void
main(int argc, char *argv[])
#else
void
spook(int nSelfOK,      LPSTR lpszWlFile, LPSTR lpszInitKey,
      LPSTR lpszCipher, LPSTR lpszAttrib, LPSTR lpszResult)
#endif
{
  long    score;	/* Goodness of fit */
  Byte    key[27];	/* Key: clear letters indexed by cipher letters */
  int     nwrds;	/* argc-like count of words of ciphergram */

#ifdef CGI
  /* Exit now if improper input */
  if (argc != 6) {
    printf(
      "\nUsage: spook selfok wordlist initial-key urlencoded-ciphergram urlencoded-attribution\n"
    );
    exit(0);
  }
  /* Initialize selfok switch value */
  gselfok = atoi(argv[1]);
  /* Set up initial key from args */
  strcpy((char *) startkey, argv[3]);
  /* Translate url-encoded argument strings to null-terminated words */
  /* Note that input can be unencoded, if enclosed in double quotes */
  urlxlate(ciphergram, argv[4]);
  urlxlate(attribution, argv[5]);
#else
  *lpszResult = '\0';
  lstrcpy(ciphergram, lpszCipher);
  lstrcpy(attribution, lpszAttrib);
  /* Set up initial key from args */
  strcpy((char *) startkey, lpszInitKey);
  /* Set state of selfok switch */
  gselfok = nSelfOK;
#endif
  /* Make a copy of the ciphergram, for deciphering later */
  strcpy(ciphertext, ciphergram);
  /* Split the ciphergram into null-terminated words without punctuation */
  nwrds = splitstring(bwlist, ciphergram);
  /* Store the words of the ciphergram in blackword[] */
  if (!storecgram(nwrds, bwlist))
    return;
  /* Determine ranks of words by size */
  storeranks();
  /* Read in and set up pointers to the sized word lists */
#ifdef CGI
  if (!readlist(argv[2]))
#else
  if (!readlist(lpszWlFile))
#endif
    return;
#ifdef CGI
  timezero = time(&scum);
#endif
  /* Derive cipher key from ciphergram and word list */
  getkey(&score, key);
#ifdef CGI
  /* Display the key and the clear text */
  printf("<hr><h3>Ciphergram and Attribution</h3>\n<p><pre>\n");
  printf("%s -- %s\n", ciphertext, attribution);
  printf("</pre>\n");
  showkey(key);
  printf("<hr><h3>Deciphered Ciphergram and Attribution</h3>\n<p><pre>\n");
  showgram(key, ciphertext);
  printf(" -- ");
  showgram(key, attribution);
  printf("\n");
  printf("</pre>\n");
#endif
#ifdef WIN32
  /* Free the memory used for the word tables */
  { int w;
    for (w = nw; w >= 1; --w)
      free(blackword[w].bptr);
  }
  free(blackword);
  free(rank);
  /* Produce the derived key */
  OutputKey(lpszResult, key);
  /* Produce deciphered Ciphergram and Attribution */
  lstrcat(lpszResult, "\r\n\r\nDeciphered Ciphergram and Attribution:\r\n\r\n");
  ApplyKey(lpszResult, key, ciphertext);
  lstrcat(lpszResult, " -- ");
  ApplyKey(lpszResult, key, attribution);
#endif
#ifdef WIN32
  GlobalUnlock(hWordList);
  GlobalFree(hWordList);
#endif
}

#ifdef CGI
/* hexnib converts a hex nybble to an integer */
int hexnib(char x)
{
  int k = toupper(x);
  return (k - (((k >= 'A') && (k <= 'F')) ? ('A' - 0x0A) : '0'));
}

/* urlxlate translates a url-encoded string into plain text */
void
urlxlate(char * plain, char * urlenc)
{
  while (*urlenc) {
    switch(*urlenc) {
    case '+':
      *plain++ = ' ';
      break;
    case '%':
      {
        int k = hexnib(*++urlenc);
        *plain++ = (k << 4) | hexnib(*++urlenc);
      }
      break;
    default:
      *plain++ = *urlenc;
    }
    ++urlenc;
  }
  *plain = '\0';
}
#endif

/* splitstring splits text into null-terminated alpha-only words */
int
splitstring(char **words, char * text)
{
  char *cp;
  int n = 1;

  for (cp = text;
       (words[n] = strtok(cp, ",.;:/?\042|\\`+=*^@-_()&%$#!~0123456789 \t\n\r")) != NULL;
       cp = NULL, ++n);
  return(n);
}

/* storecgram stores the words of the ciphergram in blackword[] */
int
storecgram(int nwords, char *black[])
{
  int     w;		/* Word index into ciphertext message */

  /* Check for empty input */
  if (nwords <= 1) {
#ifdef WIN32
    ErrorBox(szAppName, "%s", "Ciphergram is empty");
#else
    printf("\nError: Ciphergram is empty\n");
#endif
    return 0;
  }
  /* Allocate space for rank array */
  if ((rank = (int *) malloc((nwords + 1) * sizeof(int))) == NULL) {
#ifdef WIN32
    ErrorBox(szAppName, "%s", "Cannot allocate enough memory for rank array!");
    return 0;
#else
    printf("\nError: Cannot allocate enough memory for rank array!\n");
    exit(0);
#endif
  }
  /* Allocate space for bword structures */
  if ((blackword = (bwparray) malloc((nwords + 1) * sizeof(bword))) == NULL) {
#ifdef WIN32
    ErrorBox(szAppName, "%s", "Cannot allocate enough memory for ciphergram table!");
    return 0;
#else
    printf("\nError: Cannot allocate enough memory for ciphergram table!\n");
    exit(0);
#endif
  }
  /* Store the ciphergram as an array of bword structures */
  for (w = 1; w < nwords; ++w) {
    if ((blackword[w].bptr = (Byte *) malloc(strlen(black[w]) + 1)) == NULL) {
#ifdef WIN32
      ErrorBox(szAppName, "%s", "Cannot allocate enough memory for ciphergram!");
      return 0;
#else
      printf("\nError: Cannot allocate enough memory for ciphergram!\n");
      exit(0);
#endif
    }
    strcpy((char *) blackword[w].bptr, black[w]);
    strupr((char *) blackword[w].bptr);
    blackword[w].index = 0;
    blackword[w].size = strlen(black[w]);
  }
  nw = nwords - 1;
  return 1;
}

/* storeranks() sets the rank of each word: 1 is longest, nw is shortest */
void
storeranks()
{
  int     w;
  int     unsorted;

  /* Initialize the rank array */
  for (w = 1; w <= nw; ++w)
    rank[w] = w;
  unsorted = 1;
  while (unsorted) {
    unsorted = 0;
    for (w = 1; w < nw; ++w) {
      if (blackword[rank[w]].size < blackword[rank[w + 1]].size) {
	unsorted = rank[w];
	rank[w] = rank[w + 1];
	rank[w + 1] = unsorted;
      }
    }
  }
}

/* getkey() derives the key alphabet from the ciphergram and the word list */
void
getkey(long * score, Byte * key)
{
  int w;
  Byte initkey[27];

  /* Calculate best possible score */
#ifdef WEIGHTED
  for (w = 1, maxscore = 0; w <= nw; ++w)
#ifdef LINEAR
    maxscore += blackword[w].size;
#else
    maxscore += blackword[w].size * blackword[w].size;
#endif
#else
  maxscore = nw;
#endif
  /* Find where in rank[] the words of three letters or less begin */
  for (w = 1, rank3 = 0; (w <= nw) && !rank3; ++w)
    if (blackword[rank[w]].size <= 3)
      rank3 = w;
  if (!rank3)
    rank3 = nw + 1;
  /* Start with longest word, zero score, and empty key */
  highrank = 1;
  trying = 1;
  *score = 0;
  strcpy((char *) initkey, (char *) startkey);
  strcpy((char *) bestkey, (char *) initkey);
  bestscore = 0;
  rfit(1, score, initkey, key);
  strcpy((char *) key, (char *) bestkey);
}

/* rfit finds a pattern match, and recursively evaluates it */
void
rfit(int rankindex, long * guessscore, Byte * guesskey, Byte * betterkey)
{
  int     gotfit;
  long    betterscore, subscore;
  Byte    subkey[27];

#ifdef CGI
  /* Terminate if we have been here too long */
  if ((time(&scum) - timezero) > MAXTIME) {
    printf("Error: exceeded allowed execution time\n");
    exit(0);
  }
#else
  Throttle();		/* Yield time for messages */
#endif
  blackword[rank[rankindex]].index = 0;
  betterscore = 0;
  strcpy((char *) betterkey, (char *) guesskey);
  do {
    /** Return gotfit=1 if found a match to the blackword
     *  Start looking just past last index of match for the blackword
     *  Allow fit only if it agrees with guesskey
     *  Return subkey: same as guesskey if no match, improved if match
     */
    gotfit = searchfit(&blackword[rank[rankindex]].index,
		       blackword[rank[rankindex]].size,
		       blackword[rank[rankindex]].bptr,
		       guesskey, subkey);
    subscore = *guessscore;
    if (gotfit) {
      int     w;
      int     rank3fit;
      Byte    rank3key[27];
      Byte    rank3junkkey[27];

      /* Advance index, in case we need to search again */
      ++blackword[rank[rankindex]].index;
      /* Deny fit if subkey keeps words of three letters or less from fitting */
      strcpy((char *) rank3key, (char *) subkey);
      for (w = rank3, rank3fit = 1; rank3fit && (w <= nw); ++w) {
	long    windex;
	windex = 0;
	rank3fit = searchfit(&windex,
			     blackword[rank[w]].size,
			     blackword[rank[w]].bptr,
			     rank3key, rank3junkkey);
      }
      if (rank3fit) {
        /* Short words fit, too -- keep track of goodness of fit */
#ifdef WEIGHTED
#ifdef LINEAR
        subscore += blackword[rank[rankindex]].size;
#else
        subscore += blackword[rank[rankindex]].size * blackword[rank[rankindex]].size;
#endif
#else
        subscore += 1;
#endif
        /* Accumulate key */
        strcpy((char *) betterkey, (char *) subkey);
        /* Recursive scan of shorter words: improve key and accumulate score */
        if (rankindex < nw) {
          rfit(rankindex + 1, &subscore, betterkey, subkey);
        }
      }
    } else {			/* No fit */
      if (rankindex < nw) {
        /* If longest unabandoned word, abandon and calculate new maxscore */
        if (rankindex == highrank) {
	  ++highrank;
#ifdef WEIGHTED
#ifdef LINEAR
          maxscore -= blackword[rank[rankindex]].size;
#else
          maxscore -= blackword[rank[rankindex]].size * blackword[rank[rankindex]].size;
#endif
#else
          --maxscore;
#endif
        }
        /* Recursive scan of shorter words only if long enough */
        if ((bestscore < maxscore) && (blackword[rank[rankindex+1]].size > 4)) {
	  Byte    nofitguesskey[27];
	  strcpy((char *) nofitguesskey, (char *) guesskey);
          rfit(rankindex + 1, &subscore, nofitguesskey, subkey);
        }
      }
    }				/* ... no fit */
    /* See if this key is the best so far */
    if (subscore > betterscore) {
      /* Record best key so far */
      betterscore = subscore;
      strcpy((char *) betterkey, (char *) subkey);
    }
    /* Keep track of global best score, for pruning */
    if (betterscore > bestscore) {
      bestscore = betterscore;
      strcpy((char *) bestkey, (char *) betterkey);
    }
    /* We are done if we fit all the words we have not abandonded */
    if (betterscore >= maxscore) {
      trying = 0;
    }
  } while (gotfit && trying);
  *guessscore = betterscore;
  strcpy((char *) guesskey, (char *) betterkey);
}

#ifdef CGI
/* showkey(cipherkey) displays key */
void
showkey(Byte * cipherkey)
{
  /* Display the cipherkey */
  printf("<hr><h3>Deciphering Key</h3><p>\n<pre>\n");
  printf("Cipher alphabet: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]\n");
  printf(" Clear alphabet: [%s]\n</pre>\n", cipherkey);
}

/* showgram(cipherkey) goodness-of-fit score and clear text */
void
showgram(Byte * cipherkey, Byte * cipherstring)
{
  Byte   *cp;

  for (cp = cipherstring; *cp; ++cp) {
    Byte clearchar;
    clearchar = isalpha(*cp) ? cipherkey[toupper(*cp) - 'A'] : *cp;
    printf("%c", isalpha(*cp) ? ((clearchar == ' ') ? '_' : clearchar) : *cp);
  }
}
#endif

#ifdef WIN32
VOID
OutputKey(LPSTR lpszResult, LPSTR lpszKey)
{
  /* Display the cipherkey */
  lstrcpy(lpszResult, "Deciphering Key:\r\n\r\n");
  lstrcat(lpszResult, "Cipher alphabet: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]\r\n");
  lstrcat(lpszResult, " Clear alphabet: [");
  lstrcat(lpszResult, lpszKey);
  lstrcat(lpszResult, "]\r\n");
}

VOID
ApplyKey(LPSTR lpszResult, LPSTR lpszKey, LPSTR lpszCipher)
{
  LPSTR   lpszBlack, lpszPlain;
  char    clearchar;

  for (lpszBlack = lpszCipher, lpszPlain = lpszResult + lstrlen(lpszResult);
       *lpszBlack; ++lpszBlack) {
    clearchar =
      isalpha(*lpszBlack) ? lpszKey[toupper(*lpszBlack) - 'A'] : *lpszBlack;
    *lpszPlain++ =
      isalpha(*lpszBlack) ? ((clearchar == ' ') ? (char) '_' : clearchar) : *lpszBlack;
  }
  *lpszPlain = '\0';
}

/* NextLine serves the purpose of fgets() */
LPSTR
NextLine(LPSTR lpszLine, int nMax, HANDLE hFile)
{
  int      nToGet, nChar;
  LPSTR    lpszEolp, lpszP;
  static LPSTR lpszCurp;
  static char cBuf[512];

  lpszP = lpszLine;
  nMax--;				/* leave room for null terminator */
  while (nMax >= 0) {
    /* If there is anything in the buffer, copy it to the user's buffer. */
    if (dwLeftInBuf > 0) {	/* something in the buffer */
      if (nMax > dwLeftInBuf)
	nToGet = dwLeftInBuf;
      else
	nToGet = nMax;

      /* If there is a newline in the buffer, treat that as the end. */
      if ((lpszEolp = memchr(lpszCurp, '\n', nToGet)) != NULL)
	nToGet = lpszEolp - lpszCurp + 1;

      /* Copy from file buffer to user buffer, update pointers and counts. */
      memcpy(lpszP, lpszCurp, nToGet);
      lpszCurp += nToGet;
      dwLeftInBuf -= nToGet;
      lpszP += nToGet;
      nMax -= nToGet;

      /* If user buffer full, or a newline was seen, stop now. */
      if (lpszEolp != NULL || nMax == 0) {
	nChar = '\n';
	break;
      }
    } else {
      /* Buffer empty: read some data into the buffer, and get a character */
      ReadFile(hFile, cBuf, sizeof(cBuf), &dwLeftInBuf, NULL);
      lpszCurp = cBuf;
      if (dwLeftInBuf == 0) {
        nChar = -1;
        break;
      }
      nChar = *lpszCurp++;
      --dwLeftInBuf;
      *lpszP++ = (char) nChar;
      --nMax;
      if (nChar == '\n')
	break;
    }
  }
  if (-1 == nChar && lpszP == lpszLine) {
    lpszP = NULL;
  } else {
    *lpszP = '\0';
    lpszP = lpszLine;
  }
  return (lpszP);
}
#endif

/* readlist() reads the word list and stores it in heap space */
int
readlist(char * wordfile)
{
#ifdef CGI
  FILE   *listfile;
  struct stat nliststat;
#else
  HANDLE  hWlFile;
  DWORD   dwWlSize;
#endif
  int     wlen;
  register Bptr wptr;
  register Bptr lptr;
  Byte    listword[MAXWORDS + 1];

  /* Open the word list file */
#ifdef WIN32
  if (INVALID_HANDLE_VALUE == (hWlFile =
      CreateFile(wordfile,
                 GENERIC_READ,
                 0,
                 NULL,
                 OPEN_EXISTING,
                 FILE_ATTRIBUTE_READONLY | FILE_FLAG_SEQUENTIAL_SCAN,
                 NULL))) {
    ErrorBox(szAppName, "Cannot open file %s for reading!", wordfile);
    return 0;
  }
#else
  if ((listfile = fopen(wordfile, "r")) == NULL) {
    printf("\nError: Cannot open file %s for reading!\n", wordfile);
    exit(0);
  }
#endif
#ifdef WIN32
  /* Find out how big the file is */
  if ((dwWlSize = GetFileSize (hWlFile, NULL)) == 0xFFFFFFFF) {
    CloseHandle(hWlFile);
    ErrorBox(szAppName, "Cannot get size of file %s!", wordfile);
    return 0;
  }
#else
  /* Find out (among other things) how big it is */
  if (fstat(fileno(listfile), &nliststat)) {
    printf("\nError: Cannot stat file %s!\n", wordfile);
    exit(0);
  }
#endif
  /* Allocate enough memory to store the word list */
#ifdef WIN32
  if ((hWordList = GlobalAlloc(GPTR, dwWlSize + 2)) == NULL) {
    CloseHandle(hWlFile);
    ErrorBox(szAppName, "%s", "Cannot allocate enough memory for word list!");
    return 0;
  }
  wlist = GlobalLock(hWordList);
  dwLeftInBuf = 0;		/* Initialize NextLine() */
#else
  if ((wlist = (Bptr) malloc(nliststat.st_size + 100)) == NULL) {
    printf("\nError: Cannot allocate enough memory for word list!\n");
    exit(0);
  }
#endif
  /* Initialize the array of pointers to lists of words of size n */
  for (wlen = 0; wlen <= MAXWORDS; wlists[wlen] = NULL, ++wlen);
  /* Read the word list, setting pointers as we go */
  wptr = wlist;
#ifdef CGI
  while (fgets((char *) listword, MAXWORDS, listfile))
#else
  while (NextLine((char *) listword, MAXWORDS, hWlFile))
#endif
  {
    /* Store everything in upper case */
    strupr((char *) listword);
    /* Strip newline from end of each word */
    wlen = strlen((char *) listword) - 1;
    /* If length is at least one, and is not too big ... */
    if (wlen > 0 && wlen < MAXWORDS) {
      /* Set pointer to beginning of subset of this size */
      if (wlists[wlen - 1] == NULL) {
	/* Put a null at the end of each sized subset */
	if (wlen > 1) {
	  *wptr++ = '\0';
	}
	wlists[wlen - 1] = wptr;
      }
    }
    /* Store a word in the list, without trailing null */
    lptr = listword;
    while (wlen--)
      *wptr++ = *lptr++;
  }
  *wptr++ = '\0';
  /* Done reading the word list; close the file */
#ifdef WIN32
  CloseHandle(hWlFile);
#else
  fclose(listfile);
#endif
  return 1;
}

/* searchfit starts looking at word k in sublist of size l */
int
searchfit(long *wndx, int l, Byte * blackword, Byte * trialkey, Byte * newkey)
{
  Byte    clearword[MAXWORDS];
  long    k;
  Bptr    lptr;
  Bptr    lbase;
  int     i, match;

  /* Point to start of list of words of length l; exit if empty */
  if ((lbase = wlists[l - 1]) == NULL) {
    strcpy((char *) newkey, (char *) trialkey);	/* Clean up newkey */
    return 0;			/* Report no-match */
    /* NOTREACHED */
  }
  /* Convert blackword to upper case */
  strupr((char *) blackword);
  /* Form (partial) clear-text word from black-text word and trial key */
  for (i = 0; i < l; ++i) {
    Byte    bletter = blackword[i];
    /* Any non-uppercase character represents itself */
    if (isupper(bletter)) {
      bletter -= (char) 'A';
      if (trialkey[bletter] == ' ') {
	clearword[i] = ' ';
      } else {
	clearword[i] = trialkey[bletter];
      }
    } else {
      clearword[i] = bletter;
    }
  }
  clearword[l] = '\0';
  k = *wndx;
  do {
    /* Get kth word from word list l; return no-fit if no more words */
    lptr = lbase + k * l;
    if (*lptr) {		/* kth word */
      register Bptr w;
      register Byte *c;
      /* See if clear ~ word[k] */
      for (i = 0, w = lptr, c = clearword, match = 1; match && i < l; ++i, ++c, ++w) {
	if (*c != *w && (*c != ' ' || !isalpha(*w))) {
	  match = 0;
	}
      }
      /* clearword either partially matches or is empty -- test pattern */
      if (match) {
	register int newkeyindex;
	register Byte newkeyletter;
	strcpy((char *) newkey, (char *) trialkey);
	/* Set new letters in newkey using matched word and blackword */
	for (i = 0; i < l; ++i) {
	  register int j, used;
	  /* clear letter ... */
	  newkeyletter = *lptr++;
	  /* ... might go where black letter points, in trial key ... */
	  /* ... but non-uppercase must match exactly */
	  if (!isupper(blackword[i])) {
	    if (blackword[i] == newkeyletter) {
	      continue;
	    } else {
	      match = 0;
	      break;
	    }
	  }
	  newkeyindex = blackword[i] - 'A';
	  if (gselfok) {
	    /* A black letter may stand for itself in clear space */
	    used = 0;
	  } else {
	    /* No black letter may stand for itself in clear space */
	    used = (newkeyindex == (newkeyletter - 'A'));
	  }
	  /* A black letter may represent only one clear letter */
	  for (j = 0; (!used) && (j < strlen((char *) newkey)); ++j) {
	    used = (newkeyletter == newkey[j]) && (newkeyindex != j);
	  }
	  if (used) {
	    match = 0;
	    break;
	    /* NOTREACHED */
	  }
	  /* A clear letter may be represented by only one black letter */
	  if (newkey[newkeyindex] == ' ') {
	    newkey[newkeyindex] = newkeyletter;
	  } else {
	    if (newkey[newkeyindex] != newkeyletter) {
	      match = 0;
	      break;
	      /* NOTREACHED */
	    }
	  }
	}
	if (!match) {		/* NG: tried to change key */
	  ++k;
	}
      } else {			/* Did not match */
	++k;
      }
    } else {			/* No more words in list l */
      strcpy((char *) newkey, (char *) trialkey);	/* Clean up newkey */
      return 0;			/* Report no-match */
      /* NOTREACHED */
    }
  } while (!match);
  *wndx = k;			/* Point to matched word */
  return 1;			/* Claim a match */
}

#ifdef CGI
/* Unix does not define strupr(), so this will have to do */
char *
strupr(char *s)
{
  register char *p;
  for (p = s; *p; ++p)
    if (islower(*p))
      *p += 'A' - 'a';
  return s;
}
#endif
