/* $NetBSD: fsu_ecp.c,v 1.7 2009/04/14 20:12:51 stacktic Exp $ */

/*
 * Copyright (c) 2008 Arnaud Ysmal.  All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/queue.h>
#include <sys/syslimits.h>
#include <sys/stat.h>

#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <rump/rump_syscalls.h>

#include <prop_mount.h>

#include "fsu_flist.h"

#define FSU_ECP_COPY_LINK (0x01)
#define FSU_ECP_RECURSIVE (FSU_ECP_COPY_LINK<<1)
#define FSU_ECP_VERBOSE (FSU_ECP_RECURSIVE<<1)
#define FSU_ECP_GET (FSU_ECP_VERBOSE<<1)
#define FSU_ECP_PUT (FSU_ECP_GET<<1)
#define FSU_ECP_IN (FSU_ECP_PUT<<1)
#define FSU_ECP_OUT (FSU_ECP_IN<<1)

#define BUFSIZE (8192)

static int copy_dir(const char *, const char *, int);
static int copy_dir_rec(const char *, char *, int);
static int copy_fifo(const char *, const char *, int);
static int copy_file(const char *, const char *, int);
static int copy_link(const char *, const char *, int);
static int copy_special(const char *, const char *, int);
static int copy_to_dir(const char *, struct stat *,
		       const char *, int);
static int copy_to_file(const char *, struct stat *,
			const char *, int);
static int fsu_ecp(const char *, const char *, int);
static int fsu_ecp_parse_arg(int *, char ***);
static void usage(void);

struct hardlink_s {
	char *hl_from;
	char **hl_to;
	int hl_nlink;
	LIST_ENTRY(hardlink_s) next;
};

int
main(int argc, char *argv[])
{
	size_t len;
	int cur_arg, flags, rv;

	setprogname(argv[0]);
	FSU_MOUNT(argc, argv);

	flags = fsu_ecp_parse_arg(&argc, &argv);
	if (flags == -1 || argc < 2) {
		usage();
		return -1;
	}

	for (rv = 0, cur_arg = 0; cur_arg < argc-1; ++cur_arg) {
		len = strlen(argv[cur_arg]);
		while (argv[cur_arg][--len] == '/')
			argv[cur_arg][len] = '\0';
		rv |= fsu_ecp(argv[cur_arg], argv[argc-1], flags);
	}

	return rv;
}

static int
fsu_ecp_parse_arg(int *argc, char ***argv)
{
	int flags, rv;
	const char *progname;

	flags = 0;
	progname = getprogname();

	if (strcmp(progname, "get") == 0 || strcmp(progname, "fsu_get") == 0)
		flags |= FSU_ECP_GET;
	else if (strcmp(progname, "put") == 0 ||
		 strcmp(progname, "fsu_put") == 0)
		flags |= FSU_ECP_PUT;


	while ((rv = getopt(*argc, *argv, "gPpRv")) != -1) {
		switch (rv) {
		case 'g':
			flags |= FSU_ECP_GET;
			flags &= ~FSU_ECP_PUT;
			break;
		case 'P':
			flags |= FSU_ECP_COPY_LINK;
			break;
		case 'p':
			flags |= FSU_ECP_PUT;
			flags &= ~FSU_ECP_GET;
			break;
		case 'R':
			flags |= FSU_ECP_RECURSIVE;
			break;
		case 'v':
			flags |= FSU_ECP_VERBOSE;
			break;
		case '?':
		default:
			return -1;
		}
	}
	*argc -= optind;
	*argv += optind;

	if ((flags & (FSU_ECP_GET | FSU_ECP_PUT)) == 0) {
		warnx("-g or -p should be specified");
		return -1;
	}

	return flags;
}

static int
fsu_ecp(const char *from, const char *to, int flags)
{
	struct stat from_stat, to_stat;
	int rv;
	
	if (flags & FSU_ECP_PUT)
		rv = lstat(from, &from_stat);
	else
		rv = rump_sys_lstat(from, &from_stat);
	if (rv == -1) {
		warn("%s", from);
		return -1;
	}

	if (S_ISDIR(from_stat.st_mode)) {
		if (!(flags & FSU_ECP_RECURSIVE)) {
			fprintf(stderr, "%s: is a directory\n", from);
			return -1;
		}
		return copy_dir(from, to, flags);
	}

	if (flags & FSU_ECP_GET)
		rv = stat(to, &to_stat);
	else
		rv = rump_sys_stat(to, &to_stat);
	if (rv == 0 && S_ISDIR(to_stat.st_mode))
		return copy_to_dir(from, &from_stat, to, flags);

	if (from_stat.st_ino == to_stat.st_ino) {
		fprintf(stderr, "%s and %s are identical (not copied).\n",
			from, to);
		return -1;
	}
	return copy_to_file(from, &from_stat, to, flags);
}

static int
copy_dir(const char *from, const char *to, int flags)
{
	const char *filename;
	int rv;
	struct stat file_stat;
	char to_p[PATH_MAX + 1];
	size_t tlen, flen;

	tlen = strlen(to);
	rv = strlcpy(to_p, to, PATH_MAX + 1);
	if (rv != (int)tlen) {
		warn("%s", to);
		return -1;
	}
	
	if (flags & FSU_ECP_GET)
		rv = lstat(to, &file_stat);
	else
		rv = rump_sys_lstat(to, &file_stat);
	if (rv == 0) {
		if (S_ISDIR(file_stat.st_mode)) {
			filename = strrchr(from, '/');
			if (filename == NULL)
				filename = from;
			else
				++filename;

			if (to_p[tlen - 1] != '/') {
				if (filename[0] != '/') {
					to_p[tlen] = '/';
					to_p[tlen + 1] = '\0';
				}
			} else
				--tlen;

			flen = strlen(filename);
			
			rv = strlcat(to_p, filename, PATH_MAX + 1);
			if (rv != (int)(flen + tlen + 1)) {
				warn("%s/%s", to_p, filename);
				return -1;
			}
		} else {
			warnx("%s: not a directory", to);
			return -1;
		}
	}

	return copy_dir_rec(from, to_p, flags);
}

static int
copy_dir_rec(const char *from_p, char *to_p, int flags)
{
	FSU_FENT *root, *cur, *cur2, *nextelt;
	struct stat sb;
	fsu_flist *flist;
	size_t len;
	int flist_options, res, rv, off, hl_supported, curlink;
	struct hardlink_s *new;
	char hlfrom[PATH_MAX + 1], hlto[PATH_MAX + 1];

	LIST_HEAD(, hardlink_s) hl_l = LIST_HEAD_INITIALIZER(hl_l);
	curlink = res = 0;
	hl_supported = 1;

	if (flags & FSU_ECP_COPY_LINK)
		flist_options = FSU_FLIST_STATLINK;
	else
		flist_options = 0;

	if (flags & FSU_ECP_RECURSIVE)
		flist_options |= FSU_FLIST_RECURSIVE;

	if (flags & FSU_ECP_PUT)
		flist_options |= FSU_FLIST_REALFS;

	len = strlen(to_p) - 1;
	flist = fsu_flist_build(from_p, flist_options);
	if (flist == NULL)
		return -1;
	root = LIST_FIRST(flist);

	off = root->pathlen;
	LIST_FOREACH(cur, flist, next) {
		if (S_ISDIR(cur->sb.st_mode) || cur->sb.st_nlink == 1)
			continue;

		new = malloc(sizeof(struct hardlink_s));
		if (new == NULL) {
			warn("malloc");
			return -1;
		}

		new->hl_from = cur->path;
		new->hl_nlink = cur->sb.st_nlink - 1;
		new->hl_to = malloc(new->hl_nlink * sizeof(char *));
		if (new->hl_to == NULL) {
			warn("malloc");
			free(new);
			return -1;
		}
		memset(new->hl_to, 0, new->hl_nlink * sizeof(char *));

		for (curlink = 0, cur2 = cur; LIST_NEXT(cur2, next) != NULL;) {
			nextelt = LIST_NEXT(cur2, next);
			if (S_ISDIR(nextelt->sb.st_mode) ||
			    nextelt->sb.st_nlink == 1) {
				cur2 = nextelt;
				continue;
			}
			if (flags & FSU_ECP_GET)
				rump_sys_lstat(nextelt->path, &sb);
			else if (flags & FSU_ECP_PUT)
				lstat(nextelt->path, &sb);

			if (S_ISLNK(sb.st_mode)) {
				cur2 = nextelt;
				continue;
			}

			if (cur->sb.st_ino == nextelt->sb.st_ino &&
			    cur->sb.st_dev == nextelt->sb.st_dev) {
				new->hl_to[curlink] = nextelt->path;
				if (new->hl_to[curlink] == NULL)
					warn("%s", nextelt->path);
				else
					curlink++;
				LIST_REMOVE(nextelt, next);
				free(nextelt);
				if (curlink + 1 == cur->sb.st_nlink)
					break;
			} else
				cur2 = nextelt;
		}
		LIST_INSERT_HEAD(&hl_l, new, next);
	}

	if (flags & FSU_ECP_GET)
		rv = mkdir(to_p, root->sb.st_mode);
	else
		rv = rump_sys_mkdir(to_p, root->sb.st_mode);
	if (rv == -1) {
		warn("%s", to_p);
		goto out;
	}

	if (!(flags & FSU_ECP_GET)) {
		rv = rump_sys_chown(to_p, root->sb.st_uid, root->sb.st_gid);
		if (rv == -1) {
			warn("chown %s", to_p);
		}
	}

	LIST_FOREACH(cur, flist, next) {
		if (cur == root)
			continue;
		rv = strlcat(to_p, cur->path + off, PATH_MAX+1);
		if (rv != (int)(len + cur->pathlen - off + 1)) {
			warn("%s%s", to_p, cur->path + off);
			res = -1;
			break;
		}

		if (S_ISDIR(cur->sb.st_mode)) {
			if (flags & FSU_ECP_GET) {
				rv = mkdir(to_p, cur->sb.st_mode);
				if (rv == -1) {
					warn("%s", to_p);
					res = -1;
					break;
				}
			} else {
				rv = rump_sys_mkdir(to_p, cur->sb.st_mode);
				if (rv == -1) {
					warn("%s", to_p);
					res = -1;
					break;
				}
				
				rv = rump_sys_chown(to_p, cur->sb.st_uid,
						    cur->sb.st_gid);
				if (rv == -1) {
					warn("chown %s", to_p);
				}
			}
		} else {
			res |= copy_to_file(cur->path, &(cur->sb), to_p, flags);
			if (errno == ENOSPC) {
				warn(NULL);
				goto out;
			}
		}
		to_p[len + 1] = '\0';
	}

	memcpy(hlfrom, to_p, len + 1);
	memcpy(hlto, to_p, len + 1);

	while (!LIST_EMPTY(&hl_l)) {
		new = LIST_FIRST(&hl_l);
		LIST_REMOVE(new, next);

		hlfrom[len + 1] = '\0';
		strlcat(hlfrom, new->hl_from + off, PATH_MAX+1);

		for (curlink = 0;
		     curlink < new->hl_nlink && new->hl_to[curlink] != NULL;
		     ++curlink) {

			hlto[len + 1] = '\0';
			strlcat(hlto, new->hl_to[curlink] + off, PATH_MAX+1);

			if (hl_supported) {
				if (flags & FSU_ECP_GET)
					rv = link(hlfrom, hlto);
				else
					rv = rump_sys_link(hlfrom, hlto);
				if (rv != 0 && errno == EOPNOTSUPP) {
					hl_supported = 0;
					if (flags & FSU_ECP_GET)
						copy_file(hlfrom, hlto,
						    FSU_ECP_OUT);
					else
						copy_file(hlfrom, hlto,
						    FSU_ECP_IN);
				}
			} else {
				if (flags & FSU_ECP_GET)
					copy_file(hlfrom, hlto, FSU_ECP_OUT);
				else
					copy_file(hlfrom, hlto, FSU_ECP_IN);	

			}

			free(new->hl_to[curlink]);
		}

		free(new->hl_from);
		free(new);
	}

	
out:
	fsu_flist_free(flist);

	return res;
}

static int
copy_to_dir(const char *from, struct stat *frstat,
	    const char *to, int flags)
{
	char path[PATH_MAX+1];
	const char *filename;
	int len;

	filename = strrchr(from, '/');
	if (filename == NULL)
		filename = from;
	else
		++filename;

	len = snprintf(path, PATH_MAX, "%s/%s", to, filename);
	path[len] = '\0';

	return copy_to_file(from, frstat, path, flags);
}

static int
copy_to_file(const char *from, struct stat *frstat,
	     const char *to, int flags)
{
	int rv;

	switch ((frstat->st_mode & S_IFMT)) {
	case S_IFIFO:
		rv = copy_fifo(from, to, flags);
		break;
	case S_IFLNK:
		if (flags & FSU_ECP_COPY_LINK)
			rv = copy_link(from, to, flags);
		else
			rv = copy_file(from, to, flags);
		break;
	case S_IFCHR: /* FALLTHROUGH */
	case S_IFBLK:
		rv = copy_special(from, to, flags);
		break;
	case S_IFREG:
		rv = copy_file(from, to, flags);
		break;
	default:
		return -1;
		/* NOTREACHED */
	}

	if (rv != 0)
		return -1;

	if (flags & FSU_ECP_GET)
		return rv;

	if (flags & FSU_ECP_COPY_LINK)
		rv = rump_sys_lchown(to, frstat->st_uid, frstat->st_gid);
	else
		rv = rump_sys_chown(to, frstat->st_uid, frstat->st_gid);
	if (rv == -1) {
		warn("chown %s", to);
	}

	return 0;
}

