/*
 * vdevadm info — query vdev udev-compat device database
 *
 * When the udev-compat DB entry is missing (typical in a minimal initrd
 * without zzz-udev-compat.act), fall back to synthesizing properties from
 * sysfs uevent — the same baseline udevadm gets from the kernel without a
 * populated /run/udev/data store.  live-boot relies on this for DEVTYPE=disk.
 */

#include "vdevadm.h"

#include "common.h"

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

static void info_usage(const char *prog)
{
	fprintf(stderr,
			"Usage: %s info OPTIONS DEVPATH|SYSPATH|DEVNAME|DEVICE-ID\n"
			"\n"
			"Query the vdev udev-compat database ($VDEV_UDEV_ROOT/data/*).\n"
			"If no database record exists, synthesize properties from sysfs\n"
			"uevent (eudev-compatible baseline: DEVTYPE, DEVNAME, …).\n"
			"\n"
			"Options:\n"
			"  -a --attribute-walk       Not implemented (use sysfs directly)\n"
			"  -q --query=TYPE           name|path|property|all (default: all)\n"
			"  -p --path=SYSPATH         Device path in /sys\n"
			"  -n --name=FILE            Device node in /dev\n"
			"  -e --export-db            Print all database records\n"
			"  -d --device-id=ID         Database key (b8:0, c13:64, +block:sda)\n"
			"  -x --show                 Same as --query=all\n"
			"      --ctx                  Print resolved paths and exit\n"
			"  -h --help                 Show help\n",
			prog);
}

static const char *info_basename_devname(const char *devname)
{
	const char *slash;

	if (!devname || !devname[0])
		return NULL;
	slash = strrchr(devname, '/');
	return slash ? slash + 1 : devname;
}

/* Strip optional "E:" / "E: " prefix used in udev DB dumps. */
static const char *info_prop_keyval(const char *line)
{
	if (strncmp(line, "E: ", 3) == 0)
		return line + 3;
	if (strncmp(line, "E:", 2) == 0)
		return line + 2;
	return line;
}

static int info_print_db_file(const char *dbpath, const char *query)
{
	FILE *fp;
	char line[4096];
	char devname[PATH_MAX];
	char devpath[PATH_MAX];
	int show_all = !query || strcmp(query, "all") == 0;
	int show_prop = show_all || (query && strcmp(query, "property") == 0);
	int want_name = query && strcmp(query, "name") == 0;
	int want_path = query && strcmp(query, "path") == 0;

	devname[0] = '\0';
	devpath[0] = '\0';

	fp = fopen(dbpath, "r");
	if (!fp) {
		fprintf(stderr, "vdevadm info: cannot open %s: %s\n", dbpath,
				strerror(errno));
		return 1;
	}

	if (show_all)
		printf("P: %s\n", dbpath);

	while (fgets(line, sizeof(line), fp)) {
		size_t len = strlen(line);
		const char *kv;
		const char *name_val = NULL;

		while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
			line[--len] = '\0';
		if (line[0] == '\0')
			continue;

		if (strncmp(line, "N:", 2) == 0) {
			name_val = line + 2;
			while (*name_val == ' ')
				name_val++;
			if (name_val[0] && !devname[0])
				snprintf(devname, sizeof(devname), "%s", name_val);
			if (show_all)
				printf("%s\n", line);
			continue;
		}

		kv = info_prop_keyval(line);
		if (strncmp(kv, "DEVNAME=", 8) == 0) {
			snprintf(devname, sizeof(devname), "%s", kv + 8);
		} else if (strncmp(kv, "DEVPATH=", 8) == 0) {
			snprintf(devpath, sizeof(devpath), "%s", kv + 8);
		}

		if (want_name || want_path)
			continue;

		if (!show_prop)
			continue;

		if (show_all || line[0] == 'E' || line[0] == 'G' || line[0] == 'S' ||
			line[0] == 'N' || line[0] == 'L' || line[0] == 'Q' ||
			line[0] == 'W' || line[0] == 'I') {
			if (query && strcmp(query, "property") == 0 &&
				(strncmp(line, "E:", 2) == 0))
				printf("%s\n", kv);
			else
				printf("%s\n", line);
		}
	}

	fclose(fp);

	if (want_name) {
		const char *base = info_basename_devname(devname);

		if (!base || !base[0]) {
			fprintf(stderr,
					"vdevadm info: no DEVNAME in database — device may not be initialized\n");
			return 1;
		}
		printf("%s\n", base);
		return 0;
	}

	if (want_path) {
		if (!devpath[0]) {
			fprintf(stderr, "vdevadm info: no DEVPATH in database\n");
			return 1;
		}
		printf("%s\n", devpath);
		return 0;
	}

	return 0;
}

