/* vifm
 * Copyright (C) 2001 Ken Steen.
 * Copyright (C) 2011 xaizek.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "utils_nix.h"
#include "utils_int.h"

#ifdef HAVE_MAX_ARG_STRLEN
/* MAX_ARG_STRLEN is the only reason we need these headers. */
#include <linux/binfmts.h>
#include <sys/user.h>
#endif

#include <sys/types.h> /* gid_t mode_t pid_t uid_t */
#ifdef HAVE_XATTRS
#include <sys/xattr.h> /* XATTR_CREATE getxattr() listxattr() setxattr() */
#endif
#include <sys/select.h> /* select() FD_SET FD_ZERO */
#include <sys/stat.h> /* O_* S_* */
#include <sys/statvfs.h> /* statvfs statvfs() */
#include <sys/time.h> /* timeval futimens() utimes() */
#include <sys/wait.h> /* WEXITSTATUS() WIFEXITED() WIFSIGNALED() waitpid() */
#include <fcntl.h> /* open() close() */
#include <grp.h> /* getgrnam() getgrgid_r() */
#include <pthread.h> /* pthread_sigmask() */
#include <pwd.h> /* getpwnam() getpwuid_r() */
#include <unistd.h> /* X_OK chown() close() dup() dup2() getpid() isatty()
                       pause() sysconf() ttyname() */

#include <assert.h> /* assert() */
#include <ctype.h> /* isdigit() */
#include <errno.h> /* EINTR ENOTSUP errno */
#include <signal.h> /* SIG* SIG_* sigset_t kill() sigemptyset() sigfillset()
                       signal() */
#include <stddef.h> /* NULL size_t */
#include <stdio.h> /* FILE stderr fclose() fdopen() fprintf() snprintf() */
#include <stdlib.h> /* atoi() free() */
#include <string.h> /* strchr() strdup() strerror() strlen() strncmp() */

#include "../cfg/config.h"
#include "../compat/fs_limits.h"
#include "../compat/mntent.h" /* mntent setmntent() getmntent() endmntent() */
#include "../compat/os.h"
#include "../compat/reallocarray.h"
#include "../ui/tabs.h"
#include "../ui/ui.h"
#include "../filelist.h"
#include "../running.h"
#include "../status.h"
#include "cancellation.h"
#include "env.h"
#include "filemon.h"
#include "fs.h"
#include "fswatch.h"
#include "log.h"
#include "macros.h"
#include "path.h"
#include "str.h"
#include "utils.h"

/* Types of mount point information for get_mount_point_traverser_state. */
typedef enum
{
	MI_MOUNT_POINT, /* Path to the mount point. */
	MI_FS_TYPE,     /* File system name. */
}
mntinfo;

/* State for get_mount_point() traverser. */
typedef struct
{
	mntinfo type;     /* Type of information to put into the buffer. */
	const char *path; /* Path whose mount point we're looking for. */
	size_t buf_len;   /* Output buffer length. */
	char *buf;        /* Output buffer. */
	size_t curr_len;  /* Max length among all found points (len of buf string). */
}
get_mount_point_traverser_state;

static int get_mount_info_traverser(struct mntent *entry, void *arg);
static void process_cancel_request(pid_t pid,
		const cancellation_t *cancellation);
static void free_mnt_entries(struct mntent *entries, unsigned int nentries);
static struct mntent * read_mnt_entries(unsigned int *nentries);
static int clone_mnt_entry(struct mntent *lhs, const struct mntent *rhs);
static void free_mnt_entry(struct mntent *entry);
static int starts_with_list_item(const char str[], const char list[]);
static int find_path_prefix_index(const char path[], const char list[]);
static int open_tty(void);
static void clone_timestamps(const char path[], const char from[],
		const struct stat *st);
static void clone_xattrs(const char path[], const char from[]);

void
pause_shell(void)
{
	run_in_shell_no_cls(PAUSE_CMD, SHELL_BY_APP);
}

int
run_in_shell_no_cls(char command[], ShellRequester by)
{
	return run_with_input(command, /*input=*/NULL, by);
}

