/*
    Copyright (C) 2001-2006  Ben Kibbey <bjk@luxsci.net>

    This program 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 program 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, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <ctype.h>
#include <pwd.h>
#include <time.h>

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

#ifdef HAVE_GETOPT_H
#include <getopt.h>
#endif

#ifndef HAVE_ERR_H
#include "err.c"
#endif

#include "ui.h"

static void *Realloc(void *p, size_t size)
{
    void *p2;

    if ((p2 = realloc(p, size)) == NULL)
	err(EXIT_FAILURE, "%s", "realloc()");

    return p2;
}

/* This may be used in modules to keep a consistant time format with other
 * modules. */
char *stamp(time_t epoch, const char *format)
{
    static char buf[TIMEBUFSIZE];
    struct tm *t;

    t = localtime(&epoch);
    strftime(buf, sizeof(buf), format, t);

    return buf;
}

/* 
 * This may be used in modules to add a string to the buffer (ui_module_exec()).
 */
void add_string(char ***buf, const char *str)
{
    char **s;
    int i = 0;

    if (*buf) {
	for (s = *buf; *s; s++)
	    i++;
    }

    s = *buf;
    s = Realloc(s, (i + 2) * sizeof(char *));
    s[i++] = strdup(str);
    s[i] = NULL;
    *buf = s;
    return;
}

/* This is for the field separators (-F and -m). */
static int escapes(const char *str)
{
    int c = 0;

    if (str[0] != '\\')
	return str[0];

    switch (*++str) {
	case 't':
	    c = '\t';
	    break;
	case 'n':
	    c = '\n';
	    break;
	case '\\':
	    c = '\\';
	    break;
	case 'v':
	    c = '\v';
	    break;
	case 'b':
	    c = '\b';
	    break;
	case 'f':
	    c = '\f';
	    break;
	case 'r':
	    c = '\r';
	    break;
	case '\'':
	    c = '\'';
	    break;
	default:
	    c = 0;
	    break;
    }

    return c;
}

/* Help text. Module help text is displayed after this. */
static void usage_header()
{
    printf("Usage: %s [-vhVL] [-c <filename>] [-t fmt] [-m c] [-F c] [-d]\n"
	   "\t[[-xX] -O <module1> [options] [-- [-xX] -O <module2> [...]]]\n"
	   "\t[- | username | -f filename] [...]\n\n", __progname);
    return;
}

/* Help text. Module help text is displayed before this. */
static void usage()
{
    printf("  -d\tLoad the default modules (passwd.so, mail.so, and login.so).\n");
    printf("  -c\tRead a configuration file. Can be used more than once.\n");
    printf("  -O\tLoad a module. Can be used more than once.\n");
    printf("  -x\tChain module1's output to module2's input.\n");
    printf("  -X\tDon't output module1's info, only chain it.\n");
    printf("  -F c\tSeparate output with the specified character "
	   "('%c').\n", delimchar);
    printf("  -m c\tSeparate multi-string values with the specified "
	   "character ('%c').\n", multichar);
    printf("  -t tf\tstrftime(3) time format ('%s').\n", DEFAULT_TIMEFORMAT);
    printf("  -f\tUsers are the owners of the specified files.\n");
    printf("  -L\tFollow symbolic links.\n");
    printf("  -v\tVerbose output when possible (twice for all modules).\n");
    printf("  -h\tThis help text.\n");
    printf("  -V\tVersion information.\n\n");
    printf("Output key: %s=unknown/error, %s=none, %s=yes/on, "
	   "%s=no/off\n", UNKNOWN, NONE, ON, OFF);
    return;
}

/*
 * Add a module to the array of loaded modules. The index argument is the
 * current item number being added stored in an integer. The module is also
 * initialized here with ui_module_init().
 */
