/*
 * Copyright (C) 2008 Nokia Corporation.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 as published by
 * the Free Software Foundation.
 *
 * 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 St, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * Author: Artem Bityutskiy
 *
 * Part of the device table parsing code was taken from the mkfs.jffs2 utility.
 * The original author of that code is Erik Andersen, hence:
 *	Copyright (C) 2001, 2002 Erik Andersen <andersen@codepoet.org>
 */

/*
 * This file implemented device table support. Device table entries take the
 * form of:
 * <path>    <type> <mode> <uid> <gid> <major> <minor> <start>	<inc> <count>
 * /dev/mem  c       640   0     0     1       1       0        0     -
 *
 * Type can be one of:
 * f  A regular file
 * d  Directory
 * c  Character special device file
 * b  Block special device file
 * p  Fifo (named pipe)
 * l  Link
 *
 * Don't bother with hard links (special cases of regular files), or sockets
 * (why bother).
 *
 * Regular files must exist in the target root directory. If a char, block,
 * fifo, or directory does not exist, it will be created.
 *
 * Please, refer the device_table.txt file which can be found at MTD utilities
 * for more information about what the device table is.
 */

#include "mkfs.ubifs.h"
#include "hashtable/hashtable.h"
#include "hashtable/hashtable_itr.h"

/*
 * The hash table which contains paths to files/directories/device nodes
 * referred to in the device table. For example, if the device table refers
 * "/dev/loop0", the @path_htbl will contain "/dev" element.
 */
static struct hashtable *path_htbl;

/* Hash function used for hash tables */
static unsigned int r5_hash(void *s)
{
	unsigned int a = 0;
	const signed char *str = s;

	while (*str) {
		a += *str << 4;
		a += *str >> 4;
		a *= 11;
		str++;
	}

	return a;
}

/*
 * Check whether 2 keys of a hash table are equivalent. The keys are path/file
 * names, so we simply use 'strcmp()'.
 */
static int is_equivalent(void *k1, void *k2)
{
	return !strcmp(k1, k2);
}

/**
 * separate_last - separate out the last path component
 * @buf: the path to split
 * @len: length of the @buf string
 * @path: the beginning of path is returned here
 * @name: the last path component is returned here
 *
 * This helper function separates out the the last component of the full path
 * string. For example, "/dev/loop" would be split on "/dev" and "loop". This
 * function allocates memory for @path and @name and return the result there.
 * Returns zero in case of success and a negative error code in case of
 * failure.
 */
static int separate_last(const char *buf, int len, char **path, char **name)
{
	int path_len = len, name_len;
	const char *p = buf + len, *n;

	while (*--p != '/')
		path_len -= 1;

	/* Drop the final '/' unless this is the root directory */
	name_len = len - path_len;
	n = buf + path_len;
	if (path_len > 1)
		path_len -= 1;

	*path = malloc(path_len + 1);
	if (!*path)
		return err_msg("cannot allocate %d bytes of memory",
			       path_len + 1);
	memcpy(*path, buf, path_len);
	(*path)[path_len] = '\0';

	*name = malloc(name_len + 1);
	if (!*name) {
		free(*path);
		return err_msg("cannot allocate %d bytes of memory",
			       name_len + 1);
	}
	memcpy(*name, n, name_len + 1);

	return 0;
}

