/* t1disasm
 *
 * This program `disassembles' Adobe Type-1 font programs in either PFB or PFA
 * format.  It produces a human readable/editable pseudo-PostScript file by
 * performing eexec and charstring decryption as specified in the `Adobe Type 1
 * Font Format' version 1.1 (the `black book').  There is a companion program,
 * t1asm, which `assembles' such a pseudo-PostScript file into either PFB or
 * PFA format.
 *
 * Copyright (c) 1992 by I. Lee Hetherington, all rights reserved.
 * Copyright (c) 1998-2017 Eddie Kohler
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, subject to the
 * conditions listed in the Click LICENSE file, which is available in full at
 * http://github.com/kohler/click/blob/master/LICENSE. The conditions
 * include: you must preserve this copyright notice, and you cannot mention
 * the copyright holders in advertising related to the Software without
 * their permission. The Software is provided WITHOUT ANY WARRANTY, EXPRESS
 * OR IMPLIED. This notice is a summary of the Click LICENSE file; the
 * license in that file is binding.
 *
 * New change log in `NEWS'. Old change log:
 *
 * Revision 1.4  92/07/10  10:55:08  ilh
 * Added support for additional PostScript after the closefile command
 * (ie., some fonts have {restore}if' after the cleartomark).  Also,
 * removed hardwired charstring start command (-| or RD) in favor of
 * automatically determining it.
 *
 * Revision 1.3  92/06/23  10:57:53  ilh
 * MSDOS porting by Kai-Uwe Herbing (herbing@netmbx.netmbx.de)
 * incoporated.
 *
 * Revision 1.2  92/05/22  12:05:33  ilh
 * Fixed bug where we were counting on sprintf to return its first
 * argument---not true in ANSI C.  This bug was detected by Piet
 * Tutelaers (rcpt@urc.tue.nl).  Also, fixed (signed) integer overflow
 * error when testing high-order bit of integer for possible
 * sign-extension by making comparison between unsigned integers.
 *
 * Revision 1.1  92/05/22  12:04:07  ilh
 * initial version
 *
 * Ported to Microsoft C/C++ Compiler and MS-DOS operating system by
 * Kai-Uwe Herbing (herbing@netmbx.netmbx.de) on June 12, 1992. Code
 * specific to the MS-DOS version is encapsulated with #ifdef _MSDOS
 * ... #endif, where _MSDOS is an identifier, which is automatically
 * defined, if you compile with the Microsoft C/C++ Compiler.
 *
 */

/* Note: this is ANSI C. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#if defined(_MSDOS) || defined(_WIN32)
# include <fcntl.h>
# include <io.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <stdarg.h>
#include <errno.h>
#include <assert.h>
#include <lcdf/clp.h>
#include "t1lib.h"
#include "t1asmhelp.h"

#ifdef __cplusplus
extern "C" {
#endif

typedef unsigned char byte;

static FILE *ofp;
static int unknown = 0;

/* decryption stuff */
static const uint32_t c1 = 52845;
static const uint32_t c2 = 22719;
static uint16_t cr_default = 4330;
static uint16_t er_default = 55665;

static int error_count = 0;


/* Subroutine to output strings. */

static void
output(const char *string)
{
    fprintf(ofp, "%s", string);
}

/* Subroutine to neatly format output of charstring tokens.  If token = "\n",
   then a newline is output.  If at start of line (start == 1), prefix token
   with tab, otherwise a space. */

static void
output_token(const char *token)
{
    static int start = 1;
    if (strcmp(token, "\n") == 0) {
        fprintf(ofp, "\n");
        start = 1;
    } else {
        fprintf(ofp, "%s%s", start ? "\t" : " ", token);
        start = 0;
    }
}

/* Subroutine to decrypt and ASCII-ify tokens in charstring data. The
   charstring decryption machinery is fired up, skipping the first lenIV
   bytes, and the decrypted tokens are expanded into human-readable form. */

