/* -*- Mode: C; c-file-style: "gnu"; indent-tabs-mode:nil -*- */
/* Handle a .class file embedded in a .zip/.jar archive.

   Copyright (c) 1996 Cygnus Support
   Copyright (C) 1997, 1998 The Hungry Programmers

   See the file "license.terms" for information on usage and redistribution
   of this file, and for a DISCLAIMER OF ALL WARRANTIES.

   Written by Per Bothner <bothner@cygnus.com>, February 1996.
   Seriously hacked by Chris Toshok <toshok@hungry.com>
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "zipfile.h"
#include "zlib.h"
#include "compat.h"

#ifdef HAVE_SYS_MMAN_H
/* disable mmap for now */
#undef HAVE_SYS_MMAN_H
#endif

#include "plstr.h"

extern PRLogModuleInfo* inflateLm;

/* This stuff is partly based on the 28 August 1994 public release of the
Info-ZIP group's portable UnZip zipfile-extraction program (and related
utilities). */

/*************/
/*  Defines  */
/*************/

#define UNZIP
#define UNZIP_VERSION     20   /* compatible with PKUNZIP 2.0 */
#define VMS_UNZIP_VERSION 42   /* if OS-needed-to-extract is VMS:  can do */


#define ZSUFX             ".zip"
#define CENTRAL_HDR_SIG   "\113\001\002"   /* the infamous "PK" signature */
#define LOCAL_HDR_SIG     "\113\003\004"   /*  bytes, sans "P" (so unzip */
#define END_CENTRAL_SIG   "\113\005\006"   /*  executable not mistaken for */
#define EXTD_LOCAL_SIG    "\113\007\010"   /*  zipfile itself) */

#define STORED            0    /* compression methods */
#define SHRUNK            1
#define REDUCED1          2
#define REDUCED2          3
#define REDUCED3          4
#define REDUCED4          5
#define IMPLODED          6
#define TOKENIZED         7
#define DEFLATED          8
#define NUM_METHODS       9    /* index of last method + 1 */
/* don't forget to update list_files() appropriately if NUM_METHODS changes */

#define PK_OK             0    /* no error */
#define PK_COOL           0    /* no error */
#define PK_GNARLY         0    /* no error */
#define PK_WARN           1    /* warning error */
#define PK_ERR            2    /* error in zipfile */
#define PK_BADERR         3    /* severe error in zipfile */
#define PK_MEM            4    /* insufficient memory */
#define PK_MEM2           5    /* insufficient memory */
#define PK_MEM3           6    /* insufficient memory */
#define PK_MEM4           7    /* insufficient memory */
#define PK_MEM5           8    /* insufficient memory */
#define PK_NOZIP          9    /* zipfile not found */
#define PK_PARAM          10   /* bad or illegal parameters specified */
#define PK_FIND           11   /* no files found */
#define PK_DISK           50   /* disk full */
#define PK_EOF            51   /* unexpected EOF */

/*---------------------------------------------------------------------------
    True sizes of the various headers, as defined by PKWARE--so it is not
    likely that these will ever change.  But if they do, make sure both these
    defines AND the typedefs below get updated accordingly.
  ---------------------------------------------------------------------------*/
#define LREC_SIZE     26    /* lengths of local file headers, central */
#define CREC_SIZE     42    /*  directory headers, and the end-of-    */
#define ECREC_SIZE    18    /*  central-dir record, respectively      */


#ifndef SEEK_SET
#  define SEEK_SET  0
#endif
#ifndef SEEK_CUR
#  define SEEK_CUR  1
#endif
#ifndef SEEK_END
#  define SEEK_END  2
#endif

/**************/
/*  Typedefs  */
/**************/

typedef char              boolean;
typedef unsigned char     uch;  /* code assumes unsigned bytes; these type-  */
typedef unsigned short    ush;  /*  defs replace byte/UWORD/ULONG (which are */
typedef unsigned int      ulg;  /*  predefined on some systems) & match zip  */

/*---------------------------------------------------------------------------
    Zipfile layout declarations.  If these headers ever change, make sure the
    xxREC_SIZE defines (above) change with them!
  ---------------------------------------------------------------------------*/

typedef uch   local_byte_hdr[ LREC_SIZE ];
#      define L_VERSION_NEEDED_TO_EXTRACT_0     0
#      define L_VERSION_NEEDED_TO_EXTRACT_1     1
#      define L_GENERAL_PURPOSE_BIT_FLAG        2
#      define L_COMPRESSION_METHOD              4
#      define L_LAST_MOD_FILE_TIME              6
#      define L_LAST_MOD_FILE_DATE              8
#      define L_CRC32                           10
#      define L_COMPRESSED_SIZE                 14
#      define L_UNCOMPRESSED_SIZE               18
#      define L_FILENAME_LENGTH                 22
#      define L_EXTRA_FIELD_LENGTH              24

