/* modprobe.c: insert a module into the kernel, intelligently.

   Modified/trimmed by Aaron Griffin, made for early userspace
   under klibc.

   Copyright (C) 2006, Aaron Griffin <aaronmgriffin@gmail.com>

   Original:
   Copyright (C) 2001  Rusty Russell.
   Copyright (C) 2002, 2003  Rusty Russell, IBM Corporation.

   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 <sys/utsname.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <limits.h>
#include <elf.h>
#include <asm/unistd.h>
#include <sys/wait.h>

#include "list.h"

#define streq(a,b) (strcmp((a),(b)) == 0)
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

#define MODULE_DIR "/lib/modules"

int quiet = 0;

static const char *default_configs[] = 
{
    "/etc/modprobe.conf",
    "/etc/modprobe.d",
};

/* From udev_utils_string.h 
 * int strcmp_pattern(const char *p, const char *s)
 */
int fnmatch(const char *p, const char *s)
{
    if (s[0] == '\0') {
        while (p[0] == '*')
            p++;
        return (p[0] != '\0');
    }
    switch (p[0]) {
    case '[':
        {
            int not = 0;
            p++;
            if (p[0] == '!') {
                not = 1;
                p++;
            }
            while ((p[0] != '\0') && (p[0] != ']')) {
                int match = 0;
                if (p[1] == '-') {
                    if ((s[0] >= p[0]) && (s[0] <= p[2]))
                        match = 1;
                    p += 3;
                } else {
                    match = (p[0] == s[0]);
                    p++;
                }
                if (match ^ not) {
                    while ((p[0] != '\0') && (p[0] != ']'))
                        p++;
                    if (p[0] == ']')
                        return fnmatch(p+1, s+1);
                }
            }
        }
        break;
    case '*':
        if (fnmatch(p, s+1))
            return fnmatch(p+1, s);
        return 0;
    case '\0':
        if (s[0] == '\0') {
            return 0;
        }
        break;
    default:
        if ((p[0] == s[0]) || (p[0] == '?'))
            return fnmatch(p+1, s+1);
        break;
    }
    return 1;
}

