/* Copyright 1997 Free Software Foundation, Inc.
   Contributed by Marcin Dalecki <dalecki@sub994.sub.uni-goettingen.de>

   This file is part of the Linux modutils.

   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.  */

#ident "$Id: conf_file.c,v 1.2 1999/09/11 15:43:57 msw Exp $"

#include <stddef.h>
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <signal.h>
#include <limits.h>
#include <dirent.h>
#include <fnmatch.h>

#include "conf_file.h"
#include "util.h"
#include "misc.h"

/*
 * List of different paths for different sets
 * used like this: "/lib/module/SET/TYPE/bar.o
 */

struct mod_type
  {
    struct mod_type *next;
    char *type;
  };

static struct mod_set
  {
    struct mod_set *next;
    struct mod_type *types;
    char *set;
  }
 *mod_set = NULL;

char *default_types[] =
{
  "fs",
  "net",
  "scsi",
  "block",
  "cdrom",
  "ipv4",
  "ipv6",
  "sound",
  "fc4",
  "video",
  "misc",
  NULL				/* marks the end of the list! */
};

char *default_sets[] =
{
  "/lib/modules",
  "/lib/modules/default",
  NULL
};

/*
 * Operations concearned with the search paths.
 */

static void
add_set (char *set)
{
  struct mod_set *new_set;

  new_set = (struct mod_set *) xmalloc (sizeof (struct mod_set));
  new_set->next = mod_set;
  new_set->types = NULL;
  new_set->set = xstrdup (set);
  mod_set = new_set;
}

static void
add_type (struct mod_set *set, char *type)
{
  struct mod_type *new_type;

  new_type = (struct mod_type *) xmalloc (sizeof (struct mod_type));
  new_type->next = set->types;
  new_type->type = xstrdup (type);
  set->types = new_type;
}

static void
add_option (char *module, char *args, int may_unload)
{
  struct mod_option *new_opt;
  new_opt = (struct mod_option *) xmalloc (sizeof (struct mod_option));
  new_opt->next = mod_options;
  new_opt->module = xstrdup (module);
  new_opt->may_unload = may_unload;
  mod_options = new_opt;

  if (args)
      new_opt->args = xstrdup (args);
  else
      new_opt->args = NULL;
}

static struct action
{
  struct action *next;
  enum command when;
  char *module;
  char *cmd;
}
 *mod_action = NULL;

char *depfile = NULL;
char *insmod_opts = NULL;
struct mod_option *mod_options = NULL;

static struct mod_alias
  {
    struct mod_alias *next;
    char *name;
    char *alias;
  }
alias_list[] =
{
#include "alias.h"
  {
    NULL, NULL, NULL
  }
};

static struct mod_alias *mod_alias = NULL;

static void
add_alias (char *module, char *alias)
{
  struct mod_alias *tmp;

  tmp = (struct mod_alias *) xmalloc (sizeof (struct mod_alias));
  tmp->next = mod_alias;
  tmp->name = xstrdup (module);
  tmp->alias = xstrdup (alias);
  mod_alias = tmp;
}

static void
add_action (enum command when, char *module, char *action)
{
  struct action *tmp;

  tmp = (struct action *) xmalloc (sizeof (struct action));
  tmp->next = mod_action;
  tmp->when = when;
  tmp->module = xstrdup (module);
  tmp->cmd = xstrdup (action);
  mod_action = tmp;
}

static void
release_all_sets (void)
{
  struct mod_set *set;
  struct mod_set *tmp;

  set = mod_set;
  while (set)
    {
      struct mod_type *type;
      struct mod_type *ttmp;
      tmp = set;
      free (set->set);

      type = set->types;
      while (type)
	{
	  ttmp = type;
	  free (type->type);
	  type = type->next;
	  free (ttmp);
	}
      set = set->next;
      free (tmp);
    }
  mod_set = NULL;
}

/*
 * Read the modules configuration file.
 *
 * Error messages are generated.
 */
static int
error_missing_argument (int line)
{
  lprintf ("conf:%d: missing argument\n", line);
  return 1;
}

static int
expect_action (int line, int assign,
	       enum command com,
	       char *cp, char **tmp)
{
  if (assign)
    {
      lprintf ("conf:%d: %s used in assignment\n", line, *tmp);
      return 1;
    }
  if (!cp)
    return error_missing_argument (line);
  *tmp = cp + 1;
  cp = strchr (*tmp, ' ');
  if (!cp)
    return error_missing_argument (line);
  *cp = '\0';

  add_action (com, *tmp, cp + 1);

  return 0;
}

