/* 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 "fs.h"

#ifdef _WIN32
#include <aclapi.h>
#include <windows.h>
#include <ntdef.h>
#include <winioctl.h>

#include "utf8.h"
#endif

#include <sys/stat.h> /* S_* statbuf */
#include <sys/types.h> /* size_t mode_t */
#include <unistd.h> /* pathconf() readlink() */

#include <ctype.h> /* isalpha() */
#include <errno.h> /* EINVAL ERANGE errno */
#include <stddef.h> /* NULL */
#include <stdio.h> /* snprintf() remove() */
#include <stdlib.h> /* free() */
#include <string.h> /* strcpy() strdup() strlen() strncmp() strncpy() */

#include "../compat/dtype.h"
#include "../compat/fs_limits.h"
#include "../compat/os.h"
#include "../io/iop.h"
#include "log.h"
#include "path.h"
#include "str.h"
#include "string_array.h"
#include "utils.h"

static int is_dir_fast(const char path[]);
static int path_exists_internal(const char path[], const char filename[],
		int deref);

#ifndef _WIN32
static int is_directory(const char path[], int dereference_links);
#endif

int
is_dir(const char path[])
{
	if(is_dir_fast(path))
	{
		return 1;
	}

#ifndef _WIN32
	return is_directory(path, 1);
#else
	{
		const DWORD attrs = win_get_file_attrs(path);
		return attrs != INVALID_FILE_ATTRIBUTES
		    && (attrs & FILE_ATTRIBUTE_DIRECTORY);
	}
#endif
}

/* Checks if path is an existing directory faster than is_dir() does this at the
 * cost of less accurate results (fails on non-sufficient rights).
 * Automatically dereferences symbolic links.  Returns non-zero if path points
 * to a directory, otherwise zero is returned. */
static int
is_dir_fast(const char path[])
{
#ifdef __linux__
	/* Optimization idea: is_dir() ends up using stat() call, which in turn has
	 * to:
	 *  1) resolve path to an inode number;
	 *  2) find and load inode for the directory.
	 * Checking "path/." for existence is a "hack" to omit 2).
	 * Negative answer of this method doesn't guarantee directory absence, but
	 * positive answer provides correct answer faster than is_dir() would. */

	const size_t len = strlen(path);
	char path_to_selfref[len + 1 + 1 + 1];

	strcpy(path_to_selfref, path);
	path_to_selfref[len] = '/';
	path_to_selfref[len + 1] = '.';
	path_to_selfref[len + 2] = '\0';

	return os_access(path_to_selfref, F_OK) == 0;
#else
	/* Other systems report that "/path/to/file/." is a directory either for all
	 * files or for executable ones, always or sometimes... */
	return 0;
#endif
}

int
is_dir_empty(const char path[])
{
	DIR *dir;
	struct dirent *d;

	dir = os_opendir(path);
	if(dir == NULL)
	{
		return 1;
	}

	while((d = os_readdir(dir)) != NULL)
	{
		if(!is_builtin_dir(d->d_name))
		{
			break;
		}
	}
	os_closedir(dir);

	return d == NULL;
}

int
is_valid_dir(const char *path)
{
	return is_dir(path) || is_unc_root(path);
}

int
path_exists(const char path[], int deref)
{
	if(!is_path_absolute(path))
	{
		LOG_ERROR_MSG("Passed relative path where absolute one is expected: %s",
				path);
	}
	return path_exists_internal(NULL, path, deref);
}

int
path_exists_at(const char path[], const char filename[], int deref)
{
	if(is_path_absolute(filename))
	{
		LOG_ERROR_MSG("Passed absolute path where relative one is expected: %s",
				filename);
		path = NULL;
	}
	return path_exists_internal(path, filename, deref);
}

/* Checks whether path/file exists. If path is NULL, filename is assumed to
 * contain full path. */
static int
path_exists_internal(const char path[], const char filename[], int deref)
{
	char full[PATH_MAX + 1];
	if(path == NULL)
	{
		copy_str(full, sizeof(full), filename);
	}
	else
	{
		build_path(full, sizeof(full), path, filename);
	}

	/* At least on Windows extra trailing slash can mess up the check, so get rid
	 * of it. */
	if(!is_root_dir(full))
	{
		chosp(full);
	}

	if(!deref)
	{
		struct stat st;
		return os_lstat(full, &st) == 0;
	}
	return os_access(full, F_OK) == 0;
}

