/*
 * dpkg-deb - construction and deconstruction of *.deb archives
 * extract.c - extracting archives
 *
 * Copyright © 1994,1995 Ian Jackson <ijackson@chiark.greenend.org.uk>
 * Copyright © 2006-2014 Guillem Jover <guillem@debian.org>
 *
 * This 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 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, see <https://www.gnu.org/licenses/>.
 */

#include <config.h>
#include <compat.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>

#include <errno.h>
#include <limits.h>
#include <string.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <ar.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>

#include <dpkg/i18n.h>
#include <dpkg/dpkg.h>
#include <dpkg/fdio.h>
#include <dpkg/buffer.h>
#include <dpkg/subproc.h>
#include <dpkg/command.h>
#include <dpkg/compress.h>
#include <dpkg/ar.h>
#include <dpkg/deb-version.h>
#include <dpkg/options.h>

#include "dpkg-deb.h"

static void movecontrolfiles(const char *thing) {
  char buf[200];
  pid_t pid;

  sprintf(buf, "mv %s/* . && rmdir %s", thing, thing);
  pid = subproc_fork();
  if (pid == 0) {
    command_shell(buf, _("shell command to move files"));
  }
  subproc_reap(pid, _("shell command to move files"), 0);
}

static void DPKG_ATTR_NORET
read_fail(int rc, const char *filename, const char *what)
{
  if (rc >= 0)
    ohshit(_("unexpected end of file in %s in %.255s"),what,filename);
  else
    ohshite(_("error reading %s from file %.255s"), what, filename);
}

static ssize_t
read_line(int fd, char *buf, size_t min_size, size_t max_size)
{
  ssize_t line_size = 0;
  size_t n = min_size;

  while (line_size < (ssize_t)max_size) {
    ssize_t r;
    char *nl;

    r = fd_read(fd, buf + line_size, n);
    if (r <= 0)
      return r;

    nl = memchr(buf + line_size, '\n', r);
    line_size += r;

    if (nl != NULL) {
      nl[1] = '\0';
      return line_size;
    }

    n = 1;
  }

  buf[line_size] = '\0';
  return line_size;
}

void
extracthalf(const char *debar, const char *dir,
            enum dpkg_tar_options taroption, int admininfo)
{
  struct dpkg_error err;
  const char *errstr;
  struct dpkg_ar *ar;
  char versionbuf[40];
  struct deb_version version;
  off_t ctrllennum, memberlen = 0;
  ssize_t r;
  int dummy;
  pid_t c1=0,c2,c3;
  int p1[2], p2[2];
  int p2_out;
  char nlc;
  int adminmember = -1;
  bool header_done;
  enum compressor_type decompressor = COMPRESSOR_TYPE_GZIP;

  ar = dpkg_ar_open(debar);

  r = read_line(ar->fd, versionbuf, strlen(DPKG_AR_MAGIC), sizeof(versionbuf) - 1);
  if (r < 0)
    read_fail(r, debar, _("archive magic version number"));