int
run_with_input(char command[], FILE *input, ShellRequester by)
{
	int pid;
	int result;
	extern char **environ;
	struct sigaction new, old;

	if(command == NULL)
		return 1;

	new.sa_handler = SIG_DFL;
	sigemptyset(&new.sa_mask);
	new.sa_flags = SA_RESTART;
	sigaction(SIGTSTP, &new, &old);

	if(input != NULL)
	{
		fflush(input);
	}

	pid = fork();
	if(pid == -1)
	{
		sigaction(SIGTSTP, &old, NULL);
		return -1;
	}
	if(pid == 0)
	{
		signal(SIGTSTP, SIG_DFL);
		signal(SIGINT, SIG_DFL);

		prepare_for_exec();

		if(input != NULL)
		{
			rewind(input);

			if(dup2(fileno(input), STDIN_FILENO) == -1)
			{
				LOG_SERROR_MSG(errno, "Failed to duplicate stdin for `%s`", command);
				_Exit(127);
			}

			fclose(input);
		}

		char *sh_flag = (by == SHELL_BY_USER ? cfg.shell_cmd_flag : "-c");
		execve(get_execv_path(cfg.shell),
				make_execv_array(cfg.shell, sh_flag, command), environ);
		LOG_SERROR_MSG(errno, "Failed to launch a shell: `%s` `%s` `%s`", cfg.shell,
				sh_flag, command);
		_Exit(127);
	}

	result = get_proc_exit_status(pid, &no_cancellation);

	sigaction(SIGTSTP, &old, NULL);

	return result;
}

void
recover_after_shellout(void)
{
	if(curr_stats.load_stage > 0)
	{
		reset_prog_mode();
		doupdate();
	}
}

void
wait_for_data_from(pid_t pid, FILE *f, int fd,
		const cancellation_t *cancellation)
{
	const struct timeval ts_init = { .tv_sec = 0, .tv_usec = 1000 };
	struct timeval ts;
	int select_result;

	fd_set read_ready;
	FD_ZERO(&read_ready);

	fd = (f != NULL) ? fileno(f) : fd;

	do
	{
		process_cancel_request(pid, cancellation);
		ts = ts_init;
		FD_SET(fd, &read_ready);
		select_result = select(fd + 1, &read_ready, NULL, NULL, &ts);
	}
	while(select_result == 0 || (select_result == -1 && errno == EINTR));
	process_cancel_request(pid, cancellation);
}

void
block_all_thread_signals(void)
{
	sigset_t set;
	sigfillset(&set);
	pthread_sigmask(SIG_SETMASK, &set, NULL);
}

/* Checks whether cancelling of current operation is requested and sends SIGINT
 * to process specified by its process id to request cancellation. */
static void
process_cancel_request(pid_t pid, const cancellation_t *cancellation)
{
	if(cancellation_requested(cancellation))
	{
		if(kill(pid, SIGINT) != 0)
		{
			LOG_SERROR_MSG(errno, "Failed to send SIGINT to %" PRINTF_ULL,
					(unsigned long long)pid);
		}
	}
}

int
get_proc_exit_status(pid_t pid, const cancellation_t *cancellation)
{
	while(1)
	{
		int status;
		if(waitpid(pid, &status, 0) == -1)
		{
			if(errno == EINTR)
			{
				process_cancel_request(pid, cancellation);
				continue;
			}
			LOG_SERROR_MSG(errno, "waitpid(%" PRINTF_ULL ") has failed",
					(unsigned long long)pid);
			break;
		}

		if(WIFEXITED(status) || WIFSIGNALED(status))
		{
			return status;
		}
	}
	return -1;
}

void _gnuc_noreturn
run_from_fork(int pipe[2], int err_only, int preserve_stdin, char cmd[],
		ShellRequester by)
{
	/* Close read end of the pipe. */
	(void)close(pipe[0]);

	/* Redirect stderr and maybe stdout to write end of the pipe. */
	if(dup2(pipe[1], STDERR_FILENO) == -1)
	{
		_Exit(EXIT_FAILURE);
	}
	if(!err_only && dup2(pipe[1], STDOUT_FILENO) == -1)
	{
		_Exit(EXIT_FAILURE);
	}

	if(pipe[1] != STDERR_FILENO && pipe[1] != STDOUT_FILENO)
	{
		/* Close write end of the pipe after it was duplicated. */
		(void)close(pipe[1]);
	}

	const int null_fd = open("/dev/null", O_RDWR);
	if(null_fd == -1)
	{
		_Exit(EXIT_FAILURE);
	}

	if(!preserve_stdin && dup2(null_fd, STDIN_FILENO) == -1)
	{
		_Exit(EXIT_FAILURE);
	}
	if(err_only && dup2(null_fd, STDOUT_FILENO) == -1)
	{
		_Exit(EXIT_FAILURE);
	}

	if(null_fd != STDIN_FILENO && null_fd != STDOUT_FILENO)
	{
		(void)close(null_fd);
	}

	prepare_for_exec();
	char *sh_flag = (by == SHELL_BY_USER ? cfg.shell_cmd_flag : "-c");
	execvp(get_execv_path(cfg.shell), make_execv_array(cfg.shell, sh_flag, cmd));
	LOG_SERROR_MSG(errno, "Failed to launch a shell: `%s` `%s` `%s`", cfg.shell,
			sh_flag, cmd);
	_Exit(127);
}