static int open_module(char *filename, int *idx)
{
    void *m;
    module_init *init;
    char *p, s[FILENAME_MAX];
    int i;
    int chainable = 0;

    strncpy(s, filename, sizeof(s));

    if ((p = strrchr(s, '/')) != NULL)
	p++;
    else {
	strncpy(s, filename, sizeof(s));
	p = s;
    }

    for (i = 0; i < module_index; i++) {
	if (strcmp(p, modules[i].name) == 0) {
	    if (TEST_FLAG(modules[i].flags, MODULE_DUP))
		break;

	    SET_FLAG(modules[i].flags, MODULE_DUP);
	    warnx("%s: a module by this name is already loaded", p);
	}
    }

    if ((m = dlopen(filename, RTLD_NOW)) == NULL) {
	warnx("%s", dlerror());
	chaining = 0;
	chain_output = 1;
	return 1;
    }

    modules = Realloc(modules, (module_index + 2) * sizeof(struct module));
    modules[module_index].m = m;

    strncpy(modules[module_index].name, p, sizeof(modules[module_index].name));
    *idx = module_index++;

    if ((init = dlsym(modules[*idx].m, "ui_module_init")) == NULL)
	warnx("%s", dlerror());
    else
	(*init) (&chainable);

    if (chainable)
	SET_FLAG(modules[*idx].flags, MODULE_CHAINABLE);

    if (*idx - 1 >= 0 && TEST_FLAG(modules[*idx - 1].flags, MODULE_CHAINED) &&
	    !TEST_FLAG(modules[*idx].flags, MODULE_CHAINABLE)) {
	warnx("%s: this module is not chainable", modules[*idx].name);
	return 1;
    }

    /* Module chaining. See junction() for more info. */
    if (chaining)
	SET_FLAG(modules[*idx].flags, MODULE_CHAINED);

    if (chain_output)
	SET_FLAG(modules[*idx].flags, MODULE_OUTPUT);

    if (verbose)
	SET_FLAG(modules[*idx].flags, MODULE_VERBOSE);

    chaining = 0;
    chain_output = 1;
    verbose = (verbose < 2) ? 0 : 2;

    return 0;
}

/* This just free's up the array of modules. The modules should clean up after
 * themselves via the ui_module_exit() function. */
static void cleanup_modules()
{
    int i;

    for (i = 0; i < module_index; i++) {
	module_exit *e;

	if ((e = dlsym(modules[i].m, "ui_module_exit")) == NULL)
	    warnx("%s", dlerror());
	else
	    (*e) ();

	dlclose(modules[i].m);
    }

    free(modules);
    return;
}

static void output(char **s, const int sep, int which)
{
    int i;

    if (s) {
	for (i = 0; s[i]; i++) {
	    printf("%s", s[i]);

	    if (s[i + 1])
		printf("%c", sep);
	}
    }

    if (which == OUTPUT_DONE)
	printf("\n");
    else if (which == OUTPUT_APPEND)
	printf("%c", sep);

    return;
}

/* Pass the argument to each loaded module. */
static int junction(const char *arg)
{
    int i;
    struct passwd *pw;
    struct stat st;
    int ret = EXIT_SUCCESS;
    char **s = NULL;

    if (usefile) {
	if ((STAT(arg, &st)) == -1) {
	    warn("%s", arg);
	    return EXIT_FAILURE;
	}

	errno = 0;

	if ((pw = getpwuid(st.st_uid)) == NULL) {
#ifdef __NetBSD__
	    warnx("%s: no such user", arg);
#else
	    if (errno == 0 || errno == ENOENT || errno == EPERM
		|| errno == EBADF || errno == ESRCH)
		warnx("%s: no such uid %u", arg, st.st_uid);
	    else
		warn("%s", "getpwuid()");
#endif

	    return EXIT_FAILURE;
	}
    }
    else {
	errno = 0;

	if ((pw = getpwnam(arg)) == NULL) {
#ifdef __NetBSD__
	    warnx("%s: no such user", arg);
#else
	    if (errno == 0 || errno == ENOENT || errno == EPERM
		|| errno == EBADF || errno == ESRCH)
		warnx("%s: no such user", arg);
	    else
		warn("%s", "getpwnam()");
#endif

	    return EXIT_FAILURE;
	}
    }

    for (i = 0; i < module_index; i++) {
	module_exec *m_exec;
	char **p;

	if ((m_exec = dlsym(modules[i].m, "ui_module_exec")) == NULL) {
	    warnx("%s", dlerror());
	    continue;
	}

	ret |= (*m_exec) (&s, pw, multichar, TEST_FLAG(modules[i].flags, MODULE_VERBOSE), tf);

	if (!TEST_FLAG(modules[i].flags, MODULE_CHAINED) || (TEST_FLAG(modules[i].flags, MODULE_CHAINED) && TEST_FLAG(modules[i].flags, MODULE_OUTPUT))) {
	    output(s, delimchar,
		    ((i + 1) < module_index) ? OUTPUT_APPEND : OUTPUT_DONE);

	    if (!TEST_FLAG(modules[i].flags, MODULE_CHAINED)) {
		for (p = s; *p; p++) {
		    free(*p);
		}

		free(s);
		s = NULL;
	    }
	}
    }

    return ret;
}