static int info_export_db(struct vdevadm_ctx *ctx)
{
	char datadir[4096];
	DIR *d;
	struct dirent *ent;
	int rc = 0;

	if (snprintf(datadir, sizeof(datadir), "%s/data", ctx->udev_root) >=
		(int)sizeof(datadir))
		return 1;

	d = opendir(datadir);
	if (!d) {
		fprintf(stderr, "vdevadm info: cannot open %s: %s\n", datadir,
				strerror(errno));
		return 1;
	}

	while ((ent = readdir(d)) != NULL) {
		char dbpath[4096];

		if (ent->d_name[0] == '.')
			continue;
		if (snprintf(dbpath, sizeof(dbpath), "%s/%s", datadir, ent->d_name) >=
			(int)sizeof(dbpath)) {
			rc = 1;
			continue;
		}
		if (info_print_db_file(dbpath, "all") != 0)
			rc = 1;
	}

	closedir(d);
	return rc;
}

static int info_path_has_uevent(const char *syspath)
{
	char uevent[PATH_MAX];

	if (snprintf(uevent, sizeof(uevent), "%s/uevent", syspath) >=
		(int)sizeof(uevent))
		return 0;
	return access(uevent, R_OK) == 0;
}

/*
 * Resolve DEVPATH|SYSPATH|DEVNAME|DEVICE-ID to an absolute sysfs directory
 * that contains a readable uevent file.
 */
static int info_resolve_syspath(struct vdevadm_ctx *ctx, const char *path,
								char *syspath, size_t syspath_len)
{
	struct stat st;
	char candidate[PATH_MAX];
	char *resolved = NULL;
	char *sp = NULL;
	size_t splen = 0;
	int rc;

	if (!path || !path[0])
		return -EINVAL;

	/* Absolute sysfs path */
	if (strncmp(path, ctx->sysfs, strlen(ctx->sysfs)) == 0 &&
		(path[strlen(ctx->sysfs)] == '/' || path[strlen(ctx->sysfs)] == '\0')) {
		if (snprintf(candidate, sizeof(candidate), "%s", path) >=
			(int)sizeof(candidate))
			return -ENAMETOOLONG;
		goto check;
	}

	/* Kernel DEVPATH-style (/devices/…, /block/sda, /class/…, /bus/…) */
	if (path[0] == '/' && strncmp(path, "/dev/", 5) != 0 &&
		(strncmp(path, "/devices/", 9) == 0 || strncmp(path, "/block/", 7) == 0 ||
		 strncmp(path, "/class/", 7) == 0 || strncmp(path, "/bus/", 5) == 0)) {
		if (snprintf(candidate, sizeof(candidate), "%s%s", ctx->sysfs, path) >=
			(int)sizeof(candidate))
			return -ENAMETOOLONG;
		goto check;
	}

	/* Database keys: b8:0 / c1:3 / +block:sda */
	if ((path[0] == 'b' || path[0] == 'c') && strchr(path, ':')) {
		if (snprintf(candidate, sizeof(candidate), "%s/dev/%s/%s", ctx->sysfs,
					 path[0] == 'b' ? "block" : "char", path + 1) >=
			(int)sizeof(candidate))
			return -ENAMETOOLONG;
		resolved = realpath(candidate, NULL);
		if (!resolved)
			return -errno;
		if (snprintf(syspath, syspath_len, "%s", resolved) >= (int)syspath_len) {
			free(resolved);
			return -ENAMETOOLONG;
		}
		free(resolved);
		return info_path_has_uevent(syspath) ? 0 : -ENOENT;
	}

	if (path[0] == '+' && strchr(path, ':')) {
		const char *colon = strchr(path, ':');
		char subsys[64];
		size_t slen = (size_t)(colon - (path + 1));

		if (slen == 0 || slen >= sizeof(subsys) || !colon[1])
			return -EINVAL;
		memcpy(subsys, path + 1, slen);
		subsys[slen] = '\0';
		if (snprintf(candidate, sizeof(candidate), "%s/class/%s/%s", ctx->sysfs,
					 subsys, colon + 1) >= (int)sizeof(candidate))
			return -ENAMETOOLONG;
		if (!info_path_has_uevent(candidate) && strcmp(subsys, "block") == 0) {
			if (snprintf(candidate, sizeof(candidate), "%s/block/%s", ctx->sysfs,
						 colon + 1) >= (int)sizeof(candidate))
				return -ENAMETOOLONG;
		}
		goto check;
	}

	/* Device node under /dev or VDEV_MOUNTPOINT */
	if (stat(path, &st) == 0 && (S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode))) {
		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;
		}
		/* Helper returns an absolute path including the sysfs mountpoint. */
		if (snprintf(syspath, syspath_len, "%s", sp) >= (int)syspath_len) {
			free(sp);
			return -ENAMETOOLONG;
		}
		free(sp);
		return info_path_has_uevent(syspath) ? 0 : -ENOENT;
	}

	if (strncmp(path, "/dev/", 5) == 0) {
		char full[PATH_MAX];

		if (snprintf(full, sizeof(full), "%s/%s", ctx->mountpoint, path + 5) >=
			(int)sizeof(full))
			return -ENAMETOOLONG;
		if (stat(full, &st) == 0 &&
			(S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode))) {
			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, syspath_len, "%s", sp) >= (int)syspath_len) {
				free(sp);
				return -ENAMETOOLONG;
			}
			free(sp);
			return info_path_has_uevent(syspath) ? 0 : -ENOENT;
		}
	}

	return -ENOENT;

