/*
 * Copyright (c) 2013, 2015-2016, 2022-2025 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and 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.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * util.h
 * Shared Utility functions for Tix.
 */

#ifndef UTIL_H
#define UTIL_H

#define DEFAULT_GENERATION "3"

extern char** environ;

bool does_path_contain_dotdot(const char* path)
{
	size_t index = 0;
	while ( path[index] )
	{
		if ( path[index] == '.' && path[index+1] == '.' )
		{
			index += 2;
			if ( !path[index] || path[index] == '/' )
				return true;
		}
		while ( path[index] && path[index] != '/' )
			index++;
		while ( path[index] == '/' )
			index++;
	}
	return false;
}

bool parse_boolean(const char* str)
{
	return strcmp(str, "true") == 0;
}

char* strdup_null(const char* src)
{
	return src ? strdup(src) : NULL;
}

char* strdup_null_if_content(const char* src)
{
	if ( src && !src[0] )
		return NULL;
	return strdup_null(src);
}

const char* non_modify_basename(const char* path)
{
	const char* last_slash = (char*) strrchr((char*) path, '/');
	if ( !last_slash )
		return path;
	return last_slash + 1;
}

__attribute__((format(printf, 1, 2)))
char* print_string(const char* format, ...)
{
	va_list ap;
	va_start(ap, format);
	char* ret;
	if ( vasprintf(&ret, format, ap) < 0 )
		ret = NULL;
	va_end(ap);
	return ret;
}

typedef struct
{
	char** strings;
	size_t length;
	size_t capacity;
} string_array_t;

string_array_t string_array_make(void)
{
	string_array_t sa;
	sa.strings = NULL;
	sa.length = sa.capacity = 0;
	return sa;
}

void string_array_reset(string_array_t* sa)
{
	for ( size_t i = 0; i < sa->length; i++ )
		free(sa->strings[i]);
	free(sa->strings);
	*sa = string_array_make();
}

bool string_array_append(string_array_t* sa, const char* str)
{
	if ( sa->length == sa->capacity )
	{
		size_t new_capacity = sa->capacity ? sa->capacity * 2 : 8;
		size_t new_size = sizeof(char*) * new_capacity;
		char** new_strings = (char**) realloc(sa->strings, new_size);
		if ( !new_strings )
			return false;
		sa->strings = new_strings;
		sa->capacity = new_capacity;
	}
	char* copy = strdup_null(str);
	if ( str && !copy )
		return false;
	sa->strings[sa->length++] = copy;
	return true;
}

bool string_array_append_token_string(string_array_t* sa, const char* str)
{
	while ( *str )
	{
		if ( isspace((unsigned char) *str) )
		{
			str++;
			continue;
		}

		size_t input_length = 0;
		size_t output_length = 0;
		bool quoted = false;
		bool escaped = false;
		while ( str[input_length] &&
		        (escaped || quoted || !isspace((unsigned char) str[input_length])) )
		{
			if ( !escaped && str[input_length] == '\\' )
				escaped = true;
			else if ( !escaped && str[input_length] == '"' )
				quoted = !quoted;
			else
				escaped = false, output_length++;
			input_length++;
		}

		if ( quoted || escaped )
			return false;

		char* output = (char*) malloc(sizeof(char) * (output_length+1));
		if ( !output )
			return false;

		input_length = 0;
		output_length = 0;
		quoted = false;
		escaped = false;
		while ( str[input_length] &&
		        (escaped || quoted || !isspace((unsigned char) str[input_length])) )
		{
			if ( !escaped && str[input_length] == '\\' )
				escaped = true;
			else if ( !escaped && str[input_length] == '"' )
				quoted = !quoted;
			else if ( escaped && str[input_length] == 'a' )
				escaped = false, output[output_length++] = '\a';
			else if ( escaped && str[input_length] == 'b' )
				escaped = false, output[output_length++] = '\b';
			else if ( escaped && str[input_length] == 'e' )
				escaped = false, output[output_length++] = '\e';
			else if ( escaped && str[input_length] == 'f' )
				escaped = false, output[output_length++] = '\f';
			else if ( escaped && str[input_length] == 'n' )
				escaped = false, output[output_length++] = '\n';
			else if ( escaped && str[input_length] == 'r' )
				escaped = false, output[output_length++] = '\r';
			else if ( escaped && str[input_length] == 't' )
				escaped = false, output[output_length++] = '\t';
			else if ( escaped && str[input_length] == 'v' )
				escaped = false, output[output_length++] = '\v';
			else
				escaped = false, output[output_length++] = str[input_length];
			input_length++;
		}

		output[output_length] = '\0';

		if ( !string_array_append(sa, output) )
		{
			free(output);
			return false;
		}

		free(output);

		str += input_length;
	}

	return true;
}

