/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2007,2008,2009,2013 Free Software Foundation, Inc.
 *
 *  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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include <grub/archelp.h>
#include <grub/err.h>
#include <grub/fs.h>
#include <grub/disk.h>

GRUB_MOD_LICENSE("GPLv3+");

static inline void
canonicalize(char* name)
{
	char* iptr, * optr;
	for (iptr = name, optr = name; *iptr; )
	{
		while (*iptr == '/')
			iptr++;
		if (iptr[0] == '.' && (iptr[1] == '/' || iptr[1] == 0))
		{
			iptr++;
			continue;
		}
		if (iptr[0] == '.' && iptr[1] == '.' && (iptr[2] == '/' || iptr[2] == 0))
		{
			iptr += 2;
			if (optr == name)
				continue;
			for (optr -= 2; optr >= name && *optr != '/'; optr--);
			optr++;
			continue;
		}
		while (*iptr && *iptr != '/')
			*optr++ = *iptr++;
		if (*iptr)
			*optr++ = *iptr++;
	}
	*optr = 0;
}

static grub_err_t
handle_symlink(struct grub_archelp_data* data,
	struct grub_archelp_ops* arcops,
	const char* fn, char** name,
	grub_uint32_t mode, int* restart)
{
	grub_size_t flen;
	char* target;
	char* ptr;
	char* lastslash;
	grub_size_t prefixlen;
	char* rest;
	char* linktarget;
	grub_size_t linktarget_len;

	*restart = 0;

	if ((mode & GRUB_ARCHELP_ATTR_TYPE) != GRUB_ARCHELP_ATTR_LNK
		|| !arcops->get_link_target)
		return GRUB_ERR_NONE;
	flen = grub_strlen(fn);
	if (grub_memcmp(*name, fn, flen) != 0
		|| ((*name)[flen] != 0 && (*name)[flen] != '/'))
		return GRUB_ERR_NONE;
	rest = *name + flen;
	lastslash = rest;
	if (*rest)
		rest++;
	while (lastslash >= *name && *lastslash != '/')
		lastslash--;
	if (lastslash >= *name)
		prefixlen = lastslash - *name;
	else
		prefixlen = 0;

	if (prefixlen)
		prefixlen++;

	linktarget = arcops->get_link_target(data);
	if (!linktarget)
		return grub_errno;
	if (linktarget[0] == '\0')
		return GRUB_ERR_NONE;
	linktarget_len = grub_strlen(linktarget);
	target = grub_malloc(linktarget_len + grub_strlen(*name) + 2);
	if (!target)
		return grub_errno;

	grub_strcpy(target + prefixlen, linktarget);
	grub_free(linktarget);
	if (target[prefixlen] == '/')
	{
		ptr = grub_stpcpy(target, target + prefixlen);
		ptr = grub_stpcpy(ptr, rest);
		*ptr = 0;
		grub_dprintf("archelp", "symlink redirected %s to %s\n",
			*name, target);
		grub_free(*name);

		canonicalize(target);
		*name = target;
		*restart = 1;
		return GRUB_ERR_NONE;
	}
	if (prefixlen)
	{
		grub_memcpy(target, *name, prefixlen);
		target[prefixlen - 1] = '/';
	}
	grub_strcpy(target + prefixlen + linktarget_len, rest);
	grub_dprintf("archelp", "symlink redirected %s to %s\n",
		*name, target);
	grub_free(*name);
	canonicalize(target);
	*name = target;
	*restart = 1;
	return GRUB_ERR_NONE;
}