/* Copy options for each module into it's own argc and argv variables stopping
 * at -- (getopt(3)). */
static int init_module_options(int the_argc, char **the_argv, struct module mod)
{
    char tmp[255];
    module_options *m;
    module_options_init *o;
    int old_optind = optind;
    int argc = 0;
    char **argv = NULL;
    int opt;
    int ret = EXIT_SUCCESS;
    char *optstring = NULL;
    char *defaults = NULL;
    int have_an_argument = 0;

    if ((o = dlsym(mod.m, "ui_module_options_init")) == NULL) {
	warnx("%s", dlerror());
	return EXIT_FAILURE;
    }

    if ((optstring = (*o) (&defaults))) {
	argv = Realloc(argv, (argc + 2) * sizeof(char *));
	argv[argc++] = strdup(__progname);
	argv[argc] = NULL;

	/* Probably a default module. */
	if (the_argv == NULL)
	    goto blah;

	while ((opt = getopt(the_argc, the_argv, optstring)) != -1) {
	    switch (opt) {
		case '?':
		    warnx("%s: invalid option -- %c\n", mod.name,
			  optopt);
		    return EXIT_FAILURE;
		default:
		    break;
	    }

	    argv = Realloc(argv, (argc + 2) * sizeof(char *));
	    snprintf(tmp, sizeof(tmp), "-%c%s", opt, (optarg) ? optarg : "");
	    argv[argc++] = strdup(tmp);
	    argv[argc] = NULL;
	    have_an_argument = 1;
	}
    }
    else
	goto skip_option_stuff;

blah:
    /*
     * No options were specified for this module. Set the modules default
     * options (ui_module_options_init()) if any. 
     */
    if (!have_an_argument && defaults) {
	argv = Realloc(argv, (argc + 2) * sizeof(char *));
	snprintf(tmp, sizeof(tmp), "-%s", defaults);
	argv[argc++] = strdup(tmp);
	argv[argc] = NULL;
    }

    old_optind = optind;
    opterr = optind = optopt = 1;

    if ((m = dlsym(mod.m, "ui_module_options")) == NULL) {
	warnx("%s", dlerror());
	return EXIT_FAILURE;
    }

    ret |= (*m) (argc, argv);
    optind = old_optind;

skip_option_stuff:
    return ret;
}

/* 
 * parseargs.c
 *
 * This will parse a line used as an argument list for the exec() line of
 * functions returning a dynamically allocated array of character pointers so
 * you should free() it afterwards. Both ' and " quoting is supported (with
 * escapes) for multi-word arguments.
 *
 * This is my second attempt at it. Works alot better than the first. :)
 *
 * 2002/10/05
 * Ben Kibbey <bjk@luxsci.net>
 *
 * 2004/11/07
 *     Modified to handle argv[0] and argc. (Ben Kibbey <bjk@luxsci.net>)
 */
