/*
 * lsfd-cdev.c - handle associations opening character devices
 *
 * Copyright (C) 2021 Red Hat, Inc. All rights reserved.
 * Written by Masatake YAMATO <yamato@redhat.com>
 *
 * 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 would 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
 */

#include "lsfd.h"
#include "pidfd-utils.h"

#include <sys/ioctl.h>		/* ioctl */
#include <linux/if_tun.h>	/* TUNGETDEVNETNS */

static struct list_head miscdevs;
static struct list_head ttydrvs;

struct miscdev {
	struct list_head miscdevs;
	unsigned long minor;
	char *name;
};

struct ttydrv {
	struct list_head ttydrvs;
	unsigned long major;
	unsigned long minor_start, minor_end;
	char *name;
	unsigned int is_ptmx: 1;
	unsigned int is_pts: 1;
};

struct cdev {
	struct file file;
	const char *devdrv;
	const struct cdev_ops *cdev_ops;
	void *cdev_data;
};

struct cdev_ops {
	const struct cdev_ops *parent;
	bool (*probe)(struct cdev *);
	char * (*get_name)(struct cdev *);
	bool (*fill_column)(struct proc *,
			    struct cdev *,
			    struct libscols_line *,
			    int,
			    size_t,
			    char **);
	void (*init)(const struct cdev *);
	void (*free)(const struct cdev *);

	bool (*needs_target_fd)(struct cdev *);
	void (*inspect_target_fd)(struct cdev *, int);

	void (*attach_xinfo)(struct cdev *);
	int (*handle_fdinfo)(struct cdev *, const char *, const char *);
	const struct ipc_class * (*get_ipc_class)(struct cdev *);
};

static bool cdev_fill_column(struct proc *proc __attribute__((__unused__)),
			     struct file *file,
			     struct libscols_line *ln,
			     int column_id,
			     size_t column_index)
{
	struct cdev *cdev = (struct cdev *)file;
	const struct cdev_ops *ops = cdev->cdev_ops;
	char *str = NULL;

	switch(column_id) {
	case COL_NAME:
		if (cdev->cdev_ops->get_name) {
			str = cdev->cdev_ops->get_name(cdev);
			if (str)
				break;
		}
		return false;
	case COL_TYPE:
		if (scols_line_set_data(ln, column_index, "CHR"))
			err(EXIT_FAILURE, _("failed to add output data"));
		return true;
	case COL_DEVTYPE:
		if (scols_line_set_data(ln, column_index,
					"char"))
			err(EXIT_FAILURE, _("failed to add output data"));
		return true;
	case COL_CHRDRV:
		if (cdev->devdrv)
			str = xstrdup(cdev->devdrv);
		else
			xasprintf(&str, "%u",
				  major(file->stat.st_rdev));
		break;
	default:
		while (ops) {
			if (ops->fill_column
			    && ops->fill_column(proc, cdev, ln,
						column_id, column_index, &str))
				goto out;
			ops = ops->parent;
		}
		return false;
	}

 out:
	if (!str)
		err(EXIT_FAILURE, _("failed to add output data"));
	if (scols_line_refer_data(ln, column_index, str))
		err(EXIT_FAILURE, _("failed to add output data"));
	return true;
}

static struct miscdev *new_miscdev(unsigned long minor, const char *name)
{
	struct miscdev *miscdev = xcalloc(1, sizeof(*miscdev));

	INIT_LIST_HEAD(&miscdev->miscdevs);

	miscdev->minor = minor;
	miscdev->name = xstrdup(name);

	return miscdev;
}

static void free_miscdev(struct miscdev *miscdev)
{
	free(miscdev->name);
	free(miscdev);
}

static void read_misc(struct list_head *miscdevs_list, FILE *misc_fp)
{
	unsigned long minor;
	char line[256];
	char name[sizeof(line)];

	while (fgets(line, sizeof(line), misc_fp)) {
		struct miscdev *miscdev;

		if (sscanf(line, "%lu %s", &minor, name) != 2)
			continue;

		miscdev = new_miscdev(minor, name);
		list_add_tail(&miscdev->miscdevs, miscdevs_list);
	}
}