struct op_s {
	int (*op_open)(const char *, int, mode_t);
	int (*op_close)(int);
	ssize_t (*op_read)(int, void *, size_t);
	ssize_t (*op_write)(int, const void *, size_t);
	int (*op_stat)(const char *, struct stat *);
};

/* TODO */
static int
copy_file(const char *from, const char *to, int flags)
{
	uint8_t buf[BUFSIZE];
	ssize_t rd, wr;
	int fd, rv, fd2;
	struct stat from_stat;
	struct op_s op_from, op_to;

	if (flags & FSU_ECP_VERBOSE)
		printf("%s -> %s\n", from, to);

	if (flags & FSU_ECP_PUT)
		rv = stat(from, &from_stat);
	else
		rv = rump_sys_stat(from, &from_stat);
	if (rv == -1) {
		warn("%s", from);
		return -1;
	}

	/*
	 * PUT : std  -> rump
	 * GET : rump -> std
	 * IN  : rump -> rump
	 * OUT : std  -> std
	 */

	if (flags & (FSU_ECP_GET | FSU_ECP_IN)) {
		op_from.op_open = rump_sys_open;
		op_from.op_close = rump_sys_close;
		op_from.op_read = rump_sys_read;
		op_from.op_write = rump_sys_write;
		op_from.op_stat = rump_sys_stat;
	} else {
		op_from.op_open = (int (*)(const char *, int, mode_t))open;
		op_from.op_close = close;
		op_from.op_read = read;
		op_from.op_write = write;
		op_from.op_stat = stat;
	}

	if (flags & (FSU_ECP_PUT | FSU_ECP_IN)) {
		op_to.op_open = rump_sys_open;
		op_to.op_close = rump_sys_close;
		op_to.op_read = rump_sys_read;
		op_to.op_write = rump_sys_write;
		op_to.op_stat = rump_sys_stat;	
	} else {
		op_to.op_open = (int (*)(const char *, int, mode_t))open;
		op_to.op_close = close;
		op_to.op_read = read;
		op_to.op_write = write;
		op_to.op_stat = stat;
	}

	rv = op_from.op_stat(from, &from_stat);
	if (rv == -1) {
		warn("%s", from);
		return -1;
	}

	fd = op_from.op_open(from, O_RDONLY, from_stat.st_mode);
	if (fd == -1) {
		warn("%s", from);
		return -1;
	}
	fd2 = op_to.op_open(to, O_WRONLY|O_CREAT|O_EXCL, 0777);
	if (fd2 == -1) {
		warn("%s", to);
		op_from.op_close(fd);
		return -1;
	}
	
	do {
		rd = op_from.op_read(fd, buf, BUFSIZE);
		if (rd == -1) {
			warn("%s", from);
			rv = -1;
			goto out;
		}
		wr = op_to.op_write(fd2, buf, rd);
		if (wr == -1 || wr != rd) {
			warn("%s", to);
			rv = -1;
			goto out;
		}
	} while (rd > 0);

	rv = 0;
out:

	op_from.op_close(fd);
	op_to.op_close(fd2);

	return rv;
}

