/*
 * Copyright (c) 2013, Red Hat Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *     * Redistributions of source code must retain the above
 *       copyright notice, this list of conditions and the
 *       following disclaimer.
 *     * 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.
 *     * The names of contributors to this software may not be
 *       used to endorse or promote products derived from this
 *       software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "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
 * COPYRIGHT OWNER 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.
 *
 * Author: Stef Walter <stefw@redhat.com>
 */

#include "config.h"

#include "buffer.h"
#include "debug.h"
#include "dict.h"
#include "message.h"
#include "save.h"

#include <assert.h>
#include <dirent.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#ifdef ENABLE_NLS
#include <libintl.h>
#define _(x) dgettext(PACKAGE_NAME, x)
#else
#define _(x) (x)
#endif

#ifndef O_DIRECTORY
#define O_DIRECTORY 0
#endif

#define MAX_FILE_NAME 255

struct _p11_save_file {
	char *bare;
	char *extension;
	char *temp;
	int fd;
	int flags;
};

struct _p11_save_dir {
	p11_dict *cache;
	char *path;
	int flags;
};

static char *   make_unique_name    (const char *bare,
                                     const char *extension,
                                     int (*check) (void *, char *),
                                     void *data);
static void filo_free (p11_save_file *file);
static void dir_free (p11_save_dir *dir);

bool
p11_save_write_and_finish (p11_save_file *file,
                           const void *data,
                           ssize_t length)
{
	bool ret;

	if (!file)
		return false;

	ret = p11_save_write (file, data, length);
	if (!p11_save_finish_file (file, NULL, ret))
		ret = false;

	return ret;
}

p11_save_file *
p11_save_open_file (const char *path,
                    const char *extension,
                    int flags)
{
	p11_save_file *file;
	char *temp;
	mode_t mode;
	int fd;

	return_val_if_fail (path != NULL, NULL);

	if (extension == NULL)
		extension = "";

	if (asprintf (&temp, "%s%s.XXXXXX", path, extension) < 0)
		return_val_if_reached (NULL);

	mode = umask (0077);
	fd = mkstemp (temp);
	umask (mode);
	if (fd < 0) {
		p11_message_err (errno, _("couldn't create file: %s%s"), path, extension);
		free (temp);
		return NULL;
	}

	file = calloc (1, sizeof (p11_save_file));
	return_val_if_fail (file != NULL, NULL);
	file->temp = temp;
	file->bare = strdup (path);
	if (file->bare == NULL) {
		filo_free (file);
		return_val_if_reached (NULL);
	}
	file->extension = strdup (extension);
	if (file->extension == NULL) {
		filo_free (file);
		return_val_if_reached (NULL);
	}
	file->flags = flags;
	file->fd = fd;

	return file;
}

bool
p11_save_write (p11_save_file *file,
                const void *data,
                ssize_t length)
{
	const unsigned char *buf = data;
	ssize_t written = 0;
	ssize_t res;

	if (!file)
		return false;

	/* Automatically calculate length */
	if (length < 0) {
		if (!data)
			return true;
		length = strlen (data);
	}

	while (written < length) {
		res = write (file->fd, buf + written, length - written);
		if (res <= 0) {
			if (errno == EAGAIN || errno == EINTR)
				continue;
			p11_message_err (errno, _("couldn't write to file: %s"), file->temp);
			return false;
		} else {
			written += res;
		}
	}

	return true;
}

static void
filo_free (p11_save_file *file)
{
	free (file->temp);
	free (file->bare);
	free (file->extension);
	free (file);
}

static void
dir_free (p11_save_dir *dir) {
	p11_dict_free (dir->cache);
	free (dir->path);
	free (dir);
}

#ifdef OS_UNIX

static int
on_unique_try_link (void *data,
                    char *path)
{
	p11_save_file *file = data;

	if (link (file->temp, path) < 0) {
		if (errno == EEXIST)
			return 0; /* Continue trying other names */
		p11_message_err (errno, _("couldn't complete writing of file: %s"), path);
		return -1;
	}

	return 1; /* All done */
}