bool is_token_string_special_character(char c)
{
	return isspace((unsigned char) c) || c == '"' || c == '\\';
}

char* token_string_of_string_array(const string_array_t* sa)
{
	size_t result_length = 0;

	for ( size_t i = 0; i < sa->length; i++ )
	{
		if ( i )
			result_length++;

		for ( size_t n = 0; sa->strings[i][n]; n++ )
		{
			if ( is_token_string_special_character(sa->strings[i][n]) )
				result_length++;
			result_length++;
		}
	}

	char* result = (char*) malloc(sizeof(char) * (result_length + 1));
	if ( !result )
		return NULL;

	result_length = 0;

	for ( size_t i = 0; i < sa->length; i++ )
	{
		if ( i )
			result[result_length++] = ' ';

		for ( size_t n = 0; sa->strings[i][n]; n++ )
		{
			if ( is_token_string_special_character(sa->strings[i][n]) )
				result[result_length++] = '\\';
			result[result_length++] = sa->strings[i][n];
		}
	}

	result[result_length] = '\0';

	return result;
}

bool string_array_append_file(string_array_t* sa, FILE* fp)
{
	char* entry = NULL;
	size_t entry_size = 0;
	ssize_t entry_length;
	while ( 0 < (entry_length = getline(&entry, &entry_size, fp)) )
	{
		if ( entry[entry_length-1] == '\n' )
			entry[--entry_length] = '\0';
		if ( !string_array_append(sa, entry) )
			return false;
	}
	free(entry);
	return !ferror(fp);
}

bool string_array_append_file_path(string_array_t* sa, const char* path)
{
	FILE* fp = fopen(path, "r");
	if ( !fp )
		return false;
	bool result = string_array_append_file(sa, fp);
	fclose(fp);
	return result;
}

size_t string_array_find(string_array_t* sa, const char* str)
{
	for ( size_t i = 0; i < sa->length; i++ )
		if ( !strcmp(sa->strings[i], str) )
			return i;
	return SIZE_MAX;
}

bool string_array_contains(string_array_t* sa, const char* str)
{
	return string_array_find(sa, str) != SIZE_MAX;
}

size_t dictionary_lookup_index(string_array_t* sa, const char* key)
{
	size_t keylen = strlen(key);
	for ( size_t i = 0; i < sa->length; i++ )
	{
		const char* entry = sa->strings[i];
		if ( strncmp(key, entry, keylen) != 0 )
			continue;
		if ( entry[keylen] != '=' )
			continue;
		return i;
	}
	return SIZE_MAX;
}

const char* dictionary_get_entry(string_array_t* sa, const char* key)
{
	size_t index = dictionary_lookup_index(sa, key);
	if ( index == SIZE_MAX )
		return NULL;
	return sa->strings[index];
}

const char* dictionary_get_def(string_array_t* sa, const char* key,
                               const char* def)
{
	size_t keylen = strlen(key);
	const char* entry = dictionary_get_entry(sa, key);
	return entry ? entry + keylen + 1 : def;
}

const char* dictionary_get(string_array_t* sa, const char* key)
{
	return dictionary_get_def(sa, key, NULL);
}

bool dictionary_set(string_array_t* sa, const char* key, const char* value)
{
	char* entry = print_string("%s=%s", key, value);
	if ( !entry )
		return false;
	size_t index = dictionary_lookup_index(sa, key);
	if ( index == SIZE_MAX )
	{
		if ( !string_array_append(sa, entry) )
			return free(entry), false;
		return true;
	}
	free(sa->strings[index]);
	sa->strings[index] = entry;
	return true;
}

bool is_identifier_char(char c)
{
	return ('a' <= c && c <= 'z') ||
	       ('A' <= c && c <= 'Z') ||
	       ('0' <= c && c <= '9') ||
	       c == '_';
}