static int interpret_table_entry(const char *line)
{
	char buf[1024], type, *path = NULL, *name = NULL;
	int len;
	struct path_htbl_element *ph_elt = NULL;
	struct name_htbl_element *nh_elt = NULL;
	unsigned int mode = 0755, uid = 0, gid = 0, major = 0, minor = 0;
	unsigned int start = 0, increment = 0, count = 0;

	if (sscanf(line, "%1023s %c %o %u %u %u %u %u %u %u",
		   buf, &type, &mode, &uid, &gid, &major, &minor,
		   &start, &increment, &count) < 0)
		return sys_err_msg("sscanf failed");

	dbg_msg(3, "name %s, type %c, mode %o, uid %u, gid %u, major %u, "
		"minor %u, start %u, inc %u, cnt %u",
		buf, type, mode, uid, gid, major, minor, start,
		increment, count);

	len = strnlen(buf, 1024);
	if (len == 0)
		return err_msg("empty path");
	if (len == 1024)
		return err_msg("too long path");

	if (buf[0] != '/')
		return err_msg("device table entries require absolute paths");
	if (strstr(buf, "//"))
		return err_msg("'//' cannot be used in the path");
	if (len > 1 && buf[len - 1] == '/')
		return err_msg("do not put '/' at the end");

	if (strstr(buf, "/./") || strstr(buf, "/../") ||
	    !strcmp(buf + len - 2, "/.") || !strcmp(buf + len - 3, "/.."))
		return err_msg("'.' and '..' cannot be used in the path");

	switch (type) {
		case 'd':
			mode |= S_IFDIR;
			break;
		case 'f':
			mode |= S_IFREG;
			break;
		case 'p':
			mode |= S_IFIFO;
			break;
		case 'c':
			mode |= S_IFCHR;
			break;
		case 'b':
			mode |= S_IFBLK;
			break;
		case 'l':
			mode |= S_IFLNK;
			if ((mode & 0777) != 0777)
				return err_msg("link permission must be 0777");
			break;
		default:
			return err_msg("unsupported file type '%c'", type);
	}

	if (separate_last(buf, len, &path, &name))
		return -1;

	/*
	 * Check if this path already exist in the path hash table and add it
	 * if it is not.
	 */
	ph_elt = hashtable_search(path_htbl, path);
	if (!ph_elt) {
		dbg_msg(3, "inserting '%s' into path hash table", path);
		ph_elt = malloc(sizeof(struct path_htbl_element));
		if (!ph_elt) {
			err_msg("cannot allocate %zd bytes of memory",
				sizeof(struct path_htbl_element));
			goto out_free;
		}

		if (!hashtable_insert(path_htbl, path, ph_elt)) {
			err_msg("cannot insert into path hash table");
			goto out_free;
		}

		ph_elt->path = path;
		path = NULL;
		ph_elt->name_htbl = create_hashtable(128, &r5_hash,
						     &is_equivalent);
		if (!ph_elt->name_htbl) {
			err_msg("cannot create name hash table");
			goto out_free;
		}
	}

	if (increment != 0 && count == 0) {
		err_msg("count cannot be zero if increment is non-zero");
		goto out_free;
	}

	/*
	 * Add the file/directory/device node (last component of the path) to
	 * the name hashtable. The name hashtable resides in the corresponding
	 * path hashtable element.
	 */

	if (count == 0) {
		/* This entry does not require any iterating */
		nh_elt = malloc(sizeof(struct name_htbl_element));
		if (!nh_elt) {
			err_msg("cannot allocate %zd bytes of memory",
				sizeof(struct name_htbl_element));
			goto out_free;
		}

		nh_elt->mode = mode;
		nh_elt->uid = uid;
		nh_elt->gid = gid;
		nh_elt->dev = makedev(major, minor);

		dbg_msg(3, "inserting '%s' into name hash table (major %d, minor %d)",
			name, major(nh_elt->dev), minor(nh_elt->dev));

		if (hashtable_search(ph_elt->name_htbl, name)) {
			err_msg("'%s' is referred twice", buf);
			goto out_free;
		}

		nh_elt->name = name;
		if (!hashtable_insert(ph_elt->name_htbl, name, nh_elt)) {
			err_msg("cannot insert into name hash table");
			goto out_free;
		}
	} else {
		int i, num = start + count, len = strlen(name) + 20;
		char *nm;

		for (i = start; i < num; i++) {
			nh_elt = malloc(sizeof(struct name_htbl_element));
			if (!nh_elt) {
				err_msg("cannot allocate %zd bytes of memory",
					sizeof(struct name_htbl_element));
				goto out_free;
			}

			nh_elt->mode = mode;
			nh_elt->uid = uid;
			nh_elt->gid = gid;
			nh_elt->dev = makedev(major, minor + (i - start) * increment);

			nm = malloc(len);
			if (!nm) {
				err_msg("cannot allocate %d bytes of memory", len);
				goto out_free;
			}

			sprintf(nm, "%s%d", name, i);
			nh_elt->name = nm;

			dbg_msg(3, "inserting '%s' into name hash table (major %d, minor %d)",
			        nm, major(nh_elt->dev), minor(nh_elt->dev));

			if (hashtable_search(ph_elt->name_htbl, nm)) {
				err_msg("'%s' is referred twice", buf);
				free (nm);
				goto out_free;
			}

			if (!hashtable_insert(ph_elt->name_htbl, nm, nh_elt)) {
				err_msg("cannot insert into name hash table");
				free (nm);
				goto out_free;
			}
		}
		free(name);
		name = NULL;
	}

	return 0;

out_free:
	free(ph_elt);
	free(nh_elt);
	free(path);
	free(name);
	return -1;
}

