#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>

#include "nbase.h"

#ifndef WIN32
#include <dirent.h>
#include <getopt.h>
#include <pwd.h>
#include <unistd.h>
#include "config.h"
#else
#include <shlobj.h>
#include "win_config.h"
#endif

/* See the file tools/examples/minimal_client.c in the Subversion source
   directory for an example of using the svn_client API. */

#if HAVE_SUBVERSION_1_SVN_CLIENT_H
#include <subversion-1/svn_client.h>
#include <subversion-1/svn_cmdline.h>
#include <subversion-1/svn_opt.h>
#include <subversion-1/svn_pools.h>
#include <subversion-1/svn_types.h>
#else
#include <svn_client.h>
#include <svn_cmdline.h>
#include <svn_opt.h>
#include <svn_pools.h>
#include <svn_types.h>
#endif

/* From svn_auth.c. */
svn_error_t *
nmap_update_svn_cmdline_setup_auth_baton(svn_auth_baton_t **ab,
                             svn_boolean_t non_interactive,
                             const char *auth_username,
                             const char *auth_password,
                             const char *config_dir,
                             svn_boolean_t no_auth_cache,
                             svn_config_t *cfg,
                             svn_cancel_func_t cancel_func,
                             void *cancel_baton,
                             apr_pool_t *pool);

#include "default_channel.h"

#ifdef WIN32
#define PATHSEP "\\"
#else
#define PATHSEP "/"
#endif

static const char *DEFAULT_SVN_REPO = "https://svn.nmap.org/updates";

static const char *DEFAULT_CHANNELS[] = { DEFAULT_CHANNEL };


/* Internal error handling. */

#define NELEMS(a) (sizeof(a) / sizeof(*a))

#define internal_error(msg) \
do {\
	fprintf(stderr, "%s:%d: internal error: %s.\n", __FILE__, __LINE__, msg); \
	abort(); \
} while (0)

#define internal_assert(expr) \
do { \
	if (!(expr)) \
		internal_error("assertion failed: " #expr); \
} while (0)

static char *safe_strdup(const char *s)
{
	char *t;
	size_t len;

	len = strlen(s);
	t = safe_malloc(len + 1);
	memcpy(t, s, len);
	t[len] = '\0';

	return t;
}

static int streq(const char *a, const char *b)
{
	return strcmp(a, b) == 0;
}

static char *string_make(const char *begin, const char *end)
{
	char *s;

	s = safe_malloc(end - begin + 1);
	memcpy(s, begin, end - begin);
	s[end - begin] = '\0';

	return s;
}

static char *strbuf_append(char **buf, size_t *size, size_t *offset, const char *s, size_t n)
{
	internal_assert(*offset <= *size);

	/* Double the buffer size if necessary. */
	if (n >= *size - *offset) {
		*size = (*size + n) * 2;
		*buf = safe_realloc(*buf, *size + 1);
	}
	memcpy(*buf + *offset, s, n);
	*offset += n;
	(*buf)[*offset] = '\0';

	return *buf;
}

/* Append a '\0'-terminated string as with strbuf_append. */
static char *strbuf_append_str(char **buf, size_t *size, size_t *offset, const char *s)
{
	return strbuf_append(buf, size, offset, s, strlen(s));
}

static char *strbuf_append_char(char **buf, size_t *size, size_t *offset, char c)
{
	return strbuf_append(buf, size, offset, &c, 1);
}

static char *strbuf_trim(char **buf, size_t *size, size_t *offset)
{
	if (*offset < *size) {
		*size = *offset;
		*buf = safe_realloc(*buf, *size + 1);
	}
	internal_assert((*buf)[*size] == '\0');

	return *buf;
}

static char *string_unescape(const char *escaped)
{
	char *buf;
	size_t size, offset;
	const char *p;

	buf = NULL;
	size = 0;
	offset = 0;

	p = escaped;
	while (*p != '\0') {
		char hex[3], *tail;
		unsigned long byte;

		/* We support backslash escapes for '\\' and '"', and \xXX
		   hexadecimal only. */
		if (*p == '\\') {
			p++;
			switch (*p) {
			case '\\':
			case '"':
				strbuf_append_char(&buf, &size, &offset, *p);
				p++;
				break;
			case 'x':
				p++;
				if (!(isxdigit(*p) && isxdigit(*(p + 1))))
					goto bail;
				memcpy(hex, p, 2);
				hex[2] = '\0';

				errno = 0;
				byte = strtoul(hex, &tail, 16);
				if (errno != 0 || byte > 255 || *tail != '\0')
					goto bail;
				strbuf_append_char(&buf, &size, &offset, (char) byte);

				p += 2;
				break;
			default:
				goto bail;
				break;
			}
		} else {
			strbuf_append_char(&buf, &size, &offset, *p);
			p++;
		}
	}

	return strbuf_trim(&buf, &size, &offset);

bail:
	if (buf != NULL)
		free(buf);

	return NULL;
}

/* Return a newly allocated string that is the concatenation of all the va_list
   args, separated by join:
     str1 JOIN str2 JOIN str3 ...
   The final argument must be NULL. */
static char *strs_vjoin(const char *join, const char *first, va_list ap)
{
	char *buf;
	size_t size, offset;
	const char *p;

	internal_assert(first != NULL);

	buf = NULL;
	size = 0;
	offset = 0;

	strbuf_append_str(&buf, &size, &offset, first);

	while ((p = va_arg(ap, const char *)) != NULL) {
		strbuf_append_str(&buf, &size, &offset, join);
		strbuf_append_str(&buf, &size, &offset, p);
	}

	strbuf_trim(&buf, &size, &offset);

	return buf;
}

static char *strs_cat(const char *first, ...)
{
	va_list ap;
	char *result;

	va_start(ap, first);
	result = strs_vjoin("", first, ap);
	va_end(ap);

	return result;
}

static char *path_join(const char *first, ...)
{
	va_list ap;
	char *result;

	va_start(ap, first);
	result = strs_vjoin(PATHSEP, first, ap);
	va_end(ap);

	return result;
}

#ifdef WIN32
static char *get_user_dir(const char *subdir) {
	char appdata[MAX_PATH];

	if (SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, appdata) != S_OK)
		return NULL;

	return path_join(appdata, "nmap", subdir, NULL);
}
#else
static char *get_user_dir(const char *subdir) {
	static struct passwd *pw;

	errno = 0;
	pw = getpwuid(getuid());
	if (pw == NULL)
		return NULL;

	return path_join(pw->pw_dir, ".nmap", subdir, NULL);
}
#endif