void
prepare_for_exec(void)
{
	if(curr_stats.original_stdout != NULL)
	{
		(void)fclose(curr_stats.original_stdout);
	}

	/* More generic cleanup functions aren't called here to do not waste time on
	 * freeing resources which will be replaced by exec(). */
	tab_info_t tab_info;
	int i;
	for(i = 0; tabs_enum_all(i, &tab_info); ++i)
	{
		view_t *view = tab_info.view;
		fswatch_free(view->watch);
		fswatch_free(view->left_column.watch);
		fswatch_free(view->right_column.watch);
	}
}

char *
get_execv_path(char shell[])
{
	static char name[NAME_MAX + 1];
	(void)extract_cmd_name(shell, 0, sizeof(name), name);
	return name;
}

char **
make_execv_array(char shell[], char shell_flag[], char cmd[])
{
#ifdef HAVE_MAX_ARG_STRLEN
#ifndef PAGE_SIZE
#define PAGE_SIZE sysconf(_SC_PAGESIZE)
#endif
	/* Don't use maximum length, leave some room or commands fail to run. */
	const size_t safe_arg_len = MIN(MAX_ARG_STRLEN, MAX_ARG_STRLEN - 4096);
	const size_t npieces = DIV_ROUND_UP(strlen(cmd), safe_arg_len);
#else
	/* Actual value doesn't matter in this case. */
	const size_t safe_arg_len = 1;
	/* Don't break anything if Linux-specific MAX_ARG_STRLEN macro isn't
	 * defined. */
	const size_t npieces = 1U;
#endif

	char name[NAME_MAX + 1];

	char **args = reallocarray(NULL, 5 + npieces + 1, sizeof(*args));
	char *eval_cmd;
	size_t len;
	size_t i;

	char *const sh_arg = extract_cmd_name(shell, 0, sizeof(name), name);
	const int with_sh_arg = (*sh_arg != '\0');

	/* Don't use eval hack unless necessary. */
	if(npieces == 1)
	{
		i = 0U;
		args[i++] = shell;
		if(with_sh_arg)
		{
			args[i++] = sh_arg;
		}
		args[i++] = shell_flag;
		if(cmd[0] == '-')
		{
			args[i++] = "--";
		}
		args[i++] = cmd;
		args[i++] = NULL;
		return args;
	}

	/* Break very long commands into pieces and run like this:
	 * sh -c 'eval "${1}${2}...${10}...${N}"' sh quoted-substring1 ...
	 * see https://lists.gnu.org/archive/html/bug-make/2009-07/msg00012.html */
	eval_cmd = NULL;
	len = 0U;
	(void)strappend(&eval_cmd, &len, (cmd[0] == '-' ? "eval -- \"" : "eval \""));
	for(i = 0; i < npieces; ++i)
	{
		char s[32];
		char c;

		snprintf(s, sizeof(s), "$%d", (int)i);
		(void)strappend(&eval_cmd, &len, s);

		c = cmd[safe_arg_len];
		cmd[safe_arg_len] = '\0';

		args[(with_sh_arg ? 4 : 3) + i] = strdup(cmd);

		cmd[safe_arg_len] = c;
		cmd += safe_arg_len;
	}
	(void)strappend(&eval_cmd, &len, "\"");

	int j = 0;
	args[j++] = shell;
	if(with_sh_arg)
	{
		args[j++] = sh_arg;
	}
	args[j++] = shell_flag;
	args[j++] = eval_cmd;
	args[j + npieces] = NULL;

	return args;
}

