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

/*
 *  md5hash.c - Routines for MD5 hash generation from files for zippo
 *  Copyright (C) 2000, 2001 by Richard Dawe
 *      
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "common.h"

#include <ctype.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>

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

#include "base64.h"
#include "md5.h"
#include "md5hash.h"

/* ---------------------
 * - md5hash_calculate -
 * --------------------- */

int
md5hash_calculate (const char *filename, char result[MD5_HASH_SIZE])
{
  FILE *fp  = NULL;
  int   ret = 1; /* Success by default */
 
  fp = fopen(filename, "rb");
  if (fp == NULL)
    return(0);
	
  if (md5_stream(fp, (void *) result))
    ret = 0;

  fclose(fp);

  return(ret);
}

/* --------------------
 * - md5hash_internal -
 * -------------------- */

/* This function takes the name of the log file to generate, a text string
 * containing a list of files (separated by CRLF) and the prefix for the
 * package. On success returns 0, else -1 and errno set appropriately. */

static int
md5hash_internal (const char *logfile, char **files, const char *prefix)
{
  FILE *md5_fp = NULL;  
  char md5_hash[MD5_HASH_SIZE]; /* 128-bit MD5 hash = 16 bytes      */
  char md5_hash_base64[25];	/* Base64'd MD5 -> 4/3*16 + quad pad + nul */
  char temp_file[PATH_MAX];
  size_t insize, outsize;
  int ret;
  int i;

  /* No files? */
  if (files == NULL)
    return(-1);
	
  /* Open the log file. */
  md5_fp = fopen(logfile, "wt");
	
  if (md5_fp == NULL)
    return(-1);
	
  /* Create MD5 hashes for this package. */
  for (i = 0; files[i] != NULL; i++) {			
    /* Build the filename. */
    /* TODO: This will be affected by resolution of comment about prefix
     * above. */
    strcpy(temp_file, prefix);
    strcat(temp_file, files[i]);

    /* Skip directories */
    ret = isdir(temp_file);

    if (ret < 0) {
      warnf("Unable to check that '%s' is a directory (errno = %d): %s",
	    temp_file, errno, strerror(errno));
      continue;
    }

    if (ret)
      continue;

    /* Calculate the MD5 hash */
    if (!md5hash_calculate(temp_file, md5_hash)) {
      warnf("Error calculating MD5 hash for file '%s'", temp_file);
      continue;
    }

    /* Get Base64 encoded version of MD5 hash */
    insize  = sizeof(md5_hash);
    outsize = sizeof(md5_hash_base64) - 1;
    if (base64_encode((void *) md5_hash, insize,
		      (void *) md5_hash_base64, &outsize) == -1) {
      warnf("Error in Base64 encoding for file '%s'", temp_file);
      continue;
    }
			
    /* Nul-terminate the Base64 text, so we can log it. */
    md5_hash_base64[outsize] = '\0';

    /* Finally, store the MD5 hash for this file. */
    fprintf(md5_fp, "%s %s\n", temp_file, md5_hash_base64);
  }

  /* Tidy up */
  fclose(md5_fp);
  return(0);
}

/* -----------
 * - md5hash -
 * ----------- */

/* This is a more convenient function that md5hash_internal() and handles
 * filename generation. On success returns 0, else -1 and errno set
 * appropriately. */

int
md5hash (PACKAGE_INFO *package, const char *db_path, const char *prefix,
	 const char **mft_path)
{
  char   md5_file[PATH_MAX];
  char  *mft   = NULL;
  char **files = NULL;
  int    ret;
  int    i;

  /* Skip none-type, group and virtual packages - nothing to do here. */
  if (   (package->version.type == TYPE_NONE)
      || (package->version.type == TYPE_GROUP)
      || (package->version.type == TYPE_VIRTUAL))
    return(0);

  /* Build MD5 hash filename. */
  strcpy(md5_file, db_path);
  strcat(md5_file, package->dsm_name);
  strcat(md5_file, ".md5");

  /* If the MD5 file exists, skip this package. */
  if (access(md5_file, R_OK) == 0) {
    errno = EEXIST;
    return(-1);
  }

  /* Use the manifest to find a list of files to generate MD5 hashes
   * for. */
  /* TODO: What to do for packages without manifest files? */
  mft = mft_get(mft_path, package);

  if (mft == NULL) {			
    warnf("Unable to read manifest file for package %s %s",
	  package->name, package_version_string(&package->version));
    errno = ENOENT;
    return(-1);
  }

  free(mft), mft = NULL;

  files = mft_get_list(mft_path, package);
  if (files == NULL) {
    warnf("Unable to get file list for package %s %s",
	  package->name, package_version_string(&package->version));
    errno = ENOENT;
    return(-1);
  }

  /* TODO: What about prefixes? Where would they be stored?
   * In the install log? Default to the root, if the prefix is
   * not found? */
  ret = md5hash_internal(md5_file, files, prefix);

  /* Tidy up */		
  for (i = 0; files[i] != NULL; i++) { free(files[i]); }
  free(files);

  return(0);
}

