/*
 * dpkg - main program for package management
 * archives.c - actions that process archive files, mainly unpack
 *
 * Copyright (C) 1994,1995 Ian Jackson <iwj10@cus.cam.ac.uk>
 *
 * 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,
 * 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 dpkg; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <utime.h>
#include <assert.h>
#include <time.h>
#include <ctype.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <sys/types.h>

#include "config.h"
#include "dpkg.h"
#include "dpkg-db.h"
#include "myopt.h"
#include "tarfn.h"

#include "filesdb.h"
#include "main.h"
#include "archives.h"

void cu_pathname(int argc, void **argv) {
  ensure_pathname_nonexisting((char*)(argv[0]));
} 

void cu_backendpipe(int argc, void **argv) {
  FILE *f= *(FILE**)argv[0];
  if (f) fclose(f);
} 

int tarfileread(void *ud, char *buf, int len) {
  struct tarcontext *tc= (struct tarcontext*)ud;
  int r;
  r= fread(buf,1,len,tc->backendpipe);
  if (r != len && ferror(tc->backendpipe))
    ohshite("error reading from " BACKEND " pipe");
  return r;
}

int fnameidlu;
struct varbuf fnamevb;
struct varbuf fnametmpvb;
struct varbuf fnamenewvb;
struct packageinlist *deconfigure= 0;

static time_t currenttime;

static void newtarobject_utime(struct TarInfo *ti) {
  struct utimbuf utb;
  utb.actime= currenttime;
  utb.modtime= ti->ModTime;
  if (utime(fnamenewvb.buf,&utb))
    ohshite("error setting timestamps of `%.255s'",ti->Name);
}

static void newtarobject_allmodes(const char *path, struct TarInfo *ti) {
  if (chown(path,ti->UserID,ti->GroupID))
    ohshite("error setting ownership of `%.255s'",ti->Name);
  if (chmod(path,ti->Mode & ~S_IFMT))
    ohshite("error setting permissions of `%.255s'",ti->Name);
  newtarobject_utime(ti);
}

void setupfnamevbs(const char *filename) {
  fnamevb.used= fnameidlu;
  varbufaddstr(&fnamevb,filename);
  varbufaddc(&fnamevb,0);

  fnametmpvb.used= fnameidlu;
  varbufaddstr(&fnametmpvb,filename);
  varbufaddstr(&fnametmpvb,DPKGTEMPEXT);
  varbufaddc(&fnametmpvb,0);

  fnamenewvb.used= fnameidlu;
  varbufaddstr(&fnamenewvb,filename);
  varbufaddstr(&fnamenewvb,DPKGNEWEXT);
  varbufaddc(&fnamenewvb,0);

  debug(dbg_eachfiledetail, "setupvnamevbs main=`%s' tmp=`%s' new=`%s'",
        fnamevb.buf, fnametmpvb.buf, fnamenewvb.buf);
}

int unlinkorrmdir(const char *filename) {
  /* Returns 0 on success or -1 on failure, just like unlink & rmdir */
  int r, e;
  
  if (!rmdir(filename)) {
    debug(dbg_eachfiledetail,"unlinkorrmdir `%s' rmdir OK",filename);
    return 0;
  }
  
  if (errno != ENOTDIR) {
    e= errno;
    debug(dbg_eachfiledetail,"unlinkorrmdir `%s' rmdir %s",filename,strerror(e));
    errno= e; return -1;
  }
  
  r= unlink(filename); e= errno;
  debug(dbg_eachfiledetail,"unlinkorrmdir `%s' unlink %s",
        filename, r ? strerror(e) : "OK");
  errno= e; return r;
}