check:
	resolved = realpath(candidate, NULL);
	if (resolved) {
		if (snprintf(syspath, syspath_len, "%s", resolved) >= (int)syspath_len) {
			free(resolved);
			return -ENAMETOOLONG;
		}
		free(resolved);
	} else {
		if (snprintf(syspath, syspath_len, "%s", candidate) >= (int)syspath_len)
			return -ENAMETOOLONG;
	}
	return info_path_has_uevent(syspath) ? 0 : -ENOENT;
}

static int info_read_subsystem(const char *syspath, char *buf, size_t buflen)
{
	char linkpath[PATH_MAX];
	char target[PATH_MAX];
	ssize_t n;
	const char *base;

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

	n = readlink(linkpath, target, sizeof(target) - 1);
	if (n < 0)
		return -errno;
	target[n] = '\0';
	base = strrchr(target, '/');
	base = base ? base + 1 : target;
	{
		size_t blen = strlen(base);

		if (blen >= buflen)
			return -ENAMETOOLONG;
		memcpy(buf, base, blen + 1);
	}
	return 0;
}

static int info_print_from_sysfs(struct vdevadm_ctx *ctx, const char *syspath,
								 const char *query)
{
	char uevent_path[PATH_MAX];
	char subsystem[NAME_MAX + 1];
	char line[4096];
	char devname[256];
	char devpath[PATH_MAX];
	FILE *fp;
	int show_all = !query || strcmp(query, "all") == 0;
	int show_prop = show_all || (query && strcmp(query, "property") == 0);
	int want_name = query && strcmp(query, "name") == 0;
	int want_path = query && strcmp(query, "path") == 0;
	size_t sysfs_len = strlen(ctx->sysfs);

	devname[0] = '\0';
	devpath[0] = '\0';
	subsystem[0] = '\0';

	if (strncmp(syspath, ctx->sysfs, sysfs_len) == 0 &&
		syspath[sysfs_len] == '/')
		snprintf(devpath, sizeof(devpath), "%s", syspath + sysfs_len);
	else
		snprintf(devpath, sizeof(devpath), "%s", syspath);

	(void)info_read_subsystem(syspath, subsystem, sizeof(subsystem));

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

	fp = fopen(uevent_path, "r");
	if (!fp) {
		fprintf(stderr, "vdevadm info: cannot open %s: %s\n", uevent_path,
				strerror(errno));
		return 1;
	}

	if (show_all)
		printf("P: %s\n", devpath[0] ? devpath : syspath);

	while (fgets(line, sizeof(line), fp)) {
		size_t len = strlen(line);
		char key[256];
		char *eq;

		while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
			line[--len] = '\0';
		if (line[0] == '\0')
			continue;

		eq = strchr(line, '=');
		if (!eq)
			continue;
		if ((size_t)(eq - line) >= sizeof(key))
			continue;
		memcpy(key, line, (size_t)(eq - line));
		key[eq - line] = '\0';

		if (strcmp(key, "DEVNAME") == 0)
			snprintf(devname, sizeof(devname), "%s", eq + 1);
		else if (strcmp(key, "DEVPATH") == 0)
			snprintf(devpath, sizeof(devpath), "%s", eq + 1);

		if (want_name || want_path)
			continue;

		if (show_all)
			printf("E:%s\n", line);
		else if (show_prop)
			printf("%s\n", line);
	}
	fclose(fp);

	if (!want_name && !want_path && subsystem[0]) {
		if (show_all)
			printf("E:SUBSYSTEM=%s\n", subsystem);
		else if (show_prop)
			printf("SUBSYSTEM=%s\n", subsystem);
	}

	if (show_all && devname[0]) {
		const char *base = info_basename_devname(devname);

		if (base && base[0])
			printf("N: %s\n", base);
	}

	if (want_name) {
		const char *base = info_basename_devname(devname);

		if ((!base || !base[0]) && syspath[0]) {
			base = strrchr(syspath, '/');
			base = base ? base + 1 : syspath;
		}
		if (!base || !base[0]) {
			fprintf(stderr, "vdevadm info: cannot determine device name\n");
			return 1;
		}
		printf("%s\n", base);
		return 0;
	}

	if (want_path) {
		if (!devpath[0]) {
			fprintf(stderr, "vdevadm info: cannot determine device path\n");
			return 1;
		}
		printf("%s\n", devpath);
		return 0;
	}

	return 0;
}

