/* Display information gleaned from a module's .modinfo section.
   Copyright 1996, 1997 Linux International.

   Contributed by Tom Dyas <tdyas@eden.rutgers.edu>

   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: modinfo.c,v 1.8 1998/04/25 21:32:28 rth Exp $"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <getopt.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/utsname.h>

#include "module.h"
#include "obj.h"
#include "util.h"
#include "version.h"
#include "logger.h"


/* Given a bare module name, poke through the module path to find the file. */
static char *
search_module_path(char *base)
{
  static const char default_path[] =
    ".:"
    "/linux/modules:"
    "/lib/modules/%s/fs:"
    "/lib/modules/%s/net:"
    "/lib/modules/%s/scsi:"
    "/lib/modules/%s/block:"
    "/lib/modules/%s/cdrom:"
    "/lib/modules/%s/ipv4:"
    "/lib/modules/%s/ipv6:"
    "/lib/modules/%s/sound:"
    "/lib/modules/%s/fc4:"
    "/lib/modules/%s/misc:"
    "/lib/modules/default/fs:"
    "/lib/modules/default/net:"
    "/lib/modules/default/scsi:"
    "/lib/modules/default/block:"
    "/lib/modules/default/cdrom:"
    "/lib/modules/default/ipv4:"
    "/lib/modules/default/ipv6:"
    "/lib/modules/default/sound:"
    "/lib/modules/default/fc4:"
    "/lib/modules/default/misc:"
    "/lib/modules/fs:"
    "/lib/modules/net:"
    "/lib/modules/scsi:"
    "/lib/modules/block:"
    "/lib/modules/cdrom:"
    "/lib/modules/ipv4:"
    "/lib/modules/ipv6:"
    "/lib/modules/sound:"
    "/lib/modules/fc4:"
    "/lib/modules/misc";

  char *path, *p, *filename;
  struct utsname uts_info;
  size_t len;

  if ((path = getenv("MODPATH")) == NULL)
    path = (char *)default_path;

  /* Make a copy so's we can mung it with strtok.  */
  len = strlen(path)+1;
  p = alloca(len);
  path = memcpy(p, path, len);

  uname(&uts_info);
  filename = xmalloc(PATH_MAX);

  for (p = strtok(path, ":"); p != NULL ; p = strtok(NULL, ":"))
    {
      struct stat sb;

      len = snprintf(filename, PATH_MAX, p, uts_info.release);
      len += snprintf(filename+len, PATH_MAX-len, "/%s", base);

      if (stat(filename, &sb) == 0 && S_ISREG(sb.st_mode))
	return filename;

      snprintf(filename+len, PATH_MAX-len, ".o");

      if (stat(filename, &sb) == 0 && S_ISREG(sb.st_mode))
	return filename;
    }

  free(filename);
  return NULL;
}

static char *
get_modinfo_value(struct obj_file *f, const char *key)
{
  struct obj_section *sec;
  char *p, *v, *n, *ep;
  size_t klen = strlen(key);

  sec = obj_find_section(f, ".modinfo");
  if (sec == NULL)
    return NULL;

  p = sec->contents;
  ep = p + sec->header.sh_size;
  while (p < ep)
    {
      v = strchr(p, '=');
      n = strchr(p, '\0');
      if (v)
        {
          if (v-p == klen && strncmp(p, key, klen) == 0)
            return v+1;
        }
      else
        {
          if (n-p == klen && strcmp(p, key) == 0)
            return n;
        }
      p = n+1;
    }

  return NULL;
}

static int
append_modinfo_tag(struct obj_file *f, char *tag, char *def,
		   char **out_str, int n)
{
  char *p;

  p = get_modinfo_value(f, tag);
  if (!p && !def)
    return -1;

  if (!p)
    p = def;

  if (strlen(p) < n)
    n = strlen(p);

  strncpy(*out_str, p, n);
  *out_str += n;

  return 0;
}

/* Format the "format" string based on the module contents. */
static char *
format_query_string(struct obj_file *f, char *format)
{
  static char buffer[1024];
  char *in_str = format, *out_str = &buffer[0];
  char *last_char = buffer + sizeof(buffer) - 1;

  while (*in_str && out_str < last_char) {
    /* Just copy normal characters into the output. */
    if (*in_str != '%') {
      *out_str++ = *in_str++;
      continue;
    }

    /* Ensure that the replacement is there. */
    if (*++in_str == '\0') {
      error("missing replacement");
      return NULL;
    }
    
    /* Process the replacement as required. */
    switch (*in_str++) {
    case '%':
      *out_str++ = '%';
      break;

    case 'a':
      (void) append_modinfo_tag(f,"author","<none>",&out_str,last_char-out_str);
      break;

    case 'd':
      (void) append_modinfo_tag(f,"description","<none>",&out_str,last_char-out_str);
      break;

    case '{': {
      char tag[128], *end = strchr(in_str, '}');

      /* Make sure the %{...} is formatted correctly. */
      if (!end) {
	error("unterminated %%{...} construct");
	return NULL;
      }
      if (end - in_str > sizeof(tag) - 1) {
	error("%%{...} construct is too large");
	return NULL;
      }

      /* Copy out the tag name. */
      memset(tag, 0, sizeof(tag));
      strncpy(tag, in_str, end - in_str);
      tag[end-in_str] = '\0';

      /* Append the tag's value if it exists. */
      if (append_modinfo_tag(f,tag,NULL,&out_str,last_char-out_str) < 0) {
	error("%s: nonexistent tag", tag);
	return NULL;
      }

      /* Advance past the end of the replacement. */
      in_str = end + 1;
      break;
    }

    default:
      error("%c: unknown replacement", in_str[-1]);
      return NULL;
    } /* switch */
  } /* while */

  *out_str = '\0';
  return &buffer[0];
}

