/*
 * ksym.c - kernel symbol handling
 * Copyright 1998-2003 by Albert Cahalan
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdarg.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/utsname.h>
#include "procps.h"
#include "alloc.h"
#include "version.h"
#include "sysinfo.h" /* smp_num_cpus */
#include "wchan.h"  // to verify prototypes

#define KSYMS_FILENAME "/proc/ksyms"

#if 0
#undef KSYMS_FILENAME
#define KSYMS_FILENAME  "/would/be/nice/to/have/this/file"
#define SYSMAP_FILENAME "/home/albert/ps/45621/System.map-hacked"
#define linux_version_code 131598 /* ? */
#define smp_num_cpus 2
#endif

#if 0
#undef KSYMS_FILENAME
#define KSYMS_FILENAME  "/home/albert/ps/45621/ksyms-2.3.12"
#define SYSMAP_FILENAME "/home/albert/ps/45621/System.map-2.3.12"
#define linux_version_code 131852 /* 2.3.12 */
#define smp_num_cpus 2
#endif

#if 0
#undef KSYMS_FILENAME
#define KSYMS_FILENAME  "/home/albert/ps/45621/ksyms-2.3.18ac8-MODVERS"
#define SYSMAP_FILENAME "/home/albert/ps/45621/System.map-2.3.18ac8-MODVERS"
#define linux_version_code 131858 /* 2.3.18ac8 */
#define smp_num_cpus 2
#endif

#if 0
#undef KSYMS_FILENAME
#define KSYMS_FILENAME  "/home/albert/ps/45621/ksyms-2.3.18ac8-NOMODVERS"
#define SYSMAP_FILENAME "/home/albert/ps/45621/System.map-2.3.18ac8-NOMODVERS"
#define linux_version_code 131858 /* 2.3.18ac8 */
#define smp_num_cpus 2
#endif

/* These are the symbol types, with relative popularity:
 *     ? w  machine type junk for Alpha -- odd syntax
 *     ? S  not for i386
 *     4 W  not for i386
 *    60 R
 *   100 A
 *   125 r
 *   363 s  not for i386
 *   858 B
 *   905 g  generated by modutils?
 *   929 G  generated by modutils?
 *  1301 b
 *  2750 D
 *  4481 d
 * 11417 ?
 * 13666 t
 * 15442 T
 *
 * For i386, that is: "RArBbDd?tT"
 */

#define SYMBOL_TYPE_CHARS "Tt?dDbBrARGgsWS"

/*
 * '?' is a symbol type
 * '.' is part of a name (versioning?)
 * "\t[]" are for the module name in /proc/ksyms
 */
#define LEGAL_SYSMAP_CHARS "0123456789_ ?.\n\t[]" \
                     "abcdefghijklmnopqrstuvwxyz" \
                     "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

/* System.map lines look like:
 * hex num, space, one of SYMBOL_TYPE_CHARS, space, LEGAL_SYSMAP_CHARS, \n
 *
 * Alpha systems can start with a few lines that have the address replaced
 * by space padding and a 'w' for the type. For those lines, the last space
 * is followed by something like: mikasa_primo_mv p2k_mv sable_gamma_mv
 * (just one of those, always with a "_mv", then the newline)
 *
 * The /proc/ksyms lines are like System.map lines w/o the symbol type char.
 * When odd features are used, the name part contains:
 * "(.*)_R(smp_|smp2gig_|2gig_)?[0-9a-fA-F]{8,}"
 * It is likely that more crap will be added...
 */

typedef struct symb {
  unsigned KLONG addr;
  const char *name;
} symb;

/* These mostly rely on POSIX to make them zero. */

static symb hashtable[256];

static char       *sysmap_data;
static unsigned    sysmap_room;
static symb       *sysmap_index;
static unsigned    sysmap_count;

static char       *ksyms_data;
static unsigned    ksyms_room     = 4096;
static symb       *ksyms_index;
static unsigned    ksyms_count;
static unsigned    idx_room;

/*********************************/

