// clang-format off
/*
  fileio.c - Zip 3

  Copyright (c) 1990-2008 Info-ZIP.  All rights reserved.

  See the accompanying file LICENSE, version 2007-Mar-4 or later
  (the contents of which are also included in zip.h) for terms of use.
  If, for some reason, all these files are missing, the Info-ZIP license
  also may be found at:  ftp://ftp.info-zip.org/pub/infozip/license.html
*/
/*
 *  fileio.c by Mark Adler
 */
#define __FILEIO_C

#include "third_party/zip/zip.h"
#include "third_party/zip/crc32.h"

#ifdef MACOS
// MISSING #include "helpers.h"
#endif

#ifdef VMS
// MISSING #include "vms/vms.h"
#endif /* def VMS */

#include "libc/calls/struct/timespec.h"
#include "libc/calls/struct/timeval.h"
#include "libc/calls/weirdtypes.h"
#include "libc/sysv/consts/clock.h"
#include "libc/sysv/consts/sched.h"
#include "libc/sysv/consts/timer.h"
#include "libc/time/struct/tm.h"
#include "libc/time/time.h"

#ifdef NO_MKTIME
time_t mktime OF((struct tm *));
#endif

#ifdef OSF
#define EXDEV 18   /* avoid a bug in the DEC OSF/1 header files. */
#else
#include "libc/errno.h"
#endif

#ifdef NO_ERRNO
extern int errno;
#endif

/* -----------------------
   For long option support
   ----------------------- */
#include "libc/str/str.h"


#if defined(VMS) || defined(TOPS20)
#  define PAD 5
#else
#  define PAD 0
#endif

#ifdef NO_RENAME
int rename OF((ZCONST char *, ZCONST char *));
#endif


/* Local functions */
local int optionerr OF((char *, ZCONST char *, int, int));
local unsigned long get_shortopt OF((char **, int, int *, int *, char **, int *, int));
local unsigned long get_longopt OF((char **, int, int *, int *, char **, int *, int));

#ifdef UNICODE_SUPPORT
local int utf8_char_bytes OF((ZCONST char *utf8));
local long ucs4_char_from_utf8 OF((ZCONST char **utf8 ));
local int utf8_from_ucs4_char OF((char *utf8buf, ulg ch));
local int utf8_to_ucs4_string OF((ZCONST char *utf8, ulg *usc4buf,
                                  int buflen));
local int ucs4_string_to_utf8 OF((ZCONST ulg *ucs4, char *utf8buf,
                                  int buflen));
#if 0
  local int utf8_chars OF((ZCONST char *utf8));
#endif
#endif /* UNICODE_SUPPORT */

#ifndef UTIL    /* the companion #endif is a bit of ways down ... */

local int fqcmp  OF((ZCONST zvoid *, ZCONST zvoid *));
local int fqcmpz OF((ZCONST zvoid *, ZCONST zvoid *));


/* Local module level variables. */
char *label = NULL;                /* global, but only used in `system'.c */
local z_stat zipstatb;             /* now use z_stat globally - 7/24/04 EG */
#if defined(UNICODE_SUPPORT) && defined(WIN32)
 local zw_stat zipstatbw;
#endif
#if (!defined(MACOS) && !defined(WINDLL))
local int zipstate = -1;
#else
int zipstate;
#endif
/* -1 unknown, 0 old zip file exists, 1 new zip file */

#if 0
char *getnam(n, fp)
char *n;                /* where to put name (must have >=FNMAX+1 bytes) */
#endif

/* converted to return string pointer from malloc to avoid
   size limitation - 11/8/04 EG */
#define GETNAM_MAX 9000 /* hopefully big enough for now */
char *getnam(fp)
  FILE *fp;
  /* Read a \n or \r delimited name from stdin into n, and return
     n.  If EOF, then return NULL.  Also, if problem return NULL. */
{
  char name[GETNAM_MAX + 1];
  int c;                /* last character read */
  char *p;              /* pointer into name area */


  p = name;
  while ((c = getc(fp)) == '\n' || c == '\r')
    ;
  if (c == EOF)
    return NULL;
  do {
    if (p - name >= GETNAM_MAX)
      return NULL;
    *p++ = (char) c;
    c = getc(fp);
  } while (c != EOF && (c != '\n' && c != '\r'));
#ifdef WIN32
/*
 * WIN32 strips off trailing spaces and periods in filenames
 * XXX what about a filename that only consists of spaces ?
 *     Answer: on WIN32, a filename must contain at least one non-space char
 */
  while (p > name) {
    if ((c = p[-1]) != ' ' && c != '.')
      break;
    --p;
  }
#endif
  *p = 0;
  /* malloc a copy */
  if ((p = malloc(strlen(name) + 1)) == NULL) {
    return NULL;
  }
  strcpy(p, name);
  return p;
}

struct flist far *fexpel(f)
struct flist far *f;    /* entry to delete */
/* Delete the entry *f in the doubly-linked found list.  Return pointer to
   next entry to allow stepping through list. */
{
  struct flist far *t;  /* temporary variable */

  t = f->nxt;
  *(f->lst) = t;                        /* point last to next, */
  if (t != NULL)
    t->lst = f->lst;                    /* and next to last */
  if (f->name != NULL)                  /* free memory used */
    free((zvoid *)(f->name));
  if (f->zname != NULL)
    free((zvoid *)(f->zname));
  if (f->iname != NULL)
    free((zvoid *)(f->iname));
#ifdef UNICODE_SUPPORT
  if (f->uname)
    free((zvoid *)f->uname);
# ifdef WIN32
  if (f->namew)
    free((zvoid *)f->namew);
  if (f->inamew)
    free((zvoid *)f->inamew);
  if (f->znamew)
    free((zvoid *)f->znamew);
# endif
#endif
  farfree((zvoid far *)f);
  fcount--;                             /* decrement count */
  return t;                             /* return pointer to next */
}

local int fqcmp(a, b)
  ZCONST zvoid *a, *b;          /* pointers to pointers to found entries */
/* Used by qsort() to compare entries in the found list by name. */
{
  return strcmp((*(struct flist far **)a)->name,
                (*(struct flist far **)b)->name);
}

local int fqcmpz(a, b)
  ZCONST zvoid *a, *b;          /* pointers to pointers to found entries */
/* Used by qsort() to compare entries in the found list by iname. */
{
  return strcmp((*(struct flist far **)a)->iname,
                (*(struct flist far **)b)->iname);
}

char *last(p, c)
  char *p;                /* sequence of path components */
  int c;                  /* path components separator character */
/* Return a pointer to the start of the last path component. For a directory
 * name terminated by the character in c, the return value is an empty string.
 */
{
  char *t;              /* temporary variable */

  if ((t = strrchr(p, c)) != NULL)
    return t + 1;
  else
#ifndef AOS_VS
    return p;
#else
/* We want to allow finding of end of path in either AOS/VS-style pathnames
 * or Unix-style pathnames.  This presents a few little problems ...
 */
  {
    if (*p == '='  ||  *p == '^')      /* like ./ and ../ respectively */
      return p + 1;
    else
      return p;
  }
#endif
}

#if defined(UNICODE_SUPPORT) && defined(WIN32)
wchar_t *lastw(pw, c)
  wchar_t *pw;            /* sequence of path components */
  wchar_t c;              /* path components separator character */
/* Return a pointer to the start of the last path component. For a directory
 * name terminated by the character in c, the return value is an empty string.
 */
{
  wchar_t *tw;            /* temporary variable */

  if ((tw = wcsrchr(pw, c)) != NULL)
    return tw + 1;
  else
# ifndef AOS_VS
    return pw;
# else
/* We want to allow finding of end of path in either AOS/VS-style pathnames
 * or Unix-style pathnames.  This presents a few little problems ...
 */
  {
    if (*pw == (wchar_t)'='  ||  *pw == (wchar_t)'^')      /* like ./ and ../ respectively */
      return pw + 1;
    else
      return pw;
  }
# endif
}
#endif


char *msname(n)
  char *n;
/* Reduce all path components to MSDOS upper case 8.3 style names. */
{
  int c;                /* current character */
  int f;                /* characters in current component */
  char *p;              /* source pointer */
  char *q;              /* destination pointer */

  p = q = n;
  f = 0;
  while ((c = (unsigned char)*POSTINCSTR(p)) != 0)
    if (c == ' ' || c == ':' || c == '"' || c == '*' || c == '+' ||
        c == ',' || c == ';' || c == '<' || c == '=' || c == '>' ||
        c == '?' || c == '[' || c == ']' || c == '|')
      continue;                         /* char is discarded */
    else if (c == '/')
    {
      *POSTINCSTR(q) = (char)c;
      f = 0;                            /* new component */
    }
#ifdef __human68k__
    else if (ismbblead(c) && *p)
    {
      if (f == 7 || f == 11)
        f++;
      else if (*p && f < 12 && f != 8)
      {
        *q++ = c;
        *q++ = *p++;
        f += 2;
      }
    }
#endif /* __human68k__ */
    else if (c == '.')
    {
      if (f == 0)
        continue;                       /* leading dots are discarded */
      else if (f < 9)
      {
        *POSTINCSTR(q) = (char)c;
        f = 9;                          /* now in file type */
      }
      else
        f = 12;                         /* now just excess characters */
    }
    else
      if (f < 12 && f != 8)
      {
        f += CLEN(p);                   /* do until end of name or type */
        *POSTINCSTR(q) = (char)(to_up(c));
      }
  *q = 0;
  return n;
}

#ifdef UNICODE_SUPPORT
wchar_t *msnamew(nw)
  wchar_t *nw;
/* Reduce all path components to MSDOS upper case 8.3 style names. */
{
  wchar_t c;            /* current character */
  int f;                /* characters in current component */
  wchar_t *pw;          /* source pointer */
  wchar_t *qw;          /* destination pointer */

  pw = qw = nw;
  f = 0;
  while ((c = (unsigned char)*pw++) != 0)
    if (c == ' ' || c == ':' || c == '"' || c == '*' || c == '+' ||
        c == ',' || c == ';' || c == '<' || c == '=' || c == '>' ||
        c == '?' || c == '[' || c == ']' || c == '|')
      continue;                         /* char is discarded */
    else if (c == '/')
    {
      *qw++ = c;
      f = 0;                            /* new component */
    }
#ifdef __human68k__
    else if (ismbblead(c) && *pw)
    {
      if (f == 7 || f == 11)
        f++;
      else if (*pw && f < 12 && f != 8)
      {
        *qw++ = c;
        *qw++ = *pw++;
        f += 2;
      }
    }
#endif /* __human68k__ */
    else if (c == '.')
    {
      if (f == 0)
        continue;                       /* leading dots are discarded */
      else if (f < 9)
      {
        *qw++ = c;
        f = 9;                          /* now in file type */
      }
      else
        f = 12;                         /* now just excess characters */
    }
    else
      if (f < 12 && f != 8)
      {
        f++;                            /* do until end of name or type */
        *qw++ = towupper(c);
      }
  *qw = 0;
  return nw;
}
#endif


int proc_archive_name(n, caseflag)
  char *n;             /* name to process */
  int caseflag;         /* true to force case-sensitive match */
/* Process a name or sh expression in existing archive to operate
   on (or exclude).  Return an error code in the ZE_ class. */
{
  int m;                /* matched flag */
  char *p;              /* path for recursion */
  struct zlist far *z;  /* steps through zfiles list */

  if (strcmp(n, "-") == 0) {   /* if compressing stdin */
    zipwarn("Cannot select stdin when selecting archive entries", "");
    return ZE_MISS;
  }
  else
  {
    /* Search for shell expression in zip file */
    p = ex2in(n, 0, (int *)NULL);       /* shouldn't affect matching chars */
    m = 1;
    for (z = zfiles; z != NULL; z = z->nxt) {
      if (MATCH(p, z->iname, caseflag))
      {
        z->mark = pcount ? filter(z->zname, caseflag) : 1;
        if (verbose)
            fprintf(mesg, "zip diagnostic: %scluding %s\n",
               z->mark ? "in" : "ex", z->oname);
        m = 0;
      }
    }
#ifdef UNICODE_SUPPORT
    /* also check escaped Unicode names */
    for (z = zfiles; z != NULL; z = z->nxt) {
      if (z->zuname) {
#ifdef WIN32
        /* It seems something is lost in going from a listed
           name from zip -su in a console window to using that
           name in a command line.  This kluge may fix it
           and just takes zuname, converts to oem (i.e. ouname),
           then converts it back which ends up not the same as
           started with.
         */
        char *zuname = z->wuname;
#else
        char *zuname = z->zuname;
#endif
        if (MATCH(p, zuname, caseflag))
        {
          z->mark = pcount ? filter(zuname, caseflag) : 1;
          if (verbose) {
              fprintf(mesg, "zip diagnostic: %scluding %s\n",
                 z->mark ? "in" : "ex", z->oname);
              fprintf(mesg, "     Escaped Unicode:  %s\n",
                 z->ouname);
          }
          m = 0;
        }
      }
    }
#endif
    free((zvoid *)p);
    return m ? ZE_MISS : ZE_OK;
  }
}


int check_dup()
/* Sort the found list and remove duplicates.
   Return an error code in the ZE_ class. */
{
  struct flist far *f;          /* steps through found linked list */
  extent j, k;                  /* indices for s */
  struct flist far **s;         /* sorted table */
  struct flist far **nodup;     /* sorted table without duplicates */

  /* sort found list, remove duplicates */
  if (fcount)
  {
    extent fl_size = fcount * sizeof(struct flist far *);
    if ((fl_size / sizeof(struct flist far *)) != fcount ||
        (s = (struct flist far **)malloc(fl_size)) == NULL)
      return ZE_MEM;
    for (j = 0, f = found; f != NULL; f = f->nxt)
      s[j++] = f;
    /* Check names as given (f->name) */
    qsort((char *)s, fcount, sizeof(struct flist far *), fqcmp);
    for (k = j = fcount - 1; j > 0; j--)
      if (strcmp(s[j - 1]->name, s[j]->name) == 0)
        /* remove duplicate entry from list */
        fexpel(s[j]);           /* fexpel() changes fcount */
      else
        /* copy valid entry into destination position */
        s[k--] = s[j];
    s[k] = s[0];                /* First entry is always valid */
    nodup = &s[k];              /* Valid entries are at end of array s */

    /* sort only valid items and check for unique internal names (f->iname) */
    qsort((char *)nodup, fcount, sizeof(struct flist far *), fqcmpz);
    for (j = 1; j < fcount; j++)
      if (strcmp(nodup[j - 1]->iname, nodup[j]->iname) == 0)
      {
        char tempbuf[FNMAX+4081];

        sprintf(errbuf, "  first full name: %s\n", nodup[j - 1]->name);
        sprintf(tempbuf, " second full name: %s\n", nodup[j]->name);
        strcat(errbuf, "                     ");
        strcat(errbuf, tempbuf);
#ifdef EBCDIC
        strtoebc(nodup[j]->iname, nodup[j]->iname);
#endif
        sprintf(tempbuf, "name in zip file repeated: %s", nodup[j]->iname);
        strcat(errbuf, "                     ");
        strcat(errbuf, tempbuf);
        if (pathput == 0) {
          strcat(errbuf, "\n                     this may be a result of using -j");
        }
#ifdef EBCDIC
        strtoasc(nodup[j]->iname, nodup[j]->iname);
#endif
        zipwarn(errbuf, "");
        return ZE_PARMS;
      }
    free((zvoid *)s);
  }
  return ZE_OK;
}

int filter(name, casesensitive)
  char *name;
  int casesensitive;
  /* Scan the -R, -i and -x lists for matches to the given name.
     Return TRUE if the name must be included, FALSE otherwise.
     Give precedence to -x over -i and -R.
     Note that if both R and i patterns are given then must
     have a match for both.
     This routine relies on the following global variables:
       patterns                 array of match pattern structures
       pcount                   total number of patterns
       icount                   number of -i patterns
       Rcount                   number of -R patterns
     These data are set up by the command line parsing code.
   */
{
   unsigned int n;
   int slashes;
   char *p, *q;
   /* without -i patterns, every name matches the "-i select rules" */
   int imatch = (icount == 0);
   /* without -R patterns, every name matches the "-R select rules" */
   int Rmatch = (Rcount == 0);

   if (pcount == 0) return TRUE;

   for (n = 0; n < pcount; n++) {
      if (!patterns[n].zname[0])        /* it can happen... */
         continue;
      p = name;
      switch (patterns[n].select) {
       case 'R':
         if (Rmatch)
            /* one -R match is sufficient, skip this pattern */
            continue;
         /* With -R patterns, if the pattern has N path components (that is,
            N-1 slashes), then we test only the last N components of name.
          */
         slashes = 0;
         for (q = patterns[n].zname; (q = MBSCHR(q, '/')) != NULL; MB_NEXTCHAR(q))
            slashes++;
         /* The name may have M path components (M-1 slashes) */
         for (q = p; (q = MBSCHR(q, '/')) != NULL; MB_NEXTCHAR(q))
            slashes--;
         /* Now, "slashes" contains the difference "N-M" between the number
            of path components in the pattern (N) and in the name (M).
          */
         if (slashes < 0)
            /* We found "M > N"
                --> skip the first (M-N) path components of the name.
             */
            for (q = p; (q = MBSCHR(q, '/')) != NULL; MB_NEXTCHAR(q))
               if (++slashes == 0) {
                  p = q + 1;    /* q points at '/', mblen("/") is 1 */
                  break;
               }
         break;
       case 'i':
         if (imatch)
            /* one -i match is sufficient, skip this pattern */
            continue;
         break;
      }
      if (MATCH(patterns[n].zname, p, casesensitive)) {
         switch (patterns[n].select) {
            case 'x':
               /* The -x match takes precedence over everything else */
               return FALSE;
            case 'R':
               Rmatch = TRUE;
               break;
            default:
               /* this must be a type -i match */
               imatch = TRUE;
               break;
         }
      }
   }
   return imatch && Rmatch;
}


#ifdef UNICODE_SUPPORT
# ifdef WIN32

int newnamew(namew, isdir, casesensitive)
  wchar_t *namew;             /* name to add (or exclude) */
  int  isdir;                 /* true for a directory */
  int  casesensitive;         /* true for case-sensitive matching */
