/*-
 * Copyright (c) 2011-2025 Baptiste Daroussin <bapt@FreeBSD.org>
 * Copyright (c) 2012-2013 Matthew Seaman <matthew@FreeBSD.org>
 * Copyright (c) 2024 The FreeBSD Foundation
 *
 * This software was developed in part by Isaac Freund <ifreund@freebsdfoundation.org>
 * under sponsorship from the FreeBSD Foundation.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */
#ifdef HAVE_CONFIG_H
#include "pkg_config.h"
#endif

#include <ctype.h>
#include <paths.h>
#include <string.h>
#include <unistd.h>

#include "pkg.h"
#include "private/pkg_abi.h"
#include "private/binfmt.h"
#include "private/event.h"
#include "private/pkg.h"
#include "private/utils.h"
#include "xmalloc.h"

#define _PATH_UNAME "/usr/bin/uname"

#ifndef nitems
#define	nitems(x)	(sizeof((x)) / sizeof((x)[0]))
#endif

/* All possibilities on FreeBSD as of 5/26/2014 */
static const struct {
	const char *elftype;
	const char *archid;
} machine_arch_translation[] = {
	{ "x86:32", "i386" },
	{ "x86:64", "amd64" },

	{ "powerpc:32:eb", "powerpc" },
	{ "powerpc:64:eb", "powerpc64" },
	{ "powerpc:64:el", "powerpc64le" },

	{ "sparc64:64", "sparc64" },

	{ "ia64:64", "ia64" },

	{ "armv6:32:el:eabi:hardfp", "armv6" },
	{ "armv7:32:el:eabi:hardfp", "armv7" },
	{ "aarch64:64", "aarch64" },
	{ "aarch64:64:cheri", "aarch64c" },
	{ "aarch64:64:cheri:benchmark", "aarch64cb" },

	{ "mips:32:el:o32", "mipsel" },
	{ "mips:32:el:n32", "mipsn32el" },
	{ "mips:32:eb:o32", "mips" },
	{ "mips:32:eb:n32", "mipsn32" },
	{ "mips:64:el:n64", "mips64el" },
	{ "mips:64:eb:n64", "mips64" },

	{ "riscv:32:hf", "riscv32" },
	{ "riscv:32:sf", "riscv32sf" },
	{ "riscv:64:hf", "riscv64" },
	{ "riscv:64:cheri:hf", "riscv64c" },
	{ "riscv:64:sf", "riscv64sf" },
};

static const struct {
	const char *string;
	const char *amd64_string;
	bool major_version_only;
} os_table[] = {
	[PKG_OS_UNKNOWN] =	{ "Unknown" },
	[PKG_OS_FREEBSD] =	{ "FreeBSD",	"amd64",	true },
	[PKG_OS_NETBSD] =	{ "NetBSD",	"x86_64",	true },
	[PKG_OS_DRAGONFLY] =	{ "dragonfly",	"x86:64",	false },
	[PKG_OS_LINUX] =	{ "Linux",	"x86_64",	false },
	[PKG_OS_DARWIN] =	{ "Darwin",	"x86_64",	true },
	[PKG_OS_CHERIBSD] =	{ "CheriBSD",	"amd64",	true },
	[PKG_OS_ANY] =		{ "*" },
};

/* This table does not include PKG_ARCH_AMD64 as the string translation of
   that arch is os-dependent. */
static const struct {
	const char *string;
} arch_string_table[] = {
	[PKG_ARCH_UNKNOWN] =		{"unknown"},
	[PKG_ARCH_I386] =		{"i386"},
	[PKG_ARCH_ARMV6] =		{"armv6"},
	[PKG_ARCH_ARMV7] =		{"armv7"},
	[PKG_ARCH_AARCH64] =		{"aarch64"},
	[PKG_ARCH_AARCH64C] =		{"aarch64c"},
	[PKG_ARCH_AARCH64CB] =		{"aarch64cb"},
	[PKG_ARCH_POWERPC] =		{"powerpc"},
	[PKG_ARCH_POWERPC64] =		{"powerpc64"},
	[PKG_ARCH_POWERPC64LE] =	{"powerpc64le"},
	[PKG_ARCH_RISCV32] =		{"riscv32"},
	[PKG_ARCH_RISCV64] =		{"riscv64"},
	[PKG_ARCH_RISCV64C] =		{"riscv64c"},
	[PKG_ARCH_ANY] =		{"*"},
};