/**
 * parse_devtable - parse the device table.
 * @tbl_file: device table file name
 *
 * This function parses the device table and prepare the hash table which will
 * later be used by mkfs.ubifs to create the specified files/device nodes.
 * Returns zero in case of success and a negative error code in case of
 * failure.
 */
int parse_devtable(const char *tbl_file)
{
	FILE *f;
	char *line = NULL;
	struct stat st;
	size_t len;

	dbg_msg(1, "parsing device table file '%s'", tbl_file);

	path_htbl = create_hashtable(128, &r5_hash, &is_equivalent);
	if (!path_htbl)
		return err_msg("cannot create path hash table");

	f = fopen(tbl_file, "r");
	if (!f)
		return sys_err_msg("cannot open '%s'", tbl_file);

	if (fstat(fileno(f), &st) < 0) {
		sys_err_msg("cannot stat '%s'", tbl_file);
		goto out_close;
	}

	if (st.st_size < 10) {
		sys_err_msg("'%s' is too short", tbl_file);
		goto out_close;
	}

	/*
	 * The general plan now is to read in one line at a time, check for
	 * leading comment delimiters ('#'), then try and parse the line as a
	 * device table
	 */
	while (getline(&line, &len, f) != -1) {
		/* First trim off any white-space */
		len = strlen(line);

		/* Trim trailing white-space */
		while (len > 0 && isspace(line[len - 1]))
			line[--len] = '\0';
		/* Trim leading white-space */
		memmove(line, &line[strspn(line, " \n\r\t\v")], len);

		/* How long are we after trimming? */
		len = strlen(line);

		/* If this is not a comment line, try to interpret it */
		if (len && *line != '#') {
			if (interpret_table_entry(line)) {
				err_msg("cannot parse '%s'", line);
				goto out_close;
			}
		}

		free(line);
		line = NULL;
	}

	dbg_msg(1, "finished parsing");
	fclose(f);
	return 0;

out_close:
	fclose(f);
	free_devtable_info();
	return -1;
}

/**
 * devtbl_find_path - find a path in the path hash table.
 * @path: UBIFS path to find.
 *
 * This looks up the path hash table. Returns the path hash table element
 * reference if @path was found and %NULL if not.
 */
struct path_htbl_element *devtbl_find_path(const char *path)
{
	if (!path_htbl)
		return NULL;

	return hashtable_search(path_htbl, (void *)path);
}

/**
 * devtbl_find_name - find a name in the name hash table.
 * @ph_etl: path hash table element to find at
 * @name: name to find
 *
 * This looks up the name hash table. Returns the name hash table element
 * reference if @name found and %NULL if not.
 */
struct name_htbl_element *devtbl_find_name(struct path_htbl_element *ph_elt,
					   const char *name)
{
	if (!path_htbl)
		return NULL;

	return hashtable_search(ph_elt->name_htbl, (void *)name);
}

