/*
 * bomtool/main.c
 * main() routine, printer functions
 *
 * SPDX-License-Identifier: pkgconf
 *
 * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
 *     pkgconf authors (see AUTHORS).
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * This software is provided 'as is' and without any warranty, express or
 * implied.  In no event shall the authors be liable for any damages arising
 * from the use of this software.
 */

#include <ctype.h>
#include <time.h>

#include "libpkgconf/config.h"
#include <libpkgconf/stdinc.h>
#include <libpkgconf/libpkgconf.h>
#include "getopt_long.h"

#define PKG_VERSION			(((uint64_t) 1) << 1)
#define PKG_ABOUT			(((uint64_t) 1) << 2)
#define PKG_HELP			(((uint64_t) 1) << 3)
#define PKG_OUTPUT			(((uint64_t) 1) << 4)
#define PKG_DEFINE_VARIABLE	(((uint64_t) 1) << 5)
#define PKG_CREATION_TIME	(((uint64_t) 1) << 6)

static const char *spdx_version = "SPDX-2.2";
static const char *bom_license = "CC0-1.0";
static const char *document_ref = "SPDXRef-DOCUMENT";
static const char *creation_time = NULL;

static pkgconf_client_t pkg_client;
static uint64_t want_flags;
static int maximum_traverse_depth = 2000;
static FILE *error_msgout = NULL;
static FILE *sbom_out = NULL;