/* Add (or exclude) the name of an existing disk file.  Return an error
   code in the ZE_ class. */
{
  wchar_t *inamew = NULL;     /* internal name */
  wchar_t *znamew = NULL;     /* external version of iname */
  wchar_t *undosmw = NULL;    /* zname version with "-j" and "-k" options disabled */
  char *oname = NULL;         /* iname converted for display */
  char *name = NULL;
  char *iname = NULL;
  char *zname = NULL;
  char *zuname = NULL;
  char *undosm = NULL;
  struct flist far *f;        /* where in found, or new found entry */
  struct zlist far *z;        /* where in zfiles (if found) */
  int dosflag;

  /* Scanning files ...
   *
   * After 5 seconds output Scanning files...
   * then a dot every 2 seconds
   */
  if (noisy) {
    /* If find files then output message after delay */
    if (scan_count == 0) {
      time_t current = time(NULL);
      scan_start = current;
    }
    scan_count++;
    if (scan_count % 100 == 0) {
      time_t current = time(NULL);

      if (current - scan_start > scan_delay) {
        if (scan_last == 0) {
          zipmessage_nl("Scanning files ", 0);
          scan_last = current;
        }
        if (current - scan_last > scan_dot_time) {
          scan_last = current;
          fprintf(mesg, ".");
          fflush(mesg);
        }
      }
    }
  }

  /* Search for name in zip file.  If there, mark it, else add to
     list of new names to do (or remove from that list). */
  if ((inamew = ex2inw(namew, isdir, &dosflag)) == NULL)
    return ZE_MEM;

  /* Discard directory names with zip -rj */
  if (*inamew == (wchar_t)'\0') {

 /* If extensions needs to be swapped, we will have empty directory names
    instead of the original directory. For example, zipping 'c.', 'c.main'
    should zip only 'main.c' while 'c.' will be converted to '\0' by ex2in. */

    if (pathput && !recurse) error("empty name without -j or -r");
    free((zvoid *)inamew);
    return ZE_OK;
  }

  if (dosflag || !pathput) {
    int save_dosify = dosify, save_pathput = pathput;
    dosify = 0;
    pathput = 1;
    /* zname is temporarly mis-used as "undosmode" iname pointer */
    if ((znamew = ex2inw(namew, isdir, NULL)) != NULL) {
      undosmw = in2exw(znamew);
      free(znamew);
    }
    dosify = save_dosify;
    pathput = save_pathput;
  }
  if ((znamew = in2exw(inamew)) == NULL)
    return ZE_MEM;

  /* Convert names from wchar_t to char */

  name = wchar_to_local_string(namew);
  iname = wchar_to_local_string(inamew);
  zname = wchar_to_local_string(znamew);

  oname = local_to_display_string(zname);

  zuname = wchar_to_local_string(znamew);

  if (undosmw == NULL)
    undosmw = znamew;
  undosm = wchar_to_local_string(undosmw);

  if ((z = zsearch(zuname)) != NULL) {
    if (pcount && !filter(undosm, casesensitive)) {
      /* Do not clear z->mark if "exclude", because, when "dosify || !pathput"
       * is in effect, two files with different filter options may hit the
       * same z entry.
       */
      if (verbose)
        fprintf(mesg, "excluding %s\n", oname);
    } else {
      z->mark = 1;
      if ((z->name = malloc(strlen(name) + 1 + PAD)) == NULL) {
        if (undosmw != znamew)
          free(undosmw);
        if (undosm) free(undosm);
        if (inamew) free(inamew);
        if (znamew) free(znamew);
        if (name) free(name);
        if (iname) free(iname);
        if (zname) free(zname);
        if (oname) free(oname);
        if (zuname) free(zuname);
        return ZE_MEM;
      }
      strcpy(z->name, name);
      z->oname = oname;
      oname = NULL;
      z->dosflag = dosflag;

#ifdef FORCE_NEWNAME
      free((zvoid *)(z->iname));
      z->iname = iname;
      iname = NULL;
#else
      /* Better keep the old name. Useful when updating on MSDOS a zip file
       * made on Unix.
       */
#endif /* ? FORCE_NEWNAME */
    }

    if ((z->namew = (wchar_t *)malloc((wcslen(namew) + 1) * sizeof(wchar_t))) == NULL) {
      if (undosmw != znamew)
        free(undosmw);
      if (undosm) free(undosm);
      if (inamew) free(inamew);
      if (znamew) free(znamew);
      if (name) free(name);
      if (iname) free(iname);
      if (zname) free(zname);
      if (oname) free(oname);
      if (zuname) free(zuname);
      return ZE_MEM;
    }
    wcscpy(z->namew, namew);
    z->inamew = inamew;
    inamew = NULL;
    z->znamew = znamew;
    znamew = NULL;
    z->uname = wchar_to_utf8_string(z->inamew);
    if (name == label) {
       label = z->name;
    }
  } else if (pcount == 0 || filter(undosm, casesensitive)) {

    /* Check that we are not adding the zip file to itself. This
     * catches cases like "zip -m foo ../dir/foo.zip".
     */
/* Version of stat() for CMS/MVS isn't complete enough to see if       */
/* files match.  Just let ZIP.C compare the filenames.  That's good    */
/* enough for CMS anyway since there aren't paths to worry about.      */
    zw_stat statbw;     /* need for wide stat */
    wchar_t *zipfilew = local_to_wchar_string(zipfile);

    if (zipstate == -1)
       zipstate = strcmp(zipfile, "-") != 0 &&
                   zwstat(zipfilew, &zipstatbw) == 0;
    free(zipfilew);

    if (zipstate == 1 && (statbw = zipstatbw, zwstat(namew, &statbw) == 0
      && zipstatbw.st_mode  == statbw.st_mode
      && zipstatbw.st_ino   == statbw.st_ino
      && zipstatbw.st_dev   == statbw.st_dev
      && zipstatbw.st_uid   == statbw.st_uid
      && zipstatbw.st_gid   == statbw.st_gid
      && zipstatbw.st_size  == statbw.st_size
      && zipstatbw.st_mtime == statbw.st_mtime
      && zipstatbw.st_ctime == statbw.st_ctime)) {
      /* Don't compare a_time since we are reading the file */
        if (verbose)
          fprintf(mesg, "file matches zip file -- skipping\n");
        if (undosmw != znamew)
          free(undosmw);
        if (undosm) free(undosm);
        if (inamew) free(inamew);
        if (znamew) free(znamew);
        if (name) free(name);
        if (iname) free(iname);
        if (zname) free(zname);
        if (oname) free(oname);
        if (zuname) free(zuname);
        return ZE_OK;
    }

    /* allocate space and add to list */
    if ((f = (struct flist far *)farmalloc(sizeof(struct flist))) == NULL ||
        fcount + 1 < fcount ||
        (f->name = malloc(strlen(name) + 1 + PAD)) == NULL)
    {
      if (f != NULL)
        farfree((zvoid far *)f);
      if (undosmw != znamew)
        free(undosmw);
      if (undosm) free(undosm);
      if (inamew) free(inamew);
      if (znamew) free(znamew);
      if (name) free(name);
      if (iname) free(iname);
      if (zname) free(zname);
      if (oname) free(oname);
      if (zuname) free(zuname);
      return ZE_MEM;
    }
    if (undosmw != znamew)
      free((zvoid *)undosmw);
    strcpy(f->name, name);
    f->iname = iname;
    iname = NULL;
    f->zname = zname;
    zname = NULL;
    /* Unicode */
    if ((f->namew = (wchar_t *)malloc((wcslen(namew) + 1) * sizeof(wchar_t))) == NULL) {
      if (f != NULL)
        farfree((zvoid far *)f);
      if (undosmw != znamew)
        free(undosmw);
      if (undosm) free(undosm);
      if (inamew) free(inamew);
      if (znamew) free(znamew);
      if (name) free(name);
      if (iname) free(iname);
      if (zname) free(zname);
      if (oname) free(oname);
      if (zuname) free(zuname);
      return ZE_MEM;
    }
    wcscpy(f->namew, namew);
    f->znamew = znamew;
    znamew = NULL;
    f->uname = wchar_to_utf8_string(inamew);
    f->inamew = inamew;
    inamew = NULL;
    f->oname = oname;
    oname = NULL;
    f->dosflag = dosflag;
    *fnxt = f;
    f->lst = fnxt;
    f->nxt = NULL;
    fnxt = &f->nxt;
    fcount++;
    if (name == label) {
      label = f->name;
    }
  }
  if (undosm) free(undosm);
  if (inamew) free(inamew);
  if (znamew) free(znamew);
  if (name) free(name);
  if (iname) free(iname);
  if (zname) free(zname);
  if (oname) free(oname);
  if (zuname) free(zuname);
  return ZE_OK;
}

# endif
#endif

int newname(name, isdir, casesensitive)
  char *name;           /* name to add (or exclude) */
  int  isdir;           /* true for a directory */
  int  casesensitive;   /* true for case-sensitive matching */
/* Add (or exclude) the name of an existing disk file.  Return an error
   code in the ZE_ class. */
{
  char *iname, *zname;  /* internal name, external version of iname */
  char *undosm;         /* zname version with "-j" and "-k" options disabled */
  char *oname;          /* iname converted for display */
  struct flist far *f;  /* where in found, or new found entry */
  struct zlist far *z;  /* where in zfiles (if found) */
  int dosflag;

  /* Scanning files ...
   *
   * After 5 seconds output Scanning files...
   * then a dot every 2 seconds
   */
  if (noisy) {
    /* If find files then output message after delay */
    if (scan_count == 0) {
      time_t current = time(NULL);
      scan_start = current;
    }
    scan_count++;
    if (scan_count % 100 == 0) {
      time_t current = time(NULL);

      if (current - scan_start > scan_delay) {
        if (scan_last == 0) {
          zipmessage_nl("Scanning files ", 0);
          scan_last = current;
        }
        if (current - scan_last > scan_dot_time) {
          scan_last = current;
          fprintf(mesg, ".");
          fflush(mesg);
        }
      }
    }
  }

  /* Search for name in zip file.  If there, mark it, else add to
     list of new names to do (or remove from that list). */
  if ((iname = ex2in(name, isdir, &dosflag)) == NULL)
    return ZE_MEM;

  /* Discard directory names with zip -rj */
  if (*iname == '\0') {
#ifndef AMIGA
/* A null string is a legitimate external directory name in AmigaDOS; also,
 * a command like "zip -r zipfile FOO:" produces an empty internal name.
 */
# ifndef RISCOS
 /* If extensions needs to be swapped, we will have empty directory names
    instead of the original directory. For example, zipping 'c.', 'c.main'
    should zip only 'main.c' while 'c.' will be converted to '\0' by ex2in. */

    if (pathput && !recurse) error("empty name without -j or -r");

# endif /* !RISCOS */
#endif /* !AMIGA */
    free((zvoid *)iname);
    return ZE_OK;
  }
  undosm = NULL;
  if (dosflag || !pathput) {
    int save_dosify = dosify, save_pathput = pathput;
    dosify = 0;
    pathput = 1;
    /* zname is temporarly mis-used as "undosmode" iname pointer */
    if ((zname = ex2in(name, isdir, NULL)) != NULL) {
      undosm = in2ex(zname);
      free(zname);
    }
    dosify = save_dosify;
    pathput = save_pathput;
  }
  if ((zname = in2ex(iname)) == NULL)
    return ZE_MEM;
#ifdef UNICODE_SUPPORT
  /* Convert name to display or OEM name */
  oname = local_to_display_string(iname);
#else
  if ((oname = malloc(strlen(zname) + 1)) == NULL)
    return ZE_MEM;
  strcpy(oname, zname);
#endif
  if (undosm == NULL)
    undosm = zname;
  if ((z = zsearch(zname)) != NULL) {
    if (pcount && !filter(undosm, casesensitive)) {
      /* Do not clear z->mark if "exclude", because, when "dosify || !pathput"
       * is in effect, two files with different filter options may hit the
       * same z entry.
       */
      if (verbose)
        fprintf(mesg, "excluding %s\n", oname);
      free((zvoid *)iname);
      free((zvoid *)zname);
    } else {
      z->mark = 1;
      if ((z->name = malloc(strlen(name) + 1 + PAD)) == NULL) {
        if (undosm != zname)
          free((zvoid *)undosm);
        free((zvoid *)iname);
        free((zvoid *)zname);
        return ZE_MEM;
      }
      strcpy(z->name, name);
      z->oname = oname;
      z->dosflag = dosflag;

#ifdef FORCE_NEWNAME
      free((zvoid *)(z->iname));
      z->iname = iname;
#else
      /* Better keep the old name. Useful when updating on MSDOS a zip file
       * made on Unix.
       */
      free((zvoid *)iname);
      free((zvoid *)zname);
#endif /* ? FORCE_NEWNAME */
    }
#if defined(UNICODE_SUPPORT) && defined(WIN32)
    z->namew = NULL;
    z->inamew = NULL;
    z->znamew = NULL;
#endif
    if (name == label) {
       label = z->name;
    }
  } else if (pcount == 0 || filter(undosm, casesensitive)) {

    /* Check that we are not adding the zip file to itself. This
     * catches cases like "zip -m foo ../dir/foo.zip".
     */
#ifndef CMS_MVS
/* Version of stat() for CMS/MVS isn't complete enough to see if       */
/* files match.  Just let ZIP.C compare the filenames.  That's good    */
/* enough for CMS anyway since there aren't paths to worry about.      */
    z_stat statb;      /* now use structure z_stat and function zstat globally 7/24/04 EG */

    if (zipstate == -1)
       zipstate = strcmp(zipfile, "-") != 0 &&
                   zstat(zipfile, &zipstatb) == 0;

    if (zipstate == 1 && (statb = zipstatb, zstat(name, &statb) == 0
      && zipstatb.st_mode  == statb.st_mode
#ifdef VMS
      && memcmp(zipstatb.st_ino, statb.st_ino, sizeof(statb.st_ino)) == 0
      && strcmp(zipstatb.st_dev, statb.st_dev) == 0
      && zipstatb.st_uid   == statb.st_uid
#else /* !VMS */
      && zipstatb.st_ino   == statb.st_ino
      && zipstatb.st_dev   == statb.st_dev
      && zipstatb.st_uid   == statb.st_uid
      && zipstatb.st_gid   == statb.st_gid
#endif /* ?VMS */
      && zipstatb.st_size  == statb.st_size
      && zipstatb.st_mtime == statb.st_mtime
      && zipstatb.st_ctime == statb.st_ctime)) {
      /* Don't compare a_time since we are reading the file */
         if (verbose)
           fprintf(mesg, "file matches zip file -- skipping\n");
         if (undosm != zname)
           free((zvoid *)zname);
         if (undosm != iname)
           free((zvoid *)undosm);
         free((zvoid *)iname);
         free(oname);
         return ZE_OK;
    }
#endif  /* CMS_MVS */

    /* allocate space and add to list */
    if ((f = (struct flist far *)farmalloc(sizeof(struct flist))) == NULL ||
        fcount + 1 < fcount ||
        (f->name = malloc(strlen(name) + 1 + PAD)) == NULL)
    {
      if (f != NULL)
        farfree((zvoid far *)f);
      if (undosm != zname)
        free((zvoid *)undosm);
      free((zvoid *)iname);
      free((zvoid *)zname);
      free(oname);
      return ZE_MEM;
    }
    strcpy(f->name, name);
    f->iname = iname;
    f->zname = zname;
#ifdef UNICODE_SUPPORT
    /* Unicode */
    f->uname = local_to_utf8_string(iname);
#ifdef WIN32
    f->namew = NULL;
    f->inamew = NULL;
    f->znamew = NULL;
    if (strcmp(f->name, "-") == 0) {
      f->namew = local_to_wchar_string(f->name);
    }
#endif

#endif
    f->oname = oname;
    f->dosflag = dosflag;

    *fnxt = f;
    f->lst = fnxt;
    f->nxt = NULL;
    fnxt = &f->nxt;
    fcount++;
    if (name == label) {
      label = f->name;
    }
  }
  if (undosm != zname)
    free((zvoid *)undosm);
  return ZE_OK;
}

ulg dostime(y, n, d, h, m, s)
int y;                  /* year */
int n;                  /* month */
int d;                  /* day */
int h;                  /* hour */
int m;                  /* minute */
int s;                  /* second */
/* Convert the date y/n/d and time h:m:s to a four byte DOS date and
   time (date in high two bytes, time in low two bytes allowing magnitude
   comparison). */
{
  return y < 1980 ? DOSTIME_MINIMUM /* dostime(1980, 1, 1, 0, 0, 0) */ :
        (((ulg)y - 1980) << 25) | ((ulg)n << 21) | ((ulg)d << 16) |
        ((ulg)h << 11) | ((ulg)m << 5) | ((ulg)s >> 1);
}


ulg unix2dostime(t)
time_t *t;              /* unix time to convert */
/* Return the Unix time t in DOS format, rounded up to the next two
   second boundary. */
{
  time_t t_even;
  struct tm *s;         /* result of localtime() */

  t_even = (time_t)(((unsigned long)(*t) + 1) & (~1));
                                /* Round up to even seconds. */
  s = localtime(&t_even);       /* Use local time since MSDOS does. */
  if (s == (struct tm *)NULL) {
      /* time conversion error; use current time as emergency value
         (assuming that localtime() does at least accept this value!) */
      t_even = (time_t)(((unsigned long)time(NULL) + 1) & (~1));
      s = localtime(&t_even);
  }
  return dostime(s->tm_year + 1900, s->tm_mon + 1, s->tm_mday,
                 s->tm_hour, s->tm_min, s->tm_sec);
}

int issymlnk(a)
ulg a;                  /* Attributes returned by filetime() */
/* Return true if the attributes are those of a symbolic link */
{
#ifndef QDOS
#ifdef S_IFLNK
#ifdef __human68k__
  int *_dos_importlnenv(void);

  if (_dos_importlnenv() == NULL)
    return 0;
#endif
  return ((a >> 16) & S_IFMT) == S_IFLNK;
#else /* !S_IFLNK */
  return (int)a & 0;    /* avoid warning on unused parameter */
#endif /* ?S_IFLNK */
#else
  return 0;
#endif
}

#endif /* !UTIL */


#if (!defined(UTIL) && !defined(ZP_NEED_GEN_D2U_TIME))
   /* There is no need for dos2unixtime() in the ZipUtils' code. */
#  define ZP_NEED_GEN_D2U_TIME
#endif
#if ((defined(OS2) || defined(VMS)) && defined(ZP_NEED_GEN_D2U_TIME))
   /* OS/2 and VMS use a special solution to handle time-stams of files. */
