// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (c) 2026 Micron Technology, Inc.
 *
 * Command/option metadata dump for nvme-cli.
 *
 * Builds an in-memory model of every command and its options, then writes it to
 * stdout as JSON for the `dump-command-metadata` subcommand. The JSON is a
 * machine-readable description of the CLI surface, intended for tooling such as
 * shell-completion generators, documentation, and drift checks.
 *
 * The model is captured by walking the live plugin/command tree and, for each
 * command, intercepting the options array it builds on its stack via NVME_ARGS.
 * Capture installs a hook in argconfig_parse() (see argconfig_set_parse_hook):
 * when a command calls into the parser, the hook copies the options array into
 * the model and returns a sentinel so the command unwinds before opening any
 * device.
 */

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#ifdef _WIN32
#include <windows.h>
#endif

#include <shared/fs-util.h>

#include "command-metadata.h"
#include "nvme-json.h"
#include "plugin.h"

/*
 * The whole command is JSON-only, so it is compiled out entirely without
 * json-c support: the utils plugin does not register it and does not define
 * its handler, so dump_command_metadata() is never referenced.
 */
#ifdef CONFIG_JSONC

/*
 * Returned by the capture hook so argconfig_parse() unwinds before the
 * command's fn opens a device.
 */
#define METADATA_CAPTURE_SENTINEL (-ECANCELED)

/*
 * Version of the emitted JSON schema, bumped on any breaking change to the
 * output structure (renamed/removed keys, changed value semantics). Additive
 * changes that keep existing keys stable do not require a bump. Consumers
 * should reject a major version they do not understand.
 */
#define COMMAND_METADATA_SCHEMA_VERSION 1

/*
 * The command currently being captured; set by capture_command()
 * before it invokes the command fn, read by metadata_capture_hook().
 */
static struct command_metadata_command *command_metadata_cur_command;

/*
 * Capture OOM, reported out-of-band because the hook's return value is reserved
 * for the parser-unwind sentinel; checked after each command fn returns.
 */
static int command_metadata_capture_error;

/*
 * capture_saved_stderr_fd holds a dup of stderr from before the NUL redirect,
 * so crash messages still reach the user.
 */
static const char *capture_current_command;
static int capture_saved_stderr_fd = -1;

/*
 * (void)write() does not suppress GCC's warn_unused_result under
 * _FORTIFY_SOURCE; assigning to a discarded variable does.
 */
static void write_raw(int fd, const char *buf, size_t len)
{
	ssize_t ret = write(fd, buf, len);
	(void)ret;
}

/* strlen() is not async-signal-safe; measure inline for the crash handler. */
static void write_str(int fd, const char *s)
{
	size_t len = 0;

	if (!s)
		return;

	while (s[len])
		len++;

	write_raw(fd, s, len);
}

static void write_uint(int fd, unsigned int n)
{
	char buf[10];	/* enough for any 32-bit unsigned int */
	int i = sizeof(buf);

	do {
		buf[--i] = '0' + (n % 10);
		n /= 10;
	} while (n && i);

	write_raw(fd, buf + i, sizeof(buf) - i);
}

static void capture_crash_handler(int sig)
{
	int fd = capture_saved_stderr_fd >= 0 ? capture_saved_stderr_fd : STDERR_FILENO;

	write_str(fd, "dump-command-metadata: fatal: '");
	write_str(fd, capture_current_command ? capture_current_command : "(unknown)");
	write_str(fd, "' crashed during option capture (signal ");
	write_uint(fd, sig);
	write_str(fd, ")\n");

	signal(sig, SIG_DFL);
	raise(sig);
}

/*
 * On Windows, OS-level exceptions (stack overflow, access violations) bypass
 * the C signal() mechanism entirely and terminate the process silently.
 * SetUnhandledExceptionFilter intercepts these so we can print the same
 * diagnostic before dying.
 */
#ifdef _WIN32
static void write_hex(int fd, unsigned int n)
{
	char buf[8];
	int i = sizeof(buf);

	do {
		buf[--i] = "0123456789abcdef"[n & 0xf];
		n >>= 4;
	} while (n && i);

	write_raw(fd, buf + i, sizeof(buf) - i);
}

