/* 


   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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>

#include "list.h"

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

#define MODULE_DIR "/lib/modules"

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

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

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;
}

#define NOFAIL(ptr)	do_nofail((ptr), __FILE__, __LINE__, #ptr)
static void *do_nofail(void *ptr, const char *file, int line, const char *expr)
{
    if (!ptr)
    {
        fprintf(stderr,"Memory allocation failure %s line %d: %s.\n",
              file, line, expr);
        exit(1);
    }
    return ptr;
}

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

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

    list_for_each_entry(i, list, list)
    {
        if (strcmp(i->filename, filename) == 0)
            mod = i;
    }
    if (mod)
        list_del(&mod->list);
    else
    {
        const char *afterslash;
        unsigned int i;

        /* 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));

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

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

    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)
    {
        fprintf(stderr,"Could not load %s: %s\n",
              modules_dep_name, strerror(errno));
        exit(1);
    }

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


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,
                       struct module_alias **alias);

/* FIXME: Maybe should be extended to "alias a b [and|or c]...". --RR */
static int read_config_file(const char *filename,
                            const char *name,
                            struct module_alias **aliases)
{
    char *line;
    FILE *cfile;

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

    while ((line = getline(cfile)) != NULL) {
        char *ptr = line, *cmd;

        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 && fnmatch(wildcard,name) == 0)
            {
                struct module_alias *new = NOFAIL(malloc(sizeof(*new)));
                new->module = NOFAIL(strdup(realname));
                new->next = *aliases;
                *aliases = new;
            }
        } else if (strcmp(cmd, "include") == 0) {
            struct module_alias *newalias = NULL;
            char *newfilename;

            newfilename = strsep_skipspace(&ptr, "\t ");
            if (newfilename)
            {
                if (!read_config(newfilename, name, &newalias))
                    fprintf(stderr,"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;
            }
        }

        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,
                       struct module_alias **aliases)
{
    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 (!(strcmp(i->d_name,".") == 0) && !(strcmp(i->d_name,"..") == 0)) {
                char sub[strlen(filename) + 1
                    + strlen(i->d_name) + 1];

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

    if (read_config_file(filename, name, aliases))
        ret = 1;

    return ret;
}

static void read_toplevel_config(const char *filename,
                                 const char *name,
                                 struct module_alias **aliases)
{
    unsigned int i;

    if (filename) {
        if (!read_config(filename, name, aliases))
        {
            fprintf(stderr,"Failed to open config file %s: %s\n",
                  filename, strerror(errno));
            exit(1);
        }
        return;
    }

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

int main(int argc, char *argv[])
{
    struct utsname buf;
    const char *config = NULL;
    char *dirname, *aliasfilename;
    struct module_alias *aliases = NULL;
    LIST_HEAD(dependlist);

    uname(&buf);

    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") + 1));
    sprintf(aliasfilename, "%s/modules.alias", dirname);


    char *modulearg = argv[1];
    underscores(modulearg);

    read_toplevel_config(config, modulearg, &aliases);

    if (!aliases)
    {
        read_depends(dirname, modulearg, &dependlist);

        if (list_empty(&dependlist))
            read_config(aliasfilename, modulearg, &aliases);
    }

    if (aliases)
    {
        while (aliases)
        {
            read_depends(dirname, aliases->module, &dependlist);

            struct module* m = NULL;
            list_for_each_entry(m, &dependlist, list)
                printf("%s\n",m->modname);

            aliases = aliases->next;
            INIT_LIST_HEAD(&dependlist);
        }
    }

    if (!list_empty(&dependlist))
    {
        struct module* m = NULL;
        list_for_each_entry(m, &dependlist, list)
            printf("%s\n",m->modname);
    }
    return 0;
}