static char *get_install_dir(void) {
	return get_user_dir("updates");
}

static char *get_staging_dir(void) {
	return get_user_dir("updates-staging");
}

static char *get_conf_filename(void) {
	return get_user_dir("nmap-update.conf");
}


/* Configuration file parsing. */

enum token_type {
	TOKEN_ERROR,
	TOKEN_EOL,
	TOKEN_EOF,
	TOKEN_WORD,
	TOKEN_EQUALS,
	TOKEN_STRING,
};

struct config_parser {
	FILE *fp;
	unsigned long lineno;
};

struct config_entry {
	char *key;
	char *value;
};

static void config_entry_free(struct config_entry *entry)
{
	free(entry->key);
	free(entry->value);
}

static int config_parser_open(const char *filename, struct config_parser *cp)
{
	cp->fp = fopen(filename, "r");
	if (cp->fp == NULL)
		return -1;
	cp->lineno = 1;

	return 0;
}

static int config_parser_close(struct config_parser *cp)
{
	int ret;

	ret = fclose(cp->fp);
	if (ret == EOF)
		return -1;

	return 0;
}

static int is_word_char(int c)
{
	return c != EOF && !isspace(c) && c != '"' && c != '#';
}

static char *read_quoted_string(struct config_parser *cp)
{
	char *buf, *unescaped;
	size_t size, offset;
	int c;

	buf = NULL;
	size = 0;
	offset = 0;

	for (;;) {
		errno = 0;
		c = fgetc(cp->fp);
		if (c == EOF)
			/* EOF in the middle of a string is always an error. */
			return NULL;
		if (c == '\n')
			return NULL;
		if (c == '"')
			break;
		if (c == '\\') {
			strbuf_append_char(&buf, &size, &offset, c);
			errno = 0;
			c = fgetc(cp->fp);
			if (c == EOF)
				return NULL;
		}
		strbuf_append_char(&buf, &size, &offset, c);
	}

	unescaped = string_unescape(buf);
	free(buf);

	return unescaped;
}