const char *
pkg_os_to_string(enum pkg_os os)
{
	assert(os >= PKG_OS_UNKNOWN && os <= PKG_OS_ANY);

	return (os_table[os].string);
}

static enum pkg_os
pkg_os_from_string(const char *string)
{
	for (int i = 0; i < nitems(os_table); i++) {
		if (STREQ(string, os_table[i].string)) {
			return ((enum pkg_os)i);
		}
	}
	return (PKG_OS_UNKNOWN);
}

const char *
pkg_arch_to_string(enum pkg_os os, enum pkg_arch arch)
{
	if (arch == PKG_ARCH_AMD64) {
		if (os_table[os].amd64_string != NULL) {
			return (os_table[os].amd64_string);
		}
	}

	assert(arch >= PKG_ARCH_UNKNOWN && arch <= PKG_ARCH_ANY);
	return (arch_string_table[arch].string);
}

static enum pkg_arch
pkg_arch_from_string(enum pkg_os os, const char *string)
{
	if (os_table[os].amd64_string != NULL) {
		if (STREQ(string, os_table[os].amd64_string)) {
			return (PKG_ARCH_AMD64);
		}
	}

	for (int i = 0; i < nitems(arch_string_table); i++) {
		if (arch_string_table[i].string != NULL &&
		    STREQ(string, arch_string_table[i].string)) {
			return ((enum pkg_arch)i);
		}
	}

	return (PKG_ARCH_UNKNOWN);
}

bool
pkg_abi_string_only_major_version(enum pkg_os os)
{
	assert(os > PKG_OS_UNKNOWN && os < PKG_OS_ANY);

	return (os_table[os].major_version_only);
}

char *
pkg_abi_to_string(const struct pkg_abi *abi)
{
	char *ret;
	if (pkg_abi_string_only_major_version(abi->os)) {
		xasprintf(&ret, "%s:%d:%s", pkg_os_to_string(abi->os),
		    abi->major, pkg_arch_to_string(abi->os, abi->arch));
	} else {
		xasprintf(&ret, "%s:%d.%d:%s", pkg_os_to_string(abi->os),
		    abi->major, abi->minor,
		    pkg_arch_to_string(abi->os, abi->arch));
	}
	return (ret);
}