#  undef ZP_NEED_GEN_D2U_TIME
#endif
#if (defined(W32_STATROOT_FIX) && !defined(ZP_NEED_GEN_D2U_TIME))
   /* The Win32 stat()-bandaid to fix stat'ing root directories needs
    * dos2unixtime() to calculate the time-stamps. */
#  define ZP_NEED_GEN_D2U_TIME
#endif

#ifdef ZP_NEED_GEN_D2U_TIME

time_t dos2unixtime(dostime)
ulg dostime;            /* DOS time to convert */
/* Return the Unix time_t value (GMT/UTC time) for the DOS format (local)
 * time dostime, where dostime is a four byte value (date in most significant
 * word, time in least significant word), see dostime() function.
 */
{
  struct tm *t;         /* argument for mktime() */
  ZCONST time_t clock = time(NULL);

  t = localtime(&clock);
  t->tm_isdst = -1;     /* let mktime() determine if DST is in effect */
  /* Convert DOS time to UNIX time_t format */
  t->tm_sec  = (((int)dostime) <<  1) & 0x3e;
  t->tm_min  = (((int)dostime) >>  5) & 0x3f;
  t->tm_hour = (((int)dostime) >> 11) & 0x1f;
  t->tm_mday = (int)(dostime >> 16) & 0x1f;
  t->tm_mon  = ((int)(dostime >> 21) & 0x0f) - 1;
  t->tm_year = ((int)(dostime >> 25) & 0x7f) + 80;

  return mktime(t);
}

#undef ZP_NEED_GEN_D2U_TIME
#endif /* ZP_NEED_GEN_D2U_TIME */


#ifndef MACOS
int destroy(f)
  char *f;             /* file to delete */
/* Delete the file *f, returning non-zero on failure. */
{
  return unlink(f);
}


int replace(d, s)
char *d, *s;            /* destination and source file names */
/* Replace file *d by file *s, removing the old *s.  Return an error code
   in the ZE_ class. This function need not preserve the file attributes,
   this will be done by setfileattr() later.
 */
{
  z_stat t;         /* results of stat() */
#if defined(CMS_MVS)
  /* cmsmvs.h defines FOPW_TEMP as memory(hiperspace).  Since memory is
   * lost at end of run, always do copy instead of rename.
   */
  int copy = 1;
#else
  int copy = 0;
#endif
  int d_exists;

#if defined(VMS) || defined(CMS_MVS)
  /* stat() is broken on VMS remote files (accessed through Decnet).
   * This patch allows creation of remote zip files, but is not sufficient
   * to update them or compress remote files */
  unlink(d);
#else /* !(VMS || CMS_MVS) */
  d_exists = (LSTAT(d, &t) == 0);
  if (d_exists)
  {
    /*
     * respect existing soft and hard links!
     */
    if (t.st_nlink > 1
# ifdef S_IFLNK
        || (t.st_mode & S_IFMT) == S_IFLNK
# endif
        )
       copy = 1;
    else if (unlink(d))
       return ZE_CREAT;                 /* Can't erase zip file--give up */
  }
#endif /* ?(VMS || CMS_MVS) */
#ifndef CMS_MVS
  if (!copy) {
      if (rename(s, d)) {               /* Just move s on top of d */
          copy = 1;                     /* failed ? */
#if !defined(VMS) && !defined(ATARI) && !defined(AZTEC_C)
#if !defined(CMS_MVS) && !defined(RISCOS) && !defined(QDOS)
    /* For VMS, ATARI, AMIGA Aztec, VM_CMS, MVS, RISCOS,
       always assume that failure is EXDEV */
          if (errno != EXDEV
#  ifdef THEOS
           && errno != EEXIST
#  else
#    ifdef ENOTSAM
           && errno != ENOTSAM /* Used at least on Turbo C */
#    endif
#  endif
              ) return ZE_CREAT;
#endif /* !CMS_MVS && !RISCOS */
#endif /* !VMS && !ATARI && !AZTEC_C */
      }
  }
#endif /* !CMS_MVS */

  if (copy) {
    FILE *f, *g;        /* source and destination files */
    int r;              /* temporary variable */

#ifdef RISCOS
    if (SWI_OS_FSControl_26(s,d,0xA1)!=NULL) {
#endif

    /* Use zfopen for almost all opens where fopen is used.  For
       most OS that support large files we use the 64-bit file
       environment and zfopen maps to fopen, but this allows
       tweeking ports that don't do that.  7/24/04 */
    if ((f = zfopen(s, FOPR)) == NULL) {
      fprintf(mesg," replace: can't open %s\n", s);
      return ZE_TEMP;
    }
    if ((g = zfopen(d, FOPW)) == NULL)
    {
      fclose(f);
      return ZE_CREAT;
    }

    r = fcopy(f, g, (ulg)-1L);
    fclose(f);
    if (fclose(g) || r != ZE_OK)
    {
      unlink(d);
      return r ? (r == ZE_TEMP ? ZE_WRITE : r) : ZE_WRITE;
    }
    unlink(s);
#ifdef RISCOS
    }
#endif
  }
  return ZE_OK;
}
#endif /* !MACOS */


int getfileattr(f)
char *f;                /* file path */
/* Return the file attributes for file f or 0 if failure */
{
#ifdef __human68k__
  struct _filbuf buf;

  return _dos_files(&buf, f, 0xff) < 0 ? 0x20 : buf.atr;
#else
  z_stat s;

  return SSTAT(f, &s) == 0 ? (int) s.st_mode : 0;
#endif
}


int setfileattr(f, a)
char *f;                /* file path */
int a;                  /* attributes returned by getfileattr() */
/* Give the file f the attributes a, return non-zero on failure */
{
#if defined(TOPS20) || defined (CMS_MVS)
  return 0;
#else
#ifdef __human68k__
  return _dos_chmod(f, a) < 0 ? -1 : 0;
#else
  return chmod(f, a);
#endif
#endif
}


/* tempname */

#ifndef VMS /* VMS-specific function is in VMS.C. */

char *tempname(zip)
  char *zip;              /* path name of zip file to generate temp name for */

/* Return a temporary file name in its own malloc'ed space, using tempath. */
{
  char *t = zip;   /* malloc'ed space for name (use zip to avoid warning) */

# ifdef CMS_MVS
  if ((t = malloc(strlen(tempath) + L_tmpnam + 2)) == NULL)
    return NULL;

#  ifdef VM_CMS
  tmpnam(t);
  /* Remove filemode and replace with tempath, if any. */
  /* Otherwise A-disk is used by default */
  *(strrchr(t, ' ')+1) = '\0';
  if (tempath!=NULL)
     strcat(t, tempath);
  return t;
#  else   /* !VM_CMS */
  /* For MVS */
  tmpnam(t);
  if (tempath != NULL)
  {
    int l1 = strlen(t);
    char *dot;
    if (*t == '\'' && *(t+l1-1) == '\'' && (dot = strchr(t, '.')))
    {
      /* MVS and not OE.  tmpnam() returns quoted string of 5 qualifiers.
       * First is HLQ, rest are timestamps.  User can only replace HLQ.
       */
      int l2 = strlen(tempath);
      if (strchr(tempath, '.') || l2 < 1 || l2 > 8)
        ziperr(ZE_PARMS, "On MVS and not OE, tempath (-b) can only be HLQ");
      memmove(t+1+l2, dot, l1+1-(dot-t));  /* shift dot ready for new hlq */
      memcpy(t+1, tempath, l2);            /* insert new hlq */
    }
    else
    {
      /* MVS and probably OE.  tmpnam() returns filename based on TMPDIR,
       * no point in even attempting to change it.  User should modify TMPDIR
       * instead.
       */
      zipwarn("MVS, assumed to be OE, change TMPDIR instead of option -b: ",
              tempath);
    }
  }
  return t;
#  endif  /* !VM_CMS */

# else /* !CMS_MVS */

#  ifdef TANDEM
  char cur_subvol [FILENAME_MAX];
  char temp_subvol [FILENAME_MAX];
  char *zptr;
  char *ptr;
  char *cptr = &cur_subvol[0];
  char *tptr = &temp_subvol[0];
  short err;
  FILE *tempf;
  int attempts;

  t = (char *)malloc(NAMELEN); /* malloc here as you cannot free */
                               /* tmpnam allocated storage later */

  zptr = strrchr(zip, TANDEM_DELIMITER);

  if (zptr != NULL) {
    /* ZIP file specifies a Subvol so make temp file there so it can just
       be renamed at end */

    *tptr = *cptr = '\0';
    strcat(cptr, getenv("DEFAULTS"));

    strncat(tptr, zip, _min(FILENAME_MAX, (zptr - zip)) ); /* temp subvol */
    strncat(t, zip, _min(NAMELEN, ((zptr - zip) + 1)) );   /* temp stem   */

    err = chvol(tptr);
    ptr = t + strlen(t);  /* point to end of stem */
  }
  else
    ptr = t;

  /* If two zips are running in same subvol then we can get contention problems
     with the temporary filename.  As a work around we attempt to create
     the file here, and if it already exists we get a new temporary name */

  attempts = 0;
  do {
    attempts++;
    tmpnam(ptr);  /* Add filename */
    tempf = zfopen(ptr, FOPW_TMP);    /* Attempt to create file */
  } while (tempf == NULL && attempts < 100);

  if (attempts >= 100) {
    ziperr(ZE_TEMP, "Could not get unique temp file name");
  }

  fclose(tempf);

  if (zptr != NULL) {
    err = chvol(cptr);  /* Put ourself back to where we came in */
  }

  return t;

#  else /* !CMS_MVS && !TANDEM */
/*
 * Do something with TMPDIR, TMP, TEMP ????
 */
  if (tempath != NULL)
  {
    if ((t = malloc(strlen(tempath) + 12)) == NULL)
      return NULL;
    strcpy(t, tempath);

#   if (!defined(VMS) && !defined(TOPS20))
#    ifdef MSDOS
    {
      char c = (char)lastchar(t);
      if (c != '/' && c != ':' && c != '\\')
        strcat(t, "/");
    }
#    else

#     ifdef AMIGA
    {
      char c = (char)lastchar(t);
      if (c != '/' && c != ':')
        strcat(t, "/");
    }
#     else /* !AMIGA */
#      ifdef RISCOS
    if (lastchar(t) != '.')
      strcat(t, ".");
#      else /* !RISCOS */

#       ifdef QDOS
    if (lastchar(t) != '_')
      strcat(t, "_");
#       else
    if (lastchar(t) != '/')
      strcat(t, "/");
#       endif /* ?QDOS */
#      endif /* ?RISCOS */
#     endif  /* ?AMIGA */
#    endif /* ?MSDOS */
#   endif /* !VMS && !TOPS20 */
  }
  else
  {
    if ((t = malloc(12)) == NULL)
      return NULL;
    *t = 0;
  }
#   ifdef NO_MKTEMP
  {
    char *p = t + strlen(t);
    sprintf(p, "%08lx", (ulg)time(NULL));
    return t;
  }
#   else
  strcat(t, "ziXXXXXX"); /* must use lowercase for Linux dos file system */
#     if defined(UNIX) && !defined(NO_MKSTEMP)
  /* tempname should not be called */
  return t;
#     else
  return mktemp(t);
#     endif
#   endif /* NO_MKTEMP */
#  endif /* TANDEM */
# endif /* CMS_MVS */
}
#endif /* !VMS */

int fcopy(f, g, n)
  FILE *f, *g;            /* source and destination files */
  /* now use uzoff_t for all file sizes 5/14/05 CS */
  uzoff_t n;               /* number of bytes to copy or -1 for all */
/* Copy n bytes from file *f to file *g, or until EOF if (zoff_t)n == -1.
   Return an error code in the ZE_ class. */
{
  char *b;              /* malloc'ed buffer for copying */
  extent k;             /* result of fread() */
  uzoff_t m;            /* bytes copied so far */

  if ((b = malloc(CBSZ)) == NULL)
    return ZE_MEM;
  m = 0;
  while (n == (uzoff_t)(-1L) || m < n)
  {
    if ((k = fread(b, 1, n == (uzoff_t)(-1) ?
                   CBSZ : (n - m < CBSZ ? (extent)(n - m) : CBSZ), f)) == 0)
    {
      if (ferror(f))
      {
        free((zvoid *)b);
        return ZE_READ;
      }
      else
        break;
    }
    if (fwrite(b, 1, k, g) != k)
    {
      free((zvoid *)b);
      fprintf(mesg," fcopy: write error\n");
      return ZE_TEMP;
    }
    m += k;
  }
  free((zvoid *)b);
  return ZE_OK;
}


/* from zipfile.c */

#ifdef THEOS
 /* Macros cause stack overflow in compiler */
 ush SH(uch* p) { return ((ush)(uch)((p)[0]) | ((ush)(uch)((p)[1]) << 8)); }
 ulg LG(uch* p) { return ((ulg)(SH(p)) | ((ulg)(SH((p)+2)) << 16)); }
#else /* !THEOS */
 /* Macros for converting integers in little-endian to machine format */
# define SH(a) ((ush)(((ush)(uch)(a)[0]) | (((ush)(uch)(a)[1]) << 8)))
# define LG(a) ((ulg)SH(a) | ((ulg)SH((a)+2) << 16))
# ifdef ZIP64_SUPPORT           /* zip64 support 08/31/2003 R.Nausedat */
#  define LLG(a) ((zoff_t)LG(a) | ((zoff_t)LG((a)+4) << 32))
# endif
#endif /* ?THEOS */


/* always copies from global in_file to global output file y */
int bfcopy(n)
  /* now use uzoff_t for all file sizes 5/14/05 CS */
  uzoff_t n;               /* number of bytes to copy or -1 for all */
