/*
 * vdev: a virtual device manager for *nix
 * Copyright (C) 2015  Jude Nelson
 *
 * This program is dual-licensed: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or later as
 * published by the Free Software Foundation. For the terms of this
 * license, see LICENSE.GPLv3+ or <http://www.gnu.org/licenses/>.
 *
 * You are free to use this program under the terms of the GNU General
 * Public License, 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.
 *
 * Alternatively, you are free to use this program under the terms of the
 * Internet Software Consortium License, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE. For the terms of this license, see LICENSE.ISC or
 * <http://www.isc.org/downloads/software-support-policy/isc-license/>.
 */

// log a device event to each listening program in /dev/events:
// 0. read it from stdin; verify that it has a seqnum
// 1. put it into /dev/events/global/$SEQNUM
// 2. hard-link /dev/events/global/$SEQNUM to each /dev/events/*/$SEQNUM
// 3. unlink /dev/events/$SEQNUM

#include "common.h"
#include "libvdev/misc.h"
#include "libvdev/util.h"

#include <assert.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>

#define DEFAULT_DEV_EVENTS "/dev/metadata/udev/events"

// copy content from source file to target file
// return 0 on success, -1 on I/O error (do not use printf(buffer): uevent text
// can contain '%')
static int file_copy(char const *source, char const *target)
{
	char buf[8192];
	FILE *stream_R = fopen(source, "r");
	FILE *stream_W = fopen(target, "w");

	if (!stream_R || !stream_W) {
		if (stream_R)
			fclose(stream_R);
		if (stream_W)
			fclose(stream_W);
		return -1;
	}

	while (1) {
		size_t bytes = fread(buf, 1, sizeof(buf), stream_R);
		if (bytes == 0) {
			if (ferror(stream_R)) {
				fclose(stream_R);
				fclose(stream_W);
				return -1;
			}
			break;
		}
		if (fwrite(buf, 1, bytes, stream_W) != bytes) {
			fclose(stream_R);
			fclose(stream_W);
			return -1;
		}
	}

	fclose(stream_R);
	fclose(stream_W);
	return 0;
}

// path to device events directory (overrideable in command-line)
static char g_dev_events[PATH_MAX + 1];

// make a path to an event in the global queue
// event_path must have at least PATH_MAX+1 bytes
int make_event_path(uint64_t seqnum, char *event_path)
{
	return snprintf(event_path, PATH_MAX, "%s/global/%" PRIu64, g_dev_events,
					seqnum);
}

// make a path to target in the serial queue
// pathbuf must have at least PATH_MAX+1 bytes
int make_pathbuf(uint64_t seqnum, const char *dir, const char *name,
				 char *pathbuf)
{
	int n = snprintf(pathbuf, PATH_MAX + 1, "%s/%s/%" PRIu64, dir, name, seqnum);

	if (n >= PATH_MAX + 1) {
		vdev_error("Aborted route: the buffer is too small for %s/%s\n", dir, name);
		return -1;
	}
	return n;
}

// open the event file, exclusively, for writing.
// make sure that only the UID/GID of this process (i.e. root) can access it.
int open_event(char *event_path)
{
	int fd = 0;

	fd = open(event_path, O_WRONLY | O_EXCL | O_CREAT, 0600);
	if (fd < 0) {
		log_error("open('%s') rc = %d\n", event_path, -errno);
		return -errno;
	}
	return fd;
}

// clear an event
// return 0 on success
// return -errno (from unlink(2)) on failure
int clear_event(char *event_path)
{
	int rc = unlink(event_path);

	if (rc < 0) {
		rc = -errno;
		log_error("unlink('%s') rc = %d\n", event_path, rc);
	}
	return rc;
}

