/*
 *	File and directory handling
 *	Copyright Jan Engelhardt, 2002-2011
 *
 *	This file is part of libHX. libHX is free software; you can
 *	redistribute it and/or modify it under the terms of the GNU Lesser
 *	General Public License as published by the Free Software Foundation;
 *	either version 2.1 or (at your option) any later version.
 */
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined _WIN32
#	include <direct.h>
#	include <io.h>
#	include <windows.h>
#else
#	include <dirent.h>
#	include <unistd.h>
#endif
#include <libHX/ctype_helper.h>
#include <libHX/defs.h>
#include <libHX/io.h>
#include <libHX/misc.h>
#include <libHX/string.h>
#include "internal.h"

struct HXdir {
#if defined _WIN32
	char *dname;
	HANDLE ptr;
	WIN32_FIND_DATA dentry;
	bool got_first;
#else
	DIR *ptr;
	struct dirent *dentry;
#endif
};

static int mkdir_gen(const char *d, unsigned int mode)
{
	struct stat sb;
	if (lstat(d, &sb) < 0) {
#if defined(_WIN32)
		if (mkdir(d) < 0)
#else
		if (mkdir(d, mode) < 0) /* use umask() for permissions */
#endif
			return -errno;
	} else {
#if defined(_WIN32)
		if ((sb.st_mode & S_IFDIR) != S_IFDIR)
#else
		if (!S_ISDIR(sb.st_mode))
#endif
			return -errno;
	}
	return 1;
}

EXPORT_SYMBOL struct HXdir *HXdir_open(const char *s)
{
	struct HXdir *d;

#ifdef _WIN32
	if ((d = malloc(sizeof(*d))) == NULL)
		return NULL;
	if ((d->dname = malloc(strlen(s) + 3)) == NULL) {
		free(d);
		return NULL;
	}
	strcpy(d->dname, s);
	strcat(d->dname, "\\*");
	d->got_first = false;
#else
	/*
	 * On Linux-glibc, the struct dirent definition contains a wasteful
	 * and bug-concealing "char d_name[256]", while on Solaris, it is a
	 * proper "char d_name[]".
	 */
	size_t size = sizeof(*d);
	ssize_t name_max;
	DIR *tmp_dh = opendir(s);
	if (tmp_dh == NULL)
		return NULL;
	/*
	 * dirfd is POSIX.1-2008 and was present earlier in selected
	 * extensions. In case of !HAVE_DIRFD, use pathconf(s, _PC_NAME_MAX)
	 * and bite the race bullet.
	 */
	name_max = fpathconf(dirfd(tmp_dh), _PC_NAME_MAX);
	if (name_max > 0) {
		size -= sizeof(struct dirent) - offsetof(struct dirent, d_name);
		size += name_max + 1;
	} else {
#ifdef NAME_MAX
		size += NAME_MAX; /* "best effort" :-/ */
#elif defined(MAXNAMELEN)
		size += MAXNAMELEN;
#else
		fprintf(stderr, "libHX-warning: Cannot determine buffer size for readdir\n");
		closedir(tmp_dh);
		errno = EINVAL;
		return NULL;
#endif
	}
	if ((d = malloc(size)) == NULL) {
		closedir(tmp_dh);
		return NULL;
	}
	d->ptr = tmp_dh;
#endif
	return d;
}

EXPORT_SYMBOL const char *HXdir_read(struct HXdir *d)
{
	if (d == NULL)
		return NULL;

	errno = 0;
#if defined _WIN32
	if (!d->got_first) {
		d->got_first = true;
		if ((d->ptr = FindFirstFile(d->dname, &d->dentry)) == NULL)
			return NULL;
	} else if (!FindNextFile(d->ptr, &d->dentry)) {
		return NULL;
	}
	return d->dentry.cFileName;
#else
	d->dentry = readdir(d->ptr);
	if (d->dentry == NULL)
		return NULL;
	return d->dentry->d_name;
#endif
}

EXPORT_SYMBOL void HXdir_close(struct HXdir *d)
{
	if (d == NULL)
		return;
#if defined _WIN32
	FindClose(d->ptr);
	free(d->dname);
#else
	closedir(d->ptr);
#endif
	free(d);
}