void
get_perm_string(char buf[], int len, mode_t mode)
{
	static const char *const perm_sets[] =
	{ "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" };
	int u, g, o;

	u = (mode & S_IRWXU) >> 6;
	g = (mode & S_IRWXG) >> 3;
	o = (mode & S_IRWXO);

	snprintf(buf, len, "-%s%s%s", perm_sets[u], perm_sets[g], perm_sets[o]);

	if(S_ISLNK(mode))
		buf[0] = 'l';
	else if(S_ISDIR(mode))
		buf[0] = 'd';
	else if(S_ISBLK(mode))
		buf[0] = 'b';
	else if(S_ISCHR(mode))
		buf[0] = 'c';
	else if(S_ISFIFO(mode))
		buf[0] = 'p';
	else if(S_ISSOCK(mode))
		buf[0] = 's';

	if(mode & S_ISVTX)
		buf[9] = (buf[9] == '-') ? 'T' : 't';
	if(mode & S_ISGID)
		buf[6] = (buf[6] == '-') ? 'S' : 's';
	if(mode & S_ISUID)
		buf[3] = (buf[3] == '-') ? 'S' : 's';
}

int
refers_to_slower_fs(const char from[], const char to[])
{
	const int i = find_path_prefix_index(to, cfg.slow_fs_list);
	/* When destination is not on slow file system, no performance penalties are
	 * expected. */
	if(i == -1)
	{
		return 0;
	}

	/* Otherwise, same slowdown as we have for the source location is bearable. */
	return find_path_prefix_index(from, cfg.slow_fs_list) != i;
}

int
is_on_slow_fs(const char full_path[], const char slowfs_specs[])
{
	char fs_name[PATH_MAX + 1];
	get_mount_point_traverser_state state = {
		.type = MI_FS_TYPE,
		.path = full_path,
		.buf_len = sizeof(fs_name),
		.buf = fs_name,
		.curr_len = 0UL,
	};

	/* Empty list optimization. */
	if(slowfs_specs[0] == '\0')
	{
		return 0;
	}

	/* If slowfs equals "*" then all file systems are considered slow.  On cygwin
	 * obtaining list of mounts from /etc/mtab, which is linked to /proc/mounts,
	 * is very slow in presence of network drives. */
	if(strcmp(slowfs_specs, "*") == 0)
	{
		return 1;
	}

	if(traverse_mount_points(&get_mount_info_traverser, &state) == 0)
	{
		if(state.curr_len > 0)
		{
			if(starts_with_list_item(fs_name, slowfs_specs))
			{
				return 1;
			}
		}
	}

	return find_path_prefix_index(full_path, slowfs_specs) != -1;
}

int
get_mount_point(const char path[], size_t buf_len, char buf[])
{
	get_mount_point_traverser_state state = {
		.type = MI_MOUNT_POINT,
		.path = path,
		.buf_len = buf_len,
		.buf = buf,
		.curr_len = 0UL,
	};
	return traverse_mount_points(&get_mount_info_traverser, &state);
}

/* traverse_mount_points client that gets mount point info for a given path. */
static int
get_mount_info_traverser(struct mntent *entry, void *arg)
{
	get_mount_point_traverser_state *const state = arg;
	if(path_starts_with(state->path, entry->mnt_dir))
	{
		const size_t new_len = strlen(entry->mnt_dir);
		if(new_len > state->curr_len)
		{
			state->curr_len = new_len;
			switch(state->type)
			{
				case MI_MOUNT_POINT:
					copy_str(state->buf, state->buf_len, entry->mnt_dir);
					break;
				case MI_FS_TYPE:
					copy_str(state->buf, state->buf_len, entry->mnt_type);
					break;

				default:
					assert(0 && "Unknown mount information type.");
					break;
			}
		}
	}
	return 0;
}

int
traverse_mount_points(mptraverser client, void *arg)
{
	/* Cached mount entries, updated only when /etc/mtab changes. */
	static filemon_t mtab_mon;
	static struct mntent *entries;
	static unsigned int nentries;

	filemon_t mon;
	unsigned int i;

	/* Check for cache validity. */
	if(filemon_from_file("/etc/mtab", FMT_MODIFIED, &mon) != 0 ||
			!filemon_equal(&mon, &mtab_mon))
	{
		mtab_mon = mon;
		free_mnt_entries(entries, nentries);
		entries = read_mnt_entries(&nentries);
	}

	if(nentries == 0U)
	{
		return 1;
	}

	for(i = 0; i < nentries; ++i)
	{
		if(client(&entries[i], arg))
		{
			break;
		}
	}

	return 0;
}

/* Frees array of mount entries. */
static void
free_mnt_entries(struct mntent *entries, unsigned int nentries)
{
	unsigned int i;

	for(i = 0; i < nentries; ++i)
	{
		free_mnt_entry(&entries[i]);
	}

	free(entries);
}

/* Reads in array of mount entries.  Always sets *nentries.  Returns the array,
 * which might be NULL if empty.  On memory allocation error, skips entries. */