static LONG WINAPI capture_exception_filter(EXCEPTION_POINTERS *ep)
{
	int fd = capture_saved_stderr_fd >= 0 ? capture_saved_stderr_fd : STDERR_FILENO;

	write_str(fd, "dump-command-metadata: fatal: '");
	write_str(fd, capture_current_command ? capture_current_command : "(unknown)");
	write_str(fd, "' crashed during option capture (exception 0x");
	write_hex(fd, ep->ExceptionRecord->ExceptionCode);
	write_str(fd, ")\n");

	return EXCEPTION_CONTINUE_SEARCH;
}

static LPTOP_LEVEL_EXCEPTION_FILTER capture_prev_filter;
#endif

static void (*capture_prev_sigsegv)(int);
static void (*capture_prev_sigabrt)(int);
#ifdef SIGBUS
static void (*capture_prev_sigbus)(int);
#endif

/* Normalize SIG_ERR to SIG_DFL so restoration is always a valid disposition. */
static void (*set_crash_handler(int sig))(int)
{
	void (*prev)(int) = signal(sig, capture_crash_handler);

	return prev == SIG_ERR ? SIG_DFL : prev;
}

static void install_crash_handlers(void)
{
	capture_prev_sigsegv = set_crash_handler(SIGSEGV);
	capture_prev_sigabrt = set_crash_handler(SIGABRT);
#ifdef SIGBUS
	capture_prev_sigbus = set_crash_handler(SIGBUS);
#endif
#ifdef _WIN32
	capture_prev_filter = SetUnhandledExceptionFilter(capture_exception_filter);
#endif
}

static void remove_crash_handlers(void)
{
	signal(SIGSEGV, capture_prev_sigsegv);
	signal(SIGABRT, capture_prev_sigabrt);
#ifdef SIGBUS
	signal(SIGBUS, capture_prev_sigbus);
#endif
#ifdef _WIN32
	SetUnhandledExceptionFilter(capture_prev_filter);
	capture_prev_filter = NULL;
#endif
}

/* ------------------------------------------------------------------ */
/* Pass 1: capture                                                    */
/* ------------------------------------------------------------------ */

static char *xstrdup(const char *s)
{
	return s ? strdup(s) : NULL;
}

/*
 * Deep-copy an opt_val table into *out. A NULL src is not an error: *out is set
 * to NULL, the "no value table" sentinel consumers expect. Returns -ENOMEM on
 * allocation failure so the caller can abort rather than silently drop values.
 */
static int copy_opt_val(const struct argconfig_opt_val *src, const struct argconfig_opt_val **out)
{
	struct argconfig_opt_val *dst;
	int n = 0, i;

	*out = NULL;
	if (!src)
		return 0;

	for (; src[n].str; n++)
		;

	dst = calloc(n + 1, sizeof(*dst));
	if (!dst)
		return -ENOMEM;

	for (i = 0; i < n; i++) {
		dst[i] = src[i];
		/* src[i].str is non-NULL for i < n, so NULL here means OOM. */
		dst[i].str = strdup(src[i].str);
		if (!dst[i].str) {
			while (--i >= 0)
				free((char *)dst[i].str);
			free(dst);
			return -ENOMEM;
		}
	}
	dst[n].str = NULL;

	*out = dst;
	return 0;
}

/*
 * Duplicate a possibly-NULL string: NULL src succeeds (copies to NULL); a
 * non-NULL src that fails to duplicate returns -ENOMEM.
 */
static int dup_field(const char *src, const char **dst)
{
	*dst = xstrdup(src);
	if (src && !*dst)
		return -ENOMEM;
	return 0;
}

/*
 * Deep-copy an options array into *out (a heap array of *n_out entries).
 * Returns -ENOMEM on failure; the partial allocation is left for process exit
 * to reclaim, as the model is never explicitly freed.
 */
static int copy_options(const struct argconfig_commandline_options *opts,
			struct command_metadata_option **out, size_t *n_out)
{
	const struct argconfig_commandline_options *s;
	struct command_metadata_option *dst;
	size_t n = 0, i;

	*out = NULL;
	*n_out = 0;

	for (s = opts; s->option; s++)
		n++;

	if (!n)	/* calloc(0) may return NULL, indistinguishable from OOM */
		return 0;

	dst = calloc(n, sizeof(*dst));
	if (!dst)
		return -ENOMEM;