EXPORT_SYMBOL int HX_copy_file(const char *src, const char *dest,
    unsigned int opts, ...)
{
	char buf[MAXLNLEN];
	unsigned int extra_flags = 0;
	int dd, eax = 0, sd, l;

	if ((sd = open(src, O_RDONLY | O_BINARY)) < 0)
		return -errno;
	if (opts & HXF_KEEP)
		extra_flags = O_EXCL;
	dd = open(dest, O_WRONLY | O_BINARY | O_CREAT | O_TRUNC |
	     extra_flags, S_IRUGO | S_IWUGO);
	if (dd < 0) {
		eax = errno;
		close(sd);
		errno = eax;
		if (extra_flags != 0 && eax == EEXIST)
			return 1;
		return -errno;
	}

	while ((l = read(sd, buf, MAXLNLEN)) > 0 && write(dd, buf, l) > 0)
		;
	close(sd);

	if (opts & (HXF_UID | HXF_GID)) {
		struct stat sb;
		long uid, gid;
		va_list argp;
		va_start(argp, opts);

		fstat(dd, &sb);
		uid = sb.st_uid;
		gid = sb.st_gid;

		if (opts & HXF_UID) uid = va_arg(argp, long);
		if (opts & HXF_GID) gid = va_arg(argp, long);
		if (fchown(dd, uid, gid) < 0)
			{};
		va_end(argp);
	}
	close(dd);
	return 1;
}

EXPORT_SYMBOL int HX_copy_dir(const char *src, const char *dest,
    unsigned int opts, ...)
{
	void *dt = HXdir_open(src);
	long uid = -1, gid = -1;
	const char *fn;

	if (dt == NULL)
		return 0;

	{
		va_list argp;
		va_start(argp, opts);
		if (opts & HXF_UID) uid = va_arg(argp, long);
		if (opts & HXF_GID) gid = va_arg(argp, long);
		va_end(argp);
	}

	while ((fn = HXdir_read(dt)) != NULL) {
		char fsrc[MAXFNLEN], fdest[MAXFNLEN];
		struct stat sb;

		if (strcmp(fn, ".") == 0 || strcmp(fn, "..") == 0)
			continue;
		snprintf(fsrc,  MAXFNLEN, "%s/%s", src,  fn);
		snprintf(fdest, MAXFNLEN, "%s/%s", dest, fn);

		lstat(fsrc, &sb);
		sb.st_mode &= 0777; /* clear SUID/GUID/Sticky bits */

		if (S_ISREG(sb.st_mode)) {
			HX_copy_file(fsrc, fdest, opts, uid, gid);
		} else if (S_ISDIR(sb.st_mode)) {
			HX_mkdir(fdest, S_IRWXUGO);
			HX_copy_dir(fsrc, fdest, opts, uid, gid);
		} else if (S_ISLNK(sb.st_mode)) {
			char pt[MAXFNLEN];
			memset(pt, '\0', MAXFNLEN);
			if (readlink(fsrc, pt, MAXFNLEN - 1) < MAXFNLEN - 1)
				if (symlink(pt, fdest) < 0)
					{};
		} else if (S_ISBLK(sb.st_mode) || S_ISCHR(sb.st_mode)) {
			mknod(fdest, sb.st_mode, sb.st_dev);
		} else if (S_ISFIFO(sb.st_mode)) {
			mkfifo(fdest, sb.st_mode);
		}

		if (lchown(fdest, uid, gid) < 0)
			{};
		if (!S_ISLNK(sb.st_mode))
			chmod(fdest, sb.st_mode);
	}

	HXdir_close(dt);
	return 1;
}

