/* $Id: util.c,v 1.19 2002/03/10 21:16:26 richdawe Exp $ */

/*
 *  util.c - Utility functions for zippo
 *  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 <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <limits.h>
#include <glob.h>

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

/* --- Perl-isms --- */
void
die (const char *msg)
{
	fprintf(stderr, "Error: %s\n", msg);
	exit(EXIT_FAILURE);
}

void
warn (const char *msg)
{
  fprintf(stderr, "Warning: %s\n", msg);
}

void
info (const char *msg)
{
  fprintf(stderr, "Info: %s\n", msg);
}

/* New-line removal */
void
chomp (char *str)
{
  char *p = NULL;
  if (str == NULL) return;
  for (p = str + strlen(str) - 1; (*p == '\n') || (*p == '\r'); p--) {
    *p = '\0';
  }
}

/* Trailing whitespace removal */
void
rtrim (char *str)
{
  int i;

  if (strlen(str) == 0) return;
  for (i = strlen(str) - 1; i >= 0; i--) {
    if (!isspace(str[i])) break;
    str[i] = '\0';
  }
}

/* --- Multiple-argument Perl-isms --- */

void
#ifdef __GNUC__
__attribute__((format (printf, 1, 2)))
#endif
dief (const char *fmt, ...)
{
  va_list arg;

  va_start(arg, fmt);
  fprintf(stderr, "Error: ");
  vfprintf(stderr, fmt, arg);
  fprintf(stderr, "\n");
  va_end(arg);
  exit(EXIT_FAILURE);
}

void
#ifdef __GNUC__
__attribute__((format (printf, 1, 2)))
#endif
warnf (const char *fmt, ...)
{
  va_list arg;

  va_start(arg, fmt);
  fprintf(stderr, "Warning: ");
  vfprintf(stderr, fmt, arg);
  fprintf(stderr, "\n");
  va_end(arg);
}

void
#ifdef __GNUC__
__attribute__((format (printf, 1, 2)))
#endif
infof (const char *fmt, ...)
{
  va_list arg;

  va_start(arg, fmt);
  printf("Info: ");
  vprintf(fmt, arg);
  printf("\n");
  va_end(arg);
}

/* --- Slash functions --- */

void
backslashify (char *p)
{
  if (p == NULL) return;
  for (; *p != '\0'; p++) { if (*p == '/') *p = '\\'; }
}

void
forwardslashify (char *p)
{
  if (p == NULL) return;
  for (; *p != '\0'; p++) { if (*p == '\\') *p = '/'; }
}

/* Add a forward slash to the end safely, i.e. it if has one, don't do it. */
void
addforwardslash (char *p)
{
  if (p == NULL) return;
  if (p[strlen(p) - 1] == '/') return;
  strcat(p, "/");
}

/* --- String functions --- */

/* ------------
 * - strdupnx -
 * ------------ */

/* Copy a string and allocate n extra bytes. */
char *
strdupnx (const char *str, const int n)
{
  char *p = NULL;

  if (str == NULL) return(NULL);
  p = (char *) calloc(strlen(str) + 1 + n, 1);
  if (p == NULL) return(NULL);
  strcpy(p, str);
  return(p);
}

/* --- File name, path, etc. functions --- */

/* -----------------
 * - has_extension -
 * ----------------- */

/* Returns 1 if the file has the specified extension, 0 otherwise. The
 * comparison is performed case-insensitively. It'll only look at the last
 * extension. Beware! */

int
has_extension (const char *path, const char *ext)
{
  char *p = strrchr(path, '.');
  if (p == NULL) return(0);
  p++;
  if (strcasecmp(p, ext) == 0) return(1); else return(0);
}

int
iszip (const char *path)
{
  return(has_extension(path, "zip"));
}

int
isdsm (const char *path)
{
  return(has_extension(path, "dsm"));
}

/* -----------
 * - istargz -
 * ----------- */

/* Returns 1 if the file is a gzip'd tar file, 0 otherwise. This is based only
 * on file name, but should suffice. */

