/*
 * dpkg - main program for package management
 * filesdb.c - management of database of files installed on system
 *
 * Copyright (C) 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 <assert.h>
#include <unistd.h>

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

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

#define BINS (1 << 13)
 /* This must always be a power of two.  If you change it
  * consider changing the per-character hashing factor (currently
  * 1785 = 137*13) too.
  */

static struct filenamenode *bins[BINS];
static int allpackagesdone= 0;
static int nfiles= 0;

static int hash(const char *name) {
  int v= 0;
  while (*name) { v *= 1785; v += *name; name++; }
  return v;
}

struct fileiterator {
  struct filenamenode *namenode;
  int nbinn;
};

struct fileiterator *iterfilestart(void) {
  struct fileiterator *i;
  i= m_malloc(sizeof(struct fileiterator));
  i->namenode= 0;
  i->nbinn= 0;
  return i;
}

struct filenamenode *iterfilenext(struct fileiterator *i) {
  struct filenamenode *r;
  while (!i->namenode) {
    if (i->nbinn >= BINS) return 0;
    i->namenode= bins[i->nbinn++];
  }
  r= i->namenode; i->namenode= r->next; return r;
}

void iterfileend(struct fileiterator *i) {
  free(i);
}

void zero_filesdb_info(void) {
  struct filenamenode *fnn;
  int i;
  
  for (i=0; i<BINS; i++)
    for (fnn= bins[i]; fnn; fnn= fnn->next) {
      fnn->flags= 0;
      fnn->oldhash= 0;
    }
}

void note_must_reread_files_inpackage(struct pkginfo *pkg) {
  allpackagesdone= 0;
  ensure_package_clientdata(pkg);
  pkg->clientdata->fileslistvalid= 0;
}

static int saidread=0;