#else /* OS_WIN32 */

static int
on_unique_try_rename (void *data,
                      char *path)
{
	p11_save_file *file = data;

	if (rename (file->temp, path) < 0) {
		if (errno == EEXIST)
			return 0; /* Continue trying other names */
		p11_message (_("couldn't complete writing of file: %s"), path);
		return -1;
	}

	free (file->temp);
	file->temp = strdup (path);
	return 1; /* All done */
}

#endif /* OS_WIN32 */

bool
p11_save_finish_file (p11_save_file *file,
                      char **path_out,
                      bool commit)
{
	bool ret = true;
	char *path;

	if (!file)
		return false;

	if (!commit) {
		close (file->fd);
		unlink (file->temp);
		filo_free (file);
		return true;
	}

	if (asprintf (&path, "%s%s", file->bare, file->extension) < 0)
		return_val_if_reached (false);

	if (close (file->fd) < 0) {
		p11_message_err (errno, _("couldn't write file: %s"), file->temp);
		ret = false;

#ifdef OS_UNIX
	/* Set the mode of the file, readable by everyone, but not writable */
	} else if (chmod (file->temp, S_IRUSR | S_IRGRP | S_IROTH) < 0) {
		p11_message_err (errno, _("couldn't set file permissions: %s"), file->temp);
		ret = false;

	/* Atomically rename the tempfile over the filename */
	} else if (file->flags & P11_SAVE_OVERWRITE) {
		if (rename (file->temp, path) < 0) {
			p11_message_err (errno, _("couldn't complete writing file: %s"), path);
			ret = false;
		} else {
			unlink (file->temp);
		}

	/* Create a unique name if requested unique file name */
	} else if (file->flags & P11_SAVE_UNIQUE) {
		free (path);
		path = make_unique_name (file->bare, file->extension,
		                         on_unique_try_link, file);
		if (!path)
			ret = false;
		unlink (file->temp);

	/* When not overwriting, link will fail if filename exists. */
	} else {
		if (link (file->temp, path) < 0) {
			p11_message_err (errno, _("couldn't complete writing of file: %s"), path);
			ret = false;
		}
		unlink (file->temp);

#else /* OS_WIN32 */

	/* Windows does not do atomic renames, so delete original file first */
	} else {
		/* Create a unique name if requested unique file name */
		if (file->flags & P11_SAVE_UNIQUE) {
			free (path);
			path = make_unique_name (file->bare, file->extension,
			                         on_unique_try_rename, file);
			if (!path)
				ret = false;

		} else if ((file->flags & P11_SAVE_OVERWRITE) &&
			    unlink (path) < 0 && errno != ENOENT) {
			p11_message_err (errno, _("couldn't remove original file: %s"), path);
			ret = false;
		}

		if (ret == true && strcmp (file->temp, path) != 0) {
			if (rename (file->temp, path) < 0) {
				p11_message_err (errno, _("couldn't complete writing file: %s"), path);
				ret = false;
			}

			unlink (file->temp);
		}

#endif /* OS_WIN32 */
	}

	if (ret && path_out) {
		*path_out = path;
		path = NULL;
	}

	free (path);
	filo_free (file);
	return ret;
}