/* Copy n bytes from in_file to out_file, or until EOF if (zoff_t)n == -1.

   Normally we have the compressed size from either the central directory
   entry or the local header.

   If n != -1 and EOF, close current split and open next and continue
   copying.

   If n == -2, copy until find the extended header (data descriptor).  Only
   used for -FF when no size available.

   If fix == 1 calculate CRC of input entry and verify matches.

   If fix == 2 and this entry using data descriptor keep a sliding
   window in the buffer for looking for signature.

   Return an error code in the ZE_ class. */
{
  char *b;              /* malloc'ed buffer for copying */
  extent k;             /* result of fread() */
  uzoff_t m;            /* bytes copied so far */
  extent brd;           /* bytes to read */
  zoff_t data_start = 0;
  zoff_t des_start = 0;
  char *split_path;
  extent kk;
  int i;
  char sbuf[4];         /* buffer for sliding signature window for fix = 2 */
  int des = 0;          /* this entry has data descriptor to find */

  if ((b = malloc(CBSZ)) == NULL)
    return ZE_MEM;

  if (copy_only && !display_globaldots) {
    /* initialize dot count */
    dot_count = -1;
  }

  if (fix == 2 && n == (uzoff_t) -2) {
    data_start = zftello(in_file);
    for (kk = 0; kk < 4; kk++)
      sbuf[kk] = 0;
    des = 1;
  }

  des_good = 0;

  m = 0;
  while (des || n == (uzoff_t)(-1L) || m < n)
  {
    if (des || n == (uzoff_t)(-1))
      brd = CBSZ;
    else
      brd = (n - m < CBSZ ? (extent)(n - m) : CBSZ);

    des_start = zftello(in_file);

    if ((k = fread(b, 1, brd, in_file)) == 0)
    {
      if (fix == 2 && k < brd) {
        free((zvoid *)b);
        return ZE_READ;
      }
      else if (ferror(in_file))
      {
        free((zvoid *)b);
        return ZE_READ;
      }
      else {
        break;
      }
    }


    /* end at extended local header (data descriptor) signature */
    if (des) {
      des_crc = 0;
      des_csize = 0;
      des_usize = 0;

      /* If first 4 bytes in buffer are data descriptor signature then
         try to read the data descriptor.
         If not, scan for signature and break if found, let bfwrite flush
         the data and then next read should put the data descriptor at
         the beginning of the buffer.
       */

      if (
          (b[0] != 0x50 /*'P' except EBCDIC*/ ||
           b[1] != 0x4b /*'K' except EBCDIC*/ ||
           b[2] != '\07' ||
           b[3] != '\010')) {
        /* buffer is not start of data descriptor */

        for (kk = 0; kk < k; kk++) {
          /* add byte to end of sbuf */
          for (i = 0; i < 3; i++)
            sbuf[i] = sbuf[i + 1];
          sbuf[3] = b[kk];

          /* see if this is signature */
          if (
              (sbuf[0] == 0x50 /*'P' except EBCDIC*/ &&
               sbuf[1] == 0x4b /*'K' except EBCDIC*/ &&
               sbuf[2] == '\07' &&
               sbuf[3] == '\010')) {
            kk -= 3;
            if (zfseeko(in_file, bytes_this_split + kk, SEEK_SET) != 0) {
              /* seek error */
              ZIPERR(ZE_READ, "seek failed reading descriptor");
            }
            des_start = zftello(in_file);
            k = kk;
            break;
          }
        }
      }
      else

      /* signature at start of buffer */
      {
        des_good = 0;

#ifdef ZIP64_SUPPORT
        if (zip64_entry) {

          /* read Zip64 data descriptor */
          if (k < 24) {
            /* not enough bytes, so can't be data descriptor
               as data descriptors can't be split across splits
             */
          }
          else
          {
            /* read the Zip64 descriptor */

            des_crc = LG(b + 4);
            des_csize = LLG(b + 8);
            des_usize = LLG(b + 16);

            /* if this is the right data descriptor then the sizes should match */
            if ((uzoff_t)des_start - (uzoff_t)data_start != des_csize) {
              /* apparently this signature does not go with this data so skip */

              /* write out signature as data */
              k = 4;
              if (zfseeko(in_file, des_start + k, SEEK_SET) != 0) {
                /* seek error */
                ZIPERR(ZE_READ, "seek failed reading descriptor");
              }
              if (bfwrite(b, 1, k, BFWRITE_DATA) != k)
              {
                free((zvoid *)b);
                fprintf(mesg," fcopy: write error\n");
                return ZE_TEMP;
              }
              m += k;
              continue;
            }
            else
            {
              /* apparently this is the correct data descriptor */

              /* we should check the CRC but would need to inflate
                 the data */

              /* skip descriptor as will write out later */
              des_good = 1;
              k = 24;
              data_start = zftello(in_file);
              if (zfseeko(in_file, des_start + k, SEEK_SET) != 0) {
                /* seek error */
                ZIPERR(ZE_READ, "seek failed reading descriptor");
              }
              data_start = zftello(in_file);
            }
          }

        }
        else
#endif
        {
          /* read standard data descriptor */

          if (k < 16) {
            /* not enough bytes, so can't be data descriptor
               as data descriptors can't be split across splits
             */
          }
          else
          {
            /* read the descriptor */

            des_crc = LG(b + 4);
            des_csize = LG(b + 8);
            des_usize = LG(b + 12);

            /* if this is the right data descriptor then the sizes should match */
            if ((uzoff_t)des_start - (uzoff_t)data_start != des_csize) {
              /* apparently this signature does not go with this data so skip */

              /* write out signature as data */
              k = 4;
              if (zfseeko(in_file, des_start + k, SEEK_SET) != 0) {
                /* seek error */
                ZIPERR(ZE_READ, "seek failed reading descriptor");
              }
              if (bfwrite(b, 1, k, BFWRITE_DATA) != k)
              {
                free((zvoid *)b);
                fprintf(mesg," fcopy: write error\n");
                return ZE_TEMP;
              }
              m += k;
              continue;
            }
            else
            {
              /* apparently this is the correct data descriptor */

              /* we should check the CRC but this does not work for
                 encrypted data */

              /* skip descriptor as will write out later */
              des_good = 1;
              data_start = zftello(in_file);
              k = 16;
              if (zfseeko(in_file, des_start + k, SEEK_SET) != 0) {
                /* seek error */
                ZIPERR(ZE_READ, "seek failed reading descriptor");
              }
              data_start = zftello(in_file);
            }
          }


        }
      }
    }


    if (des_good) {
      /* skip descriptor as will write out later */
    } else {
      /* write out apparently wrong descriptor as data */
      if (bfwrite(b, 1, k, BFWRITE_DATA) != k)
      {
        free((zvoid *)b);
        fprintf(mesg," fcopy: write error\n");
        return ZE_TEMP;
      }
      m += k;
    }

    if (copy_only && !display_globaldots) {
      if (dot_size > 0) {
        /* initial space */
        if (noisy && dot_count == -1) {
#ifndef WINDLL
          putc(' ', mesg);
          fflush(mesg);
#else
          fprintf(stdout,"%c",' ');
#endif
          dot_count++;
        }
        dot_count += k;
        if (dot_size <= dot_count) dot_count = 0;
      }
      if ((verbose || noisy) && dot_size && !dot_count) {
#ifndef WINDLL
        putc('.', mesg);
        fflush(mesg);
#else
        fprintf(stdout,"%c",'.');
#endif
        mesg_line_started = 1;
      }
    }

    if (des_good)
      break;

    if (des)
      continue;

    if ((des || n != (uzoff_t)(-1L)) && m < n && feof(in_file)) {
      /* open next split */
      current_in_disk++;

      if (current_in_disk >= total_disks) {
        /* done */
        break;

      } else if (current_in_disk == total_disks - 1) {
        /* last disk is archive.zip */
        if ((split_path = malloc(strlen(in_path) + 1)) == NULL) {
          zipwarn("reading archive: ", in_path);
          return ZE_MEM;
        }
        strcpy(split_path, in_path);
      } else {
        /* other disks are archive.z01, archive.z02, ... */
        split_path = get_in_split_path(in_path, current_in_disk);
      }

      fclose(in_file);

      /* open the split */
      while ((in_file = zfopen(split_path, FOPR)) == NULL) {
        int r = 0;

        /* could not open split */

        if (fix == 1 && skip_this_disk) {
          free(split_path);
          free((zvoid *)b);
          return ZE_FORM;
        }

        /* Ask for directory with split.  Updates in_path */
        r = ask_for_split_read_path(current_in_disk);
        if (r == ZE_ABORT) {
          zipwarn("could not find split: ", split_path);
          free(split_path);
          free((zvoid *)b);
          return ZE_ABORT;
        }
        if (r == ZE_EOF) {
          zipmessage_nl("", 1);
          zipwarn("user ended reading - closing archive", "");
          free(split_path);
          free((zvoid *)b);
          return ZE_EOF;
        }
        if (fix == 2 && skip_this_disk) {
          /* user asked to skip this disk */
          zipwarn("skipping split file: ", split_path);
          current_in_disk++;
        }

        if (current_in_disk == total_disks - 1) {
          /* last disk is archive.zip */
          if ((split_path = malloc(strlen(in_path) + 1)) == NULL) {
            zipwarn("reading archive: ", in_path);
            return ZE_MEM;
          }
          strcpy(split_path, in_path);
        } else {
          /* other disks are archive.z01, archive.z02, ... */
          split_path = get_in_split_path(zipfile, current_in_disk);
        }
      }
      if (fix == 2 && skip_this_disk) {
        /* user asked to skip this disk */
        free(split_path);
        free((zvoid *)b);
        return ZE_FORM;
      }
      free(split_path);
    }
  }
  free((zvoid *)b);
  return ZE_OK;
}

/*------------------------------------------------------------------
 * Split archives
 */


/* ask_for_split_read_path
 *
 * If the next split file is not in the current directory, ask
 * the user where it is.
 *
 * in_path is the base path for reading splits and is usually
 * the same as zipfile.  The path in in_path must be the archive
 * file ending in .zip as this is assumed by get_in_split_path().
 *
 * Updates in_path if changed.  Returns ZE_OK if OK or ZE_ABORT if
 * user cancels reading archive.
 *
 * If fix = 1 then allow skipping disk (user may not have it).
 */

#define SPLIT_MAXPATH (FNMAX + 4010)

int ask_for_split_read_path(current_disk)
  ulg current_disk;
{
  FILE *f;
  int is_readable = 0;
  int i;
  char *split_dir = NULL;
  char *archive_name = NULL;
  char *split_name = NULL;
  char *split_path = NULL;
  char buf[SPLIT_MAXPATH + 100];

  /* get split path */
  split_path = get_in_split_path(in_path, current_disk);

  /* get the directory */
  if ((split_dir = malloc(strlen(in_path) + 40)) == NULL) {
    ZIPERR(ZE_MEM, "split path");
  }
  strcpy(split_dir, in_path);

  /* remove any name at end */
  for (i = strlen(split_dir) - 1; i >= 0; i--) {
    if (split_dir[i] == '/' || split_dir[i] == '\\'
          || split_dir[i] == ':') {
      split_dir[i + 1] = '\0';
      break;
    }
  }
  if (i < 0)
    split_dir[0] = '\0';

  /* get the name of the archive */
  if ((archive_name = malloc(strlen(in_path) + 1)) == NULL) {
    ZIPERR(ZE_MEM, "split path");
  }
  if (strlen(in_path) == strlen(split_dir)) {
    archive_name[0] = '\0';
  } else {
    strcpy(archive_name, in_path + strlen(split_dir));
  }

  /* get the name of the split */
  if ((split_name = malloc(strlen(split_path) + 1)) == NULL) {
    ZIPERR(ZE_MEM, "split path");
  }
  if (strlen(in_path) == strlen(split_dir)) {
    split_name[0] = '\0';
  } else {
    strcpy(split_name, split_path + strlen(split_dir));
  }
  if (i < 0) {
    strcpy(split_dir, "(current directory)");
  }

  fprintf(mesg, "\n\nCould not find:\n");
  fprintf(mesg, "  %s\n", split_path);
  /*
  fprintf(mesg, "Please enter the path directory (. for cur dir) where\n");
  fprintf(mesg, "  %s\n", split_name);
  fprintf(mesg, "is located\n");
  */
  for (;;) {
    if (is_readable) {
      fprintf(mesg, "\nHit c      (change path to where this split file is)");
      fprintf(mesg, "\n    q      (abort archive - quit)");
      fprintf(mesg, "\n or ENTER  (continue with this split): ");
    } else {
      if (fix == 1) {
        fprintf(mesg, "\nHit c      (change path to where this split file is)");
        fprintf(mesg, "\n    s      (skip this split)");
        fprintf(mesg, "\n    q      (abort archive - quit)");
        fprintf(mesg, "\n or ENTER  (try reading this split again): ");
      } else if (fix == 2) {
        fprintf(mesg, "\nHit c      (change path to where this split file is)");
        fprintf(mesg, "\n    s      (skip this split)");
        fprintf(mesg, "\n    q      (abort archive - quit)");
        fprintf(mesg, "\n    e      (end this archive - no more splits)");
        fprintf(mesg, "\n    z      (look for .zip split - the last split)");
        fprintf(mesg, "\n or ENTER  (try reading this split again): ");
      } else {
        fprintf(mesg, "\nHit c      (change path to where this split file is)");
        fprintf(mesg, "\n    q      (abort archive - quit)");
        fprintf(mesg, "\n or ENTER  (try reading this split again): ");
      }
    }
    fflush(mesg);
    fgets(buf, SPLIT_MAXPATH, stdin);
    /* remove any newline */
    for (i = 0; buf[i]; i++) {
      if (buf[i] == '\n') {
        buf[i] = '\0';
        break;
      }
    }
    if (toupper(buf[0]) == 'Q') {
      return ZE_ABORT;
    } else if ((fix == 1 || fix == 2) && toupper(buf[0]) == 'S') {
    /*
      fprintf(mesg, "\nSkip this split/disk?  (files in this split will not be recovered) [n/y] ");
      fflush(mesg);
      fgets(buf, SPLIT_MAXPATH, stdin);
      if (buf[0] == 'y' || buf[0] == 'Y') {
    */
      skip_this_disk = current_in_disk + 1;
      return ZE_FORM;
    } else if (toupper(buf[0]) == 'C') {
      fprintf(mesg, "\nEnter path where this split is (ENTER = same dir, . = current dir)");
      fprintf(mesg, "\n: ");
      fflush(mesg);
      fgets(buf, SPLIT_MAXPATH, stdin);
      is_readable = 0;
      /* remove any newline */
      for (i = 0; buf[i]; i++) {
        if (buf[i] == '\n') {
          buf[i] = '\0';
          break;
        }
      }
      if (buf[0] == '\0') {
        /* Hit ENTER so try old path again - could be removable media was changed */
        strcpy(buf, split_path);
      }
    } else if (fix == 2 && toupper(buf[0]) == 'E') {
      /* no more splits to read */
      return ZE_EOF;
    } else if (fix == 2 && toupper(buf[0]) == 'Z') {
      total_disks = current_disk + 1;
      free(split_path);
      split_path = get_in_split_path(in_path, current_disk);
      buf[0] = '\0';
      strncat(buf, split_path, SPLIT_MAXPATH);
    }
    if (strlen(buf) > 0) {
      /* changing path */

      /* check if user wants current directory */
      if (buf[0] == '.' && buf[1] == '\0') {
        buf[0] = '\0';
      }
      /* remove any name at end */
      for (i = strlen(buf); i >= 0; i--) {
        if (buf[i] == '/' || buf[i] == '\\'
             || buf[i] == ':') {
          buf[i + 1] = '\0';
          break;
        }
      }
      /* update base_path to newdir/split_name - in_path is the .zip file path */
      free(in_path);
      if (i < 0) {
        /* just name so current directory */
        strcpy(buf, "(current directory)");
        if (archive_name == NULL) {
          i = 0;
        } else {
          i = strlen(archive_name);
        }
        if ((in_path = malloc(strlen(archive_name) + 40)) == NULL) {
          ZIPERR(ZE_MEM, "split path");
        }
        strcpy(in_path, archive_name);
      } else {
        /* not the current directory */
        /* remove any name at end */
        for (i = strlen(buf); i >= 0; i--) {
          if (buf[i] == '/') {
            buf[i + 1] = '\0';
            break;
          }
        }
        if (i < 0) {
          buf[0] = '\0';
        }
        if ((in_path = malloc(strlen(buf) + strlen(archive_name) + 40)) == NULL) {
          ZIPERR(ZE_MEM, "split path");
        }
        strcpy(in_path, buf);
        strcat(in_path, archive_name);
      }

      free(split_path);

      /* get split path */
      split_path = get_in_split_path(in_path, current_disk);

      free(split_dir);
      if ((split_dir = malloc(strlen(in_path) + 40)) == NULL) {
        ZIPERR(ZE_MEM, "split path");
      }
      strcpy(split_dir, in_path);
      /* remove any name at end */
      for (i = strlen(split_dir); i >= 0; i--) {
        if (split_dir[i] == '/') {
          split_dir[i + 1] = '\0';
          break;
        }
      }

      /* try to open it */
      if ((f = fopen(split_path, "r")) == NULL) {
        fprintf(mesg, "\nCould not find or open\n");
        fprintf(mesg, "  %s\n", split_path);
        /*
        fprintf(mesg, "Please enter the path (. for cur dir) where\n");
        fprintf(mesg, "  %s\n", split_name);
        fprintf(mesg, "is located\n");
        */
        continue;
      }
      fclose(f);
      is_readable = 1;
      fprintf(mesg, "Found:  %s\n", split_path);
    } else {
      /* try to open it */
      if ((f = fopen(split_path, "r")) == NULL) {
        fprintf(mesg, "\nCould not find or open\n");
        fprintf(mesg, "  %s\n", split_path);
        /*
        fprintf(mesg, "Please enter the path (. for cur dir) where\n");
        fprintf(mesg, "  %s\n", split_name);
        fprintf(mesg, "is located\n");
        */
        continue;
      }
      fclose(f);
      is_readable = 1;
      fprintf(mesg, "\nFound:  %s\n", split_path);
      break;
    }
  }
  free(archive_name);
  free(split_dir);
  free(split_name);

  return ZE_OK;
}


/* ask_for_split_write_path
 *
 * Verify the directory for the next split.  Called
 * when -sp is used to pause between writing splits.
 *
 * Updates out_path and return 1 if OK or 0 if cancel
 */
int ask_for_split_write_path(current_disk)
  ulg current_disk;
{
  unsigned int num = (unsigned int)current_disk + 1;
  int i;
  char *split_dir = NULL;
  char *split_name = NULL;
  char buf[FNMAX + 40];

  /* get the directory */
  if ((split_dir = malloc(strlen(out_path) + 40)) == NULL) {
    ZIPERR(ZE_MEM, "split path");
  }
  strcpy(split_dir, out_path);

  /* remove any name at end */
  for (i = strlen(split_dir); i >= 0; i--) {
    if (split_dir[i] == '/' || split_dir[i] == '\\'
          || split_dir[i] == ':') {
      split_dir[i + 1] = '\0';
      break;
    }
  }

  /* get the name of the split */
  if ((split_name = malloc(strlen(out_path) + 1)) == NULL) {
    ZIPERR(ZE_MEM, "split path");
  }
  if (strlen(out_path) == strlen(split_dir)) {
    split_name[0] = '\0';
  } else {
    strcpy(split_name, out_path + strlen(split_dir));
  }
  if (i < 0) {
    strcpy(split_dir, "(current directory)");
  }
  if (mesg_line_started)
    fprintf(mesg, "\n");
  fprintf(mesg, "\nOpening disk %d\n", num);
  fprintf(mesg, "Hit ENTER to write to default path of\n");
  fprintf(mesg, "  %s\n", split_dir);
  fprintf(mesg, "or enter a new directory path (. for cur dir) and hit ENTER\n");
  for (;;) {
    fprintf(mesg, "\nPath (or hit ENTER to continue): ");
    fflush(mesg);
    fgets(buf, FNMAX, stdin);
    /* remove any newline */
    for (i = 0; buf[i]; i++) {
      if (buf[i] == '\n') {
        buf[i] = '\0';
        break;
      }
    }
    if (strlen(buf) > 0) {
      /* changing path */

      /* current directory */
      if (buf[0] == '.' && buf[1] == '\0') {
        buf[0] = '\0';
      }
      /* remove any name at end */
      for (i = strlen(buf); i >= 0; i--) {
        if (buf[i] == '/' || buf[i] == '\\'
             || buf[i] == ':') {
          buf[i + 1] = '\0';
          break;
        }
      }
      /* update out_path to newdir/split_name */
      free(out_path);
      if (i < 0) {
        /* just name so current directory */
        strcpy(buf, "(current directory)");
        if (split_name == NULL) {
          i = 0;
        } else {
          i = strlen(split_name);
        }
        if ((out_path = malloc(strlen(split_name) + 40)) == NULL) {
          ZIPERR(ZE_MEM, "split path");
        }
        strcpy(out_path, split_name);
      } else {
        /* not the current directory */
        /* remove any name at end */
        for (i = strlen(buf); i >= 0; i--) {
          if (buf[i] == '/') {
            buf[i + 1] = '\0';
            break;
          }
        }
        if (i < 0) {
          buf[0] = '\0';
        }
        if ((out_path = malloc(strlen(buf) + strlen(split_name) + 40)) == NULL) {
          ZIPERR(ZE_MEM, "split path");
        }
        strcpy(out_path, buf);
        strcat(out_path, split_name);
      }
      fprintf(mesg, "Writing to:\n  %s\n", buf);
      free(split_name);
      free(split_dir);
      if ((split_dir = malloc(strlen(out_path) + 40)) == NULL) {
        ZIPERR(ZE_MEM, "split path");
      }
      strcpy(split_dir, out_path);
      /* remove any name at end */
      for (i = strlen(split_dir); i >= 0; i--) {
        if (split_dir[i] == '/') {
          split_dir[i + 1] = '\0';
          break;
        }
      }
      if ((split_name = malloc(strlen(out_path) + 1)) == NULL) {
        ZIPERR(ZE_MEM, "split path");
      }
      strcpy(split_name, out_path + strlen(split_dir));
    } else {
      break;
    }
  }
  free(split_dir);
  free(split_name);

  /* for now no way out except Ctrl C */
  return 1;
}


/* split_name
 *
 * get name of split being read
 */