#define TTY_DRIVERS_LINE_LEN0 1023
#define TTY_DRIVERS_LINE_LEN  (TTY_DRIVERS_LINE_LEN0 + 1)
static struct ttydrv *new_ttydrv(unsigned int major,
				 unsigned int minor_start, unsigned int minor_end,
				 const char *name)
{
	struct ttydrv *ttydrv = xmalloc(sizeof(*ttydrv));

	INIT_LIST_HEAD(&ttydrv->ttydrvs);

	ttydrv->major = major;
	ttydrv->minor_start = minor_start;
	ttydrv->minor_end = minor_end;
	ttydrv->name = xstrdup(name);
	ttydrv->is_ptmx = 0;
	if (strcmp(name, "ptmx") == 0)
		ttydrv->is_ptmx = 1;
	ttydrv->is_pts = 0;
	if (strcmp(name, "pts") == 0)
		ttydrv->is_pts = 1;

	return ttydrv;
}

static void free_ttydrv(struct ttydrv *ttydrv)
{
	free(ttydrv->name);
	free(ttydrv);
}

static bool is_pty(const struct ttydrv *ttydrv)
{
	return ttydrv->is_ptmx || ttydrv->is_pts;
}

static struct ttydrv *read_ttydrv(const char *line)
{
	const char *p;
	char name[TTY_DRIVERS_LINE_LEN];
	unsigned long major;
	unsigned long minor_range[2];

	p = strchr(line, ' ');
	if (p == NULL)
		return NULL;

	p = strstr(p, "/dev/");
	if (p == NULL)
		return NULL;
	p += (sizeof("/dev/") - 1); /* Ignore the last null byte. */

	if (sscanf(p, "%" stringify_value(TTY_DRIVERS_LINE_LEN0) "[^ ]", name) != 1)
		return NULL;

	p += strlen(name);
	if (sscanf(p, " %lu %lu-%lu ", &major,
		   minor_range, minor_range + 1) != 3) {
		if (sscanf(p, " %lu %lu ", &major, minor_range) == 2)
			minor_range[1] = minor_range[0];
		else
			return NULL;
	}

	return new_ttydrv(major, minor_range[0], minor_range[1], name);
}

static void read_tty_drivers(struct list_head *ttydrvs_list, FILE *ttydrvs_fp)
{
	char line[TTY_DRIVERS_LINE_LEN];

	while (fgets(line, sizeof(line), ttydrvs_fp)) {
		struct ttydrv *ttydrv = read_ttydrv(line);
		if (ttydrv)
			list_add_tail(&ttydrv->ttydrvs, ttydrvs_list);
	}
}

static void cdev_class_initialize(void)
{
	FILE *misc_fp;
	FILE *ttydrvs_fp;

	INIT_LIST_HEAD(&miscdevs);
	INIT_LIST_HEAD(&ttydrvs);

	misc_fp = fopen("/proc/misc", "r");
	if (misc_fp) {
		read_misc(&miscdevs, misc_fp);
		fclose(misc_fp);
	}

	ttydrvs_fp = fopen("/proc/tty/drivers", "r");
	if (ttydrvs_fp) {
		read_tty_drivers(&ttydrvs, ttydrvs_fp);
		fclose(ttydrvs_fp);
	}
}

static void cdev_class_finalize(void)
{
	list_free(&miscdevs, struct miscdev, miscdevs, free_miscdev);
	list_free(&ttydrvs,  struct ttydrv,  ttydrvs,  free_ttydrv);
}

const char *get_miscdev(unsigned long minor)
{
	struct list_head *c;
	list_for_each(c, &miscdevs) {
		struct miscdev *miscdev = list_entry(c, struct miscdev, miscdevs);
		if (miscdev->minor == minor)
			return miscdev->name;
	}
	return NULL;
}

static const struct ttydrv *get_ttydrv(unsigned long major,
				       unsigned long minor)
{
	struct list_head *c;

	list_for_each(c, &ttydrvs) {
		struct ttydrv *ttydrv = list_entry(c, struct ttydrv, ttydrvs);
		if (ttydrv->major == major
		    && ttydrv->minor_start <= minor
		    && minor <= ttydrv->minor_end)
			return ttydrv;
	}

	return NULL;
}