  if (strcmp(versionbuf, DPKG_AR_MAGIC) == 0) {
    ctrllennum= 0;
    header_done = false;
    for (;;) {
      struct dpkg_ar_hdr arh;

      r = fd_read(ar->fd, &arh, sizeof(arh));
      if (r != sizeof(arh))
        read_fail(r, debar, _("archive member header"));

      dpkg_ar_normalize_name(&arh);

      if (dpkg_ar_member_is_illegal(&arh))
        ohshit(_("file '%.250s' is corrupt - bad archive header magic"), debar);
      memberlen = dpkg_ar_member_get_size(ar, &arh);
      if (!header_done) {
        char *infobuf;

        if (strncmp(arh.ar_name, DEBMAGIC, sizeof(arh.ar_name)) != 0)
          ohshit(_("file '%.250s' is not a Debian binary archive (try dpkg-split?)"),
                 debar);
        infobuf= m_malloc(memberlen+1);
        r = fd_read(ar->fd, infobuf, memberlen + (memberlen & 1));
        if (r != (memberlen + (memberlen & 1)))
          read_fail(r, debar, _("archive information header member"));
        infobuf[memberlen] = '\0';

        if (strchr(infobuf, '\n') == NULL)
          ohshit(_("archive has no newlines in header"));
        errstr = deb_version_parse(&version, infobuf);
        if (errstr)
          ohshit(_("archive has invalid format version: %s"), errstr);
        if (version.major != 2)
          ohshit(_("archive is format version %d.%d; get a newer dpkg-deb"),
                 version.major, version.minor);

        free(infobuf);

        header_done = true;
      } else if (arh.ar_name[0] == '_') {
        /* Members with ‘_’ are noncritical, and if we don't understand
         * them we skip them. */
        if (fd_skip(ar->fd, memberlen + (memberlen & 1), &err) < 0)
          ohshit(_("cannot skip archive member from '%s': %s"), ar->name, err.str);
      } else {
        if (strncmp(arh.ar_name, ADMINMEMBER, strlen(ADMINMEMBER)) == 0) {
          const char *extension = arh.ar_name + strlen(ADMINMEMBER);

	  adminmember = 1;
          decompressor = compressor_find_by_extension(extension);
          if (decompressor != COMPRESSOR_TYPE_NONE &&
              decompressor != COMPRESSOR_TYPE_GZIP &&
              decompressor != COMPRESSOR_TYPE_ZSTD &&
              decompressor != COMPRESSOR_TYPE_XZ)
            ohshit(_("archive '%s' uses unknown compression for member '%.*s', "
                     "giving up"),
                   debar, (int)sizeof(arh.ar_name), arh.ar_name);
        } else {
          if (adminmember != 1)
            ohshit(_("archive '%s' has premature member '%.*s' before '%s', "
                     "giving up"),
                   debar, (int)sizeof(arh.ar_name), arh.ar_name, ADMINMEMBER);

	  if (strncmp(arh.ar_name, DATAMEMBER, strlen(DATAMEMBER)) == 0) {
	    const char *extension = arh.ar_name + strlen(DATAMEMBER);

	    adminmember= 0;
	    decompressor = compressor_find_by_extension(extension);
            if (decompressor == COMPRESSOR_TYPE_UNKNOWN)
              ohshit(_("archive '%s' uses unknown compression for member '%.*s', "
                       "giving up"),
                     debar, (int)sizeof(arh.ar_name), arh.ar_name);
          } else {
            ohshit(_("archive '%s' has premature member '%.*s' before '%s', "
                     "giving up"),
                   debar, (int)sizeof(arh.ar_name), arh.ar_name, DATAMEMBER);
          }
        }
        if (adminmember == 1) {
          if (ctrllennum != 0)
            ohshit(_("archive '%.250s' contains two control members, giving up"),
                   debar);
          ctrllennum= memberlen;
        }
        if (!adminmember != !admininfo) {
          if (fd_skip(ar->fd, memberlen + (memberlen & 1), &err) < 0)
            ohshit(_("cannot skip archive member from '%s': %s"), ar->name, err.str);
        } else {
          /* Yes! - found it. */
          break;
        }
      }
    }

    if (admininfo >= 2) {
      printf(_(" new Debian package, version %d.%d.\n"
               " size %jd bytes: control archive=%jd bytes.\n"),
             version.major, version.minor,
             (intmax_t)ar->size, (intmax_t)ctrllennum);
      m_output(stdout, _("<standard output>"));
    }
  } else if (strncmp(versionbuf, "0.93", 4) == 0) {
    char ctrllenbuf[40];
    int l;

    l = strlen(versionbuf);

    if (strchr(versionbuf, '\n') == NULL)
      ohshit(_("archive has no newlines in header"));
    errstr = deb_version_parse(&version, versionbuf);
    if (errstr)
      ohshit(_("archive has invalid format version: %s"), errstr);

    r = read_line(ar->fd, ctrllenbuf, 1, sizeof(ctrllenbuf) - 1);
    if (r < 0)
      read_fail(r, debar, _("archive control member size"));
    if (sscanf(ctrllenbuf, "%jd%c%d", (intmax_t *)&ctrllennum, &nlc, &dummy) != 2 ||
        nlc != '\n')
      ohshit(_("archive has malformed control member size '%s'"), ctrllenbuf);

    if (admininfo) {
      memberlen = ctrllennum;
    } else {
      memberlen = ar->size - ctrllennum - strlen(ctrllenbuf) - l;
      if (fd_skip(ar->fd, ctrllennum, &err) < 0)
        ohshit(_("cannot skip archive control member from '%s': %s"), ar->name,
               err.str);
    }

    if (admininfo >= 2) {
      printf(_(" old Debian package, version %d.%d.\n"
               " size %jd bytes: control archive=%jd, main archive=%jd.\n"),
             version.major, version.minor,
             (intmax_t)ar->size, (intmax_t)ctrllennum,
             (intmax_t)(ar->size - ctrllennum - strlen(ctrllenbuf) - l));
      m_output(stdout, _("<standard output>"));
    }
  } else {
    if (strncmp(versionbuf, "!<arch>", 7) == 0) {
      notice(_("file looks like it might be an archive which has been\n"
               " corrupted by being downloaded in ASCII mode"));
    }

    ohshit(_("'%.255s' is not a Debian format archive"), debar);
  }