int tarobject(struct TarInfo *ti) {
  static struct varbuf conffderefn, hardlinkfn;
  const char *usename;
    
  struct tarcontext *tc= (struct tarcontext*)ti->UserData;
  int statr, fd, r;
  struct stat stab, stabd;
  size_t sz, wsz;
  FILE *thefile;
  char databuf[TARBLKSZ];
  struct fileinlist *nifd;

  /* Append to list of files.
   * The trailing / put on the end of names in tarfiles has already
   * been stripped by TarExtractor (lib/tarfn.c).
   */
  nifd= m_malloc(sizeof(struct fileinlist));
  nifd->namenode= findnamenode(ti->Name);
  nifd->next= 0; *tc->newfilesp= nifd; tc->newfilesp= &nifd->next;
  nifd->namenode->flags |= fnnf_new_inarchive;

  debug(dbg_eachfile,
        "tarobject ti->Name=`%s' Mode=%lo owner=%u.%u Type=%d(%c) "
        "          ti->LinkName=`%s'    namenode=`%s' flags=%o",
        ti->Name, (long)ti->Mode, (unsigned)ti->UserID, (unsigned)ti->GroupID, ti->Type,
        ti->Type == '\0' ? '_' :
        ti->Type >= '0' && ti->Type <= '6' ? "-hlcbdp"[ti->Type - '0'] : '?',
        ti->LinkName,
        nifd->namenode->name, nifd->namenode->flags);
        
  usename= ti->Name;
  if (nifd->namenode->flags & fnnf_new_conff) {
    /* If it's a conffile we have to extract it next to the installed
     * version (ie, we do the usual link-following).
     */
    if (conffderef(tc->pkg, &conffderefn, usename))
      usename= conffderefn.buf;
    debug(dbg_conff,"tarobject fnnf_new_conff deref=`%s'",usename);
  }
  
  setupfnamevbs(usename);

  statr= lstat(fnamevb.buf,&stab);
  if (statr) {
    /* The lstat failed. */
    if (errno != ENOENT && errno != ENOTDIR)
      ohshite("unable to stat `%.255s' (which I was about to install)",ti->Name);
    /* OK, so it doesn't exist.
     * However, it's possible that we were in the middle of some other
     * backup/restore operation and were rudely interrupted.
     * So, we see if we have .dpkg-tmp, and if so we restore it.
     */
    if (rename(fnametmpvb.buf,fnamevb.buf)) {
      if (errno != ENOENT && errno != ENOTDIR)
        ohshite("unable to clean up mess surrounding `%.255s' before "
                "installing another version",ti->Name);
      debug(dbg_eachfiledetail,"tarobject nonexistent");
    } else {
      debug(dbg_eachfiledetail,"tarobject restored tmp to main");
      statr= lstat(fnamevb.buf,&stab);
      if (statr) ohshite("unable to stat restored `%.255s' before installing"
                         " another version", ti->Name);
    }
  } else {
    debug(dbg_eachfiledetail,"tarobject already exists");
  }
  /* Now, at this stage we want to make sure neither of .dpkg-new and .dpkg-tmp
   * are hanging around.
   */
  ensure_pathname_nonexisting(fnamenewvb.buf);
  ensure_pathname_nonexisting(fnametmpvb.buf);

  /* Now we start to do things that we need to be able to undo
   * if something goes wrong.
   */
  push_cleanup(cu_installnew,~ehflag_normaltidy, 0,0, 1,(void*)nifd);

  /* Extract whatever it is as .dpkg-new ... */
  switch (ti->Type) {
  case NormalFile0: case NormalFile1:
    fd= open(fnamenewvb.buf, O_CREAT|O_EXCL|O_WRONLY,
             ti->Mode & (S_IRUSR|S_IRGRP|S_IROTH));
    if (fd < 0) ohshite("unable to create `%.255s'",ti->Name);
    thefile= fdopen(fd,"w");
    if (!thefile) { close(fd); ohshite("unable to fdopen for `%.255s'",ti->Name); }
    push_cleanup(cu_closefile,ehflag_bombout, 0,0, 1,(void*)thefile);
    debug(dbg_eachfiledetail,"tarobject NormalFile[01] open size=%lu",
          (unsigned long)ti->Size);
    for (sz= ti->Size; sz > 0; sz -= wsz) {
      wsz= sz > TARBLKSZ ? TARBLKSZ : sz;
      r= fread(databuf,1,TARBLKSZ,tc->backendpipe);
      if (r<TARBLKSZ)
        if (ferror(tc->backendpipe))
          ohshite("error reading " BACKEND " during `%.255s'",ti->Name);
        else
          ohshit("unexpected EOF from " BACKEND " in `%.255s'",ti->Name);
      if (fwrite(databuf,1,wsz,thefile) != wsz)
        ohshite("error writing to `%.255s'",ti->Name);
    }
    if (fchown(fd,ti->UserID,ti->GroupID))
      ohshite("error setting ownership of `%.255s'",ti->Name);
    if (fchmod(fd,ti->Mode & ~S_IFMT))
      ohshite("error setting permissions of `%.255s'",ti->Name);
    pop_cleanup(ehflag_normaltidy); /* thefile= fdopen(fd) */
    if (fclose(thefile))
      ohshite("error closing/writing `%.255s'",ti->Name);
    newtarobject_utime(ti);
    break;
  case CharacterDevice: case BlockDevice:
    if (mknod(fnamenewvb.buf,ti->Mode & S_IFMT,ti->Device))
      ohshite("error creating device `%.255s'",ti->Name);
    debug(dbg_eachfiledetail,"tarobject CharacterDevice|BlockDevice");
    newtarobject_allmodes(fnamenewvb.buf,ti);
    break;
  case HardLink:
    varbufreset(&hardlinkfn);
    varbufaddstr(&hardlinkfn,instdir); varbufaddc(&hardlinkfn,'/');
    varbufaddstr(&hardlinkfn,ti->LinkName); varbufaddc(&hardlinkfn,0);
    if (link(hardlinkfn.buf,fnamenewvb.buf))
      ohshite("error creating hard link `%.255s'",ti->Name);
    debug(dbg_eachfiledetail,"tarobject HardLink");
    newtarobject_allmodes(fnamenewvb.buf,ti);
    break;
  case SymbolicLink:
    /* If it's already an existing directory, do nothing. */
    if (!statr && S_ISDIR(stab.st_mode)) {
      debug(dbg_eachfiledetail,"tarobject SymbolicLink exists as directory");
      return 0;
    }
    if (symlink(ti->LinkName,fnamenewvb.buf))
      ohshite("error creating symbolic link `%.255s'",ti->Name);
    debug(dbg_eachfiledetail,"tarobject SymbolicLink creating");
    if (chown(fnamenewvb.buf,ti->UserID,ti->GroupID))
      ohshite("error setting ownership of symlink `%.255s'",ti->Name);
    break;
  case Directory:
    /* If it's already an existing directory, do nothing. */
    if (!stat(fnamevb.buf,&stabd) && S_ISDIR(stabd.st_mode)) {
      debug(dbg_eachfiledetail,"tarobject Directory exists");
      return 0;
    }
    if (mkdir(fnamenewvb.buf,
              ti->Mode & (S_IRUSR|S_IRGRP|S_IROTH | S_IXUSR|S_IXGRP|S_IXOTH)))
      ohshite("error creating directory `%.255s'",ti->Name);
    debug(dbg_eachfiledetail,"tarobject Directory creating");
    newtarobject_allmodes(fnamenewvb.buf,ti);
    break;
  default:
    ohshit("archive contained object `%.255s' of unknown type 0x%x",ti->Name,ti->Type);
  }
  /*
   * Now we have extracted the new object in .dpkg-new (or, if the
   * file already exists as a directory and we were trying to extract
   * a directory or symlink, we returned earlier, so we don't need
   * to worry about that here).
   */

  /* First, check to see if it's a conffile.  If so we don't install
   * it now - we leave it in .dpkg-new for --configure to take care of
   */
  if (nifd->namenode->flags & fnnf_new_conff) {
    debug(dbg_conffdetail,"tarobject conffile extracted");
    return 0;
  }

  /* Now we install it.  If we can do an atomic overwrite we do so.
   * If not we move aside the old file and then install the new.
   * The backup file will be deleted later.
   */
  if (statr) { /* Don't try to back it up if it didn't exist. */
    debug(dbg_eachfiledetail,"tarobject new - no backup");
  } else {
    if (ti->Type == Directory || ti->Type == SymbolicLink ||
        S_ISDIR(stab.st_mode) || S_ISLNK(stab.st_mode)) {
      debug(dbg_eachfiledetail,"tarobject directory, nonatomic");
      /* One of the two is a directory or a symlink - can't do atomic install. */
      nifd->namenode->flags |= fnnf_no_atomic_overwrite;
      if (rename(fnamevb.buf,fnametmpvb.buf))
        ohshite("unable to move aside `%.255s' to install new version",ti->Name);
    } else {
      debug(dbg_eachfiledetail,"tarobject nondirectory, `link' backup");
      if (link(fnamevb.buf,fnametmpvb.buf))
        ohshite("unable to make backup link of `%.255s' before installing new version",
                ti->Name);
    }
  }

  if (rename(fnamenewvb.buf,fnamevb.buf))
    ohshite("unable to install new version of `%.255s'",ti->Name);

  nifd->namenode->flags |= fnnf_elide_other_lists;

  debug(dbg_eachfiledetail,"tarobject done and installed");
  return 0;
}

