/* deborphan - Find orphaned libraries on a Debian system.
   Copyright (C) 2000, 2001, 2002, 2003 Cris van Pelt
   Copyright (C) 2003, 2004 Peter Palfrader
   Copyright (C) 2008, 2009 Carsten Hey

   $Id: deborphan.c 871 2010-12-12 20:19:35Z carsten $

   Distributed under the terms of the Artistic License.
*/

/* Header files we should all have. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <deborphan.h>
#include <set.h>

#ifdef HAVE_ERRNO_H
#  include <errno.h>
#endif

/* These files should already be installed on the host machine - 
   the GPL does not allow redistribution in this package. */
#ifdef ENABLE_NLS
#  include <libintl.h>
#  include <locale.h>
#endif

#ifdef USE_XALLOC
#  include <xalloc.h>
#endif

/* Use either getopt.h (getopt_long) or unistd.h (getopt) */
#ifdef HAVE_GETOPT_H
#  include <getopt.h>
#else
#  include <unistd.h>
extern int optind;
#endif

/* Name this program was called with. */
char *program_name;

/* Options given on the commandline */
int options[NUM_OPTIONS];

/* An optional list of packages to search for. */
char **search_for;

/* A bunch of packages to keep. */
dep *keep;

static int depcmp(const dep* d1, const dep* d2)
{
	return strcmp(d1->name, d2->name);
}

