#include "config.h"
#include "miscfn.h"

#if HAVE_ALLOCA_H
# include <alloca.h>
#endif 

#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "install.h"
#include "intl.h"
#include "messages.h"
#include "md5.h"
#include "misc.h"
#include "rpmdb.h"
#include "rpmlib.h"

static char * SCRIPT_PATH = "PATH=/sbin:/bin:/usr/sbin:/usr/bin:"
			                 "/usr/X11R6/bin\nexport PATH\n";

enum fileActions { REMOVE, BACKUP, KEEP };

static int sharedFileCmp(const void * one, const void * two);
static int handleSharedFiles(rpmdb db, int offset, char ** fileList, 
			     char ** fileMd5List, int fileCount, 
			     enum fileActions * fileActions);
static int removeFile(char * file, char state, unsigned int flags, char * md5, 
		      short mode, enum fileActions action, char * rmmess, 
		      int brokenMd5, int test);

static int sharedFileCmp(const void * one, const void * two) {
    if (((struct sharedFile *) one)->secRecOffset <
	((struct sharedFile *) two)->secRecOffset)
	return -1;
    else if (((struct sharedFile *) one)->secRecOffset ==
	((struct sharedFile *) two)->secRecOffset)
	return 0;
    else 
	return 1;
}

int findSharedFiles(rpmdb db, int offset, char ** fileList, int fileCount,
		    struct sharedFile ** listPtr, int * listCountPtr) {
    int i, j;
    struct sharedFile * list = NULL;
    int itemsUsed = 0;
    int itemsAllocated = 0;
    dbiIndexSet matches;

    itemsAllocated = 5;
    list = malloc(sizeof(struct sharedFile) * itemsAllocated);

    for (i = 0; i < fileCount; i++) {
	if (!rpmdbFindByFile(db, fileList[i], &matches)) {
	    for (j = 0; j < matches.count; j++) {
		if (matches.recs[j].recOffset != offset) {
		    if (itemsUsed == itemsAllocated) {
			itemsAllocated += 10;
			list = realloc(list, sizeof(struct sharedFile) * 
					    itemsAllocated);
		    }
		    list[itemsUsed].mainFileNumber = i;
		    list[itemsUsed].secRecOffset = matches.recs[j].recOffset;
		    list[itemsUsed].secFileNumber = matches.recs[j].fileNumber;
		    itemsUsed++;
		}
	    }

	    dbiFreeIndexRecord(matches);
	}
    }

    if (itemsUsed) {
	qsort(list, itemsUsed, sizeof(struct sharedFile), sharedFileCmp);
	*listPtr = list;
	*listCountPtr = itemsUsed;
    } else {
	free(list);
	*listPtr = NULL;
	*listCountPtr = 0;
    }

    return 0;
}