void ensure_packagefiles_available(struct pkginfo *pkg) {
  static struct varbuf fnvb;
  static char stdiobuf[8192];
  
  FILE *file;
  const char *filelistfile;
  struct fileinlist **lendp, *newent, *current;
  struct filepackages *packageslump;
  int search, findlast, putat, l;
  char *thefilename;
  char linebuf[1024];

  if (pkg->clientdata && pkg->clientdata->fileslistvalid) return;
  ensure_package_clientdata(pkg);

  /* Throw away any the stale data, if there was any. */
  for (current= pkg->clientdata->files;
       current;
       current= current->next) {
    /* For each file that used to be in the package,
     * go through looking for this package's entry in the list
     * of packages containing this file, and blank it out.
     */
    for (packageslump= current->namenode->packages;
         packageslump;
         packageslump= packageslump->more)
      for (search= 0;
           search < PERFILEPACKAGESLUMP && packageslump->pkgs[search];
           search++)
        if (packageslump->pkgs[search] == pkg) {
          /* Hah!  Found it. */
          for (findlast= search+1;
               findlast < PERFILEPACKAGESLUMP && packageslump->pkgs[findlast];
               findlast++);
          findlast--;
          /* findlast is now the last occupied entry, which may be the same as
           * search.  We blank out the entry for this package.  We also
           * have to copy the last entry into the empty slot, because
           * the list is null-pointer-terminated.
           */
          packageslump->pkgs[search]= packageslump->pkgs[findlast];
          packageslump->pkgs[findlast]= 0;
          /* This may result in an empty link in the list.  This is OK. */
          goto xit_search_to_delete_from_perfilenodelist;
        }
  xit_search_to_delete_from_perfilenodelist:
    ;
    /* The actual filelist links were allocated using nfmalloc, so
     * we shouldn't free them.
     */
  }
  pkg->clientdata->files= 0;

  /* Packages which aren't installed don't have a files list. */
  if (pkg->status == stat_notinstalled) {
    pkg->clientdata->fileslistvalid= 1; return;
  }

  filelistfile= pkgadminfile(pkg,LISTFILE);

  onerr_abort++;
  
  file= fopen(filelistfile,"r");

  if (!file) {
    if (errno != ENOENT)
      ohshite("unable to open files list file for package `%.250s'",pkg->name);
    onerr_abort--;
    if (pkg->status != stat_configfiles) {
      if (saidread == 1) putc('\n',stderr);
      fprintf(stderr,
              DPKG ": serious warning: files list file for package `%.250s' missing,"
              " assuming package has no files currently installed.\n", pkg->name);
    }
    pkg->clientdata->files= 0;
    pkg->clientdata->fileslistvalid= 1;
    return;
  }

  push_cleanup(cu_closefile,ehflag_bombout, 0,0, 1,(void*)file);
  
  if (setvbuf(file,stdiobuf,_IOFBF,sizeof(stdiobuf)))
    ohshite("unable to set buffering on `%.250s'",filelistfile);
  
  lendp= &pkg->clientdata->files;
  varbufreset(&fnvb);
  while (fgets(linebuf,sizeof(linebuf),file)) {
    /* This is a very important loop, and it is therefore rather messy.
     * We break the varbuf abstraction even more than usual, and we
     * avoid copying where possible.
     */
    l= strlen(linebuf);
    if (l == 0) ohshit("fgets gave an empty null-terminated string from `%.250s'",
                       filelistfile);
    l--;
    if (linebuf[l] != '\n') {
      varbufaddstr(&fnvb,linebuf);
      continue;
    } else if (!fnvb.used && l>0 && linebuf[l-1] != '/') { /* fast path */
      linebuf[l]= 0;
      thefilename= linebuf;
    } else {
      if (l>0 && linebuf[l-1] == '/') l--; /* strip trailing slashes */
      linebuf[l]= 0;
      varbufaddstr(&fnvb,linebuf);
      varbufaddc(&fnvb,0);
      fnvb.used= 0;
      thefilename= fnvb.buf;
    }
    if (!*thefilename)
      ohshit("files list file for package `%.250s' contains empty filename",pkg->name);
    newent= nfmalloc(sizeof(struct fileinlist));
    newent->namenode= findnamenode(thefilename);
    newent->next= 0;
    *lendp= newent;
    lendp= &newent->next;
  }
  if (ferror(file))
    ohshite("error reading files list file for package `%.250s'",pkg->name);
  pop_cleanup(ehflag_normaltidy); /* file= fopen() */
  if (fclose(file))
    ohshite("error closing files list file for package `%.250s'",pkg->name);
  if (fnvb.used)
    ohshit("files list file for package `%.250s' is truncated",pkg->name);

  onerr_abort--;

  for (newent= pkg->clientdata->files; newent; newent= newent->next) {
    packageslump= newent->namenode->packages;
    if (packageslump) {
      for (putat= 0;
           putat < PERFILEPACKAGESLUMP && packageslump->pkgs[putat];
           putat++);
      if (putat >= PERFILEPACKAGESLUMP) packageslump= 0;
    }
    if (!packageslump) {
      packageslump= nfmalloc(sizeof(struct filepackages));
      packageslump->more= newent->namenode->packages;
      newent->namenode->packages= packageslump;
      putat= 0;
    }
    packageslump->pkgs[putat]= pkg;
    if (++putat < PERFILEPACKAGESLUMP) packageslump->pkgs[putat]= 0;
  }      
  pkg->clientdata->fileslistvalid= 1;
}

void ensure_allinstfiles_available(void) {
  struct pkgiterator *it;
  struct pkginfo *pkg;
    
  if (allpackagesdone) return;
  if (saidread<2) { saidread=1; printf("(Reading database ... "); }
  it= iterpkgstart();
  while ((pkg= iterpkgnext(it)) != 0) ensure_packagefiles_available(pkg);
  iterpkgend(it);
  allpackagesdone= 1;

  if (saidread==1) {
    printf("%d files and directories currently installed.)\n",nfiles);
    saidread=2;
  }
}

void ensure_allinstfiles_available_quiet(void) {
  saidread=2;
  ensure_allinstfiles_available();
}