/**
 * override_attributes - override inode attributes.
 * @st: struct stat object to containing the attributes to override
 * @ph_elt: path hash table element object
 * @nh_elt: name hash table element object containing the new values
 *
 * The device table file may override attributes like UID of files. For
 * example, the device table may contain a "/dev" entry, and the UBIFS FS on
 * the host may contain "/dev" directory. In this case the attributes of the
 * "/dev" directory inode has to be as the device table specifies.
 *
 * Note, the hash element is removed by this function as well.
 */
int override_attributes(struct stat *st, struct path_htbl_element *ph_elt,
			struct name_htbl_element *nh_elt)
{
	if (!path_htbl)
		return 0;

	if (S_ISCHR(st->st_mode) || S_ISBLK(st->st_mode) ||
	    S_ISFIFO(st->st_mode))
		return err_msg("%s/%s both exists at UBIFS root at host, "
			       "and is referred from the device table",
			       strcmp(ph_elt->path, "/") ? ph_elt->path : "",
			       nh_elt->name);

	if ((st->st_mode & S_IFMT) != (nh_elt->mode & S_IFMT))
		return err_msg("%s/%s is referred from the device table also exists in "
			       "the UBIFS root directory at host, but the file type is "
			       "different", strcmp(ph_elt->path, "/") ? ph_elt->path : "",
			       nh_elt->name);

	dbg_msg(3, "set UID %d, GID %d, mode %o for %s/%s as device table says",
		nh_elt->uid, nh_elt->gid, nh_elt->mode, ph_elt->path, nh_elt->name);

	st->st_uid = nh_elt->uid;
	st->st_gid = nh_elt->gid;
	st->st_mode = nh_elt->mode;

	hashtable_remove(ph_elt->name_htbl, (void *)nh_elt->name);
	return 0;
}

/**
 * first_name_htbl_element - return first element of the name hash table.
 * @ph_elt: the path hash table the name hash table belongs to
 * @itr: double pointer to a 'struct hashtable_itr' object where the
 *       information about further iterations is stored
 *
 * This function implements name hash table iteration together with
 * 'next_name_htbl_element()'. Returns the first name hash table element or
 * %NULL if the hash table is empty.
 */
struct name_htbl_element *
first_name_htbl_element(struct path_htbl_element *ph_elt,
			struct hashtable_itr **itr)
{
	if (!path_htbl || !ph_elt || hashtable_count(ph_elt->name_htbl) == 0)
		return NULL;

	*itr = hashtable_iterator(ph_elt->name_htbl);
	return hashtable_iterator_value(*itr);
}

/**
 * first_name_htbl_element - return next element of the name hash table.
 * @ph_elt: the path hash table the name hash table belongs to
 * @itr: double pointer to a 'struct hashtable_itr' object where the
 *       information about further iterations is stored
 *
 * This function implements name hash table iteration together with
 * 'first_name_htbl_element()'. Returns the next name hash table element or
 * %NULL if there are no more elements.
 */
struct name_htbl_element *
next_name_htbl_element(struct path_htbl_element *ph_elt,
		       struct hashtable_itr **itr)
{
	if (!path_htbl || !ph_elt || !hashtable_iterator_advance(*itr))
		return NULL;

	return hashtable_iterator_value(*itr);
}

/**
 * free_devtable_info - free device table information.
 *
 * This function frees the path hash table and the name hash tables.
 */
void free_devtable_info(void)
{
	struct hashtable_itr *ph_itr;
	struct path_htbl_element *ph_elt;

	if (!path_htbl)
		return;

	if (hashtable_count(path_htbl) > 0) {
		ph_itr = hashtable_iterator(path_htbl);
		do {
			ph_elt = hashtable_iterator_value(ph_itr);
			/*
			 * Note, since we use the same string for the key and
			 * @name in the name hash table elements, we do not
			 * have to iterate name hash table because @name memory
			 * will be freed when freeing the key.
			 */
			hashtable_destroy(ph_elt->name_htbl, 1);
		} while (hashtable_iterator_advance(ph_itr));
		free(ph_itr);
	}
	hashtable_destroy(path_htbl, 1);
}