static struct mntent *
read_mnt_entries(unsigned int *nentries)
{
	FILE *f;
	struct mntent *entries = NULL;
	struct mntent *ent;

	*nentries = 0U;

	if((f = setmntent("/etc/mtab", "r")) == NULL)
	{
		return NULL;
	}

	while((ent = getmntent(f)) != NULL)
	{
		void *p = reallocarray(entries, *nentries + 1, sizeof(*entries));
		if(p != NULL)
		{
			entries = p;
			if(clone_mnt_entry(&entries[*nentries], ent) == 0)
			{
				++*nentries;
			}
		}
	}

	(void)endmntent(f);

	return entries;
}

/* Clones *rhs mount entry into *lhs, which is assumed to do not contain data.
 * Returns zero on success, otherwise non-zero is returned. */
static int
clone_mnt_entry(struct mntent *lhs, const struct mntent *rhs)
{
	lhs->mnt_fsname = strdup(rhs->mnt_fsname);
	lhs->mnt_dir = strdup(rhs->mnt_dir);
	lhs->mnt_type = strdup(rhs->mnt_type);
	lhs->mnt_opts = strdup(rhs->mnt_opts);
	lhs->mnt_freq = rhs->mnt_freq;
	lhs->mnt_passno = rhs->mnt_passno;

	if(lhs->mnt_fsname == NULL || lhs->mnt_dir == NULL || lhs->mnt_type == NULL ||
			lhs->mnt_opts == NULL)
	{
		free_mnt_entry(lhs);
		return 1;
	}

	return 0;
}

/* Frees single mount entry. */
static void
free_mnt_entry(struct mntent *entry)
{
	free(entry->mnt_fsname);
	free(entry->mnt_dir);
	free(entry->mnt_type);
	free(entry->mnt_opts);
}

/* Checks that the str has at least one of comma separated list (the list) items
 * as a prefix.  Returns non-zero if so, otherwise zero is returned. */
static int
starts_with_list_item(const char str[], const char list[])
{
	char *const list_copy = strdup(list);

	char *prefix = list_copy, *state = NULL;
	while((prefix = split_and_get(prefix, ',', &state)) != NULL)
	{
		if(starts_with(str, prefix))
		{
			break;
		}
	}

	free(list_copy);

	return prefix != NULL;
}

/* Finds such element of comma separated list of paths (the list) that the path
 * is prefixed with it.  Returns the index or -1 on failure. */
static int
find_path_prefix_index(const char path[], const char list[])
{
	char *const list_copy = strdup(list);

	char *prefix = list_copy, *state = NULL;
	int i = 0;
	while((prefix = split_and_get(prefix, ',', &state)) != NULL)
	{
		if(path_starts_with(path, prefix))
		{
			break;
		}
		++i;
	}

	free(list_copy);

	return (prefix == NULL) ? -1 : i;
}

unsigned int
get_pid(void)
{
	return getpid();
}

int
get_uid(const char user[], uid_t *uid)
{
	if(isdigit(user[0]))
	{
		*uid = atoi(user);
	}
	else
	{
		struct passwd *p;

		p = getpwnam(user);
		if(p == NULL)
			return 1;

		*uid = p->pw_uid;
	}
	return 0;
}

int
get_gid(const char group[], gid_t *gid)
{
	if(isdigit(group[0]))
	{
		*gid = atoi(group);
	}
	else
	{
		struct group *g;

		g = getgrnam(group);
		if(g == NULL)
			return 1;

		*gid = g->gr_gid;
	}
	return 0;
}

int
status_to_exit_code(int status)
{
	if(status == -1)
	{
		return -127;
	}
	if(WIFEXITED(status))
	{
		return WEXITSTATUS(status);
	}
	if(WIFSIGNALED(status))
	{
		return -WTERMSIG(status);
	}
	return -128;
}

int
S_ISEXE(mode_t mode)
{
	return ((S_IXUSR | S_IXGRP | S_IXOTH) & mode);
}

int
executable_exists(const char path[])
{
	return os_access(path, X_OK) == 0 && !is_dir(path);
}

int
get_exe_dir(char dir_buf[], size_t dir_buf_len)
{
	/* This operation isn't supported on *nix-like operating systems. */
	return 1;
}

EnvType
get_env_type(void)
{
	return ET_UNIX;
}

