/* $Id: mft.c,v 1.13 2002/02/13 21:14:01 richdawe Exp $ */

/*
 *  mft.c - .mft & .ver file parsing functions
 *  Copyright (C) 1999-2001 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 <ctype.h>
#include <limits.h>
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>

/* libzippo includes */
#include <libzippo/mft.h>
#include <libzippo/package.h>
#include <libzippo/packlist.h>
#include <libzippo/util.h>
#include <libzippo/archive.h>

#include "fnsplit.h"
#include "strlwr.h"
#include "unzip.h"

/* ------------------------
 * - ver_parse_version_l0 -
 * ------------------------ */

/* This function counts the leading zeros, given a version number string. E.g.
   for "02", it will return 1. This is to handle version numbers such as
   2.02 correctly. */

int
ver_parse_version_l0 (const char *v)
{
  int i;
	
  /* Simple cases */
  if (v == NULL) return(0);
  if (v[0] != '0') return(0);
  if (!isdigit(v[1])) return(0);

  /* All others */
  for (i = 0; i < strlen(v); i++) {
    if (v[i] != '0') break;
  }

  return(i);
}

/* ---------------------
 * - ver_parse_version -
 * --------------------- */

int
ver_parse_version (const char *str, PACKAGE_VERSION *ver)
{
  char *buf = strdup(str);
  char *p   = buf;
  char *q   = NULL;
  int i;

  if (buf == NULL) return(MFT_INTERNAL_ERROR);
  memset(ver, 0, sizeof(*ver));
  strlwr(buf);

  /* Find the start of the version number */
  p = strchr(buf, '.');
  if ((p != NULL) && (p != buf)) {
    p--;
    while (isdigit(*p)) p--;
    p++;
  }		

  if (p == NULL) {
    /* Look for a version number enclosed by whitespace */
    for (p = buf + 1; *p != '\0'; p++) {
      if (!isdigit(*p)) continue;

      /* Space, bracket or 'v' before? */
      if (   !isspace(*(p - 1))
	  && (*(p - 1) != '(')
          && (*(p - 1) != '[')
          && (*(p - 1) != '{')
          && (*(p - 1) != 'v')) continue;

      /* Scan the body */
      q = p;
      while (isdigit(*q) || (*q == '.')) q++;

      /* Space or bracket after? */
      if (   !isspace(*q)
	  && (*q != ')')
          && (*q != ']')
          && (*q != '}')) {
	p = q + 1;
	continue;
      } else {
	/* Found it */
	break;
      }
    }

    if (*p == '\0') p = NULL;
  }

  /* No version found */
  if (p == NULL) {
    free(buf);
    return(MFT_BAD_VERSION_FORMAT);
  }

  /* Parse the period-separated version numbers */
  for (i = 0, p = strtok(p, ". "); ; i++, p = strtok(NULL, ". ")) {
    if (p == NULL) break;
    if (!isdigit(*p)) break;

    switch(i) {
    case 0:
      ver->has_major = 1;
      ver->major     = atoi(p);
      break;

    case 1:
      ver->has_minor = 1;
      ver->minor     = atoi(p);
      ver->minor_l0  = ver_parse_version_l0(p);
      break;

    case 2:
      ver->has_subminor = 1;
      ver->subminor     = atoi(p);
      ver->subminor_l0  = ver_parse_version_l0(p);
      break;
    case 3:
      ver->has_subsubminor = 1;
      ver->subsubminor     = atoi(p);
      ver->subsubminor_l0  = ver_parse_version_l0(p);
      break;

    default:
      free(buf);
      return(MFT_BAD_VERSION_FORMAT);
    }
  }

  /* buf has been torn up by strtok(), so use str from here on. */
  /* TODO: What about case? */

  /* Alpha version */
  p = strstr(str, MFT_COMP_ALPHA);
  if (p != NULL) {
    for (p += strlen(MFT_COMP_ALPHA);
	 (*p != '\0') && isspace(*p);
	 p++) {
      ;
    }
    ver->has_alpha = 1;
    ver->alpha     = atoi(p);
  }

  /* Beta version */
  p = strstr(str, MFT_COMP_BETA);
  if (p != NULL) {
    for (p += strlen(MFT_COMP_BETA);
	 (*p != '\0') && isspace(*p);
	 p++) {
      ;
    }
    ver->has_beta = 1;
    ver->beta     = atoi(p);
  }

  /* Revision */
  p = strstr(str, MFT_COMP_REVISION);
  if (p != NULL) {
    for (p += strlen(MFT_COMP_REVISION);
	 (*p != '\0') && isspace(*p);
	 p++) {
      ;
    }
    ver->has_revision = 1;
    ver->revision     = atoi(p);
  }

  /* Patchlevel */
  p = strstr(str, MFT_COMP_PATCHLEVEL);
  if (p != NULL) {
    for (p += strlen(MFT_COMP_PATCHLEVEL);
	 (*p != '\0') && isspace(*p);
	 p++) {
      ;
    }
    ver->has_patchlevel = 1;
    ver->patchlevel     = atoi(p);
  }

  /* Release */
  p = strstr(str, MFT_COMP_RELEASE);
  if (p != NULL) {
    for (p += strlen(MFT_COMP_RELEASE);
	 (*p != '\0') && isspace(*p);
	 p++) {
      ;
    }
    ver->has_release = 1;
    ver->release     = atoi(p);
  }

  /* Any version info present? */
  if (   ver->has_major    || ver->has_minor
      || ver->has_minor    || ver->has_subminor || ver->has_subsubminor
      || ver->has_alpha    || ver->has_beta
      || ver->has_revision || ver->has_patchlevel || ver->has_release)
    ver->has_version = 1;

  /* OK */
  free(buf);
  return(MFT_OK);
}