int
paths_are_same(const char s[], const char t[])
{
	char s_real[PATH_MAX + 1];
	char t_real[PATH_MAX + 1];

	if(os_realpath(s, s_real) != s_real || os_realpath(t, t_real) != t_real)
	{
		return paths_are_equal(s, t);
	}
	return (stroscmp(s_real, t_real) == 0);
}

int
is_symlink(const char path[])
{
#ifndef _WIN32
	struct stat st;
	return os_lstat(path, &st) == 0 && S_ISLNK(st.st_mode);
#else
	return (win_get_reparse_point_type(path) == IO_REPARSE_TAG_SYMLINK);
#endif
}

int
is_shortcut(const char path[])
{
#ifndef _WIN32
	return 0;
#else
	return (strcasecmp(get_ext(path), "lnk") == 0);
#endif
}

SymLinkType
get_symlink_type(const char path[])
{
	char cwd[PATH_MAX + 1];
	char linkto[PATH_MAX + NAME_MAX];
	int saved_errno;
	char *filename_copy;
	char *p;

	if(get_cwd(cwd, sizeof(cwd)) == NULL)
	{
		/* getcwd() failed, just use "." rather than fail. */
		strcpy(cwd, ".");
	}

	/* Use readlink() (in get_link_target_abs) before realpath() to check for
	 * target at slow file system.  realpath() doesn't fit in this case as it
	 * resolves chains of symbolic links and we want to try only the first one. */
	if(get_link_target_abs(path, cwd, linkto, sizeof(linkto)) != 0)
	{
		LOG_SERROR_MSG(errno, "Can't readlink \"%s\"", path);
		log_cwd();
		return SLT_UNKNOWN;
	}
	if(refers_to_slower_fs(path, linkto))
	{
		return SLT_SLOW;
	}

	filename_copy = strdup(path);
	chosp(filename_copy);

	p = os_realpath(filename_copy, linkto);
	saved_errno = errno;

	free(filename_copy);

	if(p == linkto)
	{
		return is_dir(linkto) ? SLT_DIR : SLT_UNKNOWN;
	}

	LOG_SERROR_MSG(saved_errno, "Can't realpath \"%s\"", path);
	log_cwd();
	return SLT_UNKNOWN;
}

int
get_link_target_abs(const char link[], const char cwd[], char buf[],
		size_t buf_len)
{
	char link_target[PATH_MAX + 1];
	if(get_link_target(link, link_target, sizeof(link_target)) != 0)
	{
		return 1;
	}
	if(is_path_absolute(link_target))
	{
		strncpy(buf, link_target, buf_len);
		buf[buf_len - 1] = '\0';
	}
	else
	{
		build_path(buf, buf_len, cwd, link_target);
	}
	return 0;
}

int
get_link_target(const char link[], char buf[], size_t buf_len)
{
#ifndef _WIN32
	char *filename;
	ssize_t len;

	if(buf_len == 0)
	{
		return -1;
	}

	filename = strdup(link);
	chosp(filename);

	len = readlink(filename, buf, buf_len - 1);

	free(filename);

	if(len == -1)
	{
		return -1;
	}

	buf[len] = '\0';
	return 0;
#else
	if(win_symlink_read(link, buf, buf_len) == 0)
	{
		return 0;
	}
	return win_shortcut_read(link, buf, buf_len);
#endif
}

int
make_path(const char dir_name[], mode_t mode)
{
	io_args_t args = {
		.arg1.path = dir_name,
		.arg2.process_parents = 1,
		.arg3.mode = mode,
	};

	return (iop_mkdir(&args) == IO_RES_SUCCEEDED ? 0 : 1);
}

int
create_path(const char dir_name[], mode_t mode)
{
	return is_dir(dir_name) ? 1 : make_path(dir_name, mode);
}

int
symlinks_available(void)
{
#ifndef _WIN32
	return 1;
#else
	return is_vista_and_above();
#endif
}

int
has_atomic_file_replace(void)
{
#ifndef _WIN32
	return 1;
#else
	return 0;
#endif
}

int
directory_accessible(const char path[])
{
	return os_access(path, X_OK) == 0 || is_unc_root(path);
}

