/* $Id: archive.c,v 1.11 2002/06/23 20:28:31 richdawe Exp $ */

/*
 *  archive.c - Archive handling functions for pakke
 *  Copyright (C) 1999-2002 by Richard Dawe
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "common.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <time.h>
#include <utime.h>

/* libpakke includes */
#include <libpakke/archive.h>
#include <libpakke/util.h>

#include "unzip.h"

#ifdef HAVE_LIBBZ2
#include <bzlib.h>
#endif

/* Remember the last opened zip file, so we don't have to keep opening it. */
static unzFile zipfile           = NULL;
static char    zipname[PATH_MAX] = "";
static int     zip_opened        = 0;

/* ZIP file handling */
static int    zip_extract_file  (const char *archive, const char *file, 
                                 const char *dest);
static char **zip_get_toc       (const char *archive);
static int    zip_get_file_info (const char *archive, const char *file,
                                 ARCHIVE_FILE_INFO *afi);

static int    zip_extract_file_to_memory (const char *archive,
					  const char *file,
					  char *buf, unsigned long buflen);

static int    zip_check_integrity (const char *archive);

/* gzip'd tar file handling */
/* TODO: tar-gzip */

/* bzip2'd tar file handling */
/* TODO: tar-bzip2 */

/* Use a 256KB buffer size */
static const size_t ZIP_BUFFER_SIZE = 256 * 1024;

/* TODO: Error codes? */

/* ------------
 * - zip_open -
 * ------------ */

/* This opens the zip archive with "memory" between invocations of the
 * archive_*() functions. */

int __inline__
zip_open (const char *archive)
{
  if (!zip_opened) {
    zipfile = unzOpen(archive);
    if (zipfile == NULL) return(0);
    strcpy(zipname, archive);
    zip_opened = 1;
  } else {
    if (strcmp(archive, zipname) != 0) {
      unzClose(zipfile);
      zip_opened = 0;

      zipfile = unzOpen(archive);
      if (zipfile == NULL) return(0);
      strcpy(zipname, archive);
      zip_opened = 1;
    }
  }

  return(1);
}

/* -------------------
 * - archive_get_toc -
 * ------------------- */

/*
 * Retrieve the table-of-contents (TOC) of the archive, i.e. a list of all
 * files (including directories) inside the archive. This list must be freed
 * by the caller.
 *
 * Returns a NULL-terminated list on success, otherwise NULL on failure.
 */

char **
archive_get_toc (const char *archive)
{
  if (!iszip(archive))
    return(NULL);
  else
    return(zip_get_toc(archive));
}

/* ---------------
 * - zip_get_toc -
 * --------------- */

static char **
zip_get_toc (const char *archive)
{
  char **toc = NULL;
  unz_global_info gi;
  char currentfile[PATH_MAX];
  int ret, i, j;

  if (!zip_open(archive)) return(NULL);

  /* Get the global info */
  if (unzGetGlobalInfo(zipfile, &gi) != UNZ_OK) return(NULL);

  /* Allocate a TOC list */
  toc = malloc(sizeof(char *) * (gi.number_entry + 1));
  if (toc == NULL) return(NULL);
  for (i = 0; i <= gi.number_entry; i++) { toc[i] = NULL; }

  for (i = 0, ret = unzGoToFirstFile(zipfile);
       ret == UNZ_OK;
       i++, ret = unzGoToNextFile(zipfile)) {
    /* Get the file details */
    ret = unzGetCurrentFileInfo(zipfile, NULL,
				currentfile, sizeof(currentfile),
				NULL, 0, NULL, 0);

    if (ret != UNZ_OK) {
      for (j = 0; toc[j] != NULL; j++) {
	free(toc[j]);
	toc[j] = NULL;
      }
      free(toc);
      return(NULL);
    }

    toc[i] = strdup(currentfile);
  }

  return(toc);
}

/* ------------------------
 * - archive_extract_file -
 * ------------------------ */

/* Extract from the file 'file' from the archive file 'archive' into the
 * destination file 'dest'. This routine figures out which archive format
 * 'archive' is by looking at the file extension. Directories should be
 * created using mkdir(). */

