/****************************************************************************
 * Copyright 2018-2024,2025 Thomas E. Dickey                                *
 * Copyright 2006-2016,2017 Free Software Foundation, Inc.                  *
 *                                                                          *
 * Permission is hereby granted, free of charge, to any person obtaining a  *
 * copy of this software and associated documentation files (the            *
 * "Software"), to deal in the Software without restriction, including      *
 * without limitation the rights to use, copy, modify, merge, publish,      *
 * distribute, distribute with modifications, sublicense, and/or sell       *
 * copies of the Software, and to permit persons to whom the Software is    *
 * furnished to do so, subject to the following conditions:                 *
 *                                                                          *
 * The above copyright notice and this permission notice shall be included  *
 * in all copies or substantial portions of the Software.                   *
 *                                                                          *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS  *
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF               *
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.   *
 * IN NO EVENT SHALL THE ABOVE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,   *
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR    *
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR    *
 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.                               *
 *                                                                          *
 * Except as contained in this notice, the name(s) of the above copyright   *
 * holders shall not be used in advertising or otherwise to promote the     *
 * sale, use or other dealings in this Software without prior written       *
 * authorization.                                                           *
 ****************************************************************************/

/****************************************************************************
 *  Author: Thomas E. Dickey                                                *
 ****************************************************************************/

/*
 * Iterators for terminal databases.
 */

#include <curses.priv.h>

#include <time.h>
#include <tic.h>

#if USE_HASHED_DB
#include <hashed_db.h>
#endif

MODULE_ID("$Id: db_iterator.c,v 1.53 2025/01/12 00:41:56 tom Exp $")

#define HaveTicDirectory _nc_globals.have_tic_directory
#define KeepTicDirectory _nc_globals.keep_tic_directory
#define TicDirectory     _nc_globals.tic_directory
#define my_blob          _nc_globals.dbd_blob
#define my_list          _nc_globals.dbd_list
#define my_size          _nc_globals.dbd_size
#define my_time          _nc_globals.dbd_time
#define my_vars          _nc_globals.dbd_vars

static void
add_to_blob(const char *text, size_t limit)
{
    (void) limit;

    if (*text != '\0') {
	char *last = my_blob + strlen(my_blob);
	if (last != my_blob)
	    *last++ = NCURSES_PATHSEP;
	_nc_STRCPY(last, text, limit);
    }
}

static bool
check_existence(const char *name, struct stat *sb)
{
    bool result = FALSE;

    if (quick_prefix(name)) {
	result = TRUE;
    } else if (stat(name, sb) == 0
	       && (S_ISDIR(sb->st_mode)
		   || (S_ISREG(sb->st_mode) && sb->st_size))) {
	result = TRUE;
    }
#if USE_HASHED_DB
    else if (strlen(name) < PATH_MAX - sizeof(DBM_SUFFIX)) {
	char temp[PATH_MAX];
	_nc_SPRINTF(temp, _nc_SLIMIT(sizeof(temp)) "%s%s", name, DBM_SUFFIX);
	if (stat(temp, sb) == 0 && S_ISREG(sb->st_mode) && sb->st_size) {
	    result = TRUE;
	}
    }
#endif
    return result;
}

/*
 * Trim newlines (and backslashes preceding those) and tab characters to
 * help simplify scripting of the quick-dump feature.  Leave spaces and
 * other backslashes alone.
 */
static void
trim_formatting(char *source)
{
    char *target = source;
    char ch;

    while ((ch = *source++) != '\0') {
	if (ch == '\\' && *source == '\n')
	    continue;
	if (ch == '\n' || ch == '\t')
	    continue;
	*target++ = ch;
    }
    *target = '\0';
}

/*
 * Store the latest value of an environment variable in my_vars[] so we can
 * detect if one changes, invalidating the cached search-list.
 */
static bool
update_getenv(const char *name, DBDIRS which)
{
    bool result = FALSE;

    if (which < dbdLAST) {
	char *value;
	const char *cached_value = my_vars[which].value;
	bool same_value;

	if ((value = getenv(name)) != NULL) {
	    value = strdup(value);
	}
	same_value = ((value == NULL && cached_value == NULL) ||
		      (value != NULL &&
		       cached_value != NULL &&
		       strcmp(value, cached_value) == 0));

	/* Set variable name to enable checks in cache_expired(). */
	my_vars[which].name = name;

	if (!same_value) {
	    FreeIfNeeded(my_vars[which].value);
	    my_vars[which].value = value;
	    result = TRUE;
	} else {
	    free(value);
	}
    }
    return result;
}