int
is_dir_writable(const char path[])
{
	if(is_unc_root(path))
	{
		return 0;
	}

#ifndef _WIN32
	return (os_access(path, W_OK) == 0);
#else
	wchar_t *utf16_path = utf8_to_utf16(path);
	if(utf16_path == NULL)
	{
		return 0;
	}

	PSECURITY_DESCRIPTOR sec_descr;

	/* Requesting owner and group information seems to be necessary as well.
	 * There is also GetFileSecurityW(), but the biggest difference is that it
	 * won't allocate buffer for us. */
	DWORD ret = GetNamedSecurityInfoW(utf16_path, SE_FILE_OBJECT,
			OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
			DACL_SECURITY_INFORMATION, /*sidOwner=*/NULL, /*sidGroup=*/NULL,
			/*dacl=*/NULL, /*sacl=*/NULL, &sec_descr);
	free(utf16_path);

	if(ret != ERROR_SUCCESS)
	{
		LOG_ERROR_MSG("Failed to get security info");
		LOG_WERROR(GetLastError());
		return 0;
	}

	/* No, opened token won't do for the purposes of "impersonating" current
	 * process, have to use DuplicateToken(). */
	HANDLE process_token, impersonation_token;
	if(OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, &process_token))
	{
		BOOL success = DuplicateToken(process_token, SecurityImpersonation,
				&impersonation_token);
		CloseHandle(process_token);

		if(!success)
		{
			LOG_ERROR_MSG("Failed to duplicate process token for impersonation");
			LocalFree(sec_descr);
			return 0;
		}
	}

	/* Request all possible rights to see which would be granted. */
	DWORD desired_access = MAXIMUM_ALLOWED;
	GENERIC_MAPPING generic_mapping = {
		FILE_GENERIC_READ,
		FILE_GENERIC_WRITE,
		FILE_GENERIC_EXECUTE,
		FILE_ALL_ACCESS
	};
	MapGenericMask(&desired_access, &generic_mapping);

	PRIVILEGE_SET privileges_used;
	DWORD privileges_size = sizeof(privileges_used);
	DWORD granted_access;
	BOOL has_access;
	if(!AccessCheck(sec_descr, impersonation_token, desired_access,
				&generic_mapping, &privileges_used, &privileges_size, &granted_access,
				&has_access))
	{
		LOG_ERROR_MSG("Couldn't get access info");
		has_access = FALSE;
	}
	else if(has_access)
	{
		/* Consider having any of these flags set as an indication that writing is
		 * possible, after all write permission on POSIX systems might not allow
		 * removing files owned by someone else in case of sticky bit.  A more
		 * fine-grained solution requires passing in parameter which specifies
		 * kind of write operation. */
		const DWORD write_flags =
			FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD;
		has_access = (granted_access & write_flags);
	}

	CloseHandle(impersonation_token);
	LocalFree(sec_descr);
	return has_access;
#endif
}

uint64_t
get_file_size(const char path[])
{
#ifndef _WIN32
	struct stat st;
	if(os_lstat(path, &st) == 0)
	{
		return (uint64_t)st.st_size;
	}
	return 0;
#else
	wchar_t *utf16_path;
	int ok;
	WIN32_FILE_ATTRIBUTE_DATA attrs;
	LARGE_INTEGER size;

	utf16_path = utf8_to_utf16(path);
	ok = GetFileAttributesExW(utf16_path, GetFileExInfoStandard, &attrs);
	free(utf16_path);
	if(!ok)
	{
		LOG_WERROR(GetLastError());
		return 0;
	}

	size.u.LowPart = attrs.nFileSizeLow;
	size.u.HighPart = attrs.nFileSizeHigh;
	return size.QuadPart;
#endif
}

char **
list_regular_files(const char path[], char *list[], int *len)
{
	DIR *dir;
	struct dirent *d;

	dir = os_opendir(path);
	if(dir == NULL)
	{
		return list;
	}

	while((d = os_readdir(dir)) != NULL)
	{
		char full_path[PATH_MAX + 1];
		build_path(full_path, sizeof(full_path), path, d->d_name);

		if(is_regular_file(full_path))
		{
			*len = add_to_string_array(&list, *len, d->d_name);
		}
	}
	os_closedir(dir);

	return list;
}

char **
list_all_files(const char path[], int *len)
{
	DIR *dir;
	struct dirent *d;
	char **list = NULL;

	dir = os_opendir(path);
	if(dir == NULL)
	{
		*len = -1;
		return NULL;
	}

	*len = 0;
	while((d = os_readdir(dir)) != NULL)
	{
		if(!is_builtin_dir(d->d_name))
		{
			*len = add_to_string_array(&list, *len, d->d_name);
		}
	}
	os_closedir(dir);

	return list;
}

char **
list_sorted_files(const char path[], int *len)
{
	char **const list = list_all_files(path, len);
	if(*len > 0)
	{
		/* The check above is needed because *len might be negative. */
		safe_qsort(list, *len, sizeof(*list), &strossorter);
	}
	return list;
}