/* Kill this:  _R(smp_?|smp2gig_?|2gig_?)?[0-9a-f]{8,}$
 * We kill:    (_R[^A-Z]*[0-9a-f]{8,})+$
 *
 * The loop should almost never be taken, but it has to be there.
 * It gets rid of anything that _looks_ like a version code, even
 * if a real version code has already been found. This is because
 * the inability to perfectly recognize a version code may lead to
 * symbol mangling, which in turn leads to mismatches between the
 * /proc/ksyms and System.map data files.
 */
#if 0
static char *chop_version(char *arg){
  char *cp;
  cp = strchr(arg,'\t');
  if(cp) *cp = '\0';  /* kill trailing module name first */
  for(;;){
    char *p;
    int len = 0;
    cp = strrchr(arg, 'R');
    if(!cp || cp<=arg+1 || cp[-1]!='_') break;
    for(p=cp; *++p; ){
      switch(*p){
      default:
        goto out;
      case '0' ... '9':
      case 'a' ... 'f':
        len++;
        continue;
      case 'g' ... 'z':
      case '_':
        len=0;
        continue;
      }
    }
    if(len<8) break;
    cp[-1] = '\0';
  }
out:
  if(*arg=='G'){
    int len = strlen(arg);
    while( len>8 && !memcmp(arg,"GPLONLY_",8) ){
      arg += 8;
      len -= 8;
    }
  }
  return arg;
}
#endif
static char *chop_version(char *arg){
  char *cp;
  cp = strchr(arg,'\t');
  if(cp) *cp = '\0';  /* kill trailing module name first */
  for(;;){
    int len;
    cp = strrchr(arg, 'R');
    if(!cp || cp<=arg+1 || cp[-1]!='_') break;
    len=strlen(cp);
    if(len<9) break;
    if(strpbrk(cp+1,"ABCDEFGHIJKLMNOPQRSTUVWXYZ")) break;
    if(strspn(cp+len-8,"0123456789abcdef")!=8) break;
    cp[-1] = '\0';
  }
  if(*arg=='G'){
    int len = strlen(arg);
    while( len>8 && !memcmp(arg,"GPLONLY_",8) ){
      arg += 8;
      len -= 8;
    }
  }
  return arg;
}

/***********************************/

static const symb *search(unsigned KLONG address, symb *idx, unsigned count){
  unsigned left;
  unsigned mid;
  unsigned right;
  if(!idx) return NULL;   /* maybe not allocated */
  if(address < idx[0].addr) return NULL;
  if(address >= idx[count-1].addr) return idx+count-1;
  left  = 0;
  right = count-1;
  for(;;){
    mid = (left + right) / 2;
    if(address >= idx[mid].addr) left  = mid;
    if(address <= idx[mid].addr) right = mid;
    if(right-left <= 1) break;
  }
  if(address == idx[right].addr) return idx+right;
  return idx+left;
}

/*********************************/

/* allocate if needed, read, and return buffer size */
static void read_file(const char *restrict filename, char **bufp, unsigned *restrict roomp) {
  int fd = 0;
  ssize_t done;
  char *buf = *bufp;
  ssize_t total = 0;
  unsigned room = *roomp;

  if(!room) goto hell;     /* failed before */
  if(!buf) buf = xmalloc(room);
open_again:
  fd = open(filename, O_RDONLY|O_NOCTTY|O_NONBLOCK);
  if(fd<0){
    switch(errno){
    case EINTR:  goto open_again;
    default:     _exit(101);
    case EACCES:   /* somebody screwing around? */
      /* FIXME: set a flag to disable symbol lookup? */
    case ENOENT:;  /* no module support */
    }
    goto hell;
  }
  for(;;){
    done = read(fd, buf+total, room-total-1);
    if(done==0) break;  /* nothing left */
    if(done==-1){
      if(errno==EINTR) continue;  /* try again */
      perror("");
      goto hell;
    }
    if(done==(ssize_t)room-total-1){
      char *tmp;
      total += done;
      /* more to go, but no room in buffer */
      room *= 2;
      tmp = xrealloc(buf, room);
      buf = tmp;
      continue;
    }
    if(done>0 && done<(ssize_t)room-total-1){
      total += done;
      continue;   /* OK, we read some. Go do more. */
    }
    fprintf(stderr,"%ld can't happen\n", (long)done);
    /* FIXME: memory leak */
    _exit(42);
  }
  buf[total] = '\0';   // parse_ksyms() expects NUL-terminated file
  *bufp = buf;
  *roomp = room;
  close(fd);
  return;
hell:
  free(buf);
  *bufp = NULL;
  *roomp = 0;   /* this function will never work again */
  total = 0;
  if(fd>0) close(fd);
  return;
}