int
read_config_file (char *kernel_ver)
{
  struct utsname uts_info;
  int drop_default_paths = 1;
  char *depfile_tmp;
  char **list;
  struct mod_set *set;
  char *def_set;
  char *config;
  char *tmp = NULL;
  char *end;
  int line = 0;			/* current line of the configuration file */
  char linebuf [8192];

  /*
   * Initialize the list of predefined aliases.
   */
  mod_alias = alias_list;
  while (mod_alias[1].name && mod_alias[1].alias)
    {
      mod_alias->next = mod_alias + 1;
      ++mod_alias;
    }
  mod_alias = alias_list;

  /*
   * Create the full name of the output file.
   */
  if (kernel_ver)
    {
      depfile_tmp = (char *) xmalloc (strlen ("/lib/modules//modules.dep") +
				      strlen (kernel_ver) + 1);
      strcpy (depfile_tmp, "/lib/modules/");
      strcat (depfile_tmp, kernel_ver);
      strcat (depfile_tmp, "/modules.dep");
    }
  else
    depfile_tmp = xstrdup ("");

  depfile = depfile_tmp;

  /*
   * Create the primary path for modules:
   */

  list = default_sets;
  while (*list)
    {
      add_set (*list);
      ++list;
    }

  if (kernel_ver)
    tmp = kernel_ver;
  else
    {
      uname (&uts_info);
      tmp = uts_info.release;
    }
  def_set = (char *) xmalloc (strlen ("/lib/modules/") + strlen (tmp) + 1);
  strcpy (def_set, "/lib/modules/");
  strcat (def_set, tmp);
  add_set (def_set);

  free (def_set);

  /*
   * Add the default types to the just created global list.
   */
  for (set = mod_set; set; set = set->next)
    {
      char **type;

      for (type = default_types; *type; ++type)
	add_type (set, *type);
    }

  /*
   * Now read the configuration file.
   */

  config = NULL;
  if (!access ("/etc/modules.conf", R_OK))
    config = read_and_preprocess_file ("/etc/modules.conf");
  if (!config)
    config = read_and_preprocess_file ("/etc/conf.modules");

  /*
   * Parse the preprocessed file.
   */
  for (tmp = config; (end = get_concat_line (tmp, &line)); tmp = end)
    {
      char *cp;
      int assign = 0;

      if (!*tmp)		/* ignore blank lines */
	continue;

      tmp = resolve_string (tmp, linebuf, sizeof (linebuf));

      cp = strpbrk (tmp, " =");
      if (cp)
	{
	  if (*cp == '=')
	    assign = 1;
	  else
	    {

	      if (cp[1] == '=')
		{
		  assign = 1;
		  *cp = '\0';
		  ++cp;
		}
	      else
		assign = 0;
	    }
	  *cp = '\0';
	}

      /*
       * Now interpret the configuration lines
       */
      if (!strcmp (tmp, "keep"))
	{
	  if (cp)
	    {
	      lprintf ("conf:%d: \"keep\" used with argument\n", line);
	      return 1;
	    }
	  drop_default_paths = 0;
	}
      else if (!strcmp (tmp, "insmod_opt"))
	{
	  if (!assign)
	    {
	      lprintf ("conf:%d: \"insmod_opt\" not within assignment\n", line);
	      return 1;
	    }
	  if (!cp)
	    return error_missing_argument (line);
	  tmp = cp + 1;
	  insmod_opts = xstrdup (tmp);
	}
      else if (!strcmp (tmp, "depfile"))
	{
	  if (!assign)
	    {
	      lprintf ("conf:%d: depfile not within assignment\n", line);
	      return 1;
	    }
	  if (!cp)
	    return error_missing_argument (line);
	  tmp = cp + 1;
	  free (depfile_tmp);
	  depfile_tmp = xstrdup (tmp);
	}
      else if (!strcmp (tmp, "options"))
	{
	  if (assign)
	    {
	      lprintf ("conf:%d: options used in assignment\n", line);
	      return 1;
	    }
	  if (!cp)
	    return error_missing_argument (line);
	  tmp = cp + 1;
	  cp = strchr (tmp, ' ');
	  if (!cp)
	    {
	      lprintf ("conf:%d: missing module argument\n", line);
	      return 1;
	    }
	  *cp = '\0';

	  if (!strcmp(tmp, "-k")) 
	    {
	      tmp = cp + 1;
	      cp = strchr (tmp, ' ');
	
	      if (cp)
		{
		  *cp = '\0';
		  add_option (tmp, cp + 1, 0);
		}
	      else
		{
		  add_option (tmp, NULL, 0);
		}
	    }
	  else
	    {
	      add_option (tmp, cp + 1, 1);
	    }
	}
      else if (!strcmp (tmp, "alias"))
	{
	  if (assign)
	    {
	      lprintf ("conf:%d: alias used in assignment\n", line);
	      return 1;
	    }
	  if (!cp)
	    return error_missing_argument (line);
	  tmp = cp + 1;
	  cp = strchr (tmp, ' ');
	  if (!cp)
	    return error_missing_argument (line);
	  *cp = '\0';

	  if (strchr (cp, ' '))
	    {
	      lprintf ("conf:%d: ambigious alias requested\n", line);
	      return 1;
	    }
	  add_alias (tmp, cp + 1);
	}
      else if (!strcmp (tmp, "pre-install"))
	{
	  if (expect_action (line, assign, PRE_INSTALL, cp, &tmp))
	    return 1;
	}
      else if (!strcmp (tmp, "install"))
	{
	  if (expect_action (line, assign, INSTALL, cp, &tmp))
	    return 1;
	}
      else if (!strcmp (tmp, "post-install"))
	{
	  if (expect_action (line, assign, POST_INSTALL, cp, &tmp))
	    return 1;
	}
      else if (!strcmp (tmp, "pre-remove"))
	{
	  if (expect_action (line, assign, PRE_REMOVE, cp, &tmp))
	    return 1;
	}
      else if (!strcmp (tmp, "remove"))
	{
	  if (expect_action (line, assign, REMOVE, cp, &tmp))
	    return 1;
	}
      else if (!strcmp (tmp, "post-remove"))
	{
	  if (expect_action (line, assign, POST_REMOVE, cp, &tmp))
	    return 1;
	}
      else if (strstr (tmp, "path") == tmp)
	{
	  if (!assign)
	    {
	      lprintf ("conf:%d: path not within assignment\n", line);
	      return 1;
	    }
	  if (!cp)
	    return error_missing_argument (line);
	  if (strchr (tmp, '['))
	    {
	      char *type;

	      if (tmp != strstr (tmp, "path["))
		{
		  lprintf ("conf:%d: syntax error \"%s\"\n", line, tmp);
		  return 1;
		}
	      type = strchr (tmp, '[') + 1;
	      if (!(tmp = strchr (tmp, ']')))
		{
		  lprintf ("conf:%d: missing \"]\"\n", line);
		  return 1;
		}
	      *tmp = '\0';
	      if (tmp == type)
		{
		  lprintf ("conf:%d: empty cathegory in path[...]\n", line);
		  return 1;
		}
	      if (drop_default_paths)
	      {
		/* We only do it once. */
		drop_default_paths = 0;
		release_all_sets ();
	      }
	      add_set (cp + 1);
	      add_type (mod_set, type);
	    }
	  else
	    {
	      char **type;

	      if (strcmp (tmp, "path"))
		{
		  lprintf ("conf:%d: unknown keyword \"%s\"\n", line, tmp);
		  return 1;
		}
	      if (drop_default_paths)
	      {
		/* We only do it once. */
		drop_default_paths = 0;
		release_all_sets ();
	      }
	      add_set (cp + 1);
	      /*
	       * And now add all default types to this set.
	       */
	      for (type = default_types; *type; ++type)
		add_type (mod_set, *type);
	    }
	}
      else
	{
	  lprintf ("conf:%d: unknown keyword \"%s\"\n", line, tmp);
	  return 1;
	}
    }
  free (config);

  /*
   * If the dependancy file wasn't specified by the configuration file and
   * we where not forced to use one special, then give a default:
   */
  if (!*depfile_tmp)
    {
      if (kernel_ver)
	tmp = kernel_ver;
      else
	{
	  uname (&uts_info);
	  tmp = uts_info.release;
	}
      free (depfile_tmp);
      depfile_tmp = (char *) xmalloc (strlen ("/lib/modules//modules.dep") +
				      strlen (tmp) + 1);
      strcpy (depfile_tmp, "/lib/modules/");
      strcat (depfile_tmp, tmp);
      strcat (depfile_tmp, "/modules.dep");
    }
  depfile = depfile_tmp;

  return 0;
}