typedef uch   cdir_byte_hdr[ CREC_SIZE ];
#      define C_VERSION_MADE_BY_0               0
#      define C_VERSION_MADE_BY_1               1
#      define C_VERSION_NEEDED_TO_EXTRACT_0     2
#      define C_VERSION_NEEDED_TO_EXTRACT_1     3
#      define C_GENERAL_PURPOSE_BIT_FLAG        4
#      define C_COMPRESSION_METHOD              6
#      define C_LAST_MOD_FILE_TIME              8
#      define C_LAST_MOD_FILE_DATE              10
#      define C_CRC32                           12
#      define C_COMPRESSED_SIZE                 16
#      define C_UNCOMPRESSED_SIZE               20
#      define C_FILENAME_LENGTH                 24
#      define C_EXTRA_FIELD_LENGTH              26
#      define C_FILE_COMMENT_LENGTH             28
#      define C_DISK_NUMBER_START               30
#      define C_INTERNAL_FILE_ATTRIBUTES        32
#      define C_EXTERNAL_FILE_ATTRIBUTES        34
#      define C_RELATIVE_OFFSET_LOCAL_HEADER    38

typedef uch   ec_byte_rec[ ECREC_SIZE+4 ];
/*     define SIGNATURE                         0   space-holder only */
#      define NUMBER_THIS_DISK                  4
#      define NUM_DISK_WITH_START_CENTRAL_DIR   6
#      define NUM_ENTRIES_CENTRL_DIR_THS_DISK   8
#      define TOTAL_ENTRIES_CENTRAL_DIR         10
#      define SIZE_CENTRAL_DIRECTORY            12
#      define OFFSET_START_CENTRAL_DIRECTORY    16
#      define ZIPFILE_COMMENT_LENGTH            20


typedef struct local_file_header {                 /* LOCAL */
  uch version_needed_to_extract[2];
  ush general_purpose_bit_flag;
  ush compression_method;
  ush last_mod_file_time;
  ush last_mod_file_date;
  ulg crc32;
  ulg csize;
  ulg ucsize;
  ush filename_length;
  ush extra_field_length;
} local_file_hdr;

typedef struct central_directory_file_header {     /* CENTRAL */
  uch version_made_by[2];
  uch version_needed_to_extract[2];
  ush general_purpose_bit_flag;
  ush compression_method;
  ush last_mod_file_time;
  ush last_mod_file_date;
  ulg crc32;
  ulg csize;
  ulg ucsize;
  ush filename_length;
  ush extra_field_length;
  ush file_comment_length;
  ush disk_number_start;
  ush internal_file_attributes;
  ulg external_file_attributes;
  ulg relative_offset_local_header;
} cdir_file_hdr;

typedef struct end_central_dir_record {            /* END CENTRAL */
  ush number_this_disk;
  ush num_disk_with_start_central_dir;
  ush num_entries_centrl_dir_ths_disk;
  ush total_entries_central_dir;
  ulg size_central_directory;
  ulg offset_start_central_directory;
  ush zipfile_comment_length;
} ecdir_rec;


/************/
/*  Macros  */
/************/

#ifndef MAX
#  define MAX(a,b)   ((a) > (b) ? (a) : (b))
#endif
#ifndef MIN
#  define MIN(a,b)   ((a) < (b) ? (a) : (b))
#endif


/***********************/
/* Function makeword() */
/***********************/

static inline ush makeword(uch* b)
{
  /*
   * Convert Intel style 'short' integer to non-Intel non-16-bit
   * host format.  This routine also takes care of byte-ordering.
     */
  return (ush)((b[1] << 8) | b[0]);
}


/***********************/
/* Function makelong() */
/***********************/

static inline ulg makelong(uch* sig)
{
  /*
   * Convert intel style 'long' variable to non-Intel non-16-bit
   * host format.  This routine also takes care of byte-ordering.
     */
  return (((ulg)sig[3]) << 24)
    + (((ulg)sig[2]) << 16)
    + (((ulg)sig[1]) << 8)
    + ((ulg)sig[0]);
}

static int
zip_dir_compare(const void *p1, const void *p2)
{
  ZipDirectory *z1 = *(ZipDirectory**)p1;
  ZipDirectory *z2 = *(ZipDirectory**)p2;

  return PL_strcmp(ZIPDIR_FILENAME(z1), ZIPDIR_FILENAME(z2));
}