char *get_in_split_path(base_path, disk_number)
  char *base_path;
  ulg disk_number;
{
  char *split_path = NULL;
  int base_len = 0;
  int path_len = 0;
  ulg num = disk_number + 1;
  char ext[6];
#ifdef VMS
  int vers_len;                         /* File version length. */
  char *vers_ptr;                       /* File version string. */
#endif /* def VMS */

  /*
   * A split has extension z01, z02, ..., z99, z100, z101, ... z999
   * We currently support up to .z99999
   * WinZip will also read .100, .101, ... but AppNote 6.2.2 uses above
   * so use that.  Means on DOS can only have 100 splits.
   */

  if (num == total_disks) {
    /* last disk is base path */
    if ((split_path = malloc(strlen(base_path) + 1)) == NULL) {
      ZIPERR(ZE_MEM, "base path");
    }
    strcpy(split_path, base_path);

    return split_path;
  } else {
    if (num > 99999) {
      ZIPERR(ZE_BIG, "More than 99999 splits needed");
    }
    sprintf(ext, "z%02lu", num);
  }

  /* create path for this split - zip.c checked for .zip extension */
  base_len = strlen(base_path) - 3;
  path_len = base_len + strlen(ext);

#ifdef VMS
  /* On VMS, locate the file version, and adjust base_len accordingly.
     Note that path_len is correct, as-is.
  */
  vers_ptr = vms_file_version( base_path);
  vers_len = strlen( vers_ptr);
  base_len -= vers_len;
#endif /* def VMS */

  if ((split_path = malloc(path_len + 1)) == NULL) {
    ZIPERR(ZE_MEM, "split path");
  }
  /* copy base_path except for end zip */
  strcpy(split_path, base_path);
  split_path[base_len] = '\0';
  /* add extension */
  strcat(split_path, ext);

#ifdef VMS
  /* On VMS, append (preserve) the file version. */
  strcat(split_path, vers_ptr);
#endif /* def VMS */

  return split_path;
}


/* split_name
 *
 * get name of split being written
 */
char *get_out_split_path(base_path, disk_number)
  char *base_path;
  ulg disk_number;
{
  char *split_path = NULL;
  int base_len = 0;
  int path_len = 0;
  ulg num = disk_number + 1;
  char ext[6];
#ifdef VMS
  int vers_len;                         /* File version length. */
  char *vers_ptr;                       /* File version string. */
#endif /* def VMS */

  /*
   * A split has extension z01, z02, ..., z99, z100, z101, ... z999
   * We currently support up to .z99999
   * WinZip will also read .100, .101, ... but AppNote 6.2.2 uses above
   * so use that.  Means on DOS can only have 100 splits.
   */

  if (num > 99999) {
    ZIPERR(ZE_BIG, "More than 99999 splits needed");
  }
  sprintf(ext, "z%02lu", num);

  /* create path for this split - zip.c checked for .zip extension */
  base_len = strlen(base_path) - 3;
  path_len = base_len + strlen(ext);

#ifdef VMS
  /* On VMS, locate the file version, and adjust base_len accordingly.
     Note that path_len is correct, as-is.
  */
  vers_ptr = vms_file_version( base_path);
  vers_len = strlen( vers_ptr);
  base_len -= vers_len;
#endif /* def VMS */

  if ((split_path = malloc(path_len + 1)) == NULL) {
    ZIPERR(ZE_MEM, "split path");
  }
  /* copy base_path except for end zip */
  strcpy(split_path, base_path);
  split_path[base_len] = '\0';
  /* add extension */
  strcat(split_path, ext);

#ifdef VMS
  /* On VMS, append (preserve) the file version. */
  strcat(split_path, vers_ptr);
#endif /* def VMS */

  return split_path;
}

/* close_split
 *
 * close a split - assume that the paths needed for the splits are
 * available.
 */
int close_split(disk_number, tempfile, temp_name)
  ulg disk_number;
  FILE *tempfile;
  char *temp_name;
{
  char *split_path = NULL;

  split_path = get_out_split_path(out_path, disk_number);

  if (noisy_splits) {
    zipmessage("\tClosing split ", split_path);
  }

  fclose(tempfile);

  rename_split(temp_name, split_path);
  set_filetype(split_path);

  return ZE_OK;
}

/* bfwrite
   Does the fwrite but also counts bytes and does splits */
size_t bfwrite(buffer, size, count, mode)
  ZCONST void *buffer;
  size_t size;
  size_t count;
  int mode;
{
  size_t bytes_written = 0;
  size_t r;
  size_t b = size * count;
  uzoff_t bytes_left_in_split = 0;
  size_t bytes_to_write = b;


  /* -------------------------------- */
  /* local header */
  if (mode == BFWRITE_LOCALHEADER) {
    /* writing local header - reset entry data count */
    bytes_this_entry = 0;
    /* save start of local header so we can rewrite later */
    current_local_file = y;
    current_local_disk = current_disk;
    current_local_offset = bytes_this_split;
  }

  if (split_size == 0)
    bytes_left_in_split = bytes_to_write;
  else
    bytes_left_in_split = split_size - bytes_this_split;

  if (bytes_to_write > bytes_left_in_split) {
    if (mode == BFWRITE_HEADER ||
        mode == BFWRITE_LOCALHEADER ||
        mode == BFWRITE_CENTRALHEADER) {
      /* if can't write entire header save for next split */
      bytes_to_write = 0;
    } else {
      /* normal data so fill the split */
      bytes_to_write = (size_t)bytes_left_in_split;
    }
  }

  /* -------------------------------- */
  /* central header */
  if (mode == BFWRITE_CENTRALHEADER) {
    /* set start disk for CD */
    if (cd_start_disk == (ulg)-1) {
      cd_start_disk = current_disk;
      cd_start_offset = bytes_this_split;
    }
    cd_entries_this_disk++;
    total_cd_entries++;
  }

  /* -------------------------------- */
  if (bytes_to_write > 0) {
    /* write out the bytes for this split */
    r = fwrite(buffer, size, bytes_to_write, y);
    bytes_written += r;
    bytes_to_write = b - r;
    bytes_this_split += r;
    if (mode == BFWRITE_DATA)
      /* if data descriptor do not include in count */
      bytes_this_entry += r;
  } else {
    bytes_to_write = b;
  }

  if (bytes_to_write > 0) {
    if (split_method) {
      /* still bytes to write so close split and open next split */
      bytes_prev_splits += bytes_this_split;

      if (split_method == 1 && ferror(y)) {
        /* if writing all splits to same place and have problem then bad */
        ZIPERR(ZE_WRITE, "Could not write split");
      }

      if (split_method == 2 && ferror(y)) {
        /* A split must be at least 64K except last .zip split */
        if (bytes_this_split < 64 * (uzoff_t)0x400) {
          ZIPERR(ZE_WRITE, "Not enough space to write split");
        }
      }

      /* close this split */
      if (split_method == 1 && current_local_disk == current_disk) {
        /* keep split open so can update it */
        current_local_tempname = tempzip;
      } else {
        /* close split */
        close_split(current_disk, y, tempzip);
        y = NULL;
        free(tempzip);
        tempzip = NULL;
      }
      cd_entries_this_disk = 0;
      bytes_this_split = 0;

      /* increment disk - disks are numbered 0, 1, 2, ... and
         splits are 01, 02, ... */
      current_disk++;

      if (split_method == 2 && split_bell) {
        /* bell when pause to ask for next split */
        putc('\007', mesg);
        fflush(mesg);
      }

      for (;;) {
        /* if method 2 pause and allow changing path */
        if (split_method == 2) {
          if (ask_for_split_write_path(current_disk) == 0) {
            ZIPERR(ZE_ABORT, "could not write split");
          }
        }

        /* open next split */
#if defined(UNIX) && !defined(NO_MKSTEMP)
        {
          int yd;
          int i;

          /* use mkstemp to avoid race condition and compiler warning */

          if (tempath != NULL)
          {
            /* if -b used to set temp file dir use that for split temp */
            if ((tempzip = malloc(strlen(tempath) + 12)) == NULL) {
              ZIPERR(ZE_MEM, "allocating temp filename");
            }
            strcpy(tempzip, tempath);
            if (lastchar(tempzip) != '/')
              strcat(tempzip, "/");
          }
          else
          {
            /* create path by stripping name and appending template */
            if ((tempzip = malloc(strlen(zipfile) + 12)) == NULL) {
            ZIPERR(ZE_MEM, "allocating temp filename");
            }
            strcpy(tempzip, zipfile);
            for(i = strlen(tempzip); i > 0; i--) {
              if (tempzip[i - 1] == '/')
                break;
            }
            tempzip[i] = '\0';
          }
          strcat(tempzip, "ziXXXXXX");

          if ((yd = mkstemp(tempzip)) == EOF) {
            ZIPERR(ZE_TEMP, tempzip);
          }
          if ((y = fdopen(yd, FOPW_TMP)) == NULL) {
            ZIPERR(ZE_TEMP, tempzip);
          }
        }
#else
        if ((tempzip = tempname(zipfile)) == NULL) {
          ZIPERR(ZE_MEM, "allocating temp filename");
        }
        if ((y = zfopen(tempzip, FOPW_TMP)) == NULL) {
          ZIPERR(ZE_TEMP, tempzip);
        }
#endif

        r = fwrite((char *)buffer + bytes_written, 1, bytes_to_write, y);
        bytes_written += r;
        bytes_this_split += r;
        if (!(mode == BFWRITE_HEADER ||
              mode == BFWRITE_LOCALHEADER ||
              mode == BFWRITE_CENTRALHEADER)) {
          bytes_this_entry += r;
        }
        if (bytes_to_write > r) {
          /* buffer bigger than split */
          if (split_method == 2) {
            /* let user choose another disk */
            zipwarn("Not enough room on disk", "");
            continue;
          } else {
            ZIPERR(ZE_WRITE, "Not enough room on disk");
          }
        }
        if (mode == BFWRITE_LOCALHEADER ||
            mode == BFWRITE_HEADER ||
            mode == BFWRITE_CENTRALHEADER) {
          if (split_method == 1 && current_local_file &&
              current_local_disk != current_disk) {
            /* We're opening a new split because the next header
               did not fit on the last split.  We need to now close
               the last split and update the pointers for
               the current split. */
            close_split(current_local_disk, current_local_file,
                        current_local_tempname);
            free(current_local_tempname);
          }
          current_local_tempname = tempzip;
          current_local_file = y;
          current_local_offset = 0;
          current_local_disk = current_disk;
        }
        break;
      }
    }
    else
    {
      /* likely have more than fits but no splits */

      /* probably already have error "no space left on device" */
      /* could let flush_outbuf() handle error but bfwrite() is called for
         headers also */
      if (ferror(y))
        ziperr(ZE_WRITE, "write error on zip file");
    }
  }


  /* display dots for archive instead of for each file */
  if (display_globaldots) {
    if (dot_size > 0) {
      /* initial space */
      if (dot_count == -1) {
#ifndef WINDLL
        putc(' ', mesg);
        fflush(mesg);
#else
        fprintf(stdout,"%c",' ');
#endif
        /* assume a header will be written first, so avoid 0 */
        dot_count = 1;
      }
      /* skip incrementing dot count for small buffers like for headers */
      if (size * count > 1000) {
        dot_count++;
        if (dot_size <= dot_count * (zoff_t)size * (zoff_t)count) dot_count = 0;
      }
    }
    if (dot_size && !dot_count) {
      dot_count++;
#ifndef WINDLL
      putc('.', mesg);
      fflush(mesg);
#else
      fprintf(stdout,"%c",'.');
#endif
      mesg_line_started = 1;
    }
  }


  return bytes_written;
}


#ifdef UNICODE_SUPPORT

/*---------------------------------------------
 * Unicode conversion functions
 *
 * Provided by Paul Kienitz
 *
 * Some modifications to work with Zip
 *
 *---------------------------------------------
 */

/*
   NOTES APPLICABLE TO ALL STRING FUNCTIONS:

   All of the x_to_y functions take parameters for an output buffer and
   its available length, and return an int.  The value returned is the
   length of the string that the input produces, which may be larger than
   the provided buffer length.  If the returned value is less than the
   buffer length, then the contents of the buffer will be null-terminated;
   otherwise, it will not be terminated and may be invalid, possibly
   stopping in the middle of a multibyte sequence.

   In all cases you may pass NULL as the buffer and/or 0 as the length, if
   you just want to learn how much space the string is going to require.

   The functions will return -1 if the input is invalid UTF-8 or cannot be
   encoded as UTF-8.
*/

/* utility functions for managing UTF-8 and UCS-4 strings */


/* utf8_char_bytes
 *
 * Returns the number of bytes used by the first character in a UTF-8
 * string, or -1 if the UTF-8 is invalid or null.
 */
local int utf8_char_bytes(utf8)
  ZCONST char *utf8;
{
  int      t, r;
  unsigned lead;

  if (!utf8)
    return -1;          /* no input */
  lead = (unsigned char) *utf8;
  if (lead < 0x80)
    r = 1;              /* an ascii-7 character */
  else if (lead < 0xC0)
    return -1;          /* error: trailing byte without lead byte */
  else if (lead < 0xE0)
    r = 2;              /* an 11 bit character */
  else if (lead < 0xF0)
    r = 3;              /* a 16 bit character */
  else if (lead < 0xF8)
    r = 4;              /* a 21 bit character (the most currently used) */
  else if (lead < 0xFC)
    r = 5;              /* a 26 bit character (shouldn't happen) */
  else if (lead < 0xFE)
    r = 6;              /* a 31 bit character (shouldn't happen) */
  else
    return -1;          /* error: invalid lead byte */
  for (t = 1; t < r; t++)
    if ((unsigned char) utf8[t] < 0x80 || (unsigned char) utf8[t] >= 0xC0)
      return -1;        /* error: not enough valid trailing bytes */
  return r;
}


/* ucs4_char_from_utf8
 *
 * Given a reference to a pointer into a UTF-8 string, returns the next
 * UCS-4 character and advances the pointer to the next character sequence.
 * Returns ~0 and does not advance the pointer when input is ill-formed.
 *
 * Since the Unicode standard says 32-bit values won't be used (just
 * up to the current 21-bit mappings) changed this to signed to allow -1 to
 * be returned.
 */
long ucs4_char_from_utf8(utf8)
  ZCONST char **utf8;
{
  ulg  ret;
  int  t, bytes;

  if (!utf8)
    return -1;                          /* no input */
  bytes = utf8_char_bytes(*utf8);
  if (bytes <= 0)
    return -1;                          /* invalid input */
  if (bytes == 1)
    ret = **utf8;                       /* ascii-7 */
  else
    ret = **utf8 & (0x7F >> bytes);     /* lead byte of a multibyte sequence */
  (*utf8)++;
  for (t = 1; t < bytes; t++)           /* consume trailing bytes */
    ret = (ret << 6) | (*((*utf8)++) & 0x3F);
  return (long) ret;
}


/* utf8_from_ucs4_char - Convert UCS char to UTF-8
 *
 * Returns the number of bytes put into utf8buf to represent ch, from 1 to 6,
 * or -1 if ch is too large to represent.  utf8buf must have room for 6 bytes.
 */
local int utf8_from_ucs4_char(utf8buf, ch)
  char *utf8buf;
  ulg ch;
{
  int trailing = 0;
  int leadmask = 0x80;
  int leadbits = 0x3F;
  ulg tch = ch;
  int ret;

  if (ch > 0x7FFFFFFF)
    return -1;                /* UTF-8 can represent 31 bits */
  if (ch < 0x7F)
  {
    *utf8buf++ = (char) ch;   /* ascii-7 */
    return 1;
  }
  do {
    trailing++;
    leadmask = (leadmask >> 1) | 0x80;
    leadbits >>= 1;
    tch >>= 6;
  } while (tch & ~leadbits);
  ret = trailing + 1;
  /* produce lead byte */
  *utf8buf++ = (char) (leadmask | (ch >> (6 * trailing)));
  /* produce trailing bytes */
  while (--trailing >= 0)
    *utf8buf++ = (char) (0x80 | ((ch >> (6 * trailing)) & 0x3F));
  return ret;
}


/*===================================================================*/

/* utf8_to_ucs4_string - convert UTF-8 string to UCS string
 *
 * Return UCS count.  Now returns int so can return -1.
 */
local int utf8_to_ucs4_string(utf8, ucs4buf, buflen)
  ZCONST char *utf8;
  ulg *ucs4buf;
  int buflen;
{
  int count = 0;

  for (;;)
  {
    long ch = ucs4_char_from_utf8(&utf8);
    if (ch == -1)
      return -1;
    else
    {
      if (ucs4buf && count < buflen)
        ucs4buf[count] = ch;
      if (ch == 0)
        return count;
      count++;
    }
  }
}


/* ucs4_string_to_utf8
 *
 *
 */
local int ucs4_string_to_utf8(ucs4, utf8buf, buflen)
  ZCONST ulg *ucs4;
  char *utf8buf;
  int buflen;
{
  char mb[6];
  int  count = 0;

  if (!ucs4)
    return -1;
  for (;;)
  {
    int mbl = utf8_from_ucs4_char(mb, *ucs4++);
    int c;
    if (mbl <= 0)
      return -1;
    /* We could optimize this a bit by passing utf8buf + count */
    /* directly to utf8_from_ucs4_char when buflen >= count + 6... */
    c = buflen - count;
    if (mbl < c)
      c = mbl;
    if (utf8buf && count < buflen)
      strncpy(utf8buf + count, mb, c);
    if (mbl == 1 && !mb[0])
      return count;           /* terminating nul */
    count += mbl;
  }
}


#if 0  /* currently unused */
/* utf8_chars
 *
 * Wrapper: counts the actual unicode characters in a UTF-8 string.
 */
local int utf8_chars(utf8)
  ZCONST char *utf8;
{
  return utf8_to_ucs4_string(utf8, NULL, 0);
}
#endif


/* --------------------------------------------------- */
/* Unicode Support
 *
 * These functions common for all Unicode ports.
 *
 * These functions should allocate and return strings that can be
 * freed with free().
 *
 * 8/27/05 EG
 *
 * Use zwchar for wide char which is unsigned long
 * in zip.h and 32 bits.  This avoids problems with
 * different sizes of wchar_t.
 */

#ifdef WIN32

zwchar *wchar_to_wide_string(wchar_string)
  wchar_t *wchar_string;
{
  int i;
  int wchar_len;
  zwchar *wide_string;

  wchar_len = wcslen(wchar_string);

  if ((wide_string = malloc((wchar_len + 1) * sizeof(zwchar))) == NULL) {
    ZIPERR(ZE_MEM, "wchar to wide conversion");
  }
  for (i = 0; i <= wchar_len; i++) {
    wide_string[i] = wchar_string[i];
  }

  return wide_string;
}

/* is_ascii_stringw
 * Checks if a wide string is all ascii
 */
int is_ascii_stringw(wstring)
  wchar_t *wstring;
{
  wchar_t *pw;
  wchar_t cw;

  if (wstring == NULL)
    return 0;

  for (pw = wstring; (cw = *pw) != '\0'; pw++) {
    if (cw > 0x7F) {
      return 0;
    }
  }
  return 1;
}