/*
 * generic (fallback implementation)
 */
static bool cdev_generic_probe(struct cdev *cdev __attribute__((__unused__)))
{
	return true;
}

static bool cdev_generic_fill_column(struct proc *proc  __attribute__((__unused__)),
				     struct cdev *cdev,
				  struct libscols_line *ln __attribute__((__unused__)),
				  int column_id,
				  size_t column_index __attribute__((__unused__)),
				  char **str)
{
	struct file *file = &cdev->file;

	switch(column_id) {
	case COL_SOURCE:
		if (cdev->devdrv) {
			xasprintf(str, "%s:%u", cdev->devdrv,
				  minor(file->stat.st_rdev));
			return true;
		}
		FALLTHROUGH;
	case COL_MAJMIN:
		xasprintf(str, "%u:%u",
			  major(file->stat.st_rdev),
			  minor(file->stat.st_rdev));
		return true;
	default:
		return false;
	}
}

static struct cdev_ops cdev_generic_ops = {
	.probe = cdev_generic_probe,
	.fill_column = cdev_generic_fill_column,
};

/*
 * misc device driver
 */
static bool cdev_misc_probe(struct cdev *cdev)
{
	return cdev->devdrv && strcmp(cdev->devdrv, "misc") == 0;
}

static bool cdev_misc_fill_column(struct proc *proc  __attribute__((__unused__)),
				  struct cdev *cdev,
				  struct libscols_line *ln __attribute__((__unused__)),
				  int column_id,
				  size_t column_index __attribute__((__unused__)),
				  char **str)
{
	struct file *file = &cdev->file;
	const char *miscdev;

	switch(column_id) {
	case COL_MISCDEV:
		miscdev = get_miscdev(minor(file->stat.st_rdev));
		if (miscdev)
			*str = xstrdup(miscdev);
		else
			xasprintf(str, "%u",
				  minor(file->stat.st_rdev));
		return true;
	case COL_SOURCE:
		miscdev = get_miscdev(minor(file->stat.st_rdev));
		if (miscdev)
			xasprintf(str, "misc:%s", miscdev);
		else
			xasprintf(str, "misc:%u",
				  minor(file->stat.st_rdev));
		return true;
	}
	return false;
}

static struct cdev_ops cdev_misc_ops = {
	.parent = &cdev_generic_ops,
	.probe = cdev_misc_probe,
	.fill_column = cdev_misc_fill_column,
};

/*
 * tun device driver
 */
struct tundata {
	const char *iff;
	ino_t devnetns;		/* 0 implies no value given. */
	ino_t socknetns;
};

static bool cdev_tun_probe(struct cdev *cdev)
{
	const char *miscdev;

	if ((!cdev->devdrv) || strcmp(cdev->devdrv, "misc"))
		return false;

	miscdev = get_miscdev(minor(cdev->file.stat.st_rdev));
	if (miscdev && strcmp(miscdev, "tun") == 0) {
		struct tundata *tundata = xcalloc(1, sizeof *tundata);
		cdev->cdev_data = tundata;
		return true;
	}
	return false;
}

static void cdev_tun_free(const struct cdev *cdev)
{
	if (cdev->cdev_data) {
		struct tundata *tundata = cdev->cdev_data;
		free((void *)tundata->iff);
		free(tundata);
	}
}

static char * cdev_tun_get_name(struct cdev *cdev)
{
	char *str = NULL;
	struct tundata *tundata = cdev->cdev_data;

	if (tundata == NULL || tundata->iff == NULL)
		return NULL;

	xasprintf(&str, "iface=%s", tundata->iff);
	if (tundata->devnetns)
		xstrfappend(&str, " devnetns=%llu",
			    (unsigned long long)tundata->devnetns);
	if (tundata->socknetns)
		xstrfappend(&str, " socknetns=%llu",
			    (unsigned long long)tundata->socknetns);

	return str;
}