static int
copy_fifo(const char *from, const char *to, int flags)
{
	int rv;
	struct stat file_stat;

	if (flags & FSU_ECP_VERBOSE)
		printf("Copying fifo %s -> %s\n", from, to);

	if (flags & FSU_ECP_GET)
		rv = lstat(to, &file_stat);
	else
		rv = rump_sys_lstat(to, &file_stat);
	if (rv != -1) {
		if (flags & FSU_ECP_GET)
			rv = remove(to);
		else
			rv = rump_sys_unlink(to);
		if (rv == -1) {
			warn("%s", to);
			return -1;
		}
	}

	if (flags & FSU_ECP_PUT)
		rv = lstat(from, &file_stat);
	else
		rv = rump_sys_lstat(from, &file_stat);
	if (rv == -1) {
		warn("%s", from);
		return -1;
	}

	if (flags & FSU_ECP_GET)
		rv = mkfifo(to, file_stat.st_mode);
	else
		rv = rump_sys_mkfifo(to, file_stat.st_mode);
	if (rv == -1) {
		warn("%s", to);
		return -1;
	}
	
	return 0;
}

static int
copy_special(const char *from, const char *to, int flags)
{
	int rv;
	struct stat file_stat;

	if (flags & FSU_ECP_VERBOSE)
		printf("%s -> %s\n", from, to);

	if (flags & FSU_ECP_GET)
		rv = lstat(to, &file_stat);
	else
		rv = rump_sys_lstat(to, &file_stat);
	if (rv != -1) {
		rv = rump_sys_unlink(to);
		if (rv == -1) {
			warn("%s", to);
			return -1;
		}
	}

	if (flags & FSU_ECP_PUT)
		rv = lstat(from, &file_stat);
	else
		rv = rump_sys_lstat(from, &file_stat);
	if (rv == -1) {
		warn("%s", from);
		return -1;
	}

	if (flags & FSU_ECP_GET)
		rv = mknod(to, file_stat.st_mode, file_stat.st_rdev);
	else
		rv = rump_sys_mknod(to, file_stat.st_mode, file_stat.st_rdev);
	if (rv == -1) {
		warn("%s", to);
		return -1;
	}
	
	return 0;
}