// link all events to all directories that are siblings to the parent directory
// of the event path. try to do so even if we fail to link in some cases return
// 0 on success return -errno if at least one failed
int multicast_event(uint64_t seqnum, char *event_path)
{
	int rc = 0;
	int failures = 0;
	DIR *dirh = NULL;
	struct dirent *entry;
	char event_queues_dir[PATH_MAX + 1];
	char seqbuf[10];

	seqbuf[9] = 0;

	itoa10_safe(seqnum, seqbuf);

	memset(event_queues_dir, 0, sizeof(event_queues_dir));

	strncpy_t(event_queues_dir, sizeof(event_queues_dir), g_dev_events,
			  strlen(g_dev_events));
	strcat(event_queues_dir, "/serial");

	dirh = opendir(event_queues_dir);
	if (!dirh)
		// OOM
		return -errno;

	while (1) {
		struct dirent *result = NULL;
		char pathbuf[PATH_MAX + 1];
		char head_link[PATH_MAX + 1];
		char head_path[PATH_MAX + 1];
		char tail_link[PATH_MAX + 1];
		char tail_path[PATH_MAX + 1];
		char *tmp = NULL;
		struct stat st;

		errno = 0;
		entry = readdir(dirh);
		if (entry == NULL) {
			if (errno != 0) {
				vdev_error("readdir('%s') errno = %d", event_queues_dir, errno);
				rc = -errno;
			}
			break;
		}

		// skip . and ..
		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
			continue;

		// link to this directory
		make_pathbuf(seqnum, event_queues_dir, entry->d_name, pathbuf);

		// file copy
		if (file_copy(event_path, pathbuf) < 0) {
			vdev_error("[WARN]: event-put: deliver to '%s' failed: %s\n",
					   pathbuf, strerror(errno));
			failures++;
			continue;
		}

		memset(head_link, 0, sizeof(head_link));
		memset(head_path, 0, sizeof(head_path));
		strncpy_t(head_path, sizeof(head_path), event_queues_dir,
				  strlen(event_queues_dir));
		strcat(head_path, "/");
		strcat(head_path, entry->d_name);
		strcat(head_path, "/head");

		rc = readlink(head_path, head_link, sizeof(head_link) - 1);
		if (rc < 0) {
			vdev_error("[WARN]: event-put: readlink('%s') failed: %s\n",
					   head_path, strerror(errno));
			failures++;
			continue;
		}
		head_link[rc] = '\0';
		/*
		 *       rc = -errno;
		 *       if( rc != -ENOENT ) {
		 *
		 *          log_error("[ERR]: readlink('%s') errno = %d\n", head_path, rc );
		 *       }
		 *
		 *       break;
		 */

		memset(tail_link, 0, sizeof(tail_link));
		memset(tail_path, 0, sizeof(tail_path));
		strncpy_t(tail_path, sizeof(tail_path), event_queues_dir,
				  strlen(event_queues_dir));
		strcat(tail_path, "/");
		strcat(tail_path, entry->d_name);
		strcat(tail_path, "/tail");

		rc = readlink(tail_path, tail_link, sizeof(tail_link) - 1);
		if (rc < 0) {
			vdev_error("[WARN]: event-put: readlink('%s') failed: %s\n",
					   tail_path, strerror(errno));
			failures++;
			continue;
		}
		tail_link[rc] = '\0';
		/*
		 *       rc = -errno;
		 *       if( rc != -ENOENT ) {
		 *
		 *          log_error("[ERR]: readlink('%s') errno = %d\n", tail_path, rc );
		 *       }
		 *
		 *       break;
		 */
		tail_link[rc] = '\0';

		if (strcmp(head_link, tail_link)) {
			uint64_t seq;
			char *rest;
			seq = (uint64_t)strtol(head_link, &rest, 10);
			if (rest[0] == '\0' && seq > 1 && (seq < seqnum - 5))
				unlink(head_path);
		}
	}

	closedir(dirh);

	if (failures > 0)
		rc = -EIO;

	return rc;
}

// print usage statement
int usage(char const *progname)
{
	int i = 0;
	char const *usage_text[] = {
		"Usage: ",
		progname,
		" [-v] [-n SEQNUM] [-s SOURCE-QUEUE] [-t TARGET-QUEUES]\n",
		"Options:\n",
		"   -n SEQNUM\n",
		"                  Event sequence number.  Must be\n",
		"                  unique across all pending events.\n",
		"\n",
		"   -s SOURCE-QUEUE\n",
		"                  Path to the source event queue.  An\n",
		"                  event will be added as a child of this\n",
		"                  directory, and linked into all directories\n",
		"                  in TARGET-QUEUES.  The default is\n",
		"                  " DEFAULT_DEV_EVENTS,
		"\n",
		"\n",
		NULL
	};

	for (i = 0; usage_text[i] != NULL; i++)
		fprintf(stderr, usage_text[i]);

	return 0;
}

// print a verbose help statement
int help(char const *progname)
{
	usage(progname);

	int i = 0;
	char const *help_text[] = {
		"This program reads a device event from standard input, such that\n",
		"each line takes the form of KEY=VALUE.  Valid keys and values are:\n",
		"\n",
		"   DEVPATH=(/devices path) [REQUIRED]\n",
		"   SUBSYSTEM=(subsystem name) [REQUIRED]\n",
		"   SEQNUM=(kernel uevent sequence number) [REQUIRED if -n is not "
		"given]\n",
		"   DEVNAME=(/dev node path)\n",
		"   DEVLINKS=(space-separated list of symlinks)\n",
		"   TAGS=(colon-separated list of tags)\n",
		"   USEC_INITIALIZED=(microseconds since device initialization)\n",
		"   DRIVER=(name of device driver)\n",
		"   ACTION=(add, remove, change, move, etc.)\n",
		"   MAJOR=(major device number)\n",
		"   MINOR=(minor device number)\n",
		"   DEVPATH_OLD=(old device path (on move)\n",
		"   IFINDEX=(device interface index, e.g. for USB and network "
		"interfaces)\n",
		"   DEVMODE=(device node permission bits)\n",
		"   DEVUID=(device node UID)\n",
		"   DEVGID=(device node GID)\n",
		"\n",
		"Keys marked with [REQUIRED] must be present.\n",
		"NOTE: events are limited to 8192 bytes.\n",
		NULL
	};

	for (i = 0; help_text[i] != NULL; i++)
		fprintf(stderr, help_text[i]);

	return 0;
}