static int handleSharedFiles(rpmdb db, int offset, char ** fileList, 
			     char ** fileMd5List, int fileCount, 
			     enum fileActions * fileActions) {
    Header sech = NULL;
    int secOffset = 0;
    struct sharedFile * sharedList;
    int sharedCount;
    char * name, * version, * release;
    int secFileCount;
    char ** secFileMd5List, ** secFileList;
    char * secFileStatesList;
    int type;
    int i;
    int rc = 0;

    if (findSharedFiles(db, offset, fileList, fileCount, &sharedList, 
			&sharedCount)) {
	return 1;
    }

    if (!sharedCount) {
	return 0;
    }

    for (i = 0; i < sharedCount; i++) {
	if (secOffset != sharedList[i].secRecOffset) {
	    if (secOffset) {
		headerFree(sech);
		free(secFileMd5List);
		free(secFileList);
	    }

	    secOffset = sharedList[i].secRecOffset;
	    sech = rpmdbGetRecord(db, secOffset);
	    if (!sech) {
		rpmError(RPMERR_DBCORRUPT, 
			 _("cannot read header at %d for uninstall"), offset);
		rc = 1;
		break;
	    }

	    headerGetEntry(sech, RPMTAG_NAME, &type, (void **) &name, 
		     &secFileCount);
	    headerGetEntry(sech, RPMTAG_VERSION, &type, (void **) &version, 
		     &secFileCount);
	    headerGetEntry(sech, RPMTAG_RELEASE, &type, (void **) &release, 
		     &secFileCount);

	    rpmMessage(RPMMESS_DEBUG, 
			_("package %s-%s-%s contain shared files\n"), 
		    	name, version, release);

	    if (!headerGetEntry(sech, RPMTAG_FILENAMES, &type, 
			  (void **) &secFileList, &secFileCount)) {
		rpmError(RPMERR_DBCORRUPT, "package %s contains no files",
		      name);
		headerFree(sech);
		rc = 1;
		break;
	    }

	    headerGetEntry(sech, RPMTAG_FILESTATES, &type, 
		     (void **) &secFileStatesList, &secFileCount);
	    headerGetEntry(sech, RPMTAG_FILEMD5S, &type, 
		     (void **) &secFileMd5List, &secFileCount);
	}

	rpmMessage(RPMMESS_DEBUG, "file %s is shared\n",
		fileList[sharedList[i].mainFileNumber]);
	
	switch (secFileStatesList[sharedList[i].secFileNumber]) {
	  case RPMFILE_STATE_REPLACED:
	    rpmMessage(RPMMESS_DEBUG, "     file has already been replaced\n");
	    break;

	  case RPMFILE_STATE_NOTINSTALLED:
	    rpmMessage(RPMMESS_DEBUG, "     file was never installed\n");
	    break;
    
	  case RPMFILE_STATE_NETSHARED:
	    rpmMessage(RPMMESS_DEBUG, "     file is netshared (so don't touch it)\n");
	    fileActions[sharedList[i].mainFileNumber] = KEEP;
	    break;
    
	  case RPMFILE_STATE_NORMAL:
	    if (!strcmp(fileMd5List[sharedList[i].mainFileNumber],
			secFileMd5List[sharedList[i].secFileNumber])) {
		rpmMessage(RPMMESS_DEBUG, "    file is truely shared - saving\n");
	    }
	    fileActions[sharedList[i].mainFileNumber] = KEEP;
	    break;
	}
    }

    if (secOffset) {
	headerFree(sech);
	free(secFileMd5List);
	free(secFileList);
    }
    free(sharedList);

    return rc;
}