  m_pipe(p1);
  c1 = subproc_fork();
  if (!c1) {
    close(p1[0]);
    if (fd_fd_copy(ar->fd, p1[1], memberlen, &err) < 0)
      ohshit(_("cannot copy archive member from '%s' to decompressor pipe: %s"),
             ar->name, err.str);
    if (close(p1[1]))
      ohshite(_("cannot close decompressor pipe"));
    exit(0);
  }
  close(p1[1]);

  if (taroption) {
    m_pipe(p2);
    p2_out = p2[1];
  } else {
    p2_out = 1;
  }

  c2 = subproc_fork();
  if (!c2) {
    if (taroption)
      close(p2[0]);
    decompress_filter(decompressor, p1[0], p2_out,
                      _("decompressing archive member"));
    exit(0);
  }
  close(p1[0]);
  dpkg_ar_close(ar);
  if (taroption) close(p2[1]);

  if (taroption) {
    c3 = subproc_fork();
    if (!c3) {
      struct command cmd;

      command_init(&cmd, TAR, "tar");
      command_add_arg(&cmd, "tar");

      if ((taroption & DPKG_TAR_LIST) && (taroption & DPKG_TAR_EXTRACT))
        command_add_arg(&cmd, "-xv");
      else if (taroption & DPKG_TAR_EXTRACT)
        command_add_arg(&cmd, "-x");
      else if (taroption & DPKG_TAR_LIST)
        command_add_arg(&cmd, "-tv");
      else
        internerr("unknown or missing tar action '%d'", taroption);

      if (taroption & DPKG_TAR_PERMS)
        command_add_arg(&cmd, "-p");
      if (taroption & DPKG_TAR_NOMTIME)
        command_add_arg(&cmd, "-m");

      command_add_arg(&cmd, "-f");
      command_add_arg(&cmd, "-");
      command_add_arg(&cmd, "--warning=no-timestamp");

      m_dup2(p2[0],0);
      close(p2[0]);

      unsetenv("TAR_OPTIONS");

      if (dir) {
        if (mkdir(dir, 0777) != 0) {
          if (errno != EEXIST)
            ohshite(_("failed to create directory"));

          if (taroption & DPKG_TAR_CREATE_DIR)
            ohshite(_("unexpected pre-existing pathname %s"), dir);
        }
        if (chdir(dir) != 0)
          ohshite(_("failed to chdir to directory"));
      }

      command_exec(&cmd);
    }
    close(p2[0]);
    subproc_reap(c3, "tar", 0);
  }