void *grab_fd(int fd, unsigned long *size)
{
    struct stat st;
    void *map;

    fstat(fd, &st);
    *size = st.st_size;
    map = mmap(0, *size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
    if (map == MAP_FAILED)
        map = NULL;
    return map;
}

void release_file(void *data, unsigned long size)
{
    munmap(data, size);
}

extern long init_module(void *, unsigned long, const char *);
extern long delete_module(const char *, unsigned int);

struct module {
    struct list_head list;
    char *modname;
    char filename[0];
};

typedef void (*errfn_t)(const char *fmt, ...);

static void message(const char *prefix, const char *fmt, va_list *arglist)
{
    char *buf, *buf2;

    if (!quiet) {
        vasprintf(&buf, fmt, *arglist);
        asprintf(&buf2, "%s%s", prefix, buf);

        fprintf(stderr, "%s", buf2);
        free(buf2);
        free(buf);
    }
}

static int warned = 0;
static void warn(const char *fmt, ...)
{
    va_list arglist;
    warned++;
    va_start(arglist, fmt);
    message("WARNING: ", fmt, &arglist);
    va_end(arglist);
}

static void fatal(const char *fmt, ...)
{
    va_list arglist;
    va_start(arglist, fmt);
    message("FATAL: ", fmt, &arglist);
    va_end(arglist);
    exit(1);
}


static void grammar(const char *cmd, const char *filename, unsigned int line)
{
    warn("%s line %u: ignoring bad line starting with '%s'\n",
         filename, line, cmd);
}

static void *do_nofail(void *ptr, const char *file, int line, const char *expr)
{
    if (!ptr) {
        fatal("Memory allocation failure %s line %d: %s.\n",
              file, line, expr);
    }
    return ptr;
}

#define NOFAIL(ptr)	do_nofail((ptr), __FILE__, __LINE__, #ptr)

static void print_usage(const char *progname)
{
    fprintf(stderr,
            "Usage: %s [-v] [-V] [-C config-file] [-i] [-q] [-b] <modname> [parameters...]\n",
            progname);
    exit(1);
}

static int fgetc_wrapped(FILE *file, unsigned int *linenum)
{
    for (;;) {
        int ch = fgetc(file);
        if (ch != '\\')
            return ch;
        ch = fgetc(file);
        if (ch != '\n')
            return ch;
        if (linenum)
            (*linenum)++;
    }
}

static char *getline_wrapped(FILE *file, unsigned int *linenum)
{
    int size = 1024;
    int i = 0;
    char *buf = NOFAIL(malloc(size));
    for(;;) {
        int ch = fgetc_wrapped(file, linenum);
        if (i == size) {
            size *= 2;
            buf = NOFAIL(realloc(buf, size));
        }
        if (ch < 0 && i == 0) {
            free(buf);
            return NULL;
        }
        if (ch < 0 || ch == '\n') {
            if (linenum)
                (*linenum)++;
            buf[i] = '\0';
            return NOFAIL(realloc(buf, i+1));
        }
        buf[i++] = ch;
    }
}

static struct module *find_module(const char *filename, struct list_head *list)
{
    struct module *i;

    list_for_each_entry(i, list, list) {
        if (strcmp(i->filename, filename) == 0)
            return i;
    }
    return NULL;
}

/* Convert filename to the module name.  Works if filename == modname, too. */
static void filename2modname(char *modname, const char *filename)
{
    const char *afterslash;
    unsigned int i;

    afterslash = strrchr(filename, '/');
    if (!afterslash)
        afterslash = filename;
    else
        afterslash++;

    /* Convert to underscores, stop at first . */
    for (i = 0; afterslash[i] && afterslash[i] != '.'; i++) {
        if (afterslash[i] == '-')
            modname[i] = '_';
        else
            modname[i] = afterslash[i];
    }
    modname[i] = '\0';
}

static int lock_file(const char *filename)
{
    int fd = open(filename, O_RDWR, 0);

    if (fd >= 0) {
        struct flock lock;
        lock.l_type = F_WRLCK;
        lock.l_whence = SEEK_SET;
        lock.l_start = 0;
        lock.l_len = 1;
        fcntl(fd, F_SETLKW, &lock);
    } else
        /* Read-only filesystem?  There goes locking... */
        fd = open(filename, O_RDONLY, 0);
    return fd;
}

static void unlock_file(int fd)
{
    /* Valgrind is picky... */
    close(fd);
}

static void add_module(char *filename, int namelen, struct list_head *list)
{
    struct module *mod;

    /* If it's a duplicate: move it to the end, so it gets
       inserted where it is *first* required. */
    mod = find_module(filename, list);
    if (mod)
        list_del(&mod->list);
    else {
        /* No match.  Create a new module. */
        mod = NOFAIL(malloc(sizeof(struct module) + namelen + 1));
        memcpy(mod->filename, filename, namelen);
        mod->filename[namelen] = '\0';
        mod->modname = NOFAIL(malloc(namelen + 1));
        filename2modname(mod->modname, mod->filename);
    }

    list_add_tail(&mod->list, list);
}

/* Compare len chars of a to b, with _ and - equivalent. */
static int modname_equal(const char *a, const char *b, unsigned int len)
{
    unsigned int i;

    if (strlen(b) != len)
        return 0;

    for (i = 0; i < len; i++) {
        if ((a[i] == '_' || a[i] == '-')
            && (b[i] == '_' || b[i] == '-'))
            continue;
        if (a[i] != b[i])
            return 0;
    }
    return 1;
}

/* Fills in list of modules if this is the line we want. */
static int add_modules_dep_line(char *line,
                                const char *name,
                                struct list_head *list)
{
    char *ptr;
    int len;
    char *modname;

    /* Ignore lines without : or which start with a # */
    ptr = index(line, ':');
    if (ptr == NULL || line[strspn(line, "\t ")] == '#')
        return 0;

    /* Is this the module we are looking for? */
    *ptr = '\0';
    if (strrchr(line, '/'))
        modname = strrchr(line, '/') + 1;
    else
        modname = line;

    len = strlen(modname);
    if (strchr(modname, '.'))
        len = strchr(modname, '.') - modname;
    if (!modname_equal(modname, name, len))
        return 0;

    /* Create the list. */
    add_module(line, ptr - line, list);

    ptr++;
    for(;;) {
        char *dep_start;
        ptr += strspn(ptr, " \t");
        if (*ptr == '\0')
            break;
        dep_start = ptr;
        ptr += strcspn(ptr, " \t");
        add_module(dep_start, ptr - dep_start, list);
    }
    return 1;
}

static void read_depends(const char *dirname,
                         const char *start_name,
                         struct list_head *list)
{
    char *modules_dep_name;
    char *line;
    FILE *modules_dep;
    int done = 0;

    asprintf(&modules_dep_name, "%s/%s", dirname, "modules.dep");
    modules_dep = fopen(modules_dep_name, "r");
    if (!modules_dep)
        fatal("Could not load %s: %s\n",
              modules_dep_name, strerror(errno));

    /* Stop at first line, as we can have duplicates (eg. symlinks
       from boot/ */
    while (!done && (line = getline_wrapped(modules_dep, NULL)) != NULL) {
        done = add_modules_dep_line(line, start_name, list);
        free(line);
    }
    fclose(modules_dep);
    free(modules_dep_name);
}

/* We use error numbers in a loose translation... */
static const char *insert_moderror(int err)
{
    switch (err) {
    case ENOEXEC:
        return "Invalid module format";
    case ENOENT:
        return "Unknown symbol in module, or unknown parameter (see dmesg)";
    case ENOSYS:
        return "Kernel does not have module support";
    default:
        return strerror(err);
    }
}

static const char *remove_moderror(int err)
{
    switch (err) {
    case ENOENT:
        return "No such module";
    case ENOSYS:
        return "Kernel does not have module unloading support";
    default:
        return strerror(err);
    }
}

/* Is module in /proc/modules?  If so, fill in usecount if not NULL. 
   0 means no, 1 means yes, -1 means unknown.
   */
static int module_in_kernel(const char *modname, unsigned int *usecount)
{
    FILE *proc_modules;
    char *line;

again:
    /* Might not be mounted yet.  Don't fail. */
    proc_modules = fopen("/proc/modules", "r");
    if (!proc_modules)
        return -1;

    while ((line = getline_wrapped(proc_modules, NULL)) != NULL) {
        char *entry = strtok(line, " \n");

        if (entry && streq(entry, modname)) {
            /* If it exists, usecount is the third entry. */
            if (!strtok(NULL, " \n"))
                goto out;

            if (!(entry = strtok(NULL, " \n"))) /* usecount */
                goto out;
            else
                if (usecount)
                    *usecount = atoi(entry);

            /* Followed by - then status. */
            if (strtok(NULL, " \n")
                && (entry = strtok(NULL, " \n")) != NULL) {
                /* Locking will fail on ro fs, we might hit
                 * cases where module is in flux.  Spin. */
                if (streq(entry, "Loading")
                    || streq(entry, "Unloading")) {
                    usleep(100000);
                    free(line);
                    fclose(proc_modules);
                    goto again;
                }
            }

out:
            free(line);
            fclose(proc_modules);
            return 1;
        }
        free(line);
    }
    fclose(proc_modules);
    return 0;
}

struct module_options
{
    struct module_options *next;
    char *modulename;
    char *options;
};

struct module_command
{
    struct module_command *next;
    char *modulename;
    char *command;
};

struct module_alias
{
    struct module_alias *next;
    char *module;
};

struct module_blacklist
{
    struct module_blacklist *next;
    char *modulename;
};

/* Link in a new option line from the config file. */
static struct module_options *
add_options(const char *modname,
            const char *option,
            struct module_options *options)
{
    struct module_options *new;
    char *tab; 

    new = NOFAIL(malloc(sizeof(*new)));
    new->modulename = NOFAIL(strdup(modname));
    new->options = NOFAIL(strdup(option));
    /* We can handle tabs, kernel can't. */
    for (tab = strchr(new->options, '\t'); tab; tab = strchr(tab, '\t'))
        *tab = ' ';
    new->next = options;
    return new;
}

/* Link in a new install line from the config file. */
static struct module_command *
add_command(const char *modname,
            const char *command,
            struct module_command *commands)
{
    struct module_command *new;

    new = NOFAIL(malloc(sizeof(*new)));
    new->modulename = NOFAIL(strdup(modname));
    new->command = NOFAIL(strdup(command));
    new->next = commands;
    return new;
}

/* Link in a new alias line from the config file. */
static struct module_alias *
add_alias(const char *modname, struct module_alias *aliases)
{
    struct module_alias *new;

    new = NOFAIL(malloc(sizeof(*new)));
    new->module = NOFAIL(strdup(modname));
    new->next = aliases;
    return new;
}

/* Link in a new blacklist line from the config file. */
static struct module_blacklist *
add_blacklist(const char *modname, struct module_blacklist *blacklist)
{
    struct module_blacklist *new;

    new = NOFAIL(malloc(sizeof(*new)));
    new->modulename = NOFAIL(strdup(modname));
    new->next = blacklist;
    return new;
}

/* Find blacklist commands if any. */
static  int
find_blacklist(const char *modname, const struct module_blacklist *blacklist)
{
    while (blacklist) {
        if (strcmp(blacklist->modulename, modname) == 0)
            return 1;
        blacklist = blacklist->next;
    }
    return 0;
}

/* return a new alias list, with backlisted elems filtered out */
static struct module_alias *
apply_blacklist(const struct module_alias *aliases,
                const struct module_blacklist *blacklist)
{
    struct module_alias *result = NULL;
    while (aliases) {
        char *modname = aliases->module;
        if (!find_blacklist(modname, blacklist))
            result = add_alias(modname, result);
        aliases = aliases->next;
    }
    return result;
}

/* Find install commands if any. */
static const char *find_command(const char *modname,
                                const struct module_command *commands)
{
    while (commands) {
        if (fnmatch(commands->modulename, modname) == 0)
            return commands->command;
        commands = commands->next;
    }
    return NULL;
}

static char *append_option(char *options, const char *newoption)
{
    options = NOFAIL(realloc(options, strlen(options) + 1
                             + strlen(newoption) + 1));
    if (strlen(options)) strcat(options, " ");
    strcat(options, newoption);
    return options;
}

/* Add to options */
static char *add_extra_options(const char *modname,
                               char *optstring,
                               const struct module_options *options)
{
    while (options) {
        if (strcmp(options->modulename, modname) == 0)
            optstring = append_option(optstring, options->options);
        options = options->next;
    }
    return optstring;
}

/* If we don't flush, then child processes print before we do */
static void verbose_printf(int verbose, const char *fmt, ...)
{
    va_list arglist;

    if (verbose) {
        va_start(arglist, fmt);
        vprintf(fmt, arglist);
        fflush(stdout);
        va_end(arglist);
    }
}

/* Do an install/remove command: replace $CMDLINE_OPTS if it's specified. */
static void do_command(const char *modname,
                       const char *command,
                       int verbose,
                       errfn_t error,
                       const char *type,
                       const char *cmdline_opts)
{
    int ret;
    char *p, *replaced_cmd = NOFAIL(strdup(command));

    while ((p = strstr(replaced_cmd, "$CMDLINE_OPTS")) != NULL) {
        char *new;
        asprintf(&new, "%.*s%s%s",
                 p - replaced_cmd, replaced_cmd, cmdline_opts,
                 p + strlen("$CMDLINE_OPTS"));
        NOFAIL(new);
        free(replaced_cmd);
        replaced_cmd = new;
    }

    verbose_printf(verbose, "%s %s\n", type, replaced_cmd);

    ret = system(replaced_cmd);
    if (ret == -1 || WEXITSTATUS(ret))
        error("Error running %s command for %s\n", type, modname);
    free(replaced_cmd);
}

/* Actually do the insert.  Frees second arg. */
static void insmod(struct list_head *list,
                   char *optstring,
                   errfn_t error,
                   int verbose,
                   const struct module_options *options,
                   const struct module_command *commands,
                   int ignore_commands,
                   const char *cmdline_opts)
{
    int ret, fd;
    unsigned long len;
    void *map;
    const char *command;
    struct module *mod = list_entry(list->next, struct module, list);

    /* Take us off the list. */
    list_del(&mod->list);

    /* Do things we (or parent) depend on first, but don't die if
     * they fail. */
    if (!list_empty(list)) {
        insmod(list, NOFAIL(strdup("")), warn,
               verbose, options, commands, 0, 
               cmdline_opts);
    }

    /* Lock before we look, in case it's initializing. */
    fd = lock_file(mod->filename);
    if (fd < 0) {
        error("Could not open '%s': %s\n",
              mod->filename, strerror(errno));
        goto out_optstring;
    }

    /* Don't do ANYTHING if already in kernel. */
    if (module_in_kernel(mod->modname, NULL) == 1) {
        goto out_unlock;
    }

    command = find_command(mod->modname, commands);
    if (command && !ignore_commands) {
        /* It might recurse: unlock. */
        unlock_file(fd);
        do_command(mod->modname, command, verbose, error,
                   "install", cmdline_opts);
        goto out_optstring;
    }

    map = grab_fd(fd, &len);
    if (!map) {
        error("Could not read '%s': %s\n",
              mod->filename, strerror(errno));
        goto out_unlock;
    }

    /* Config file might have given more options */
    optstring = add_extra_options(mod->modname, optstring, options);

    verbose_printf(verbose, "insmod %s %s\n", mod->filename, optstring);

    ret = init_module(map, len, optstring);
    if (ret != 0) {
        if (errno == EEXIST) {
            goto out_unlock;
        }
        error("Error inserting %s (%s): %s\n",
              mod->modname, mod->filename, insert_moderror(errno));
    }

    release_file(map, len);
out_unlock:
    unlock_file(fd);
out_optstring:
    free(optstring);
    return;
}

/* Do recursive removal. */
static void rmmod(struct list_head *list,
                  const char *name,
                  errfn_t error,
                  int verbose,
                  struct module_command *commands,
                  int ignore_commands,
                  int ignore_inuse,
                  const char *cmdline_opts)
{
    const char *command;
    unsigned int usecount = 0;
    int lock;
    struct module *mod = list_entry(list->next, struct module, list);

    /* Take first one off the list. */
    list_del(&mod->list);

    /* Ignore failure; it's best effort here. */
    lock = lock_file(mod->filename);

    if (!name)
        name = mod->modname;

    /* Even if renamed, find commands to orig. name. */
    command = find_command(mod->modname, commands);
    if (command && !ignore_commands) {
        /* It might recurse: unlock. */
        unlock_file(lock);
        do_command(mod->modname, command, verbose, error,
                   "remove", cmdline_opts);
        goto remove_rest_no_unlock;
    }

    if (module_in_kernel(name, &usecount) == 0)
        goto nonexistent_module;

    if (usecount != 0) {
        if (!ignore_inuse)
            error("Module %s is in use.\n", name);
        goto remove_rest;
    }

    verbose_printf(verbose, "rmmod %s\n", mod->filename);

    if (delete_module(name, O_EXCL) != 0) {
        if (errno == ENOENT)
            goto nonexistent_module;
        error("Error removing %s (%s): %s\n",
              name, mod->filename,
              remove_moderror(errno));
    }

remove_rest:
    unlock_file(lock);
remove_rest_no_unlock:
    /* Now do things we depend. */
    if (!list_empty(list))
        rmmod(list, NULL, warn, verbose, commands,
              0, 1, cmdline_opts);
    return;

nonexistent_module:
    goto remove_rest;
}

static char *underscores(char *string)
{
    if (string) {
        unsigned int i;
        for (i = 0; string[i]; i++)
            if (string[i] == '-')
                string[i] = '_';
    }
    return string;
}

static char *strsep_skipspace(char **string, char *delim)
{
    if (!*string)
        return NULL;
    *string += strspn(*string, delim);
    return strsep(string, delim);
}

/* Recursion */
static int read_config(const char *filename,
                       const char *name,
                       int removing,
                       struct module_options **options,
                       struct module_command **commands,
                       struct module_alias **alias,
                       struct module_blacklist **blacklist);

/* FIXME: Maybe should be extended to "alias a b [and|or c]...". --RR */
static int read_config_file(const char *filename,
                            const char *name,
                            int removing,
                            struct module_options **options,
                            struct module_command **commands,
                            struct module_alias **aliases,
                            struct module_blacklist **blacklist)
{
    char *line;
    unsigned int linenum = 0;
    FILE *cfile;

    cfile = fopen(filename, "r");
    if (!cfile)
        return 0;

    while ((line = getline_wrapped(cfile, &linenum)) != NULL) {
        char *ptr = line;
        char *cmd, *modname;

        cmd = strsep_skipspace(&ptr, "\t ");
        if (cmd == NULL || cmd[0] == '#' || cmd[0] == '\0')
            continue;

        if (strcmp(cmd, "alias") == 0) {
            char *wildcard
                = underscores(strsep_skipspace(&ptr, "\t "));
            char *realname
                = underscores(strsep_skipspace(&ptr, "\t "));

            if (!wildcard || !realname)
                grammar(cmd, filename, linenum);
            else if (fnmatch(wildcard,name) == 0)
                *aliases = add_alias(realname, *aliases);
        } else if (strcmp(cmd, "include") == 0) {
            struct module_alias *newalias = NULL;
            char *newfilename;

            newfilename = strsep_skipspace(&ptr, "\t ");
            if (!newfilename)
                grammar(cmd, filename, linenum);
            else {
                if (!read_config(newfilename, name,
                                 removing,
                                 options, commands, &newalias,
                                 blacklist))
                    warn("Failed to open included"
                         " config file %s: %s\n",
                         newfilename, strerror(errno));

                /* Files included override aliases,
                   etc that was already set ... */
                if (newalias)
                    *aliases = newalias;
            }
        } else if (strcmp(cmd, "options") == 0) {
            modname = strsep_skipspace(&ptr, "\t ");
            if (!modname || !ptr)
                grammar(cmd, filename, linenum);
            else {
                ptr += strspn(ptr, "\t ");
                *options = add_options(underscores(modname),
                                       ptr, *options);
            }
        } else if (strcmp(cmd, "install") == 0) {
            modname = strsep_skipspace(&ptr, "\t ");
            if (!modname || !ptr)
                grammar(cmd, filename, linenum);
            else if (!removing) {
                ptr += strspn(ptr, "\t ");
                *commands = add_command(underscores(modname),
                                        ptr, *commands);
            }
        } else if (strcmp(cmd, "blacklist") == 0) {
            modname = strsep_skipspace(&ptr, "\t ");
            if (!modname)
                grammar(cmd, filename, linenum);
            else if (!removing) {
                *blacklist = add_blacklist(underscores(modname),
                                           *blacklist);
            }
        } else if (strcmp(cmd, "remove") == 0) {
            modname = strsep_skipspace(&ptr, "\t ");
            if (!modname || !ptr)
                grammar(cmd, filename, linenum);
            else if (removing) {
                ptr += strspn(ptr, "\t ");
                *commands = add_command(underscores(modname),
                                        ptr, *commands);
            }
        } else
            grammar(cmd, filename, linenum);

        free(line);
    }
    fclose(cfile);
    return 1;
}

/* Simple format, ignore lines starting with #, one command per line.
   Returns true or false. */
static int read_config(const char *filename,
                       const char *name,
                       int removing,
                       struct module_options **options,
                       struct module_command **commands,
                       struct module_alias **aliases,
                       struct module_blacklist **blacklist)
{
    DIR *dir;
    int ret = 0;

    /* Reiser4 has file/directory duality: treat it as both. */
    dir = opendir(filename);
    if (dir) {
        struct dirent *i;
        while ((i = readdir(dir)) != NULL) {
            if (!streq(i->d_name,".") && !streq(i->d_name,"..")) {
                char sub[strlen(filename) + 1
                    + strlen(i->d_name) + 1];

                sprintf(sub, "%s/%s", filename, i->d_name);
                if (!read_config(sub, name,
                                 removing, options,
                                 commands, aliases, blacklist))
                    warn("Failed to open"
                         " config file %s: %s\n",
                         sub, strerror(errno));
            }
        }
        closedir(dir);
        ret = 1;
    }

    if (read_config_file(filename, name, removing,
                         options, commands, aliases, blacklist))
        ret = 1;

    return ret;
}

static void read_toplevel_config(const char *filename,
                                 const char *name,
                                 int removing,
                                 struct module_options **options,
                                 struct module_command **commands,
                                 struct module_alias **aliases,
                                 struct module_blacklist **blacklist)
{
    unsigned int i;

    if (filename) {
        if (!read_config(filename, name, removing,
                         options, commands, aliases, blacklist))
            fatal("Failed to open config file %s: %s\n",
                  filename, strerror(errno));
        return;
    }

    /* Try defaults. */
    for (i = 0; i < ARRAY_SIZE(default_configs); i++) {
        if (read_config(default_configs[i], name, removing,
                        options, commands, aliases, blacklist))
            return;
    }
}

static char *gather_options(char *argv[])
{
    char *optstring = NOFAIL(strdup(""));

    /* Rest is module options */
    while (*argv) {
        /* Quote value if it contains spaces. */
        unsigned int eq = strcspn(*argv, "=");

        if (strchr(*argv+eq, ' ') && !strchr(*argv, '"')) {
            char quoted[strlen(*argv) + 3];
            (*argv)[eq] = '\0';
            sprintf(quoted, "%s=\"%s\"", *argv, *argv+eq+1);
            optstring = append_option(optstring, quoted);
        } else
            optstring = append_option(optstring, *argv);
        argv++;
    }
    return optstring;
}

static void handle_module(const char *modname,
                          struct list_head *todo_list,
                          int remove,
                          char *options,
                          errfn_t error,
                          int verbose,
                          struct module_options *modoptions,
                          struct module_command *commands,
                          int ignore_commands,
                          const char *cmdline_opts)
{
    if (list_empty(todo_list)) {
        const char *command;

        /* The dependencies have to be real modules, but
           handle case where the first is completely bogus. */
        command = find_command(modname, commands);
        if (command && !ignore_commands) {
            do_command(modname, command, verbose, error,
                       remove ? "remove":"install", cmdline_opts);
            return;
        }

        error("Module %s not found.\n", modname);
        return;
    }

    if (remove)
        rmmod(todo_list, modname, error, verbose,
              commands, ignore_commands, 0, cmdline_opts);
    else
        insmod(todo_list, NOFAIL(strdup(options)), 
               error, verbose, modoptions,
               commands, ignore_commands, 
               cmdline_opts);
}

int main(int argc, char *argv[])
{
    struct utsname buf;
    int opt;
    int remove = 0;
    int verbose = 0;
    int all = 0;
    int ignore_commands = 0;
    int use_blacklist = 0;
    unsigned int i, num_modules;
    const char *config = NULL;
    char *dirname, *optstring;
    char *aliasfilename, *symfilename;
    errfn_t error = fatal;

    uname(&buf);
    /* TODO, add short option
       { "show-depends", 0, NULL, 'D' },
       */
    while ((opt = getopt(argc, argv, "vVC:qraib")) != -1){
        switch (opt) {
        case 'v':
            verbose = 1;
            break;
        case 'V':
            puts("klibc modprobe");
            exit(0);
        case 'C':
            config = optarg;
            break;
        case 'q':
            quiet = 1;
            break;
        case 'r':
            remove = 1;
            break;
        case 'a':
            all = 1;
            error = warn;
            break;
        case 'i':
            ignore_commands = 1;
            break;
        case 'b':
            use_blacklist = 1;
            break;
        default:
            print_usage(argv[0]);
        }
    }

    if (argc < optind + 1 && !remove)
        print_usage(argv[0]);

    dirname = NOFAIL(malloc(strlen(buf.release) + sizeof(MODULE_DIR) + 1));
    sprintf(dirname, "%s/%s", MODULE_DIR, buf.release);
    aliasfilename = NOFAIL(malloc(strlen(dirname)
                                  + sizeof("/modules.alias")));
    sprintf(aliasfilename, "%s/modules.alias", dirname);
    symfilename = NOFAIL(malloc(strlen(dirname)
                                + sizeof("/modules.symbols")));
    sprintf(symfilename, "%s/modules.symbols", dirname);

    if (remove || all) {
        num_modules = argc - optind;
        optstring = NOFAIL(strdup(""));
    } else {
        num_modules = 1;
        optstring = gather_options(argv+optind+1);
    }

    /* num_modules is always 1 except for -r or -a. */
    for (i = 0; i < num_modules; i++) {
        struct module_command *commands = NULL;
        struct module_options *modoptions = NULL;
        struct module_alias *aliases = NULL;
        struct module_blacklist *blacklist = NULL;
        LIST_HEAD(list);
        char *modulearg = argv[optind + i];

        /* Convert name we are looking for */
        underscores(modulearg);

        /* Returns the resolved alias, options */
        read_toplevel_config(config, modulearg,
                             remove, &modoptions, &commands, &aliases, &blacklist);

        /* No luck?  Try symbol names, if starts with symbol:. */
        if (!aliases
            && strncmp(modulearg, "symbol:", strlen("symbol:")) == 0)
            read_config(symfilename, modulearg,
                        remove, &modoptions, &commands,
                        &aliases, &blacklist);

        if (!aliases) {
            /* We only use canned aliases as last resort. */
            read_depends(dirname, modulearg, &list);

            if (list_empty(&list)
                && !find_command(modulearg, commands))
            {
                read_config(aliasfilename, modulearg,
                            remove, &modoptions, &commands,
                            &aliases, &blacklist);
                aliases = apply_blacklist(aliases, blacklist);
            }
        }

        if (aliases) {
            errfn_t err = error;

            /* More than one alias?  Don't bail out on failure. */
            if (aliases->next)
                err = warn;
            while (aliases) {
                /* Add the options for this alias. */
                char *opts = NOFAIL(strdup(optstring));
                opts = add_extra_options(modulearg,
                                         opts, modoptions);

                read_depends(dirname, aliases->module, &list);
                handle_module(aliases->module, &list, 
                              remove, opts, err,
                              verbose, modoptions,
                              commands, ignore_commands,
                              optstring);

                aliases = aliases->next;
                INIT_LIST_HEAD(&list);
            }
        } else {
            if (use_blacklist
                && find_blacklist(modulearg, blacklist))
                continue;

            handle_module(modulearg, &list, remove,
                          optstring, error, 
                          verbose, modoptions, commands,
                          ignore_commands, optstring);
        }
    }
    return 0;
}
