///////////////////////////////////////////////////////////////////////////////
//
/// \file       io.c
/// \brief      File opening, unlinking, and closing
//
//  Copyright (C) 2007 Lasse Collin
//
//  This program is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 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
//  Lesser General Public License for more details.
//
///////////////////////////////////////////////////////////////////////////////

#include "private.h"

#if defined(HAVE_FUTIMES) || defined(HAVE_FUTIMESAT)
#	include <sys/time.h>
#endif

#ifndef O_SEARCH
#	define O_SEARCH O_RDONLY
#endif


/// \brief      Number of open file_pairs
///
/// Once the main() function has requested processing of all files,
/// we wait that open_pairs drops back to zero. Then it is safe to
/// exit from the program.
static size_t open_pairs = 0;


/// \brief      mutex for file system operations
///
/// All file system operations are done via the functions in this file.
/// They use fchdir() to avoid some race conditions (more portable than
/// openat() & co.).
///
/// Synchronizing all file system operations shouldn't affect speed notably,
/// since the actual reading from and writing to files is done in parallel.
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;


/// This condition is invoked when a file is closed and the value of
/// the open_files variable has dropped to zero. The only listener for
/// this condition is io_finish() which is called from main().
static pthread_cond_t io_cond = PTHREAD_COND_INITIALIZER;


/// True when stdout is being used by some thread
static bool stdout_in_use = false;


/// This condition is signalled when a thread releases stdout (no longer
/// writes data to it).
static pthread_cond_t stdout_cond = PTHREAD_COND_INITIALIZER;


/// \brief      Directory where we were started
///
/// This is needed when a new file, whose name was given on command line,
/// is opened.
static int start_dir;


static uid_t uid;
static gid_t gid;


extern void
io_init(void)
{
	start_dir = open(".", O_SEARCH | O_NOCTTY);
	if (start_dir == -1) {
		errmsg(V_ERROR, _("Cannot get file descriptor of the current "
				"directory: %s"), strerror(errno));
		my_exit(ERROR);
	}

	uid = getuid();
	gid = getgid();

	return;
}


/// Waits until the number of open file_pairs has dropped to zero.
extern void
io_finish(void)
{
	pthread_mutex_lock(&mutex);

	while (open_pairs != 0)
		pthread_cond_wait(&io_cond, &mutex);

	(void)close(start_dir);

	pthread_mutex_unlock(&mutex);

	return;
}


/// \brief      Unlinks a file
///
/// \param      dir_fd  File descriptor of the directory containing the file
/// \param      name    Name of the file with or without path
///
/// \return     Zero on success. On error, -1 is returned and errno set.
///
static void
io_unlink(int dir_fd, const char *name, ino_t ino)
{
	const char *base = str_filename(name);
	if (base == NULL) {
		// This shouldn't happen.
		errmsg(V_ERROR, _("%s: Invalid filename"), name);
		return;
	}

	pthread_mutex_lock(&mutex);

	if (fchdir(dir_fd)) {
		errmsg(V_ERROR, _("Cannot change directory: %s"),
				strerror(errno));
	} else {
		struct stat st;
		if (lstat(base, &st) || st.st_ino != ino)
			errmsg(V_ERROR, _("%s: File seems to be moved, "
					"not removing"), name);

		// There's a race condition between lstat() and unlink()
		// but at least we have tried to avoid removing wrong file.
		else if (unlink(base))
			errmsg(V_ERROR, _("%s: Cannot remove: %s"),
					name, strerror(errno));
	}

	pthread_mutex_unlock(&mutex);

	return;
}