grub_err_t
grub_archelp_dir(struct grub_archelp_data* data,
	struct grub_archelp_ops* arcops,
	const char* path_in,
	grub_fs_dir_hook_t hook, void* hook_data)
{
	char* prev, * name, * path, * ptr;
	grub_size_t len;
	int symlinknest = 0;

	path = grub_strdup(path_in + 1);
	if (!path)
		return grub_errno;
	canonicalize(path);
	for (ptr = path + grub_strlen(path) - 1; ptr >= path && *ptr == '/'; ptr--)
		*ptr = 0;

	prev = 0;

	len = grub_strlen(path);
	while (1)
	{
		grub_int32_t mtime;
		grub_uint32_t mode;
		grub_err_t err;

		if (arcops->find_file(data, &name, &mtime, (grub_archelp_mode_t*)&mode))
			goto fail;

		if (mode == GRUB_ARCHELP_ATTR_END)
			break;

		canonicalize(name);

		if (grub_memcmp(path, name, len) == 0
			&& (name[len] == 0 || name[len] == '/' || len == 0))
		{
			char* p, * n;

			n = name + len;
			while (*n == '/')
				n++;

			p = grub_strchr(n, '/');
			if (p)
				*p = 0;

			if ((*n == 0) && ((mode & GRUB_ARCHELP_ATTR_TYPE)
				!= GRUB_ARCHELP_ATTR_DIR))
			{
				grub_error(GRUB_ERR_BAD_FILE_TYPE, N_("not a directory"));
				grub_free(name);
				goto fail;
			}

			if (((!prev) || (grub_strcmp(prev, name) != 0)) && *n != 0)
			{
				struct grub_dirhook_info info;
				grub_memset(&info, 0, sizeof(info));
				info.dir = (p != NULL) || ((mode & GRUB_ARCHELP_ATTR_TYPE)
					== GRUB_ARCHELP_ATTR_DIR);
				info.symlink = ((mode & GRUB_ARCHELP_ATTR_TYPE) == GRUB_ARCHELP_ATTR_LNK);
				if (!(mode & GRUB_ARCHELP_ATTR_NOTIME))
				{
					info.mtime = mtime;
					info.mtimeset = 1;
				}
				if (hook(n, &info, hook_data))
				{
					grub_free(name);
					goto fail;
				}
				grub_free(prev);
				prev = name;
			}
			else
			{
				int restart = 0;
				err = handle_symlink(data, arcops, name,
					&path, mode, &restart);
				grub_free(name);
				if (err)
					goto fail;
				if (restart)
				{
					len = grub_strlen(path);
					if (++symlinknest == 8)
					{
						grub_error(GRUB_ERR_SYMLINK_LOOP,
							N_("too deep nesting of symlinks"));
						goto fail;
					}
					arcops->rewind(data);
				}
			}
		}
		else
			grub_free(name);
	}

fail:

	grub_free(path);
	grub_free(prev);

	return grub_errno;
}

grub_err_t
grub_archelp_open(struct grub_archelp_data* data,
	struct grub_archelp_ops* arcops,
	const char* name_in)
{
	char* fn;
	char* name = grub_strdup(name_in + 1);
	int symlinknest = 0;

	if (!name)
		return grub_errno;

	canonicalize(name);

	while (1)
	{
		grub_uint32_t mode;
		grub_int32_t mtime;
		int restart;

		if (arcops->find_file(data, &fn, &mtime, (grub_archelp_mode_t*)&mode))
			goto fail;

		if (mode == GRUB_ARCHELP_ATTR_END)
		{
			grub_error(GRUB_ERR_FILE_NOT_FOUND, N_("file `%s' not found"), name_in);
			break;
		}

		canonicalize(fn);

		if (handle_symlink(data, arcops, fn, &name, mode, &restart))
		{
			grub_free(fn);
			goto fail;
		}

		if (restart)
		{
			arcops->rewind(data);
			if (++symlinknest == 8)
			{
				grub_error(GRUB_ERR_SYMLINK_LOOP,
					N_("too deep nesting of symlinks"));
				goto fail;
			}
			goto no_match;
		}

		if (grub_strcmp(name, fn) != 0)
			goto no_match;

		grub_free(fn);
		grub_free(name);

		return GRUB_ERR_NONE;

	no_match:

		grub_free(fn);
	}

fail:
	grub_free(name);

	return grub_errno;
}