static void
decrypt_charstring(unsigned char *line, int len)
{
  int i;
  int32_t val;
  char buf[20];

  /* decrypt charstring */
  if (lenIV >= 0) {
    /* only decrypt if lenIV >= 0 -- negative lenIV means unencrypted
       charstring. Thanks to Tom Kacvinsky <tjk@ams.org> */
    uint16_t cr = cr_default;
    byte plain;
    for (i = 0; i < len; i++) {
      byte cipher = line[i];
      plain = (byte)(cipher ^ (cr >> 8));
      cr = (uint16_t)((cipher + cr) * c1 + c2);
      line[i] = plain;
    }
    line += lenIV;
    len -= lenIV;
  }

  /* handle each charstring command */
  for (i = 0; i < len; i++) {
    byte b = line[i];

    if (b >= 32) {
      if (b >= 32 && b <= 246)
        val = b - 139;
      else if (b >= 247 && b <= 250) {
        i++;
        val = (b - 247)*256 + 108 + line[i];
      } else if (b >= 251 && b <= 254) {
        i++;
        val = -(b - 251)*256 - 108 - line[i];
      } else {
        uint32_t uval;
        uval =  (uint32_t) line[i+1] << 24;
        uval |= (uint32_t) line[i+2] << 16;
        uval |= (uint32_t) line[i+3] <<  8;
        uval |= (uint32_t) line[i+4] <<  0;
        /* in case an int32 is larger than four bytes---sign extend */
#if INT_MAX > 0x7FFFFFFFUL
        if (uval & 0x80000000U)
          uval |= ~0x7FFFFFFFU;
#endif
        val = (int32_t) uval;
        i += 4;
      }
      sprintf(buf, "%d", val);
      output_token(buf);

    } else {
      switch (b) {
      case 0: output_token("error"); break;             /* special */
      case 1: output_token("hstem"); break;
      case 3: output_token("vstem"); break;
      case 4: output_token("vmoveto"); break;
      case 5: output_token("rlineto"); break;
      case 6: output_token("hlineto"); break;
      case 7: output_token("vlineto"); break;
      case 8: output_token("rrcurveto"); break;
      case 9: output_token("closepath"); break;         /* Type 1 ONLY */
      case 10: output_token("callsubr"); break;
      case 11: output_token("return"); break;
      case 13: output_token("hsbw"); break;             /* Type 1 ONLY */
      case 14: output_token("endchar"); break;
      case 16: output_token("blend"); break;            /* Type 2 */
      case 18: output_token("hstemhm"); break;          /* Type 2 */
      case 19: output_token("hintmask"); break;         /* Type 2 */
      case 20: output_token("cntrmask"); break;         /* Type 2 */
      case 21: output_token("rmoveto"); break;
      case 22: output_token("hmoveto"); break;
      case 23: output_token("vstemhm"); break;          /* Type 2 */
      case 24: output_token("rcurveline"); break;       /* Type 2 */
      case 25: output_token("rlinecurve"); break;       /* Type 2 */
      case 26: output_token("vvcurveto"); break;        /* Type 2 */
      case 27: output_token("hhcurveto"); break;        /* Type 2 */
      case 28: {                /* Type 2 */
        /* short integer */
        val =  (line[i+1] & 0xff) << 8;
        val |= (line[i+2] & 0xff);
        i += 2;
        if (val & 0x8000)
          val |= ~0x7FFF;
        sprintf(buf, "%d", val);
        output_token(buf);
      }
      case 29: output_token("callgsubr"); break;        /* Type 2 */
      case 30: output_token("vhcurveto"); break;
      case 31: output_token("hvcurveto"); break;
      case 12:
        i++;
        b = line[i];
        switch (b) {
        case 0: output_token("dotsection"); break;      /* Type 1 ONLY */
        case 1: output_token("vstem3"); break;          /* Type 1 ONLY */
        case 2: output_token("hstem3"); break;          /* Type 1 ONLY */
        case 3: output_token("and"); break;             /* Type 2 */
        case 4: output_token("or"); break;              /* Type 2 */
        case 5: output_token("not"); break;             /* Type 2 */
        case 6: output_token("seac"); break;            /* Type 1 ONLY */
        case 7: output_token("sbw"); break;             /* Type 1 ONLY */
        case 8: output_token("store"); break;           /* Type 2 */
        case 9: output_token("abs"); break;             /* Type 2 */
        case 10: output_token("add"); break;            /* Type 2 */
        case 11: output_token("sub"); break;            /* Type 2 */
        case 12: output_token("div"); break;
        case 13: output_token("load"); break;           /* Type 2 */
        case 14: output_token("neg"); break;            /* Type 2 */
        case 15: output_token("eq"); break;             /* Type 2 */
        case 16: output_token("callothersubr"); break;  /* Type 1 ONLY */
        case 17: output_token("pop"); break;            /* Type 1 ONLY */
        case 18: output_token("drop"); break;           /* Type 2 */
        case 20: output_token("put"); break;            /* Type 2 */
        case 21: output_token("get"); break;            /* Type 2 */
        case 22: output_token("ifelse"); break;         /* Type 2 */
        case 23: output_token("random"); break;         /* Type 2 */
        case 24: output_token("mul"); break;            /* Type 2 */
        case 26: output_token("sqrt"); break;           /* Type 2 */
        case 27: output_token("dup"); break;            /* Type 2 */
        case 28: output_token("exch"); break;           /* Type 2 */
        case 29: output_token("index"); break;          /* Type 2 */
        case 30: output_token("roll"); break;           /* Type 2 */
        case 33: output_token("setcurrentpoint"); break;/* Type 1 ONLY */
        case 34: output_token("hflex"); break;          /* Type 2 */
        case 35: output_token("flex"); break;           /* Type 2 */
        case 36: output_token("hflex1"); break;         /* Type 2 */
        case 37: output_token("flex1"); break;          /* Type 2 */
        default:
          sprintf(buf, "escape_%d", b);
          unknown++;
          output_token(buf);
          break;
        }
        break;
      default:
       sprintf(buf, "UNKNOWN_%d", b);
       unknown++;
       output_token(buf);
       break;
      }
      output_token("\n");
    }
  }
  if (i > len) {
    output("\terror\n");
    error("disassembly error: charstring too short");
  }
}