	/*
	 * Deep-copy: option/meta/help and the opt_val table are valid while the
	 * parser runs but may point at command-local storage that is freed once
	 * the command's fn returns, so duplicate rather than alias them.
	 */
	for (i = 0; i < n; i++) {
		if (dup_field(opts[i].option, &dst[i].option) ||
		    dup_field(opts[i].meta, &dst[i].meta) ||
		    dup_field(opts[i].help, &dst[i].help) ||
		    copy_opt_val(opts[i].opt_val, &dst[i].opt_val))
			return -ENOMEM;
		dst[i].short_option = opts[i].short_option;
		dst[i].config_type = opts[i].config_type;
		dst[i].argument_type = opts[i].argument_type;
		dst[i].hidden = opts[i].hidden;
	}

	*out = dst;
	*n_out = n;
	return 0;
}

/*
 * argconfig_parse() hook installed by build_model(): copies the current
 * command's options into the model, then returns the sentinel so the parser
 * unwinds before the command opens a device.
 */
static int metadata_capture_hook(int argc, char **argv, const char *program_desc,
				 struct argconfig_commandline_options *options)
{
	(void)argc;
	(void)argv;
	(void)program_desc;

	if (command_metadata_cur_command && !command_metadata_cur_command->captured) {
		int err = copy_options(options,
				&command_metadata_cur_command->options,
				&command_metadata_cur_command->num_options);
		if (err)
			command_metadata_capture_error = err;
		command_metadata_cur_command->captured = true;
	}

	return METADATA_CAPTURE_SENTINEL;
}

/* Returns 0 on success, or -ENOMEM if the capture hook failed to allocate. */
static int capture_command(struct command_metadata_command *mc, struct command *cmd,
			   struct plugin *plugin)
{
	/*
	 * argv[1] is a placeholder device; the sentinel returns before it is
	 * ever opened, so it need not (and must not) name a real device.
	 */
	char *argv[] = { cmd->name, (char *)"metadata-dump-dummy-device", NULL };

	mc->name = cmd->name;
	mc->alias = cmd->alias;
	mc->help = cmd->help;
	mc->captured = false;

	/*
	 * Don't invoke the dump command itself: it would re-enter
	 * dump_command_metadata() and recurse forever. It has no
	 * completable options, so leave its options array empty.
	 */
	if (!strcmp(cmd->name, "dump-command-metadata"))
		return 0;

	command_metadata_capture_error = 0;
	command_metadata_cur_command = mc;
	capture_current_command = cmd->name;
	(void)cmd->fn(2, argv, cmd, plugin);
	capture_current_command = NULL;
	command_metadata_cur_command = NULL;

	/*
	 * If the hook never fired, the command returned before reaching the
	 * parser (e.g. gen-hostnqn) and has no completable options; its options
	 * array is simply left empty.
	 */
	return command_metadata_capture_error;
}

static size_t count_commands(struct command **commands)
{
	size_t n = 0;

	while (commands && commands[n])
		n++;

	return n;
}

static size_t count_plugins(struct plugin *p)
{
	size_t n = 0;

	for (; p; p = p->next)
		n++;

	return n;
}

static struct command_metadata_program *build_model(struct program *prog)
{
	struct command_metadata_program *model;
	struct plugin *plugin;
	int saved_stdout = -1, saved_stderr = -1, devnull;
	int err = 0;
	size_t pi;

	model = calloc(1, sizeof(*model));
	if (!model)
		return NULL;

	model->name = prog->name;
	model->version = prog->version;
	model->desc = prog->desc;
	model->num_plugins = count_plugins(prog->extensions);
	if (model->num_plugins) {
		model->plugins = calloc(model->num_plugins, sizeof(*model->plugins));
		if (!model->plugins) {
			free(model);
			return NULL;
		}
	}

	/*
	 * Suppress stdout/stderr while invoking command fns: some commands
	 * may print before or during option capture, which would corrupt the
	 * JSON output.
	 */
	fflush(stdout);
	fflush(stderr);
	devnull = open(shr_dev_null(), O_WRONLY);
	if (devnull >= 0) {
		saved_stdout = dup(STDOUT_FILENO);
		saved_stderr = dup(STDERR_FILENO);
		capture_saved_stderr_fd = saved_stderr;
		if (saved_stdout >= 0)
			dup2(devnull, STDOUT_FILENO);
		if (saved_stderr >= 0)
			dup2(devnull, STDERR_FILENO);
	}