static int try_remove_can(struct deppossi *pdep,
                          struct pkginfo *fixbyrm,
                          const char *why) {
  struct packageinlist *newdeconf;
  
  if (force_depends(pdep)) {
    fprintf(stderr, DPKG ": warning - "
            "ignoring dependency problem with removal of %s:\n%s",
            fixbyrm->name, why);
    return 1;
  } else if (f_autodeconf) {
    pdep->up->up->clientdata->istobe= itb_deconfigure;
    newdeconf= m_malloc(sizeof(struct packageinlist));
    newdeconf->next= deconfigure;
    newdeconf->pkg= pdep->up->up;
    deconfigure= newdeconf;
    return 1;
  } else {
    fprintf(stderr, DPKG ": no, cannot remove %s:\n%s",
            fixbyrm->name, why);
    return 0;
  }
}

void check_conflict(struct dependency *dep, struct pkginfo *pkg,
                    const char *pfilename, struct pkginfo **conflictorp) {
  struct pkginfo *fixbyrm;
  struct deppossi *pdep, flagdeppossi;
  struct varbuf conflictwhy, removalwhy;
  struct dependency *providecheck;
  
  varbufinit(&conflictwhy);
  varbufinit(&removalwhy);

  fixbyrm= 0;
  if (depisok(dep, &conflictwhy, *conflictorp ? 0 : &fixbyrm)) {
    varbuffree(&conflictwhy);
    varbuffree(&removalwhy);
    return;
  }
  if (fixbyrm &&
      ((pkg->available.essential && fixbyrm->installed.essential) ||
       (fixbyrm->want != want_install &&
        (!fixbyrm->installed.essential || fc_removeessential)))) {
    ensure_package_clientdata(fixbyrm);
    assert(fixbyrm->clientdata->istobe == itb_normal);
    fixbyrm->clientdata->istobe= itb_remove;
    fprintf(stderr, DPKG ": considering removing %s in favour of %s ...\n",
            fixbyrm->name, pkg->name);
    if (fixbyrm->status != stat_installed) {
      fprintf(stderr,
              "%s is not properly installed - ignoring any dependencies on it.\n",
              fixbyrm->name);
      pdep= 0;
    } else {
      for (pdep= fixbyrm->installed.depended;
           pdep;
           pdep= pdep->nextrev) {
        if (pdep->up->type != dep_depends) continue;
        if (depisok(pdep->up, &removalwhy, 0)) continue;
        varbufaddc(&removalwhy,0);
        if (!try_remove_can(pdep,fixbyrm,removalwhy.buf))
          break;
      }
      if (!pdep) {
        /* If we haven't found a reason not to yet, let's look some more. */
        for (providecheck= fixbyrm->installed.depends;
             providecheck;
             providecheck= providecheck->next) {
          if (providecheck->type != dep_provides) continue;
          for (pdep= providecheck->list->ed->installed.depended;
               pdep;
               pdep= pdep->nextrev) {
            if (pdep->up->type != dep_depends) continue;
            if (depisok(pdep->up, &removalwhy, 0)) continue;
            varbufaddc(&removalwhy,0);
            fprintf(stderr, DPKG
                    ": may have trouble removing %s, as it provides %s ...\n",
                    fixbyrm->name, providecheck->list->ed->name);
            if (!try_remove_can(pdep,fixbyrm,removalwhy.buf))
              goto break_from_both_loops_at_once;
          }
        }
      break_from_both_loops_at_once:;
      }
    }
    if (!pdep && skip_due_to_hold(fixbyrm)) {
      pdep= &flagdeppossi;
    }
    if (!pdep && (fixbyrm->eflag & eflagf_reinstreq)) {
      if (fc_removereinstreq) {
        fprintf(stderr, DPKG ": package %s requires reinstallation, but will"
                " remove anyway as you request.\n", fixbyrm->name);
      } else {
        fprintf(stderr, DPKG ": package %s requires reinstallation, will not remove.\n",
                fixbyrm->name);
        pdep= &flagdeppossi;
      }
    }
    if (!pdep) {
      /* This conflict is OK - we'll remove the conflictor. */
      *conflictorp= fixbyrm;
      varbuffree(&conflictwhy); varbuffree(&removalwhy);
      fprintf(stderr, DPKG ": yes, will remove %s in favour of %s.\n",
              fixbyrm->name, pkg->name);
      return;
    }
    fixbyrm->clientdata->istobe= itb_normal; /* put it back */
  }
  varbufaddc(&conflictwhy,0);
  fprintf(stderr, DPKG ": regarding %s containing %s:\n%s",
          pfilename, pkg->name, conflictwhy.buf);
  if (!force_conflicts(dep->list))
    ohshit("conflicting packages - not installing %.250s",pkg->name);
  fprintf(stderr, DPKG ": warning - ignoring conflict, may proceed anyway !\n");
  varbuffree(&conflictwhy);
  
  return;
}