static bool cdev_tun_fill_column(struct proc *proc  __attribute__((__unused__)),
				 struct cdev *cdev,
				 struct libscols_line *ln __attribute__((__unused__)),
				 int column_id,
				 size_t column_index __attribute__((__unused__)),
				 char **str)
{
	struct tundata *tundata = cdev->cdev_data;

	switch(column_id) {
	case COL_MISCDEV:
		*str = xstrdup("tun");
		return true;
	case COL_SOURCE:
		*str = xstrdup("misc:tun");
		return true;
	case COL_TUN_DEVNETNS:
		if (tundata && tundata->devnetns) {
			xasprintf(str, "%llu", (unsigned long long)tundata->devnetns);
			return true;
		}
		break;
	case COL_TUN_IFACE:
		if (tundata && tundata->iff) {
			*str = xstrdup(tundata->iff);
			return true;
		}
		break;
	case COL_SOCK_NETNS:
		if (tundata && tundata->socknetns) {
			xasprintf(str, "%llu", (unsigned long long)tundata->socknetns);
			return true;
		}
		break;
	}
	return false;
}

static int cdev_tun_handle_fdinfo(struct cdev *cdev, const char *key, const char *val)
{
	struct tundata *tundata = cdev->cdev_data;

	if (strcmp(key, "iff") == 0 && tundata->iff == NULL) {
		tundata->iff = xstrdup(val);
		return 1;
	}
	return 0;
}

static bool cdev_tun_needs_target_fd(struct cdev *cdev)
{
	struct tundata *tundata = cdev->cdev_data;

	return (tundata->iff != NULL);
}

static void cdev_tun_inspect_target_fd(struct cdev *cdev, int fd)
{
	struct tundata *tundata = cdev->cdev_data;

	tundata->socknetns = get_netns_from_socket(fd);

#ifdef TUNGETDEVNETNS
	int nsfd = ioctl(fd, TUNGETDEVNETNS);
	struct stat sb;

	if (nsfd < 0)
		return;

	if (fstat(nsfd, &sb) == 0) {
		tundata->devnetns = sb.st_ino;
		load_fdsk_xinfo(tundata->devnetns, nsfd);
	}

	close(nsfd);
#endif	/* TUNGETDEVNETNS */
}

static struct cdev_ops cdev_tun_ops = {
	.parent = &cdev_misc_ops,
	.probe = cdev_tun_probe,
	.free  = cdev_tun_free,
	.get_name = cdev_tun_get_name,
	.fill_column = cdev_tun_fill_column,
	.handle_fdinfo = cdev_tun_handle_fdinfo,
	.needs_target_fd = cdev_tun_needs_target_fd,
	.inspect_target_fd = cdev_tun_inspect_target_fd,
};

/*
 * tty devices
 */
struct ttydata {
	struct cdev *cdev;
	const struct ttydrv *drv;
#define NO_TTY_INDEX -1
	int tty_index;		/* used only in ptmx devices */
	struct ipc_endpoint endpoint;
};

static bool cdev_tty_probe(struct cdev *cdev)
{
	const struct ttydrv *ttydrv = get_ttydrv(major(cdev->file.stat.st_rdev),
						 minor(cdev->file.stat.st_rdev));
	struct ttydata *data;

	if (!ttydrv)
		return false;

	data = xmalloc(sizeof(struct ttydata));
	data->cdev = cdev;
	data->drv = ttydrv;
	data->tty_index = NO_TTY_INDEX;
	cdev->cdev_data = data;

	return true;
}

static void cdev_tty_free(const struct cdev *cdev)
{
	if (cdev->cdev_data)
		free(cdev->cdev_data);
}

static char * cdev_tty_get_name(struct cdev *cdev)
{
	struct ttydata *data = cdev->cdev_data;
	char *str = NULL;

	if (!data->drv->is_ptmx)
		return NULL;

	if (data->tty_index == NO_TTY_INDEX)
		str = xstrdup("tty-index=");
	else
		xasprintf(&str, "tty-index=%d", data->tty_index);
	return str;
}

