/*-
 * Copyright (c) 1998 John D. Polstra
 * Copyright (c) 2012 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
 */

#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>

#include "pkg.h"
#include "pkghash.h"
#include "private/event.h"
#include "private/pkg.h"
#include "private/pkgdb.h"
#include "private/utils.h"
#include "xmalloc.h"

struct pkgbase {
	charv_t system_shlibs;
	/*
	 * unused yet but will be in the future when we will start using
	 * provides/requires in pkgbase
	 */
	charv_t provides;
	bool ignore_compat32;
};

static int
scan_dir_for_shlibs(charv_t *shlib_list, const char *dir,
    enum pkg_shlib_flags flags)
{
	DIR *dirp= opendir(dir);
	if (dirp == NULL) {
		if (errno == ENOENT) {
			return (EPKG_OK);
		}
		pkg_errno("Failed to open '%s' to scan for shared libraries", dir);
		return (EPKG_FATAL);
	}

	struct dirent *dp;
	size_t cnt = 0;
	while ((dp = readdir(dirp)) != NULL) {
		/* Only regular files and sym-links. On some
		   filesystems d_type is not set, on these the d_type
		   field will be DT_UNKNOWN. */
		if (dp->d_type != DT_REG && dp->d_type != DT_LNK &&
		    dp->d_type != DT_UNKNOWN)
			continue;

		int len = strlen(dp->d_name);
		/* Name can't be shorter than "libx.so" */
		if (len < 7 || strncmp(dp->d_name, "lib", 3) != 0)
			continue;

		const char *vers = dp->d_name + len;
		while (vers > dp->d_name &&
		       (isdigit(*(vers-1)) || *(vers-1) == '.'))
			vers--;
		if (vers == dp->d_name + len) {
			if (strncmp(vers - 3, ".so", 3) != 0)
				continue;
		} else if (vers < dp->d_name + 3 ||
		    strncmp(vers - 3, ".so.", 4) != 0)
			continue;

		/* We have a valid shared library name. */
		char *full = pkg_shlib_name_with_flags(dp->d_name, flags);
		vec_push(shlib_list, full);
		cnt++;
	}
	if (cnt == 0)
		errno = ENOENT;

	closedir(dirp);

	return (EPKG_OK);
}

static const struct {
	const char *dir;
	enum pkg_shlib_flags flags;
} system_shlib_table[] = {
	{"/lib", PKG_SHLIB_FLAGS_NONE },
	{"/usr/lib", PKG_SHLIB_FLAGS_NONE },
	{"/usr/lib32", PKG_SHLIB_FLAGS_COMPAT_32 },
};

int
scan_system_shlibs(charv_t *system_shlibs, const char *rootdir)
{
	int r = EPKG_OK;
	for (size_t i = 0; i < NELEM(system_shlib_table); i++) {
		char *dir;
		if (rootdir != NULL) {
			xasprintf(&dir, "%s%s", rootdir, system_shlib_table[i].dir);
		} else {
			dir = xstrdup(system_shlib_table[i].dir);
		}
		errno = 0;
		int ret = scan_dir_for_shlibs(system_shlibs, dir, system_shlib_table[i].flags);
		free(dir);
		if (errno == ENOENT)
			r = EPKG_NOCOMPAT32;
		if (ret != EPKG_OK) {
			return (ret);
		}
	}
	if (system_shlibs->d)
		qsort(system_shlibs->d, system_shlibs->len, sizeof(char *), char_cmp);

	return (r);
}

struct pkgbase *
pkgbase_new(struct pkgdb *db)
{
	struct pkgbase *pb = xcalloc(1, sizeof(*pb));

	if (!pkgdb_file_exists(db, "/usr/bin/uname"))
		if (scan_system_shlibs(&pb->system_shlibs, ctx.pkg_rootdir) == EPKG_NOCOMPAT32)
			pb->ignore_compat32 = true;

	return (pb);
}

void
pkgbase_free(struct pkgbase *pb)
{
	if (pb == NULL)
		return;
	vec_autofree(&pb->system_shlibs);
	vec_autofree(&pb->provides);
	free(pb);
}

bool
pkgbase_provide_shlib(struct pkgbase *pb, const char *shlib)
{
	if (pb->ignore_compat32 && str_ends_with(shlib, ":32"))
		return (true);
	return (charv_search(&pb->system_shlibs, shlib) != NULL);
}

bool
pkgbase_provide(struct pkgbase *pb, const char *provide)
{
	return (charv_search(&pb->provides, provide) != NULL);
}