// 0 on success, -1 on error, -2 on syntax error.
int variables_parse_fp(string_array_t* sa, const char* line, FILE* fp)
{
	size_t i = 0;
	while ( isspace((unsigned char) line[i]) )
		i++;
	if ( !line[i] || line[i] == '#' )
		return 0;
	if ( line[i] == '=' )
		return -2;
	size_t keylen = 0;
	while ( line[i + keylen] &&
	        line[i + keylen] != '=' &&
	        line[i + keylen] != '#' &&
	        !isspace((unsigned char) line[i + keylen]) )
	{
		if ( fputc((unsigned char) line[i + keylen++], fp) == EOF )
			return -1;
	}
	i += keylen;
	if ( line[i++] != '=' )
		return -2;
	fputc('=', fp);
	bool escaped = false;
	bool singly_quote = false;
	bool doubly_quote = false;
	while ( true )
	{
		unsigned char c = (unsigned char) line[i++];
		if ( !c )
		{
			i--;
			break;
		}
		else if ( !escaped && !singly_quote && c == '\\' )
			escaped = true;
		else if ( !escaped && !doubly_quote && c == '\'' )
			singly_quote = !singly_quote;
		else if ( !escaped && !singly_quote && c == '"' )
			doubly_quote = !doubly_quote;
		else if ( !escaped && !singly_quote && c == '$' &&
		          (is_identifier_char(line[i]) || line[i] == '{') )
		{
			size_t start = i;
			size_t keylen = 0;
			if ( line[start] == '{' )
			{
				start++;
				while ( line[start + keylen] && line[start + keylen] != '}' )
					keylen++;
				if ( !keylen )
					return -2;
				if ( line[start + keylen] != '}' )
					return -2;
				i = start + keylen + 1;
			}
			else
			{
				while ( line[start + keylen] &&
				        is_identifier_char(line[start + keylen]) )
					keylen++;
				i = start + keylen;
			}
			const char* key = line + start;
			const char* value = NULL;
			for ( size_t n = 0; !value && n < sa->length; n++ )
			{
				const char* entry = sa->strings[n];
				if ( strncmp(key, entry, keylen) != 0 )
					continue;
				if ( entry[keylen] != '=' )
					continue;
				value = entry + keylen + 1;
			}
			if ( !value )
				return -2;
			size_t length = strlen(value);
			if ( fwrite(value, 1, length, fp) != length )
				return -1;
		}
		else
		{
			if ( !escaped && !singly_quote && !doubly_quote && isspace(c) )
			{
				i--;
				break;
			}
			if ( fputc(c, fp) == EOF )
				return -1;
			escaped = false;
		}
	}
	while ( isspace((unsigned char) line[i]) )
		i++;
	if ( line[i] && line[i] != '#' )
		return -2;
	return 1;
}

int variables_parse(string_array_t* sa, const char* line, char** out_ptr)
{
	size_t out_size;
	FILE* fp = open_memstream(out_ptr, &out_size);
	if ( !fp )
		return -1;
	int result = variables_parse_fp(sa, line, fp);
	if ( fclose(fp) == EOF )
		result = -1;
	if ( result <= 0 )
		free(*out_ptr);
	return result;
}

int variables_append_file(string_array_t* sa, FILE* fp)
{
	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_length;
	while ( 0 < (line_length = getline(&line, &line_size, fp)) )
	{
		if ( line[line_length-1] == '\n' )
			line[--line_length] = '\0';
		char* entry;
		int result = variables_parse(sa, line, &entry);
		if ( result == 0 )
			continue;
		if ( result < 0 )
		{
			free(line);
			return result;
		}
		if ( !string_array_append(sa, entry) )
		{
			free(entry);
			free(line);
			return -1;
		}
		free(entry);
	}
	free(line);
	if ( ferror(fp) )
		return -1;
	return 0;
}

int variables_append_file_path(string_array_t* sa, const char* path)
{
	FILE* fp = fopen(path, "r");
	if ( !fp )
		return -1;
	int result = variables_append_file(sa, fp);
	fclose(fp);
	return result;
}

char* read_single_line(FILE* fp)
{
	char* ret = NULL;
	size_t ret_size = 0;
	ssize_t ret_len = getline(&ret, &ret_size, fp);
	if ( ret_len < 0 )
	{
		free(ret);
		return NULL;
	}
	if ( ret[ret_len-1] == '\n' )
		ret[--ret_len] = '\0';
	return ret;
}

pid_t fork_or_death(void)
{
	pid_t child_pid = fork();
	if ( child_pid < 0 )
		err(1, "fork");
	return child_pid;
}

void waitpid_or_death_def(pid_t child_pid, bool die_on_error)
{
	int status;
	waitpid(child_pid, &status, 0);
	if ( die_on_error )
	{
		if ( WIFEXITED(status) && WEXITSTATUS(status) != 0 )
			exit(WEXITSTATUS(status));
		if ( WIFSIGNALED(status) )
			errx(128 + WTERMSIG(status), "child with pid %ji was killed by "
			     "signal %i (%s).", (intmax_t) child_pid, WTERMSIG(status),
			     strsignal(WTERMSIG(status)));
	}
}