#endif

/* is_ascii_string
 * Checks if a string is all ascii
 */
int is_ascii_string(mbstring)
  char *mbstring;
{
  char *p;
  uch c;

  if (mbstring == NULL)
    return 0;

  for (p = mbstring; (c = (uch)*p) != '\0'; p++) {
    if (c > 0x7F) {
      return 0;
    }
  }
  return 1;
}

/* local to UTF-8 */
char *local_to_utf8_string(local_string)
  char *local_string;
{
  zwchar *wide_string = local_to_wide_string(local_string);
  char *utf8_string = wide_to_utf8_string(wide_string);

  free(wide_string);
  return utf8_string;
}

/* wide_char_to_escape_string
   provides a string that represents a wide char not in local char set

   An initial try at an algorithm.  Suggestions welcome.

   If not an ASCII char, probably need 2 bytes at least.  So if
   a 2-byte wide encode it as 4 hex digits with a leading #U.
   Since the Unicode standard has been frozen, it looks like 3 bytes
   should be enough for any large Unicode character.  In these cases
   prefix the string with #L.
   So
   #U1234
   is a 2-byte wide character with bytes 0x12 and 0x34 while
   #L123456
   is a 3-byte wide with bytes 0x12, 0x34, and 0x56.
   On Windows, wide that need two wide characters as a surrogate pair
   to represent them need to be converted to a single number.
  */

 /* set this to the max bytes an escape can be */
#define MAX_ESCAPE_BYTES 8

char *wide_char_to_escape_string(wide_char)
  zwchar wide_char;
{
  int i;
  zwchar w = wide_char;
  uch b[9];
  char e[7];
  int len;
  char *r;

  /* fill byte array with zeros */
  for (len = 0; len < sizeof(zwchar); len++) {
    b[len] = 0;
  }
  /* get bytes in right to left order */
  for (len = 0; w; len++) {
    b[len] = (char)(w % 0x100);
    w /= 0x100;
  }

  if ((r = malloc(MAX_ESCAPE_BYTES + 8)) == NULL) {
    ZIPERR(ZE_MEM, "wide_char_to_escape_string");
  }
  strcpy(r, "#");
  /* either 2 bytes or 4 bytes */
  if (len < 3) {
    len = 2;
    strcat(r, "U");
  } else {
    len = 3;
    strcat(r, "L");
  }
  for (i = len - 1; i >= 0; i--) {
    sprintf(e, "%02x", b[i]);
    strcat(r, e);
  }
  return r;
}

#if 0
/* returns the wide character represented by the escape string */
zwchar escape_string_to_wide(escape_string)
  char *escape_string;
{
  int i;
  zwchar w;
  char c;
  char u;
  int len;
  char *e = escape_string;

  if (e == NULL) {
    return 0;
  }
  if (e[0] != '#') {
    /* no leading # */
    return 0;
  }
  len = strlen(e);
  /* either #U1234 or #L123456 format */
  if (len != 6 && len != 8) {
    return 0;
  }
  w = 0;
  if (e[1] == 'L') {
    if (len != 8) {
      return 0;
    }
    /* 3 bytes */
    for (i = 2; i < 8; i++) {
      c = e[i];
      u = toupper(c);
      if (u >= 'A' && u <= 'F') {
        w = w * 0x10 + (zwchar)(u + 10 - 'A');
      } else if (c >= '0' && c <= '9') {
        w = w * 0x10 + (zwchar)(c - '0');
      } else {
        return 0;
      }
    }
  } else if (e[1] == 'U') {
    /* 2 bytes */
    for (i = 2; i < 6; i++) {
      c = e[i];
      u = toupper(c);
      if (u >= 'A' && u <= 'F') {
        w = w * 0x10 + (zwchar)(u + 10 - 'A');
      } else if (c >= '0' && c <= '9') {
        w = w * 0x10 + (zwchar)(c - '0');
      } else {
        return 0;
      }
    }
  }
  return w;
}
#endif


char *local_to_escape_string(local_string)
  char *local_string;
{
  zwchar *wide_string = local_to_wide_string(local_string);
  char *escape_string = wide_to_escape_string(wide_string);

  free(wide_string);
  return escape_string;
}

#ifdef WIN32
char *wchar_to_local_string(wstring)
  wchar_t *wstring;
{
  zwchar *wide_string = wchar_to_wide_string(wstring);
  char *local_string = wide_to_local_string(wide_string);

  free(wide_string);

  return local_string;
}
#endif


#ifndef WIN32   /* The Win32 port uses a system-specific variant. */
/* convert wide character string to multi-byte character string */
char *wide_to_local_string(wide_string)
  zwchar *wide_string;
{
  int i;
  wchar_t wc;
  int b;
  int state_dependent;
  int wsize = 0;
  int max_bytes = MB_CUR_MAX;
  char buf[9];
  char *buffer = NULL;
  char *local_string = NULL;

  for (wsize = 0; wide_string[wsize]; wsize++) ;

  if (MAX_ESCAPE_BYTES > max_bytes)
    max_bytes = MAX_ESCAPE_BYTES;

  if ((buffer = (char *)malloc(wsize * max_bytes + 1)) == NULL) {
    ZIPERR(ZE_MEM, "wide_to_local_string");
  }

  /* convert it */
  buffer[0] = '\0';
  /* set initial state if state-dependent encoding */
  wc = (wchar_t)'a';
  b = wctomb(NULL, wc);
  if (b == 0)
    state_dependent = 0;
  else
    state_dependent = 1;
  for (i = 0; i < wsize; i++) {
    if (sizeof(wchar_t) < 4 && wide_string[i] > 0xFFFF) {
      /* wchar_t probably 2 bytes */
      /* could do surrogates if state_dependent and wctomb can do */
      wc = zwchar_to_wchar_t_default_char;
    } else {
      wc = (wchar_t)wide_string[i];
    }
    b = wctomb(buf, wc);
    if (unicode_escape_all) {
      if (b == 1 && (uch)buf[0] <= 0x7f) {
        /* ASCII */
        strncat(buffer, buf, b);
      } else {
        /* use escape for wide character */
        char *e = wide_char_to_escape_string(wide_string[i]);
        strcat(buffer, e);
        free(e);
      }
    } else if (b > 0) {
      /* multi-byte char */
      strncat(buffer, buf, b);
    } else {
      /* no MB for this wide */
      if (use_wide_to_mb_default) {
        /* default character */
        strcat(buffer, wide_to_mb_default_string);
      } else {
        /* use escape for wide character */
        char *e = wide_char_to_escape_string(wide_string[i]);
        strcat(buffer, e);
        free(e);
      }
    }
  }
  if ((local_string = (char *)malloc(strlen(buffer) + 1)) == NULL) {
    free(buffer);
    ZIPERR(ZE_MEM, "wide_to_local_string");
  }
  strcpy(local_string, buffer);
  free(buffer);

  return local_string;
}
#endif /* !WIN32 */


/* convert wide character string to escaped string */
char *wide_to_escape_string(wide_string)
  zwchar *wide_string;
{
  int i;
  int wsize = 0;
  char buf[9];
  char *buffer = NULL;
  char *escape_string = NULL;

  for (wsize = 0; wide_string[wsize]; wsize++) ;

  if ((buffer = (char *)malloc(wsize * MAX_ESCAPE_BYTES + 1)) == NULL) {
    ZIPERR(ZE_MEM, "wide_to_escape_string");
  }

  /* convert it */
  buffer[0] = '\0';
  for (i = 0; i < wsize; i++) {
    if (wide_string[i] <= 0x7f && isprint((char)wide_string[i])) {
      /* ASCII */
      buf[0] = (char)wide_string[i];
      buf[1] = '\0';
      strcat(buffer, buf);
    } else {
      /* use escape for wide character */
      char *e = wide_char_to_escape_string(wide_string[i]);
      strcat(buffer, e);
      free(e);
    }
  }
  if ((escape_string = (char *)malloc(strlen(buffer) + 1)) == NULL) {
    ZIPERR(ZE_MEM, "wide_to_escape_string");
  }
  strcpy(escape_string, buffer);
  free(buffer);

  return escape_string;
}


/* convert local string to display character set string */
char *local_to_display_string(local_string)
  char *local_string;
{
  char *temp_string;
  char *display_string;

  /* For Windows, OEM string should never be bigger than ANSI string, says
     CharToOem description.
     On UNIX, non-printable characters (0x00 - 0xFF) will be replaced by
     "^x", so more space may be needed.  Note that "^" itself is a valid
     name character, so this leaves an ambiguity, but UnZip displays
     names this way, too.  (0x00 is not possible, I hope.)
     For all other ports, just make a copy of local_string.
  */

#ifdef UNIX
  char *cp_dst;                 /* Character pointers used in the */
  char *cp_src;                 /*  copying/changing procedure.   */
#endif

  if ((temp_string = (char *)malloc(2 * strlen(local_string) + 1)) == NULL) {
    ZIPERR(ZE_MEM, "local_to_display_string");
  }

#ifdef WIN32
  /* convert to OEM display character set */
  local_to_oem_string(temp_string, local_string);
#else
# ifdef UNIX
  /* Copy source string, expanding non-printable characters to "^x". */
  cp_dst = temp_string;
  cp_src = local_string;
  while (*cp_src != '\0') {
    if ((unsigned char)*cp_src < ' ') {
      *cp_dst++ = '^';
      *cp_dst++ = '@'+ *cp_src++;
    }
    else {
      *cp_dst++ = *cp_src++;
    }
  }
  *cp_dst = '\0';
# else /* not UNIX */
  strcpy(temp_string, local_string);
# endif /* UNIX */
#endif

#ifdef EBCDIC
  {
    char *ebc;

    if ((ebc = malloc(strlen(display_string) + 1)) ==  NULL) {
      ZIPERR(ZE_MEM, "local_to_display_string");
    }
    strtoebc(ebc, display_string);
    free(display_string);
    display_string = ebc;
  }
#endif

  if ((display_string = (char *)malloc(strlen(temp_string) + 1)) == NULL) {
    ZIPERR(ZE_MEM, "local_to_display_string");
  }
  strcpy(display_string, temp_string);
  free(temp_string);

  return display_string;
}

/* UTF-8 to local */
char *utf8_to_local_string(utf8_string)
  char *utf8_string;
{
  zwchar *wide_string = utf8_to_wide_string(utf8_string);
  char *loc = wide_to_local_string(wide_string);
  if (wide_string)
    free(wide_string);
  return loc;
}

/* UTF-8 to local */
char *utf8_to_escape_string(utf8_string)
  char *utf8_string;
{
  zwchar *wide_string = utf8_to_wide_string(utf8_string);
  char *escape_string = wide_to_escape_string(wide_string);
  free(wide_string);
  return escape_string;
}

#ifndef WIN32   /* The Win32 port uses a system-specific variant. */
/* convert multi-byte character string to wide character string */
zwchar *local_to_wide_string(local_string)
  char *local_string;
{
  int wsize;
  wchar_t *wc_string;
  zwchar *wide_string;

  /* for now try to convert as string - fails if a bad char in string */
  wsize = mbstowcs(NULL, local_string, MB_CUR_MAX );
  if (wsize == (size_t)-1) {
    /* could not convert */
    return NULL;
  }

  /* convert it */
  if ((wc_string = (wchar_t *)malloc((wsize + 1) * sizeof(wchar_t))) == NULL) {
    ZIPERR(ZE_MEM, "local_to_wide_string");
  }
  wsize = mbstowcs(wc_string, local_string, strlen(local_string) + 1);
  wc_string[wsize] = (wchar_t) 0;

  /* in case wchar_t is not zwchar */
  if ((wide_string = (zwchar *)malloc((wsize + 1) * sizeof(zwchar))) == NULL) {
    ZIPERR(ZE_MEM, "local_to_wide_string");
  }
  for (wsize = 0; (wide_string[wsize] = (zwchar)wc_string[wsize]); wsize++) ;
  wide_string[wsize] = (zwchar)0;
  free(wc_string);

  return wide_string;
}
#endif /* !WIN32 */


#if 0
/* All wchar functions are only used by Windows and are
   now in win32zip.c so that the Windows functions can
   be used and multiple character wide characters can
   be handled easily. */
# ifndef WIN32
char *wchar_to_utf8_string(wstring)
  wchar_t *wstring;
{
  zwchar *wide_string = wchar_to_wide_string(wstring);
  char *local_string = wide_to_utf8_string(wide_string);

  free(wide_string);

  return local_string;
}
# endif
#endif


/* convert wide string to UTF-8 */
char *wide_to_utf8_string(wide_string)
  zwchar *wide_string;
{
  int mbcount;
  char *utf8_string;

  /* get size of utf8 string */
  mbcount = ucs4_string_to_utf8(wide_string, NULL, 0);
  if (mbcount == -1)
    return NULL;
  if ((utf8_string = (char *) malloc(mbcount + 1)) == NULL) {
    ZIPERR(ZE_MEM, "wide_to_utf8_string");
  }
  mbcount = ucs4_string_to_utf8(wide_string, utf8_string, mbcount + 1);
  if (mbcount == -1)
    return NULL;

  return utf8_string;
}

/* convert UTF-8 string to wide string */
zwchar *utf8_to_wide_string(utf8_string)
  char *utf8_string;
{
  int wcount;
  zwchar *wide_string;

  wcount = utf8_to_ucs4_string(utf8_string, NULL, 0);
  if (wcount == -1)
    return NULL;
  if ((wide_string = (zwchar *) malloc((wcount + 2) * sizeof(zwchar))) == NULL) {
    ZIPERR(ZE_MEM, "utf8_to_wide_string");
  }
  wcount = utf8_to_ucs4_string(utf8_string, wide_string, wcount + 1);

  return wide_string;
}


#endif /* UNICODE_SUPPORT */


/*---------------------------------------------------------------
 *  Long option support
 *  8/23/2003
 *
 *  Defines function get_option() to get and process the command
 *  line options and arguments from argv[].  The caller calls
 *  get_option() in a loop to get either one option and possible
 *  value or a non-option argument each loop.
 *
 *  This version does not include argument file support and can
 *  work directly on argv.  The argument file code complicates things and
 *  it seemed best to leave it out for now.  If argument file support (reading
 *  in command line arguments stored in a file and inserting into
 *  command line where @filename is found) is added later the arguments
 *  can change and a freeable copy of argv will be needed and can be
 *  created using copy_args in the left out code.
 *
 *  Supports short and long options as defined in the array options[]
 *  in zip.c, multiple short options in an argument (like -jlv), long
 *  option abbreviation (like --te for --temp-file if --te unique),
 *  short and long option values (like -b filename or --temp-file filename
 *  or --temp-file=filename), optional and required values, option negation
 *  by trailing - (like -S- to not include hidden and system files in MSDOS),
 *  value lists (like -x a b c), argument permuting (returning all options
 *  and values before any non-option arguments), and argument files (where any
 *  non-option non-value argument in form @path gets substituted with the
 *  white space separated arguments in the text file at path).  In this
 *  version argument file support has been removed to simplify development but
 *  may be added later.
 *
 *  E. Gordon
 */


/* message output - char casts are needed to handle constants */
#define oWARN(message) zipwarn((char *) message, "")
#define oERR(err,message) ZIPERR(err, (char *) message)


/* Although the below provides some support for multibyte characters
   the proper thing to do may be to use wide characters and support
   Unicode.  May get to it soon.  EG
 */

/* For now stay with muti-byte characters.  May support wide characters
   in Zip 3.1.
 */

/* multibyte character set support
   Multibyte characters use typically two or more sequential bytes
   to represent additional characters than can fit in a single byte
   character set.  The code used here is based on the ANSI mblen function. */
#ifdef MULTIBYTE_GETOPTNS
  int mb_clen(ptr)
    ZCONST char *ptr;
  {
    /* return the number of bytes that the char pointed to is.  Return 1 if
       null character or error like not start of valid multibyte character. */
    int cl;

    cl = mblen(ptr, MB_CUR_MAX);
    return (cl > 0) ? cl : 1;
  }
#endif


  /* moved to zip.h */
#if 0
#ifdef UNICODE_SUPPORT
# define MB_CLEN(ptr) (1)
# define MB_NEXTCHAR(ptr) ((ptr)++)
# ifdef MULTIBYTE_GETOPTNS
#    undef MULTIBYTE_GETOPTNS
# endif
#else
# ifdef _MBCS
#  ifndef MULTIBYTE_GETOPTNS
#    define MULTIBYTE_GETOPTNS
#  endif
# endif
/* multibyte character set support
   Multibyte characters use typically two or more sequential bytes
   to represent additional characters than can fit in a single byte
   character set.  The code used here is based on the ANSI mblen function. */
#  ifdef MULTIBYTE_GETOPTNS
  local int mb_clen OF((ZCONST char *));  /* declare proto first */
  local int mb_clen(ptr)
    ZCONST char *ptr;
  {
    /* return the number of bytes that the char pointed to is.  Return 1 if
       null character or error like not start of valid multibyte character. */
    int cl;

    cl = mblen(ptr, MB_CUR_MAX);
    return (cl > 0) ? cl : 1;
  }
#  define MB_CLEN(ptr) mb_clen(ptr)
#  define MB_NEXTCHAR(ptr) ((ptr) += MB_CLEN(ptr))
# else
#  define MB_CLEN(ptr) (1)
#  define MB_NEXTCHAR(ptr) ((ptr)++)
# endif
#endif
#endif


/* constants */

/* function get_args_from_arg_file() can return this in depth parameter */
#define ARG_FILE_ERR -1

/* internal settings for optchar */
#define SKIP_VALUE_ARG   -1
#define THIS_ARG_DONE    -2
#define START_VALUE_LIST -3
#define IN_VALUE_LIST    -4
#define NON_OPTION_ARG   -5
#define STOP_VALUE_LIST  -6
/* 7/25/04 EG */
#define READ_REST_ARGS_VERBATIM -7


/* global veriables */

int enable_permute = 1;                     /* yes - return options first */
/* 7/25/04 EG */
int doubledash_ends_options = 1;            /* when -- what follows are not options */

/* buffer for error messages (this sizing is a guess but must hold 2 paths) */
#define OPTIONERR_BUF_SIZE (FNMAX * 2 + 4000)
local char Far optionerrbuf[OPTIONERR_BUF_SIZE + 1];