/* -------------
 * - ver_parse -
 * ------------- */

int
ver_parse (const char *str, PACKAGE_INFO *package)
{
  char *buf = NULL;
  char pseudover[17]; /* Made-up version from manifest name */
  char *p = NULL;
  int ret = MFT_BAD;
  int i, j;
  size_t len;  

  /* File extensions to be removed */
  char *bad_ext[] = { ".zip", ".taz", ".tgz", ".tar.gz", NULL };

  /* Initialisation */
  buf = strdup(str);
  if (buf == NULL)
    return(MFT_INTERNAL_ERROR);

  memset(pseudover, 0, sizeof(pseudover));

  /* Find the start of the description */
  for (p = buf; (*p != '\0') && !isspace(*p); p++) {;}
  *p = '\0', p++;

  /* Store the package name */
  strncpy(package->name, buf, sizeof(package->name));
  package->name[sizeof(package->name) - 1] = '\0';

  /* Store the description */
  len = sizeof(package->short_description) - 1;
  if (strlen(p) < len) len = strlen(p);
  strncpy(package->short_description, p, len);
  package->short_description[len] = '\0';

  /* Try to find the version number from the description, else
   * try to get it from the package name. */
  ret = ver_parse_version(package->short_description, &package->version);

  if (ret != MFT_OK) {
    /* Last-ditch attempt to construct a version number
     * from manifest name. */
    /* TODO: This could guess the package type too. */
    memset(pseudover, 0, sizeof(pseudover));

    for (i = j = 0; i < 8; i++) {
      if (!isdigit(buf[i])) continue;

      /* abc -> a.b.c */
      pseudover[j]   = buf[i];
      pseudover[j+1] = '.';
      j += 2;
    }

    if (strlen(pseudover) != 0) {
      ret = ver_parse_version(pseudover, &package->version);

      /* Set fake version flag, so caller can warn, if necessary. */
      if (ret == MFT_OK)
	package->has_faked_version = 1;
    }
  }

  if (ret != MFT_OK)
    memset(&package->version, 0, sizeof(package->version));

  /* The package name should not contain '.zip', '.taz',
   * '.tgz' or '.tar.gz', so remove that if necessary. */
  for (i = 0; bad_ext[i] != NULL; i++) {
    p = strstr(package->name, bad_ext[i]);
    if (p == NULL) continue;
    *p = '\0';
  }

  /* Work out package type from last letter of its name, but only
   * if the letter is prefixed by a number. If no letter is
   * present, the type is unknown. */
  package->version.type = TYPE_NONE;

  if (strlen(package->name) > 0) {
    p = package->name + strlen(package->name) - 1;

    switch(tolower(*p)) {
    case 'b':
      package->version.type = TYPE_BINARIES;
      break;

    case 's':
      package->version.type = TYPE_SOURCES;
      break;

    case 'd':
      package->version.type = TYPE_DOCUMENTATION;
      break;
    }
  }

  /* If the package type is unknown, look for often-used strings like
   * "(binaries)" or binaries. */
  if (package->version.type == TYPE_NONE) {
    if (   (strstr(package->short_description, "(binaries)") != NULL)
	|| (strstr(package->short_description, "binaries") != NULL)) {
      package->version.type = TYPE_BINARIES;
    } else if (   (strstr(package->short_description, "(sources)") != NULL)
	       || (strstr(package->short_description, "sources") != NULL)) {
      package->version.type = TYPE_SOURCES;
    } else if (   (strstr(package->short_description,
			  "(documentation)") != NULL)
	       || (strstr(package->short_description,
			  "documentation") != NULL)) {
      package->version.type = TYPE_DOCUMENTATION;
    }
  }

  /* If no type found yet, default to binaries. */
  if (package->version.type == TYPE_NONE)
    package->version.type = TYPE_BINARIES;

  /* We always have a type. */
  package->version.has_type = 1;

  /* Tidy up */
  free(buf);

  return(MFT_OK);
}

