/* Tasc:  HP48-binary <-> HP48-ASC.
   Copyright 1992 by Jonathan T. Higa.
   Distribute freely.
   You may make modifications to suit your needs; simply document the changes.
   
   Dec 1991 to Mar 1992: parses hp48 binary correctly,
      determines type of source file, handles stdin & stdout
   12 June 1992: v2.50 -- auto name completion
   19 June 1992: v2.51 -- bug fix for names < 4 chars; added @ comments
   22 June 1992: v2.52 -- generalized report syntax
*/

#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ATOB 1
#define BTOA 2

typedef unsigned long ulong;

const char ASCSUF[]=".asc"; /* ASC file suffix */
const char VERSION[]="2.52";          /* TASC version */
const char *ROMFMT="ROM Revision: %c\n";
   /* ROM Revision report format: use %c */
const char *BYTESFMT="BYTES: #%lXh %ld%s\n";
const char *HALFFMT=".5";
   /* BYTES report: use long for checksum, long for integral bytes,
      and string for half-byte format string */
int verb=1;                 /* verbosity flag */
char rom='E';               /* default rom revision letter */

void *alloc(size_t n);
int stricmp(const char *s1, const char *s2);
ulong popbitq(ulong *buf, int *bsize, int nbits);
ulong pushbitq(ulong *buf, int *bsize, int nbits, ulong bits);
int bintoasc(FILE *fbin, FILE *fasc);
int translate(const char *fsrc, const char *fdest, int mode);

#define calc_crc(crc, hex) (crc=(crc>>4)^(((crc^(hex))&0xF)*0x1081))
/* Recalculates the Cyclic Redundancy Check value based on the old CRC
   value crc and a new nibble hex.
   Note:  crc should be an unsigned lvalue initialized to 0. */

#define lowbits(n) ((1uL << (n)) - 1uL)
/* Creates an unsigned long with only the low n bits set. */

void *alloc(size_t n)
/* Safe memory allocation.
   Exits on error. */
{
   void *p;
   if (n <= 0) {
      fputs("mem: Invalid block size\n", stderr);
      exit(1);
   }
   p = malloc(n);
   if (!p) {
      perror("mem");
      exit(1);
   }
   return p;
}

int stricmp(const char *s1, const char *s2)
/* Case-insensitive string comparison via conversion to upper case.
   Return -1 if s1 is alphabetically before s2, 1 if after, 0 if equal. */
{
   int i, c1, c2;
   i = 0;
   do {
      c1 = toupper(s1[i]);
      c2 = toupper(s2[i]);
      i++;
      if (c1 < c2) return -1;
      if (c1 > c2) return 1;
   } while (c1);
   return 0;
}

ulong popbitq(ulong *buf, int *bsize, int nbits)
/* Returns the lowest nbits bits of *buf.
   Removes those bits from *buf and adjusts *bsize appropriately. */
{
   ulong b;
   b = *buf & lowbits(nbits);
   *buf >>= nbits;
   if ((*bsize -= nbits) < 0) {
      fputs("tasc: Bit buffer underflow\n", stderr);
      exit(1);
   }
   return b;
}

ulong pushbitq(ulong *buf, int *bsize, int nbits, ulong bits)
/* Pushes the low nbits of bits onto the high end of *buf.
   Adjusts *bsize appropriately.
   Returns the bits actually pushed. */
{
   bits &= lowbits(nbits);
   *buf |= bits << *bsize;
   if ((*bsize += nbits) > sizeof *buf * CHAR_BIT) {
      fputs("tasc: Bit buffer overflow\n", stderr);
      exit(1);
   }
   return bits;
}

int asctobin(FILE *fasc, FILE *fbin)
/* Translate ASC to HP48 binary.
   Returns 0 if ok, 1 on error. */
{
   ulong buf=0, crc=0;
   long nibs;
   int bsize=0, c, d;
   if (verb) fputs("Mode: ASC->bin\n", stderr);

   /* scan input file to find line beginning with '"' */
   c = '\n';
   do {
      d = c;
      c = getc(fasc);
   } while (c != EOF && (d != '\n' || c != '"'));
   if (c == EOF) {
      fputs("ASC->bin: Invalid ASC file\n", stderr);
      return 1;
   }

   /* write header into binary file */
   fputs("HPHP48-", fbin);
   putc(rom, fbin);
   if (verb)
      fprintf(stderr, ROMFMT, rom);

   /* translate data */
   nibs = -4;
   while (fscanf(fasc, "%1x", &d) == 1) {
      pushbitq(&buf, &bsize, 4, d);
      calc_crc(crc, d);
      nibs++;
      if (bsize >= 16 + CHAR_BIT)
          fputc((int) popbitq(&buf, &bsize, CHAR_BIT), fbin);
   }

   /* check CRC */
   if (bsize > 16)
      fputc((int) popbitq(&buf, &bsize, bsize - 16), fbin);
   if (crc || bsize != 16) {
      fputs("ASC->bin: CRC failed\n", stderr);
      return 1;
   }
   if (verb)
      fprintf(stderr, BYTESFMT, buf, nibs/2, nibs&1?HALFFMT:"");
   return 0;
}