	install_crash_handlers();
	argconfig_set_parse_hook(metadata_capture_hook);

	for (pi = 0, plugin = prog->extensions; plugin; plugin = plugin->next, pi++) {
		struct command_metadata_plugin *mp = &model->plugins[pi];
		size_t ci;

		mp->name = plugin->name;
		mp->desc = plugin->desc;
		mp->num_commands = count_commands(plugin->commands);
		if (!mp->num_commands)
			continue;
		mp->commands = calloc(mp->num_commands, sizeof(*mp->commands));
		if (!mp->commands) {
			mp->num_commands = 0;
			err = -ENOMEM;
			break;
		}

		for (ci = 0; ci < mp->num_commands; ci++) {
			err = capture_command(&mp->commands[ci],
					plugin->commands[ci], plugin);
			if (err)
				break;
		}
		if (err)
			break;
	}

	argconfig_set_parse_hook(NULL);
	remove_crash_handlers();
	capture_saved_stderr_fd = -1;

	fflush(stdout);
	fflush(stderr);
	if (saved_stdout >= 0) {
		dup2(saved_stdout, STDOUT_FILENO);
		shr_close(saved_stdout);
	}
	if (saved_stderr >= 0) {
		dup2(saved_stderr, STDERR_FILENO);
		shr_close(saved_stderr);
	}
	if (devnull >= 0)
		shr_close(devnull);

	/*
	 * An allocation failure while capturing would leave an incomplete model
	 * that looks complete in the emitted JSON; fail the dump instead. The
	 * partial model is left for process exit to reclaim (it is never freed
	 * on the success path either).
	 */
	if (err)
		return NULL;

	return model;
}

/* ------------------------------------------------------------------ */
/* Model helpers shared by emitters                                   */
/* ------------------------------------------------------------------ */

static bool opt_is_separator(const struct command_metadata_option *o)
{
	return o->config_type == CFG_GROUP_SEPARATOR;
}

static bool opt_is_global_separator(const struct command_metadata_option *o)
{
	return opt_is_separator(o) && o->help && !strcmp(o->help, "Global options");
}

/* "none" / "required" / "optional" — how the option consumes its argument. */
static const char *opt_argument(const struct command_metadata_option *o)
{
	switch (o->argument_type) {
	case optional_argument:
		return "optional";
	case no_argument:
		return "none";
	default:
		return "required";
	}
}

static bool opt_takes_value(const struct command_metadata_option *o)
{
	return o->argument_type != no_argument;
}

/*
 * True for an option that should be emitted: a real, named, non-separator
 * option. Hidden options are emitted too (tagged "hidden" in the output) so
 * the dump describes the full set of accepted options; consumers that only
 * want user-facing options (e.g. completion generators) filter on that tag.
 */
static bool opt_is_emittable(const struct command_metadata_option *o)
{
	return !opt_is_separator(o) && o->option && o->option[0];
}

/* ------------------------------------------------------------------ */
/* Pass 2: JSON                                                       */
/* ------------------------------------------------------------------ */

/*
 * The value set for an option, when the generator can derive it. Returns a
 * json array of strings, or NULL if the option has no known value set. The
 * caller owns the returned array.
 *
 * output-format is special-cased because its values are not represented via an
 * opt_val table; keep the hard-coded list below in sync with
 * validate_output_format() / DESC_OUTPUT_FORMAT. Since the whole command is
 * compiled out without json-c, "json" is always a valid value here. Every
 * other value set comes from the option's opt_val table, which is the set the
 * parser actually enforces; options whose value is unconstrained (e.g. any
 * OPT_UINT such as output-format-version) have no values array.
 */
static struct json_object *json_option_values(const struct command_metadata_option *o)
{
	const struct argconfig_opt_val *v;
	struct json_object *vals;

	if (!strcmp(o->option, "output-format")) {
		vals = json_create_array();
		json_array_add_value_string(vals, "normal");
		json_array_add_value_string(vals, "json");
		json_array_add_value_string(vals, "binary");
		json_array_add_value_string(vals, "tabular");
		return vals;
	}
	if (!o->opt_val)
		return NULL;

	vals = json_create_array();
	for (v = o->opt_val; v->str; v++)
		json_array_add_value_string(vals, v->str);
	return vals;
}