bool
pkg_abi_from_string(struct pkg_abi *abi, const char *string)
{
	*abi = (struct pkg_abi){0};

	bool ret = false;

	char *copy = xstrdup(string);

	char *iter = copy;
	char *os = strsep(&iter, ":");
	assert(os != NULL);
	abi->os = pkg_os_from_string(os);
	if (abi->os == PKG_OS_UNKNOWN) {
		pkg_emit_error("Unknown OS '%s' in ABI string", os);
		goto out;
	}

	char *version = strsep(&iter, ":");
	if (version == NULL) {
		if (abi->os == PKG_OS_ANY) {
			abi->major = 0;
			abi->minor = 0;
			abi->arch = PKG_ARCH_ANY;
			ret = true;
			goto out;
		}
		pkg_emit_error("Invalid ABI string '%s', "
		    "missing version and architecture", string);
		goto out;
	}
	const char *errstr = NULL;
	if (pkg_abi_string_only_major_version(abi->os)) {
		abi->major = strtonum(version, 1, INT_MAX, &errstr);
	} else {
		/* XXX add tests for this */
		char *major = strsep(&version, ".");
		char *minor = strsep(&version, ".");

		assert(major != NULL);
		if (minor == NULL) {
			pkg_emit_error("Invalid ABI string %s, "
			    "missing minor OS version", string);
			goto out;
		}

		abi->major = strtonum(major, 1, INT_MAX, &errstr);
		if (errstr != NULL) {
			abi->minor = strtonum(minor, 1, INT_MAX, &errstr);
		}
	}
	if (errstr != NULL) {
		if (STREQ(version, "*")) {
			abi->major = 0;
			abi->minor = 0;
			abi->arch = PKG_ARCH_ANY;
			ret = true;
			goto out;
		}
		pkg_emit_error("Invalid version in ABI string '%s'", string);
		goto out;
	}

	/* DragonFlyBSD continues to use the legacy/altabi format.
	   For example: dragonfly:5.10:x86:64
	   This means we can't use strsep again since that would split the arch
	   string for dragonfly. */
	char *arch = iter;
	if (arch == NULL) {
		pkg_emit_error("Invalid ABI string '%s', "
		    "missing architecture", string);
		goto out;
	}

	abi->arch = pkg_arch_from_string(abi->os, arch);
	if (abi->arch == PKG_ARCH_UNKNOWN) {
		pkg_emit_error("Unknown architecture '%s' in ABI string", arch);
		goto out;
	}

	if (abi->os == PKG_OS_DRAGONFLY && abi->arch != PKG_ARCH_AMD64) {
		pkg_emit_error("Invalid ABI string '%s', "
		    "only x86:64 is supported on dragonfly.", string);
		goto out;
	}

	ret = true;
out:
	free(copy);
	return (ret);
}

void
pkg_abi_set_freebsd_osversion(struct pkg_abi *abi, int osversion)
{
	assert(abi->os == PKG_OS_FREEBSD);

	abi->major = osversion / 100000;
	abi->minor = (osversion / 1000) % 100;
	abi->patch = osversion % 1000;
}

int
pkg_abi_get_freebsd_osversion(struct pkg_abi *abi)
{
	assert(abi->os == PKG_OS_FREEBSD);

	return (abi->major * 100000) + (abi->minor * 1000) + abi->patch;
}