int
istargz (const char *path)
{
  char *p = strrchr(path, '.');
  if (p == NULL) return(0);
  p++;
  if (strcasecmp(p, "tgz") == 0) return(1);
  if (strcasecmp(p, "taz") == 0) return(1);
  p -= strlen("tar.");
  if (p < path) return(0);
  if (strcasecmp(p, "tar.gz") == 0) return(1);
  return(0);
}

/* ------------
 * - istarbz2 -
 * ------------ */

/* Returns 1 if the file is a bzip2'd tar file, 0 otherwise. This is based only
 * on file name, but should suffice. */

int
istarbz2 (const char *path)
{
  char *p = strrchr(path, '.');
  if (p == NULL) return(0);
  p++;
  if (strcasecmp(p, "tbz") == 0) return(1);
  if (strcasecmp(p, "tbz2") == 0) return(1);
  p -= strlen("tar.");
  if (p < path) return(0);
  if (strcasecmp(p, "tar.bz2") == 0) return(1);
  return(0);
}

/* -------------
 * - isarchive -
 * ------------- */

/* This calls iszip(), istargz() to find out, if it's an archive or not.
 * If other archive types are added, they should be added here too. */

int
isarchive (const char *path)
{
  return(iszip(path) || istargz(path));
}

/* ---------
 * - isurl -
 * --------- */

/* Is this a URL? 1 = yes, 0 = no */

int
isurl (const char *path)
{
  char *p = strstr(path, "://");
  char *q = NULL;
  if (p == NULL) return(0);

  /* Check the scheme part is OK, i.e. only alphanumeric chars */
  for (q = p; q != p; q++) { if (!isalpha(*q)) return(0); }

  /* TODO: Write better checking here */
  /* Assume the rest is OK */
  return(1);
}

/* -------------------
 * - ends_with_slash -
 * ------------------- */

/* Does the file name end with a slash? 1 = yes, 0 = no. This is used by
 * code that deals with archives, since the directories are stored with
 * trailing slashes in archives - well ZIPs at least. */

int
ends_with_slash (const char *d)
{
  if (d == NULL)
    return(0);

  if (strlen(d) == 0)
    return(0);

  if (d[strlen(d) - 1] != '/')
    return(0);

  return(1);
}

/* ---------
 * - isdir -
 * --------- */

/*
 * Check that the named path is a directory. If it is, 1 is returned,
 * otherwise 0. On error -1 is returned and errno will contain the error
 * from stat().
 */

int
isdir (const char *path)
{
  struct stat s;

  if (stat(path, &s) == -1)
    return(-1);

  if (S_ISDIR(s.st_mode))
    return(1);

  /* Fail by default */
  return(0);
}

/* -------------
 * - isabspath -
 * ------------- */

/* Is the path given absolute? If so, return 1, else 0. */

int
isabspath (const char *path)
{	
  if (path[0] && (path[0] == '/'))
    return(1);

#ifdef MSDOS
  /* On MS-DOS, cope with a drive prefix too. */
  if ((strlen(path) >= 3) && (path[1] == ':') && (path[2] == '/'))
    return(1);
#endif /* MSDOS */
	
  return(0);
}

/* -------------
 * - isrelpath -
 * ------------- */

/* Is this a relative path? Simply !isabspath(). */

int
isrelpath (const char *path)
{
  return(!isabspath(path));
}

/* -----------------
 * - isspecialpath -
 * ----------------- */

/* Is this a 'special' path? By 'special' it is meant that the path is not
 * a valid place for reading/writing files. */

int
isspecialpath (const char *path)
{
#ifdef __DJGPP__
  {
    /* /dev is not a valid path for DJGPP. But allow access via /dev/env
     * and /dev/? since they will be expanded by DJGPP's libc. */
    const char PREFIX_DEV[]     = "/dev/";
    const char PREFIX_DEV_ENV[] = "/dev/env/";
    int good_dev = 0;

    if (strcmp(path, PREFIX_DEV) == 0)
      return(1);

    if (strstr(path, PREFIX_DEV) == path) {      
      good_dev  = (strstr(path, PREFIX_DEV_ENV) == path);

      good_dev |= (   isalpha(path[strlen(PREFIX_DEV)])
		   && (   (path[strlen(PREFIX_DEV) + 1] == '/')
		       || (path[strlen(PREFIX_DEV) + 1] == '\\')));

      if (!good_dev)
	return(1);
    }
  }
#endif /* __DJGPP__ */

  return(0);
}