EXPORT_SYMBOL int HX_mkdir(const char *idir, unsigned int mode)
{
	int i = 0, len = strlen(idir);
	char buf[MAXFNLEN], dir[MAXFNLEN];

#if defined(_WIN32)
	{
		char *p = dir;
		HX_strlcpy(dir, idir, sizeof(dir));
		for (; *p != '\0'; ++p)
			if (*p == '\\')
				*p = '/';
		if (HX_isalpha(dir[0]) && dir[1] == ':')
			i = 2;
	}
#else
	HX_strlcpy(dir, idir, sizeof(dir));
#endif

	if (dir[i] == '/')
		++i;
	for (; i < len; ++i) {
		int v;
		if (dir[i] == '/') {
			strncpy(buf, dir, i);
			buf[i] = '\0';
			if ((v = mkdir_gen(buf, mode)) <= 0)
				return v;
		} else if (i == len - 1) {
			strncpy(buf, dir, len);
			buf[len] = '\0';
			if ((v = mkdir_gen(buf, mode)) <= 0)
				return v;
		}
	}
	return 1;
}

/* Readlink - with a trailing zero (provided by HXmc) */
EXPORT_SYMBOL int HX_readlink(hxmc_t **target, const char *path)
{
	bool dnull = *target == NULL;
	char *tb;
	int ret;

	if (dnull) {
		*target = HXmc_meminit(NULL, PATH_MAX);
		if (*target == NULL)
			return -errno;
	}
	tb  = *target;
	ret = readlink(path, tb, PATH_MAX);
	if (ret < 0) {
		ret = -errno;
		if (!dnull) {
			HXmc_free(*target);
			*target = NULL;
		}
		return ret;
	}
	HXmc_setlen(target, ret);
	return ret;
}

/**
 * The buffers HX_realpath_symres are used are retained across symres calls to
 * not do unnecessarily many allocation calls. Downside is that the state is
 * roughly 12K in the worst case.
 */
struct HX_realpath_state {
	hxmc_t *dest;
	hxmc_t *link_target;
	hxmc_t *new_path;
	hxmc_t *symres_tmp;
	const char *path;
	unsigned int deref_count;
};

/**
 * Perform symlink resolution on the currently last component (state->dest).
 */
static int
HX_realpath_symres(struct HX_realpath_state *state, const char *path)
{
	int ret;

	ret = HX_readlink(&state->link_target, state->dest);
	if (ret == -EINVAL)
		return -EINVAL;
	else if (ret < 0)
		return -errno;
#ifdef __MINGW32__
	else if (state->deref_count++ >= 40)
		/*
		 * not that this is ever going to happen on mingw,
		 * because Windows does not seem to have symlinks, ...
		 */
		return -EINVAL;
#else
	else if (state->deref_count++ >= 40)
		return -ELOOP;
#endif

	if (*state->link_target == '/') {
		*state->dest = '\0';
		if (HXmc_setlen(&state->dest, 0) == NULL)
			return -errno;
	} else {
		char *dptr = state->dest + HXmc_length(state->dest);
		while (*--dptr != '/')
			;
		*dptr = '\0';
		if (HXmc_setlen(&state->dest, dptr - state->dest) == NULL)
			return -errno;
	}

	if (HXmc_strcpy(&state->symres_tmp, state->link_target) == NULL)
		return -errno;
	/*
	 * @path could be pointing to @state->new_path already, so we need
	 * to construct the new path in a temp buffer (@symres_tmp) first.
	 */
	if (HXmc_strcat(&state->symres_tmp, path) == NULL)
		return -errno;
	if (HXmc_strcpy(&state->new_path, state->symres_tmp) == NULL)
		return -errno;
	state->path = state->new_path;
	return 1;
}