int
pkg_abi_from_file(struct pkg_abi *abi)
{
	char rooted_abi_file[PATH_MAX];
	const char *abi_files[] = {
		getenv("ABI_FILE"),
		_PATH_UNAME,
		_PATH_BSHELL,
	};
	char work_abi_file[PATH_MAX];
	char work_arch_hint[PATH_MAX];

	size_t i;
	int fd;

	/*
	 * Perhaps not yet needed, but it may be in the future that there's no
	 * need to check root under some conditions where there is a rootdir.
	 * This also helps alleviate some excessive wrapping later.
	 */
	bool checkroot = ctx.pkg_rootdir != NULL;
	for (fd = -1, i = 0; i < NELEM(abi_files); i++) {
		if (abi_files[i] == NULL)
			continue;

		const char *sep = strrchr(abi_files[i], '#');
		if (sep) {
			strlcpy(work_abi_file, abi_files[i],
			    MIN(sep - abi_files[i] + 1, sizeof(work_abi_file)));
			strlcpy(work_arch_hint, sep + 1,
			    sizeof(work_arch_hint));
		} else {
			strlcpy(work_abi_file, abi_files[i],
			    sizeof(work_abi_file));
			work_arch_hint[0] = '\0';
		}

		/*
		 * Try prepending rootdir and using that if it exists.  If
		 * ABI_FILE is specified, assume that the consumer didn't want
		 * it mangled by rootdir.
		 */
		if (i > 0 && checkroot &&
		    snprintf(rooted_abi_file, PATH_MAX, "%s/%s",
			ctx.pkg_rootdir, work_abi_file) < PATH_MAX) {
			if ((fd = open(rooted_abi_file, O_RDONLY)) >= 0) {
				strlcpy(work_abi_file, rooted_abi_file,
				    sizeof(work_abi_file));
				break;
			}
		}
		if ((fd = open(work_abi_file, O_RDONLY)) >= 0) {
			break;
		}
		/* if the ABI_FILE was provided we only care about it */
		if (i == 0)
			break;
	}
	if (fd == -1) {
		pkg_emit_error(
		    "Unable to determine the ABI, none of the ABI_FILEs can be read.");
		return EPKG_FATAL;
	}


	int ret = pkg_elf_abi_from_fd(fd, abi);
	if (EPKG_OK != ret) {
		if (-1 == lseek(fd, 0, SEEK_SET)) {
			pkg_emit_errno("Error seeking file", work_abi_file);
			ret = EPKG_FATAL;
			goto close_out;
		}

		enum pkg_arch arch_hint = PKG_ARCH_UNKNOWN;
		if (work_arch_hint[0]) {
			arch_hint = pkg_arch_from_string(PKG_OS_DARWIN, work_arch_hint);
			if (arch_hint == PKG_ARCH_UNKNOWN) {
				pkg_emit_error("Invalid ABI_FILE architecture hint %s",
				    work_arch_hint);
				ret = EPKG_FATAL;
				goto close_out;
			}
		}

		ret = pkg_macho_abi_from_fd(fd, abi, arch_hint);
		if (EPKG_OK != ret) {
			pkg_emit_error(
			    "Unable to determine ABI, %s cannot be parsed.",
			    work_abi_file);
			ret = EPKG_FATAL;
			goto close_out;
		}
	}

close_out:
	if (close(fd)) {
		pkg_emit_errno("Error closing file", work_abi_file);
		ret = EPKG_FATAL;
	}
	return ret;
}

int
pkg_arch_to_legacy(const char *arch, char *dest, size_t sz)
{
	int i = 0;

	memset(dest, '\0', sz);
	/* Lower case the OS */
	while (arch[i] != ':' && arch[i] != '\0') {
		dest[i] = tolower(arch[i]);
		i++;
	}
	if (arch[i] == '\0')
		return (0);

	dest[i++] = ':';

	/* Copy the version */
	while (arch[i] != ':' && arch[i] != '\0') {
		dest[i] = arch[i];
		i++;
	}
	if (arch[i] == '\0')
		return (0);

	dest[i++] = ':';

	for (int j = 0; j < nitems(machine_arch_translation); j++) {
		if (STREQ(arch + i, machine_arch_translation[j].archid)) {
			strlcpy(dest + i, machine_arch_translation[j].elftype,
			    sz - (arch + i - dest));
			return (0);
		}
	}
	strlcpy(dest + i, arch + i, sz - (arch + i - dest));

	return (0);
}

void
pkg_cleanup_shlibs_required(struct pkg *pkg, charv_t *internal_provided)
{
	struct pkg_file *file = NULL;
	const char *lib;

	vec_foreach(pkg->shlibs_required, i) {
		char *s = pkg->shlibs_required.d[i];
		if (charv_search(&pkg->shlibs_provided, s) != NULL ||
		    charv_search(internal_provided, s) != NULL ||
		    charv_search(&pkg->shlibs_provided_ignore, s) != NULL) {
			pkg_debug(2,
			    "remove %s from required shlibs as the "
			    "package %s provides this library itself",
			    s, pkg->name);
			vec_remove_and_free(&pkg->shlibs_required, i, free);
			i--;
			continue;
		}
		if (charv_search(&pkg->shlibs_required_ignore, s) != NULL) {
			pkg_debug(2,
			    "remove %s from required shlibs for package %s as it "
			    "is listed in shlibs_required_ignore.",
			    s, pkg->name);
			vec_remove_and_free(&pkg->shlibs_required, i, free);
			i--;
			continue;
		}
		if (match_ucl_lists(s,
		    pkg_config_get("SHLIB_REQUIRE_IGNORE_GLOB"),
		    pkg_config_get("SHLIB_REQUIRE_IGNORE_REGEX"))) {
			pkg_debug(2,
			    "remove %s from required shlibs for package %s as it "
			    "is matched by SHLIB_REQUIRE_IGNORE_GLOB/REGEX.",
			    s, pkg->name);
			vec_remove(&pkg->shlibs_required, i);
			if (charv_insert_sorted(&pkg->shlibs_required_ignore, s) != NULL) {
				free(s);
			}
			i--;
			continue;
		}
		file = NULL;
		while (pkg_files(pkg, &file) == EPKG_OK) {
			if ((lib = strstr(file->path, s)) != NULL &&
			    strlen(lib) == strlen(s) && lib[-1] == '/') {
				pkg_debug(2,
				    "remove %s from required shlibs as "
				    "the package %s provides this file itself",
				    s, pkg->name);

				vec_remove_and_free(&pkg->shlibs_required, i,
				    free);
				i--;
				break;
			}
		}
	}
}