p11_save_dir *
p11_save_open_directory (const char *path,
                         int flags)
{
#ifdef OS_UNIX
	struct stat sb;
	int fd;
#endif
	p11_save_dir *dir;

	return_val_if_fail (path != NULL, NULL);

#ifdef OS_UNIX
	/* We update the permissions when we finish writing */
	if (mkdir (path, S_IRWXU) < 0) {
#else /* OS_WIN32 */
	if (mkdir (path) < 0) {
#endif
		/* Some random error, report it */
		if (errno != EEXIST) {
			p11_message_err (errno, _("couldn't create directory: %s"), path);

		/* The directory exists and we're not overwriting */
		} else if (!(flags & P11_SAVE_OVERWRITE)) {
			p11_message (_("directory already exists: %s"), path);
			return NULL;
		}
#ifdef OS_UNIX
		/*
		 * If the directory exists on unix, we may have restricted
		 * the directory permissions to read-only. We have to change
		 * them back to writable in order for things to work.
		 */
		fd = open (path, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
		if (fd < 0) {
			p11_message_err (errno, _("couldn't open directory: %s"), path);
			return NULL;
		}
		if (fstat (fd, &sb) < 0) {
			p11_message_err (errno, _("couldn't check directory permissions: %s"), path);
			close (fd);
			return NULL;
		}
		if ((sb.st_mode & S_IRWXU) != S_IRWXU &&
		    fchmod (fd, S_IRWXU | sb.st_mode) < 0) {
			p11_message_err (errno, _("couldn't make directory writable: %s"), path);
			close (fd);
			return NULL;
		}
		close (fd);
#endif /* OS_UNIX */
	}

	dir = calloc (1, sizeof (p11_save_dir));
	return_val_if_fail (dir != NULL, NULL);

	dir->path = strdup (path);
	if (dir->path == NULL) {
		dir_free (dir);
		return_val_if_reached (NULL);
	}

	dir->cache = p11_dict_new (p11_dict_str_hash, p11_dict_str_equal, free, NULL);
	if (dir->cache == NULL) {
		dir_free (dir);
		return_val_if_reached (NULL);
	}

	dir->flags = flags;
	return dir;
}

static char *
make_unique_name (const char *bare,
                  const char *extension,
                  int (*check) (void *, char *),
                  void *data)
{
	char unique[16];
	p11_buffer buf;
	int ret;
	int i;
	int bare_len, ext_len, diff;

	assert (bare != NULL);
	assert (check != NULL);

	p11_buffer_init_null (&buf, 0);

	/*
	 * Make sure the name will not be longer then MAX_FILE_NAME
	 */
	bare_len = strlen (bare);
	ext_len = extension ? strlen (extension) : 0;
	diff = bare_len + ext_len + sizeof (unique) - MAX_FILE_NAME;
	if (diff > 0)
		bare_len -= diff;
	return_val_if_fail (bare_len > 0, NULL);

	for (i = 0; true; i++) {

		p11_buffer_reset (&buf, 64);

		switch (i) {

		/*
		 * For the first iteration, just build the filename as
		 * provided by the caller.
		 */
		case 0:
			p11_buffer_add (&buf, bare, bare_len);
			break;

		/*
		 * On later iterations we try to add a numeric .N suffix
		 * before the extension, so the resulting file might look
		 * like filename.1.ext.
		 *
		 * As a special case if the extension is already '.0' then
		 * just just keep incerementing that.
		 */
		case 1:
			if (extension && strcmp (extension, ".0") == 0)
				extension = NULL;
			/* fall through */

		default:
			p11_buffer_add (&buf, bare, bare_len);
			snprintf (unique, sizeof (unique), ".%d", i);
			p11_buffer_add (&buf, unique, -1);
			break;
		}

		if (extension)
			p11_buffer_add (&buf, extension, ext_len);

		return_val_if_fail (p11_buffer_ok (&buf), NULL);

		ret = check (data, buf.data);
		if (ret < 0)
			return NULL;
		else if (ret > 0)
			return p11_buffer_steal (&buf, NULL);
	}

	return_val_if_reached (NULL);
}

static int
on_unique_check_dir (void *data,
                     char *name)
{
	p11_save_dir *dir = data;

	if (!p11_dict_get (dir->cache, name))
		return 1;

	return 0; /* Keep looking */
}

p11_save_file *
p11_save_open_file_in (p11_save_dir *dir,
                       const char *basename,
                       const char *extension)
{
	p11_save_file *file = NULL;
	char *name;
	char *path;

	return_val_if_fail (dir != NULL, NULL);
	return_val_if_fail (basename != NULL, NULL);

	name = make_unique_name (basename, extension, on_unique_check_dir, dir);
	return_val_if_fail (name != NULL, NULL);

	if (asprintf (&path, "%s/%s", dir->path, name) < 0)
		return_val_if_reached (NULL);

	file = p11_save_open_file (path, NULL, dir->flags);

	if (file) {
		if (!p11_dict_set (dir->cache, name, name))
			return_val_if_reached (NULL);
		name = NULL;
	}

	free (name);
	free (path);

	return file;
}

#ifdef OS_UNIX

bool
p11_save_symlink_in (p11_save_dir *dir,
                     const char *linkname,
                     const char *extension,
                     const char *destination)
{
	char *name;
	char *path;
	bool ret;

	return_val_if_fail (dir != NULL, false);
	return_val_if_fail (linkname != NULL, false);
	return_val_if_fail (destination != NULL, false);

	name = make_unique_name (linkname, extension, on_unique_check_dir, dir);
	return_val_if_fail (name != NULL, false);

	if (asprintf (&path, "%s/%s", dir->path, name) < 0)
		return_val_if_reached (false);

	unlink (path);

	if (symlink (destination, path) < 0) {
		p11_message_err (errno, _("couldn't create symlink: %s"), path);
		ret = false;
	} else {
		if (!p11_dict_set (dir->cache, name, name))
			return_val_if_reached (false);
		name = NULL;
		ret = true;
	}

	free (path);
	free (name);

	return ret;
}

#endif /* OS_UNIX */

static bool
cleanup_directory (const char *directory,
                   p11_dict *cache)
{
	struct dirent *dp;
	struct stat st;
	p11_dict *remove;
	p11_dictiter iter;
	char *path;
	DIR *dir;
	bool ret;

	/* First we load all the modules */
	dir = opendir (directory);
	if (!dir) {
		p11_message_err (errno, _("couldn't list directory: %s"), directory);
		return false;
	}

	remove = p11_dict_new (p11_dict_str_hash, p11_dict_str_equal, free, NULL);

	while ((dp = readdir (dir)) != NULL) {
		if (p11_dict_get (cache, dp->d_name))
			continue;

		if (asprintf (&path, "%s/%s", directory, dp->d_name) < 0)
			return_val_if_reached (false);


		if (stat (path, &st) >= 0 && !S_ISDIR (st.st_mode)) {
			if (!p11_dict_set (remove, path, path))
				return_val_if_reached (false);
		} else {
			free (path);
		}
	}

	closedir (dir);

	ret = true;

	/* Remove all the files still in the cache */
	p11_dict_iterate (remove, &iter);
	while (p11_dict_next (&iter, (void **)&path, NULL)) {
		if (unlink (path) < 0 && errno != ENOENT) {
			p11_message_err (errno, _("couldn't remove file: %s"), path);
			ret = false;
			break;
		}
	}

	p11_dict_free (remove);

	return ret;
}

bool
p11_save_finish_directory (p11_save_dir *dir,
                           bool commit)
{
	bool ret = true;

	if (!dir)
		return false;

	if (commit) {
		if (dir->flags & P11_SAVE_OVERWRITE)
			ret = cleanup_directory (dir->path, dir->cache);

#ifdef OS_UNIX
		/* Try to set the mode of the directory to readable */
		if (ret && chmod (dir->path, S_IRUSR | S_IXUSR | S_IRGRP |
		                             S_IXGRP | S_IROTH | S_IXOTH) < 0) {
			p11_message_err (errno, _("couldn't set directory permissions: %s"), dir->path);
			ret = false;
		}
#endif /* OS_UNIX */
	}

	p11_dict_free (dir->cache);
	free (dir->path);
	free (dir);

	return ret;
}