/* TODO: Symlinks? Or not supported? */

int
archive_extract_file (const char *archive, const char *file, const char *dest)
{
  ARCHIVE_FILE_INFO afi;  
  time_t            t;
  struct utimbuf    ut;
  int               ret;

  /* We currently only support .zip files. */
  if (!iszip(archive))
    return(0);
  else
    ret = zip_extract_file(archive, file, dest);

  if (!ret)
    return(0);

  /* Set the file attributes. */
  /* TODO: Set permissions, etc. - see InfoZIP appnote for description
   * of ZIP internal/external permissions. */
  if (!archive_get_file_info(archive, file, &afi))
    return(0);

  t = mktime(&afi.modtime);

  if (t != (time_t) -1) {
    /* If mktime() failed, do not set the timestamp. This will result in
     * the file timestamp being way, way into the future. */
    /* TODO: Return an error if mktime() fails, so we can generate
     * a warning about being unable to set the timestamp. */
    ut.actime = ut.modtime = t;

    if (utime(dest, &ut))
      return(0);
  }

  return(1);
}

/* --------------------
 * - zip_extract_file -
 * -------------------- */

static int
zip_extract_file (const char *archive, const char *file, const char *dest)
{  
  FILE   *fp = NULL;
  char   *buf = NULL;
  size_t  buflen = ZIP_BUFFER_SIZE;
  int     nread;
  int     ret = 1; /* Succeed by default */

  /* Open the archive */
  if (!zip_open(archive))
    return(0);

  /* Find the requested file - 0 = default case sensitivity */
  if (unzLocateFile(zipfile, file, 0) != UNZ_OK)
    return(0);

  /* Allocate file buffer */
  buf = malloc(buflen);
  if (buf == NULL)
    return(0);

  /* Now extract */
  fp = fopen(dest, "wb");
  if (fp == NULL)
    return(0);

  if (unzOpenCurrentFile(zipfile) != UNZ_OK) {
    fclose(fp);
    return(0);
  }

  while ( (nread = unzReadCurrentFile(zipfile, buf, buflen)) > 0) {
    if (fwrite(buf, nread, 1, fp) <= 0) {
      ret = 0;
      break;
    }
  }
	
  fclose(fp);

  /* Free buffer */
  free(buf), buf = NULL;

  /* TODO: Better error handling */
  if (unzCloseCurrentFile(zipfile) != UNZ_OK)
    ret = 0;

  return(ret);
}

/* -------------------------
 * - archive_get_file_info -
 * ------------------------- */

int
archive_get_file_info (const char *archive, const char *file,  
		       ARCHIVE_FILE_INFO *afi)
{
  if (!iszip(archive))
    return(0);
  else
    return(zip_get_file_info(archive, file, afi));
}

/* ---------------------
 * - zip_get_file_info -
 * --------------------- */

static
int zip_get_file_info (const char *archive, const char *file,
		       ARCHIVE_FILE_INFO *afi)
{
  unz_file_info fi;
  char currentfile[PATH_MAX];
  int len;

  if (!zip_open(archive)) return(0);

  /* Find the requested file - 0 = default case sensitivity */
  if (unzLocateFile(zipfile, file, 0) != UNZ_OK) return(0);

  if (unzGetCurrentFileInfo(zipfile, &fi,
			    currentfile, sizeof(currentfile),
			    NULL, 0, NULL, 0) != UNZ_OK)
    return(0);

  /* Store the details */
  len = strlen(currentfile) + 1;
  if (sizeof(afi->name) < len) len = sizeof(afi->name);
  strncpy(afi->name, currentfile, sizeof(afi->name));
  afi->name[len - 1] = '\0';

  afi->size = fi.uncompressed_size;

  memset(&afi->modtime, 0, sizeof(afi->modtime));
  afi->modtime.tm_sec   = fi.tmu_date.tm_sec;
  afi->modtime.tm_min   = fi.tmu_date.tm_min;
  afi->modtime.tm_hour  = fi.tmu_date.tm_hour;
  afi->modtime.tm_mday  = fi.tmu_date.tm_mday;
  afi->modtime.tm_mon   = fi.tmu_date.tm_mon;
  /* Count years since 1900 */
  afi->modtime.tm_year  = fi.tmu_date.tm_year - 1900;

  return(1);
}