#if NCURSES_USE_DATABASE || NCURSES_USE_TERMCAP
static char *
cache_getenv(const char *name, DBDIRS which)
{
    char *result = NULL;

    (void) update_getenv(name, which);
    if (which < dbdLAST) {
	result = my_vars[which].value;
    }
    return result;
}
#endif

/*
 * The cache expires if at least a second has passed since the initial lookup,
 * or if one of the environment variables changed.
 *
 * Only a few applications use multiple lookups of terminal entries, seems that
 * aside from bulk I/O such as tic and toe, that leaves interactive programs
 * which should not be modifying the terminal databases in a way that would
 * invalidate the search-list.
 *
 * The "1-second" is to allow for user-directed changes outside the program.
 */
static bool
cache_expired(void)
{
    bool result = FALSE;
    time_t now = time((time_t *) 0);

    if (now > my_time) {
	result = TRUE;
    } else {
	DBDIRS n;
	for (n = (DBDIRS) 0; n < dbdLAST; ++n) {
	    if (my_vars[n].name != NULL
		&& update_getenv(my_vars[n].name, n)) {
		result = TRUE;
		break;
	    }
	}
    }
    return result;
}

static void
free_cache(void)
{
    FreeAndNull(my_blob);
    FreeAndNull(my_list);
}

static void
update_tic_dir(const char *update)
{
    free((char *) TicDirectory);
    TicDirectory = update;
}

/*
 * Record the "official" location of the terminfo directory, according to
 * the place where we're writing to, or the normal default, if not.
 */
NCURSES_EXPORT(const char *)
_nc_tic_dir(const char *path)
{
    T(("_nc_tic_dir %s", NonNull(path)));
    if (!KeepTicDirectory) {
	if (path != NULL) {
	    if (path != TicDirectory)
		update_tic_dir(strdup(path));
	    HaveTicDirectory = TRUE;
	} else if (HaveTicDirectory == 0) {
	    if (use_terminfo_vars()) {
		const char *envp;
		if ((envp = getenv("TERMINFO")) != NULL)
		    return _nc_tic_dir(envp);
	    }
	}
    }
    return TicDirectory ? TicDirectory : TERMINFO;
}

/*
 * Special fix to prevent the terminfo directory from being moved after tic
 * has chdir'd to it.  If we let it be changed, then if $TERMINFO has a
 * relative path, we'll lose track of the actual directory.
 */
NCURSES_EXPORT(void)
_nc_keep_tic_dir(const char *path)
{
    _nc_tic_dir(path);
    KeepTicDirectory = TRUE;
}

/*
 * Cleanup.
 */
NCURSES_EXPORT(void)
_nc_last_db(void)
{
    if (my_blob != NULL && cache_expired()) {
	free_cache();
    }
}

/*
 * This is a simple iterator which allows the caller to step through the
 * possible locations for a terminfo directory.  ncurses uses this to find
 * terminfo files to read.
 */
NCURSES_EXPORT(const char *)
_nc_next_db(DBDIRS * state, int *offset)
{
    const char *result;

    (void) offset;
    if ((int) *state < my_size
	&& my_list != NULL
	&& my_list[*state] != NULL) {
	result = my_list[*state];
	(*state)++;
    } else {
	result = NULL;
    }
    if (result != NULL) {
	T(("_nc_next_db %d %s", *state, result));
    }
    return result;
}

