/*
 * vdevadm trigger — synthesize kernel uevents on sysfs nodes
 */

#include "vdevadm.h"

#include "common.h"

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <unistd.h>

static void trigger_usage(const char *prog)
{
	fprintf(stderr,
			"Usage: %s trigger [OPTIONS] [DEVPATH]\n"
			"\n"
			"Write an action to sysfs .../uevent (default: add).\n"
			"vdevd reacts via netlink like a normal kernel uevent.\n"
			"\n"
			"Options:\n"
			"  -a --action=ACTION     add|remove|change|bind|unbind (default: add)\n"
			"  -s --subsystem-match=S Devices under class/S, bus/S/devices, or\n"
			"                         subsystem/S/devices (eudev-style scan)\n"
			"      --sysfs             Recursive scan of %s/devices (full coldplug)\n"
			"  -p --path-match=PATH   sysfs path prefix under %s\n"
			"  -n --name-match=NAME   final sysfs component name (e.g. event0)\n"
			"      --name-prefix=P     sysfs name must start with P (e.g. event)\n"
			"      --dry-run           Print paths without writing uevent\n"
			"  -h --help              Show help\n",
			prog, "/sys", "/sys");
}

static bool trigger_action_valid(const char *action)
{
	return strcmp(action, "add") == 0 ||
		   strcmp(action, "remove") == 0 ||
		   strcmp(action, "change") == 0 ||
		   strcmp(action, "bind") == 0 ||
		   strcmp(action, "unbind") == 0;
}

static int trigger_write_uevent(const char *syspath, const char *action,
								bool dry_run)
{
	char uevent[PATH_MAX];
	char msg[32];
	int fd;
	ssize_t nw;
	size_t len;

	if (snprintf(uevent, sizeof(uevent), "%s/uevent", syspath) >=
		(int)sizeof(uevent))
		return -ENAMETOOLONG;

	if (dry_run) {
		printf("%s -> %s\n", uevent, action);
		return 0;
	}

	/* Match settle-sync / udevadm: O_WRONLY write, not fopen("w") truncate. */
	if (snprintf(msg, sizeof(msg), "%s\n", action) >= (int)sizeof(msg))
		return -EINVAL;
	len = strlen(msg);

	fd = open(uevent, O_WRONLY | O_CLOEXEC);
	if (fd < 0)
		return -errno;

	nw = write(fd, msg, len);
	if (nw < 0) {
		int err = errno;

		close(fd);
		return -err;
	}
	close(fd);
	return 0;
}

static int trigger_one(struct vdevadm_ctx *ctx, const char *arg,
					   const char *action, bool dry_run)
{
	char syspath[PATH_MAX];
	struct stat st;
	int rc;

	if (strncmp(arg, ctx->sysfs, strlen(ctx->sysfs)) == 0) {
		if (snprintf(syspath, sizeof(syspath), "%s", arg) >= (int)sizeof(syspath))
			return -ENAMETOOLONG;
	} else if (arg[0] == '/') {
		if (snprintf(syspath, sizeof(syspath), "%s%s", ctx->sysfs, arg) >=
			(int)sizeof(syspath))
			return -ENAMETOOLONG;
	} else if (stat(arg, &st) == 0 &&
			   (S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode))) {
		char *sp = NULL;
		size_t splen = 0;

		rc = vdev_sysfs_get_syspath_from_device(
			ctx->sysfs, st.st_mode, major(st.st_rdev), minor(st.st_rdev),
			&sp, &splen);
		if (rc != 0) {
			free(sp);
			return rc;
		}
		if (snprintf(syspath, sizeof(syspath), "%s%s", ctx->sysfs, sp) >=
			(int)sizeof(syspath)) {
			free(sp);
			return -ENAMETOOLONG;
		}
		free(sp);
	} else {
		return -ENOENT;
	}

	return trigger_write_uevent(syspath, action, dry_run);
}

static bool trigger_name_matches(const char *name, const char *name_match,
								 const char *name_prefix)
{
	if (name_match && strcmp(name, name_match) != 0)
		return false;
	if (name_prefix && strncmp(name, name_prefix, strlen(name_prefix)) != 0)
		return false;
	return true;
}

static bool trigger_is_real_dir(const char *path)
{
	struct stat st;

	if (lstat(path, &st) != 0)
		return false;
	return S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode);
}

static bool trigger_is_device_dir(const char *parent, const char *name)
{
	char path[PATH_MAX];

	if (snprintf(path, sizeof(path), "%s/%s", parent, name) >= (int)sizeof(path))
		return false;
	return trigger_is_real_dir(path);
}

/*
 * class/ and bus/.../devices entries are almost always symlinks into
 * /sys/devices. Follow them (stat) and require a uevent node.
 */