static char **parseargv(char *str, const char *progname, int *me_argc)
{
    char **pptr, *s;
    char arg[255];
    int idx = 0;
    int quote = 0;
    int lastchar = 0;
    int i;
    int my_argc = 0;

    if (!str)
	return NULL;

    if (!(pptr = malloc(sizeof(char *))))
	return NULL;

    pptr = Realloc(pptr, (idx + 2) * sizeof(char *));
    pptr[idx++] = strdup(progname);
    my_argc++;

    for (i = 0, s = str; *s; lastchar = *s++) {
	if ((*s == '\"' || *s == '\'') && lastchar != '\\') {
	    quote = (quote) ? 0 : 1;
	    continue;
	}

	if (*s == ' ' && !quote) {
	    arg[i] = 0;
	    pptr = Realloc(pptr, (idx + 2) * sizeof(char *));
	    pptr[idx++] = strdup(arg);
	    my_argc++;
	    arg[0] = i = 0;
	    continue;
	}

	if ((i + 1) == sizeof(arg))
	    continue;

	arg[i++] = *s;
    }

    arg[i] = 0;

    if (arg[0]) {
	pptr = Realloc(pptr, (idx + 2) * sizeof(char *));
	pptr[idx++] = strdup(arg);
	my_argc++;
    }

    pptr[idx] = NULL;
    *me_argc = my_argc;
    return pptr;
}

static char *get_home_directory()
{
    struct passwd *pw;
    static char dir[FILENAME_MAX];

    errno = 0;

    if ((pw = getpwuid(getuid())) == NULL) {
	if (errno)
	    warn("getpwuid()");
	else
	    warnx("getpwuid(): no such uid");

	return NULL;
    }

    strncpy(dir, pw->pw_dir, sizeof(dir));
    return dir;
}

/* Read in a configuration file adding modules to the module array and
 * checking any module options. */
static int parse_rc_file(const char *filename)
{
    char line[LINE_MAX], *p;
    FILE *fp;
    int idx;
    int old_optind = optind;
    
    if ((fp = fopen(filename, "r")) == NULL) {
	warn("%s", filename);
	return 1;
    }

    while ((p = fgets(line, sizeof(line), fp)) != NULL) {
	char name[FILENAME_MAX], options[LINE_MAX], tmp[LINE_MAX], *s;
	int my_argc;
	char **my_argv;
	int lastchar = '\0';

	while (*p && isspace((unsigned char) *p))
	    p++;

	if (*p == '#')
	    continue;

	s = name;

	if (*p == '>' || *p == '-') {
	    chaining = 1;

	    if (*p == '-')
		chain_output = 0;

	    p++;
	}

	while (*p && *p != ' ' && *p != '\t') {
	    if (*p == '\n') {
		p++;
		break;
	    }

	    *s++ = *p++;
	}

	*s = '\0';

	if (!name[0])
	    continue;

	s = options;

	while (*p && isspace((unsigned char) *p))
	    p++;

	lastchar = *p;

	while (*p) {
	    if (*p == '\n' || (*p == '#' && lastchar != '\\'))
		break;

	    if (*p == '#' && lastchar == '\\') {
		lastchar = *--s = *p++;
		s++;
		continue;
	    }

	    lastchar = *s++ = *p++;
	}

	*s = '\0';
	p = name;

	if (*p == '~') {
	    s = get_home_directory();
	    strncpy(tmp, s, sizeof(tmp));
	    p++;
	    strncat(tmp, p, sizeof(tmp));
	    strncpy(name, tmp, sizeof(name));
	}

	if (open_module(name, &idx))
	    continue;

	if ((my_argv = parseargv(options, __progname, &my_argc)) == NULL)
	    continue;

	optind = 0;

	if (init_module_options(my_argc, my_argv, modules[idx])) {
	    fclose(fp);
	    return 2;
	}

	optind = old_optind;
    }

    fclose(fp);
    return 0;
}