ExecEnvType
get_exec_env_type(void)
{
	const char *const term = env_get("TERM");
	if(term != NULL && ends_with(term, "linux"))
	{
		return EET_LINUX_NATIVE;
	}
	else
	{
		const char *display = env_get("DISPLAY");
		const char *wayland_display = env_get("WAYLAND_DISPLAY");
		return is_null_or_empty(display) && is_null_or_empty(wayland_display)
		     ? EET_EMULATOR
		     : EET_EMULATOR_WITH_X;
	}
}

ShellType
get_shell_type(const char shell_cmd[])
{
	return ST_POSIX;
}

char *
shell_arg_escape(const char what[], ShellType shell_type)
{
	assert(shell_type == ST_POSIX && "Unexpected shell type.");
	return posix_like_escape(what, /*type=*/0);
}

int
format_help_cmd(char cmd[], size_t cmd_size)
{
	int bg;
	char *const escaped = posix_like_escape(get_installed_data_dir(), /*type=*/0);
	snprintf(cmd, cmd_size, "%s %s/" VIFM_HELP, cfg_get_vicmd(&bg), escaped);
	free(escaped);
	return bg;
}

void
display_help(const char cmd[])
{
	(void)rn_shell(cmd, PAUSE_ON_ERROR, 1, SHELL_BY_APP);
}

void
wait_for_signal(void)
{
	pause();
}

void
stop_process(void)
{
	struct sigaction new, old;

	new.sa_handler = SIG_DFL;
	sigemptyset(&new.sa_mask);
	new.sa_flags = SA_RESTART;
	sigaction(SIGTSTP, &new, &old);

	kill(0, SIGTSTP);

	sigaction(SIGTSTP, &old, NULL);
}

void
update_terminal_settings(void)
{
	/* Do nothing. */
}

void
get_uid_string(const dir_entry_t *entry, int as_num, size_t buf_len, char buf[])
{
	/* Cache for the last user id requested in symbolic form. */
	static uid_t last_uid = (uid_t)-1;
	static char uid_buf[26];

	/* Numeric form doesn't need caching. */
	if(as_num)
	{
		snprintf(buf, buf_len, "%d", (int)entry->uid);
		return;
	}

	if(entry->uid == last_uid)
	{
		copy_str(buf, buf_len, uid_buf);
		return;
	}

	/* Numeric form is a fallback for the case of an unknown user id. */
	last_uid = entry->uid;
	snprintf(uid_buf, sizeof(uid_buf), "%d", (int)last_uid);

	enum { MAX_TRIES = 4 };
	size_t size = MAX(sysconf(_SC_GETPW_R_SIZE_MAX) + 1, PATH_MAX);
	int i;
	for(i = 0; i < MAX_TRIES; ++i, size *= 2)
	{
		char buf[size];
		struct passwd pwd_b;
		struct passwd *pwd_buf;

		if(getpwuid_r(last_uid, &pwd_b, buf, sizeof(buf), &pwd_buf) == 0 &&
				pwd_buf != NULL)
		{
			copy_str(uid_buf, sizeof(uid_buf), pwd_buf->pw_name);
			break;
		}
	}

	copy_str(buf, buf_len, uid_buf);
}

void
get_gid_string(const dir_entry_t *entry, int as_num, size_t buf_len, char buf[])
{
	/* Cache for the last group id requested in symbolic form. */
	static gid_t last_gid = (gid_t)-1;
	static char gid_buf[26];

	/* Numeric form doesn't need caching. */
	if(as_num)
	{
		snprintf(buf, buf_len, "%d", (int)entry->gid);
		return;
	}

	if(entry->gid == last_gid)
	{
		copy_str(buf, buf_len, gid_buf);
		return;
	}

	/* Numeric form is a fallback for the case of an unknown group id. */
	last_gid = entry->gid;
	snprintf(gid_buf, sizeof(gid_buf), "%d", (int)last_gid);

	enum { MAX_TRIES = 4 };
	size_t size = MAX(sysconf(_SC_GETGR_R_SIZE_MAX) + 1, PATH_MAX);
	int i;
	for(i = 0; i < MAX_TRIES; ++i, size *= 2)
	{
		char buf[size];
		struct group group_b;
		struct group *group_buf;

		if(getgrgid_r(last_gid, &group_b, buf, sizeof(buf), &group_buf) == 0 &&
				group_buf != NULL)
		{
			copy_str(gid_buf, sizeof(gid_buf), group_buf->gr_name);
			break;
		}
	}

	copy_str(buf, buf_len, gid_buf);
}