static enum token_type config_parser_read_token(struct config_parser *cp,
	char **token)
{
	size_t size, offset;
	unsigned long prev_lineno;
	int c;

	*token = NULL;
	size = 0;
	offset = 0;

	/* Skip comments and blank space. */
	prev_lineno = cp->lineno;
	do {
		errno = 0;
		while (isspace(c = fgetc(cp->fp))) {
			if (c == '\n')
				cp->lineno++;
		}
		if (c == EOF) {
			if (errno != 0)
				goto bail;
			*token = NULL;
			return TOKEN_EOF;
		}
		if (c == '#') {
			while ((c = fgetc(cp->fp)) != EOF && c != '\n')
				;
			if (c == EOF) {
				if (errno != 0)
					goto bail;
				*token = NULL;
				return TOKEN_EOF;
			} else if (c == '\n') {
				cp->lineno++;
			}
		}
	} while (isspace(c) || c == '#');

	/* Collapse multiple consecutive line endings. */
	if (cp->lineno != prev_lineno) {
		ungetc(c, cp->fp);
		*token = NULL;
		return TOKEN_EOL;
	}

	if (c == '=') {
		strbuf_append_char(token, &size, &offset, c);
		return TOKEN_EQUALS;
	} else if (is_word_char(c)) {
		while (is_word_char(c)) {
			strbuf_append_char(token, &size, &offset, c);
			errno = 0;
			c = fgetc(cp->fp);
			if (c == EOF && errno != 0)
				goto bail;
		}
		return TOKEN_WORD;
	} else if (c == '"') {
		char *qs;

		qs = read_quoted_string(cp);
		if (qs == NULL)
			goto bail;
		*token = safe_strdup(qs);
		return TOKEN_STRING;
	} else {
		goto bail;
	}

bail:
	if (*token != NULL)
		free(*token);
	*token = NULL;

	return TOKEN_ERROR;
}

static int config_parser_next(struct config_parser *cp, struct config_entry *entry)
{
	char *token;
	enum token_type type;

	while ((type = config_parser_read_token(cp, &token)) == TOKEN_EOL)
		;
	if (type == TOKEN_EOF) {
		free(token);
		return 0;
	}
	if (type != TOKEN_WORD) {
		free(token);
		return -1;
	}
	entry->key = token;

	type = config_parser_read_token(cp, &token);
	if (type != TOKEN_EQUALS) {
		free(token);
		return -1;
	}
	free(token);

	type = config_parser_read_token(cp, &token);
	if (!(type == TOKEN_WORD || type == TOKEN_STRING)) {
		free(token);
		return -1;
	}
	entry->value = token;

	return 1;
}


/* Global state. */

static char *program_name;
static struct {
	int verbose;
	const char *install_dir;
	const char *staging_dir;
	const char *conf_filename;
	const char **channels;
	unsigned int num_channels;
	char *svn_repo;
	char *username;
	char *password;
} options;

struct metadata {
	int is_expired;
	time_t expiry_date;
};

static void metadata_init(struct metadata *metadata)
{
	metadata->is_expired = 0;
	metadata->expiry_date = 0;
}

static void init_options(void)
{
	options.verbose = 0;
	options.install_dir = get_install_dir();
	if (options.install_dir == NULL) {
		fprintf(stderr, "Could not find an install directory: %s.\n",
			strerror(errno));
		exit(1);
	}
	options.staging_dir = get_staging_dir();
	if (options.staging_dir == NULL) {
		fprintf(stderr, "Could not find a staging directory: %s.\n",
			strerror(errno));
		exit(1);
	}
	options.conf_filename = get_conf_filename();
	if (options.conf_filename == NULL) {
		fprintf(stderr, "Could not find the configuration file: %s.\n",
			strerror(errno));
		exit(1);
	}
	options.channels = DEFAULT_CHANNELS;
	options.num_channels = NELEMS(DEFAULT_CHANNELS);

	options.svn_repo = NULL;
	options.username = NULL;
	options.password = NULL;
}