void waitpid_or_death(pid_t child_pid)
{
	return waitpid_or_death_def(child_pid, true);
}

bool fork_and_wait_or_death_def(bool die_on_error)
{
	pid_t child_pid = fork_or_death();
	if ( !child_pid )
		return true;
	waitpid_or_death_def(child_pid, die_on_error);
	return false;
}

bool fork_and_wait_or_death(void)
{
	return fork_and_wait_or_death_def(true);
}

const char* getenv_def(const char* var, const char* def)
{
	const char* ret = getenv(var);
	return ret ? ret : def;
}

int mkdir_p(const char* path, mode_t mode)
{
	int saved_errno = errno;
	if ( !mkdir(path, mode) )
		return 0;
	if ( errno == ENOENT )
	{
		char* prev = strdup(path);
		if ( !prev )
			return -1;
		int status = mkdir_p(dirname(prev), mode | 0500);
		free(prev);
		if ( status < 0 )
			return -1;
		errno = saved_errno;
		if ( !mkdir(path, mode) )
			return 0;
	}
	if ( errno == EEXIST )
		return errno = saved_errno, 0;
	return -1;
}

static void compact_arguments(int* argc, char*** argv)
{
	for ( int i = 0; i < *argc; i++ )
	{
		while ( i < *argc && !(*argv)[i] )
		{
			for ( int n = i; n < *argc; n++ )
				(*argv)[n] = (*argv)[n+1];
			(*argc)--;
		}
	}
}

char* GetBuildTriplet(void)
{
#if defined(__sortix__) && defined(__i386__)
#if defined(__i686__)
	return "i686-sortix";
#elif defined(__i586__)
	return "i586-sortix";
#elif defined(__i486__)
	return "i486-sortix";
#else
	return "i386-sortix";
#endif
#elif defined(__sortix__) && defined(__x86_64__)
	return strdup("x86_64-sortix");
#elif defined(__sortix__)
	#warning "Add your build triplet here"
#endif
	FILE* fp = popen("cc -dumpmachine", "r");
	if ( !fp )
		return NULL;
	char* ret = read_single_line(fp);
	pclose(fp);
	return ret;
}

bool get_option_variable(const char* option, char** varptr,
                         const char* arg, int argc, char** argv, int* ip,
                         const char* argv0)
{
	size_t option_len = strlen(option);
	if ( strncmp(option, arg, option_len) != 0 )
		return false;
	if ( arg[option_len] == '=' )
	{
		*varptr = strdup(arg + option_len + 1);
		return true;
	}
	if ( arg[option_len] != '\0' )
		return false;
	if ( *ip + 1 == argc )
	{
		fprintf(stderr, "%s: expected operand after `%s'\n", argv0, option);
		exit(1);
	}
	*varptr = strdup(argv[++*ip]), argv[*ip] = NULL;
	return true;
}

char* join_paths(const char* a, const char* b)
{
	size_t a_len = strlen(a);
	bool has_slash = (a_len && a[a_len-1] == '/') || b[0] == '/';
	return has_slash ? print_string("%s%s", a, b) : print_string("%s/%s", a, b);
}

bool IsFile(const char* path)
{
	struct stat st;
	return stat(path, &st) == 0 && S_ISREG(st.st_mode);
}

bool IsDirectory(const char* path)
{
	struct stat st;
	return stat(path, &st) == 0 &&
	       (S_ISDIR(st.st_mode) || (errno = ENOTDIR, false));
}

size_t count_tar_components(const char* path)
{
	if ( !*path )
		return 0;
	size_t slashes = 1;
	for ( size_t i = 0; path[i]; i++ )
		if ( path[i] == '/' )
			slashes++;
	return slashes;
}

bool is_valid_package_name(const char* package)
{
	return strcmp(package, ".") != 0 && strcmp(package, "..") != 0;
}

#define GET_OPTION_VARIABLE(str, varptr) \
        get_option_variable(str, varptr, arg, argc, argv, &i, argv0)