int rpmRemovePackage(char * prefix, rpmdb db, unsigned int offset, int flags) {
    Header h;
    int i;
    int fileCount;
    char * rmmess, * name, * version, * release;
    char * fnbuffer = NULL;
    dbiIndexSet matches;
    int fnbuffersize = 0;
    int prefixLength = strlen(prefix);
    char ** fileList, ** fileMd5List;
    int type, count;
    uint_32 * fileFlagsList;
    int_16 * fileModesList;
    char * fileStatesList;
    enum { REMOVE, BACKUP, KEEP } * fileActions;
    int scriptArg;

    if (flags & RPMUNINSTALL_JUSTDB)
	flags |= RPMUNINSTALL_NOSCRIPTS;

    h = rpmdbGetRecord(db, offset);
    if (!h) {
	rpmError(RPMERR_DBCORRUPT, "cannot read header at %d for uninstall",
	      offset);
	return 1;
    }

    headerGetEntry(h, RPMTAG_NAME, &type, (void **) &name,  &count);
    headerGetEntry(h, RPMTAG_VERSION, &type, (void **) &version,  &count);
    headerGetEntry(h, RPMTAG_RELEASE, &type, (void **) &release,  &count);
    /* when we run scripts, we pass an argument which is the number of 
       versions of this package that will be installed when we are finished */
    if (rpmdbFindPackage(db, name, &matches)) {
	rpmError(RPMERR_DBCORRUPT, "cannot read packages named %s for uninstall",
	      name);
	return 1;
    }
 
    scriptArg = matches.count - 1;
    dbiFreeIndexRecord(matches);

    if (flags & RPMUNINSTALL_TEST) {
	rmmess = "would remove";
    } else {
	rmmess = "removing";
    }

    rpmMessage(RPMMESS_DEBUG, "running preuninstall script (if any)\n");

    if (runScript(prefix, h, RPMTAG_PREUN, RPMTAG_PREUNPROG, scriptArg, 
		 flags & RPMUNINSTALL_NOSCRIPTS)) {
	headerFree(h);
	return 1;
    }
    
    rpmMessage(RPMMESS_DEBUG, "%s files test = %d\n", rmmess, flags & RPMUNINSTALL_TEST);
    if (!(flags & RPMUNINSTALL_JUSTDB) &&
	headerGetEntry(h, RPMTAG_FILENAMES, &type, (void **) &fileList, 
	               &fileCount)) {
	if (prefix[0]) {
	    fnbuffersize = 1024;
	    fnbuffer = alloca(fnbuffersize);
	}

	headerGetEntry(h, RPMTAG_FILESTATES, &type, (void **) &fileStatesList, 
		 &fileCount);
	headerGetEntry(h, RPMTAG_FILEMD5S, &type, (void **) &fileMd5List, 
		 &fileCount);
	headerGetEntry(h, RPMTAG_FILEFLAGS, &type, (void **) &fileFlagsList, 
		 &fileCount);
	headerGetEntry(h, RPMTAG_FILEMODES, &type, (void **) &fileModesList, 
		 &fileCount);

	fileActions = alloca(sizeof(*fileActions) * fileCount);
	for (i = 0; i < fileCount; i++) 
	    if (fileStatesList[i] == RPMFILE_STATE_NOTINSTALLED ||
		fileStatesList[i] == RPMFILE_STATE_NETSHARED) 
		fileActions[i] = KEEP;
	    else
		fileActions[i] = REMOVE;

	handleSharedFiles(db, offset, fileList, fileMd5List, fileCount, fileActions);

	/* go through the filelist backwards to help insure that rmdir()
	   will work */
	for (i = fileCount - 1; i >= 0; i--) {
	    if (strcmp(prefix, "/")) {
		if ((strlen(fileList[i]) + prefixLength + 1) > fnbuffersize) {
		    fnbuffersize = (strlen(fileList[i]) + prefixLength) * 2;
		    fnbuffer = alloca(fnbuffersize);
		}
		strcpy(fnbuffer, prefix);
		strcat(fnbuffer, "/");
		strcat(fnbuffer, fileList[i]);
	    } else {
		fnbuffer = fileList[i];
	    }

	    removeFile(fnbuffer, fileStatesList[i], fileFlagsList[i],
		       fileMd5List[i], fileModesList[i], fileActions[i], 
		       rmmess, !headerIsEntry(h, RPMTAG_RPMVERSION),
		       flags & RPMUNINSTALL_TEST);
	}

	free(fileList);
	free(fileMd5List);
    }

    rpmMessage(RPMMESS_DEBUG, "running postuninstall script (if any)\n");
    runScript(prefix, h, RPMTAG_POSTUN, RPMTAG_POSTUNPROG, scriptArg, 
		flags & RPMUNINSTALL_NOSCRIPTS);

    headerFree(h);

    rpmMessage(RPMMESS_DEBUG, "%s database entry\n", rmmess);
    if (!(flags & RPMUNINSTALL_TEST))
	rpmdbRemove(db, offset, 0);

    return 0;
}