FILE *
reopen_term_stdout(void)
{
	FILE *fp;
	int outfd, ttyfd;

	outfd = dup(STDOUT_FILENO);
	if(outfd == -1)
	{
		fprintf(stderr, "Failed to store original output stream.\n");
		return NULL;
	}

	fp = fdopen(outfd, "w");
	if(fp == NULL)
	{
		close(outfd);
		fprintf(stderr, "Failed to open original output stream.\n");
		return NULL;
	}

	ttyfd = open_tty();
	if(ttyfd == -1)
	{
		fprintf(stderr, "Failed to open terminal for output: %s\n",
				strerror(errno));
		fclose(fp);
		return NULL;
	}
	if(dup2(ttyfd, STDOUT_FILENO) == -1)
	{
		close(ttyfd);
		fclose(fp);
		fprintf(stderr, "Failed to setup terminal as standard output stream.\n");
		return NULL;
	}

	close(ttyfd);
	return fp;
}

int
reopen_term_stdin(void)
{
	int ttyfd;

	if(close(STDIN_FILENO))
	{
		fprintf(stderr, "Failed to close original input stream.\n");
		return 1;
	}

	ttyfd = open_tty();
	if(ttyfd != STDIN_FILENO)
	{
		if(ttyfd != -1)
		{
			close(ttyfd);
		}

		fprintf(stderr, "Failed to open terminal for input.\n");
		return 1;
	}

	return 0;
}

/* Opens controlling terminal doing its best at guessing its name.  This might
 * not work if all the standard streams are redirected, in which case /dev/tty
 * is used, which can't be passed around between processes, so some applications
 * won't work in this (quite rare) case.  Returns opened file descriptor or -1
 * on error. */
static int
open_tty(void)
{
	int fd = -1;

	/* XXX: we might be better off duplicating descriptor if possible. */

	if(isatty(STDIN_FILENO))
	{
		fd = open(ttyname(STDIN_FILENO), O_RDWR);
	}
	if(fd == -1 && isatty(STDOUT_FILENO))
	{
		fd = open(ttyname(STDOUT_FILENO), O_RDWR);
	}
	if(fd == -1 && isatty(STDERR_FILENO))
	{
		fd = open(ttyname(STDERR_FILENO), O_RDWR);
	}

	if(fd == -1)
	{
		fd = open("/dev/tty", O_RDWR);
	}

	return fd;
}

FILE *
read_cmd_output(const char cmd[], int preserve_stdin)
{
	FILE *fp;
	pid_t pid;
	int out_pipe[2];

	if(pipe(out_pipe) != 0)
	{
		return NULL;
	}

	pid = fork();
	if(pid == (pid_t)-1)
	{
		return NULL;
	}

	if(pid == 0)
	{
		run_from_fork(out_pipe, 0, preserve_stdin, (char *)cmd, SHELL_BY_USER);
		return NULL;
	}

	/* Close write end of pipe. */
	close(out_pipe[1]);

	fp = fdopen(out_pipe[0], "r");
	if(fp == NULL)
	{
		close(out_pipe[0]);
	}
	return fp;
}

const char *
get_installed_data_dir(void)
{
	static char data_dir[PATH_MAX + 1];
	if(data_dir[0] == '\0')
	{
		const char *prefix = env_get_def("VIFM_APPDIR_ROOT", "");
		snprintf(data_dir, sizeof(data_dir), "%s%s", prefix, PACKAGE_DATA_DIR);
	}
	return data_dir;
}

const char *
get_sys_conf_dir(int idx)
{
	static char conf_dir[PATH_MAX + 1];
	static int init_done;

	if(!init_done)
	{
		init_done = 1;

		const char *prefix = env_get("VIFM_APPDIR_ROOT");
		if(!is_null_or_empty(prefix))
		{
			int written = snprintf(conf_dir, sizeof(conf_dir), "%s%s", prefix,
					PACKAGE_SYSCONF_DIR);
			if(written < 0 || written >= (int)sizeof(conf_dir))
			{
				conf_dir[0] = '\0';
			}
		}
	}

	if(conf_dir[0] == '\0')
	{
		/* Offset indices by one to skip AppImage configuration directory which
		 * isn't present. */
		++idx;
	}

	if(idx == 0)
	{
		return conf_dir;
	}
	if(idx == 1)
	{
		return PACKAGE_SYSCONF_DIR;
	}

	return NULL;
}

void
clone_attribs(const char path[], const char from[], const struct stat *st)
{
	if(chown(path, st->st_uid, st->st_gid) != 0)
	{
		LOG_SERROR_MSG(errno, "Failed to chown `%s`", path);
	}
	clone_timestamps(path, from, st);
	clone_xattrs(path, from);
}