int bintoasc(FILE *fbin, FILE *fasc)
/* Translates HP48 binary to ASC format.
   Return 0 if ok, 1 on error. */
{
   ulong buf=0, crc=0, skip=0;
   long nibs;
   int bsize=0, c, width=0;
   enum { NONE, SIZE, ASCIC, ASCIX, DIR } state=NONE;
   const int MAXWIDTH=64;
   char str[7];
   if (verb) fputs("Mode: bin->ASC\n", stderr);

   /* check input for "HPHP48-" header */
   if (fread(str, 1, 7, fbin) != 7
      || strncmp(str, "HPHP48-", 7)
      || (c = getc(fbin)) == EOF) {
      fputs("bin->ASC: Invalid source file header\n", stderr);
      return 1;
   }
   if (verb)
      fprintf(stderr, ROMFMT, c);

   /* write header into ASC file */
   fprintf(fasc, "%%%%HP: T(1)A(R)F(.); @ tasc v%s file\n\"", VERSION);

   nibs = 0;
   while ((c = getc(fbin)) != EOF) {
      pushbitq(&buf, &bsize, CHAR_BIT, c);

      /* parse input HP objects */
      if (!skip)
          switch (state) {
             case NONE: if (bsize >= 20) {
                ulong pro = buf & lowbits(20);
                skip = 5;
                if (pro == 0x29e8uL || pro == 0x2a0auL || pro == 0x2a2cuL
                     || pro == 0x2a4euL || pro == 0x2b1euL || pro == 0x2b40uL
                     || pro == 0x2b62uL || pro == 0x2b88uL || pro == 0x2dccuL)
                     state = SIZE;
                else if (pro == 0x2e48uL || pro == 0x2e6duL || pro == 0x2afcuL)
                     state = ASCIC;
                else if (pro == 0x2a96uL) state = DIR, skip = 8;
                else if (pro == 0x2911uL) skip = 10;
                else if (pro == 0x2933uL) skip = 21;
                else if (pro == 0x2955uL) skip = 26;
                else if (pro == 0x2977uL) skip = 37;
                else if (pro == 0x299duL) skip = 47;
                else if (pro == 0x29bfuL) skip = 7;
                else if (pro == 0x2e92uL) skip = 11;
             }
             break;
             case SIZE: if (bsize >= 20)
                state = NONE, skip = buf & lowbits(20);
             break;
             case ASCIC: if (bsize >= 8)
                state = NONE, skip = 2 + 2 * (buf & lowbits(8));
             break;
             case ASCIX: if (bsize >= 8)
                state = NONE, skip = 4 + 2 * (buf & lowbits(8));
             break;
             case DIR: if (bsize >= 20)
                state = ASCIX, skip = buf & lowbits(20);
             break;
          }

      /* write already interpreted binary data */
      while (skip && bsize >= 4) {
          c = (int) popbitq(&buf, &bsize, 4);
          if (width == MAXWIDTH) {
             putc('\n', fasc);
             width = 0;
          }
          fprintf(fasc, "%1.1X", c);
          width++;
          calc_crc(crc, c);
          skip--;
          nibs++;
      }
   }
   if (buf) {
      fprintf(stderr, "bin->ASC: Binary parsed incorrectly\n");
      return 1;
   }

   /* append CRC */
   buf = crc;
   bsize = 16;
   while (bsize) {
      if (width == MAXWIDTH) {
          putc('\n', fasc);
          width = 0;
      }
      fprintf(fasc, "%1.1X", (int) popbitq(&buf, &bsize, 4));
      width++;
   }
   fputs("\"\n@ ", fasc);
   fprintf(fasc, BYTESFMT, crc, nibs/2, nibs&1?HALFFMT:"");
   if (verb)
      fprintf(stderr, BYTESFMT, crc, nibs/2, nibs&1?HALFFMT:"");
   return 0;
}