/* Disassembly font_reader functions */

static int in_eexec = 0;
static unsigned char *save = 0;
static int save_len = 0;
static int save_cap = 0;

static void
append_save(const unsigned char *line, int len)
{
  if (line == save) {
    assert(len <= save_cap);
    save_len = len;
    return;
  }
  if (save_len + len >= save_cap) {
    unsigned char *new_save;
    if (!save_cap) save_cap = 1024;
    while (save_len + len >= save_cap) save_cap *= 2;
    new_save = (unsigned char *)malloc(save_cap);
    if (!new_save)
      fatal_error("out of memory");
    memcpy(new_save, save, save_len);
    free(save);
    save = new_save;
  }
  memcpy(save + save_len, line, len);
  save_len += len;
}


static unsigned char*
check_eexec_charstrings_begin(unsigned char* line, int line_len)
{
    unsigned char* line_end = line + line_len;
    line = memmem(line, line_len, "/CharStrings ", 13);
    if (!line)
        return 0;
    line += 13;
    while (line < line_end && isspace(*line))
        ++line;
    if (line == line_end || !isdigit(*line))
        return 0;
    while (line < line_end && isdigit(*line))
        ++line;
    if (line == line_end || !isspace(*line))
        return 0;
    while (line < line_end && isspace(*line))
        ++line;
    if (line_end - line < 14 || memcmp(line, "dict dup begin", 14) != 0)
        return 0;
    line += 14;
    while (line < line_end && isspace(*line))
        ++line;
    if (line == line_end || *line != '/')
        return 0;
    return line;
}


