/*
 * psql - the PostgreSQL interactive terminal
 *
 * Copyright (c) 2000-2025, PostgreSQL Global Development Group
 *
 * src/bin/psql/variables.c
 */
#include "postgres_fe.h"

#include "common.h"
#include "common/logging.h"
#include "variables.h"

/*
 * Check whether a variable's name is allowed.
 *
 * We allow any non-ASCII character, as well as ASCII letters, digits, and
 * underscore.  Keep this in sync with the definition of variable_char in
 * psqlscan.l and psqlscanslash.l.
 */
static bool
valid_variable_name(const char *name)
{
	const unsigned char *ptr = (const unsigned char *) name;

	/* Mustn't be zero-length */
	if (*ptr == '\0')
		return false;

	while (*ptr)
	{
		if (IS_HIGHBIT_SET(*ptr) ||
			strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"
				   "_0123456789", *ptr) != NULL)
			ptr++;
		else
			return false;
	}

	return true;
}

/*
 * A "variable space" is represented by an otherwise-unused struct _variable
 * that serves as list header.
 *
 * The list entries are kept in name order (according to strcmp).  This
 * is mainly to make the output of PrintVariables() more pleasing.
 */
VariableSpace
CreateVariableSpace(void)
{
	struct _variable *ptr;

	ptr = pg_malloc(sizeof *ptr);
	ptr->name = NULL;
	ptr->value = NULL;
	ptr->substitute_hook = NULL;
	ptr->assign_hook = NULL;
	ptr->next = NULL;

	return ptr;
}

/*
 * Get string value of variable, or NULL if it's not defined.
 *
 * Note: result is valid until variable is next assigned to.
 */
const char *
GetVariable(VariableSpace space, const char *name)
{
	struct _variable *current;

	if (!space)
		return NULL;

	for (current = space->next; current; current = current->next)
	{
		int			cmp = strcmp(current->name, name);

		if (cmp == 0)
		{
			/* this is correct answer when value is NULL, too */
			return current->value;
		}
		if (cmp > 0)
			break;				/* it's not there */
	}

	return NULL;
}

/*
 * Try to interpret "value" as a boolean value, and if successful,
 * store it in *result.  Otherwise don't clobber *result.
 *
 * Valid values are: true, false, yes, no, on, off, 1, 0; as well as unique
 * prefixes thereof.
 *
 * "name" is the name of the variable we're assigning to, to use in error
 * report if any.  Pass name == NULL to suppress the error report.
 *
 * Return true when "value" is syntactically valid, false otherwise.
 */
bool
ParseVariableBool(const char *value, const char *name, bool *result)
{
	size_t		len;
	bool		valid = true;

	/* Treat "unset" as an empty string, which will lead to error below */
	if (value == NULL)
		value = "";

	len = strlen(value);

	if (len > 0 && pg_strncasecmp(value, "true", len) == 0)
		*result = true;
	else if (len > 0 && pg_strncasecmp(value, "false", len) == 0)
		*result = false;
	else if (len > 0 && pg_strncasecmp(value, "yes", len) == 0)
		*result = true;
	else if (len > 0 && pg_strncasecmp(value, "no", len) == 0)
		*result = false;
	/* 'o' is not unique enough */
	else if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0)
		*result = true;
	else if (pg_strncasecmp(value, "off", (len > 2 ? len : 2)) == 0)
		*result = false;
	else if (pg_strcasecmp(value, "1") == 0)
		*result = true;
	else if (pg_strcasecmp(value, "0") == 0)
		*result = false;
	else
	{
		/* string is not recognized; don't clobber *result */
		if (name)
			pg_log_error("unrecognized value \"%s\" for \"%s\": Boolean expected",
						 value, name);
		valid = false;
	}
	return valid;
}

/*
 * Try to interpret "value" as an integer value, and if successful,
 * store it in *result.  Otherwise don't clobber *result.
 *
 * "name" is the name of the variable we're assigning to, to use in error
 * report if any.  Pass name == NULL to suppress the error report.
 *
 * Return true when "value" is syntactically valid, false otherwise.
 */
bool
ParseVariableNum(const char *value, const char *name, int *result)
{
	char	   *end;
	long		numval;

	/* Treat "unset" as an empty string, which will lead to error below */
	if (value == NULL)
		value = "";

	errno = 0;
	numval = strtol(value, &end, 0);
	if (errno == 0 && *end == '\0' && end != value && numval == (int) numval)
	{
		*result = (int) numval;
		return true;
	}
	else
	{
		/* string is not recognized; don't clobber *result */
		if (name)
			pg_log_error("invalid value \"%s\" for \"%s\": integer expected",
						 value, name);
		return false;
	}
}

/*
 * Print values of all variables.
 */
void
PrintVariables(VariableSpace space)
{
	struct _variable *ptr;

	if (!space)
		return;

	for (ptr = space->next; ptr; ptr = ptr->next)
	{
		if (ptr->value)
			printf("%s = '%s'\n", ptr->name, ptr->value);
		if (cancel_pressed)
			break;
	}
}

/*
 * Set the variable named "name" to value "value",
 * or delete it if "value" is NULL.
 *
 * Returns true if successful, false if not; in the latter case a suitable
 * error message has been printed, except for the unexpected case of
 * space or name being NULL.
 */