int
read_zip_archive (zipf)
     register ZipFile *zipf;
{
  int i;
  int dir_last_pad;
  char *dir_ptr;
#if defined(HAVE_SYS_MMAN_H) && defined(HAVE_UNALIGNEDACCESS)
  char *buffer;
#else
  char buffer[100];
#endif

  zipf->size = PR_Seek (zipf->fd, 0L, SEEK_END);
  if (zipf->size < (ECREC_SIZE+4))
    return -1;

#if defined(HAVE_SYS_MMAN_H) && defined(HAVE_UNALIGNEDACCESS)
  /* Map R/W for now so's we can muck with the directory down below.  */
  zipf->mmap_base = mmap(0, zipf->size, PROT_READ|PROT_WRITE,
                         MAP_PRIVATE, zipf->fd, 0);
  if (zipf->mmap_base == (char *)-1)
    return -1;
  buffer = zipf->mmap_base + zipf->size - (ECREC_SIZE+4);
#else
  if (PR_Seek (zipf->fd, -(ECREC_SIZE+4), SEEK_CUR) <= 0)
    return -1;
  if (PR_Read (zipf->fd, buffer, ECREC_SIZE+4) != ECREC_SIZE+4)
    return -2;
#endif

  zipf->count = makeword(&buffer[TOTAL_ENTRIES_CENTRAL_DIR]);
  zipf->dir_size = makelong(&buffer[SIZE_CENTRAL_DIRECTORY]);

#if defined(HAVE_SYS_MMAN_H) && defined(HAVE_UNALIGNEDACCESS)
  /* XXX Is this the same as the PR_MALLOC down below? [pere] */
  zipf->central_directory = (zipf->mmap_base + zipf->size -
                             (zipf->dir_size + ECREC_SIZE + 4));
#else
  /* Allocate 1 more to allow appending '\0' to last filename. */
  zipf->central_directory = PR_MALLOC (zipf->dir_size+1);

  if (PR_Seek (zipf->fd, -(zipf->dir_size+ECREC_SIZE+4), SEEK_CUR) < 0)
    return -2;
  if (PR_Read (zipf->fd, zipf->central_directory, zipf->dir_size) < 0)
    return -2;
#endif

#ifdef TEST
  printf ("number_this_disk = %d\n", makeword(&buffer[NUMBER_THIS_DISK]));
  printf ("num_disk_with_start_central_dir = %d\n",
          makeword(&buffer[NUM_DISK_WITH_START_CENTRAL_DIR]));
  printf ("num_entries_centrl_dir_ths_disk = %d\n",
          makeword(&buffer[NUM_ENTRIES_CENTRL_DIR_THS_DISK]));
  printf ("total_entries_central_dir = %d\n",
          makeword(&buffer[TOTAL_ENTRIES_CENTRAL_DIR]));
  printf ("size_central_directory = %d\n",
          makelong(&buffer[SIZE_CENTRAL_DIRECTORY]));
  printf ("offset_start_central_directory = %d\n",
          makelong(&buffer[OFFSET_START_CENTRAL_DIRECTORY]));
  printf ("zipfile_comment_length = %d\n",
          makeword(&buffer[ZIPFILE_COMMENT_LENGTH]));
#endif

  dir_last_pad = 0;
  dir_ptr = zipf->central_directory;
  zipf->sorted_entries = (ZipDirectory**)PR_MALLOC(zipf->count * sizeof(ZipDirectory*));
  for (i = 0; i < zipf->count; i++)
    {
      ZipDirectory *zipd = (ZipDirectory*)(dir_ptr + dir_last_pad);
      int compression_method = makeword (&dir_ptr[4+C_COMPRESSION_METHOD]);
      long compressed_size = makelong (&dir_ptr[4+C_COMPRESSED_SIZE]);
      long uncompressed_size = makelong (&dir_ptr[4+C_UNCOMPRESSED_SIZE]);
      long filename_length = makeword (&dir_ptr[4+C_FILENAME_LENGTH]);
      long extra_field_length = makeword (&dir_ptr[4+C_EXTRA_FIELD_LENGTH]);
      long file_comment_length = makeword (&dir_ptr[4+C_FILE_COMMENT_LENGTH]);
      long record_length;
      int unpadded_direntry_length;
      int zip_version = (unsigned char)dir_ptr[4+C_VERSION_MADE_BY_0];
      
      record_length = filename_length+extra_field_length+file_comment_length;

      if ((dir_ptr-zipf->central_directory)+record_length+CREC_SIZE+4
          > zipf->dir_size)
        return -1;

      zipf->sorted_entries[i] = zipd;

      zipd->filename_length = filename_length;
      zipd->compression_method = compression_method;
      zipd->uncompressed_size = uncompressed_size;
      zipd->compressed_size = compressed_size;
      zipd->zip_version = zip_version;
      /*
        #if defined(__GNUC__)
        #define DIR_ALIGN __alignof__(ZipDirectory)
        #else*/
#define DIR_ALIGN sizeof(long)
      /*
        #endif
      */
      zipd->filestart = makelong (&dir_ptr[4+C_RELATIVE_OFFSET_LOCAL_HEADER])
        + (LREC_SIZE+4) + record_length;
      /* This is definately a hack - but it fixes things for now */
      if (record_length != filename_length) {
        zipd->filestart += 4;
      }
      zipd->filename_offset = CREC_SIZE+4 - dir_last_pad;
      unpadded_direntry_length = zipd->filename_offset + record_length;
      zipd->direntry_size =
        ((unpadded_direntry_length + DIR_ALIGN) / DIR_ALIGN) * DIR_ALIGN;
      dir_last_pad = zipd->direntry_size - unpadded_direntry_length;

      dir_ptr[4+CREC_SIZE+filename_length] = 0;
      dir_ptr = (char*)zipd + unpadded_direntry_length;
    }

#ifdef HAVE_MERGESORT
  mergesort(zipf->sorted_entries, zipf->count, sizeof(ZipDirectory*),
            zip_dir_compare);
#elif defined(HAVE_QSORT)
  qsort(zipf->sorted_entries, zipf->count, sizeof(ZipDirectory*),
        zip_dir_compare);
#else
#error um... i need to sort stuff here.
#endif

#if defined(HAVE_SYS_MMAN_H) && defined(HAVE_UNALIGNEDACCESS)
  /* Now that we're done mucking, set the whole thing back read-only.  */
  i = zipf->size;
#if defined(HAVE_GETPAGESIZE)
  /* Round size up to page size - this does no harm on any system and
   * avoids a bug on BSDIs.
   */
  i = (i + getpagesize() - 1) & -getpagesize();
#endif
  mprotect(zipf->mmap_base, i, PROT_READ);
#endif

  return 0;
}