/* ---------------
 * - md5hash_get -
 * --------------- */

md5hash_entry **
md5hash_get (PACKAGE_INFO *package, const char *db_path)
{
  md5hash_entry **list   = NULL;
  md5hash_entry  *e      = NULL;
  char            buf[PATH_MAX + 32];  /* Max path + space for MD5 hash */
  char            md5_hash[MD5_HASH_SIZE]; /* 128-bit MD5 hash = 16 bytes */
  size_t          md5_hash_size = 0;
  char            md5_hash_base64[25]; /* Base64'd MD5 - 4/3*16 + quad + nul */
  char            md5_file[PATH_MAX];
  FILE           *md5_fp = NULL;
  char           *p      = NULL;
  char           *q      = NULL;
  int             nfiles = 0;
  int             i      = 0;
  int             line   = 0;
  ptrdiff_t       len    = 0;
  
  /* Build MD5 hash filename. */
  strcpy(md5_file, db_path);
  strcat(md5_file, package->dsm_name);
  strcat(md5_file, ".md5");

  /* Open the MD5 file. */
  md5_fp = fopen(md5_file, "rt");
	
  if (md5_fp == NULL)
    return(list);

  /* Count the number of files */
  /* TODO: This assumes that the line does not exceed 'buf' in length. */
  for (nfiles = 0; fgets(buf, sizeof(buf), md5_fp) != NULL; nfiles++) {;}

  /* Reread file */
  rewind(md5_fp);

  /* Allocate some memory for the list. */
  list = malloc(sizeof(*list) * (nfiles + 1));
  if (list == NULL) {
    fclose(md5_fp);
    return(list);
  }

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

  /* Now parse file into a list of md5hash_entry structs. */
  line = 0;

  while (fgets(buf, sizeof(buf), md5_fp) != NULL) {
    /* Skip all the whitespace */
    for (p = buf; isspace(*p); p++) {;}

    /* Nuke all CR, LFs at the end of the line. */    
    for (q = p + strlen(p) - 1;
	 (q > p) && ((*q == '\r') || (*q == '\n'));
	 q--) {
      *q = '\0';
    }

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

    /* Find space separator */
    q = strchr(p, ' ');
    if (q == NULL)
      continue;

    /* Allocate memory for entry - the block includes both the structure
     * and the filename. Hence, only the block will need freeing. Freeing
     * the filename will lead to a SIGSEGV. */
    len = q - p; /* Length of filename */

    e = malloc(sizeof(*e) + (size_t) (len + 1 /* nul */));
    if (e == NULL) {
      /* Tidy up memory */
      md5hash_entries_free(list);
      free(list), list = NULL;
      break;
    }

    /* Copy filename */
    e->filename = (char *) ((void *) e + sizeof(*e));
    strncpy(e->filename, p, len);
    e->filename[len] = '\0';

    /* Get MD5 hash */
    while (isspace(*q)) { q++; } /* Skip whitespace */

    strncpy(md5_hash_base64, q, sizeof(md5_hash_base64) - 1);
    md5_hash_base64[sizeof(md5_hash_base64) - 1] = '\0';

    md5_hash_size = sizeof(md5_hash);
    if (   (base64_decode(md5_hash_base64, strlen(md5_hash_base64),
			  md5_hash, &md5_hash_size) != 0)
	|| (md5_hash_size != sizeof(md5_hash)) ) {
      /* Base64 decode failed */

      /* Tidy up memory */
      md5hash_entries_free(list);
      free(list), list = NULL;
      break;
    }

    memcpy(e->md5hash, md5_hash, sizeof(e->md5hash));

    /* Update list */
    list[line] = e;
    line++;
  }

  /* Close the MD5 file */
  fclose(md5_fp);

  /* Done */
  return(list);
}

/* ------------------------
 * - md5hash_entries_free -
 * ------------------------ */

int
md5hash_entries_free (md5hash_entry **list)
{
  int ret = 1; /* Suceed by default */
  int i;

  if (list == NULL)
    return(ret);

  for (i = 0; list[i] != NULL; i++) {
    free(list[i]);
    list[i] = NULL;
  }

  return(ret);
}

/* -------------------
 * - md5hash_compare -
 * ------------------- */

int
md5hash_compare (PACKAGE_INFO *package, const char *db_path, md5hash_entry *e)
{
  int ret = 0; /* Fail by default */

  return(ret);
}