/* -------------------
 * - recursive_mkdir -
 * ------------------- */

/*
 * This works like mkdir, except that all the directory components are created.
 * This is like 'mkdir -p' from GNU fileutils.
 *
 * On success 0 is returned, else -1 is returned and 'errno' will contain
 * the failure code.
 */

int
recursive_mkdir (const char *pathname, const mode_t mode)
{
  /* Allocate space to add trailing slash, if necessary. */
  char *buf = strdupnx(pathname, 1);
  char *p   = buf;
  int ret;

  addforwardslash(buf);
	
#ifdef __DJGPP__
  /* Skip the drive specifier. */
  if (buf[1] == ':') p += 2;
#endif

  while ((p = strchr(p + 1, '/')) != NULL) {
    /* Quit if it's a trailing slash */
    if (*p == '\0') break;

    /* Chop up temporarily & create a component. */
    *p = '\0';
    ret = mkdir(buf, mode);
    *p = '/';

    if ((ret != 0) && (errno != EEXIST)) {
      /* Pass down error */
      free(buf);
      return(-1);
    }
  }

  free(buf);
  return(0);
}

/* -----------------
 * - find_in_paths _
 * ----------------- */

/*
 * This searches the supplied paths (NULL-terminated list)  for the file named
 * 'name'. If 'case_sensitivity' is true (non-zero), case sensitive
 * comparisons are performed, else case insensitive.
 *
 * On success, the file name is returned; on failure, NULL is returned. The
 * matching file name is returned in a static buffer, so beware.
 */

char *
find_in_paths (const char *name, const char **paths,
	       const int case_sensitivity)
{
  DIR *d = NULL;
  struct dirent *de = NULL;
  static char buf[PATH_MAX];
  int ret, i;

  if (paths == NULL) return(NULL);

  for (i = 0; paths[i] != NULL; i++) {
    d = opendir(paths[i]);
    if (d == NULL) continue;

    /* Read all directory entries. */
    while((de = readdir(d)) != NULL) {
      ret = case_sensitivity
	  ? strcmp(de->d_name, name) : strcasecmp(de->d_name, name);

      if (ret == 0) {
	/* Found a match */
	strcpy(buf, paths[i]);
	addforwardslash(buf);
	strcat(buf, name);
	closedir(d);
	return(buf);
      }
    }

    closedir(d);
  }

  return(NULL);
}

/* -------------------------------
 * - find_in_paths_with_suffixes -
 * ------------------------------- */

/*
 * This function searches for 'name' using the paths given in
 * the NULL-terminated list 'paths'. If that fails, it also searches
 * the sub-directories given in 'suffixes' for each path in 'paths'.
 * If 'suffixes' is NULL, then the search terminates after looking in 'paths'.
 *
 * If 'case_sensitivity' is true (non-zero), case sensitive comparisons are
 * performed, else case insensitive.
 *
 * 'suffixes' would typically be 'djgpp_archive_prefixes', which contains
 * e.g. 'v2gnu'. These directories are prefixes to the files on the Simtelnet
 * archive, but become suffixes to the search paths here.
 *
 * On success, the file name is returned; on failure, NULL is returned. The
 * matching file name is returned in a static buffer, so beware.
 */

char *
find_in_paths_with_suffixes (const char *name,
			     const char **paths, const char **suffixes,
			     const int case_sensitivity)
{
  static char  buf[PATH_MAX];
  char         subdir[PATH_MAX];
  char        *subdirs[2] = { NULL, NULL };
  char        *match = NULL;
  int          i     = 0;
  int          j     = 0;

  /* Try a normal match */
  match = find_in_paths(name, (const char **) paths, case_sensitivity);
  if (match) {
    /* Copy match into our static buffer. */
    strncpy(buf, match, sizeof(buf));
    buf[sizeof(buf) - 1] = '\0';
    return(buf);
  }

  /* Now try a suffix match, if there are any. */
  if (suffixes == NULL)
    return(NULL);

  subdirs[0] = subdir;
  subdirs[1] = NULL;

  for (i = 0; paths[i] != NULL; i++) {
    for (j = 0; suffixes[j] != NULL; j++) {
      /* Build & search sub-directory. */
      strcpy(subdir, paths[i]);
      addforwardslash(subdir);
      strcat(subdir, suffixes[j]);

      match = find_in_paths(name, (const char **) subdirs, case_sensitivity);
      if (match != NULL)
	break;
    }

    if (match != NULL)
      break;
  }

  if (match) {
    /* Copy match into our static buffer. */
    strncpy(buf, match, sizeof(buf));
    buf[sizeof(buf) - 1] = '\0';
    return(buf);
  }

  return(NULL);
}