/* Build one option as a json object and add it to the given array. */
static void json_option(struct json_object *arr, const struct command_metadata_option *o,
			bool global)
{
	struct json_object *jo, *vals;
	char shortbuf[2] = { o->short_option, '\0' };

	if (!opt_is_emittable(o))
		return;

	jo = json_create_object();
	json_object_add_value_string(jo, "long", o->option);
	if (o->short_option)
		json_object_add_value_string(jo, "short", shortbuf);
	json_object_add_value_string(jo, "argument", opt_argument(o));
	if (o->meta && opt_takes_value(o))
		json_object_add_value_string(jo, "metavar", o->meta);
	if (o->help)
		json_object_add_value_string(jo, "description", o->help);
	if (global)
		json_object_add_value_bool(jo, "global", true);
	if (o->hidden)
		json_object_add_value_bool(jo, "hidden", true);

	vals = json_option_values(o);
	if (vals)
		json_object_add_value_array(jo, "values", vals);

	json_array_add_value_object(arr, jo);
}

/* Build one command as a json object: name, alias, description, and options. */
static struct json_object *json_command(const struct command_metadata_command *c)
{
	struct json_object *jc, *opts;
	bool global = false;
	size_t i;

	jc = json_create_object();
	json_object_add_value_string(jc, "name", c->name);
	if (c->alias)
		json_object_add_value_string(jc, "alias", c->alias);
	if (c->help)
		json_object_add_value_string(jc, "description", c->help);

	opts = json_create_array();
	for (i = 0; i < c->num_options; i++) {
		/*
		 * Options after the "Global options" separator are the shared
		 * NVME_ARGS globals; flag them so generators can group them.
		 */
		if (opt_is_global_separator(&c->options[i])) {
			global = true;
			continue;
		}
		json_option(opts, &c->options[i], global);
	}
	json_object_add_value_array(jc, "options", opts);

	return jc;
}

/* Build one named plugin as a json object: name, description, commands. */
static struct json_object *json_plugin(const struct command_metadata_plugin *p)
{
	struct json_object *jp, *cmds;
	size_t i;

	jp = json_create_object();
	assert(p->name);	/* builtin (NULL-name) is emitted inline by json_program() */
	json_object_add_value_string(jp, "name", p->name);
	if (p->desc)
		json_object_add_value_string(jp, "description", p->desc);

	cmds = json_create_array();
	for (i = 0; i < p->num_commands; i++)
		json_array_add_value_object(cmds, json_command(&p->commands[i]));
	json_object_add_value_array(jp, "commands", cmds);

	return jp;
}

static void json_program(const struct command_metadata_program *m)
{
	struct json_object *root, *builtin, *plugins;
	size_t i;

	root = json_create_object();
	json_object_add_value_int(root, "schema_version",
				  COMMAND_METADATA_SCHEMA_VERSION);
	json_object_add_value_string(root, "name", m->name);
	if (m->version)
		json_object_add_value_string(root, "version", m->version);
	if (m->desc)
		json_object_add_value_string(root, "description", m->desc);

	/*
	 * Builtin (top-level) commands live in their own array; named plugins
	 * go under "plugins" so generators can build the dispatch nesting.
	 */
	builtin = json_create_array();
	plugins = json_create_array();
	for (i = 0; i < m->num_plugins; i++) {
		const struct command_metadata_plugin *p = &m->plugins[i];
		size_t j;

		if (!p->name) {
			for (j = 0; j < p->num_commands; j++)
				json_array_add_value_object(builtin,
					json_command(&p->commands[j]));
		} else {
			json_array_add_value_object(plugins, json_plugin(p));
		}
	}
	json_object_add_value_array(root, "commands", builtin);
	json_object_add_value_array(root, "plugins", plugins);

	json_print_object(root, NULL);
	printf("\n");
	json_free_object(root);
}

/* ------------------------------------------------------------------ */
/* Entry point                                                        */
/* ------------------------------------------------------------------ */

int dump_command_metadata(struct program *prog)
{
	struct command_metadata_program *model;

	model = build_model(prog);
	if (!model)
		return -ENOMEM;

	json_program(model);

	free(model->plugins);
	free(model);
	return 0;
}

#endif /* CONFIG_JSONC */