static inline char *cdev_tty_xstrendpoint(struct file *file)
{
	char *str = NULL;
	xasprintf(&str, "%d,%s,%d%c%c",
		  file->proc->pid, file->proc->command, file->association,
		  (file->mode & S_IRUSR)? 'r': '-',
		  (file->mode & S_IWUSR)? 'w': '-');
	return str;
}

static bool cdev_tty_fill_column(struct proc *proc  __attribute__((__unused__)),
				 struct cdev *cdev,
				 struct libscols_line *ln __attribute__((__unused__)),
				 int column_id,
				 size_t column_index __attribute__((__unused__)),
				 char **str)
{
	struct file *file = &cdev->file;
	struct ttydata *data = cdev->cdev_data;

	switch(column_id) {
	case COL_SOURCE:
		if (data->drv->minor_start == data->drv->minor_end)
			*str = xstrdup(data->drv->name);
		else
			xasprintf(str, "%s:%u", data->drv->name,
				  minor(file->stat.st_rdev));
		return true;
	case COL_PTMX_TTY_INDEX:
		if (data->drv->is_ptmx) {
			xasprintf(str, "%d", data->tty_index);
			return true;
		}
		return false;
	case COL_ENDPOINTS:
		if (is_pty(data->drv)) {
			struct ttydata *this = data;
			struct list_head *e;
			foreach_endpoint(e, data->endpoint) {
				char *estr;
				struct ttydata *other = list_entry(e, struct ttydata, endpoint.endpoints);
				if (this == other)
					continue;

				if ((this->drv->is_ptmx && !other->drv->is_pts)
				    || (this->drv->is_pts && !other->drv->is_ptmx))
					continue;

				if (*str)
					xstrputc(str, '\n');
				estr = cdev_tty_xstrendpoint(&other->cdev->file);
				xstrappend(str, estr);
				free(estr);
			}
			if (*str)
				return true;
		}
		return false;
	default:
		return false;
	}
}

static int cdev_tty_handle_fdinfo(struct cdev *cdev, const char *key, const char *val)
{
	struct ttydata *data = cdev->cdev_data;

	if (!data->drv->is_ptmx)
		return 0;

	if (strcmp(key, "tty-index") == 0) {
		errno = 0;
		data->tty_index = (int)strtol(val, NULL, 10);
		if (errno) {
			data->tty_index = NO_TTY_INDEX;
			return 0;
		}
		return 1;
	}

	return 0;
}

struct cdev_pty_ipc {
	struct ipc ipc;
	int tty_index;
};

static unsigned int cdev_pty_get_hash(struct file *file)
{
	struct cdev *cdev = (struct cdev *)file;
	struct ttydata *data = cdev->cdev_data;

	return data->drv->is_ptmx?
		(unsigned int)data->tty_index:
		(unsigned int)minor(file->stat.st_rdev);
}

static bool cdev_pty_is_suitable_ipc(struct ipc *ipc, struct file *file)
{
	struct cdev *cdev = (struct cdev *)file;
	struct ttydata *data = cdev->cdev_data;
	struct cdev_pty_ipc *cdev_pty_ipc = (struct cdev_pty_ipc *)ipc;

	return (data->drv->is_ptmx)?
		cdev_pty_ipc->tty_index == (int)data->tty_index:
		cdev_pty_ipc->tty_index == (int)minor(file->stat.st_rdev);
}

static const struct ipc_class *cdev_tty_get_ipc_class(struct cdev *cdev)
{
	static const struct ipc_class cdev_pty_ipc_class = {
		.size = sizeof(struct cdev_pty_ipc),
		.get_hash = cdev_pty_get_hash,
		.is_suitable_ipc = cdev_pty_is_suitable_ipc,
	};

	struct ttydata *data = cdev->cdev_data;

	if (is_pty(data->drv))
		return &cdev_pty_ipc_class;

	return NULL;
}