/* -----------------
 * - glob_in_paths -
 * ----------------- */

/* Glob for a file in all the specified paths. For a description
 * of return values, see glob()'s man/info documentation. */

int
glob_in_paths (const char *pattern,
	       const char **paths,
	       int flags,
	       int (*errfunc)(const char *epath, int eerrno),
	       glob_t *pglob)
{
  char buf[PATH_MAX * 2]; /* Allow some space for pattern. */
  int  ret = 0;           /* Succeed by default. */
  int  i;  

  for (i = 0; paths[i] != NULL; i++) {
    /* Skip files or inaccessible directories. */
    if (isdir(paths[i]) <= 0)
      continue;

    strcpy(buf, paths[i]);
    addforwardslash(buf);
    strcat(buf, pattern);

    ret = glob(buf, flags, errfunc, pglob);

    /* Abort, if an error occurred. */
    if (ret && (ret != GLOB_NOMATCH))
      break;

    /* Append on next loop round. */
    flags |= (GLOB_APPEND|GLOB_DOOFFS);
  }

  return(ret);
}

/* -------------------------------
 * - glob_in_paths_with_suffixes -
 * ------------------------------- */

/*
 * For each path in paths, glob in the path itself and path with each
 * of the suffixes from suffixes.
 *
 * NB: This relies on the fact that we can pass in (GLOB_APPEND|GLOB_DOOFS)
 * as the flags member.
 */

int
glob_in_paths_with_suffixes (const char *pattern,
			     const char **paths,
			     const char **suffixes,
			     int flags,
			     int (*errfunc)(const char *epath, int eerrno),
			     glob_t *pglob)
{
  char        buf[PATH_MAX];
  const char *mypaths[2] = { buf, NULL }; /* Fake list for path+suffix. */
  int         ret = 0;
  int         i, j;

  /* Glob in paths first. */
  ret = glob_in_paths(pattern, paths, flags, errfunc, pglob);

  /* Abort, if an error occurred. */
  if (ret && (ret != GLOB_NOMATCH))
    return(ret);

  /* No suffixes => just call glob_in_paths(). */
  if (suffixes == NULL)
    return(ret);

  /* Append to list. */
  flags |= (GLOB_APPEND|GLOB_DOOFFS);

  /* Now glob each path+suffix. */
  for (i = 0; paths[i] != NULL; i++) {
    for (j = 0; suffixes[j] != NULL; j++) {
      strcpy(buf, paths[i]);
      addforwardslash(buf);
      strcat(buf, suffixes[j]);

      ret = glob_in_paths(pattern, mypaths, flags, errfunc, pglob);

      /* Abort, if an error occurred. */
      if (ret && (ret != GLOB_NOMATCH))
	break;
    }

    /* Abort, if an error occurred. */
    if (ret && (ret != GLOB_NOMATCH))
      break;
  }

  return(ret);
}

/* ----------------------------
 * - read_text_file_to_memory -
 * ---------------------------- */

/* Given a file name, this creates a buffer and reads the file's contents
 * into it. */

char *
read_text_file_to_memory (const char *file)
{
  FILE          *fp     = NULL;
  char          *buf    = NULL;
  struct stat    sbuf;
  int            ret;

  fp = fopen(file, "rb");
  if (fp == NULL) return(NULL);

  /* Get the file's details. */
  if (fstat(fileno(fp), &sbuf) != 0) {
    fclose(fp);
    return(NULL);
  }

  /* Create the buffer */
  buf = (char *) calloc(1, sbuf.st_size + 1);
  if (buf == NULL) {
    fclose(fp);
    return(NULL);
  }

  /* Read the file */
  ret = fread(buf, sbuf.st_size, 1, fp);
  if (ret <= 0) {
    free(buf);
    fclose(fp);
    return(NULL);
  }

  fclose(fp);
  return(buf);
}