#define WSIZE 0x8000
/* this next function blatantly taken and munged into a much smaller,
   more specialized form from the unzip source by toshok@hungry.com,
   Mar 3, 1998.  Here's the message from the top of the unzip source
   file: */

/* inflate.c -- put in the public domain by Mark Adler
   version c15c, 28 March 1997 */
static int
inflate_entry(PRUint8 *output_buffer,
              PRUint32 output_length,
              ZipFile *zipf,
              ZipDirectory *zipd)
{
  int input_length = zipd->compressed_size;
  char *input_buffer = PR_MALLOC(input_length);
  int i, windowBits, err=Z_OK;
  int wsize = WSIZE;
  z_stream zstrm;

  PR_ASSERT(NULL != input_buffer); /* XXX Should probably throw exception instead */

  PR_Seek(zipf->fd, zipd->filestart, SEEK_SET);
  PR_Read(zipf->fd, input_buffer, input_length);

  /* GRR:  "U" may not be compatible with K&R compilers */
  if (wsize < 256U)   /* window sizes 2^8 .. 2^15 allowed currently */
    return 2;
  
  /* windowBits = log2(wsize) */
  for (i = wsize, windowBits = 0;  !(i & 1);  i >>= 1, ++windowBits);
  
  zstrm.next_out = output_buffer;
  zstrm.avail_out = output_length; /*wsize;*/
  
  zstrm.next_in = input_buffer;
  zstrm.avail_in = input_length;
  
  zstrm.zalloc = (alloc_func)Z_NULL;
  zstrm.zfree = (free_func)Z_NULL;
  
  PR_LOG(inflateLm, PR_LOG_DEBUG, ("initializing inflate()\n"));
  err = inflateInit2(&zstrm, -windowBits);
  
  if (err == Z_MEM_ERROR)
    return 3;
  else if (err != Z_OK)
    PR_LOG(inflateLm, PR_LOG_DEBUG, ("oops!  (inflateInit2() err = %d)\n",
                                     err));
  
  while (err != Z_STREAM_END) {
    while (zstrm.avail_out > 0) {
      err = inflate(&zstrm, Z_PARTIAL_FLUSH);
      
      if (err == Z_DATA_ERROR)
        return 2;
      else if (err == Z_MEM_ERROR)
        return 3;
      else if (err != Z_OK && err != Z_STREAM_END)
        PR_LOG(inflateLm, PR_LOG_DEBUG,
               ("oops!  (inflate(first loop) err = %d)\n", err));
      
      if (err == Z_STREAM_END)          /* "END-of-entry-condition" ? */
        break;
      
      PR_LOG(inflateLm, PR_LOG_DEBUG, ("     avail_in = %d\n",
                                       zstrm.avail_in));
    }
  }
  
  /* no more input, so loop until we have all output */
  PR_LOG(inflateLm, PR_LOG_DEBUG, ("beginning final loop:  err = %d\n", err));
  while (err != Z_STREAM_END) {
    err = inflate(&zstrm, Z_PARTIAL_FLUSH);
    if (err == Z_DATA_ERROR)
      return 2;
    else if (err == Z_MEM_ERROR)
      return 3;
    else if (err == Z_BUF_ERROR) {              /* DEBUG */
      PR_LOG(inflateLm, PR_LOG_DEBUG,
             ("zlib inflate() did not detect stream end\n"));
      break;
    } else if (err != Z_OK && err != Z_STREAM_END) {
      PR_LOG(inflateLm, PR_LOG_DEBUG,
             ("oops!  (inflate(final loop) err = %d)\n", err));
      return 3;
    }

    PR_LOG(inflateLm, PR_LOG_DEBUG, ("final loop:  flushing %ld bytes\n",
                                     (long)(wsize - zstrm.avail_out)));
  }
  PR_LOG(inflateLm, PR_LOG_DEBUG, ("total in = %ld, total out = %ld\n",
                                   zstrm.total_in, zstrm.total_out));
  
  err = inflateReset(&zstrm);
  if (err != Z_OK)
    PR_LOG(inflateLm, PR_LOG_DEBUG, ("oops!  (inflateReset() err = %d)\n",
                                     err));

  PR_DELETE(input_buffer);

  return Z_OK;
}