/* ---------------------
 * - ver_get_and_parse -
 * --------------------- */

/* Parse a .ver file. This returns MFT_OK on success, else an MFT_* error
 * code. */

int
ver_get_and_parse (const char *filename, PACKAGE_INFO *package, char **ver)
{
  FILE *fp = NULL;
  char buf[1024];
  char f[MAXFILE];
  int ret = MFT_BAD;

  memset(package, 0, sizeof(*package));
  memset(buf, 0, sizeof(buf));  

  /* Set package defaults */
  package_set_defaults(package);

  fp = fopen(filename, "rt");
  if (fp == NULL)
    return(MFT_NONEXISTENT);

  /* ASSUMPTION: We can get the manifest name from the .ver filename.
   * For DJGPP packages they are supposed to be the same. This avoids
   * the problem with .ver files that don't contain the name of the
   * manifest/package as the first word in the .ver file. */
  strcpy(buf, filename);
  if ((fnsplit(buf, NULL, NULL, f, NULL) & FILENAME) == 0) {
    fclose(fp);
    return(MFT_BAD);
  }
		
  strncpy(package->manifest, f, sizeof(package->manifest));
  package->manifest[sizeof(package->manifest) - 1] = '\0';
	
  /* Only one line here */
  if (fgets(buf, sizeof(buf), fp) != NULL) {
    chomp(buf);
    rtrim(buf);

    ret = ver_parse(buf, package);
  }

  /* If the callee wants a copy of the file, return one. */
  if ((ret == MFT_OK) && (ver != NULL)) {
    *ver = strdup(buf);
    if (*ver == NULL)
      ret = MFT_INTERNAL_ERROR;
  }

  /* Tidy up */
  fclose(fp);

  return(ret);
}

/* ----------------
 * - ver_load_all -
 * ---------------- */

PACKAGE_INFO *
ver_load_all (const char **path, PACKAGE_INFO *packages)
{
  PACKAGE_INFO *list       = packages;
  PACKAGE_INFO *newpackage = NULL;
  char mypath[PATH_MAX], verfile[PATH_MAX];
  DIR *d = NULL;
  struct dirent *de = NULL;
  int i, ret;

  /* Check params */
  if (path == NULL) return(list); /* Nothing to do */

  /* Go through all the spec'd directories looking for '.ver's */
  for (i = 0; path[i] != NULL; i++) {
    /* Format the path name for later */
    strcpy(mypath, path[i]);
    addforwardslash(mypath);

    d = opendir(mypath);
    if (d == NULL) continue;

    while ( (de = readdir(d)) != NULL ) {
      /* Suitable file name? */
      if (strcmp(de->d_name, ".")    == 0) continue;
      if (strcmp(de->d_name, "..")   == 0) continue;
      if (strstr(de->d_name, ".ver") == NULL) continue;
      if (strstr(de->d_name, ".ver") == de->d_name) continue;

      /* Set up the new package info struct. */
      newpackage = (PACKAGE_INFO *)
	calloc(1, sizeof(*newpackage));
			
      if (newpackage == NULL) {
	warn("Unable to allocate memory!");
	continue;
      }
			
      /* Parse the package file */
      strcpy(verfile, mypath);
      strcat(verfile, de->d_name);			
      ret = ver_get_and_parse(verfile, newpackage, NULL);
			
      if (ret != MFT_OK) {
	warnf("Unable to parse '%s'", verfile);
	free(newpackage);
      } else {
	/* Add package to list. If the list doesn't
	 * exist, create it. */
	if (list == NULL) {
	  list = newpackage;
	} else {
	  packlist_add(list, newpackage);
	}
      }
    }
		
    closedir(d);
  }
	
  return(list);
}

/* ------------------------
 * - mft_get_from_archive -
 * ------------------------ */