/* -------------
 * - copy_file -
 * ------------- */

/* This copies the specified file from 'src' to 'dest', like the 'cp'
 * command. It will overwrite the destination file. It returns 1 on success,
 * 0 otherwise. */

int
copy_file (const char *src, const char *dest)
{
  FILE *sfp = NULL, *dfp = NULL;
  int sfd = 0;
  struct stat sbuf;
  char buf[65536]; /* Reasonable size? */
  int ret;

  sfp = fopen(src,  "rb");
  dfp = fopen(dest, "wb");
  if ((sfp == NULL) || (dfp == NULL)) return(0);

  sfd = fileno(sfp);
  if (fstat(sfd, &sbuf) != 0) {
    /* Abort - can't get input file info! */
    fclose(dfp);
    fclose(sfp);
    unlink(dest);
    return(0);
  }

  while ( (ret = read(sfd, buf, sizeof(buf))) > 0) {
    if (fwrite(buf, ret, 1, dfp) <= 0) {
      /* Write failed, abort! */
      fclose(dfp);
      fclose(sfp);
      unlink(dest);
      return(0);
    }
  }

  fclose(dfp);
  fclose(sfp);

  /* Change the output file's permissions to match the input file. */
  chmod(dest, sbuf.st_mode);

  return(1);
}

/* ------------
 * - count_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
count_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);
}

/* ----------------
 * - find_archive -
 * ---------------- */

char *
find_archive (const char *name, const char *req_name,
	      const char **archive_paths)
{
  static char  buf[PATH_MAX];
  char        *dirs[] = { NULL, NULL };
  char        *match  = NULL;
  char        *p      = NULL;

  /*
   * Match case insensitively on archive name. Search the following locations:
   *
   * 1. the directory path used in the request, e.g. for the first ZIP file;
   *
   * 2. the ZIP directories;
   *
   * 3. any of the standard DJGPP archive directories off the
   *    ZIP directories.
   *
   * If none of these match, bail out.
   */

  /* Step 1 */
  if ((req_name != NULL) && (strchr(req_name, '/') != NULL)) {
    dirs[0] = strdup(req_name);
    dirs[1] = NULL;

    forwardslashify(dirs[0]);
    p = strrchr(dirs[0], '/');
    *p = '\0';

    match = find_in_paths(name, (const char **) dirs, 0);

    free(dirs[0]);
    dirs[0] = NULL;
  }				
		
  /* Step 2, if step 1 failed. */
  if (match == NULL)
    match = find_in_paths(name, (const char **) archive_paths, 0);

  /* Step 3, if step 1 and/or 2 failed. */ 
  if (match == NULL)
    match = find_in_paths_with_suffixes(name, archive_paths,
					djgpp_archive_prefixes, 0);

  if (match != NULL) {
    /* Copy match into our static buffer. */
    strncpy(buf, match, sizeof(buf));
    buf[sizeof(buf) - 1] = '\0';
    return(buf);
  }

  return(NULL);
}

/* ---------------------
 * - get_url_component -
 * --------------------- */

/* Parse an HTTP or FTP into its components. */

int
get_url_component (const url_comp_t comp, const char *url,
		   char *buf, const size_t buflen)
{
  size_t n;
  int ok = 1; /* Succeed by default */

  switch(comp) {
  case URL_COMP_SCHEME:
    /* Look for 'scheme:' */
    for (n = 0; url[n] != '\0'; n++) {
      if (url[n] == ':')
	break;
    }

    /* Found scheme delimiter */
    if (n < buflen) {
      strncpy(buf, url, n);
      buf[n] = '\0';
    } else {
      /* Insufficient space */
      ok = 0;
    }
    break;

    /* TODO: Other components */

  default:
    /* Unknown component */
    ok = 0;
    break;
  }

  return(ok);
}