/* error messages */
static ZCONST char Far op_not_neg_err[] = "option %s not negatable";
static ZCONST char Far op_req_val_err[] = "option %s requires a value";
static ZCONST char Far op_no_allow_val_err[] = "option %s does not allow a value";
static ZCONST char Far sh_op_not_sup_err[] = "short option '%c' not supported";
static ZCONST char Far oco_req_val_err[] = "option %s requires one character value";
static ZCONST char Far oco_no_mbc_err[] = "option %s does not support multibyte values";
static ZCONST char Far num_req_val_err[] = "option %s requires number value";
static ZCONST char Far long_op_ambig_err[] = "long option '%s' ambiguous";
static ZCONST char Far long_op_not_sup_err[] = "long option '%s' not supported";

static ZCONST char Far no_arg_files_err[] = "argument files not enabled\n";


/* below removed as only used for processing argument files */

/* get_nextarg */
/* get_args_from_string */
/* insert_args */
/* get_args_from_arg_file */


/* copy error, option name, and option description if any to buf */
local int optionerr(buf, err, optind, islong)
  char *buf;
  ZCONST char *err;
  int optind;
  int islong;
{
  char optname[50];

  if (options[optind].name && options[optind].name[0] != '\0') {
    if (islong)
      sprintf(optname, "'%s' (%s)", options[optind].longopt, options[optind].name);
    else
      sprintf(optname, "'%s' (%s)", options[optind].shortopt, options[optind].name);
  } else {
    if (islong)
      sprintf(optname, "'%s'", options[optind].longopt);
    else
      sprintf(optname, "'%s'", options[optind].shortopt);
  }
  sprintf(buf, err, optname);
  return 0;
}


/* copy_args
 *
 * Copy arguments in args, allocating storage with malloc.
 * Copies until a NULL argument is found or until max_args args
 * including args[0] are copied.  Set max_args to 0 to copy
 * until NULL.  Always terminates returned args[] with NULL arg.
 *
 * Any argument in the returned args can be freed with free().  Any
 * freed argument should be replaced with either another string
 * allocated with malloc or by NULL if last argument so that free_args
 * will properly work.
 */
char **copy_args(args, max_args)
  char **args;
  int max_args;
{
  int j;
  char **new_args;

  if (args == NULL) {
    return NULL;
  }

  /* count args */
  for (j = 0; args[j] && (max_args == 0 || j < max_args); j++) ;

  if ((new_args = (char **) malloc((j + 1) * sizeof(char *))) == NULL) {
    oERR(ZE_MEM, "ca");
  }

  for (j = 0; args[j] && (max_args == 0 || j < max_args); j++) {
    if (args[j] == NULL) {
      /* null argument is end of args */
      new_args[j] = NULL;
      break;
    }
    if ((new_args[j] = malloc(strlen(args[j]) + 1)) == NULL) {
      free_args(new_args);
      oERR(ZE_MEM, "ca");
    }
    strcpy(new_args[j], args[j]);
  }
  new_args[j] = NULL;

  return new_args;
}


/* free args - free args created with one of these functions */
int free_args(args)
  char **args;
{
  int i;

  if (args == NULL) {
    return 0;
  }

  for (i = 0; args[i]; i++) {
    free(args[i]);
  }
  free(args);
  return i;
}


/* insert_arg
 *
 * Insert the argument arg into the array args before argument at_arg.
 * Return the new count of arguments (argc).
 *
 * If free_args is true, this function frees the old args array
 * (but not the component strings).  DO NOT set free_args on original
 * argv but only on args allocated with malloc.
 */

int insert_arg(pargs, arg, at_arg, free_args)
   char ***pargs;
   ZCONST char *arg;
   int at_arg;
   int free_args;
{
   char *newarg = NULL;
   char **args;
   char **newargs = NULL;
   int argnum;
   int newargnum;
   int argcnt;
   int newargcnt;

   if (pargs == NULL) {
     return 0;
   }
   args = *pargs;

   /* count args */
   if (args == NULL) {
     argcnt = 0;
   } else {
     for (argcnt = 0; args[argcnt]; argcnt++) ;
   }
   if (arg == NULL) {
     /* done */
     return argcnt;
   }
   newargcnt = argcnt + 1;

   /* get storage for new args */
   if ((newargs = (char **) malloc((newargcnt + 1) * sizeof(char *))) == NULL) {
     oERR(ZE_MEM, "ia");
   }

   /* copy argument pointers from args to position at_arg, copy arg, then rest args */
   argnum = 0;
   newargnum = 0;
   if (args) {
     for (; args[argnum] && argnum < at_arg; argnum++) {
       newargs[newargnum++] = args[argnum];
     }
   }
   /* copy new arg */
   if ((newarg = (char *) malloc(strlen(arg) + 1)) == NULL) {
     oERR(ZE_MEM, "ia");
   }
   strcpy(newarg, arg);

   newargs[newargnum++] = newarg;
   if (args) {
     for ( ; args[argnum]; argnum++) {
       newargs[newargnum++] = args[argnum];
     }
   }
   newargs[newargnum] = NULL;

   /* free old args array but not component strings - this assumes that
      args was allocated with malloc as copy_args does.  DO NOT DO THIS
      on the original argv.
    */
   if (free_args)
     free(args);

   *pargs = newargs;

   return newargnum;
}

/* ------------------------------------- */




/* get_shortopt
 *
 * Get next short option from arg.  The state is stored in argnum, optchar, and
 * option_num so no static storage is used.  Returns the option_ID.
 *
 * parameters:
 *    args        - argv array of arguments
 *    argnum      - index of current arg in args
 *    optchar     - pointer to index of next char to process.  Can be 0 or
 *                  const defined at top of this file like THIS_ARG_DONE
 *    negated     - on return pointer to int set to 1 if option negated or 0 otherwise
 *    value       - on return pointer to string set to value of option if any or NULL
 *                  if none.  If value is returned then the caller should free()
 *                  it when not needed anymore.
 *    option_num  - pointer to index in options[] of returned option or
 *                  o_NO_OPTION_MATCH if none.  Do not change as used by
 *                  value lists.
 *    depth       - recursion depth (0 at top level, 1 or more in arg files)
 */
local unsigned long get_shortopt(args, argnum, optchar, negated, value,
                                 option_num, depth)
  char **args;
  int argnum;
  int *optchar;
  int *negated;
  char **value;
  int *option_num;
  int depth;
{
  char *shortopt;
  int clen;
  char *nextchar;
  char *s;
  char *start;
  int op;
  char *arg;
  int match = -1;


  /* get arg */
  arg = args[argnum];
  /* current char in arg */
  nextchar = arg + (*optchar);
  clen = MB_CLEN(nextchar);
  /* next char in arg */
  (*optchar) +=  clen;
  /* get first char of short option */
  shortopt = arg + (*optchar);
  /* no value */
  *value = NULL;

  if (*shortopt == '\0') {
    /* no more options in arg */
    *optchar = 0;
    *option_num = o_NO_OPTION_MATCH;
    return 0;
  }

  /* look for match in options */
  clen = MB_CLEN(shortopt);
  for (op = 0; options[op].option_ID; op++) {
    s = options[op].shortopt;
    if (s && s[0] == shortopt[0]) {
      if (s[1] == '\0' && clen == 1) {
        /* single char match */
        match = op;
      } else {
        /* 2 wide short opt.  Could support more chars but should use long opts instead */
        if (s[1] == shortopt[1]) {
          /* match 2 char short opt or 2 byte char */
          match = op;
          if (clen == 1) (*optchar)++;
          break;
        }
      }
    }
  }

  if (match > -1) {
    /* match */
    clen = MB_CLEN(shortopt);
    nextchar = arg + (*optchar) + clen;
    /* check for trailing dash negating option */
    if (*nextchar == '-') {
      /* negated */
      if (options[match].negatable == o_NOT_NEGATABLE) {
        if (options[match].value_type == o_NO_VALUE) {
          optionerr(optionerrbuf, op_not_neg_err, match, 0);
          if (depth > 0) {
            /* unwind */
            oWARN(optionerrbuf);
            return o_ARG_FILE_ERR;
          } else {
            oERR(ZE_PARMS, optionerrbuf);
          }
        }
      } else {
        *negated = 1;
        /* set up to skip negating dash */
        (*optchar) += clen;
        clen = 1;
      }
    }

    /* value */
    clen = MB_CLEN(arg + (*optchar));
    /* optional value, one char value, and number value must follow option */
    if (options[match].value_type == o_ONE_CHAR_VALUE) {
      /* one char value */
      if (arg[(*optchar) + clen]) {
        /* has value */
        if (MB_CLEN(arg + (*optchar) + clen) > 1) {
          /* multibyte value not allowed for now */
          optionerr(optionerrbuf, oco_no_mbc_err, match, 0);
          if (depth > 0) {
            /* unwind */
            oWARN(optionerrbuf);
            return o_ARG_FILE_ERR;
          } else {
            oERR(ZE_PARMS, optionerrbuf);
          }
        }
        if ((*value = (char *) malloc(2)) == NULL) {
          oERR(ZE_MEM, "gso");
        }
        (*value)[0] = *(arg + (*optchar) + clen);
        (*value)[1] = '\0';
        *optchar += clen;
        clen = 1;
      } else {
        /* one char values require a value */
        optionerr(optionerrbuf, oco_req_val_err, match, 0);
        if (depth > 0) {
          oWARN(optionerrbuf);
          return o_ARG_FILE_ERR;
        } else {
          oERR(ZE_PARMS, optionerrbuf);
        }
      }
    } else if (options[match].value_type == o_NUMBER_VALUE) {
      /* read chars until end of number */
      start = arg + (*optchar) + clen;
      if (*start == '+' || *start == '-') {
        start++;
      }
      s = start;
      for (; isdigit(*s); MB_NEXTCHAR(s)) ;
      if (s == start) {
        /* no digits */
        optionerr(optionerrbuf, num_req_val_err, match, 0);
        if (depth > 0) {
          oWARN(optionerrbuf);
          return o_ARG_FILE_ERR;
        } else {
          oERR(ZE_PARMS, optionerrbuf);
        }
      }
      start = arg + (*optchar) + clen;
      if ((*value = (char *) malloc((int)(s - start) + 1)) == NULL) {
        oERR(ZE_MEM, "gso");
      }
      *optchar += (int)(s - start);
      strncpy(*value, start, (int)(s - start));
      (*value)[(int)(s - start)] = '\0';
      clen = MB_CLEN(s);
    } else if (options[match].value_type == o_OPTIONAL_VALUE) {
      /* optional value */
      /* This seemed inconsistent so now if no value attached to argument look
         to the next argument if that argument is not an option for option
         value - 11/12/04 EG */
      if (arg[(*optchar) + clen]) {
        /* has value */
        /* add support for optional = - 2/6/05 EG */
        if (arg[(*optchar) + clen] == '=') {
          /* skip = */
          clen++;
        }
        if (arg[(*optchar) + clen]) {
          if ((*value = (char *)malloc(strlen(arg + (*optchar) + clen) + 1))
              == NULL) {
            oERR(ZE_MEM, "gso");
          }
          strcpy(*value, arg + (*optchar) + clen);
        }
        *optchar = THIS_ARG_DONE;
      } else if (args[argnum + 1] && args[argnum + 1][0] != '-') {
        /* use next arg for value */
        if ((*value = (char *)malloc(strlen(args[argnum + 1]) + 1)) == NULL) {
          oERR(ZE_MEM, "gso");
        }
        /* using next arg as value */
        strcpy(*value, args[argnum + 1]);
        *optchar = SKIP_VALUE_ARG;
      }
    } else if (options[match].value_type == o_REQUIRED_VALUE ||
               options[match].value_type == o_VALUE_LIST) {
      /* see if follows option */
      if (arg[(*optchar) + clen]) {
        /* has value following option as -ovalue */
        /* add support for optional = - 6/5/05 EG */
        if (arg[(*optchar) + clen] == '=') {
          /* skip = */
          clen++;
        }
          if ((*value = (char *)malloc(strlen(arg + (*optchar) + clen) + 1))
              == NULL) {
          oERR(ZE_MEM, "gso");
        }
        strcpy(*value, arg + (*optchar) + clen);
        *optchar = THIS_ARG_DONE;
      } else {
        /* use next arg for value */
        if (args[argnum + 1]) {
          if ((*value = (char *)malloc(strlen(args[argnum + 1]) + 1)) == NULL) {
            oERR(ZE_MEM, "gso");
          }
          strcpy(*value, args[argnum + 1]);
          if (options[match].value_type == o_VALUE_LIST) {
            *optchar = START_VALUE_LIST;
          } else {
            *optchar = SKIP_VALUE_ARG;
          }
        } else {
          /* no value found */
          optionerr(optionerrbuf, op_req_val_err, match, 0);
          if (depth > 0) {
            oWARN(optionerrbuf);
            return o_ARG_FILE_ERR;
          } else {
            oERR(ZE_PARMS, optionerrbuf);
          }
        }
      }
    }

    *option_num = match;
    return options[match].option_ID;
  }
  sprintf(optionerrbuf, sh_op_not_sup_err, *shortopt);
  if (depth > 0) {
    /* unwind */
    oWARN(optionerrbuf);
    return o_ARG_FILE_ERR;
  } else {
    oERR(ZE_PARMS, optionerrbuf);
  }
  return 0;
}


/* get_longopt
 *
 * Get the long option in args array at argnum.
 * Parameters same as for get_shortopt.
 */

local unsigned long get_longopt(args, argnum, optchar, negated, value,
                                option_num, depth)
  char **args;
  int argnum;
  int *optchar;
  int *negated;
  char **value;
  int *option_num;
  int depth;
{
  char *longopt;
  char *lastchr;
  char *valuestart;
  int op;
  char *arg;
  int match = -1;
  *value = NULL;

  if (args == NULL) {
    *option_num = o_NO_OPTION_MATCH;
    return 0;
  }
  if (args[argnum] == NULL) {
    *option_num = o_NO_OPTION_MATCH;
    return 0;
  }
  /* copy arg so can chop end if value */
  if ((arg = (char *)malloc(strlen(args[argnum]) + 1)) == NULL) {
    oERR(ZE_MEM, "glo");
  }
  strcpy(arg, args[argnum]);

  /* get option */
  longopt = arg + 2;
  /* no value */
  *value = NULL;

  /* find = */
  for (lastchr = longopt, valuestart = longopt;
       *valuestart && *valuestart != '=';
       lastchr = valuestart, MB_NEXTCHAR(valuestart)) ;
  if (*valuestart) {
    /* found =value */
    *valuestart = '\0';
    valuestart++;
  } else {
    valuestart = NULL;
  }

  if (*lastchr == '-') {
    /* option negated */
    *negated = 1;
    *lastchr = '\0';
  } else {
    *negated = 0;
  }

  /* look for long option match */
  for (op = 0; options[op].option_ID; op++) {
    if (options[op].longopt && strcmp(options[op].longopt, longopt) == 0) {
      /* exact match */
      match = op;
      break;
    }
    if (options[op].longopt && strncmp(options[op].longopt, longopt, strlen(longopt)) == 0) {
      if (match > -1) {
        sprintf(optionerrbuf, long_op_ambig_err, longopt);
        free(arg);
        if (depth > 0) {
          /* unwind */
          oWARN(optionerrbuf);
          return o_ARG_FILE_ERR;
        } else {
          oERR(ZE_PARMS, optionerrbuf);
        }
      }
      match = op;
    }
  }

  if (match == -1) {
    sprintf(optionerrbuf, long_op_not_sup_err, longopt);
    free(arg);
    if (depth > 0) {
      oWARN(optionerrbuf);
      return o_ARG_FILE_ERR;
    } else {
      oERR(ZE_PARMS, optionerrbuf);
    }
  }

  /* one long option an arg */
  *optchar = THIS_ARG_DONE;

  /* if negated then see if allowed */
  if (*negated && options[match].negatable == o_NOT_NEGATABLE) {
    optionerr(optionerrbuf, op_not_neg_err, match, 1);
    free(arg);
    if (depth > 0) {
      /* unwind */
      oWARN(optionerrbuf);
      return o_ARG_FILE_ERR;
    } else {
      oERR(ZE_PARMS, optionerrbuf);
    }
  }
  /* get value */
  if (options[match].value_type == o_OPTIONAL_VALUE) {
    /* optional value in form option=value */
    if (valuestart) {
      /* option=value */
      if ((*value = (char *)malloc(strlen(valuestart) + 1)) == NULL) {
        free(arg);
        oERR(ZE_MEM, "glo");
      }
      strcpy(*value, valuestart);
    }
  } else if (options[match].value_type == o_REQUIRED_VALUE ||
             options[match].value_type == o_NUMBER_VALUE ||
             options[match].value_type == o_ONE_CHAR_VALUE ||
             options[match].value_type == o_VALUE_LIST) {
    /* handle long option one char and number value as required value */
    if (valuestart) {
      /* option=value */
      if ((*value = (char *)malloc(strlen(valuestart) + 1)) == NULL) {
        free(arg);
        oERR(ZE_MEM, "glo");
      }
      strcpy(*value, valuestart);
    } else {
      /* use next arg */
      if (args[argnum + 1]) {
        if ((*value = (char *)malloc(strlen(args[argnum + 1]) + 1)) == NULL) {
          free(arg);
          oERR(ZE_MEM, "glo");
        }
        /* using next arg as value */
        strcpy(*value, args[argnum + 1]);
        if (options[match].value_type == o_VALUE_LIST) {
          *optchar = START_VALUE_LIST;
        } else {
          *optchar = SKIP_VALUE_ARG;
        }
      } else {
        /* no value found */
        optionerr(optionerrbuf, op_req_val_err, match, 1);
        free(arg);
        if (depth > 0) {
          /* unwind */
          oWARN(optionerrbuf);
          return o_ARG_FILE_ERR;
        } else {
          oERR(ZE_PARMS, optionerrbuf);
        }
      }
    }
  } else if (options[match].value_type == o_NO_VALUE) {
    /* this option does not accept a value */
    if (valuestart) {
      /* --option=value */
      optionerr(optionerrbuf, op_no_allow_val_err, match, 1);
      free(arg);
      if (depth > 0) {
        oWARN(optionerrbuf);
        return o_ARG_FILE_ERR;
      } else {
        oERR(ZE_PARMS, optionerrbuf);
      }
    }
  }
  free(arg);

  *option_num = match;
  return options[match].option_ID;
}