static bool trigger_is_class_or_bus_entry(const char *parent, const char *name)
{
	char path[PATH_MAX];
	char uevent[PATH_MAX];
	struct stat st;

	if (snprintf(path, sizeof(path), "%s/%s", parent, name) >= (int)sizeof(path))
		return false;
	if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
		return false;
	if (snprintf(uevent, sizeof(uevent), "%s/uevent", path) >= (int)sizeof(uevent))
		return false;
	return access(uevent, F_OK) == 0;
}

/*
 * Scan one sysfs listing (e.g. /sys/class/net or /sys/bus/block/devices).
 * Returns 0 if the directory was opened; negative errno if opendir failed.
 */
static int trigger_scan_dir(const char *dirpath, const char *name_match,
							const char *name_prefix, const char *action,
							bool dry_run, int *count, int *last_err)
{
	DIR *d;
	struct dirent *ent;

	d = opendir(dirpath);
	if (!d)
		return -errno;

	while ((ent = readdir(d)) != NULL) {
		char syspath[PATH_MAX];
		int trc;

		if (ent->d_name[0] == '.')
			continue;
		if (!trigger_name_matches(ent->d_name, name_match, name_prefix))
			continue;
		if (!trigger_is_class_or_bus_entry(dirpath, ent->d_name))
			continue;
		if (snprintf(syspath, sizeof(syspath), "%s/%s", dirpath,
					 ent->d_name) >= (int)sizeof(syspath)) {
			if (*last_err == 0)
				*last_err = -ENAMETOOLONG;
			continue;
		}
		trc = trigger_write_uevent(syspath, action, dry_run);
		if (trc == 0) {
			(*count)++;
		} else if (*last_err == 0) {
			*last_err = trc;
		}
	}

	closedir(d);
	return 0;
}

static int trigger_scan_subsystem(struct vdevadm_ctx *ctx, const char *subsystem,
								  const char *name_match,
								  const char *name_prefix,
								  const char *action, bool dry_run)
{
	char dirpath[PATH_MAX];
	struct stat st;
	int count = 0;
	int last_err = 0;
	bool any_path = false;
	int rc;

	/* eudev udev_enumerate_scan_devices: class, bus, and subsystem layouts. */
	if (snprintf(dirpath, sizeof(dirpath), "%s/class/%s", ctx->sysfs,
				 subsystem) < (int)sizeof(dirpath) &&
		stat(dirpath, &st) == 0 && S_ISDIR(st.st_mode)) {
		any_path = true;
		rc = trigger_scan_dir(dirpath, name_match, name_prefix, action,
							  dry_run, &count, &last_err);
		if (rc < 0 && last_err == 0)
			last_err = rc;
	}

	if (snprintf(dirpath, sizeof(dirpath), "%s/bus/%s/devices", ctx->sysfs,
				 subsystem) < (int)sizeof(dirpath) &&
		stat(dirpath, &st) == 0 && S_ISDIR(st.st_mode)) {
		any_path = true;
		rc = trigger_scan_dir(dirpath, name_match, name_prefix, action,
							  dry_run, &count, &last_err);
		if (rc < 0 && last_err == 0)
			last_err = rc;
	}

	if (snprintf(dirpath, sizeof(dirpath), "%s/subsystem/%s/devices", ctx->sysfs,
				 subsystem) < (int)sizeof(dirpath) &&
		stat(dirpath, &st) == 0 && S_ISDIR(st.st_mode)) {
		any_path = true;
		rc = trigger_scan_dir(dirpath, name_match, name_prefix, action,
							  dry_run, &count, &last_err);
		if (rc < 0 && last_err == 0)
			last_err = rc;
	}

	if (!any_path)
		return -ENOENT;
	/* Directory existed but nothing matched / nothing writable. */
	if (count == 0)
		return last_err != 0 ? last_err : 0;
	return 0;
}

/*
 * Match vdevd coldplug discovery: only nodes with a readable uevent file.
 */