/* returns 1 if next \n should be deleted */

static int
eexec_line(unsigned char *line, int line_len)
{
    int cs_start_len = strlen(cs_start);
    int pos;
    int first_space;
    int digits;
    int cut_newline = 0;

    /* append this data to the end of `save' if necessary */
    if (save_len) {
        append_save(line, line_len);
        line = save;
        line_len = save_len;
        save_len = 0;
    }

    if (!line_len)
        return 0;

    /* Look for charstring start */

    /* skip first word */
    for (pos = 0; pos < line_len && isspace(line[pos]); pos++)
        ;
    while (pos < line_len && !isspace(line[pos]))
        pos++;
    if (pos >= line_len)
        goto not_charstring;

    /* skip spaces */
    first_space = pos;
    while (pos < line_len && isspace(line[pos]))
        pos++;
    if (pos >= line_len || !isdigit(line[pos]))
        goto not_charstring;

    /* skip number */
    digits = pos;
    while (pos < line_len && isdigit(line[pos]))
        pos++;

    /* check for subr (another number) */
    if (pos < line_len - 1 && isspace(line[pos]) && isdigit(line[pos+1])) {
        first_space = pos;
        digits = pos + 1;
        for (pos = digits; pos < line_len && isdigit(line[pos]); pos++)
            ;
    }

    /* check for charstring start */
    if (pos + 2 + cs_start_len < line_len
        && pos > digits
        && line[pos] == ' '
        && strncmp((const char *)(line + pos + 1), cs_start, cs_start_len) == 0
        && line[pos + 1 + cs_start_len] == ' ') {
        /* check if charstring is long enough */
        int cs_len = atoi((const char *)(line + digits));
        if (pos + 2 + cs_start_len + cs_len < line_len) {
            /* long enough! */
            if (line[line_len - 1] == '\r') {
                line[line_len - 1] = '\n';
                cut_newline = 1;
            }
            fprintf(ofp, "%.*s {\n", first_space, line);
            decrypt_charstring(line + pos + 2 + cs_start_len, cs_len);
            pos += 2 + cs_start_len + cs_len;
            fprintf(ofp, "\t}%.*s", line_len - pos, line + pos);
            return cut_newline;
        } else {
            /* not long enough! */
            append_save(line, line_len);
            return 0;
        }
    }

    /* otherwise, just output the line */
  not_charstring:
    /* 6.Oct.2003 - Werner Lemberg reports a stupid Omega font that behaves
       badly: a charstring definition follows "/Charstrings ... begin", ON THE
       SAME LINE. */
    {
        unsigned char* csbegin = check_eexec_charstrings_begin(line, line_len);
        if (csbegin) {
            int len = csbegin - line;
            fprintf(ofp, "%.*s\n", len, line);
            return eexec_line(csbegin, line_len - len);
        }
    }

    if (line[line_len - 1] == '\r') {
        line[line_len - 1] = '\n';
        cut_newline = 1;
    }
    set_lenIV((char*) line, line_len);
    set_cs_start((char*) line, line_len);
    fprintf(ofp, "%.*s", line_len, line);

    /* look for `currentfile closefile' to see if we should stop decrypting */
    if (memmem(line, line_len, "currentfile closefile", 21) != 0)
        in_eexec = -1;

    return cut_newline;
}

static int
all_zeroes(const char *string)
{
    if (*string != '0')
        return 0;
    while (*string == '0')
        string++;
    return *string == '\0' || *string == '\n';
}