/// \brief      Copies owner/group and permissions
///
/// \todo       ACL and EA support
///
static void
io_copy_attrs(const file_pair *pair)
{
	// This function is more tricky than you may think at first.
	// Blindly copying permissions may permit users to access the
	// destination file who didn't have permission to access the
	// source file.

	if (uid == 0 && fchown(pair->dest_fd, pair->src_st.st_uid, -1))
		errmsg(V_WARNING, _("%s: Cannot set the file owner: %s"),
				pair->dest_name, strerror(errno));

	mode_t mode;

	if (fchown(pair->dest_fd, -1, pair->src_st.st_gid)) {
		errmsg(V_WARNING, _("%s: Cannot set the file group: %s"),
				pair->dest_name, strerror(errno));
		// We can still safely copy some additional permissions:
		// `group' must be at least as strict as `other' and
		// also vice versa.
		//
		// NOTE: After this, the owner of the source file may
		// get additional permissions. This shouldn't be too bad,
		// because the owner would have had permission to chmod
		// the original file anyway.
		mode = ((pair->src_st.st_mode & 0070) >> 3)
				& (pair->src_st.st_mode & 0007);
		mode = (pair->src_st.st_mode & 0700) | (mode << 3) | mode;
	} else {
		// Drop the setuid, setgid, and sticky bits.
		mode = pair->src_st.st_mode & 0777;
	}

	if (fchmod(pair->dest_fd, mode))
		errmsg(V_WARNING, _("%s: Cannot set the file permissions: %s"),
				pair->dest_name, strerror(errno));

	// Copy the timestamps only if we have a secure function to do it.
#if defined(HAVE_FUTIMES) || defined(HAVE_FUTIMESAT)
	struct timeval tv[2];
	tv[0].tv_sec = pair->src_st.st_atime;
	tv[1].tv_sec = pair->src_st.st_mtime;

#	if defined(HAVE_STRUCT_STAT_ST_ATIM_TV_NSEC)
	tv[0].tv_usec = pair->src_st.st_atim.tv_nsec / 1000;
#	elif defined(HAVE_STRUCT_STAT_ST_ATIMESPEC_TV_NSEC)
	tv[0].tv_usec = pair->src_st.st_atimespec.tv_nsec / 1000;
#	else
	tv[0].tv_usec = 0;
#	endif

#	if defined(HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC)
	tv[1].tv_usec = pair->src_st.st_mtim.tv_nsec / 1000;
#	elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC)
	tv[1].tv_usec = pair->src_st.st_mtimespec.tv_nsec / 1000;
#	else
	tv[1].tv_usec = 0;
#	endif

#	ifdef HAVE_FUTIMES
	(void)futimes(pair->dest_fd, tv);
#	else
	(void)futimesat(pair->dest_fd, NULL, tv);
#	endif
#endif

	return;
}


/// Opens and changes into the directory containing the source file.
static int
io_open_dir(file_pair *pair)
{
	if (pair->src_name == stdin_filename)
		return 0;

	if (fchdir(start_dir)) {
		errmsg(V_ERROR, _("Cannot change directory: %s"),
				strerror(errno));
		return -1;
	}

	const char *split = strrchr(pair->src_name, '/');
	if (split == NULL) {
		pair->dir_fd = start_dir;
	} else {
		// Copy also the slash. It's needed to support filenames
		// like "/foo" (dirname being "/"), and it never hurts anyway.
		const size_t dirname_len = split - pair->src_name + 1;
		char dirname[dirname_len + 1];
		memcpy(dirname, pair->src_name, dirname_len);
		dirname[dirname_len] = '\0';

		// Open the directory and change into it.
		pair->dir_fd = open(dirname, O_SEARCH | O_NOCTTY);
		if (pair->dir_fd == -1 || fchdir(pair->dir_fd)) {
			errmsg(V_ERROR, _("%s: Cannot open the directory "
					"containing the file: %s"),
					pair->src_name, strerror(errno));
			(void)close(pair->dir_fd);
			return -1;
		}
	}

	return 0;
}


static void
io_close_dir(file_pair *pair)
{
	if (pair->dir_fd != start_dir)
		(void)close(pair->dir_fd);

	return;
}