void
free_zipfile_entry(ZipDirectory *zipd,
                   PRUint8 *buf,
                   PRUint32 length,
                   PRBool malloced)
{
  if (malloced) 
    {
      PR_DELETE(buf);
    }
#if defined(HAVE_SYS_MMAN_H)
  else
    {
      /* XXX These +4 must also change like the one in get_zipfile_entry */
      int offset;
      int difference;
      
      offset = zipd->filestart + 4 - ((zipd->filestart + 4) % 4096);
      
      difference = zipd->filestart + 4 - offset;
      
      munmap(buf - difference,
             zipd->uncompressed_size + difference);
    }
#endif
}

int
get_zipfile_entry(ZipFile *zipf,
                  ZipDirectory *zipd,
                  PRUint8 **buf,
                  PRUint32 *length,
                  PRBool *malloced)
{
  int strange_skip_value = 0;
  *malloced = PR_FALSE;
  *length = zipd->uncompressed_size;
  PR_LOG(inflateLm, PR_LOG_DEBUG, ("Zip entry v=0x%x\n", zipd->zip_version));

  /*
   * XXX This is an ugly hack,  I don't know what I am doing, nor do I know
   * XXX the .zip file format.  No idea if the version and the +4 is
   * XXX related, but it was on my system. :-) [pere 1998-05-14]
   */
  if (0x15 < zipd->zip_version)
    strange_skip_value = 4;

  if (zipd->compression_method == STORED)
    {
      /* the easy one. */
#if defined(HAVE_SYS_MMAN_H)
      int offset;
      int difference;

      offset = zipd->filestart + strange_skip_value
        - ((zipd->filestart + strange_skip_value) % 4096);

      difference = zipd->filestart + strange_skip_value - offset;

      *buf = mmap(0, *length + difference, PROT_READ,
                  MAP_PRIVATE, zipf->fd,
                  offset);
      if (*buf != MAP_FAILED)
        {
          *buf += difference;

          return Z_OK;
        }

      perror("mmap");

      /* fall through to the PR_MALLOC case. */
#endif
      *malloced = PR_TRUE;
      *buf = (PRUint8*)PR_MALLOC(*length);
      PR_ASSERT(NULL != *buf);
      PR_Seek(zipf->fd, zipd->filestart + strange_skip_value, SEEK_SET);
      PR_Read(zipf->fd, *buf, *length);
      return Z_OK;
    }
  else if (zipd->compression_method == DEFLATED)
    {
      *malloced = PR_TRUE;
      *buf = (PRUint8*)PR_MALLOC(*length);
      PR_ASSERT(NULL != *buf);

      return (inflate_entry(*buf, *length, zipf, zipd) == Z_OK ? 0 : -1);
    }
  else
    {
#ifdef DEBUG
      fprintf (stderr, "unhandled compression type %d, skipping zip entry\n", zipd->compression_method);
#endif
      return -1;
    }
}