static int read_config_file(const char *conf_filename)
{
	struct config_parser cp;
	struct config_entry entry;
	int ret;

	if (options.verbose)
		printf("Trying to open configuration file %s.\n", conf_filename);

	errno = 0;
	if (config_parser_open(conf_filename, &cp) == -1) {
		if (options.verbose)
			printf("Failed to open %s: %s.\n", conf_filename, strerror(errno));
		return -1;
	}

	while ((ret = config_parser_next(&cp, &entry)) > 0) {
		if (streq(entry.key, "username")) {
			if (options.username != NULL) {
				fprintf(stderr, "Warning: %s:%lu: duplicate \"%s\".\n",
					conf_filename, cp.lineno, entry.key);
				free(options.username);
			}
			options.username = safe_strdup(entry.value);
		} else if (streq(entry.key, "password")) {
			if (options.password != NULL) {
				fprintf(stderr, "Warning: %s:%lu: duplicate \"%s\".\n",
					conf_filename, cp.lineno, entry.key);
				free(options.password);
			}
			options.password = safe_strdup(entry.value);
		} else if (streq(entry.key, "repo")) {
			if (options.svn_repo != NULL) {
				fprintf(stderr, "Warning: %s:%lu: duplicate \"%s\".\n",
					conf_filename, cp.lineno, entry.key);
				free(options.svn_repo);
			}
			options.svn_repo = safe_strdup(entry.value);
		} else {
			fprintf(stderr, "Warning: %s:%lu: unknown key \"%s\".\n",
				conf_filename, cp.lineno, entry.key);
		}

		config_entry_free(&entry);
	}
	if (ret == -1) {
		fprintf(stderr, "Parse error on line %lu of %s.\n",
			cp.lineno, conf_filename);
		exit(1);
	}

	errno = 0;
	if (config_parser_close(&cp) == -1) {
		if (options.verbose)
			printf("Failed to close %s: %s.\n", conf_filename, strerror(errno));
		return -1;
	}

	return 0;
}