static void
disasm_output_ascii(char *line, int len)
{
    int was_in_eexec = in_eexec;
    (void) len;                 /* avoid warning */
    in_eexec = 0;

    /* if we came from a binary section, we need to process that too */
    if (was_in_eexec > 0) {
        unsigned char zero = 0;
        eexec_line(&zero, 0);
    }

    /* if we just came from the "ASCII part" of an eexec section, we need to
       process the saved lines */
    if (was_in_eexec < 0) {
        int i = 0;
        int save_char = 0;      /* note: save[] is unsigned char * */

        while (i < save_len) {
            /* grab a line */
            int start = i;
            while (i < save_len && save[i] != '\r' && save[i] != '\n')
                i++;
            if (i < save_len) {
                if (i < save_len - 1 && save[i] == '\r' && save[i+1] == '\n')
                    save_char = -1;
                else
                    save_char = save[i+1];
                save[i] = '\n';
                save[i+1] = 0;
            } else
                save[i] = 0;

            /* output it */
            disasm_output_ascii((char *)(save + start), -1);

            /* repair damage */
            if (i < save_len) {
                if (save_char >= 0) {
                    save[i+1] = save_char;
                    i++;
                } else
                    i += 2;
            }
        }
        save_len = 0;
    }

    if (!all_zeroes(line))
        output(line);
}

/* collect until '\n' or end of binary section */

static void
disasm_output_binary(unsigned char *data, int len)
{
    static int ignore_newline;
    static uint16_t er;

    byte plain;
    int i;

    /* in the ASCII portion of a binary section, just save this data */
    if (in_eexec < 0) {
        append_save(data, len);
        return;
    }

    /* eexec initialization */
    if (in_eexec == 0) {
        er = er_default;
        ignore_newline = 0;
        in_eexec = 0;
    }
    if (in_eexec < 4) {
        for (i = 0; i < len && in_eexec < 4; i++, in_eexec++) {
            byte cipher = data[i];
            plain = (byte)(cipher ^ (er >> 8));
            er = (uint16_t)((cipher + er) * c1 + c2);
            data[i] = plain;
        }
        data += i;
        len -= i;
    }

    /* now make lines: collect until '\n' or '\r' and pass them off to
       eexec_line. */
    i = 0;
    while (in_eexec > 0) {
        int start = i;

        for (; i < len; i++) {
            byte cipher = data[i];
            plain = (byte)(cipher ^ (er >> 8));
            er = (uint16_t)((cipher + er) * c1 + c2);
            data[i] = plain;
            if (plain == '\r' || plain == '\n')
                break;
        }

        if (ignore_newline && start < i && data[start] == '\n') {
            ignore_newline = 0;
            continue;
        }

        if (i >= len) {
            if (start < len)
                append_save(data + start, i - start);
            break;
        }

        i++;
        ignore_newline = eexec_line(data + start, i - start);
    }

    /* if in_eexec < 0, we have some plaintext lines sitting around in a binary
       section of the PFB. save them for later */
    if (in_eexec < 0 && i < len)
        append_save(data + i, len - i);
}

static void
disasm_output_end(void)
{
    /* take care of leftover saved data */
    static char crap[1] = "";
    disasm_output_ascii(crap, 0);
}


/*****
 * Command line
 **/

#define OUTPUT_OPT      301
#define VERSION_OPT     302
#define HELP_OPT        303

static Clp_Option options[] = {
  { "help", 0, HELP_OPT, 0, 0 },
  { "output", 'o', OUTPUT_OPT, Clp_ValString, 0 },
  { "version", 0, VERSION_OPT, 0, 0 },
};
static const char *program_name;

void
fatal_error(const char *message, ...)
{
  va_list val;
  va_start(val, message);
  fprintf(stderr, "%s: ", program_name);
  vfprintf(stderr, message, val);
  fputc('\n', stderr);
  va_end(val);
  exit(1);
}

void
error(const char *message, ...)
{
  va_list val;
  va_start(val, message);
  fprintf(stderr, "%s: ", program_name);
  vfprintf(stderr, message, val);
  fputc('\n', stderr);
  error_count++;
  va_end(val);
}