// TODO: This is a bit inefficient but doing it otherwise would involve looking
//       through the error stream for messages such as "file not found", which
//       can be hard to distinguish from the common "oh no, an error occured"
//       case in which we need to abort as well.
bool TarContainsFile(const char* archive, const char* file)
{
	int pipes[2];
	if ( pipe(pipes) )
		err(1, "pipe");
	pid_t tar_pid = fork_or_death();
	if ( !tar_pid )
	{
		dup2(pipes[1], 1);
		close(pipes[1]);
		close(pipes[0]);
		const char* cmd_argv[] =
		{
			"tar",
			"--list",
			"--file", archive,
			NULL
		};
		execvp(cmd_argv[0], (char* const*) cmd_argv);
		err(127, "%s", cmd_argv[0]);
	}
	close(pipes[1]);
	FILE* fp = fdopen(pipes[0], "r");

	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_len;
	bool ret = false;
	while ( 0 < (line_len = getline(&line, &line_size, fp)) )
	{
		if ( line[line_len-1] == '\n' )
			line[--line_len] = '\0';
		if ( strcmp(line, file) == 0 )
		{
			ret = true;
		#if !defined(__sortix__)
			kill(tar_pid, SIGPIPE);
			break;
		#endif
		}
	}
	free(line);
	if ( ferror(fp) )
		err(1, "getline: tar");

	fclose(fp);
	int tar_exit_status;
	waitpid(tar_pid, &tar_exit_status, 0);
	bool sigpiped = WIFSIGNALED(tar_exit_status) &&
	                WTERMSIG(tar_exit_status) == SIGPIPE;
	bool errored = !WIFEXITED(tar_exit_status) ||
	               WEXITSTATUS(tar_exit_status) != 0;
	if ( errored && !sigpiped )
	{
		errx(1, "Unable to list contents of `%s'.", archive);
		exit(WEXITSTATUS(tar_exit_status));
	}
	return ret;
}

void TarExtractFileToFD(const char* archive, const char* file, int fd)
{
	pid_t tar_pid = fork_or_death();
	if ( !tar_pid )
	{
		if ( dup2(fd, 1) < 0 )
		{
			warn("dup2");
			_exit(127);
		}
		close(fd);
		const char* cmd_argv[] =
		{
			"tar",
			"--to-stdout",
			"--extract",
			"--file", archive,
			"--", file,
			NULL
		};
		execvp(cmd_argv[0], (char* const*) cmd_argv);
		warn("%s", cmd_argv[0]);
		_exit(127);
	}
	int tar_exit_status;
	waitpid(tar_pid, &tar_exit_status, 0);
	if ( !WIFEXITED(tar_exit_status) || WEXITSTATUS(tar_exit_status) != 0 )
	{
		errx(1, "Unable to extract `%s/%s'", archive, file);
		exit(WEXITSTATUS(tar_exit_status));
	}
}

FILE* TarOpenFile(const char* archive, const char* file)
{
	FILE* fp = tmpfile();
	if ( !fp )
		err(1, "tmpfile");
	TarExtractFileToFD(archive, file, fileno(fp));
	if ( fseeko(fp, 0, SEEK_SET) < 0 )
		err(1, "fseeko(tmpfile(), 0, SEEK_SET)");
	return fp;
}

void TarIndexToFD(const char* archive, int fd)
{
	pid_t tar_pid = fork_or_death();
	if ( !tar_pid )
	{
		if ( dup2(fd, 1) < 0 )
		{
			warn("dup2");
			_exit(127);
		}
		close(fd);
		const char* cmd_argv[] =
		{
			"tar",
			"--list",
			"--file", archive,
			NULL
		};
		execvp(cmd_argv[0], (char* const*) cmd_argv);
		warn("%s", cmd_argv[0]);
		_exit(127);
	}
	int tar_exit_status;
	waitpid(tar_pid, &tar_exit_status, 0);
	if ( !WIFEXITED(tar_exit_status) || WEXITSTATUS(tar_exit_status) != 0 )
	{
		errx(1, "Unable to list contents of `%s'", archive);
		exit(WEXITSTATUS(tar_exit_status));
	}
}

FILE* TarOpenIndex(const char* archive)
{
	FILE* fp = tmpfile();
	if ( !fp )
		err(1, "tmpfile");
	TarIndexToFD(archive, fileno(fp));
	if ( fseeko(fp, 0, SEEK_SET) < 0 )
		err(1, "fseeko(tmpfile(), 0, SEEK_SET)");
	return fp;
}

const char* VerifyInfoVariable(string_array_t* info, const char* var,
                               const char* path)
{
	const char* ret = dictionary_get(info, var);
	if ( !ret )
		errx(1, "error: `%s': no `%s' variable declared", path, var);
	return ret;
}

