/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "str.h"
#include "path-util.h"

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#define PATH_UTIL_MAX_PATH      8*1024
#define PATH_UTIL_MAX_SYMLINKS  80

static int t_getcwd_noalloc(char **dir_r, size_t *asize_r,
			    const char **error_r) ATTR_NULL(2)
{
	/* @UNSAFE */
	char *dir;
	size_t asize = 128;

	dir = t_buffer_get(asize);
	while (getcwd(dir, asize) == NULL) {
		if (errno != ERANGE) {
			*error_r = t_strdup_printf("getcwd() failed: %m");
			return -1;
		}
		asize = nearest_power(asize+1);
		dir = t_buffer_get(asize);
	}
	if (asize_r != NULL)
		*asize_r = asize;
	*dir_r = dir;
	return 0;
}

static int path_normalize(const char *path, bool resolve_links,
			  const char **npath_r, const char **error_r)
{
	/* @UNSAFE */
	unsigned int link_count = 0;
	char *npath, *npath_pos;
	const char *p;
	size_t asize;

	i_assert(path != NULL);
	i_assert(npath_r != NULL);
	i_assert(error_r != NULL);

	if (path[0] != '/') {
		/* relative; initialize npath with current directory */
		if (t_getcwd_noalloc(&npath, &asize, error_r) < 0)
			return -1;
		npath_pos = npath + strlen(npath);
		i_assert(npath[0] == '/');
	} else {
		/* absolute; initialize npath with root */
		asize = 128;
		npath = t_buffer_get(asize);
		npath[0] = '/';
		npath_pos = npath + 1;
	}

	p = path;
	while (*p != '\0') {
		struct stat st;
		ptrdiff_t seglen;
		const char *segend;

		/* skip duplicate slashes */
		while (*p == '/')
			p++;

		/* find end of path segment */
		for (segend = p; *segend != '\0' && *segend != '/'; segend++);

		if (segend == p)
			break; /* '\0' */
		seglen = segend - p;
		if (seglen == 1 && p[0] == '.') {
			/* a reference to this segment; nothing to do */
		} else if (seglen == 2 && p[0] == '.' && p[1] == '.') {
			/* a reference to parent segment; back up to previous
			 * slash */
			i_assert(npath_pos >= npath);
			if ((npath_pos - npath) > 1) {
				if (*(npath_pos-1) == '/')
					npath_pos--;
				for (; *(npath_pos-1) != '/'; npath_pos--);
			}
		} else {
			/* allocate space if necessary */
			i_assert(npath_pos >= npath);
			if ((size_t)((npath_pos - npath) + seglen + 1) >= asize) {
				ptrdiff_t npath_offset = npath_pos - npath;
				asize = nearest_power(npath_offset + seglen + 2);
				npath = t_buffer_reget(npath, asize);
				npath_pos = npath + npath_offset;
			}

			/* make sure npath now ends in slash */
			i_assert(npath_pos > npath);
			if (*(npath_pos-1) != '/') {
				i_assert((size_t)((npath_pos - npath) + 1) < asize);
				*(npath_pos++) = '/';
			}

			/* copy segment to normalized path */
			i_assert(npath_pos >= npath);
			i_assert((size_t)((npath_pos - npath) + seglen) < asize);
			memmove(npath_pos, p, seglen);
			npath_pos += seglen;
		}

		if (resolve_links) {
			/* stat path up to here (segend points to tail) */
			*npath_pos = '\0';
			if (lstat(npath, &st) < 0) {
				*error_r = t_strdup_printf("lstat() failed: %m");
				return -1;
			}

			if (S_ISLNK (st.st_mode)) {
				/* symlink */
				char *npath_link;
				size_t lsize = 128, tlen = strlen(segend), espace;
				size_t ltlen = (link_count == 0 ? 0 : tlen);
				ssize_t ret;

				/* limit link dereferences */
				if (++link_count > PATH_UTIL_MAX_SYMLINKS) {
					errno = ELOOP;
					*error_r = "Too many symlink dereferences";
					return -1;
				}

				/* allocate space for preserving tail of previous symlink and
				   first attempt at reading symlink with room for the tail

				   buffer will look like this:
				   [npath][0][preserved tail][link buffer][room for tail][0]
				 */
				espace = ltlen + tlen + 2;
				i_assert(npath_pos >= npath);
				if ((size_t)((npath_pos - npath) + espace + lsize) >= asize) {
					ptrdiff_t npath_offset = npath_pos - npath;
					asize = nearest_power((npath_offset + espace + lsize) + 1);
					lsize = asize - (npath_offset + espace);
					npath = t_buffer_reget(npath, asize);
					npath_pos = npath + npath_offset;
				}

				if (ltlen > 0) {
					/* preserve tail just after end of npath */
					i_assert(npath_pos >= npath);
					i_assert((size_t)((npath_pos + 1 - npath) + ltlen) < asize);
					memmove(npath_pos + 1, segend, ltlen);
				}

				/* read the symlink after the preserved tail */
				for (;;) {
					npath_link = (npath_pos + 1) + ltlen;

					i_assert(npath_link >= npath_pos);
					i_assert((size_t)((npath_link - npath) + lsize) < asize);

					/* attempt to read the link */
					if ((ret=readlink(npath, npath_link, lsize)) < 0) {
						*error_r = t_strdup_printf("readlink() failed: %m");
						return -1;
					}
					if ((size_t)ret < lsize) {
						/* POSIX doesn't guarantee the presence of a NIL */
						npath_link[ret] = '\0';
						break;
					}

					/* sum of new symlink content length
					 * and path tail length may not
					   exceed maximum */
					if ((size_t)(ret + tlen) >= PATH_UTIL_MAX_PATH) {
						errno = ENAMETOOLONG;
						*error_r = "Resulting path is too long";
						return -1;
					}

					/* try again with bigger buffer,
					   we need to allocate more space as well if lsize == ret,
					   because the returned link may have gotten truncated */
					espace = ltlen + tlen + 2;
					i_assert(npath_pos >= npath);
					if ((size_t)((npath_pos - npath) + espace + lsize) >= asize ||
					    lsize == (size_t)ret) {
						ptrdiff_t npath_offset = npath_pos - npath;
						asize = nearest_power((npath_offset + espace + lsize) + 1);
						lsize = asize - (npath_offset + espace);
						npath = t_buffer_reget(npath, asize);
						npath_pos = npath + npath_offset;
					}
				}

				/* add tail of previous path at end of symlink */
				i_assert(npath_link >= npath);
				if (ltlen > 0) {
					i_assert(npath_pos >= npath);
					i_assert((size_t)((npath_pos - npath) + 1 + tlen) < asize);
					i_assert((size_t)((npath_link - npath) + ret + tlen) < asize);
					memcpy(npath_link + ret, npath_pos + 1, tlen);
				} else {
					i_assert((size_t)((npath_link - npath) + ret + tlen) < asize);
					memcpy(npath_link + ret, segend, tlen);
				}
				*(npath_link+ret+tlen) = '\0';

				/* use as new source path */
				path = segend = npath_link;

				if (path[0] == '/') {
					/* absolute symlink; start over at root */
					npath_pos = npath + 1;
				} else {
					/* relative symlink; back up to previous segment */
					i_assert(npath_pos >= npath);
					if ((npath_pos - npath) > 1) {
						if (*(npath_pos-1) == '/')
							npath_pos--;
						for (; *(npath_pos-1) != '/'; npath_pos--);
					}
				}

			} else if (*segend != '\0' && !S_ISDIR (st.st_mode)) {
				/* not last segment, but not a directory either */
				errno = ENOTDIR;
				*error_r = t_strdup_printf("Not a directory: %s", npath);
				return -1;
			}
		}

		p = segend;
	}

	i_assert(npath_pos >= npath);
	i_assert((size_t)(npath_pos - npath) < asize);

	/* remove any trailing slash */
	if ((npath_pos - npath) > 1 && *(npath_pos-1) == '/')
		npath_pos--;
	*npath_pos = '\0';

	t_buffer_alloc(npath_pos - npath + 1);
	*npath_r = npath;
	return 0;
}