/* get_option
 *
 * Main interface for user.  Use this function to get options, values and
 * non-option arguments from a command line provided in argv form.
 *
 * To use get_option() first define valid options by setting
 * the global variable options[] to an array of option_struct.  Also
 * either change defaults below or make variables global and set elsewhere.
 * Zip uses below defaults.
 *
 * Call get_option() to get an option (like -b or --temp-file) and any
 * value for that option (like filename for -b) or a non-option argument
 * (like archive name) each call.  If *value* is not NULL after calling
 * get_option() it is a returned value and the caller should either store
 * the char pointer or free() it before calling get_option() again to avoid
 * leaking memory.  If a non-option non-value argument is returned get_option()
 * returns o_NON_OPTION_ARG and value is set to the entire argument.
 * When there are no more arguments get_option() returns 0.
 *
 * The parameters argnum (after set to 0 on initial call),
 * optchar, first_nonopt_arg, option_num, and depth (after initial
 * call) are set and maintained by get_option() and should not be
 * changed.  The parameters argc, negated, and value are outputs and
 * can be used by the calling program.  get_option() returns either the
 * option_ID for the current option, a special value defined in
 * zip.h, or 0 when no more arguments.
 *
 * The value returned by get_option() is the ID value in the options
 * table.  This value can be duplicated in the table if different
 * options are really the same option.  The index into the options[]
 * table is given by option_num, though the ID should be used as
 * option numbers change when the table is changed.  The ID must
 * not be 0 for any option as this ends the table.  If get_option()
 * finds an option not in the table it calls oERR to post an
 * error and exit.  Errors also result if the option requires a
 * value that is missing, a value is present but the option does
 * not take one, and an option is negated but is not
 * negatable.  Non-option arguments return o_NON_OPTION_ARG
 * with the entire argument in value.
 *
 * For Zip, permuting is on and all options and their values are
 * returned before any non-option arguments like archive name.
 *
 * The arguments "-" alone and "--" alone return as non-option arguments.
 * Note that "-" should not be used as part of a short option
 * entry in the table but can be used in the middle of long
 * options such as in the long option "a-long-option".  Now "--" alone
 * stops option processing, returning any arguments following "--" as
 * non-option arguments instead of options.
 *
 * Argument file support is removed from this version. It may be added later.
 *
 * After each call:
 *   argc       is set to the current size of args[] but should not change
 *                with argument file support removed,
 *   argnum     is the index of the current arg,
 *   value      is either the value of the returned option or non-option
 *                argument or NULL if option with no value,
 *   negated    is set if the option was negated by a trailing dash (-)
 *   option_num is set to either the index in options[] for the option or
 *                o_NO_OPTION_MATCH if no match.
 * Negation is checked before the value is read if the option is negatable so
 * that the - is not included in the value.  If the option is not negatable
 * but takes a value then the - will start the value.  If permuting then
 * argnum and first_nonopt_arg are unreliable and should not be used.
 *
 * Command line is read from left to right.  As get_option() finds non-option
 * arguments (arguments not starting with - and that are not values to options)
 * it moves later options and values in front of the non-option arguments.
 * This permuting is turned off by setting enable_permute to 0.  Then
 * get_option() will return options and non-option arguments in the order
 * found.  Currently permuting is only done after an argument is completely
 * processed so that any value can be moved with options they go with.  All
 * state information is stored in the parameters argnum, optchar,
 * first_nonopt_arg and option_num.  You should not change these after the
 * first call to get_option().  If you need to back up to a previous arg then
 * set argnum to that arg (remembering that args may have been permuted) and
 * set optchar = 0 and first_nonopt_arg to the first non-option argument if
 * permuting.  After all arguments are returned the next call to get_option()
 * returns 0.  The caller can then call free_args(args) if appropriate.
 *
 * get_option() accepts arguments in the following forms:
 *  short options
 *       of 1 and 2 characters, e.g. a, b, cc, d, and ba, after a single
 *       leading -, as in -abccdba.  In this example if 'b' is followed by 'a'
 *       it matches short option 'ba' else it is interpreted as short option
 *       b followed by another option.  The character - is not legal as a
 *       short option or as part of a 2 character short option.
 *
 *       If a short option has a value it immediately follows the option or
 *       if that option is the end of the arg then the next arg is used as
 *       the value.  So if short option e has a value, it can be given as
 *             -evalue
 *       or
 *             -e value
 *       and now
 *             -e=value
 *       but now that = is optional a leading = is stripped for the first.
 *       This change allows optional short option values to be defaulted as
 *             -e=
 *       Either optional or required values can be specified.  Optional values
 *       now use both forms as ignoring the later got confusing.  Any
 *       non-value short options can preceed a valued short option as in
 *             -abevalue
 *       Some value types (one_char and number) allow options after the value
 *       so if oc is an option that takes a character and n takes a number
 *       then
 *             -abocVccn42evalue
 *       returns value V for oc and value 42 for n.  All values are strings
 *       so programs may have to convert the "42" to a number.  See long
 *       options below for how value lists are handled.
 *
 *       Any short option can be negated by following it with -.  Any - is
 *       handled and skipped over before any value is read unless the option
 *       is not negatable but takes a value and then - starts the value.
 *
 *       If the value for an optional value is just =, then treated as no
 *       value.
 *
 *  long options
 *       of arbitrary length are assumed if an arg starts with -- but is not
 *       exactly --.  Long options are given one per arg and can be abbreviated
 *       if the abbreviation uniquely matches one of the long options.
 *       Exact matches always match before partial matches.  If ambiguous an
 *       error is generated.
 *
 *       Values are specified either in the form
 *             --longoption=value
 *       or can be the following arg if the value is required as in
 *             --longoption value
 *       Optional values to long options must be in the first form.
 *
 *       Value lists are specified by o_VALUE_LIST and consist of an option
 *       that takes a value followed by one or more value arguments.
 *       The two forms are
 *             --option=value
 *       or
 *             -ovalue
 *       for a single value or
 *             --option value1 value2 value3 ... --option2
 *       or
 *             -o value1 value2 value3 ...
 *       for a list of values.  The list ends at the next option, the
 *       end of the command line, or at a single "@" argument.
 *       Each value is treated as if it was preceeded by the option, so
 *             --option1 val1 val2
 *       with option1 value_type set to o_VALUE_LIST is the same as
 *             --option1=val1 --option1=val2
 *
 *       Long options can be negated by following the option with - as in
 *             --longoption-
 *       Long options with values can also be negated if this makes sense for
 *       the caller as:
 *             --longoption-=value
 *       If = is not followed by anything it is treated as no value.
 *
 *  @path
 *       When an argument in the form @path is encountered, the file at path
 *       is opened and white space separated arguments read from the file
 *       and inserted into the command line at that point as if the contents
 *       of the file were directly typed at that location.  The file can
 *       have options, files to zip, or anything appropriate at that location
 *       in the command line.  Since Zip has permuting enabled, options and
 *       files will propagate to the appropriate locations in the command
 *       line.
 *
 *       Argument files support has been removed from this version.  It may
 *       be added back later.
 *
 *  non-option argument
 *       is any argument not given above.  If enable_permute is 1 then
 *       these are returned after all options, otherwise all options and
 *       args are returned in order.  Returns option ID o_NON_OPTION_ARG
 *       and sets value to the argument.
 *
 *
 * Arguments to get_option:
 *  char ***pargs          - pointer to arg array in the argv form
 *  int *argc              - returns the current argc for args incl. args[0]
 *  int *argnum            - the index of the current argument (caller
 *                            should set = 0 on first call and not change
 *                            after that)
 *  int *optchar           - index of next short opt in arg or special
 *  int *first_nonopt_arg  - used by get_option to permute args
 *  int *negated           - option was negated (had trailing -)
 *  char *value            - value of option if any (free when done with it) or NULL
 *  int *option_num        - the index in options of the last option returned
 *                            (can be o_NO_OPTION_MATCH)
 *  int recursion_depth    - current depth of recursion
 *                            (always set to 0 by caller)
 *                            (always 0 with argument files support removed)
 *
 *  Caller should only read the returned option ID and the value, negated,
 *  and option_num (if required) parameters after each call.
 *
 *  Ed Gordon
 *  24 August 2003 (last updated 2 April 2008 EG)
 *
 */

unsigned long get_option(pargs, argc, argnum, optchar, value,
                         negated, first_nonopt_arg, option_num, recursion_depth)
  char ***pargs;
  int *argc;
  int *argnum;
  int *optchar;
  char **value;
  int *negated;
  int *first_nonopt_arg;
  int *option_num;
  int recursion_depth;
{
  char **args;
  unsigned long option_ID;

  int argcnt;
  int first_nonoption_arg;
  char *arg = NULL;
  int h;
  int optc;
  int argn;
  int j;
  int v;
  int read_rest_args_verbatim = 0;  /* 7/25/04 - ignore options and arg files for rest args */

  /* value is outdated.  The caller should free value before
     calling get_option again. */
  *value = NULL;

  /* if args is NULL then done */
  if (pargs == NULL) {
    *argc = 0;
    return 0;
  }
  args = *pargs;
  if (args == NULL) {
    *argc = 0;
    return 0;
  }

  /* count args */
  for (argcnt = 0; args[argcnt]; argcnt++) ;

  /* if no provided args then nothing to do */
  if (argcnt < 1 || (recursion_depth == 0 && argcnt < 2)) {
    *argc = argcnt;
    /* return 0 to note that no args are left */
    return 0;
  }

  *negated = 0;
  first_nonoption_arg = *first_nonopt_arg;
  argn = *argnum;
  optc = *optchar;

  if (optc == READ_REST_ARGS_VERBATIM) {
    read_rest_args_verbatim = 1;
  }

  if (argn == -1 || (recursion_depth == 0 && argn == 0)) {
    /* first call */
    /* if depth = 0 then args[0] is argv[0] so skip */
    *option_num = o_NO_OPTION_MATCH;
    optc = THIS_ARG_DONE;
    first_nonoption_arg = -1;
  }

  /* if option_num is set then restore last option_ID in case continuing value list */
  option_ID = 0;
  if (*option_num != o_NO_OPTION_MATCH) {
    option_ID = options[*option_num].option_ID;
  }

  /* get next option if any */
  for (;;)  {
    if (read_rest_args_verbatim) {
      /* rest of args after "--" are non-option args if doubledash_ends_options set */
      argn++;
      if (argn > argcnt || args[argn] == NULL) {
        /* done */
        option_ID = 0;
        break;
      }
      arg = args[argn];
      if ((*value = (char *)malloc(strlen(arg) + 1)) == NULL) {
        oERR(ZE_MEM, "go");
      }
      strcpy(*value, arg);
      *option_num = o_NO_OPTION_MATCH;
      option_ID = o_NON_OPTION_ARG;
      break;

    /* permute non-option args after option args so options are returned first */
    } else if (enable_permute) {
      if (optc == SKIP_VALUE_ARG || optc == THIS_ARG_DONE ||
          optc == START_VALUE_LIST || optc == IN_VALUE_LIST ||
          optc == STOP_VALUE_LIST) {
        /* moved to new arg */
        if (first_nonoption_arg > -1 && args[first_nonoption_arg]) {
          /* do the permuting - move non-options after this option */
          /* if option and value separate args or starting list skip option */
          if (optc == SKIP_VALUE_ARG || optc == START_VALUE_LIST) {
            v = 1;
          } else {
            v = 0;
          }
          for (h = first_nonoption_arg; h < argn; h++) {
            arg = args[first_nonoption_arg];
            for (j = first_nonoption_arg; j < argn + v; j++) {
              args[j] = args[j + 1];
            }
            args[j] = arg;
          }
          first_nonoption_arg += 1 + v;
        }
      }
    } else if (optc == NON_OPTION_ARG) {
      /* if not permuting then already returned arg */
      optc = THIS_ARG_DONE;
    }

    /* value lists */
    if (optc == STOP_VALUE_LIST) {
      optc = THIS_ARG_DONE;
    }

    if (optc == START_VALUE_LIST || optc == IN_VALUE_LIST) {
      if (optc == START_VALUE_LIST) {
        /* already returned first value */
        argn++;
        optc = IN_VALUE_LIST;
      }
      argn++;
      arg = args[argn];
      /* if end of args and still in list and there are non-option args then
         terminate list */
      if (arg == NULL && (optc == START_VALUE_LIST || optc == IN_VALUE_LIST)
          && first_nonoption_arg > -1) {
        /* terminate value list with @ */
        /* this is only needed for argument files */
        /* but is also good for show command line so command lines with lists
           can always be read back in */
        argcnt = insert_arg(&args, "@", first_nonoption_arg, 1);
        argn++;
        if (first_nonoption_arg > -1) {
          first_nonoption_arg++;
        }
      }

      arg = args[argn];
      if (arg && arg[0] == '@' && arg[1] == '\0') {
          /* inserted arguments terminator */
          optc = STOP_VALUE_LIST;
          continue;
      } else if (arg && arg[0] != '-') {  /* not option */
        /* - and -- are not allowed in value lists unless escaped */
        /* another value in value list */
        if ((*value = (char *)malloc(strlen(args[argn]) + 1)) == NULL) {
          oERR(ZE_MEM, "go");
        }
        strcpy(*value, args[argn]);
        break;

      } else {
        argn--;
        optc = THIS_ARG_DONE;
      }
    }

    /* move to next arg */
    if (optc == SKIP_VALUE_ARG) {
      argn += 2;
      optc = 0;
    } else if (optc == THIS_ARG_DONE) {
      argn++;
      optc = 0;
    }
    if (argn > argcnt) {
      break;
    }
    if (args[argn] == NULL) {
      /* done unless permuting and non-option args */
      if (first_nonoption_arg > -1 && args[first_nonoption_arg]) {
        /* return non-option arguments at end */
        if (optc == NON_OPTION_ARG) {
          first_nonoption_arg++;
        }
        /* after first pass args are permuted but skipped over non-option args */
        /* swap so argn points to first non-option arg */
        j = argn;
        argn = first_nonoption_arg;
        first_nonoption_arg = j;
      }
      if (argn > argcnt || args[argn] == NULL) {
        /* done */
        option_ID = 0;
        break;
      }
    }

    /* after swap first_nonoption_arg points to end which is NULL */
    if (first_nonoption_arg > -1 && (args[first_nonoption_arg] == NULL)) {
      /* only non-option args left */
      if (optc == NON_OPTION_ARG) {
        argn++;
      }
      if (argn > argcnt || args[argn] == NULL) {
        /* done */
        option_ID = 0;
        break;
      }
      if ((*value = (char *)malloc(strlen(args[argn]) + 1)) == NULL) {
        oERR(ZE_MEM, "go");
      }
      strcpy(*value, args[argn]);
      optc = NON_OPTION_ARG;
      option_ID = o_NON_OPTION_ARG;
      break;
    }

    arg = args[argn];

    /* is it an option */
    if (arg[0] == '-') {
      /* option */
      if (arg[1] == '\0') {
        /* arg = - */
        /* treat like non-option arg */
        *option_num = o_NO_OPTION_MATCH;
        if (enable_permute) {
          /* permute args to move all non-option args to end */
          if (first_nonoption_arg < 0) {
            first_nonoption_arg = argn;
          }
          argn++;
        } else {
          /* not permute args so return non-option args when found */
          if ((*value = (char *)malloc(strlen(arg) + 1)) == NULL) {
            oERR(ZE_MEM, "go");
          }
          strcpy(*value, arg);
          optc = NON_OPTION_ARG;
          option_ID = o_NON_OPTION_ARG;
          break;
        }

      } else if (arg[1] == '-') {
        /* long option */
        if (arg[2] == '\0') {
          /* arg = -- */
          if (doubledash_ends_options) {
            /* Now -- stops permuting and forces the rest of
               the command line to be read verbatim - 7/25/04 EG */

            /* never permute args after -- and return as non-option args */
            if (first_nonoption_arg < 1) {
              /* -- is first non-option argument - 8/7/04 EG */
              argn--;
            } else {
              /* go back to start of non-option args - 8/7/04 EG */
              argn = first_nonoption_arg - 1;
            }

            /* disable permuting and treat remaining arguments as not
               options */
            read_rest_args_verbatim = 1;
            optc = READ_REST_ARGS_VERBATIM;

          } else {
            /* treat like non-option arg */
            *option_num = o_NO_OPTION_MATCH;
            if (enable_permute) {
              /* permute args to move all non-option args to end */
              if (first_nonoption_arg < 0) {
                first_nonoption_arg = argn;
              }
              argn++;
            } else {
              /* not permute args so return non-option args when found */
              if ((*value = (char *)malloc(strlen(arg) + 1)) == NULL) {
                oERR(ZE_MEM, "go");
              }
              strcpy(*value, arg);
              optc = NON_OPTION_ARG;
              option_ID = o_NON_OPTION_ARG;
              break;
            }
          }

        } else {
          option_ID = get_longopt(args, argn, &optc, negated, value, option_num, recursion_depth);
          if (option_ID == o_ARG_FILE_ERR) {
            /* unwind as only get this if recursion_depth > 0 */
            return option_ID;
          }
          break;
        }

      } else {
        /* short option */
        option_ID = get_shortopt(args, argn, &optc, negated, value, option_num, recursion_depth);

        if (option_ID == o_ARG_FILE_ERR) {
          /* unwind as only get this if recursion_depth > 0 */
          return option_ID;
        }

        if (optc == 0) {
          /* if optc = 0 then ran out of short opts this arg */
          optc = THIS_ARG_DONE;
        } else {
          break;
        }
      }

#if 0
    /* argument file code left out
       so for now let filenames start with @
    */

    } else if (allow_arg_files && arg[0] == '@') {
      /* arg file */
      oERR(ZE_PARMS, no_arg_files_err);
#endif

    } else {
      /* non-option */
      if (enable_permute) {
        /* permute args to move all non-option args to end */
        if (first_nonoption_arg < 0) {
          first_nonoption_arg = argn;
        }
        argn++;
      } else {
        /* no permute args so return non-option args when found */
        if ((*value = (char *)malloc(strlen(arg) + 1)) == NULL) {
          oERR(ZE_MEM, "go");
        }
        strcpy(*value, arg);
        *option_num = o_NO_OPTION_MATCH;
        optc = NON_OPTION_ARG;
        option_ID = o_NON_OPTION_ARG;
        break;
      }

    }
  }

  *pargs = args;
  *argc = argcnt;
  *first_nonopt_arg = first_nonoption_arg;
  *argnum = argn;
  *optchar = optc;

  return option_ID;
}