void cu_cidir(int argc, void **argv) {
  char *cidir= (char*)argv[0];
  char *cidirrest= (char*)argv[1];
  *cidirrest= 0;
  ensure_pathname_nonexisting(cidir);
}  

void cu_fileslist(int argc, void **argv) {
  struct fileinlist **headp= (struct fileinlist**)argv[0];
  struct fileinlist *current, *next;
  for (current= *headp; current; current= next) {
    next= current->next;
    free(current);
  }
}  

void archivefiles(const char *const *argv) {
  const char *volatile thisarg;
  const char *const *volatile argp;
  jmp_buf ejbuf;
  int pi[2], fc, nfiles, c, i;
  FILE *pf;
  static struct varbuf findoutput;
  const char **arglist;
  char *p;

  if (f_recursive) {
    
    if (!*argv)
      badusage("--%s --recursive needs at least one path argument",cipaction->olong);
    
    m_pipe(pi);
    if (!(fc= m_fork())) {
      const char *const *ap;
      int i;
      m_dup2(pi[1],1); close(pi[0]);
      for (i=0, ap=argv; *ap; ap++, i++);
      arglist= m_malloc(sizeof(char*)*(i+10));
      arglist[0]= FIND;
      for (i=1, ap=argv; *ap; ap++, i++) {
        if (strchr(FIND_EXPRSTARTCHARS,(*ap)[0])) {
          char *a;
          a= m_malloc(strlen(*ap)+10);
          strcpy(a,"./");
          strcat(a,*ap);
          arglist[i]= a;
        } else {
          arglist[i]= *ap;
        }
      }
      arglist[i++]= "-name";
      arglist[i++]= ARCHIVE_FILENAME_PATTERN;
      arglist[i++]= "-type";
      arglist[i++]= "f";
      arglist[i++]= "-print0";
      arglist[i++]= 0;
      execvp(FIND, (char* const*)arglist);
      ohshite("failed to exec " FIND " for --recursive");
    }

    nfiles= 0;
    pf= fdopen(pi[0],"r");  if (!pf) ohshite("failed to fdopen find's pipe");
    close(pi[1]);
    varbufreset(&findoutput);
    while ((c= fgetc(pf)) != EOF) {
      varbufaddc(&findoutput,c);
      if (!c) nfiles++;
    }
    if (ferror(pf)) ohshite("error reading find's pipe");
    if (fclose(pf)) ohshite("error closing find's pipe");
    waitsubproc(fc,"find",0);

    if (!nfiles) ohshit("searched, but found no packages (files matching "
                        ARCHIVE_FILENAME_PATTERN ")");

    varbufaddc(&findoutput,0);
    varbufaddc(&findoutput,0);
    
    arglist= m_malloc(sizeof(char*)*(nfiles+1));
    p= findoutput.buf; i=0;
    while (*p) {
      arglist[i++]= p;
      while ((c= *p++) != 0);
    }
    arglist[i]= 0;
    argp= arglist;

  } else {

    if (!*argv) badusage("--%s needs at least one package archive file argument",
                         cipaction->olong);
    argp= argv;
    
  }

  modstatdb_init(admindir,
                 f_noact ?                     msdbrw_readonly
               : cipaction->arg == act_avail ? msdbrw_write
               :                               msdbrw_needsuperuser);

  currenttime= time(0);

  varbufaddstr(&fnamevb,instdir); varbufaddc(&fnamevb,'/');
  varbufaddstr(&fnametmpvb,instdir); varbufaddc(&fnametmpvb,'/');
  varbufaddstr(&fnamenewvb,instdir); varbufaddc(&fnamenewvb,'/');
  fnameidlu= fnamevb.used;
  
  while ((thisarg= *argp++) != 0) {
    if (setjmp(ejbuf)) {
      error_unwind(ehflag_bombout);
      if (onerr_abort > 0) break;
      continue;
    }
    push_error_handler(&ejbuf,print_error_perpackage,thisarg);
    process_archive(thisarg);
    onerr_abort++;
    if (ferror(stdout)) werr("stdout");
    if (ferror(stderr)) werr("stderr");
    onerr_abort--;
    set_error_display(0,0);
    error_unwind(ehflag_normaltidy);
  }

  switch (cipaction->arg) {
  case act_install:
  case act_configure:
  case act_remove:
  case act_purge:
    process_queue();
  case act_unpack:
  case act_avail:
    break;
  default:
    internerr("unknown action");
  }

  modstatdb_shutdown();
}