int t_normpath(const char *path, const char **npath_r, const char **error_r)
{
	return path_normalize(path, FALSE, npath_r, error_r);
}

int t_normpath_to(const char *path, const char *root, const char **npath_r,
		  const char **error_r)
{
	i_assert(path != NULL);
	i_assert(root != NULL);
	i_assert(npath_r != NULL);

	if (*path == '/')
		return t_normpath(path, npath_r, error_r);

	return t_normpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
}

int t_realpath(const char *path, const char **npath_r, const char **error_r)
{
	return path_normalize(path, TRUE, npath_r, error_r);
}

int t_realpath_to(const char *path, const char *root, const char **npath_r,
		  const char **error_r)
{
	i_assert(path != NULL);
	i_assert(root != NULL);
	i_assert(npath_r != NULL);

	if (*path == '/')
		return t_realpath(path, npath_r, error_r);

	return t_realpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
}

int t_abspath(const char *path, const char **abspath_r, const char **error_r)
{
	i_assert(path != NULL);
	i_assert(abspath_r != NULL);
	i_assert(error_r != NULL);

	if (*path == '/') {
		*abspath_r = path;
		return 0;
	}

	const char *dir, *error;
	if (t_get_working_dir(&dir, &error) < 0) {
		*error_r = t_strconcat("Failed to get working directory: ",
				       error, NULL);
		return -1;
	}
	*abspath_r = t_strconcat(dir, "/", path, NULL);
	return 0;
}