/* ----------------------------------
 * - archive_extract_file_to_memory -
 * ---------------------------------- */

/* This function extracts the given file into a buffer created for it. The
 * caller should free this buffer. */

static char *
__extract_to_memory (const char *archive, const char *file, const int textmode)
{
  ARCHIVE_FILE_INFO afi;
  char          *buf    = NULL;
  unsigned long  buflen = 0;

  /* Create a big enough buffer. */
  if (!archive_get_file_info(archive, file, &afi)) return(NULL);

  buflen = afi.size;
  if (!textmode)
    buf = malloc(buflen);
  else
    buf = malloc(buflen + 1); /* Space for nul */

  if (buf == NULL) return(NULL);

  /* TODO: Other formats */
  if (!zip_extract_file_to_memory(archive, file, buf, buflen)) {
    free(buf);
    return(NULL);
  }

  if (textmode) buf[buflen] = '\0';

  return(buf);
}

char *
archive_extract_file_to_memory (const char *archive, const char *file)
{
  return(__extract_to_memory(archive, file, 0));
}

char *
archive_extract_text_file_to_memory (const char *archive, const char *file)
{
  return(__extract_to_memory(archive, file, 1));
}

/* ------------------------------
 * - zip_extract_file_to_memory -
 * ------------------------------ */

static int
zip_extract_file_to_memory (const char *archive, const char *file,
			    char *buf, const unsigned long buflen)
{
  if (!zip_open(archive))
    return(0);

  /* Find the requested file - 0 = default case sensitivity */
  if (unzLocateFile(zipfile, file, 0) != UNZ_OK)
    return(0);

  /* Now read it into 'buf' */
  if (unzOpenCurrentFile(zipfile) != UNZ_OK)
    return(0);

  /* TODO: Can I assume that this will read all data? */
  if (unzReadCurrentFile(zipfile, buf, buflen) != buflen)
    return(0);

  /* TODO: Better error handling */
  if (unzCloseCurrentFile(zipfile) != UNZ_OK)
    return(0);

  return(1);
}

/* ---------------------------
 * - archive_check_integrity -
 * --------------------------- */

/* Check the integrity of 'archive'. If its integrity is intact, return 1,
 * otherwise return 0. */

int
archive_check_integrity (const char *archive)
{
  if (!iszip(archive))
    return(0);
  else
    return(zip_check_integrity(archive));
}

/* -----------------------
 * - zip_check_integrity -
 * ----------------------- */

static int
zip_check_integrity (const char *archive)
{
  char   *buf       = NULL; /* Buffer for reading in files */
  size_t  buflen    = ZIP_BUFFER_SIZE;
  int     ret       = UNZ_OK;
  int     crc_error = 0;
  size_t  nread     = 0;
  int     i;

  /* Open the archive */
  if (!zip_open(archive))
    return(0);

  /* Allocate file buffer */
  buf = malloc(buflen);
  if (buf == NULL)
    return(0);

  /* Enumerate all files */
  for (i = 0, ret = unzGoToFirstFile(zipfile);
       ret == UNZ_OK;
       i++, ret = unzGoToNextFile(zipfile)) {
    /* Open and read entire file */
    if (unzOpenCurrentFile(zipfile) != UNZ_OK) {
      /* TODO: Better error */
      crc_error = 1;
      continue;
    }

    for ( ; (nread = unzReadCurrentFile(zipfile, buf, buflen)) > 0; ) {;}

    /* Close file; check for CRC error */
    if (unzCloseCurrentFile(zipfile) != UNZ_OK) {
      crc_error = 1;
      continue;
    }
  }

  /* Free buffer */
  free(buf), buf = NULL;

  /* Done */
  if (!crc_error)
    return(1);

  return(0);
}