bool
SetVariable(VariableSpace space, const char *name, const char *value)
{
	struct _variable *current,
			   *previous;

	if (!space || !name)
		return false;

	if (!valid_variable_name(name))
	{
		/* Deletion of non-existent variable is not an error */
		if (!value)
			return true;
		pg_log_error("invalid variable name: \"%s\"", name);
		return false;
	}

	for (previous = space, current = space->next;
		 current;
		 previous = current, current = current->next)
	{
		int			cmp = strcmp(current->name, name);

		if (cmp == 0)
		{
			/*
			 * Found entry, so update, unless assign hook returns false.
			 *
			 * We must duplicate the passed value to start with.  This
			 * simplifies the API for substitute hooks.  Moreover, some assign
			 * hooks assume that the passed value has the same lifespan as the
			 * variable.  Having to free the string again on failure is a
			 * small price to pay for keeping these APIs simple.
			 */
			char	   *new_value = value ? pg_strdup(value) : NULL;
			bool		confirmed;

			if (current->substitute_hook)
				new_value = current->substitute_hook(new_value);

			if (current->assign_hook)
				confirmed = current->assign_hook(new_value);
			else
				confirmed = true;

			if (confirmed)
			{
				pg_free(current->value);
				current->value = new_value;

				/*
				 * If we deleted the value, and there are no hooks to
				 * remember, we can discard the variable altogether.
				 */
				if (new_value == NULL &&
					current->substitute_hook == NULL &&
					current->assign_hook == NULL)
				{
					previous->next = current->next;
					free(current->name);
					free(current);
				}
			}
			else
				pg_free(new_value); /* current->value is left unchanged */

			return confirmed;
		}
		if (cmp > 0)
			break;				/* it's not there */
	}

	/* not present, make new entry ... unless we were asked to delete */
	if (value)
	{
		current = pg_malloc(sizeof *current);
		current->name = pg_strdup(name);
		current->value = pg_strdup(value);
		current->substitute_hook = NULL;
		current->assign_hook = NULL;
		current->next = previous->next;
		previous->next = current;
	}
	return true;
}

/*
 * Attach substitute and/or assign hook functions to the named variable.
 * If you need only one hook, pass NULL for the other.
 *
 * If the variable doesn't already exist, create it with value NULL, just so
 * we have a place to store the hook function(s).  (The substitute hook might
 * immediately change the NULL to something else; if not, this state is
 * externally the same as the variable not being defined.)
 *
 * The substitute hook, if given, is immediately called on the variable's
 * value.  Then the assign hook, if given, is called on the variable's value.
 * This is meant to let it update any derived psql state.  If the assign hook
 * doesn't like the current value, it will print a message to that effect,
 * but we'll ignore it.  Generally we do not expect any such failure here,
 * because this should get called before any user-supplied value is assigned.
 */
void
SetVariableHooks(VariableSpace space, const char *name,
				 VariableSubstituteHook shook,
				 VariableAssignHook ahook)
{
	struct _variable *current,
			   *previous;

	if (!space || !name)
		return;

	if (!valid_variable_name(name))
		return;

	for (previous = space, current = space->next;
		 current;
		 previous = current, current = current->next)
	{
		int			cmp = strcmp(current->name, name);

		if (cmp == 0)
		{
			/* found entry, so update */
			current->substitute_hook = shook;
			current->assign_hook = ahook;
			if (shook)
				current->value = (*shook) (current->value);
			if (ahook)
				(void) (*ahook) (current->value);
			return;
		}
		if (cmp > 0)
			break;				/* it's not there */
	}

	/* not present, make new entry */
	current = pg_malloc(sizeof *current);
	current->name = pg_strdup(name);
	current->value = NULL;
	current->substitute_hook = shook;
	current->assign_hook = ahook;
	current->next = previous->next;
	previous->next = current;
	if (shook)
		current->value = (*shook) (current->value);
	if (ahook)
		(void) (*ahook) (current->value);
}

/*
 * Return true iff the named variable has substitute and/or assign hook
 * functions.
 */
bool
VariableHasHook(VariableSpace space, const char *name)
{
	struct _variable *current;

	Assert(space);
	Assert(name);

	for (current = space->next; current; current = current->next)
	{
		int			cmp = strcmp(current->name, name);

		if (cmp == 0)
			return (current->substitute_hook != NULL ||
					current->assign_hook != NULL);
		if (cmp > 0)
			break;				/* it's not there */
	}

	return false;
}

/*
 * Convenience function to set a variable's value to "on".
 */
bool
SetVariableBool(VariableSpace space, const char *name)
{
	return SetVariable(space, name, "on");
}

/*
 * Attempt to delete variable.
 *
 * If unsuccessful, print a message and return "false".
 * Deleting a nonexistent variable is not an error.
 */
bool
DeleteVariable(VariableSpace space, const char *name)
{
	return SetVariable(space, name, NULL);
}

/*
 * Emit error with suggestions for variables or commands
 * accepting enum-style arguments.
 * This function just exists to standardize the wording.
 * suggestions should follow the format "fee, fi, fo, fum".
 */
void
PsqlVarEnumError(const char *name, const char *value, const char *suggestions)
{
	pg_log_error("unrecognized value \"%s\" for \"%s\"\n"
				 "Available values are: %s.",
				 value, name, suggestions);
}