void VerifyTixInformation(string_array_t* tixinfo, const char* tix_path)
{
	const char* tix_version = dictionary_get(tixinfo, "tix.version");
	if ( !tix_version )
		errx(1, "error: `%s': no `tix.version' variable declared",
		        tix_path);
	if ( atoi(tix_version) != 1 )
		errx(1, "error: `%s': tix version `%s' not supported", tix_path,
		        tix_version);
	const char* tix_class = dictionary_get(tixinfo, "tix.class");
	if ( !tix_class )
		errx(1, "error: `%s': no `tix.class' variable declared", tix_path);
	if ( !strcmp(tix_class, "srctix") )
		errx(1, "error: `%s': this object is a source tix and needs to be "
		        "compiled into a binary tix prior to installation.",
		        tix_path);
	if ( strcmp(tix_class, "tix") )
		errx(1, "error: `%s': tix class `%s' is not `tix': this object is "
		        "not suitable for installation.", tix_path, tix_class);
	if ( !(dictionary_get(tixinfo, "tix.platform")) )
		errx(1, "error: `%s': no `tix.platform' variable declared", tix_path);
	if ( !(dictionary_get(tixinfo, "pkg.name")) )
		errx(1, "error: `%s': no `pkg.name' variable declared", tix_path);
}

bool IsCollectionPrefixRatherThanCommand(const char* arg)
{
	return strchr(arg, '/') || !strcmp(arg, ".") || !strcmp(arg, "..");
}

void ParseOptionalCommandLineCollectionPrefix(char** collection, int* argcp,
                                              char*** argvp)
{
	if ( 2 <= *argcp && IsCollectionPrefixRatherThanCommand((*argvp)[1]) )
	{
		if ( !*collection )
		{
			free(*collection);
			*collection = strdup((*argvp)[1]);
		}
		(*argvp)[1] = NULL;
		compact_arguments(argcp, argvp);
	}
	else if ( !*collection )
	{
		*collection = strdup("/");
	}
}

void VerifyCommandLineCollection(char** collection)
{
	if ( !*collection )
		errx(1, "error: you need to specify which tix collection to administer "
		        "using --collection or giving the prefix as the first "
		        "argument.");

	if ( !**collection )
	{
		free(*collection);
		*collection = strdup("/");
	}

	char* collection_rel = *collection;
	if ( !(*collection = realpath(collection_rel, NULL)) )
		err(1, "realpath: %s", collection_rel);
	free(collection_rel);
}

void VerifyTixCollectionConfiguration(string_array_t* info, const char* path)
{
	// TODO: After releasing Sortix 1.1, remove generation 2 compatibility.
	const char* tix_version = dictionary_get(info, "tix.version");
	if ( tix_version )
	{
		if ( !tix_version )
			errx(1, "error: `%s': no `tix.version' variable declared", path);
		if ( atoi(tix_version) != 1 )
			errx(1, "error: `%s': tix version `%s' not supported", path,
				    tix_version);
		const char* tix_class = dictionary_get(info, "tix.class");
		if ( !tix_class )
			errx(1, "error: `%s': no `tix.class' variable declared", path);
		if ( strcmp(tix_class, "collection") != 0 )
			errx(1, "error: `%s': error: unexpected tix class `%s'.", path,
				    tix_class);
		if ( !(dictionary_get(info, "collection.prefix")) )
			errx(1, "error: `%s': no `collection.prefix' variable declared",
			        path);
		if ( !(dictionary_get(info, "collection.platform")) )
			errx(1, "error: `%s': no `collection.platform' variable declared",
				    path);
		return;
	}

	const char* version = dictionary_get(info, "TIX_COLLECTION_VERSION");
	if ( !version )
		errx(1, "%s: Mandatory TIX_COLLECTION_VERSION was not set", path);
	if ( atoi(version) != 3 )
		errx(1, "%s: Unsupported: TIX_COLLECTION_VERSION: %s", path, version);
	if ( !(dictionary_get(info, "PREFIX")) )
		errx(1, "%s: Mandatory PREFIX was not set", path);
	if ( !(dictionary_get(info, "PLATFORM")) )
		errx(1, "%s: Mandatory PLATFORM was not set", path);
}

static pid_t original_pid;
char* tmp_root = NULL;
static void cleanup_tmp(void)
{
	if ( original_pid != getpid() )
		return;
	if ( !tmp_root )
		return;
	pid_t pid = fork();
	if ( pid < 0 )
	{
		warn("fork");
		return;
	}
	if ( pid == 0 )
	{
		const char* cmd_argv[] =
		{
			"rm",
			"-rf",
			"--",
			(const char*) tmp_root,
			NULL,
		};
		execvp(cmd_argv[0], (char* const*) cmd_argv);
		warn("%s", cmd_argv[0]);
		_exit(127);
	}
	int code;
	waitpid(pid, &code, 0);
	free(tmp_root);
	tmp_root = NULL;
}