int
is_regular_file(const char path[])
{
	char path_real[PATH_MAX + 1];
	if(os_realpath(path, path_real) != path_real)
	{
		return 0;
	}

	return is_regular_file_noderef(path_real);
}

int
is_regular_file_noderef(const char path[])
{
#ifndef _WIN32
	struct stat s;
	return os_lstat(path, &s) == 0 && (s.st_mode & S_IFMT) == S_IFREG;
#else
	const DWORD attrs = win_get_file_attrs(path);
	if(attrs == INVALID_FILE_ATTRIBUTES)
	{
		return 0;
	}
	return (attrs & FILE_ATTRIBUTE_DIRECTORY) == 0UL;
#endif
}

int
rename_file(const char src[], const char dst[])
{
	int error;
#ifdef _WIN32
	(void)remove(dst);
#endif
	if((error = os_rename(src, dst)))
	{
		LOG_SERROR_MSG(errno, "Rename operation failed: {%s => %s}", src, dst);
	}
	return error != 0;
}

void
remove_dir_content(const char path[])
{
	DIR *dir;
	struct dirent *d;

	dir = os_opendir(path);
	if(dir == NULL)
	{
		return;
	}

	while((d = os_readdir(dir)) != NULL)
	{
		if(!is_builtin_dir(d->d_name))
		{
			char *const full_path = format_str("%s/%s", path, d->d_name);
			if(entry_is_dir(full_path, d))
			{
				/* Attempt to make sure that we can change the directory we are
				 * descending into. */
				(void)os_chmod(full_path, 0777);
				remove_dir_content(full_path);
				(void)os_rmdir(full_path);
			}
			else
			{
				(void)remove(full_path);
			}
			free(full_path);
		}
	}
	os_closedir(dir);
}

int
entry_is_link(const char path[], const struct dirent *dentry)
{
#ifndef _WIN32
	if(get_dirent_type(dentry, path) == DT_LNK)
	{
		return 1;
	}
#endif
	return is_symlink(path);
}

int
entry_is_dir(const char full_path[], const struct dirent *dentry)
{
#ifndef _WIN32
	const unsigned char type = get_dirent_type(dentry, full_path);
	return (type == DT_UNKNOWN)
	     ? is_directory(full_path, 0)
	     : type == DT_DIR;
#else
	const DWORD MASK = FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY;
	const DWORD attrs = win_get_file_attrs(full_path);
	return attrs != INVALID_FILE_ATTRIBUTES
	    && (attrs & MASK) == FILE_ATTRIBUTE_DIRECTORY;
#endif
}

int
is_dirent_targets_dir(const char full_path[], const struct dirent *d)
{
#ifdef _WIN32
	return is_dir(full_path);
#else
	const unsigned char type = get_dirent_type(d, full_path);

	if(type == DT_UNKNOWN)
	{
		return is_dir(full_path);
	}

	return  type == DT_DIR
	    || (type == DT_LNK && get_symlink_type(full_path) != SLT_UNKNOWN);
#endif
}

int
is_in_subtree(const char path[], const char root[], int include_root)
{
	/* This variable must remain on this level because it can be being pointed
	 * to. */
	char path_copy[PATH_MAX + 1];
	if(!include_root)
	{
		copy_str(path_copy, sizeof(path_copy), path);
		remove_last_path_component(path_copy);
		path = path_copy;
	}

	char path_real[PATH_MAX + 1];
	if(os_realpath(path, path_real) != path_real)
	{
		return 0;
	}

	char root_real[PATH_MAX + 1];
	if(os_realpath(root, root_real) != root_real)
	{
		return 0;
	}

	return path_starts_with(path_real, root_real);
}

int
are_on_the_same_fs(const char s[], const char t[])
{
	struct stat s_stat, t_stat;
	if(os_lstat(s, &s_stat) != 0 || os_lstat(t, &t_stat) != 0)
	{
		return 0;
	}

	return s_stat.st_dev == t_stat.st_dev;
}

int
is_case_change(const char src[], const char dst[])
{
	if(case_sensitive_paths(src))
	{
		return 0;
	}

	return strcasecmp(src, dst) == 0 && strcmp(src, dst) != 0;
}

int
case_sensitive_paths(const char at[])
{
#if HAVE_DECL__PC_CASE_SENSITIVE
	return pathconf(at, _PC_CASE_SENSITIVE) != 0;
#elif !defined(_WIN32)
	return 1;
#else
	return 0;
#endif
}