/*********************************/

static int parse_ksyms(void) {
  char *endp;
  if(!ksyms_room || !ksyms_data) goto quiet_goodbye;
  endp = ksyms_data;
  ksyms_count = 0;
  if(idx_room) goto bypass;  /* some space already allocated */
  idx_room = 512;
  for(;;){
    void *vp;
    idx_room *= 2;
    vp = xrealloc(ksyms_index, sizeof(symb)*idx_room);
    ksyms_index = vp;
bypass:
    for(;;){
      char *saved;
      if(!*endp) return 1;
      saved = endp;
      ksyms_index[ksyms_count].addr = STRTOUKL(endp, &endp, 16);
      if(endp==saved || *endp != ' ') goto bad_parse;
      endp++;
      saved = endp;
      endp = strchr(endp,'\n');
      if(!endp) goto bad_parse;   /* no newline */
      *endp = '\0';
      ksyms_index[ksyms_count].name = chop_version(saved);
      ++endp;
      if(++ksyms_count >= idx_room) break;  /* need more space */
    }
  }

  if(0){
bad_parse:
    fprintf(stderr, "Warning: "KSYMS_FILENAME" not normal\n");
  }
quiet_goodbye:
  idx_room = 0;
  free(ksyms_data);
  ksyms_data = NULL;
  ksyms_room = 0;
  free(ksyms_index);
  ksyms_index = NULL;
  ksyms_count = 0;
  return 0;
}

/*********************************/

#define VCNT 16