static void cdev_tty_attach_xinfo(struct cdev *cdev)
{
	struct ttydata *data = cdev->cdev_data;
	struct ipc *ipc;
	unsigned int hash;


	if (! is_pty(data->drv))
		return;

	init_endpoint(&data->endpoint);
	ipc = get_ipc(&cdev->file);
	if (ipc)
		goto link;

	ipc = new_ipc(cdev_tty_get_ipc_class(cdev));
	hash = cdev_pty_get_hash(&cdev->file);
	((struct cdev_pty_ipc *)ipc)->tty_index = (int)hash;

	add_ipc(ipc, hash);
 link:
	add_endpoint(&data->endpoint, ipc);
}

static struct cdev_ops cdev_tty_ops = {
	.parent = &cdev_generic_ops,
	.probe = cdev_tty_probe,
	.free = cdev_tty_free,
	.get_name = cdev_tty_get_name,
	.fill_column = cdev_tty_fill_column,
	.attach_xinfo  = cdev_tty_attach_xinfo,
	.handle_fdinfo = cdev_tty_handle_fdinfo,
	.get_ipc_class = cdev_tty_get_ipc_class,
};

static const struct cdev_ops *const cdev_ops[] = {
	&cdev_tun_ops,
	&cdev_misc_ops,
	&cdev_tty_ops,
	&cdev_generic_ops		  /* This must be at the end. */
};

static const struct cdev_ops *cdev_probe(struct cdev *cdev)
{
	const struct cdev_ops *r = NULL;

	for (size_t i = 0; i < ARRAY_SIZE(cdev_ops); i++) {
		if (cdev_ops[i]->probe(cdev)) {
			r = cdev_ops[i];
			break;
		}
	}

	assert(r);
	return r;
}

static void init_cdev_content(struct file *file)
{
	struct cdev *cdev = (struct cdev *)file;

	cdev->devdrv = get_chrdrv(major(file->stat.st_rdev));

	cdev->cdev_data = NULL;
	cdev->cdev_ops = cdev_probe(cdev);
	if (cdev->cdev_ops->init)
		cdev->cdev_ops->init(cdev);
}

static void free_cdev_content(struct file *file)
{
	struct cdev *cdev = (struct cdev *)file;

	if (cdev->cdev_ops->free)
		cdev->cdev_ops->free(cdev);
}

static void cdev_attach_xinfo(struct file *file)
{
	struct cdev *cdev = (struct cdev *)file;

	if (cdev->cdev_ops->attach_xinfo)
		cdev->cdev_ops->attach_xinfo(cdev);
}

static int cdev_handle_fdinfo(struct file *file, const char *key, const char *value)
{
	struct cdev *cdev = (struct cdev *)file;

	if (cdev->cdev_ops->handle_fdinfo)
		return cdev->cdev_ops->handle_fdinfo(cdev, key, value);
	return 0;		/* Should be handled in parents */
}

static const struct ipc_class *cdev_get_ipc_class(struct file *file)
{
	struct cdev *cdev = (struct cdev *)file;

	if (cdev->cdev_ops->get_ipc_class)
		return cdev->cdev_ops->get_ipc_class(cdev);
	return NULL;
}

static bool cdev_needs_target_fd(struct file *file)
{
	struct cdev *cdev = (struct cdev *)file;

	if (cdev->cdev_ops->needs_target_fd)
		return cdev->cdev_ops->needs_target_fd(cdev);
	return false;
}

static void cdev_inspect_target_fd(struct file *file, int fd)
{
	struct cdev *cdev = (struct cdev *)file;

	if (cdev->cdev_ops->inspect_target_fd)
		cdev->cdev_ops->inspect_target_fd(cdev, fd);
}

const struct file_class cdev_class = {
	.super = &file_class,
	.size = sizeof(struct cdev),
	.initialize_class = cdev_class_initialize,
	.finalize_class = cdev_class_finalize,
	.fill_column = cdev_fill_column,
	.initialize_content = init_cdev_content,
	.free_content = free_cdev_content,
	.attach_xinfo = cdev_attach_xinfo,
	.handle_fdinfo = cdev_handle_fdinfo,
	.get_ipc_class = cdev_get_ipc_class,
	.needs_target_fd = cdev_needs_target_fd,
	.inspect_target_fd = cdev_inspect_target_fd,
};