int
enum_dir_content(const char path[], dir_content_client_func client, void *param)
{
#ifndef _WIN32
	DIR *dir;
	struct dirent *d;

	if((dir = os_opendir(path)) == NULL)
	{
		return -1;
	}

	while((d = os_readdir(dir)) != NULL)
	{
		if(client(d->d_name, d, param) != 0)
		{
			break;
		}
	}
	os_closedir(dir);

	return 0;
#else
	char find_pat[PATH_MAX + 1];
	wchar_t *utf16_path;
	HANDLE hfind;
	WIN32_FIND_DATAW ffd;

	snprintf(find_pat, sizeof(find_pat), "%s/*", path);
	utf16_path = utf8_to_utf16(find_pat);
	hfind = FindFirstFileW(utf16_path, &ffd);
	free(utf16_path);

	if(hfind == INVALID_HANDLE_VALUE)
	{
		return -1;
	}

	do
	{
		char *const utf8_name = utf8_from_utf16(ffd.cFileName);
		if(client(utf8_name, &ffd, param) != 0)
		{
			break;
		}
		free(utf8_name);
	}
	while(FindNextFileW(hfind, &ffd));
	FindClose(hfind);

	return 0;
#endif
}

int
count_dir_items(const char path[])
{
	DIR *dir;
	struct dirent *d;
	int count;

	dir = os_opendir(path);
	if(dir == NULL)
	{
		return -1;
	}

	count = 0;
	while((d = os_readdir(dir)) != NULL)
	{
		if(!is_builtin_dir(d->d_name))
		{
			++count;
		}
	}
	os_closedir(dir);

	return count;
}

char *
get_cwd(char buf[], size_t size)
{
	if(os_getcwd(buf, size) == NULL)
	{
		return NULL;
	}
	return buf;
}

char *
save_cwd(void)
{
	char cwd[PATH_MAX + 1];
	if(os_getcwd(cwd, sizeof(cwd)) != cwd)
	{
		return NULL;
	}
	return strdup(cwd);
}

void
restore_cwd(char saved_cwd[])
{
	if(saved_cwd != NULL)
	{
		(void)vifm_chdir(saved_cwd);
		free(saved_cwd);
	}
}

FILE *
make_tmp_file(char path[], mode_t mode, int auto_delete)
{
	int fd = create_unique_file(path, mode, auto_delete);
	if(fd == -1)
	{
		return NULL;
	}

	FILE *file = fdopen(fd, "w+b");
	if(file == NULL)
	{
		int error = errno;
		(void)close(fd);
		errno = error;
	}

	return file;
}

FILE *
make_file_in_tmp(const char prefix[], mode_t mode, int auto_delete,
		char full_path[], size_t full_path_len)
{
	if(contains_slash(prefix))
	{
		errno = EINVAL;
		return NULL;
	}

	int len = snprintf(full_path, full_path_len, "%s/%s-XXXXXX", get_tmpdir(),
			prefix);
	if(len < 0 || (size_t)len >= full_path_len)
	{
		errno = ERANGE;
		return NULL;
	}

	system_to_internal_slashes(full_path);
	return make_tmp_file(full_path, mode, auto_delete);
}

#ifndef _WIN32

/* Checks if path (dereferenced for a symbolic link) is an existing directory.
 * Automatically dereferences symbolic links. */
static int
is_directory(const char path[], int dereference_links)
{
	struct stat statbuf;
	if((dereference_links ? &os_stat : &os_lstat)(path, &statbuf) != 0)
	{
		LOG_SERROR_MSG(errno, "Can't stat \"%s\"", path);
		log_cwd();
		return 0;
	}

	return S_ISDIR(statbuf.st_mode);
}

#else

int
S_ISLNK(mode_t mode)
{
	return 0;
}

int
readlink(const char *path, char *buf, size_t len)
{
	return -1;
}

int
drive_exists(char letter)
{
	const wchar_t drive[] = { (wchar_t)letter, L':', L'\\', L'\0' };
	const int drive_type = GetDriveTypeW(drive);

	switch(drive_type)
	{
		case DRIVE_CDROM:
		case DRIVE_REMOTE:
		case DRIVE_RAMDISK:
		case DRIVE_REMOVABLE:
		case DRIVE_FIXED:
			return 1;

		case DRIVE_UNKNOWN:
		case DRIVE_NO_ROOT_DIR:
		default:
			return 0;
	}
}

int
is_win_symlink(uint32_t attr, uint32_t tag)
{
	return (attr & FILE_ATTRIBUTE_REPARSE_POINT)
	    && (tag == IO_REPARSE_TAG_SYMLINK);
}

#endif

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