char *
mft_get_from_archive (PACKAGE_INFO *package)
{
  char   filename[PATH_MAX];
  char  *mft = NULL;
  char **toc = NULL;
  char  *p   = NULL;
  int    i, found;
	
  /*
   * Try finding a DSM file in the archive & read it into a buffer.
   * Try to match the following file names:
   *
   * '<whatever>.mft'
   * 'manifest/<whatever>.mft'.
   */
		
  /* TODO: Multiple manifest file checks? */

  /* Get the table of contents. */
  toc = archive_get_toc(package->path);
  if (toc == NULL) return(NULL);
	
  /* Scan for manifests. */
  for (found = -1, i = 0; toc[i] != NULL; i++) {
    if (!has_extension(toc[i], "mft")) continue;

    /* Convert to forward slashes for the check. */
    strcpy(filename, toc[i]);
    forwardslashify(filename);

    /* Chop off the extension. */
    p = strrchr(filename, '.');
    if (p == NULL) continue;
    *p = '\0';

    /* Find manifest in root of archive. */
    if (strcasecmp(filename, package->manifest) == 0) {
      found = i;
      break;
    }

    /* Find manifest in manifest/ of archive. */
#define MFTDIR    "manifest/"				
#define MFTDIRLEN strlen(MFTDIR)
    if (   (strncasecmp(filename, MFTDIR, MFTDIRLEN) == 0)
        && (strchr(filename + MFTDIRLEN, '/') == NULL)
        && (strcasecmp(filename + MFTDIRLEN, package->manifest) == 0) ) {
      found = i;
      break;
    }			       
  }

  if (found >= 0) {
    mft = archive_extract_text_file_to_memory(package->path,
					      toc[i]);
  } else {
    mft = NULL;
  }
			
  /* Clean up the TOC */
  for (i = 0; toc[i] != NULL; i++) { free(toc[i]); }
  free(toc), toc = NULL;

  return(mft);
}

/* -----------
 * - mft_get -
 * ----------- */

char *
mft_get (const char **mft_path, PACKAGE_INFO *package)
{
  char   filename[PATH_MAX];
  char  *mft = NULL;
  char  *p   = NULL;

  if (!isarchive(package->path)) {
    /* If a path name is provided, use it. Otherwise, search
     * the manifest paths for the manifest file. */
    if (strlen(package->path) != 0) {
      strcpy(filename, package->path);
      addforwardslash(filename);
      strcat(filename, package->manifest);
      strcat(filename, ".mft");
   } else {
      /* Find the manifest file case insensitively. */
      strcpy(filename, package->manifest);
      strcat(filename, ".mft");
		
      p = find_in_paths(filename, mft_path, 0);
      if (p == NULL) return(0); /* Not found */
      strcpy(filename, p);
    }

    /* Now get the contents */
    mft = read_text_file_to_memory(filename);
    if (mft == NULL) return(0);
  } else {
    mft = mft_get_from_archive(package);
  }

  return(mft);
}

/* ----------------
 * - mft_get_list -
 * ---------------- */

char **
mft_get_list (const char **mft_path, PACKAGE_INFO *package)
{
  char **list      = NULL;
  char **new_list  = NULL;
  char  *mft       = NULL;
  char  *p         = NULL;
  char  *next      = NULL;
  int    file      = 0;
  size_t max_files = 32; /* Initial guess for max number of entries */
  int    i         = 0;

  /* Read the manifest file */
  mft = mft_get(mft_path, package);
  if (mft == NULL)
    return(NULL);  

  /* Allocate initial list, with NULL terminator */
  list = malloc(sizeof(char *) * (max_files + 1));
  if (list == NULL) {
    free(mft);
    return(NULL);
  }

  for (i = 0; i <= max_files; i++) { list[i] = NULL; }

  /* Decompose the manifest text into a list of filenames */
  for (file = 0, p = mft, next = NULL; p != NULL; p = next) {
    /* Set up next pointer by finding the end-of-current
     * line, to get a filename. */
    next = strchr(p, '\n');
			
    if (next != NULL) {
      /* Rewind before skipping carriage-returns and
       * linefeeds. */
      while ((*next == '\r') || (*next == '\n')) {
	next--;
      }
      next++;
      while ((*next == '\r') || (*next == '\n')) {
	*next = '\0';
	next++;
      }
    }

    /* Skip blank filenames */
    if (*p == '\0')
      continue;

    /* Add file to list */
    if (file == max_files) {
      /* Need to lengthen list first */
      max_files *= 2;

      /* realloc() with space for NULL terminator */
      new_list = realloc(list, sizeof(char *) * (max_files + 1));
      if (new_list == NULL) {
	/* Failed => bail out */
	for (i = 0; list[i] != NULL; i++) { free(list[i]); }
	free(list);
	list = NULL;
	break;
      }

      list = new_list;
      for (i = file; i <= max_files; i++) { list[i] = NULL; }
    }

    list[file] = strdup(p);
    if (list[file] == NULL) {
      for (i = 0; list[i] != NULL; i++) { free(list[i]); }
      free(list);
      list = NULL;
      break;
    }
    file++;
  }

  /* Tidy up */
  free(mft);

  /* If the list is too big, shrink it. If realloc() fails, don't barf, just
   * return original list. */
  if (file < max_files) {
    max_files = file;

    /* realloc() with space for NULL terminator */
    new_list = realloc(list, sizeof(char *) * (max_files + 1));
    if (new_list != NULL)
      list = new_list;
  }

  /* Done */
  return(list);
}