/// Opens the source file. The file is opened using the plain filename without
/// path, thus the file must be in the current working directory. This is
/// ensured because io_open_dir() is always called before this function.
static int
io_open_src(file_pair *pair)
{
	if (pair->src_name == stdin_filename) {
		pair->src_fd = STDIN_FILENO;
	} else {
		// Strip the pathname. Thanks to io_open_dir(), the file
		// is now in the current working directory.
		const char *filename = str_filename(pair->src_name);
		if (filename == NULL)
			return -1;

		// Symlinks are followed if --stdout or --force has been
		// specified.
		const bool follow_symlinks = opt_stdout || opt_force;
		pair->src_fd = open(filename, O_RDONLY | O_NOCTTY
				| (follow_symlinks ? 0 : O_NOFOLLOW));
		if (pair->src_fd == -1) {
			// Give an understandable error message in if reason
			// for failing was that the file was a symbolic link.
			//  - Linux, OpenBSD, Solaris: ELOOP
			//  - FreeBSD: EMLINK
			//  - Tru64: ENOTSUP
			// It seems to be safe to check for all these, since
			// those errno values aren't used for other purporses
			// on any of the listed operating system *when* the
			// above flags are used with open().
			if (!follow_symlinks
					&& (errno == ELOOP
#ifdef EMLINK
					|| errno == EMLINK
#endif
#ifdef ENOTSUP
					|| errno == ENOTSUP
#endif
					)) {
				errmsg(V_WARNING, _("%s: Is a symbolic link, "
						"skipping"), pair->src_name);
			} else {
				errmsg(V_ERROR, "%s: %s", pair->src_name,
						strerror(errno));
			}

			return -1;
		}

		if (fstat(pair->src_fd, &pair->src_st)) {
			errmsg(V_ERROR, "%s: %s", pair->src_name,
					strerror(errno));
			goto error;
		}

		if (S_ISDIR(pair->src_st.st_mode)) {
			errmsg(V_WARNING, _("%s: Is a directory, skipping"),
					pair->src_name);
			goto error;
		}

		if (!opt_stdout) {
			if (!opt_force && !S_ISREG(pair->src_st.st_mode)) {
				errmsg(V_WARNING, _("%s: Not a regular file, "
						"skipping"), pair->src_name);
				goto error;
			}

			if (pair->src_st.st_mode & (S_ISUID | S_ISGID)) {
				// Setuid and setgid files are rejected even
				// with --force. This is good for security
				// (hopefully) but it's a bit weird to reject
				// file when --force was given. At least this
				// matches gzip's behavior.
				errmsg(V_WARNING, _("%s: File has setuid or "
						"setgid bit set, skipping"),
						pair->src_name);
				goto error;
			}

			if (!opt_force && (pair->src_st.st_mode & S_ISVTX)) {
				errmsg(V_WARNING, _("%s: File has sticky bit "
						"set, skipping"),
						pair->src_name);
				goto error;
			}

			if (pair->src_st.st_nlink > 1) {
				errmsg(V_WARNING, _("%s: Input file has more "
						"than one hard link, "
						"skipping"), pair->src_name);
				goto error;
			}
		}
	}

	return 0;

error:
	(void)close(pair->src_fd);
	return -1;
}


/// \brief      Closes source file of the file_pair structure
///
/// \param      pair    File whose src_fd should be closed
/// \param      success If true, the file will be removed from the disk if
///                     closing succeeds and --keep hasn't been used.
static void
io_close_src(file_pair *pair, bool success)
{
	if (pair->src_fd == STDIN_FILENO || pair->src_fd == -1)
		return;

	if (close(pair->src_fd)) {
		errmsg(V_ERROR, _("%s: Closing the file failed: %s"),
				pair->src_name, strerror(errno));
	} else if (success && !opt_keep_original) {
		io_unlink(pair->dir_fd, pair->src_name, pair->src_st.st_ino);
	}

	return;
}


static int
io_open_dest(file_pair *pair)
{
	if (opt_stdout || pair->src_fd == STDIN_FILENO) {
		// We don't modify or free() this.
		pair->dest_name = (char *)"(stdout)";
		pair->dest_fd = STDOUT_FILENO;

		// Synchronize the order in which files get written to stdout.
		// Unlocking the mutex is safe, because opening the file_pair
		// can no longer fail.
		while (stdout_in_use)
			pthread_cond_wait(&stdout_cond, &mutex);

		stdout_in_use = true;

	} else {
		pair->dest_name = get_dest_name(pair->src_name);
		if (pair->dest_name == NULL)
			return -1;

		// This cannot fail, because get_dest_name() doesn't return
		// invalid names.
		const char *filename = str_filename(pair->dest_name);
		assert(filename != NULL);

		pair->dest_fd = open(filename, O_WRONLY | O_NOCTTY | O_CREAT
				| (opt_force ? O_TRUNC : O_EXCL),
				S_IRUSR | S_IWUSR);
		if (pair->dest_fd == -1) {
			errmsg(V_ERROR, "%s: %s", pair->dest_name,
					strerror(errno));
			free(pair->dest_name);
			return -1;
		}

		// If this really fails... well, we have a safe fallback.
		struct stat st;
		if (fstat(pair->dest_fd, &st))
			pair->dest_ino = 0;
		else
			pair->dest_ino = st.st_ino;
	}

	return 0;
}


/// \brief      Closes destination file of the file_pair structure
///
/// \param      pair    File whose dest_fd should be closed
/// \param      success If false, the file will be removed from the disk.
///
/// \return     Zero if closing succeeds. On error, -1 is returned and
///             error message printed.
static int
io_close_dest(file_pair *pair, bool success)
{
	if (pair->dest_fd == -1)
		return 0;

	if (pair->dest_fd == STDOUT_FILENO) {
		stdout_in_use = false;
		pthread_cond_signal(&stdout_cond);
		return 0;
	}

	if (close(pair->dest_fd)) {
		errmsg(V_ERROR, _("%s: Closing the file failed: %s"),
				pair->dest_name, strerror(errno));

		// Closing destination file failed, so we cannot trust its
		// contents. Get rid of junk:
		io_unlink(pair->dir_fd, pair->dest_name, pair->dest_ino);
		free(pair->dest_name);
		return -1;
	}

	// If the operation using this file wasn't successful, we git rid
	// of the junk file.
	if (!success)
		io_unlink(pair->dir_fd, pair->dest_name, pair->dest_ino);

	free(pair->dest_name);

	return 0;
}