/*
 * Find all modules matching the globbing pattern "match" in directoryies of
 * type "type". "many" specifies if we are looking for all possible matches
 * or only the first one.
 */
struct mod_path *
find_matching_mods (const char *match, const char *type, int many)
{
  struct mod_set *set;
  struct mod_path *it = NULL;

  /*
   * Go through all classes looking for the type.
   */
  for (set = mod_set; set; set = set->next)
    {
      struct mod_type *t;
      for (t = set->types; t; t = t->next)
	if (!type || !strcmp (type, t->type))
	  {
	    struct stat statb;
	    DIR *dp;
	    struct dirent *ep;
	    char *dir = (char *) xmalloc (strlen (set->set)
					  + strlen (t->type) + 2);
	    strcpy (dir, set->set);
	    strcat (dir, "/");
	    strcat (dir, t->type);

	    /*
	     * OK. Now we know where to search. Go through the contents of this
	     * directory and search for matches.
	     */

	    dp = opendir (dir);
	    if (dp)
	      while ((ep = readdir (dp)))
		{
		  /*
		   * Look if it's a regular file!
		   */
		  char *file = (char *) xmalloc (strlen (dir)
						 + strlen (ep->d_name) + 2);
		  strcpy (file, dir);
		  strcat (file, "/");
		  strcat (file, ep->d_name);

		  /*
		   * Check if it's a regular file and we have the permissions
		   * to access it.
		   */
		  if (stat (file, &statb) == -1
		      || (statb.st_mode & S_IFMT) != S_IFREG
		      || access (file, R_OK))
		    {
		      free (file);
		      continue;
		    }

		  /*
		   * And now check if it is matching our search pattern
		   */

		  if (!fnmatch (match, ep->d_name, FNM_PERIOD))
		    {
		      struct mod_path *tmp =
		      (struct mod_path *) xmalloc (sizeof (struct mod_path));
		      tmp->next = it;
		      tmp->path = file;
		      it = tmp;

		      if (!many)	/* search only for the first match */
			{
			  free (dir);
			  return it;
			}
		    }
		  else
		    free (file);
		}
	    free (dir);
	  }
    }

  return it;
}