static int
copy_link(const char *from, const char *to, int flags)
{
	char target[PATH_MAX+1];
	int rv;
	struct stat file_stat;

	if (flags & FSU_ECP_PUT)
		rv = readlink(from, target, PATH_MAX);
	else
		rv = rump_sys_readlink(from, target, PATH_MAX);
	if (rv == -1) {
		warn("%s", from);
		return -1;
	}
	target[rv] = '\0';

	if (flags & FSU_ECP_VERBOSE)
		printf("%s -> %s : %s", from, to, target);

	if (flags & FSU_ECP_GET)
		rv = lstat(to, &file_stat);
	else
		rv = rump_sys_lstat(to, &file_stat);
	if (rv != -1) {
		if (flags & FSU_ECP_GET)
			rv = remove(to);
		else
			rv = rump_sys_unlink(to);
		if (rv == -1) {
			warn("%s", to);
			return -1;
		}
	}

	if (flags & FSU_ECP_GET)
		rv = symlink(target, to);
	else
		rv = rump_sys_symlink(target, to);
	if (rv == -1) {
		warn("%s", target);
		return -1;
	}
	return 0;
}

static void
usage(void)
{

	fprintf(stderr,	"usage: %s %s [-gPpRv] src target\n"
		"usage: %s %s [-gPpRv] src... directory\n",
		getprogname(), fsu_mount_usage(),
		getprogname(), fsu_mount_usage());

	exit(EXIT_FAILURE);
}
