/*
 * Copyright 1998-2002 by Albert Cahalan; all rights resered.
 * 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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <sys/sysmacros.h>
#include "version.h"
#include "devname.h"

#ifndef PAGE_SIZE
#define PAGE_SIZE sysconf(_SC_PAGE_SIZE)
#endif

/* Who uses what:
 *
 * tty_to_dev   oldps, w (there is a fancy version in ps)
 * dev_to_tty   oldps, top, ps
 */

typedef struct tty_map_node {
	struct tty_map_node *next;
	int major_number;	/* not unsigned! Ugh... */
	int minor_first, minor_last;
	char name[16];
	char devfs_type;
} tty_map_node;

static tty_map_node *tty_map = NULL;

/* Load /proc/tty/drivers for device name mapping use. */
static void load_drivers(void)
{
	char buf[10000];
	char *p;
	int fd;
	int bytes;
	fd = open("/proc/tty/drivers", O_RDONLY);
	if (fd == -1)
		goto fail;
	bytes = read(fd, buf, sizeof(buf) - 1);
	if (bytes == -1)
		goto fail;
	buf[bytes] = '\0';
	p = buf;
	while ((p = strstr(p, " /dev/"))) {
		tty_map_node *tmn;
		int len;
		char *end;
		p += 6;
		end = strchr(p, ' ');
		if (!end)
			continue;
		len = end - p;
		tmn = calloc(1, sizeof(tty_map_node));
		tmn->next = tty_map;
		tty_map = tmn;
		/* if we have a devfs type name such as /dev/tts/%d then strip the %d but
		   keep a flag. */
		if (len >= 3 && !strncmp(end - 2, "%d", 2)) {
			len -= 2;
			tmn->devfs_type = 1;
		}
		strncpy(tmn->name, p, len);
		p = end;	/* set p to point past the %d as well if there is one */
		while (*p == ' ')
			p++;
		tmn->major_number = atoi(p);
		p += strspn(p, "0123456789");
		while (*p == ' ')
			p++;
		switch (sscanf(p, "%d-%d", &tmn->minor_first, &tmn->minor_last)) {
		default:
			/* Can't finish parsing this line so we remove it from the list */
			tty_map = tty_map->next;
			free(tmn);
			break;
		case 1:
			tmn->minor_last = tmn->minor_first;
			break;
		case 2:
			break;
		}
	}
fail:
	if (fd != -1)
		close(fd);
	if (!tty_map)
		tty_map = (tty_map_node *) - 1;
}

/* Try to guess the device name from /proc/tty/drivers info. */
static int driver_name(char *restrict const buf, int maj, int min)
{
	struct stat sbuf;
	tty_map_node *tmn;
	if (!tty_map)
		load_drivers();
	if (tty_map == (tty_map_node *) - 1)
		return 0;
	tmn = tty_map;
	for (;;) {
		if (!tmn)
			return 0;
		if (tmn->major_number == maj && tmn->minor_first <= min
		    && tmn->minor_last >= min)
			break;
		tmn = tmn->next;
	}
	sprintf(buf, "/dev/%s%d", tmn->name, min);	/* like "/dev/ttyZZ255" */
	if (stat(buf, &sbuf) < 0) {
		if (tmn->devfs_type)
			return 0;
		sprintf(buf, "/dev/%s", tmn->name);	/* like "/dev/ttyZZ255" */
		if (stat(buf, &sbuf) < 0)
			return 0;
	}
	if (min != minor(sbuf.st_rdev))
		return 0;
	if (maj != major(sbuf.st_rdev))
		return 0;
	return 1;
}