  subproc_reap(c2, _("<decompress>"), SUBPROC_NOPIPE);
  if (c1 != -1)
    subproc_reap(c1, _("paste"), 0);
  if (version.major == 0 && admininfo) {
    /* Handle the version as a float to preserve the behaviour of old code,
     * because even if the format is defined to be padded by 0's that might
     * not have been always true for really ancient versions... */
    while (version.minor && (version.minor % 10) == 0)
      version.minor /= 10;

    if (version.minor ==  931)
      movecontrolfiles(OLDOLDDEBDIR);
    else if (version.minor == 932 || version.minor == 933)
      movecontrolfiles(OLDDEBDIR);
  }
}

int
do_ctrltarfile(const char *const *argv)
{
  const char *debar;

  debar = *argv++;
  if (debar == NULL)
    badusage(_("--%s needs a .deb filename argument"), cipaction->olong);
  if (*argv)
    badusage(_("--%s takes only one argument (.deb filename)"),
             cipaction->olong);

  extracthalf(debar, NULL, DPKG_TAR_PASSTHROUGH, 1);

  return 0;
}

int
do_fsystarfile(const char *const *argv)
{
  const char *debar;

  debar = *argv++;
  if (debar == NULL)
    badusage(_("--%s needs a .deb filename argument"),cipaction->olong);
  if (*argv)
    badusage(_("--%s takes only one argument (.deb filename)"),cipaction->olong);
  extracthalf(debar, NULL, DPKG_TAR_PASSTHROUGH, 0);

  return 0;
}

int
do_control(const char *const *argv)
{
  const char *debar, *dir;

  debar = *argv++;
  if (debar == NULL)
    badusage(_("--%s needs a .deb filename argument"), cipaction->olong);

  dir = *argv++;
  if (dir == NULL)
    dir = EXTRACTCONTROLDIR;
  else if (*argv)
    badusage(_("--%s takes at most two arguments (.deb and directory)"),
             cipaction->olong);

  extracthalf(debar, dir, DPKG_TAR_EXTRACT, 1);

  return 0;
}

int
do_extract(const char *const *argv)
{
  const char *debar, *dir;
  enum dpkg_tar_options options = DPKG_TAR_EXTRACT | DPKG_TAR_PERMS;

  if (opt_verbose)
    options |= DPKG_TAR_LIST;

  debar = *argv++;
  if (debar == NULL)
    badusage(_("--%s needs .deb filename and directory arguments"),
             cipaction->olong);

  dir = *argv++;
  if (dir == NULL)
    badusage(_("--%s needs a target directory.\n"
               "Perhaps you should be using dpkg --install ?"),
             cipaction->olong);
  else if (*argv)
    badusage(_("--%s takes at most two arguments (.deb and directory)"),
             cipaction->olong);

  extracthalf(debar, dir, options, 0);

  return 0;
}

int
do_vextract(const char *const *argv)
{
  /* XXX: Backward compatibility. */
  opt_verbose = 1;
  return do_extract(argv);
}

int
do_raw_extract(const char *const *argv)
{
  enum dpkg_tar_options data_options;
  const char *debar, *dir;
  char *control_dir;

  debar = *argv++;
  if (debar == NULL)
    badusage(_("--%s needs .deb filename and directory arguments"),
             cipaction->olong);
  else if (strcmp(debar, "-") == 0)
    badusage(_("--%s does not support (yet) reading the .deb from standard input"),
             cipaction->olong);

  dir = *argv++;
  if (dir == NULL)
    badusage(_("--%s needs a target directory.\n"
               "Perhaps you should be using dpkg --install ?"),
             cipaction->olong);
  else if (*argv)
    badusage(_("--%s takes at most two arguments (.deb and directory)"),
             cipaction->olong);

  control_dir = str_fmt("%s/%s", dir, EXTRACTCONTROLDIR);

  data_options = DPKG_TAR_EXTRACT | DPKG_TAR_PERMS;
  if (opt_verbose)
    data_options |= DPKG_TAR_LIST;

  extracthalf(debar, dir, data_options, 0);
  extracthalf(debar, control_dir, DPKG_TAR_EXTRACT | DPKG_TAR_CREATE_DIR, 1);

  free(control_dir);

  return 0;
}