int runScript(char * prefix, Header h, int scriptTag, int progTag,
	      int arg, int norunScripts) {
    char * script;
    char * fn;
    int fd = -1;
    int isdebug = rpmIsDebug();
    int child;
    int status;
    char upgradeArg[20];
    char * installPrefix = NULL;
    char * installPrefixEnv = NULL;
    char * argv[10];
    char ** argvPtr = argv;
    char * program;
    int pipes[2];

    sprintf(upgradeArg, "%d", arg);

    if (norunScripts) return 0;

    /* headerGetEntry() sets the data pointer to NULL if the entry doesn
       not exist */
    headerGetEntry(h, progTag, NULL, (void **) &program, NULL);
    headerGetEntry(h, scriptTag, NULL, (void **) &script, NULL);

    if (!program && !script)
	return 0;
    else if (!program)
	program = "/bin/sh";

    if (headerGetEntry(h, RPMTAG_INSTALLPREFIX, NULL, (void **) &installPrefix,
	    	       NULL)) {
	installPrefixEnv = alloca(strlen(installPrefix) + 30);
	strcpy(installPrefixEnv, "RPM_INSTALL_PREFIX=");
	strcat(installPrefixEnv, installPrefix);
	strcat(installPrefixEnv, "\n");
    }

    rpmMessage(RPMMESS_DEBUG, "running inst helper %s\n", program);

    if (script) {
	if (makeTempFile(prefix, &fn, &fd))
	    return 1;
	write(fd, "#!", 2);
	write(fd, program, strlen(program));


	write(fd, "\n", 1);

	if (isdebug && !strcmp(program, "/bin/sh")) 
	    write(fd, "set -x\n", 7);

	if (installPrefixEnv)
	    write(fd, installPrefixEnv, strlen(installPrefixEnv));

	write(fd, SCRIPT_PATH, strlen(SCRIPT_PATH));
	write(fd, script, strlen(script));
	close(fd);

	program = fn + strlen(prefix);
    }

    *argvPtr++ = program;
    if (script) {
	*argvPtr++ = upgradeArg;
    }
    *argvPtr++ = NULL;
	
    if (!(child = fork())) {
	/* make stdin inaccessible */
	pipe(pipes);
	close(pipes[1]);

	dup2(pipes[0], 0);

	if (strcmp(prefix, "/")) {
	    rpmMessage(RPMMESS_DEBUG, "performing chroot(%s)\n", prefix);
	    chroot(prefix);
	    chdir("/");
	}

	execv(argv[0], argv);
	_exit(-1);
    }

    waitpid(child, &status, 0);

    if (script) {
	if (!isdebug) unlink(fn);
	free(fn);
    }

    if (!WIFEXITED(status) || WEXITSTATUS(status)) {
	rpmError(RPMERR_SCRIPT, _("execution of script failed"));
	return 1;
    }

    return 0;
}

static int removeFile(char * file, char state, unsigned int flags, char * md5, 
		      short mode, enum fileActions action, char * rmmess, 
		      int brokenMd5, int test) {
    char currentMd5[40];
    int rc = 0;
    char * newfile;
	
    switch (state) {
      case RPMFILE_STATE_REPLACED:
	rpmMessage(RPMMESS_DEBUG, "%s has already been replaced\n", file);
	break;

      case RPMFILE_STATE_NORMAL:
	if ((action == REMOVE) && (flags & RPMFILE_CONFIG)) {
	    /* if it's a config file, we may not want to remove it */
	    rpmMessage(RPMMESS_DEBUG, "finding md5sum of %s\n", file);
	    if (brokenMd5)
		rc = mdfileBroken(file, currentMd5);
	    else
		rc = mdfile(file, currentMd5);

	    if (mdfile(file, currentMd5)) 
		rpmMessage(RPMMESS_DEBUG, 
				"    failed - assuming file removed\n");
	    else {
		if (strcmp(currentMd5, md5)) {
		    rpmMessage(RPMMESS_DEBUG, "    file changed - will save\n");
		    action = BACKUP;
		} else {
		    rpmMessage(RPMMESS_DEBUG, 
				"    file unchanged - will remove\n");
		}
	    }
	}

	switch (action) {

	  case KEEP:
	    rpmMessage(RPMMESS_DEBUG, "keeping %s\n", file);
	    break;

	  case BACKUP:
	    rpmMessage(RPMMESS_DEBUG, "saving %s as %s.rpmsave\n", file, file);
	    if (!test) {
		newfile = alloca(strlen(file) + 20);
		strcpy(newfile, file);
		strcat(newfile, ".rpmsave");
		if (rename(file, newfile)) {
		    rpmError(RPMERR_RENAME, _("rename of %s to %s failed: %s"),
				file, newfile, strerror(errno));
		    rc = 1;
		}
	    }
	    break;

	  case REMOVE:
	    rpmMessage(RPMMESS_DEBUG, "%s - %s\n", file, rmmess);
	    if (S_ISDIR(mode)) {
		if (!test) {
		    if (rmdir(file)) {
			if (errno == ENOTEMPTY)
			    rpmError(RPMERR_RMDIR, 
				_("cannot remove %s - directory not empty"), 
				file);
			else
			    rpmError(RPMERR_RMDIR, _("rmdir of %s failed: %s"),
					file, strerror(errno));
			rc = 1;
		    }
		}
	    } else {
		if (!test) {
		    if (unlink(file)) {
			rpmError(RPMERR_UNLINK, _("removal of %s failed: %s"),
				    file, strerror(errno));
			rc = 1;
		    }
		}
	    }
	    break;
	}
   }
 
   return 0;
}