int main(int argc, char *argv[])
{
    int i = 0;
    int ret = EXIT_SUCCESS;
    int opt;
    char line[LINE_MAX], *s = NULL;
    int want_help = 0;

#ifndef HAVE___PROGNAME
    __progname = argv[0];
#endif
    delimchar = DEFAULT_DELIMINATING_CHAR;
    multichar = DEFAULT_MULTI_CHAR;
    strncpy(tf, DEFAULT_TIMEFORMAT, sizeof(tf));
    chain_output = 1;

    while ((opt = getopt(argc, argv, "+x:X:dm:c:hO:F:t:vVLf")) != -1) {
	/*
	 * See getopt(3). 
	 */
	opterr = 0;

	switch (opt) {
	    case 'd':
		i = module_index;

		if (open_module("passwd.so", &i) == 0) {
		    if (init_module_options(1, NULL, modules[i]))
			want_help = 1;
		}
		else {
		    ret = EXIT_FAILURE;
		    goto cleanup;
		}

		if (open_module("mail.so", &i) == 0) {
		    if (init_module_options(1, NULL, modules[i]))
			want_help = 1;
		}
		else {
		    ret = EXIT_FAILURE;
		    goto cleanup;
		}

		if (open_module("login.so", &i) == 0) {
		    if (init_module_options(1, NULL, modules[i]))
			want_help = 1;
		}
		else {
		    ret = EXIT_FAILURE;
		    goto cleanup;
		}

		break;
	    case 'm':
		if ((optarg[0] != '\\' && strlen(optarg) > 1) ||
		    (optarg[0] == '\\' && strlen(optarg) != 2)) {
		    want_help = 1;
		    break;
		}

		if ((multichar = escapes(optarg)) == 0)
		    want_help = 1;

		break;
	    case 'c':
		if ((ret = parse_rc_file(optarg)) != 0) {
		    if (ret == 2)
			want_help = 1;
		    else
			exit(EXIT_FAILURE);
		}
		break;
	    case 'F':
		if ((optarg[0] != '\\' && strlen(optarg) > 1) ||
		    (optarg[0] == '\\' && strlen(optarg) != 2)) {
		    want_help = 1;
		    break;
		}

		if ((delimchar = escapes(optarg)) == 0)
		    want_help = 1;

		break;
	    case 't':
		strncpy(tf, optarg, sizeof(tf));
		break;
	    case 'V':
		printf("%s\n%s\n", PACKAGE_STRING, COPYRIGHT);
		exit(EXIT_SUCCESS);
		break;
	    case 'L':
		followsymlinks = 1;
		break;
	    case 'f':
		usefile = 1;
		break;
	    case 'v':
		verbose++;
		break;
	    case 'X':
		chain_output = 0;
	    case 'x':
		chaining = 1;
	    case 'O':
		if (open_module(optarg, &i)) {
		    ret = EXIT_FAILURE;
		    goto cleanup;
		}

		if (init_module_options(argc, argv, modules[i]))
		    want_help = 1;

		/*
		 * For modules which have no options at all (to keep getopt
		 * from interpreting the rest as arguments.
		 */
		if (optind < argc) {
		    if (strcmp(argv[optind], "--") == 0)
			optind++;
		}

		break;
	    case 'h':
	    default:
		want_help = 1;
		break;
	}
    }

    /* The last module cannot be chained (syntax). */
    if (!module_index || TEST_FLAG(modules[module_index - 1].flags, MODULE_CHAINED))
	want_help = 1;

    /* Cycle through the modules and output their help text. */
    if (want_help) {
	usage_header();

	for (i = 0; i < module_index; i++) {
	    module_help *m_help;

	    if (TEST_FLAG(modules[i].flags, MODULE_DUP))
		continue;

	    if ((m_help = dlsym(modules[i].m, "ui_module_help")) == NULL) {
		warnx("%s", dlerror());
		continue;
	    }

	    fprintf(stderr, "%s\n", modules[i].name);
	    (*m_help) ();
	}

	usage();
	cleanup_modules();
	exit(EXIT_FAILURE);
    }

    if (argc == optind || strcmp(argv[optind], "-") == 0) {
	while ((s = fgets(line, sizeof(line), stdin)) != NULL) {
	    if (s[strlen(s) - 1] == '\n')
		s[strlen(s) - 1] = '\0';

	    ret |= junction(s);
	}
    }
    else {
	for (; optind < argc; optind++)
	    ret |= junction(argv[optind]);
    }

cleanup:
    cleanup_modules();
    exit(ret);
}


syntax highlighted by Code2HTML, v. 0.9.1