static void
short_usage(void)
{
  fprintf(stderr, "Usage: %s [INPUT [OUTPUT]]\n\
Try `%s --help' for more information.\n",
          program_name, program_name);
}

static void
usage(void)
{
  printf("\
`T1disasm' translates a PostScript Type 1 font in PFB or PFA format into a\n\
human-readable, human-editable form. The result is written to the standard\n\
output unless an OUTPUT file is given.\n\
\n\
Usage: %s [OPTION]... [INPUT [OUTPUT]]\n\
\n\
Options:\n\
  -o, --output=FILE             Write output to FILE.\n\
  -h, --help                    Print this message and exit.\n\
      --version                 Print version number and warranty and exit.\n\
\n\
Report bugs to <ekohler@gmail.com>.\n", program_name);
}

#ifdef __cplusplus
}
#endif


int
main(int argc, char *argv[])
{
  struct font_reader fr;
  int c;
  FILE *ifp = 0;
  const char *ifp_filename = "<stdin>";

  Clp_Parser *clp =
    Clp_NewParser(argc, (const char * const *)argv, sizeof(options) / sizeof(options[0]), options);
  program_name = Clp_ProgramName(clp);

  /* interpret command line arguments using CLP */
  while (1) {
    int opt = Clp_Next(clp);
    switch (opt) {

     output_file:
     case OUTPUT_OPT:
      if (ofp)
        fatal_error("output file already specified");
      if (strcmp(clp->vstr, "-") == 0)
        ofp = stdout;
      else {
        ofp = fopen(clp->vstr, "w");
        if (!ofp) fatal_error("%s: %s", clp->vstr, strerror(errno));
      }
      break;

     case HELP_OPT:
      usage();
      exit(0);
      break;

     case VERSION_OPT:
      printf("t1disasm (LCDF t1utils) %s\n", VERSION);
      printf("Copyright (C) 1992-2017 I. Lee Hetherington, Eddie Kohler et al.\n\
This is free software; see the source for copying conditions.\n\
There is NO warranty, not even for merchantability or fitness for a\n\
particular purpose.\n");
      exit(0);
      break;

     case Clp_NotOption:
      if (ifp && ofp)
        fatal_error("too many arguments");
      else if (ifp)
        goto output_file;
      if (strcmp(clp->vstr, "-") == 0)
        ifp = stdin;
      else {
        ifp_filename = clp->vstr;
        ifp = fopen(clp->vstr, "rb");
        if (!ifp) fatal_error("%s: %s", clp->vstr, strerror(errno));
      }
      break;

     case Clp_Done:
      goto done;

     case Clp_BadOption:
      short_usage();
      exit(1);
      break;

    }
  }

 done:
  if (!ifp) ifp = stdin;
  if (!ofp) ofp = stdout;

#if defined(_MSDOS) || defined(_WIN32)
  /* As we might be processing a PFB (binary) input file, we must set its file
     mode to binary. */
  _setmode(_fileno(ifp), _O_BINARY);
#endif

  /* prepare font reader */
  fr.output_ascii = disasm_output_ascii;
  fr.output_binary = disasm_output_binary;
  fr.output_end = disasm_output_end;

  /* peek at first byte to see if it is the PFB marker 0x80 */
  c = getc(ifp);
  ungetc(c, ifp);

  /* do the file */
  if (c == PFB_MARKER)
    process_pfb(ifp, ifp_filename, &fr);
  else if (c == '%')
    process_pfa(ifp, ifp_filename, &fr);
  else
    fatal_error("%s does not start with font marker (`%%' or 0x80)", ifp_filename);

  fclose(ifp);
  fclose(ofp);

  if (unknown)
    error((unknown > 1
           ? "encountered %d unknown charstring commands"
           : "encountered %d unknown charstring command"),
          unknown);

  return (error_count ? 1 : 0);
}