void initialize_tmp(const char* tmp, const char* purpose)
{
	if ( tmp_root )
		errx(1, "error: initialize_tmp called twice");
	if ( asprintf(&tmp_root, "%s/%s.XXXXXX", tmp, purpose) < 0 )
		err(1, "error: asprintf");
	if ( !mkdtemp(tmp_root) )
		err(1, "mkdtemp: `%s'", tmp_root);
	original_pid = getpid();
	if ( atexit(cleanup_tmp) != 0 )
	{
		int errnum = errno;
		cleanup_tmp();
		errno = errnum;
		err(1, "atexit");
	}
}

mode_t get_umask_value(void)
{
	mode_t result = umask(0);
	umask(result);
	return result;
}

int fchmod_plus_x(int fd)
{
	struct stat st;
	if ( fstat(fd, &st) != 0 )
		return -1;
	mode_t new_mode = st.st_mode | (0111 & ~get_umask_value());
	if ( fchmod(fd, new_mode) != 0 )
		return -1;
	return 0;
}

void fprint_shell_variable_assignment(FILE* fp, const char* variable, const char* value)
{
	if ( value )
	{
		fprintf(fp, "export %s='", variable);
		for ( size_t i = 0; value[i]; i++ )
			if ( value[i] == '\'' )
				fprintf(fp, "'\\''");
			else
				fputc(value[i], fp);
		fprintf(fp, "'\n");
	}
	else
	{
		fprintf(fp, "unset %s\n", variable);
	}
}

bool needs_single_quote(const char* string)
{
	for ( size_t i = 0; string[i]; i++ )
	{
		char c = string[i];
		if ( !(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
		       ('0' <= c && c <= '9') || c == '@' || c == '^' ||
		       c == '/' || c == '_' || c == '.' || c == '+' || c == ':' ||
		       c == '%' || c == '{' || c == ',' || c == '}' || c == '-') )
			return true;
	}
	return false;
}

void fwrite_value(FILE* fp, const char* value)
{
	if ( !needs_single_quote(value) )
		fprintf(fp, "%s\n", value);
	else
	{
		fputc('\'', fp);
		for ( size_t i = 0; value[i]; i++ )
			if ( value[i] == '\'' )
				fprintf(fp, "'\\''");
			else
				fputc(value[i], fp);
		fputs("'\n", fp);
	}
}

void fwrite_variable(FILE* fp, const char* key, const char* value)
{
	fprintf(fp, "%s=", key);
	fwrite_value(fp, value);
}

void fwrite_variable_raw(FILE* fp, const char* string)
{
	size_t assignment = strcspn(string, "=");
	assert(string[assignment] == '=');
	fwrite(string, assignment + 1, 1, fp);
	fwrite_value(fp, string + assignment + 1);
}

bool is_success_exit_status(int status)
{
	return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}

__attribute__((noreturn))
bool exit_like_exit_status(int status)
{
	if ( WIFEXITED(status) )
		exit(WEXITSTATUS(status));
	if ( WIFSIGNALED(status) )
		exit(128 + WTERMSIG(status));
	exit(1);
}

enum recovery_state
{
	RECOVERY_STATE_NONE,
	RECOVERY_STATE_PRINT_COMMAND,
	RECOVERY_STATE_RUN_SHELL,
};

enum recovery_state
recovery_configure_state_def(bool set, enum recovery_state to_what)
{
	static enum recovery_state recovery_state = RECOVERY_STATE_NONE;
	if ( set )
		recovery_state = to_what;
	return recovery_state;
}

enum recovery_state
recovery_configure_state(bool set)
{
	return recovery_configure_state_def(set, RECOVERY_STATE_NONE);
}

bool recovery_print_attempted_execution(void)
{
	pid_t child_pid = fork();
	if ( child_pid < 0 )
		return false;

	if ( !child_pid )
	{
		// Redirect stdout and stderr to /dev/null to prevent duplicate errors.
		// The recovery_execvp function below will automatically re-open the
		// terminal device when it needs to talk to the terminal.
		int dev_null = open("/dev/null", O_WRONLY);
		if ( 0 <= dev_null )
		{
			dup2(dev_null, 1);
			dup2(dev_null, 2);
			close(dev_null);
		}

		recovery_configure_state_def(true, RECOVERY_STATE_PRINT_COMMAND);
		return true;
	}

	int status;
	waitpid(child_pid, &status, 0);

	return false;
}

