/*
 * Copyright 1998-2002 by Albert Cahalan; all rights reserved.
 * This file may be used subject to the terms and conditions of the
 * GNU Library General Public License Version 2, or any later version
 * at your option, as published by the Free Software Foundation.
 * 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 Library General Public License for more details.
 */
#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 "version.h"
#include "sysinfo.h"		/* smp_num_cpus */

#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 {
	const char *name;
	unsigned long addr;
} symb;

static const symb fail = { "?", 0 };

static const char dash[] = "-";

/* 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 void 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:
				return;
			case '0' ... '9':
			case 'a' ... 'f':
				len++;
				continue;
			case 'g' ... 'z':
			case '_':
				len = 0;
				continue;
			}
		}
		if (len < 8)
			break;
		cp[-1] = '\0';
	}
}
#endif
static void 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';
	}
}

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

static const symb *search(unsigned long 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 = malloc(room);
	if (!buf)
		goto hell;
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 = realloc(buf, room);
			if (!tmp)
				goto hell;
			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);
	}
	*bufp = buf;
	*roomp = room;
	close(fd);
	return;
hell:
	free(buf);
	*bufp = NULL;
	*roomp = 0;		/* this function will never work again */
	total = 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 = realloc(ksyms_index, sizeof(symb) * idx_room);
		if (!vp)
			goto bad_alloc;
		ksyms_index = vp;
bypass:
		for (;;) {
			char *saved;
			if (!*endp)
				return 1;
			saved = endp;
			ksyms_index[ksyms_count].addr =
			    strtoul(endp, &endp, 16);
			if (endp == saved || *endp != ' ')
				goto bad_parse;
			endp++;
			ksyms_index[ksyms_count].name = endp;
			saved = endp;
			endp = strchr(endp, '\n');
			if (!endp)
				goto bad_parse;	/* no newline */
			*endp = '\0';
			chop_version(saved);
			++endp;
			if (++ksyms_count >= idx_room)
				break;	/* need more space */
		}
	}

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

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

#define VCNT 16

static int sysmap_mmap(const char *restrict const filename,
		       void (*message) (const char *restrict, ...))
{
	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 = realloc(sysmap_index, sizeof(symb) * sysmap_room);
		if (!vp)
			goto bad_alloc;
		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 =
			    strtoul(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++;
			sysmap_index[sysmap_count].name = endp;
			vstart = endp;
			endp = strchr(endp, '\n');
			if (!endp)
				goto bad_parse;	/* no newline */
			*endp = '\0';
			++endp;
			chop_version(vstart);
			if (*vstart == 'V' && *Version
			    && !strcmp(Version, vstart))
				*Version = '\0';
			if (++sysmap_count >= sysmap_room)
				break;	/* need more space */
		}
	}

	if (0) {
bad_match:
		message("Warning: %s does not match kernel data.\n", filename);
	}
	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");
	}
	if (0) {
bad_parse:
		message("Warning: %s not parseable as a System.map\n",
			filename);
	}
	if (0) {
bad_open:
		message("Warning: %s could not be opened as a System.map\n",
			filename);
	}

	sysmap_room = 0;
	sysmap_count = 0;
	if (sysmap_index)
		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, ...)
{
	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,
		      void (*message) (const char *, ...))
{
	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[64];
	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"))
	    ) {
		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);
	do {
		int did_ksyms = 0;
		snprintf(path, sizeof path, *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);
}

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

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

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

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

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

/* return pointer to temporary static buffer with function name */
const char *wchan(unsigned long 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;

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