extern file_pair *
io_open(const char *src_name)
{
	if (is_empty_filename(src_name))
		return NULL;

	file_pair *pair = malloc(sizeof(file_pair));
	if (pair == NULL) {
		out_of_memory();
		return NULL;
	}

	*pair = (file_pair){
		.src_name = src_name,
		.dest_name = NULL,
		.dir_fd = -1,
		.src_fd = -1,
		.dest_fd = -1,
		.src_eof = false,
	};

	pthread_mutex_lock(&mutex);

	++open_pairs;

	if (io_open_dir(pair))
		goto error_dir;

	if (io_open_src(pair))
		goto error_src;

	if (user_abort || io_open_dest(pair))
		goto error_dest;

	pthread_mutex_unlock(&mutex);

	return pair;

error_dest:
	io_close_src(pair, false);
error_src:
	io_close_dir(pair);
error_dir:
	--open_pairs;
	pthread_mutex_unlock(&mutex);
	free(pair);
	return NULL;
}


/// \brief      Closes the file descriptors and frees the structure
extern void
io_close(file_pair *pair, bool success)
{
	if (success && pair->dest_fd != STDOUT_FILENO)
		io_copy_attrs(pair);

	// Close the destination first. If it fails, we must not remove
	// the source file!
	if (!io_close_dest(pair, success)) {
		// Closing destination file succeeded. Remove the source file
		// if the operation using this file pair was successful
		// and we haven't been requested to keep the source file.
		io_close_src(pair, success);
	} else {
		// We don't care if operation using this file pair was
		// successful or not, since closing the destination file
		// failed. Don't remove the original file.
		io_close_src(pair, false);
	}

	io_close_dir(pair);

	free(pair);

	pthread_mutex_lock(&mutex);

	if (--open_pairs == 0)
		pthread_cond_signal(&io_cond);

	pthread_mutex_unlock(&mutex);

	return;
}


/// \brief      Reads from a file to a buffer
///
/// \param      pair    File pair having the sourcefile open for reading
/// \param      buf     Destination buffer to hold the read data
/// \param      size    Size of the buffer; assumed be smaller than SSIZE_MAX
///
/// \return     On success, number of bytes read is returned. On end of
///             file zero is returned and pair->src_eof set to true.
///             On error, SIZE_MAX is returned and error message printed.
///
/// \note       This does no locking, thus two threads must not read from
///             the same file. This no problem in this program.
extern size_t
io_read(file_pair *pair, uint8_t *buf, size_t size)
{
	// We use small buffers here.
	assert(size < SSIZE_MAX);

	size_t left = size;

	while (left > 0) {
		const ssize_t amount = read(pair->src_fd, buf, left);

		if (amount == 0) {
			pair->src_eof = true;
			break;
		}

		if (amount == -1) {
			if (errno == EINTR) {
				if (user_abort)
					return SIZE_MAX;

				continue;
			}

			errmsg(V_ERROR, _("%s: Read error: %s"),
					pair->src_name, strerror(errno));

			// FIXME Is this needed?
			pair->src_eof = true;

			return SIZE_MAX;
		}

		buf += (size_t)(amount);
		left -= (size_t)(amount);
	}

	return size - left;
}


/// \brief      Writes a buffer to a file
///
/// \param      pair    File pair having the destination file open for writing
/// \param      buf     Buffer containing the data to be written
/// \param      size    Size of the buffer; assumed be smaller than SSIZE_MAX
///
/// \return     On success, zero is returned. On error, -1 is returned
///             and error message printed.
///
/// \note       This does no locking, thus two threads must not write to
///             the same file. This no problem in this program.
extern int
io_write(const file_pair *pair, const uint8_t *buf, size_t size)
{
	assert(size < SSIZE_MAX);

	while (size > 0) {
		const ssize_t amount = write(pair->dest_fd, buf, size);
		if (amount == -1) {
			if (errno == EINTR) {
				if (user_abort)
					return -1;

				continue;
			}

			errmsg(V_ERROR, _("%s: Write error: %s"),
					pair->dest_name, strerror(errno));
			return -1;
		}

		buf += (size_t)(amount);
		size -= (size_t)(amount);
	}

	return 0;
}