NCURSES_EXPORT(void)
_nc_first_db(DBDIRS * state, int *offset)
{
    bool cache_has_expired = FALSE;
    *state = dbdTIC;
    *offset = 0;

    T((T_CALLED("_nc_first_db")));

    /* build a blob containing all of the strings we will use for a lookup
     * table.
     */
    if (my_blob == NULL || (cache_has_expired = cache_expired())) {
	size_t blobsize = 0;
	const char *values[dbdLAST];
	struct stat *my_stat;
	int j;

	if (cache_has_expired)
	    free_cache();

	for (j = 0; j < dbdLAST; ++j)
	    values[j] = NULL;

	/*
	 * This is the first item in the list, and is used only when tic is
	 * writing to the database, as a performance improvement.
	 */
	values[dbdTIC] = TicDirectory;

#if NCURSES_USE_DATABASE
#ifdef TERMINFO_DIRS
	values[dbdCfgList] = TERMINFO_DIRS;
#endif
#ifdef TERMINFO
	values[dbdCfgOnce] = TERMINFO;
#endif
#endif

#if NCURSES_USE_TERMCAP
	values[dbdCfgList2] = TERMPATH;
#endif

	if (use_terminfo_vars()) {
#if NCURSES_USE_DATABASE
	    values[dbdEnvOnce] = cache_getenv("TERMINFO", dbdEnvOnce);
	    values[dbdHome] = _nc_home_terminfo();
	    (void) cache_getenv("HOME", dbdHome);
	    values[dbdEnvList] = cache_getenv("TERMINFO_DIRS", dbdEnvList);

#endif
#if NCURSES_USE_TERMCAP
	    values[dbdEnvOnce2] = cache_getenv("TERMCAP", dbdEnvOnce2);
	    /* only use $TERMCAP if it is an absolute path */
	    if (values[dbdEnvOnce2] != NULL
		&& *values[dbdEnvOnce2] != '/') {
		values[dbdEnvOnce2] = NULL;
	    }
	    values[dbdEnvList2] = cache_getenv("TERMPATH", dbdEnvList2);
#endif /* NCURSES_USE_TERMCAP */
	}

	for (j = 0; j < dbdLAST; ++j) {
	    if (values[j] == NULL)
		values[j] = "";
	    blobsize += 2 + strlen(values[j]);
	}

	my_blob = malloc(blobsize);
	if (my_blob != NULL) {
	    *my_blob = '\0';
	    for (j = 0; j < dbdLAST; ++j) {
		add_to_blob(values[j], blobsize);
	    }

	    /* Now, build an array which will be pointers to the distinct
	     * strings in the blob.
	     */
	    blobsize = 2;
	    for (j = 0; my_blob[j] != '\0'; ++j) {
		if (my_blob[j] == NCURSES_PATHSEP)
		    ++blobsize;
	    }
	    my_list = typeCalloc(char *, blobsize);
	    my_stat = typeCalloc(struct stat, blobsize);
	    if (my_list != NULL && my_stat != NULL) {
		int k = 0;
		my_list[k++] = my_blob;
		for (j = 0; my_blob[j] != '\0'; ++j) {
		    if (my_blob[j] == NCURSES_PATHSEP
			&& ((&my_blob[j] - my_list[k - 1]) != 3
			    || !quick_prefix(my_list[k - 1]))) {
			my_blob[j] = '\0';
			my_list[k++] = &my_blob[j + 1];
		    }
		}

		/*
		 * Eliminate duplicates from the list.
		 */
		for (j = 0; my_list[j] != NULL; ++j) {
#ifdef TERMINFO
		    if (*my_list[j] == '\0') {
			char *my_copy = strdup(TERMINFO);
			if (my_copy != NULL)
			    my_list[j] = my_copy;
		    }
#endif
		    trim_formatting(my_list[j]);
		    for (k = 0; k < j; ++k) {
			if (!strcmp(my_list[j], my_list[k])) {
			    T(("duplicate %s", my_list[j]));
			    k = j - 1;
			    while ((my_list[j] = my_list[j + 1]) != NULL) {
				++j;
			    }
			    j = k;
			    break;
			}
		    }
		}

		/*
		 * Eliminate non-existent databases, and those that happen to
		 * be symlinked to another location.
		 */
		for (j = 0; my_list[j] != NULL; ++j) {
		    bool found = check_existence(my_list[j], &my_stat[j]);
#if HAVE_LINK
		    if (found) {
			for (k = 0; k < j; ++k) {
			    if (my_stat[j].st_dev == my_stat[k].st_dev
				&& my_stat[j].st_ino == my_stat[k].st_ino) {
				found = FALSE;
				break;
			    }
			}
		    }
#endif
		    if (!found) {
			T(("not found %s", my_list[j]));
			k = j;
			while ((my_list[k] = my_list[k + 1]) != NULL) {
			    ++k;
			}
			--j;
		    }
		}
		my_size = j;
		my_time = time((time_t *) 0);
	    } else {
		FreeAndNull(my_blob);
	    }
	    free(my_stat);
	}
    }
    returnVoid;
}

#if NO_LEAKS
void
_nc_db_iterator_leaks(void)
{
    DBDIRS which;

    if (my_blob != NULL)
	FreeAndNull(my_blob);
    if (my_list != NULL)
	FreeAndNull(my_list);
    for (which = 0; (int) which < dbdLAST; ++which) {
	my_vars[which].name = NULL;
	FreeIfNeeded(my_vars[which].value);
	my_vars[which].value = NULL;
    }
    update_tic_dir(NULL);
}
#endif