#define OUTPUT_OR_RET(client, f, fmt, ...) \
	do { \
		if (!pkgconf_output_file_fmt((f), (fmt), ##__VA_ARGS__)) { \
			pkgconf_error((client), "bomtool: Could not output to file: %s", strerror(errno)); \
			return; \
		} \
	} while (0)

#define OUTPUT_OR_RET_FALSE(client, f, fmt, ...) \
	do { \
		if (!pkgconf_output_file_fmt((f), (fmt), ##__VA_ARGS__)) { \
			pkgconf_error((client), "bomtool: Could not output to file: %s", strerror(errno)); \
			return false; \
		} \
	} while (0)

static const char *
environ_lookup_handler(const pkgconf_client_t *client, const char *key)
{
	(void) client;

	return getenv(key);
}

static bool
error_handler(const char *msg, const pkgconf_client_t *client, void *data)
{
	(void) client;
	(void) data;
	OUTPUT_OR_RET_FALSE(client, error_msgout, "%s", msg);
	return true;
}

static const char *
sbom_spdx_identity(pkgconf_pkg_t *pkg)
{
	static char buf[PKGCONF_ITEM_SIZE];
	size_t i, o;

	/* Sanitize the package ID: only letters, numbers, dot (.) and dash (-)
	 * are allowed.
	 */
	for (i = 0, o = 0; i < strlen(pkg->id) && o < sizeof(buf); i++, o++) {
		char c = pkg->id[i];
		if (c == '-' || c == '.' || isalnum(c))
			buf[o] = c;
		else {
			snprintf(buf + o, sizeof(buf) - o, "C%02x", c);
			o += 2;
		}
	}
	snprintf(buf + o, sizeof(buf) - o, "C40%s", pkg->version);
	/*                                  ^^^ 0x40 is the at sign (@) */
	return buf;
}

static char *
sbom_name(pkgconf_pkg_t *world)
{
	pkgconf_buffer_t name = PKGCONF_BUFFER_INITIALIZER;
	pkgconf_node_t *node;

	pkgconf_buffer_append(&name, "SBOM-SPDX");

	PKGCONF_FOREACH_LIST_ENTRY(world->required.head, node)
	{
		pkgconf_dependency_t *dep = node->data;
		pkgconf_pkg_t *match = dep->match;

		if ((dep->flags & PKGCONF_PKG_DEPF_QUERY) != PKGCONF_PKG_DEPF_QUERY)
			continue;

		if (!dep->match)
			continue;

		pkgconf_buffer_append_fmt(&name, "-%s", sbom_spdx_identity(match));
	}

	return pkgconf_buffer_freeze(&name);
}

static bool
write_sbom_header(pkgconf_client_t *client, pkgconf_pkg_t *world)
{
	time_t t;
	struct tm *tm;
	char buf[21];

	OUTPUT_OR_RET_FALSE(client, sbom_out, "SPDXVersion: %s\n", spdx_version);
	OUTPUT_OR_RET_FALSE(client, sbom_out, "DataLicense: %s\n", bom_license);
	OUTPUT_OR_RET_FALSE(client, sbom_out, "SPDXID: %s\n", document_ref);

	char *docname = sbom_name(world);
	if (!docname)
	{
		pkgconf_error(client, "write_sbom_header: out of memory");
		return false;
	}

	if (!pkgconf_output_file_fmt(sbom_out, "DocumentName: %s\n", docname))
	{
		free(docname);
		return false;
	}

	free(docname);

	OUTPUT_OR_RET_FALSE(client, sbom_out, "DocumentNamespace: https://spdx.org/spdxdocs/bomtool\n");
	OUTPUT_OR_RET_FALSE(client, sbom_out, "Creator: Tool: bomtool\n");

	if (creation_time != NULL)
	{
		OUTPUT_OR_RET_FALSE(client, sbom_out, "Created: %s\n", creation_time);
	}
	else
	{
		const char *source_date_epoch = getenv("SOURCE_DATE_EPOCH");

		if (source_date_epoch != NULL && *source_date_epoch != '\0')
			t = (time_t) strtoll(source_date_epoch, NULL, 10);
		else
			t = time(NULL);

		tm = gmtime(&t);
		strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", tm);
		OUTPUT_OR_RET_FALSE(client, sbom_out, "Created: %s\n", buf);
	}

	OUTPUT_OR_RET_FALSE(client, sbom_out, "\n\n");

	return true;
}

static const char *
sbom_identity(pkgconf_pkg_t *pkg)
{
	static char buf[PKGCONF_ITEM_SIZE];

	snprintf(buf, sizeof buf, "%s@%s", pkg->id, pkg->version);

	return buf;
}

static bool
write_copyright_lines(pkgconf_client_t *client, const pkgconf_list_t *copyright_lines)
{
	const pkgconf_node_t *node;

	if (copyright_lines->head == NULL) {
		OUTPUT_OR_RET_FALSE(client, sbom_out, "PackageCopyrightText: NOASSERTION\n");
		return true;
	}

	OUTPUT_OR_RET_FALSE(client, sbom_out, "PackageCopyrightText: <text>");

	PKGCONF_FOREACH_LIST_ENTRY(copyright_lines->head, node)
	{
		const pkgconf_bufferset_t *set = node->data;
		OUTPUT_OR_RET_FALSE(client, sbom_out, "%s%s", pkgconf_buffer_str_or_empty(&set->buffer), node->prev != NULL ? "\n" : "");
	}

	OUTPUT_OR_RET_FALSE(client, sbom_out, "</text>\n");

	return true;
}

static void
write_sbom_package(pkgconf_client_t *client, pkgconf_pkg_t *pkg, void *unused, unsigned int iter_flags)
{
	(void) iter_flags;

	pkgconf_buffer_t license_buf = PKGCONF_BUFFER_INITIALIZER;
	(void) client;
	(void) unused;

	if (pkg->flags & PKGCONF_PKG_PROPF_VIRTUAL)
		return;

	OUTPUT_OR_RET(client, sbom_out, "##### Package: %s\n\n", sbom_identity(pkg));
	OUTPUT_OR_RET(client, sbom_out, "PackageName: %s\n", sbom_identity(pkg));
	OUTPUT_OR_RET(client, sbom_out, "SPDXID: SPDXRef-Package-%s\n", sbom_spdx_identity(pkg));
	OUTPUT_OR_RET(client, sbom_out, "PackageVersion: %s\n", pkg->version);

	/* NOASSERTION is not a valid value for PackageVerificationCode. It
	 * expect 40 lowercase hexadecimal digits.
	 */
#if 0
	OUTPUT_OR_RET(client, sbom_out, "PackageVerificationCode: NOASSERTION\n");
#endif

	/* XXX: What about projects? */
	if (pkg->maintainer != NULL)
		OUTPUT_OR_RET(client, sbom_out, "PackageSupplier: Person: %s\n", pkg->maintainer);

	if (pkg->url != NULL)
		OUTPUT_OR_RET(client, sbom_out, "PackageHomePage: %s\n", pkg->url);

	if (pkg->license.head != NULL)
	{
		if (!pkgconf_license_render(client, &pkg->license, &license_buf))
		{
			pkgconf_buffer_finalize(&license_buf);
			pkgconf_error(client, "bomtool: could not render package license");
			return;
		}

		bool ret = pkgconf_output_file_fmt(sbom_out, "PackageLicenseDeclared: %s\n", pkgconf_buffer_str_or_empty(&license_buf));
		int errno_save = errno;
		pkgconf_buffer_finalize(&license_buf);
		if (!ret)
		{
			pkgconf_error(client, "bomtool: could not output to file: %s", strerror(errno_save));
			return;
		}
	}
	else
		OUTPUT_OR_RET(client, sbom_out, "PackageLicenseDeclared: NOASSERTION\n");
	OUTPUT_OR_RET(client, sbom_out, "PackageLicenseConcluded: NOASSERTION\n");

	if (!write_copyright_lines(client, &pkg->copyright))
		return;

	if (pkg->description != NULL)
		OUTPUT_OR_RET(client, sbom_out, "PackageSummary: <text>%s</text>\n", pkg->description);

	if (pkg->source != NULL)
		OUTPUT_OR_RET(client, sbom_out, "PackageDownloadLocation: %s\n", pkg->source);
	else
		OUTPUT_OR_RET(client, sbom_out, "PackageDownloadLocation: NOASSERTION\n");

	OUTPUT_OR_RET(client, sbom_out, "\n\n");
}

static void
write_sbom_relationships(pkgconf_client_t *client, pkgconf_pkg_t *pkg, void *unused, unsigned int iter_flags)
{
	(void) iter_flags;

	(void) client;
	(void) unused;

	char baseref[PKGCONF_ITEM_SIZE];
	pkgconf_node_t *node;

	if (pkg->flags & PKGCONF_PKG_PROPF_VIRTUAL)
		return;

	snprintf(baseref, sizeof baseref, "SPDXRef-Package-%s", sbom_spdx_identity(pkg));

	PKGCONF_FOREACH_LIST_ENTRY(pkg->required.head, node)
	{
		pkgconf_dependency_t *dep = node->data;
		pkgconf_pkg_t *match = dep->match;

		if (!dep->match)
			continue;

		OUTPUT_OR_RET(client, sbom_out, "Relationship: %s DEPENDS_ON SPDXRef-Package-%s\n", baseref, sbom_spdx_identity(match));
		OUTPUT_OR_RET(client, sbom_out, "Relationship: SPDXRef-Package-%s DEPENDENCY_OF %s\n", sbom_spdx_identity(match), baseref);
	}

	PKGCONF_FOREACH_LIST_ENTRY(pkg->requires_private.head, node)
	{
		pkgconf_dependency_t *dep = node->data;
		pkgconf_pkg_t *match = dep->match;

		if (!dep->match)
			continue;

		OUTPUT_OR_RET(client, sbom_out, "Relationship: %s DEPENDS_ON SPDXRef-Package-%s\n", baseref, sbom_spdx_identity(match));
		OUTPUT_OR_RET(client, sbom_out, "Relationship: SPDXRef-Package-%s DEV_DEPENDENCY_OF %s\n", sbom_spdx_identity(match), baseref);
	}

	if (pkg->required.head != NULL || pkg->requires_private.head != NULL)
		OUTPUT_OR_RET(client, sbom_out, "\n\n");
}

static bool
generate_sbom_from_world(pkgconf_client_t *client, pkgconf_pkg_t *world)
{
	int eflag;
	pkgconf_node_t *node;

	if (!write_sbom_header(client, world))
		return false;

	eflag = pkgconf_pkg_traverse(client, world, write_sbom_package, NULL, maximum_traverse_depth, 0);
	if (eflag != PKGCONF_PKG_ERRF_OK)
		return false;

	eflag = pkgconf_pkg_traverse(client, world, write_sbom_relationships, NULL, maximum_traverse_depth, 0);
	if (eflag != PKGCONF_PKG_ERRF_OK)
		return false;

	PKGCONF_FOREACH_LIST_ENTRY(world->required.head, node)
	{
		pkgconf_dependency_t *dep = node->data;
		pkgconf_pkg_t *match = dep->match;

		if (!dep->match)
			continue;

		OUTPUT_OR_RET_FALSE(client, sbom_out, "Relationship: %s DESCRIBES SPDXRef-Package-%s\n", document_ref, sbom_spdx_identity(match));
	}

	return true;
}

static int
version(void)
{
	printf("bomtool %s\n", PACKAGE_VERSION);
	return EXIT_SUCCESS;
}

static int
about(void)
{
	printf("bomtool (%s %s)\n", PACKAGE_NAME, PACKAGE_VERSION);
	printf("Copyright (c) 2011-2026 pkgconf authors (see AUTHORS in documentation directory)\n\n");
	printf("Permission to use, copy, modify, and/or distribute this software for any\n");
	printf("purpose with or without fee is hereby granted, provided that the above\n");
	printf("copyright notice and this permission notice appear in all copies.\n\n");
	printf("This software is provided 'as is' and without any warranty, express or\n");
	printf("implied.  In no event shall the authors be liable for any damages arising\n");
	printf("from the use of this software.\n\n");
	printf("Report bugs at <%s>.\n", PACKAGE_BUGREPORT);
	return EXIT_SUCCESS;
}

static int
usage(void)
{
	printf("usage: bomtool [--flags] [modules]\n");

	printf("\nbasic options:\n\n");

	printf("  --help                            this message\n");
	printf("  --about                           print bomtool version and license to stdout\n");
	printf("  --version                         print bomtool version to stdout\n");
	printf("  --output FILE                     output SBOM text to FILE\n");
	printf("  --define-variable=varname=value   define variable 'varname' as 'value'\n");
	printf("  --creation-time                   Use string as creation time (Should be in ISO8601 format) [default: current time]\n");

	return EXIT_SUCCESS;
}

int
main(int argc, char *argv[])
{
	int ret = EXIT_SUCCESS;
	pkgconf_list_t pkgq = PKGCONF_LIST_INITIALIZER;
	unsigned int want_client_flags = PKGCONF_PKG_PKGF_SEARCH_PRIVATE;
	pkgconf_cross_personality_t *personality = pkgconf_cross_personality_default();
	pkgconf_pkg_t world = {
		.id = "virtual:world",
		.realname = "virtual world package",
		.flags = PKGCONF_PKG_PROPF_STATIC | PKGCONF_PKG_PROPF_VIRTUAL,
	};

	error_msgout = stderr;
	sbom_out = stdout;

	struct pkg_option options[] = {
		{ "version", no_argument, &want_flags, PKG_VERSION, },
		{ "about", no_argument, &want_flags, PKG_ABOUT, },
		{ "help", no_argument, &want_flags, PKG_HELP, },
		{ "output", required_argument, NULL, PKG_OUTPUT, },
		{ "define-variable", required_argument, NULL, PKG_DEFINE_VARIABLE, },
		{ "creation-time", required_argument, NULL, PKG_CREATION_TIME, },
		{ NULL, 0, NULL, 0 }
	};

	while ((ret = pkg_getopt_long_only(argc, argv, "", options, NULL)) != -1)
	{
		switch (ret)
		{
		case PKG_OUTPUT:
			sbom_out = fopen(pkg_optarg, "w");
			if (sbom_out == NULL)
			{
				pkgconf_output_file_fmt(stderr, "unable to open %s: %s\n", pkg_optarg, strerror(errno));
				return EXIT_FAILURE;
			}

			break;
		case PKG_DEFINE_VARIABLE:
			pkgconf_tuple_define_global(&pkg_client, pkg_optarg);
			break;
		case PKG_CREATION_TIME:
			creation_time = pkg_optarg;
			break;
		case '?':
		case ':':
			return EXIT_FAILURE;
		default:
			break;
		}
	}

	pkgconf_client_options_t client_options = {
		.error_handler = error_handler,
		.personality = personality,
		.environ_lookup_handler = environ_lookup_handler,
	};
	pkgconf_client_init_with_options(&pkg_client, &client_options);

	/* we have determined what features we want most likely.  in some cases, we override later. */
	pkgconf_client_set_flags(&pkg_client, want_client_flags);

	/* at this point, want_client_flags should be set, so build the dir list */
	pkgconf_client_dir_list_build(&pkg_client, personality);

	if ((want_flags & PKG_ABOUT) == PKG_ABOUT)
		return about();

	if ((want_flags & PKG_VERSION) == PKG_VERSION)
		return version();

	if ((want_flags & PKG_HELP) == PKG_HELP)
		return usage();

	/* Join the remaining arguments into a single query string, as the main
	 * pkgconf CLI does, and let the dependency parser handle module names,
	 * comparison operators and versions.
	 */
	pkgconf_buffer_t queryparams = PKGCONF_BUFFER_INITIALIZER;

	while (pkg_optind < argc && argv[pkg_optind] != NULL)
	{
		if ((pkgconf_buffer_len(&queryparams) > 0 &&
			 !pkgconf_buffer_push_byte(&queryparams, ' ')) ||
			!pkgconf_buffer_append(&queryparams, argv[pkg_optind]))
		{
			pkgconf_buffer_finalize(&queryparams);
			ret = EXIT_FAILURE;
			goto out;
		}

		pkg_optind++;
	}

	if (pkgconf_buffer_len(&queryparams) > 0)
		pkgconf_queue_push(&pkgq, pkgconf_buffer_str(&queryparams));

	pkgconf_buffer_finalize(&queryparams);

	if (pkgq.head == NULL)
	{
		pkgconf_output_file_fmt(stderr, "Please specify at least one package name on the command line.\n");
		ret = EXIT_FAILURE;
		goto out;
	}

	ret = EXIT_SUCCESS;

	if (!pkgconf_queue_solve(&pkg_client, &pkgq, &world, maximum_traverse_depth))
	{
		ret = EXIT_FAILURE;
		goto out;
	}

	if (!generate_sbom_from_world(&pkg_client, &world))
	{
		ret = EXIT_FAILURE;
		goto out;
	}

out:
	if (sbom_out != stdout)
		fclose(sbom_out);

	pkgconf_solution_free(&pkg_client, &world);
	pkgconf_queue_free(&pkgq);
	pkgconf_cross_personality_deinit(personality);
	pkgconf_client_deinit(&pkg_client);

	return ret;
}