struct filenamenode *findnamenode(const char *name) {
  struct filenamenode **pointerp, *newnode;

  /* We skip initial slashes and ./ pairs, and add our own single leading slash. */
  name= skip_slash_dotslash(name);

  pointerp= bins + (hash(name) & (BINS-1));
  while (*pointerp) {
    assert((*pointerp)->name[0] == '/');
    if (!strcmp((*pointerp)->name+1,name)) break;
    pointerp= &(*pointerp)->next;
  }
  if (*pointerp) return *pointerp;

  newnode= nfmalloc(sizeof(struct filenamenode));
  newnode->packages= 0;
  newnode->name= nfmalloc(strlen(name)+2);
  newnode->name[0]= '/'; strcpy(newnode->name+1,name);
  newnode->flags= 0;
  newnode->next= 0;
  *pointerp= newnode;
  nfiles++;

  return newnode;
}

void write_filelist_except(struct pkginfo *pkg, struct fileinlist *list, int leaveout) {
  /* If leaveout is nonzero, will not write any file whose filenamenode
   * has the fnnf_elide_other_lists flag set.
   */
  static struct varbuf vb, newvb;
  FILE *file;

  varbufreset(&vb);
  varbufaddstr(&vb,admindir);
  varbufaddstr(&vb,"/" INFODIR);
  varbufaddstr(&vb,pkg->name);
  varbufaddstr(&vb,"." LISTFILE);
  varbufaddc(&vb,0);

  varbufreset(&newvb);
  varbufaddstr(&newvb,vb.buf);
  varbufaddstr(&newvb,NEWDBEXT);
  varbufaddc(&newvb,0);
  
  file= fopen(newvb.buf,"w+");
  if (!file)
    ohshite("unable to create updated files list file for package %s",pkg->name);
  push_cleanup(cu_closefile,ehflag_bombout, 0,0, 1,(void*)file);
  while (list) {
    if (!(leaveout && (list->namenode->flags & fnnf_elide_other_lists))) {
      fputs(list->namenode->name,file);
      putc('\n',file);
    }
    list= list->next;
  }
  if (ferror(file))
    ohshite("failed to write to updated files list file for package %s",pkg->name);
  if (fflush(file))
    ohshite("failed to flush updated files list file for package %s",pkg->name);
  if (fsync(fileno(file)))
    ohshite("failed to sync updated files list file for package %s",pkg->name);
  pop_cleanup(ehflag_normaltidy); /* file= fopen() */
  if (fclose(file))
    ohshite("failed to close updated files list file for package %s",pkg->name);
  if (rename(newvb.buf,vb.buf))
    ohshite("failed to install updated files list file for package %s",pkg->name);

  note_must_reread_files_inpackage(pkg);
}

void reversefilelist_init(struct reversefilelistiter *iterptr,
                          struct fileinlist *files) {
  /* Initialises an iterator that appears to go through the file
   * list `files' in reverse order, returning the namenode from
   * each.  What actually happens is that we walk the list here,
   * building up a reverse list, and then peel it apart one
   * entry at a time.
   */
  struct fileinlist *newent;
  
  iterptr->todo= 0;
  while (files) {
    newent= m_malloc(sizeof(struct fileinlist));
    newent->namenode= files->namenode;
    newent->next= iterptr->todo;
    iterptr->todo= newent;
    files= files->next;
  }
}

struct filenamenode *reversefilelist_next(struct reversefilelistiter *iterptr) {
  struct filenamenode *ret;
  struct fileinlist *todo;

  todo= iterptr->todo;
  if (!todo) return 0;
  ret= todo->namenode;
  iterptr->todo= todo->next;
  free(todo);
  return ret;
}

void reversefilelist_abort(struct reversefilelistiter *iterptr) {
  /* Clients must call this function to clean up the reversefilelistiter
   * if they wish to break out of the iteration before it is all done.
   * Calling this function is not necessary if reversefilelist_next has
   * been called until it returned 0.
   */
  while (reversefilelist_next(iterptr));
}