static int parse_date(const char *s, time_t *t)
{
	struct tm tm = {0};

	if (sscanf(s, "%d-%d-%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3)
		return -1;
	tm.tm_year -= 1900;
	tm.tm_mon -= 1;
	*t = mktime(&tm);
	if (*t == -1)
		return -1;

	return 0;
}

static int date_is_after(time_t t, time_t now)
{
	return difftime(t, now) > 0;
}

static int read_metadata_file(const char *metadata_filename, struct metadata *metadata)
{
	struct config_parser cp;
	struct config_entry entry;
	int ret;

	errno = 0;
	if (config_parser_open(metadata_filename, &cp) == -1) {
		/* A missing file is not an error for metadata. */
		return 0;
	}

	while ((ret = config_parser_next(&cp, &entry)) > 0) {
		if (streq(entry.key, "expired")) {
			if (parse_date(entry.value, &metadata->expiry_date) == -1) {
				fprintf(stderr, "Warning: %s:%lu: can't parse date \"%s\".\n",
					metadata_filename, cp.lineno, entry.value);
			} else {
				if (date_is_after(time(NULL), metadata->expiry_date))
					metadata->is_expired = 1;
			}
		} else {
			fprintf(stderr, "Warning: %s:%lu: unknown key \"%s\".\n",
				metadata_filename, cp.lineno, entry.key);
		}

		config_entry_free(&entry);
	}
	if (ret == -1) {
		fprintf(stderr, "Parse error on line %lu of %s.\n",
			cp.lineno, metadata_filename);
		config_parser_close(&cp);
		return -1;
	}

	errno = 0;
	if (config_parser_close(&cp) == -1) {
		if (options.verbose)
			printf("Failed to close %s: %s.\n", metadata_filename, strerror(errno));
		return -1;
	}

	return 0;
}


static void usage(FILE *fp)
{
	char *install_dir;

	internal_assert(program_name != NULL);
	install_dir = get_install_dir();
	fprintf(fp, "\
Usage: %s [-d INSTALL_DIR] [CHANNEL...]\n\
Updates system-independent Nmap files. By default the new files are installed to\n\
%s. Each CHANNEL is a version number like \"" DEFAULT_CHANNEL "\".\n\
\n\
  -d DIR               install files to DIR (default %s).\n\
  -h, --help           show this help.\n\
  -r, --repo REPO      use REPO as SVN repository and path (default %s).\n\
  -v, --verbose        be more verbose.\n\
  --username USERNAME  use this username.\n\
  --password PASSWORD  use this password.\n\
", program_name, install_dir, install_dir, DEFAULT_SVN_REPO);
	free(install_dir);
}

static void usage_error(void)
{
	usage(stderr);
	exit(1);
}


static const char *try_channels(const char *channels[], unsigned int num_channels);
static int stage_and_install(const char *channel);
static int stage_channel(const char *channel, const char *staging_dir);
static int install(const char *staging_dir, const char *install_dir);
static int channel_is_expired(const char *channel, time_t *expiry_date);

static void summarize_options(void)
{
	unsigned int i;

	printf("Installing to directory: %s.\n", options.install_dir);
	printf("Using staging directory: %s.\n", options.staging_dir);

	printf("Using channels:");
	for (i = 0; i < options.num_channels; i++)
		printf(" %s", options.channels[i]);
	printf(".\n");
}

const struct option LONG_OPTIONS[] = {
	{ "help", no_argument, NULL, 'h' },
	{ "repo", required_argument, NULL, 'r' },
	{ "verbose", required_argument, NULL, 'v' },
	{ "username", required_argument, NULL, '?' },
	{ "password", required_argument, NULL, '?' },
};

int main(int argc, char *argv[])
{
	int opt, longoptidx;
	const char *successful_channel;
	const char *username, *password, *svn_repo;
	time_t expiry_date;

	internal_assert(argc > 0);
	program_name = argv[0];

	init_options();

	if (svn_cmdline_init(program_name, stderr) != 0)
		internal_error("svn_cmdline_init");

	username = NULL;
	password = NULL;
	svn_repo = NULL;

	while ((opt = getopt_long(argc, argv, "d:hr:v", LONG_OPTIONS, &longoptidx)) != -1) {
		if (opt == 'd') {
			options.install_dir = optarg;
		} else if (opt == 'h') {
			usage(stdout);
			exit(0);
		} else if (opt == 'r') {
			svn_repo = optarg;
		} else if (opt == 'v') {
			options.verbose = 1;
		} else if (opt == '?' && streq(LONG_OPTIONS[longoptidx].name, "username")) {
			username = optarg;
		} else if (opt == '?' && streq(LONG_OPTIONS[longoptidx].name, "password")) {
			password = optarg;
		} else {
			usage_error();
		}
	}

	/* User-specified channels. */
	if (optind < argc) {
		options.channels = (const char **) argv + optind;
		options.num_channels = argc - optind;
	}
	internal_assert(options.channels != NULL);
	internal_assert(options.num_channels > 0);

	if (options.verbose)
		summarize_options();

	read_config_file(options.conf_filename);

	/* Default options. */
	if (options.svn_repo == NULL)
		options.svn_repo = safe_strdup(DEFAULT_SVN_REPO);

	/* Possibly override configuration file. */
	if (username != NULL) {
		free(options.username);
		options.username = safe_strdup(username);
	}
	if (password != NULL) {
		free(options.password);
		options.password = safe_strdup(password);
	}
	if (svn_repo != NULL) {
		free(options.svn_repo);
		options.svn_repo = safe_strdup(svn_repo);
	}

	successful_channel = try_channels(options.channels, options.num_channels);

	if (successful_channel != NULL && channel_is_expired(successful_channel, &expiry_date)) {
		fprintf(stderr, "\
\n\
UPDATE CHANNEL %s HAS EXPIRED:\n\
\n\
The channel %s has expired and won't receive any more\n\
updates.  Visit https://nmap.org for a newer Nmap release with \n\
supported updates.\n\
", successful_channel, successful_channel);
	}

	if (successful_channel == NULL && options.username == NULL) {
		fprintf(stderr, "\
\n\
Could not stage any channels and don't have authentication credentials.\n\
\n\
Edit the file %s and enter your username and password. For example:\n\
  username = user\n\
  password = secret\n\
", options.conf_filename);
	}

	if (successful_channel != NULL)
		return 0;
	else
		return 1;
}


static const char *try_channels(const char *channels[], unsigned int num_channels)
{
	unsigned int i;

	for (i = 0; i < num_channels; i++) {
		if (stage_and_install(channels[i]) == 0)
			return channels[i];
	}

	return NULL;
}


static void fatal_err_svn(svn_error_t *err)
{
	svn_handle_error2(err, stderr, TRUE, "nmap-update: ");
}

static svn_error_t *checkout_svn(const char *url, const char *path)
{
	svn_error_t *err;
	apr_pool_t *pool;
	svn_opt_revision_t peg_revision, revision;
	svn_client_ctx_t *ctx;
	svn_revnum_t revnum;
	svn_config_t *cfg;

	peg_revision.kind = svn_opt_revision_unspecified;
	revision.kind = svn_opt_revision_head;

	pool = svn_pool_create(NULL);

	err = svn_client_create_context(&ctx, pool);
	if (err != NULL)
		fatal_err_svn(err);

	/* The creation of this directory is needed to cache credentials. */
	err = svn_config_ensure(NULL, pool);
	if (err != NULL)
		fatal_err_svn(err);

	err = svn_config_get_config(&ctx->config, NULL, pool);
	if (err != NULL)
		fatal_err_svn(err);
	cfg = apr_hash_get(ctx->config, SVN_CONFIG_CATEGORY_CONFIG,
		APR_HASH_KEY_STRING);
	svn_config_set_bool(cfg, SVN_CONFIG_SECTION_GLOBAL,
		SVN_CONFIG_OPTION_SSL_TRUST_DEFAULT_CA, TRUE);
	nmap_update_svn_cmdline_setup_auth_baton(&ctx->auth_baton,
		FALSE, /* non_interactive */
		options.username, /* username */
		options.password, /* password */
		NULL, /* config_dir */
		FALSE, /* no_auth_cache */
		cfg, /* cfg */
		NULL, /* cancel_func */
		NULL, /* cancel_baton */
		pool);

	err = svn_client_checkout2(&revnum, url, path,
		&peg_revision, &revision,
		TRUE, /* recurse */
		TRUE, /* ignore_externals */
		ctx, pool);
	svn_pool_destroy(pool);
	if (err != NULL)
		return err;

	printf("Checked out r%lu\n", (unsigned long) revnum);

	return SVN_NO_ERROR;
}

static int stage_and_install(const char *channel)
{
	char *staging_dir, *install_dir;
	int rc;

	internal_assert(options.staging_dir != NULL);

	staging_dir = path_join(options.staging_dir, channel, NULL);
	rc = stage_channel(channel, staging_dir);
	if (rc == -1) {
		free(staging_dir);
		return -1;
	}

	install_dir = path_join(options.install_dir, channel, NULL);
	rc = install(staging_dir, install_dir);

	free(staging_dir);
	free(install_dir);

	return rc;
}

static int stage_channel(const char *channel, const char *staging_dir)
{
	char *svn_url;
	svn_error_t *err;
	int rc;

	rc = 0;

	svn_url = strs_cat(options.svn_repo, "/", channel, NULL);

	if (options.verbose)
		printf("Checking out %s to %s.\n", svn_url, staging_dir);

	printf("\
\n\
The Nmap Updater is currently only available to a small set of users\n\
for testing purposes. We hope to expand it in the future.\n\
\n\
");

	err = checkout_svn(svn_url, staging_dir);
	if (err != NULL) {
		svn_handle_error2(err, stderr, FALSE, "nmap-update: ");
		fprintf(stderr, "Error checking out %s.\n", svn_url);
		rc = -1;
	}

	free(svn_url);

	return rc;
}

static int channel_is_expired(const char *channel, time_t *expiry_date)
{
	char *metadata_filename;
	struct metadata metadata;
	int rc;

	metadata_init(&metadata);

	metadata_filename = path_join(options.staging_dir, channel, "metadata.conf", NULL);
	rc = read_metadata_file(metadata_filename, &metadata);
	if (rc == -1) {
		fprintf(stderr, "Can't read metadata file %s.\n", metadata_filename);
		free(metadata_filename);
		exit(1);
	}
	free(metadata_filename);

	*expiry_date = metadata.expiry_date;

	return metadata.is_expired;
}

static int copy_tree(const char *from_dirname, const char *to_dirname);
static int rename_file(const char *from_filename, const char *to_filename);

static int install(const char *staging_dir, const char *install_dir)
{
	if (options.verbose)
		printf("Installing from %s to %s.\n", staging_dir, install_dir);

	return copy_tree(staging_dir, install_dir);
}

static int copy_file(const char *from_filename, const char *to_filename)
{
	char buf[BUFSIZ];
	char *tmp_filename;
	FILE *from_fd, *tmp_fd;
	int rc, from_rc, tmp_rc;
	size_t nr, nw;

	tmp_filename = NULL;
	from_fd = NULL;
	tmp_fd = NULL;

	errno = 0;
	from_fd = fopen(from_filename, "rb");
	if (from_fd == NULL) {
		fprintf(stderr, "Can't open %s: %s.\n", from_filename, strerror(errno));
		goto bail;
	}

	tmp_filename = strs_cat(to_filename, "-tmp", NULL);
	errno = 0;
	tmp_fd = fopen(tmp_filename, "wb");
	if (tmp_fd == NULL) {
		fprintf(stderr, "Can't open %s: %s.\n", tmp_filename, strerror(errno));
		goto bail;
	}

	errno = 0;
	while ((nr = fread(buf, 1, sizeof(buf), from_fd)) != 0) {
		errno = 0;
		nw = fwrite(buf, 1, nr, tmp_fd);
		if (nw != nr || errno != 0) {
			printf("%lu %lu\n", nw, nr);
			fprintf(stderr, "Error writing to %s: %s.\n", tmp_filename, strerror(errno));
			goto bail;
		}
	}
	if (errno != 0) {
		fprintf(stderr, "Error reading from %s: %s.\n", from_filename, strerror(errno));
		goto bail;
	}

	from_rc = fclose(from_fd);
	from_fd = NULL;
	if (from_rc == -1) {
		fprintf(stderr, "Can't close %s: %s.\n", from_filename, strerror(errno));
		goto bail;
	}
	tmp_rc = fclose(tmp_fd);
	tmp_fd = NULL;
	if (tmp_rc == -1) {
		fprintf(stderr, "Can't close %s: %s.\n", to_filename, strerror(errno));
		goto bail;
	}

	rc = rename_file(tmp_filename, to_filename);
	if (rc == -1) {
		fprintf(stderr, "Can't rename %s to %s: %s.\n",
			tmp_filename, to_filename, strerror(errno));
		goto bail;
	}

	free(tmp_filename);
	tmp_filename = NULL;

	return 0;

bail:
	if (from_fd != NULL)
		fclose(from_fd);
	if (tmp_fd != NULL)
		fclose(tmp_fd);
	if (tmp_filename != NULL)
		free(tmp_filename);

	return -1;
}

static int is_pathsep(int c)
{
#ifdef WIN32
	return c == '/' || c == '\\';
#else
	return c == '/';
#endif
}

static char *parent_dir(const char *path)
{
	const char *p;

	p = path + strlen(path) - 1;
	while (p > path && is_pathsep(*p))
		p--;
	while (p > path && !is_pathsep(*p))
		p--;
	while (p > path && is_pathsep(*p))
		p--;

	if (p == path)
		return safe_strdup("/");

	return string_make(path, p + 1);
}

#ifdef WIN32
static int rename_file(const char *from_filename, const char *to_filename)
{
	int rc;

	/* Windows rename doesn't remove the destination if it exists. */
	errno = 0;
	rc = _unlink(to_filename);
	if (rc == -1 && errno != ENOENT)
		return -1;

	return rename(from_filename, to_filename);
}

static int makedir(const char *dirname)
{
	return CreateDirectory(dirname, NULL) != 0 ? 0 : -1;
}

static int makedirs(const char *dirname)
{
	char *parent;
	int rc;

	rc = makedir(dirname);
	if (rc == 0 || GetLastError() == ERROR_ALREADY_EXISTS)
		return 0;

	if (GetLastError() != ERROR_PATH_NOT_FOUND)
		return -1;

	parent = parent_dir(dirname);
	rc = makedirs(parent);
	free(parent);
	if (rc == -1)
		return -1;

	rc = makedir(dirname);
	if (rc == -1)
		return -1;

	return rc;
}

static int copy_tree(const char *from_dirname, const char *to_dirname)
{
	WIN32_FIND_DATA ffd;
	HANDLE find_handle;
	DWORD dwError;
	char *from_pattern;
	int rc;

	rc = makedirs(to_dirname);
	if (rc == -1) {
		fprintf(stderr, "Can't create the directory %s: %s.\n",
			to_dirname, strerror(errno));
		return -1;
	}

	from_pattern = path_join(from_dirname, "*", NULL);
	find_handle = FindFirstFile(from_pattern, &ffd);
	free(from_pattern);
	if (find_handle == INVALID_HANDLE_VALUE) {
		fprintf(stderr, "Can't open the directory %s.\n", from_dirname);
		return -1;
	}

	do {
		char *from_path, *to_path;
		int error;

		from_path = path_join(from_dirname, ffd.cFileName, NULL);
		to_path = path_join(to_dirname, ffd.cFileName, NULL);

		error = 0;
		if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
			if (streq(ffd.cFileName, ".") || streq(ffd.cFileName, ".."))
				continue;
			if (streq(ffd.cFileName, ".svn"))
				continue;
			rc = makedirs(to_path);
			if (rc == 0) {
				rc = copy_tree(from_path, to_path);
				if (rc == -1)
					error = 1;
			} else {
				error = 1;
			}
		} else {
			rc = copy_file(from_path, to_path);
			if (rc == -1)
				error = 1;
		}

		free(from_path);
		free(to_path);

		if (error)
			goto bail;
	} while (FindNextFile(find_handle, &ffd) != 0);
	dwError = GetLastError();
	if (dwError != ERROR_NO_MORE_FILES) {
		fprintf(stderr, "Error in FindFirstFile/FindNextFile.\n");
		goto bail;
	}

	FindClose(find_handle);

	return 0;

bail:
	FindClose(find_handle);

	return -1;
}
#else
static int rename_file(const char *from_filename, const char *to_filename)
{
	return rename(from_filename, to_filename);
}

static int makedir(const char *dirname)
{
	return mkdir(dirname, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
}

static int makedirs(const char *dirname)
{
	char *parent;
	int rc;

	rc = makedir(dirname);
	if (rc == 0 || errno == EEXIST)
		return 0;

	if (errno != ENOENT)
		return -1;

	parent = parent_dir(dirname);
	rc = makedirs(parent);
	free(parent);
	if (rc == -1)
		return -1;

	rc = makedir(dirname);
	if (rc == -1)
		return -1;

	return rc;
}

static int copy_tree(const char *from_dirname, const char *to_dirname)
{
	DIR *dir;
	const struct dirent *ent;
	int rc;

	rc = makedirs(to_dirname);
	if (rc == -1) {
		fprintf(stderr, "Can't create the directory %s: %s.\n",
			to_dirname, strerror(errno));
		return -1;
	}

	dir = opendir(from_dirname);
	if (dir == NULL) {
		fprintf(stderr, "Can't open the directory %s: %s.\n",
			from_dirname, strerror(errno));
		return -1;
	}

	errno = 0;
	while ((ent = readdir(dir)) != NULL) {
		char *from_path, *to_path;
		int error;

		from_path = path_join(from_dirname, ent->d_name, NULL);
		to_path = path_join(to_dirname, ent->d_name, NULL);

		error = 0;
		if (ent->d_type == DT_DIR) {
			if (streq(ent->d_name, ".") || streq(ent->d_name, ".."))
				continue;
			if (streq(ent->d_name, ".svn"))
				continue;
			rc = makedirs(to_path);
			if (rc == 0) {
				rc = copy_tree(from_path, to_path);
				if (rc == -1)
					error = 1;
			} else {
				error = 1;
			}
		} else if (ent->d_type == DT_REG) {
			rc = copy_file(from_path, to_path);
			if (rc == -1)
				error = 1;
		} else {
			fprintf(stderr, "Warning: unknown file type %u of %s.\n",
				ent->d_type, ent->d_name);
		}

		free(from_path);
		free(to_path);

		if (error)
			goto bail;
	}
	if (errno != 0) {
		fprintf(stderr, "Error in readdir: %s.\n", strerror(errno));
		goto bail;
	}

	rc = closedir(dir);
	if (rc == -1) {
		fprintf(stderr, "Can't close the directory %s: %s.\n",
			from_dirname, strerror(errno));
		return -1;
	}

	return 0;

bail:
	closedir(dir);

	return -1;
}
#endif