static int sysmap_mmap(const char *restrict const filename, message_fn message) {
  struct stat sbuf;
  char *endp;
  int fd;
  char Version[32];
  fd = open(filename, O_RDONLY|O_NOCTTY|O_NONBLOCK);
  if(fd<0) return 0;
  if(fstat(fd, &sbuf) < 0) goto bad_open;
  if(!S_ISREG(sbuf.st_mode)) goto bad_open;
  if(sbuf.st_size < 5000) goto bad_open;  /* if way too small */
  /* Would be shared read-only, but we want '\0' after each name. */
  endp = mmap(0, sbuf.st_size + 1, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
  sysmap_data = endp;
  while(*endp==' '){  /* damn Alpha machine types */
    if(strncmp(endp,"                 w ", 19)) goto bad_parse;
    endp += 19;
    endp = strchr(endp,'\n');
    if(!endp) goto bad_parse;   /* no newline */
    if(strncmp(endp-3, "_mv\n", 4)) goto bad_parse;
    endp++;
  }
  if(sysmap_data == (caddr_t) -1) goto bad_open;
  close(fd);
  fd = -1;
  sprintf(Version, "Version_%d", linux_version_code);
  sysmap_room = 512;
  for(;;){
    void *vp;
    sysmap_room *= 2;
    vp = xrealloc(sysmap_index, sizeof(symb)*sysmap_room);
    sysmap_index = vp;
    for(;;){
      char *vstart;
      if(endp - sysmap_data >= sbuf.st_size){   /* if we reached the end */
        int i = VCNT;            /* check VCNT times to verify this file */
        if(*Version) goto bad_version;
        if(!ksyms_index) return 1; /* if can not verify, assume success */
        while(i--){
#if 1
          const symb *findme;
          const symb *map_symb;
          /* Choose VCNT entries from /proc/ksyms to test */
          findme = ksyms_index + (ksyms_count*i/VCNT);
          /* Search for them in the System.map */
          map_symb = search(findme->addr, sysmap_index, sysmap_count);
          if(map_symb){
            if(map_symb->addr != findme->addr) continue;
            /* backup to first matching address */
            while (map_symb != sysmap_index){
              if (map_symb->addr != (map_symb-1)->addr) break;
              map_symb--;
            }
            /* search for name in symbols with same address */
            while (map_symb != (sysmap_index+sysmap_count)){
              if (map_symb->addr != findme->addr) break;
              if (!strcmp(map_symb->name,findme->name)) goto good_match;
              map_symb++;
            }
            map_symb--; /* backup to last symbol with matching address */
            message("{%s} {%s}\n",map_symb->name,findme->name);
            goto bad_match;
          }
good_match:;
#endif
        }
        return 1; /* success */
      }
      sysmap_index[sysmap_count].addr = STRTOUKL(endp, &endp, 16);
      if(*endp != ' ') goto bad_parse;
      endp++;
      if(!strchr(SYMBOL_TYPE_CHARS, *endp)) goto bad_parse;
      endp++;
      if(*endp != ' ') goto bad_parse;
      endp++;
      vstart = endp;
      endp = strchr(endp,'\n');
      if(!endp) goto bad_parse;   /* no newline */
      *endp = '\0';
      ++endp;
      vstart = chop_version(vstart);
      sysmap_index[sysmap_count].name = vstart;
      if(*vstart=='V' && *Version && !strcmp(Version,vstart)) *Version='\0';
      if(++sysmap_count >= sysmap_room) break;  /* need more space */
    }
  }

#ifdef BUILD_WITH_WHINE
  if(0){
bad_match:
    message("Warning: %s does not match kernel data.\n", filename);
  }
#endif
  if(0){
bad_version:
    message("Warning: %s has an incorrect kernel version.\n", filename);
  }
  if(0){
bad_alloc:
    message("Warning: not enough memory available\n");
  }
#ifdef BUILD_WITH_WHINE
  if(0){
bad_parse:
    message("Warning: %s not parseable as a System.map\n", filename);
  }
#endif
  if(0){
bad_open:
    message("Warning: %s could not be opened as a System.map\n", filename);
  }

  sysmap_room=0;
  sysmap_count=0;
  free(sysmap_index);
  sysmap_index = NULL;
  if(fd>=0) close(fd);
  if(sysmap_data) munmap(sysmap_data, sbuf.st_size + 1);
  sysmap_data = NULL;
  return 0;
}

/*********************************/

static void read_and_parse(void){
  static time_t stamp;    /* after data gets old, load /proc/ksyms again */
  if(time(NULL) != stamp){
    read_file(KSYMS_FILENAME, &ksyms_data, &ksyms_room);
    parse_ksyms();
    memset((void*)hashtable,0,sizeof(hashtable)); /* invalidate cache */
    stamp = time(NULL);
  }
}

/*********************************/

static void default_message(const char *restrict format, ...) __attribute__((format(printf,1,2)));
static void default_message(const char *restrict format, ...) {
    va_list arg;

    va_start (arg, format);
    vfprintf (stderr, format, arg);
    va_end (arg);
}

/*********************************/

static int use_wchan_file;

int open_psdb_message(const char *restrict override, message_fn message) {
  static const char *sysmap_paths[] = {
    "/boot/System.map-%s",
    "/boot/System.map",
    "/lib/modules/%s/System.map",
    "/usr/src/linux/System.map",
    "/System.map",
    NULL
  };
  struct stat sbuf;
  struct utsname uts;
  char path[128];
  const char **fmt = sysmap_paths;
  const char *sm;

#ifdef SYSMAP_FILENAME    /* debug feature */
  override = SYSMAP_FILENAME;
#endif

  // first allow for a user-selected System.map file
  if(
    (sm=override)
    ||
    (sm=getenv("PS_SYSMAP"))
    ||
    (sm=getenv("PS_SYSTEM_MAP"))
  ){
    if(!have_privs){
      read_and_parse();
      if(sysmap_mmap(sm, message)) return 0;
    }
    /* failure is better than ignoring the user & using bad data */
    return -1;           /* ought to return "Namelist not found." */
  }

  // next try the Linux 2.5.xx method
  if(!stat("/proc/self/wchan", &sbuf)){
    use_wchan_file = 1; // hack
    return 0;
  }

  // finally, search for the System.map file
  uname(&uts);
  path[sizeof path - 1] = '\0';
  do{
    int did_ksyms = 0;
    snprintf(path, sizeof path - 1, *fmt, uts.release);
    if(!stat(path, &sbuf)){
      if (did_ksyms++) read_and_parse();
      if (sysmap_mmap(path, message)) return 0;
    }
  }while(*++fmt);
  /* TODO: Without System.map, no need to keep ksyms loaded. */
  return -1;
}

/***************************************/

int open_psdb(const char *restrict override) {
    return open_psdb_message(override, default_message);
}

/***************************************/

static const char * read_wchan_file(unsigned pid){
  static char buf[64];
  const char *ret = buf;
  ssize_t num;
  int fd;

  snprintf(buf, sizeof buf, "/proc/%d/wchan", pid);
  fd = open(buf, O_RDONLY);
  if(fd==-1) return "?";
  num = read(fd, buf, sizeof buf - 1);
  close(fd);
  if(num<1) return "?"; // allow for "0"
  buf[num] = '\0';

  if(buf[0]=='0' && buf[1]=='\0') return "-";

  // would skip over numbers if they existed -- but no

  // lame ppc64 has a '.' in front of every name
  if(*ret=='.') ret++;
  switch(*ret){
    case 's': if(!strncmp(ret, "sys_", 4)) ret += 4;   break;
    case 'd': if(!strncmp(ret, "do_",  3)) ret += 3;   break;
    case '_': while(*ret=='_') ret++;                  break;
  }
  return ret;
}

/***************************************/

static const symb fail = { .name = "?" };
static const char dash[] = "-";
static const char star[] = "*";

#define MAX_OFFSET (0x1000*sizeof(long))  /* past this is generally junk */

/* return pointer to temporary static buffer with function name */
const char * lookup_wchan(unsigned KLONG address, unsigned pid) {
  const symb *mod_symb;
  const symb *map_symb;
  const symb *good_symb;
  const char *ret;
  unsigned hash;

  // can't cache it due to a race condition :-(
  if(use_wchan_file) return read_wchan_file(pid);

  if(!address)  return dash;
  if(!~address) return star;

  read_and_parse();
  hash = (address >> 4) & 0xff;  /* got 56/63 hits & 7/63 misses */
  if(hashtable[hash].addr == address) return hashtable[hash].name;
  mod_symb = search(address, ksyms_index,  ksyms_count);
  if(!mod_symb) mod_symb = &fail;
  map_symb = search(address, sysmap_index, sysmap_count);
  if(!map_symb) map_symb = &fail;

  /* which result is closest? */
  good_symb = (mod_symb->addr > map_symb->addr)
            ? mod_symb
            : map_symb
  ;
  if(address > good_symb->addr + MAX_OFFSET) good_symb = &fail;

  /* good_symb->name has the data, but needs to be trimmed */
  ret = good_symb->name;
  // lame ppc64 has a '.' in front of every name
  if(*ret=='.') ret++;
  switch(*ret){
    case 's': if(!strncmp(ret, "sys_", 4)) ret += 4;   break;
    case 'd': if(!strncmp(ret, "do_",  3)) ret += 3;   break;
    case '_': while(*ret=='_') ret++;                  break;
  }
  /* if(!*ret) ret = fail.name; */  /* not likely (name was "sys_", etc.) */

  /* cache name after abbreviation */
  hashtable[hash].addr = address;
  hashtable[hash].name = ret;

  return ret;
}