int
pkg_analyse_files(struct pkgdb *db __unused, struct pkg *pkg, const char *stage)
{
	struct pkg_file *file = NULL;
	int ret = EPKG_OK;
	char fpath[MAXPATHLEN + 1];
	bool failures = false;

	int (*pkg_analyse_init)(const char *stage);
	int (*pkg_analyse)(const bool developer_mode, struct pkg *pkg,
	    const char *fpath, char **provided, enum pkg_shlib_flags *flags);
	int (*pkg_analyse_close)(void);

	if (0 == strncmp(pkg->abi, "Darwin", 6)) {
		pkg_analyse_init=pkg_analyse_init_macho;
		pkg_analyse=pkg_analyse_macho;
		pkg_analyse_close=pkg_analyse_close_macho;
	} else {
		pkg_analyse_init=pkg_analyse_init_elf;
		pkg_analyse=pkg_analyse_elf;
		pkg_analyse_close=pkg_analyse_close_elf;
	}

	if (vec_len(&pkg->shlibs_required) != 0) {
		vec_free_and_free(&pkg->shlibs_required, free);
	}

	if (vec_len(&pkg->shlibs_provided) != 0) {
		vec_free_and_free(&pkg->shlibs_provided, free);
	}

	ret = pkg_analyse_init(stage);
	if (ret != EPKG_OK) {
		goto cleanup;
	}

	/* Assume no architecture dependence, for contradiction */
	if (ctx.developer_mode)
		pkg->flags &= ~(PKG_CONTAINS_ELF_OBJECTS |
		    PKG_CONTAINS_STATIC_LIBS | PKG_CONTAINS_LA);

	/* shlibs that are provided by files in the package but not matched by
	   SHLIB_PROVIDE_PATHS_* are still used to filter the shlibs
	   required by the package */
	charv_t internal_provided = vec_init();
	/* list of shlibs that are in the path to be evaluated for provided but are symlinks */
	charv_t maybe_provided = vec_init();

	while (pkg_files(pkg, &file) == EPKG_OK) {
		struct stat st;
		if (stage != NULL)
			snprintf(fpath, sizeof(fpath), "%s/%s", stage,
			    RELATIVE_PATH(file->path));
		else
			strlcpy(fpath, file->path, sizeof(fpath));

		char *provided = NULL;
		enum pkg_shlib_flags provided_flags = PKG_SHLIB_FLAGS_NONE;

		ret = pkg_analyse(ctx.developer_mode, pkg, fpath, &provided, &provided_flags);
		if (EPKG_WARN == ret) {
			failures = true;
		}

		if (provided != NULL) {
			const ucl_object_t *paths = NULL;

			switch (provided_flags) {
			case PKG_SHLIB_FLAGS_NONE:
				paths = pkg_config_get("SHLIB_PROVIDE_PATHS_NATIVE");
				break;
			case PKG_SHLIB_FLAGS_COMPAT_32:
				paths = pkg_config_get("SHLIB_PROVIDE_PATHS_COMPAT_32");
				break;
			case PKG_SHLIB_FLAGS_COMPAT_LINUX:
				paths = pkg_config_get("SHLIB_PROVIDE_PATHS_COMPAT_LINUX");
				break;
			case PKG_SHLIB_FLAGS_COMPAT_LINUX_32:
				paths = pkg_config_get("SHLIB_PROVIDE_PATHS_COMPAT_LINUX_32");
				break;
			default:
				assert(0);
			}
			assert(paths != NULL);

			if (lstat(fpath, &st) != 0) {
				pkg_emit_errno("lstat() failed for", fpath);
				free(provided);
				continue;
			}
			/* If the corresponding PATHS option isn't set (i.e. an empty ucl array)
			   don't do any filtering for backwards compatibility. */
			if (ucl_array_size(paths) == 0 || pkg_match_paths_list(paths, file->path)) {
				if (S_ISREG(st.st_mode)) {
					pkg_addshlib_provided(pkg, provided, provided_flags);
				} else {
					vec_push(&maybe_provided, pkg_shlib_name_with_flags(provided, provided_flags));
				}
			} else {
				char *ip = pkg_shlib_name_with_flags(provided, provided_flags);
				if (charv_insert_sorted(&internal_provided, ip) != NULL)
					free(ip);
			}
			free(provided);
		}
	}

	vec_foreach(maybe_provided, i) {
		vec_foreach(internal_provided, j) {
			if (STREQ(maybe_provided.d[i], internal_provided.d[j])) {
				pkg_addshlib_provided(pkg, maybe_provided.d[i], PKG_SHLIB_FLAGS_NONE);
				vec_remove_and_free(&internal_provided, j, free);
				j--;
			}
		}
		vec_remove_and_free(&maybe_provided, i, free);
		i--;
	}
	vec_free(&maybe_provided);
	/*
	 * Do not depend on libraries that a package provides itself
	 */
	pkg_cleanup_shlibs_required(pkg, &internal_provided);
	while (internal_provided.len > 0) {
		char *s = vec_pop(&internal_provided);
		if (charv_insert_sorted(&pkg->shlibs_provided_ignore, s) != NULL) {
			free(s);
		}
	}

	vec_foreach(pkg->shlibs_provided, i) {
		char *s = pkg->shlibs_provided.d[i];
		if (charv_search(&pkg->shlibs_provided_ignore, s) != NULL) {
			pkg_debug(2,
			    "remove %s from provided shlibs for package %s as it "
			    "is listed in shlibs_provided_ignore.",
			    s, pkg->name);
			vec_remove_and_free(&pkg->shlibs_provided, i, free);
			i--;
			continue;
		}
		if (match_ucl_lists(s,
		    pkg_config_get("SHLIB_PROVIDE_IGNORE_GLOB"),
		    pkg_config_get("SHLIB_PROVIDE_IGNORE_REGEX"))) {
			pkg_debug(2,
			    "remove %s from provided shlibs for package %s as it "
			    "is matched by SHLIB_PROVIDE_IGNORE_GLOB/REGEX.",
			    s, pkg->name);
			vec_remove(&pkg->shlibs_provided, i);
			if (charv_insert_sorted(&pkg->shlibs_provided_ignore, s) != NULL) {
				free(s);
			}
			i--;
			continue;
		}
	}

	/*
	 * if the package is not supposed to provide share libraries then
	 * drop the provided one
	 */
	if (pkg_kv_get(&pkg->annotations, "no_provide_shlib") != NULL) {
		vec_free_and_free(&pkg->shlibs_provided, free);
	}

	if (failures)
		goto cleanup;

	ret = EPKG_OK;

cleanup:
	ret = pkg_analyse_close();

	return (ret);
}