/* Clones timestamps from file specified by from to file at path. */
static void
clone_timestamps(const char path[], const char from[], const struct stat *st)
{
#if defined(HAVE_STRUCT_STAT_ST_MTIM) && defined(HAVE_FUTIMENS) && \
    defined(HAVE_CONSISTENT_TIMESPEC)
	const int fd = open(path, O_WRONLY);
	if(fd != -1)
	{
		struct timespec ts[2] = { st->st_atim, st->st_mtim };
		if(futimens(fd, ts) == 0)
		{
			close(fd);
			return;
		}
		close(fd);
		/* Not sure how useful this is, but we can try both functions in a row. */
	}
#endif

	struct timeval tv[2];
#ifdef HAVE_STRUCT_STAT_ST_MTIM
	tv[0].tv_sec = st->st_atim.tv_sec;
	tv[0].tv_usec = st->st_atim.tv_nsec/1000;
	tv[1].tv_sec = st->st_mtim.tv_sec;
	tv[1].tv_usec = st->st_mtim.tv_nsec/1000;
#else
	tv[0].tv_sec = st->st_atime;
	tv[0].tv_usec = 0;
	tv[1].tv_sec = st->st_mtime;
	tv[1].tv_usec = 0;
#endif
	utimes(path, tv);
}

/* Clones extended attributes from file specified by from to file at path. */
static void
clone_xattrs(const char path[], const char from[])
{
#ifdef HAVE_XATTRS
	char empty[1];

	ssize_t list_size = llistxattr(from, empty, 0U);
	if(list_size < 0)
	{
		return;
	}

	/* Size of VLA should never be zero. */
	char list[list_size + 1];
	list_size = llistxattr(from, list, list_size);
	if(list_size < 0)
	{
		return;
	}

	const char *name = list;
	while(name - list < list_size)
	{
		ssize_t value_size = lgetxattr(from, name, empty, 0U);
		if(value_size < 0)
		{
			break;
		}

		/* Size of VLA should never be zero. */
		char value[value_size + 1];
		value_size = lgetxattr(from, name, value, value_size);
		if(value_size < 0)
		{
			break;
		}

		if(lsetxattr(path, name, value, value_size, XATTR_CREATE) != 0)
		{
			if(errno == ENOTSUP)
			{
				/* Mount of the destination doesn't support extended attributes. */
				break;
			}
		}

		name += strlen(name) + 1U;
	}
#endif
}

int
get_drive_info(const char at[], uint64_t *total_bytes, uint64_t *free_bytes)
{
	struct statvfs st;
	if(statvfs(at, &st) != 0)
	{
		return -1;
	}

	*total_bytes = st.f_blocks*st.f_frsize;

#ifdef __linux__
	/* Linux didn't have `f_frsize` until 2.6 and still uses `f_bsize` as a unit
	 * for a bunch of fields.  `f_bsize` is essentially always equal to
	 * `f_frsize`.
	 *
	 * `f_frsize` should be less or equal to `f_bsize`, but not sure it's safe to
	 * use that instead of an #ifdef. */
	const uint64_t block_size = st.f_bsize;
#else
	const uint64_t block_size = st.f_frsize;
#endif
	*free_bytes = st.f_bavail*block_size;

	return 0;
}

uint64_t
get_true_inode(const dir_entry_t *entry)
{
	if(entry->type != FT_LINK || entry->slow_target)
	{
		return entry->inode;
	}

	char full_path[PATH_MAX + 1];
	get_full_path_of(entry, sizeof(full_path), full_path);

	struct stat s;
	if(os_stat(full_path, &s) == 0)
	{
		return s.st_ino;
	}
	return entry->inode;
}

void
bind_pipe_or_die(int fd, int pipe_end, int pipe_other)
{
	if(dup2(pipe_end, fd) == -1)
	{
		perror("dup2");
		_Exit(EXIT_FAILURE);
	}

	/* Close original input pipe descriptors. */
	if(pipe_end != fd)
	{
		close(pipe_end);
	}
	close(pipe_other);
}

int
create_new_file(const char path[], mode_t mode, int auto_delete)
{
	int fd = open(path, O_RDWR | O_CREAT | O_EXCL, mode);
	if(fd == -1)
	{
		return -1;
	}

	if(auto_delete && unlink(path) != 0)
	{
		/* Something weird has happened. */
		int error = errno;
		(void)close(fd);
		errno = error;
		return -1;
	}

	return fd;
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