const char *t_abspath_to(const char *path, const char *root)
{
	i_assert(path != NULL);
	i_assert(root != NULL);

	if (*path == '/')
		return path;

	return t_strconcat(root, "/", path, NULL);
}

int t_get_working_dir(const char **dir_r, const char **error_r)
{
	char *dir;

	i_assert(dir_r != NULL);
	i_assert(error_r != NULL);
	if (t_getcwd_noalloc(&dir, NULL, error_r) < 0)
		return -1;

	t_buffer_alloc(strlen(dir) + 1);
	*dir_r = dir;
	return 0;
}

int t_readlink(const char *path, const char **dest_r, const char **error_r)
{
	i_assert(error_r != NULL);

	/* @UNSAFE */
	ssize_t ret;
	char *dest;
	size_t size = 128;

	dest = t_buffer_get(size);
	while ((ret = readlink(path, dest, size)) >= (ssize_t)size) {
		size = nearest_power(size+1);
		dest = t_buffer_get(size);
	}
	if (ret < 0) {
		*error_r = t_strdup_printf("readlink() failed: %m");
		return -1;
	}

	dest[ret] = '\0';
	t_buffer_alloc(ret + 1);
	*dest_r = dest;
	return 0;
}

bool t_binary_abspath(const char **binpath, const char **error_r)
{
	const char *path_env, *const *paths;
	string_t *path;

	if (**binpath == '/') {
		/* already have absolute path */
		return TRUE;
	} else if (strchr(*binpath, '/') != NULL) {
		/* relative to current directory */
		const char *error;
		if (t_abspath(*binpath, binpath, &error) < 0) {
			*error_r = t_strdup_printf("t_abspath(%s) failed: %s",
						   *binpath, error);
			return FALSE;
		}
		return TRUE;
	} else if ((path_env = getenv("PATH")) != NULL) {
		/* we have to find our executable from path */
		path = t_str_new(256);
		paths = t_strsplit(path_env, ":");
		for (; *paths != NULL; paths++) {
			str_append(path, *paths);
			str_append_c(path, '/');
			str_append(path, *binpath);
			if (access(str_c(path), X_OK) == 0) {
				*binpath = str_c(path);
				return TRUE;
			}
			str_truncate(path, 0);
		}
		*error_r = "Could not find the wanted executable from PATH";
		return FALSE;
	} else {
		*error_r = "PATH environment variable undefined";
		return FALSE;
	}
}