static void
show_parameter(struct obj_file *f, char *key, char *value, char *desc)
{
  struct obj_symbol *sym;
  int min, max;
  char *p = value;
  
  sym = obj_find_symbol(f, key);
  if (sym == NULL)
    printf("warning: symbol for parameter %s not found\n", key);

  if (isdigit(*p))
    {
      min = strtoul(p, &p, 10);
      if (*p == '-')
	max = strtoul(p+1, &p, 10);
      else
	max = min;
    }
  else
    min = max = 1;

  if (max < min)
    printf("warning: parameter %s has max < min!\n", key);

  printf("%s ", key);
  
  switch (*p) {
  case 'c':
    printf("char");
    break;
  case 'b':
    printf("byte");
    break;
  case 'h':
    printf("short");
    break;
  case 'i':
    printf("int");
    break;
  case 'l':
    printf("long");
    break;
  case 's':
    printf("string");
    break;
  case '\0':
    printf("no format character!\n");
    return;
  default:
    printf("unknown format character %c", *p);
    return;
  }

  if (min > 1 || max > 1)
    printf(" array (min = %d, max = %d)", min, max);

  if (desc)
    printf(", description \"%s\"", desc);

  printf("\n");
}

static void
show_module_parameters(struct obj_file *f)
{
  struct obj_section *sec;
  char *ptr, *value, *n, *endptr;
  int namelen;

  sec = obj_find_section(f, ".modinfo");
  if (sec == NULL) {
    error("module does not support typed parameters");
    return;
  }

  ptr = sec->contents;
  endptr = ptr + sec->header.sh_size;
  while (ptr < endptr)
    {
      value = strchr(ptr, '=');
      n = strchr(ptr, '\0');
      if (value)
        {
	  namelen = value - ptr;
          if (namelen >= 5  && strncmp(ptr, "parm_", 5) == 0
	      && !(namelen > 10 && strncmp(ptr, "parm_desc_", 10) == 0))
	    {
	      char *pname = xmalloc(namelen+1);
	      char *desckey = xmalloc(namelen + 5 + 1);

	      strncpy(pname, ptr + 5, namelen - 5);
	      pname[namelen - 5] = '\0';

	      strcpy(desckey, "parm_desc_");
	      strncat(desckey, ptr + 5, namelen - 5);
	      desckey[namelen + 5] = '\0';

	      show_parameter(f, pname, value+1, get_modinfo_value(f,desckey));

	      free(pname);
	      free(desckey);
	    }
	}
      else
	{
	  if (n-ptr >= 5 && strncmp(ptr, "parm_", 5) == 0) {
	    error("parameter %s found with no value", ptr);
	  }
	}
      ptr = n + 1;
    }
}

static int
show_module_info(char * filename, char *fmtstr, int do_parameters)
{
  FILE *fp;
  struct obj_file *f;
  char *p;

  /* Locate the file to be loaded.  */
  if (!strchr(filename, '/') && !strchr(filename, '.'))
    {
      char *tmp = search_module_path(filename);
      if (tmp == NULL)
        {
          error("%s: no module by that name found", filename);
          return 1;
        }
      filename = tmp;
    }

  error_file = filename;

  /* Attempt to open and parse the module file. */
  if ((fp = fopen(filename, "r")) == NULL)
    {
      error("%s: %m", filename);
      return -1;
    }
  else if ((f = obj_load(fp)) == NULL)
    return -1;
  fclose(fp);

  if (do_parameters) {
    show_module_parameters(f);
  } else if (fmtstr) {
    p = format_query_string(f, fmtstr);
    if (p)
      fputs(p, stdout);
  }
  return 0;
}

static void
usage(void)
{
  printf("usage: modinfo <parameters> <module>\n");
  printf("  -a, --author            display module author\n");
  printf("  -d, --description       display module description\n");
  printf("  -f <str>\n");
  printf("      --format <str>      print the <query-string>\n");
  printf("  -p, --parameters        display module parameters\n");
  printf("  -V, --version           show version\n");
  printf("  -h, --help              this usage screen\n");
}

int main(int argc, char *argv[])
{
  static struct option long_opts[] = {
    { "author", 0, 0, 'a' },
    { "description", 0, 0, 'd' },
    { "format", 0, 0, 'f' },
    { "parameters", 0, 0, 'p' },
    { "version", 0, 0, 'V' },
    { "help", 0, 0, 'h' },
    { 0, 0, 0, 0}
  };
  int do_parameters = 0, opt;
  char *fmtstr = NULL;

  while ((opt = getopt_long(argc, argv, "adq:pVh",
			    &long_opts[0], NULL)) != EOF)
    switch (opt) {
    case 'a':
      fmtstr = "%a\n";
      break;
    case 'd':
      fmtstr = "%d\n";
      break;
    case 'f':
      fmtstr = xstrdup(optarg);
      break;
    case 'p':
      do_parameters = 1;
      break;
    case 'V':
      fputs("modinfo (Linux modutils) " MODUTILS_VERSION "\n", stdout);
      exit(0);
    case 'h':
    default:
      usage();
      exit(opt == 'h' ? 0 : 1);
    }

  if (optind >= argc) {
    usage();
    exit(1);
  }

  error_file = "modinfo";

  while (optind < argc)
    show_module_info(argv[optind], fmtstr, do_parameters);

  return 0;
}
