/*
**  Copyright 1998-2003 University of Illinois Board of Trustees
**  Copyright 1998-2003 Mark D. Roth
**  All rights reserved.
**
**  extract.c - libtar code to extract a file from a tar archive
**
**  Mark D. Roth <roth@uiuc.edu>
**  Campus Information Technologies and Educational Services
**  University of Illinois at Urbana-Champaign
*/

#include <libtarint/internal.h>

#include <stdio.h>
#include <libtar/compat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>

#if defined(_WIN32) && !defined(__CYGWIN__)
# ifdef _MSC_VER
#  include <sys/utime.h>
# else
#  include <utime.h>
# endif
# include <io.h>
# include <direct.h>
#else
# include <utime.h>
# include <sys/param.h>
#endif

#ifdef STDC_HEADERS
# include <stdlib.h>
# include <string.h>
#endif

#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif

#ifdef HAVE_SYS_MKDEV_H
# include <sys/mkdev.h>
#endif


struct linkname
{
  char ln_save[TAR_MAXPATHLEN];
  char ln_real[TAR_MAXPATHLEN];
};
typedef struct linkname linkname_t;


static int
tar_set_file_perms(TAR *t, char *realname)
{
  mode_t mode;
  uid_t uid;
  gid_t gid;
  struct utimbuf ut;
  char *filename;
  char *pathname = 0;

  if (realname)
    {
    filename = realname;
    }
  else
    {
    pathname = th_get_pathname(t);
    filename = pathname;
    }

  mode = th_get_mode(t);
  uid = th_get_uid(t);
  gid = th_get_gid(t);
  ut.modtime = ut.actime = th_get_mtime(t);

  /* change owner/group */
#ifndef WIN32
  if (geteuid() == 0)
#ifdef HAVE_LCHOWN
    if (lchown(filename, uid, gid) == -1)
    {
# ifdef DEBUG
      fprintf(stderr, "lchown(\"%s\", %d, %d): %s\n",
        filename, uid, gid, strerror(errno));
# endif
#else /* ! HAVE_LCHOWN */
    if (!TH_ISSYM(t) && chown(filename, uid, gid) == -1)
    {
# ifdef DEBUG
      fprintf(stderr, "chown(\"%s\", %d, %d): %s\n",
        filename, uid, gid, strerror(errno));
# endif
#endif /* HAVE_LCHOWN */
    if (pathname)
      {
      free(pathname);
      }
      return -1;
    }

  /* change access/modification time */
  if (!TH_ISSYM(t) && utime(filename, &ut) == -1)
  {
#ifdef DEBUG
    perror("utime()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }
  /* change permissions */
  if (!TH_ISSYM(t) && chmod(filename, mode & 07777) == -1)
  {
#ifdef DEBUG
    perror("chmod()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }

#else /* WIN32 */
  (void)filename;
  (void)gid;
  (void)uid;
  (void)mode;
#endif /* WIN32 */

  if (pathname)
    {
    free(pathname);
    }
  return 0;
}


/* switchboard */
int
tar_extract_file(TAR *t, char *realname)
{
  int i;
  linkname_t *lnp;
  char *pathname;

  if (t->options & TAR_NOOVERWRITE)
  {
    struct stat s;

#ifdef WIN32
    if (stat(realname, &s) == 0 || errno != ENOENT)
#else
    if (lstat(realname, &s) == 0 || errno != ENOENT)
#endif
    {
      errno = EEXIST;
      return -1;
    }
  }

  if (TH_ISDIR(t))
  {
    i = tar_extract_dir(t, realname);
    if (i == 1)
      i = 0;
  }
#ifndef _WIN32
  else if (TH_ISLNK(t))
    i = tar_extract_hardlink(t, realname);
  else if (TH_ISSYM(t))
    i = tar_extract_symlink(t, realname);
  else if (TH_ISCHR(t))
    i = tar_extract_chardev(t, realname);
  else if (TH_ISBLK(t))
    i = tar_extract_blockdev(t, realname);
  else if (TH_ISFIFO(t))
    i = tar_extract_fifo(t, realname);
#endif
  else /* if (TH_ISREG(t)) */
    i = tar_extract_regfile(t, realname);

  if (i != 0)
    return i;

  i = tar_set_file_perms(t, realname);
  if (i != 0)
    return i;

  lnp = (linkname_t *)calloc(1, sizeof(linkname_t));
  if (lnp == NULL)
    return -1;
  pathname = th_get_pathname(t);
  strlcpy(lnp->ln_save, pathname, sizeof(lnp->ln_save));
  strlcpy(lnp->ln_real, realname, sizeof(lnp->ln_real));
#ifdef DEBUG
  printf("tar_extract_file(): calling libtar_hash_add(): key=\"%s\", "
         "value=\"%s\"\n", pathname, realname);
#endif
  if (pathname)
    {
    free(pathname);
    }
  if (libtar_hash_add(t->h, lnp) != 0)
    return -1;

  return 0;
}


/* extract regular file */
int
tar_extract_regfile(TAR *t, char *realname)
{
  mode_t mode;
  size_t size;
  uid_t uid;
  gid_t gid;
  int fdout;
  ssize_t i, k;
  char buf[T_BLOCKSIZE];
  char *filename;
  char *pathname = 0;

#ifdef DEBUG
  printf("==> tar_extract_regfile(t=0x%lx, realname=\"%s\")\n", t,
         realname);
#endif

  if (!TH_ISREG(t))
  {
    errno = EINVAL;
    return -1;
  }

  if (realname)
    {
    filename = realname;
    }
  else
    {
    pathname = th_get_pathname(t);
    filename = pathname;
    }
  mode = th_get_mode(t);
  size = th_get_size(t);
  uid = th_get_uid(t);
  gid = th_get_gid(t);

  /* Make a copy of the string because dirname and mkdirhier may modify the
   * string */
  strncpy(buf, filename, sizeof(buf)-1);
  buf[sizeof(buf)-1] = 0;

  if (mkdirhier(dirname(buf)) == -1)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }

#ifdef DEBUG
  printf("  ==> extracting: %s (mode %04o, uid %d, gid %d, %d bytes)\n",
         filename, mode, uid, gid, size);
#endif
  fdout = open(filename, O_WRONLY | O_CREAT | O_TRUNC
#ifdef O_BINARY
         | O_BINARY
#endif
        , 0666);
  if (fdout == -1)
  {
#ifdef DEBUG
    perror("open()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }

#if 0
  /* change the owner.  (will only work if run as root) */
  if (fchown(fdout, uid, gid) == -1 && errno != EPERM)
  {
#ifdef DEBUG
    perror("fchown()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }

  /* make sure the mode isn't inheritted from a file we're overwriting */
  if (fchmod(fdout, mode & 07777) == -1)
  {
#ifdef DEBUG
    perror("fchmod()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }
#endif

  /* extract the file */
  for (i = size; i > 0; i -= T_BLOCKSIZE)
  {
    k = tar_block_read(t, buf);
    if (k != T_BLOCKSIZE)
    {
      if (k != -1)
        errno = EINVAL;
      if (pathname)
        {
        free(pathname);
        }
      return -1;
    }

    /* write block to output file */
    if (write(fdout, buf,
        ((i > T_BLOCKSIZE) ? T_BLOCKSIZE : (unsigned int)i)) == -1)
      {
      if (pathname)
        {
        free(pathname);
        }
      return -1;
      }
  }

  /* close output file */
  if (close(fdout) == -1)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }

#ifdef DEBUG
  printf("### done extracting %s\n", filename);
#endif

  (void)filename;
  (void)gid;
  (void)uid;
  (void)mode;

  if (pathname)
    {
    free(pathname);
    }
  return 0;
}


/* skip regfile */
int
tar_skip_regfile(TAR *t)
{
  ssize_t i, k;
  size_t size;
  char buf[T_BLOCKSIZE];

  if (!TH_ISREG(t))
  {
    errno = EINVAL;
    return -1;
  }

  size = th_get_size(t);
  for (i = size; i > 0; i -= T_BLOCKSIZE)
  {
    k = tar_block_read(t, buf);
    if (k != T_BLOCKSIZE)
    {
      if (k != -1)
        errno = EINVAL;
      return -1;
    }
  }

  return 0;
}


/* hardlink */
int
tar_extract_hardlink(TAR * t, char *realname)
{
  char *filename;
  char *linktgt;
  linkname_t *lnp;
  libtar_hashptr_t hp;
  char buf[T_BLOCKSIZE];
  char *pathname = 0;

  if (!TH_ISLNK(t))
  {
    errno = EINVAL;
    return -1;
  }

  if (realname)
    {
    filename = realname;
    }
  else
    {
    pathname = th_get_pathname(t);
    filename = pathname;
    }

  /* Make a copy of the string because dirname and mkdirhier may modify the
   * string */
  strncpy(buf, filename, sizeof(buf)-1);
  buf[sizeof(buf)-1] = 0;

  if (mkdirhier(dirname(buf)) == -1)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }
  libtar_hashptr_reset(&hp);
  if (libtar_hash_getkey(t->h, &hp, th_get_linkname(t),
             (libtar_matchfunc_t)libtar_str_match) != 0)
  {
    lnp = (linkname_t *)libtar_hashptr_data(&hp);
    linktgt = lnp->ln_real;
  }
  else
    linktgt = th_get_linkname(t);

#ifdef DEBUG
  printf("  ==> extracting: %s (link to %s)\n", filename, linktgt);
#endif
#ifndef WIN32
  if (link(linktgt, filename) == -1)
#else
  (void)linktgt;
#endif
  {
#ifdef DEBUG
    perror("link()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }

#ifndef WIN32
  if (pathname)
    {
    free(pathname);
    }
  return 0;
#endif
}


/* symlink */
int
tar_extract_symlink(TAR *t, char *realname)
{
  char *filename;
  char buf[T_BLOCKSIZE];
  char *pathname = 0;

#ifndef _WIN32
  if (!TH_ISSYM(t))
  {
    errno = EINVAL;
    return -1;
  }
#endif

  if (realname)
    {
    filename = realname;
    }
  else
    {
    pathname = th_get_pathname(t);
    filename = pathname;
    }

  /* Make a copy of the string because dirname and mkdirhier may modify the
   * string */
  strncpy(buf, filename, sizeof(buf)-1);
  buf[sizeof(buf)-1] = 0;

  if (mkdirhier(dirname(buf)) == -1)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }

  if (unlink(filename) == -1 && errno != ENOENT)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }

#ifdef DEBUG
  printf("  ==> extracting: %s (symlink to %s)\n",
         filename, th_get_linkname(t));
#endif
#ifndef WIN32
  if (symlink(th_get_linkname(t), filename) == -1)
#endif
  {
#ifdef DEBUG
    perror("symlink()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }

#ifndef WIN32
  if (pathname)
    {
    free(pathname);
    }
  return 0;
#endif
}


/* character device */
int
tar_extract_chardev(TAR *t, char *realname)
{
  mode_t mode;
  unsigned long devmaj, devmin;
  char *filename;
  char buf[T_BLOCKSIZE];
  char *pathname = 0;

#ifndef _WIN32
  if (!TH_ISCHR(t))
  {
    errno = EINVAL;
    return -1;
  }
#endif
  if (realname)
    {
    filename = realname;
    }
  else
    {
    pathname = th_get_pathname(t);
    filename = pathname;
    }
  mode = th_get_mode(t);
  devmaj = th_get_devmajor(t);
  devmin = th_get_devminor(t);

  /* Make a copy of the string because dirname and mkdirhier may modify the
   * string */
  strncpy(buf, filename, sizeof(buf)-1);
  buf[sizeof(buf)-1] = 0;

  if (mkdirhier(dirname(buf)) == -1)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }

#ifdef DEBUG
  printf("  ==> extracting: %s (character device %ld,%ld)\n",
         filename, devmaj, devmin);
#endif
#ifndef WIN32
  if (mknod(filename, mode | S_IFCHR,
      compat_makedev(devmaj, devmin)) == -1)
#else
  (void)devmin;
  (void)devmaj;
  (void)mode;
#endif
  {
#ifdef DEBUG
    perror("mknod()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }

#ifndef WIN32
  if (pathname)
    {
    free(pathname);
    }
  return 0;
#endif
}


/* block device */
int
tar_extract_blockdev(TAR *t, char *realname)
{
  mode_t mode;
  unsigned long devmaj, devmin;
  char *filename;
  char buf[T_BLOCKSIZE];
  char *pathname = 0;

  if (!TH_ISBLK(t))
  {
    errno = EINVAL;
    return -1;
  }

  if (realname)
    {
    filename = realname;
    }
  else
    {
    pathname = th_get_pathname(t);
    filename = pathname;
    }
  mode = th_get_mode(t);
  devmaj = th_get_devmajor(t);
  devmin = th_get_devminor(t);

  /* Make a copy of the string because dirname and mkdirhier may modify the
   * string */
  strncpy(buf, filename, sizeof(buf)-1);
  buf[sizeof(buf)-1] = 0;

  if (mkdirhier(dirname(buf)) == -1)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }

#ifdef DEBUG
  printf("  ==> extracting: %s (block device %ld,%ld)\n",
         filename, devmaj, devmin);
#endif
#ifndef WIN32
  if (mknod(filename, mode | S_IFBLK,
      compat_makedev(devmaj, devmin)) == -1)
#else
  (void)devmin;
  (void)devmaj;
  (void)mode;
#endif
  {
#ifdef DEBUG
    perror("mknod()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }

#ifndef WIN32
  if (pathname)
    {
    free(pathname);
    }
  return 0;
#endif
}


/* directory */
int
tar_extract_dir(TAR *t, char *realname)
{
  mode_t mode;
  char *filename;
  char buf[T_BLOCKSIZE];
  char *pathname = 0;
  size_t len = 0;

  if (!TH_ISDIR(t))
  {
    errno = EINVAL;
    return -1;
  }

  if (realname)
    {
    filename = realname;
    }
  else
    {
    pathname = th_get_pathname(t);
    filename = pathname;
    }
  mode = th_get_mode(t);

  /* Make a copy of the string because dirname and mkdirhier may modify the
   * string */
  strncpy(buf, filename, sizeof(buf)-1);
  buf[sizeof(buf)-1] = 0;

  if (mkdirhier(dirname(buf)) == -1)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }

    /* Strip trailing '/'...it confuses some Unixes (and BeOS)... */
    strncpy(buf, filename, sizeof(buf)-1);
    buf[sizeof(buf)-1] = 0;
    len = strlen(buf);
    if ((len > 0) && (buf[len-1] == '/'))
      {
      buf[len-1] = '\0';
      }

#ifdef DEBUG
  printf("  ==> extracting: %s (mode %04o, directory)\n", filename,
         mode);
#endif
#ifdef WIN32
  if (mkdir(buf) == -1)
#else
  if (mkdir(buf, mode & 07777) == -1)
#endif
  {
#ifdef __BORLANDC__
  /* There is a bug in the Borland Run time library which makes MKDIR
     return EACCES when it should return EEXIST
     if it is some other error besides directory exists
     then return false */
    if ( errno == EACCES) 
    {
      errno = EEXIST;
    }
#endif      
    if (errno == EEXIST)
    {
      if (chmod(filename, mode & 07777) == -1)
      {
#ifdef DEBUG
        perror("chmod()");
#endif
        if (pathname)
          {
          free(pathname);
          }
        return -1;
      }
      else
      {
#ifdef DEBUG
        puts("  *** using existing directory");
#endif
        if (pathname)
          {
          free(pathname);
          }
        return 1;
      }
    }
    else
    {
#ifdef DEBUG
      perror("mkdir()");
#endif
      if (pathname)
        {
        free(pathname);
        }
      return -1;
    }
  }

  if (pathname)
    {
    free(pathname);
    }
  return 0;
}


/* FIFO */
int
tar_extract_fifo(TAR *t, char *realname)
{
  mode_t mode;
  char *filename;
  char buf[T_BLOCKSIZE];
  char *pathname = 0;

  if (!TH_ISFIFO(t))
  {
    errno = EINVAL;
    return -1;
  }

  if (realname)
    {
    filename = realname;
    }
  else
    {
    pathname = th_get_pathname(t);
    filename = pathname;
    }
  mode = th_get_mode(t);

  /* Make a copy of the string because dirname and mkdirhier may modify the
   * string */
  strncpy(buf, filename, sizeof(buf)-1);
  buf[sizeof(buf)-1] = 0;

  if (mkdirhier(dirname(buf)) == -1)
    {
    if (pathname)
      {
      free(pathname);
      }
    return -1;
    }

#ifdef DEBUG
  printf("  ==> extracting: %s (fifo)\n", filename);
#endif
#ifndef WIN32
  if (mkfifo(filename, mode & 07777) == -1)
#else
    (void)mode;
#endif
  {
#ifdef DEBUG
    perror("mkfifo()");
#endif
    if (pathname)
      {
      free(pathname);
      }
    return -1;
  }

#ifndef WIN32
  if (pathname)
    {
    free(pathname);
    }
  return 0;
#endif
}