bool recovery_run_shell(void)
{
	pid_t child_pid = fork();
	if ( child_pid < 0 )
		return false;

	if ( !child_pid )
	{
		recovery_configure_state_def(true, RECOVERY_STATE_RUN_SHELL);
		return true;
	}

	int status;
	waitpid(child_pid, &status, 0);

	return false;
}

int recovery_execvp(const char* path, char* const* argv)
{
	if ( recovery_configure_state(false) == RECOVERY_STATE_NONE )
		return execvp(path, argv);

	// Make sure that stdout and stderr go to an interactive terminal.
	if ( !isatty(1) )
	{
		int dev_tty = open("/dev/tty", O_WRONLY);
		if ( 0 <= dev_tty )
		{
			dup2(dev_tty, 1);
			dup2(dev_tty, 2);
			close(dev_tty);
		}
	}

	printf("Attempted command was: ");

	for ( int i = 0; argv[i]; i++ )
	{
		if ( i )
			putchar(' ');
		putchar('\'');
		for ( size_t n = 0; argv[i][n]; n++ )
			if ( argv[i][n] == '\'' )
				printf("'\\''");
			else
				putchar(argv[i][n]);
		putchar('\'');
	}

	printf("\n");
	fflush(stdout);

	if ( recovery_configure_state(false) == RECOVERY_STATE_PRINT_COMMAND )
		_exit(0);

	const char* cmd_argv[] =
	{
		getenv_def("SHELL", "sh"),
		NULL
	};
	execvp(cmd_argv[0], (char* const*) cmd_argv);
	err(127, "%s", cmd_argv[0]);
}

bool fork_and_wait_or_recovery(void)
{
	int default_selection = 1;

	while ( true )
	{
		pid_t child_pid = fork_or_death();
		if ( !child_pid )
			return true;

		int status;
		waitpid(child_pid, &status, 0);

		if ( is_success_exit_status(status) )
			return false;

		if ( WIFEXITED(status) )
			warnx("child with pid %ju exited with status %i.",
			      (intmax_t) child_pid, WEXITSTATUS(status));
		else if ( WIFSIGNALED(status) )
			warnx("child with pid %ji was killed by signal %i (%s).",
			      (intmax_t) child_pid, WTERMSIG(status),
			      strsignal(WTERMSIG(status)));
		else
			warnx("child with pid %ji exited in an unusual manner (%i).",
			      (intmax_t) child_pid, status);

		if ( recovery_print_attempted_execution() )
			return true;

		if ( !isatty(0) )
			exit_like_exit_status(status);

		FILE* output = fopen("/dev/tty", "we");
		if ( !output )
			exit_like_exit_status(status);

retry_ask_recovery_method:

		fprintf(output, "\n");
		fprintf(output, "1. Abort\n");
		fprintf(output, "2. Try again\n");
		fprintf(output, "3. Pretend command was successful\n");
		fprintf(output, "4. Run $SHELL -i to investigate\n");
		fprintf(output, "5. Dump environment\n");
		fprintf(output, "\n");
		fprintf(output, "Please choose one: [%i] ", default_selection);
		fflush(output);

		char* input = read_single_line(stdin);
		if ( !input )
		{
			fprintf(output, "\n");
			fclose(output);
			warn("stdin");
			exit_like_exit_status(status);
		}

		int selection = default_selection;
		if ( input[0] )
		{
			char* input_end;
			selection = (int) strtol(input, &input_end, 0);
			if ( *input_end )
			{
				warnx("error: `%s' is not an allowed choice", input);
				goto retry_ask_recovery_method;
			}

			if ( 5 < selection )
			{
				warnx("error: `%i' is not an allowed choice", selection);
				goto retry_ask_recovery_method;
			}
		}

		if ( selection == 1 )
			exit_like_exit_status(status);

		if ( selection == 2 )
		{
			fprintf(output, "\nTrying to execute command again.\n\n");
			fclose(output);
			continue;
		}

		if ( selection == 3 )
		{
			fprintf(output, "\nPretending the command executed successfully.\n\n");
			fclose(output);
			return false;
		}

		if ( selection == 4 )
		{
			fprintf(output, "\nDropping you to a recovery shell, type `exit' "
			                "when you are done.\n\n");
			if ( recovery_run_shell() )
				return true;
		}

		if ( selection == 5 )
		{
			for ( size_t i = 0; environ[i]; i++ )
				fprintf(output, "%s\n", environ[i]);
			goto retry_ask_recovery_method;
		}

		default_selection = 2;

		goto retry_ask_recovery_method;
	}
}

#endif