// entry point
int main(int argc, char **argv)
{
	int rc = 0;
	uint64_t seqnum = 0;
	int opt_index = 0;
	int c = 0;
	int fd = 0;
	char *tmp = NULL;
	char event_buf[8192];
	char event_buf_tmp[8292];   // has to hold event_buf plus an 8-byte sequence
	                            // number
	char event_path[PATH_MAX + 1];

	bool have_seqnum = false;
	ssize_t nr = 0;
	char const *required_fields[] = { "\nSUBSYSTEM=", "\nDEVPATH=", NULL };

	char target_queues[PATH_MAX + 1];

	// default event queue
	memset(g_dev_events, 0, sizeof(g_dev_events));
	memset(target_queues, 0, sizeof(target_queues));
	memset(event_path, 0, sizeof(event_path));

	strncpy_t(g_dev_events, sizeof(g_dev_events), DEFAULT_DEV_EVENTS,
			  strlen(DEFAULT_DEV_EVENTS));

	static struct option opts[] = { { "source-queue", required_argument, 0, 's' },
									{ "seqnum",		  required_argument, 0, 'n' },
									{ "help",		  no_argument,		 0, 'h' },
									{ 0,			  0,				 0, 0	} };

	char const *optstr = "n:s:h";

	while (rc == 0 && c != -1) {
		c = getopt_long(argc, argv, optstr, opts, &opt_index);
		if (c == -1)
			break;

		switch (c) {
		case 's':
			memset(g_dev_events, 0, sizeof(g_dev_events));
			strncpy_t(g_dev_events, sizeof(g_dev_events), optarg, strlen(optarg));
			break;
		case 'n':
			seqnum = (uint64_t)strtoull(optarg, &tmp, 10);
			if (seqnum == 0 && (tmp == optarg || *tmp != '\0')) {
				usage(argv[0]);
				exit(1);
			}
			have_seqnum = true;
			break;
		case 'h':
			help(argv[0]);
			exit(0);
		default:
			vdev_error("%s: Unrecognized option '%c'\n", argv[0], c);
			usage(argv[0]);
			exit(1);
		}
	}

	// get the event
	memset(event_buf, 0, sizeof(event_buf));
	nr = vdev_read_uninterrupted(STDIN_FILENO, event_buf, sizeof(event_buf));
	if (nr <= 0) {
		vdev_error("%s: Failed to read event from stdin: %s\n", argv[0],
				   strerror(errno));
		exit(1);
	}

	// simple sanity check for requirements
	for (int i = 0; required_fields[i] != NULL; i++) {
		if (strstr(event_buf, required_fields[i]) == NULL) {
			// head of line? with no leading '\n'?
			if (strncmp(event_buf, required_fields[i] + 1,
						strlen(required_fields[i]) - 1) != 0) {
				vdev_error("%s: Missing required field '%s'\n", argv[0],
						   required_fields[i] + 1);
				vdev_error("%s: Pass -h for a list of required fields\n", argv[0]);
				exit(1);
			}
		}
	}

	// do we have a seqnum?
	if (!have_seqnum) {
		char *seqnum_str = strstr(event_buf, "SEQNUM=");
		// go find it in the device event
		if (seqnum_str == NULL) {
			vdev_error(
				"%s: Missing SEQNUM.  Pass -n or include SEQNUM= in the input.\n",
				argv[0]);
			exit(1);
		}

		// is it a valid seqnum?
		seqnum = (uint64_t)strtoull(seqnum_str + strlen("SEQNUM="), &tmp, 10);
		if (seqnum == 0 &&
			(tmp == seqnum_str + strlen("SEQNUM=") || *tmp != '\n')) {
			// invalid seqnum
			vdev_error("%s: Invalid SEQNUM.  Pass -n or include a valid SEQNUM in "
					   "the input.\n",
					   argv[0]);
			exit(1);
		}
	}

	// send it off!
	make_event_path(seqnum, event_path);

	memset(event_path, 0, sizeof(event_path));

	snprintf(event_path, sizeof(event_path), "%s/global/%" PRIu64, g_dev_events,
			 seqnum);

	fd = open_event(event_path);
	if (fd < 0) {
		vdev_error("%s: Failed to open '%s': %s\n", argv[0], event_path,
				   strerror(-fd));
		exit(1);
	}

	rc = vdev_write_uninterrupted(fd, event_buf, nr);
	if (rc < 0) {
		vdev_error("%s: Failed to write '%s': %s\n", argv[0], event_path,
				   strerror(-fd));
		clear_event(event_path);
		close(fd);
		fd = -1;
		exit(1);
	}

	// propagate....
	rc = multicast_event(seqnum, event_path);
	if (rc < 0) {
		vdev_error("%s: Failed to multicast '%s': %s\n", argv[0], event_path,
				   strerror(-rc));
		clear_event(event_path);
		close(fd);
		exit(1);
	}

	// done!
	clear_event(event_path);
	close(fd);
	fd = -1;

	return 0;
}