int vdevadm_info_main(struct vdevadm_ctx *ctx, int argc, char *argv[])
{
	static const struct option opts[] = {
		{ "attribute-walk", no_argument, NULL, 'a' },
		{ "query", required_argument, NULL, 'q' },
		{ "path", required_argument, NULL, 'p' },
		{ "name", required_argument, NULL, 'n' },
		{ "export-db", no_argument, NULL, 'e' },
		{ "device-id", required_argument, NULL, 'd' },
		{ "show", no_argument, NULL, 'x' },
		{ "ctx", no_argument, NULL, 'C' },
		{ "help", no_argument, NULL, 'h' },
		{}
	};
	const char *query = "all";
	const char *devsel = NULL;
	char device_id[256];
	char dbpath[4096];
	char syspath[PATH_MAX];
	int c;
	int have_db;
	int sys_rc;

	while ((c = getopt_long(argc, argv, "aq:p:n:ed:xh", opts, NULL)) >= 0) {
		switch (c) {
		case 'a':
			fprintf(stderr,
					"vdevadm info: attribute-walk not implemented; read %s directly\n",
					ctx->sysfs);
			return 1;
		case 'q':
			query = optarg;
			break;
		case 'p':
		case 'n':
		case 'd':
			devsel = optarg;
			break;
		case 'e':
			return info_export_db(ctx);
		case 'x':
			query = "all";
			break;
		case 'C':
			vdevadm_ctx_dump(ctx);
			return 0;
		case 'h':
			info_usage(argv[0]);
			return 0;
		default:
			info_usage(argv[0]);
			return 1;
		}
	}

	if (!devsel && optind < argc)
		devsel = argv[optind];

	if (!devsel) {
		fprintf(stderr, "vdevadm info: missing device argument\n");
		info_usage(argv[0]);
		return 1;
	}

	device_id[0] = '\0';
	if (vdevadm_device_id_from_path(ctx, devsel, device_id,
									sizeof(device_id)) == 0) {
		if (vdevadm_device_db_path(ctx, device_id, dbpath, sizeof(dbpath)) != 0) {
			fprintf(stderr, "vdevadm info: path too long\n");
			return 1;
		}
		have_db = access(dbpath, R_OK) == 0;
	} else {
		have_db = 0;
		dbpath[0] = '\0';
	}

	if (have_db) {
		if (device_id[0] && (!query || strcmp(query, "all") == 0))
			printf("E:VDEVADM_DEVICE_ID=%s\n", device_id);
		return info_print_db_file(dbpath, query);
	}

	sys_rc = info_resolve_syspath(ctx, devsel, syspath, sizeof(syspath));
	if (sys_rc != 0) {
		fprintf(stderr,
				"vdevadm info: no udev-compat record for '%s' and cannot read sysfs uevent (%s)\n",
				devsel, strerror(sys_rc < 0 ? -sys_rc : ENOENT));
		return 1;
	}

	if (device_id[0] && (!query || strcmp(query, "all") == 0))
		printf("E:VDEVADM_DEVICE_ID=%s\n", device_id);

	return info_print_from_sysfs(ctx, syspath, query);
}