EXPORT_SYMBOL int HX_realpath(hxmc_t **dest_pptr, const char *path,
    unsigned int flags)
{
	struct HX_realpath_state state = {.dest = *dest_pptr};
	bool rq_slash = false, dnull = state.dest == NULL;
	const char *cptr, *orig_path = path;
	int ret = 0;

	if (dnull) {
		state.dest = HXmc_meminit(NULL, PATH_MAX);
		if (state.dest == NULL)
			goto err;
	}

	if (*path == '/') {
		rq_slash = true;
	} else if (flags & HX_REALPATH_ABSOLUTE) {
		if (getcwd(state.dest, PATH_MAX) == NULL)
			goto err;
		rq_slash = true;
		if (HXmc_setlen(&state.dest, strlen(state.dest)) == NULL)
			goto err;
	}

	while (*path != '\0') {
		if (*path == '/') {
			++path;
			continue;
		} else if (path[0] == '.' &&
		    (path[1] == '/' || path[1] == '\0') &&
		    flags & HX_REALPATH_SELF) {
			++path;
			continue;
		} else if (path[0] == '.' && path[1] == '.' &&
		    (path[2] == '/' || path[2] == '\0') &&
		    flags & HX_REALPATH_PARENT &&
		    ((flags & HX_REALPATH_ABSOLUTE) || *state.dest != '\0')) {
			cptr  = state.dest + HXmc_length(state.dest);
			path += 2;
			while (cptr > state.dest && *--cptr != '/')
				;
			state.dest[cptr-state.dest] = '\0';
			if (HXmc_setlen(&state.dest,
			    cptr - state.dest) == NULL)
				goto err;
			continue;
		}

		for (cptr = path; *cptr != '\0' && *cptr != '/'; ++cptr)
			;
		if (rq_slash && HXmc_strcat(&state.dest, "/") == NULL)
			goto out;
		if (HXmc_memcat(&state.dest, path, cptr - path) == NULL)
			goto out;
		path = cptr;
		rq_slash = true;

		ret = HX_realpath_symres(&state, path);
		if (ret == -EINVAL)
			continue;
		else if (ret < 0)
			goto out;
		path = state.path;
	}

	if (*state.dest == '\0') {
		if (*orig_path == '/') {
			if (HXmc_strcpy(&state.dest, "/") == NULL)
				goto err;
		} else {
			if (HXmc_strcpy(&state.dest, ".") == NULL)
				goto err;
		}
	}

	*dest_pptr = state.dest;
	HXmc_free(state.link_target);
	HXmc_free(state.new_path);
	HXmc_free(state.symres_tmp);
	return 1;

 err:
	ret = -errno;
 out:
	if (dnull) {
		/* If caller supplied a buffer, do not take it away. */
		HXmc_free(state.dest);
		*dest_pptr = NULL;
	}
	HXmc_free(state.link_target);
	HXmc_free(state.new_path);
	HXmc_free(state.symres_tmp);
	return ret;
}

EXPORT_SYMBOL int HX_rrmdir(const char *dir)
{
	struct HXdir *ptr;
	const char *trav;
	hxmc_t *fn = NULL;
	int ret = 0;

	if ((ptr = HXdir_open(dir)) == NULL)
		return -errno;

	while ((trav = HXdir_read(ptr)) != NULL) {
		struct stat sb;

		if (strcmp(trav, ".") == 0 || strcmp(trav, "..") == 0)
			continue;
		HXmc_strcpy(&fn, dir);
		HXmc_strcat(&fn, "/");
		HXmc_strcat(&fn, trav);
		if (lstat(fn, &sb) < 0) {
			if (ret == 0)
				ret = -errno;
			continue;
		}

		if (S_ISDIR(sb.st_mode)) {
			if (HX_rrmdir(fn) <= 0) {
				if (ret == 0)
					ret = -errno;
				continue;
			}
		} else if (unlink(fn) < 0) {
			if (ret == 0)
				ret = -errno;
			continue;
		}
	}

	if (rmdir(dir) < 0) {
		if (ret == 0)
			ret = -errno;
	}
	HXdir_close(ptr);
	HXmc_free(fn);
	return ret;
}

EXPORT_SYMBOL ssize_t HXio_fullread(int fd, void *vbuf, size_t size)
{
	char *buf = vbuf;
	size_t rem = size;
	ssize_t ret;

	while (rem > 0) {
		ret = read(fd, buf, rem);
		if (ret < 0)
			return ret;
		rem -= ret;
		buf += ret;
	}
	return size;
}

EXPORT_SYMBOL ssize_t HXio_fullwrite(int fd, const void *vbuf, size_t size)
{
	const char *buf = vbuf;
	size_t rem = size;
	ssize_t ret;

	while (rem > 0) {
		ret = write(fd, buf, rem);
		if (ret < 0)
			return ret;
		rem -= ret;
		buf += ret;
	}
	return size;
}