int
main(int argc, char *argv[])
{
    char *line, *sfile = NULL, *kfile = NULL;
#ifdef LOW_MEM
    FILE *sfile_content;
#else
    char *sfile_content, *sfile_content2;
#endif
    pkg_info *package, *this;
    int i, argind;
    size_t exclude_list_cnt = 0;
    size_t exclude_list_max = 0;
    dep *exclude_list = NULL;

    /*@unused@*/ /* Actually it is used but splint does not recognise this. */
    struct option longopts[] = {
	{"version", 0, 0, 'v'},
	{"help", 0, 0, 'h'},
	{"status-file", 1, 0, 'f'},
	{"show-deps", 0, 0, 'd'},
	{"show-deps-pristine", 0, 0, 'd'},
	{"nice-mode", 0, 0, 'n'},
	{"ignore-recommends", 0, 0, 61},
	{"ignore-suggests", 0, 0, 62},
	{"print-guess-list", 0, 0, 200},
	{"check-options", 0, 0, 201},
	{"all-packages-pristine", 0, 0, 202},
	{"all-packages", 0, 0, 'a'},
	{"priority", 1, 0, 'p'},
	{"show-section", 0, 0, 's'},
	{"no-show-section", 0, 0, 0},
	{"show-priority", 0, 0, 'P'},
	{"show-size", 0, 0, 'z'},
	{"force-hold", 0, 0, 'H'},
	{"keep-file", 1, 0, 'k'},
	{"add-keep", 0, 0, 'A'},
	{"del-keep", 0, 0, 'R'},
	{"list-keep", 0, 0, 'L'},
	{"zero-keep", 0, 0, 'Z'},
	{"guess-dev", 0, 0, 1},
	{"no-guess-dev", 0, 0, 31},
	{"guess-perl", 0, 0, 2},
	{"no-guess-perl", 0, 0, 32},
	{"guess-section", 0, 0, 3},
	{"no-guess-section", 0, 0, 33},
	{"guess-all", 0, 0, 4},
	{"no-guess-all", 0, 0, 34},
	{"guess-debug", 0, 0, 5},
	{"no-guess-debug", 0, 0, 35},
#ifdef DEBFOSTER_KEEP
        {"df-keep", 0, 0, 6},
#endif
	{"guess-only", 0, 0, 7},
#ifdef DEBFOSTER_KEEP
        {"no-df-keep", 0, 0, 8},
#endif
	{"guess-pike", 0, 0, 9},
	{"no-guess-pike", 0, 0, 39},
	{"guess-python", 0, 0, 10},
	{"no-guess-python", 0, 0, 40},
	{"guess-ruby", 0, 0, 11},
	{"no-guess-ruby", 0, 0, 41},
	{"guess-interpreters", 0, 0, 12},
	{"no-guess-interpreters", 0, 0, 42},
	{"guess-dummy", 0, 0, 13},
	{"no-guess-dummy", 0, 0, 43},
	{"guess-common", 0, 0, 14},
	{"no-guess-common", 0, 0, 44},
	{"guess-data", 0, 0, 15},
	{"no-guess-data", 0, 0, 45},
	{"guess-doc", 0, 0, 16},
	{"no-guess-doc", 0, 0, 46},
	{"find-config", 0, 0, 17},
	{"libdevel", 0, 0, 18},
	{"guess-mono", 0, 0, 19},
	{"no-guess-mono", 0, 0, 49},
	{"guess-kernel", 0, 0, 20},
	{"no-guess-kernel", 0, 0, 50},
	{"exclude", 1, 0, 'e'},
	{0, 0, 0, 0}
    };

    program_name = argv[0];
    memset(options, 0, NUM_OPTIONS * sizeof(int));

    options[PRIORITY] = DEFAULT_PRIORITY;
#ifdef IGNORE_DEBFOSTER
    options[NO_DEBFOSTER] = 1;
#endif


#ifdef ENABLE_NLS
    setlocale(LC_ALL, "");
    bindtextdomain(PACKAGE, LOCALEDIR);
    textdomain(PACKAGE);
#endif

    while ((i = _get_opt(argc, argv, "p:advhe:nf:sPzHk:ARLZ", 
			 longopts, NULL)) != EOF) {
	switch (i) {
	case 'd':
	    options[SHOW_DEPS] = 1;
	    break;
	case 'h':
	    exit_help();
	    break;
	case 'v':
	    exit_version();
	    break;
	case 'f':
	    sfile = (char *) xmalloc((strlen(optarg) + 1) * sizeof(sfile));
	    strcpy(sfile, optarg);
	    break;
	case 'n':
	    options[NICE_MODE] = 1;
	    break;
	case 61:
	    options[IGNORE_RECOMMENDS] = 1;
	    break;
	case 62:
	    options[IGNORE_SUGGESTS] = 1;
	    break;
	case 200:
	    printf("common\ndata\ndebug\ndev\ndoc\ndummy\ninterpreters\nkernel\nmono\nperl\npike\npython\nruby\nsection\n");
	    exit(EXIT_SUCCESS);
	case 201:
	    options[CHECK_OPTIONS] = 1;
	    break;
	case 202:
	    /* ALL_PACKAGES_IMPLY_SECTION is defined anyway, so this
	     * fall through is sufficient for now. */
	case 'a':
	    options[ALL_PACKAGES] = 1;
#ifdef ALL_PACKAGES_IMPLY_SECTION
	    options[SHOW_SECTION]++;
#endif
	    break;
	case 'p':
	    options[PRIORITY] = string_to_priority(optarg);
	    if (!options[PRIORITY]) {
		if (!sscanf(optarg, "%d", &options[PRIORITY])) {
		    print_usage(stderr);
		    error(EXIT_FAILURE, 0, "invalid priority: %s", optarg);
		}
	    }
	    break;
	case 's':
	    options[SHOW_SECTION]++;
	    break;
	case 0:
	    options[SHOW_SECTION]--;
	    break;
	case 'P':
	    options[SHOW_PRIORITY] = 1;
	    break;
	case 'z':
	    options[SHOW_SIZE] = 1;
	    break;
	case 'H':
	    options[FORCE_HOLD] = 1;
	    break;
	case 'k':
	    kfile = optarg;
	    break;
	case 'A':
	    options[ADD_KEEP] = 1;
	    break;
	case 'R':
	    options[DEL_KEEP] = 1;
	    break;
	case 'L':
	    options[LIST_KEEP] = 1;
	    break;
	case 'Z':
            options[ZERO_KEEP] = 1;
            break;
	case 1:
	    guess_set(GUESS_DEV);
	    break;
	case 31:
	    guess_clr(GUESS_DEV);
	    break;
	case 2:
	    guess_set(GUESS_PERL);
	    break;
	case 32:
	    guess_clr(GUESS_PERL);
	    break;
	case 3:
	    guess_set(GUESS_SECTION);
	    break;
	case 33:
	    guess_clr(GUESS_SECTION);
	    break;
	case 4:
	    guess_set(GUESS_ALL);
	    break;
	case 34:
	    guess_clr(GUESS_ALL);
	    break;
	case 5:
	    guess_set(GUESS_DEBUG);
	    break;
	case 35:
	    guess_clr(GUESS_DEBUG);
	    break;
#ifdef DEBFOSTER_KEEP
        case 6:
            options[NO_DEBFOSTER] = 0;
            break;
#endif
	case 7:
	    options[GUESS_ONLY] = 1;
	    break;
#ifdef DEBFOSTER_KEEP
        case 8:
            options[NO_DEBFOSTER] = 1;
            break;
#endif
	case 9:
	    guess_set(GUESS_PIKE);
	    break;
	case 39:
	    guess_clr(GUESS_PIKE);
	    break;
	case 10:
	    guess_set(GUESS_PYTHON);
	    break;
	case 40:
	    guess_clr(GUESS_PYTHON);
	    break;
	case 11:
	    guess_set(GUESS_RUBY);
	    break;
	case 41:
	    guess_clr(GUESS_RUBY);
	    break;
	case 12:
	    guess_set(GUESS_IP);
	    break;
	case 42:
	    guess_clr(GUESS_IP);
	    break;
	case 13:
	    guess_set(GUESS_DUMMY);
	    break;
	case 43:
	    guess_clr(GUESS_DUMMY);
	    break;
	case 14:
	    guess_set(GUESS_COMMON);
	    break;
	case 44:
	    guess_clr(GUESS_COMMON);
	    break;
	case 15:
	    guess_set(GUESS_DATA);
	    break;
	case 45:
	    guess_clr(GUESS_DATA);
	    break;
	case 16:
	    guess_set(GUESS_DOC);
	    break;
	case 46:
	    guess_clr(GUESS_DOC);
	    break;
	case 17:
	    options[FIND_CONFIG] = 1;
	    options[ALL_PACKAGES] = 1;
	    break;
	case 18:
	    options[SEARCH_LIBDEVEL] = 1;
	    break;
	case 19:
	    guess_set(GUESS_MONO);
	    break;
	case 49:
	    guess_clr(GUESS_MONO);
	    break;
	case 20:
	    guess_set(GUESS_KERNEL);
	    break;
	case 50:
	    guess_clr(GUESS_KERNEL);
	    break;
	case 'e':
	    while ( optarg ) {
		    char *c_ptr;
		    if ( exclude_list_cnt >= exclude_list_max ) {
			/* grow exclude_list[] array */
			exclude_list_max = exclude_list_max ? exclude_list_max*2 : INIT_EXCLUDES_COUNT;
#ifdef DEBUG
			fprintf(stderr, "Growing excludes field to %d.\n", exclude_list_max);
			fflush(stderr);
#endif /* DEBUG */
			exclude_list = xrealloc( exclude_list, exclude_list_max * sizeof(exclude_list[0]) );
		    }
		    if ( exclude_list_cnt >= exclude_list_max )
			    error(EXIT_FAILURE, 0, "Too many exclude packages");
		    c_ptr = strsep(&optarg, ",");
		    exclude_list[ exclude_list_cnt ].name = c_ptr;
		    exclude_list[ exclude_list_cnt ].namehash = strhash(c_ptr);
		    ++exclude_list_cnt;
	    }
	    qsort(exclude_list, exclude_list_cnt, sizeof(exclude_list[0]),
			    (int(*)(const void*, const void*))depcmp);
	    break;
	case '?':
	    if (options[CHECK_OPTIONS])
		exit(EXIT_FAILURE);
	    print_usage(stderr);
	    exit(EXIT_FAILURE);
	}
    }
#ifdef DEFAULT_NICE		/* Invert the value of nice_mode */
   options[NICE_MODE] ^= 1;
#endif

   if (options[CHECK_OPTIONS])
	exit(EXIT_SUCCESS);

   if (options[ZERO_KEEP]) {
	if (!kfile)
	    kfile = KEEPER_FILE;
	if (zerokeep(kfile) < 0)
	    error(EXIT_FAILURE, errno, "%s", kfile);

	/* Don't always exit. "deborphan -Z -A <foo>" should be valid. */
	if (!options[ADD_KEEP])
		exit(EXIT_SUCCESS);
   }
   
   if (options[LIST_KEEP]) {
       if (!kfile)
	   kfile = KEEPER_FILE;
       
       if (!listkeep(kfile)) {
#ifndef DEBFOSTER_KEEP
	   if (errno != ENOENT) /* If the file doesn't exit, we just have an empty list */
	       error(EXIT_FAILURE, errno, "%s", kfile); /* fatal */
#else
	   error(0, errno, "%s", kfile); /* non-fatal */
#endif
       }
#ifdef DEBFOSTER_KEEP
       if (!options[NO_DEBFOSTER]) {
	   fprintf(stderr, "-- %s -- \n", DEBFOSTER_KEEP);
	   listkeep(DEBFOSTER_KEEP);
       }
#endif
       exit(EXIT_SUCCESS);
   }

   if (options[GUESS_ONLY] && !options[GUESS]) {
       print_usage(stderr);
       error(EXIT_FAILURE, 0, "need at least one other --guess option.");
   }

    argind = optind;
    i = 0;
    if (kfile == NULL)
	kfile = KEEPER_FILE;
    if (sfile == NULL)
	sfile = STATUS_FILE;

    if ((argc - argind) > 50)
	error(EXIT_FAILURE, E2BIG, "");

    if (argind < argc) {
	options[SEARCH] = 1;
	options[ALL_PACKAGES] = 1;
	options[SHOW_DEPS] = 1;
	options[PRIORITY] = 0;
    }

    keep = readkeep(kfile);

    if (options[ADD_KEEP] || options[DEL_KEEP]) {
	char **args;
	if (argind >= argc)
	    error(EXIT_FAILURE, 0, "not enough arguments for %s.",
		  options[ADD_KEEP] ? "--add-keep" : "--del-keep");

	args = parseargs(argind, argc, argv);

	if (options[DEL_KEEP]) {
	    switch (delkeep(kfile, args)) {
	    case -1:
		error(EXIT_FAILURE, errno, "%s", kfile);
                break; /* make splint happy */
	    case 1:
		error(EXIT_FAILURE, 0, "no packages removed.");
	    }
	} else {
	    i = pkggrep(sfile, args);
	    if (i < 0)
		error(EXIT_FAILURE, errno, sfile);
	    if (i)
		error(EXIT_FAILURE, 0, "%s: no such package.", args[i-1]);

	    if ((i = hasduplicate(args)))
		error(EXIT_FAILURE, 0, "%s: duplicate entry.", args[i-1]);

	    if (addkeep(kfile, args) < 0)
		error(EXIT_FAILURE, errno, "%s", kfile);
	}
	
	exit(EXIT_SUCCESS);
    }

    /* We don't want to merge the files if we're adding, because it's perfectly
       alright to have the same entry in debfoster and deborphan.
    */
#ifdef DEBFOSTER_KEEP
    if (!options[NO_DEBFOSTER]) {
	dep *dfkeep = readkeep(DEBFOSTER_KEEP);

	if (dfkeep) {
	    dep *d = keep;
	    keep = mergekeep(keep, dfkeep);
	    free(d);
	    free(dfkeep);
	}
    }
#endif

    search_for = parseargs(argind, argc, argv);

    if (!(sfile_content = debopen(sfile)))
	error(EXIT_FAILURE, errno, "%s", sfile);

#ifndef LOW_MEM
    sfile_content2 = sfile_content;
#endif

    this = package = (pkg_info *) xmalloc(sizeof(pkg_info));
    init_pkg(this);
    init_pkg_regex();

    while ((line = nextline(&sfile_content)) != NULL) {
	if ((!strchr("IPpSsEeDdRr", *line)) || (*line == ' '))
	    continue;

	if (*line != '\0') {
	    strstripchr(line, ' ');
	    get_pkg_info(line, this);
	} else {
	    if ( (this->self.name && exclude_list &&
				    bsearch(&this->self, exclude_list, exclude_list_cnt,
					    sizeof(exclude_list[0]),
					    (int(*)(const void*, const void*))depcmp) )
		  || (!this->install && !options[FIND_CONFIG]) ) {
		reinit_pkg(this);
	    } else {
		this->next = xmalloc(sizeof(pkg_info));
		this = this->next;
		init_pkg(this);
	    }
	}
    }

#ifdef LOW_MEM
    free(line);
#else
    free(sfile_content2);
#endif

    this->next = NULL;
    this = package;

    /* Check the dependencies. Last package is handled specially, because it
       is not terminated with a \n in STATUS_FILE. */
    while (this->next) {
	check_lib_deps(package, this);
	this = this->next;
    }
    if (this->install)
	check_lib_deps(package, this);

    free_pkg_regex();

    fflush(stdout);

    for (i = 0; options[SEARCH] && search_for[i]; i++) {
	fprintf(stderr, "%s: package %s not found or not installed\n",
		argv[0], search_for[i]);
    }

    return i ? EXIT_FAILURE : EXIT_SUCCESS;
}