int translate(const char *fsrc, const char *fdest, int mode)
/* Translate file named fsrc to file named fdest, using mode mode.
   fsrc == NULL means use stdin; fdest == NULL means use stdout.
   Return 0 if ok, 1 on error. */
{
   FILE *in, *out;
   int i;
   switch (mode) {
      case ATOB: if (fsrc) {
          in = fopen(fsrc, "r");
          if (!in) { perror(fsrc); return 1; }
      } else
          in = stdin;
      if (fdest) {
          out = fopen(fdest, "wb");
          if (!out) { perror(fdest); return 1; }
      } else {
          fputs("ASC->bin: Case not implemented\n",stderr);
          exit(1);
      }
      i = asctobin(in, out);
      break;
      case BTOA: if (fsrc) {
          in = fopen(fsrc, "rb");
          if (!in) { perror(fsrc); return 1; }
      } else {
          fputs("bin->ASC: Case not implemented\n",stderr);
          exit(1);
      }
      if (fdest) {
          out = fopen(fdest, "w");
          if (!out) { perror(fdest); return 1; }
      } else
          out = stdout;
      i = bintoasc(in, out);
      break;
      default: fputs("tasc: Unimplemented translation mode\n", stderr);
      exit(1);
   }
   fclose(in);
   fclose(out);
   return i;
}

int main(int argc, char **argv, char **envp)
/* Uses:
   tasc options files
   options:
   -d Force ASC->bin translation (decode ASC)
   -e Force bin->ASC translation (encode ASC)
   -i Use stdin for -a mode / stdout for -b mode:
      ignored in auto-translation
   -q Suppress printouts (quiet)
   -r<let> Set rom revision <let>:
      ignored in bin->ASC translation
   files:
   names of input/output files
   return code: 0 ok, 1 error */
{
   int e, mode=0, stdio=0, argi;
   char *p, *q;

   /* interpret options as described above */
   e = 0;
   for (argi = 1; argv[argi] && argv[argi][0] == '-'; argi++)
      for (p = argv[argi] + 1; *p; p++) {
          switch (*p) {
             case 'd': if (mode) e++; else mode = ATOB; break;
             case 'e': if (mode) e++; else mode = BTOA; break;
             case 'i': if (stdio) e++; else stdio = 1; break;
             case 'q': if (verb) verb = 0; else e++; break;
             case 'r': if (p[1]) {
                p++;
                if (!strchr("ABCDEF", rom = toupper(*p))) e++;
             } else e++;
             break;
             default: e++;
          }
      }
   if (e) {
      fprintf(stderr, "Use: %s [-deiqr<let>] file [file]\n", argv[0]);
      return 1;
   }
   if (verb) fprintf(stderr, "TASC version %s\nCompiled on %s at %s\n",
      VERSION, __DATE__, __TIME__);

   /* translate files by method specified */
   switch (mode) {
      case ATOB: if (stdio) {
          if (argc - argi == 1) e = translate(0, argv[argi], ATOB);
          else {
             fputs("ASC->bin: Specify output filename\n", stderr);
             return 1;
          }
      } else {
          if (argc - argi == 2) e = translate(argv[argi], argv[argi+1], ATOB);
          else {
             fputs("ASC->bin: Specify input and output filenames\n", stderr);
             return 1;
          }
      }
      break;
      case BTOA: if (stdio) {
          if (argc - argi == 1) e = translate(argv[argi], 0, BTOA);
          else {
             fputs("bin->ASC: Specify input filename\n", stderr);
             return 1;
          }
      } else {
          if (argc - argi == 2) e = translate(argv[argi], argv[argi+1], BTOA);
          else {
             fputs("bin->ASC: Specify input and output filenames\n", stderr);
             return 1;
          }
      }
      break;
      case 0: if (argc - argi == 1) {
          int n;
          n = strlen(argv[argi]);
          q = alloc(n + sizeof ASCSUF);
          strcpy(q, argv[argi]);
          n -= sizeof ASCSUF - 1;
          if (n > 0 && !stricmp(q+n, ASCSUF)) {
             q[n] = 0;
             e = translate(argv[argi], q, ATOB);
          } else {
             p = strrchr(q, ASCSUF[0]);
             if (p) strcpy(p, ASCSUF); else strcat(q, ASCSUF);
             e = translate(argv[argi], q, BTOA);
          }
          free(q);
      } else if (argc - argi == 2) {
          e = translate(argv[argi], argv[argi+1],
             stricmp(argv[argi]+strlen(argv[argi])-(sizeof ASCSUF -1), ASCSUF)
             ? BTOA : ATOB);
      } else {
          fputs("tasc: Specify source [and target] filename[s]\n", stderr);
          return 1;
      }
      break;
   }
   return e;
}