/* Try to guess the device name (useful until /proc/PID/tty is added) */
static int guess_name(char *restrict const buf, int maj, int min)
{
	struct stat sbuf;
	int t0, t1;
	int tmpmin = min;
	switch (maj) {
	case 4:
		if (min < 64) {
			sprintf(buf, "/dev/tty%d", min);
			break;
		}
		if (min < 128) {	/* to 255 on newer systems */
			sprintf(buf, "/dev/ttyS%d", min - 64);
			break;
		}
		tmpmin = min & 0x3f;	/* FALL THROUGH */
	case 3:		/* /dev/[pt]ty[p-za-o][0-9a-z] is 936 */
		t0 = "pqrstuvwxyzabcde"[tmpmin >> 4];
		t1 = "0123456789abcdef"[tmpmin & 0x0f];
		sprintf(buf, "/dev/tty%c%c", t0, t1);
		break;
	case 17:
		sprintf(buf, "/dev/ttyH%d", min);
		break;
	case 19:
		sprintf(buf, "/dev/ttyC%d", min);
		break;
	case 22:
		sprintf(buf, "/dev/ttyD%d", min);
		break;		/* devices.txt */
	case 23:
		sprintf(buf, "/dev/ttyD%d", min);
		break;		/* driver code */
	case 24:
		sprintf(buf, "/dev/ttyE%d", min);
		break;
	case 32:
		sprintf(buf, "/dev/ttyX%d", min);
		break;
	case 43:
		sprintf(buf, "/dev/ttyI%d", min);
		break;
	case 46:
		sprintf(buf, "/dev/ttyR%d", min);
		break;
	case 48:
		sprintf(buf, "/dev/ttyL%d", min);
		break;
	case 57:
		sprintf(buf, "/dev/ttyP%d", min);
		break;
	case 71:
		sprintf(buf, "/dev/ttyF%d", min);
		break;
	case 75:
		sprintf(buf, "/dev/ttyW%d", min);
		break;
	case 78:
		sprintf(buf, "/dev/ttyM%d", min);
		break;		/* conflict */
	case 105:
		sprintf(buf, "/dev/ttyV%d", min);
		break;
	case 112:
		sprintf(buf, "/dev/ttyM%d", min);
		break;		/* conflict */
		/* 136 ... 143 are /dev/pts/0, /dev/pts/1, /dev/pts/2 ... */
	case 136 ... 143:
		sprintf(buf, "/dev/pts/%d", min + (maj - 136) * 256);
		break;
	case 148:
		sprintf(buf, "/dev/ttyT%d", min);
		break;
	case 154:
		sprintf(buf, "/dev/ttySR%d", min);
		break;
	case 156:
		sprintf(buf, "/dev/ttySR%d", min + 256);
		break;
	case 164:
		sprintf(buf, "/dev/ttyCH%d", min);
		break;
	case 166:
		sprintf(buf, "/dev/ttyACM%d", min);
		break;		/* bummer, 9-char */
	case 172:
		sprintf(buf, "/dev/ttyMX%d", min);
		break;
	case 174:
		sprintf(buf, "/dev/ttySI%d", min);
		break;
	case 188:
		sprintf(buf, "/dev/ttyUSB%d", min);
		break;		/* bummer, 9-char */
	default:
		return 0;
	}
	if (stat(buf, &sbuf) < 0)
		return 0;
	if (min != minor(sbuf.st_rdev))
		return 0;
	if (maj != major(sbuf.st_rdev))
		return 0;
	return 1;
}

/* Linux 2.2 can give us filenames that might be correct.
 * Useful names could be in /proc/PID/fd/2 (stderr, seldom redirected)
 * and in /proc/PID/fd/255 (used by bash to remember the tty).
 */
static int link_name(char *restrict const buf, int maj, int min, int pid,
		     const char *restrict name)
{
	struct stat sbuf;
	char path[32];
	int count;
	sprintf(path, "/proc/%d/%s", pid, name);	/* often permission denied */
	count = readlink(path, buf, PAGE_SIZE - 1);
	if (count == -1)
		return 0;
	buf[count] = '\0';
	if (stat(buf, &sbuf) < 0)
		return 0;
	if (min != minor(sbuf.st_rdev))
		return 0;
	if (maj != major(sbuf.st_rdev))
		return 0;
	return 1;
}

/* number --> name */
unsigned dev_to_tty(char *restrict ret, unsigned chop, int dev, int pid,
		    unsigned int flags)
{
	static char buf[PAGE_SIZE];
	char *restrict tmp = buf;
	unsigned i = 0;
	int c;
	if ((short)dev == (short)0)
		goto no_tty;
	if (linux_version_code > LINUX_VERSION(2, 7, 0)) {	// not likely to make 2.6.xx
		if (link_name(tmp, major(dev), minor(dev), pid, "tty"))
			goto abbrev;
	}
	if (driver_name(tmp, major(dev), minor(dev)))
		goto abbrev;
	if (link_name(tmp, major(dev), minor(dev), pid, "fd/2"))
		goto abbrev;
	if (guess_name(tmp, major(dev), minor(dev)))
		goto abbrev;
	if (link_name(tmp, major(dev), minor(dev), pid, "fd/255"))
		goto abbrev;
	// fall through if unable to find a device file
no_tty:
	strcpy(ret, "?");
	return 1;
abbrev:
	if ((flags & ABBREV_DEV) && !strncmp(tmp, "/dev/", 5) && tmp[5])
		tmp += 5;
	if ((flags & ABBREV_TTY) && !strncmp(tmp, "tty", 3) && tmp[3])
		tmp += 3;
	if ((flags & ABBREV_PTS) && !strncmp(tmp, "pts/", 4) && tmp[4])
		tmp += 4;
	/* gotta check before we chop or we may chop someone else's memory */
	if (chop + (unsigned long)(tmp - buf) <= sizeof buf)
		tmp[chop] = '\0';
	/* replace non-ASCII characters with '?' and return the number of chars */
	for (;;) {
		c = *tmp;
		tmp++;
		if (!c)
			break;
		i++;
		if (c <= ' ')
			c = '?';
		if (c > 126)
			c = '?';
		*ret = c;
		ret++;
	}
	*ret = '\0';
	return i;
}

/* name --> number */
int tty_to_dev(const char *restrict const name)
{
	struct stat sbuf;
	static char buf[32];
	if (name[0] == '/' && stat(name, &sbuf) >= 0)
		return sbuf.st_rdev;
	snprintf(buf, 32, "/dev/%s", name);
	if (stat(buf, &sbuf) >= 0)
		return sbuf.st_rdev;
	snprintf(buf, 32, "/dev/tty%s", name);
	if (stat(buf, &sbuf) >= 0)
		return sbuf.st_rdev;
	snprintf(buf, 32, "/dev/pts/%s", name);
	if (stat(buf, &sbuf) >= 0)
		return sbuf.st_rdev;
	return -1;
}