/*
 * Print out the currently active configuration.
 */
void
print_active_config (void)
{
  struct mod_set *set;
  struct mod_option *opt;
  struct mod_alias *alias;
  struct action *action;

  printf ("# This file was generated by: modprobe -c (" MODUTILS_VERSION ")\n");

  /*
   * Go through all classes looking for the type.
   */

  for (set = mod_set; set; set = set->next)
    {
      struct mod_type *t;
      if ((t = set->types))
	while (t)
	  {
	    printf ("path[%s]=%s\n", t->type, set->set);
	    t = t->next;
	  }
      else
	printf ("path=%s\n", set->set);
    }
  puts ("# Aliases");
  for (alias = mod_alias; alias; alias = alias->next)
    printf ("alias %s %s\n", alias->name, alias->alias);
  puts ("# Options");
  for (opt = mod_options; opt; opt = opt->next)
    printf ("options %s %s\n", opt->module, opt->args);

  if (mod_action)
    {
      puts ("# Commands");
      for (action = mod_action; action; action = action->next)
	{
	  switch (action->when)
	    {
	    case PRE_INSTALL:
	      printf ("pre-install ");
	      break;
	    case INSTALL:
	      printf ("install ");
	      break;
	    case POST_INSTALL:
	      printf ("post-install ");
	      break;
	    case PRE_REMOVE:
	      printf ("pre-remove ");
	      break;
	    case REMOVE:
	      printf ("remove ");
	      break;
	    case POST_REMOVE:
	      printf ("post-remove ");
	      break;
	    }
	  printf ("%s %s\n", action->module, action->cmd);
	}
    }
}

/*
 * Look for a command associated with this action.
 */
char *
find_assoc_cmd (enum command when, char *mod)
{
  char *modname = strip_o (mod);
  struct action *action;

  for (action = mod_action; action; action = action->next)
    if ((action->when == when) && !strcmp (action->module, modname))
      {
	free (modname);
	return action->cmd;
      }

  free (modname);
  return NULL;
}

/*
 * Check if a module name is indeed an alias for another module.
 * Return the real module or this one (mod) if it is not an alias
 */
static char *
translate_alias (char *orig_mod)
{
  struct mod_alias *alias;
  char *mod = orig_mod;
  char *modname = strip_o (mod);
  int count = 0;

restart:
  for (alias = mod_alias; alias; alias = alias->next)
    if (!fnmatch (alias->name, mod, 0)
	|| (modname && !fnmatch (alias->name, modname, 0)))
      {
	if (modname)
	  {
	    free (modname);
	    modname = NULL;
	  }
	if (++count > 1000)
	  {
	    lprintf ("warning: alias loop resolving \"%s\"\n", orig_mod);
	    return orig_mod;
	  }
	mod = alias->alias;
	goto restart;
      }

  if (modname)
    free (modname);
  return mod;
}


/*
 *  Locate all modules matching the "match".
 */
struct mod_path *
locate_mod_obj (char *match, char *type)
{
  struct mod_path *it = NULL;
  char *match_o;

  match = translate_alias (match);

  if (strchr (match, '/') != NULL)
    {
      /*
       * Absolute path. Don't search any further!
       */

      it = (struct mod_path *) xmalloc (sizeof (struct mod_path));
      it->next = NULL;
      it->path = xstrdup (match);
      return it;
    }

  if (!match || !(*match))
    return NULL;
  if (!strcmp (match, "off"))
    return (void *) (-1);

  match_o = (char *) xmalloc (strlen (match) + 3);
  strcpy (match_o, match);
  strcat (match_o, ".o");

  it = find_matching_mods (match_o, type, 0);
  free (match_o);

  if (!it)
    if (!(it = find_matching_mods (match, type, 0)))
      return NULL;

  return it;
}