static bool trigger_uevent_readable(const char *uevent_path)
{
	struct stat st;
	char dummy;
	ssize_t nr;
	int fd;

	if (lstat(uevent_path, &st) != 0)
		return false;
	if (S_ISLNK(st.st_mode) && access(uevent_path, F_OK) != 0)
		return false;

	fd = open(uevent_path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
	if (fd < 0)
		return false;
	nr = read(fd, &dummy, 1);
	close(fd);
	return nr > 0;
}

/*
 * Depth-first walk of /sys/devices — same scope as vdevd terminal coldplug.
 */
static int trigger_scan_sysfs_dir(const char *dirpath, const char *action,
								  bool dry_run, int *count, int *last_err)
{
	DIR *d;
	struct dirent *ent;
	char child[PATH_MAX];
	char uevent[PATH_MAX];
	int trc;

	d = opendir(dirpath);
	if (!d)
		return 0;

	while ((ent = readdir(d)) != NULL) {
		if (ent->d_name[0] == '.')
			continue;
		if (!trigger_is_device_dir(dirpath, ent->d_name))
			continue;
		if (snprintf(child, sizeof(child), "%s/%s", dirpath,
					 ent->d_name) >= (int)sizeof(child))
			continue;

		if (snprintf(uevent, sizeof(uevent), "%s/uevent", child) <
				(int)sizeof(uevent) &&
			trigger_uevent_readable(uevent)) {
			trc = trigger_write_uevent(child, action, dry_run);
			if (trc == 0) {
				(*count)++;
			} else if (*last_err == 0) {
				*last_err = trc;
			}
		}

		trigger_scan_sysfs_dir(child, action, dry_run, count, last_err);
	}

	closedir(d);
	return 0;
}

static int trigger_scan_sysfs(struct vdevadm_ctx *ctx, const char *action,
							  bool dry_run)
{
	char root[PATH_MAX];
	struct stat st;
	int count = 0;
	int last_err = 0;

	if (snprintf(root, sizeof(root), "%s/devices", ctx->sysfs) >=
		(int)sizeof(root))
		return -ENAMETOOLONG;
	if (stat(root, &st) != 0 || !S_ISDIR(st.st_mode))
		return -ENOENT;

	trigger_scan_sysfs_dir(root, action, dry_run, &count, &last_err);
	if (count == 0)
		return last_err != 0 ? last_err : -ENOENT;
	return 0;
}

int vdevadm_trigger_main(struct vdevadm_ctx *ctx, int argc, char *argv[])
{
	static const struct option opts[] = {
		{ "action", required_argument, NULL, 'a' },
		{ "subsystem-match", required_argument, NULL, 's' },
		{ "path-match", required_argument, NULL, 'p' },
		{ "name-match", required_argument, NULL, 'n' },
		{ "name-prefix", required_argument, NULL, 'P' },
		{ "sysfs", no_argument, NULL, 'S' },
		{ "dry-run", no_argument, NULL, 'D' },
		{ "help", no_argument, NULL, 'h' },
		{}
	};
	const char *action = "add";
	const char *subsystem = NULL;
	const char *path_match = NULL;
	const char *name_match = NULL;
	const char *name_prefix = NULL;
	bool scan_sysfs = false;
	bool dry_run = false;
	int c;
	int rc = 0;

	while ((c = getopt_long(argc, argv, "a:s:p:n:P:Sh", opts, NULL)) >= 0) {
		switch (c) {
		case 'a':
			action = optarg;
			break;
		case 's':
			subsystem = optarg;
			break;
		case 'p':
			path_match = optarg;
			break;
		case 'n':
			name_match = optarg;
			break;
		case 'P':
			name_prefix = optarg;
			break;
		case 'S':
			scan_sysfs = true;
			break;
		case 'D':
			dry_run = true;
			break;
		case 'h':
			trigger_usage(argv[0]);
			return 0;
		default:
			trigger_usage(argv[0]);
			return 1;
		}
	}

	if (!trigger_action_valid(action)) {
		fprintf(stderr, "vdevadm trigger: invalid action '%s'\n", action);
		return 1;
	}

	if (scan_sysfs) {
		rc = trigger_scan_sysfs(ctx, action, dry_run);
		if (rc != 0) {
			fprintf(stderr, "vdevadm trigger: sysfs scan: %s\n",
					strerror(-rc));
			return 1;
		}
		return 0;
	}

	if (subsystem) {
		rc = trigger_scan_subsystem(ctx, subsystem, name_match, name_prefix,
									action, dry_run);
		if (rc != 0) {
			fprintf(stderr, "vdevadm trigger: scan %s: %s\n", subsystem,
					strerror(-rc));
			return 1;
		}
		return 0;
	}

	if (path_match) {
		char syspath[PATH_MAX];

		if (path_match[0] == '/') {
			if (strncmp(path_match, ctx->sysfs, strlen(ctx->sysfs)) == 0) {
				if (snprintf(syspath, sizeof(syspath), "%s", path_match) >=
					(int)sizeof(syspath))
					return 1;
			} else {
				if (snprintf(syspath, sizeof(syspath), "%s%s", ctx->sysfs,
							 path_match) >= (int)sizeof(syspath))
					return 1;
			}
		} else {
			if (snprintf(syspath, sizeof(syspath), "%s/%s", ctx->sysfs,
						 path_match) >= (int)sizeof(syspath))
				return 1;
		}
		rc = trigger_write_uevent(syspath, action, dry_run);
		if (rc != 0) {
			fprintf(stderr, "vdevadm trigger: %s: %s\n", syspath,
					strerror(-rc));
			return 1;
		}
		return 0;
	}

	if (optind < argc) {
		for (; optind < argc; optind++) {
			rc = trigger_one(ctx, argv[optind], action, dry_run);
			if (rc != 0) {
				fprintf(stderr, "vdevadm trigger: %s: %s\n", argv[optind],
						strerror(-rc));
				return 1;
			}
		}
		return 0;
	}

	fprintf(stderr,
			"vdevadm trigger: specify DEVPATH, --sysfs, or --subsystem-match\n");
	trigger_usage(argv[0]);
	return 1;
}
