/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "nfs-workarounds.h"
#include "read-full.h"
#include "write-full.h"
#include "ostream.h"
#include "mail-index-private.h"
#include "mail-transaction-log-private.h"

#include <stdio.h>

static int mail_index_create_backup(struct mail_index *index)
{
	const char *backup_path, *tmp_backup_path;
	int ret;

	if (index->fd != -1) {
		/* we very much want to avoid creating a backup file that
		   hasn't been written to disk yet */
		if (fdatasync(index->fd) < 0) {
			mail_index_set_error(index, "fdatasync(%s) failed: %m",
					     index->filepath);
			return -1;
		}
	}

	backup_path = t_strconcat(index->filepath, ".backup", NULL);
	tmp_backup_path = t_strconcat(backup_path, ".tmp", NULL);
	ret = link(index->filepath, tmp_backup_path);
	if (ret < 0 && errno == EEXIST) {
		if (unlink(tmp_backup_path) < 0 && errno != ENOENT) {
			mail_index_set_error(index, "unlink(%s) failed: %m",
					     tmp_backup_path);
			return -1;
		}
		ret = link(index->filepath, tmp_backup_path);
	}
	if (ret < 0) {
		if (errno == ENOENT) {
			/* no dovecot.index file, ignore */
			return 0;
		}
		mail_index_set_error(index, "link(%s, %s) failed: %m",
				     index->filepath, tmp_backup_path);
		return -1;
	}

	if (rename(tmp_backup_path, backup_path) < 0) {
		mail_index_set_error(index, "rename(%s, %s) failed: %m",
				     tmp_backup_path, backup_path);
		return -1;
	}
	return 0;
}

static int mail_index_recreate(struct mail_index *index)
{
	struct mail_index_map *map = index->map;
	struct ostream *output;
	unsigned int base_size;
	const char *path;
	int ret = 0, fd;

	i_assert(!MAIL_INDEX_IS_IN_MEMORY(index));
	i_assert(map->hdr.indexid == index->indexid);
	i_assert((map->hdr.flags & MAIL_INDEX_HDR_FLAG_CORRUPTED) == 0);
	i_assert(index->indexid != 0);

	fd = mail_index_create_tmp_file(index, index->filepath, &path);
	if (fd == -1)
		return -1;

	output = o_stream_create_fd_file(fd, 0, FALSE);
	o_stream_cork(output);

	struct mail_index_header hdr = map->hdr;
	/* Write tail_offset the same as head_offset. This function must not
	   be called unless it's safe to do this. See the explanations in
	   mail_index_sync_commit(). */
	hdr.log_file_tail_offset = hdr.log_file_head_offset;

	base_size = I_MIN(hdr.base_header_size, sizeof(hdr));
	o_stream_nsend(output, &hdr, base_size);
	o_stream_nsend(output, MAIL_INDEX_MAP_HDR_OFFSET(map, base_size),
		       hdr.header_size - base_size);
	o_stream_nsend(output, map->rec_map->records,
		       map->rec_map->records_count * hdr.record_size);
	if (o_stream_finish(output) < 0) {
		mail_index_file_set_syscall_error(index, path, "write()");
		ret = -1;
	}
	o_stream_destroy(&output);

	if (ret == 0 && index->set.fsync_mode != FSYNC_MODE_NEVER) {
		if (fdatasync(fd) < 0) {
			mail_index_file_set_syscall_error(index, path,
							  "fdatasync()");
			ret = -1;
		}
	}

	if (close(fd) < 0) {
		mail_index_file_set_syscall_error(index, path, "close()");
		ret = -1;
	}

	if ((index->flags & MAIL_INDEX_OPEN_FLAG_KEEP_BACKUPS) != 0)
		(void)mail_index_create_backup(index);

	if (ret == 0 && rename(path, index->filepath) < 0) {
		mail_index_set_error(index, "rename(%s, %s) failed: %m",
				     path, index->filepath);
		ret = -1;
	}

	if (ret < 0)
		i_unlink(path);
	return ret;
}

static bool mail_index_should_recreate(struct mail_index *index)
{
	struct stat st1, st2;

	if (nfs_safe_stat(index->filepath, &st1) < 0) {
		if (errno != ENOENT) {
			mail_index_set_syscall_error(index, "stat()");
			return FALSE;
		} else if (index->fd == -1) {
			/* main index hasn't been created yet */
			return TRUE;
		} else {
			/* mailbox was just deleted? don't log an error */
			return FALSE;
		}
	}
	if (index->fd == -1) {
		/* main index was just created by another process */
		return FALSE;
	}
	if (fstat(index->fd, &st2) < 0) {
		if (!ESTALE_FSTAT(errno))
			mail_index_set_syscall_error(index, "fstat()");
		return FALSE;
	}
	if (st1.st_ino != st2.st_ino ||
	    !CMP_DEV_T(st1.st_dev, st2.st_dev)) {
		/* Index has already been recreated since we last read it.
		   We can't trust our decisions about whether to recreate it. */
		return FALSE;
	}
	return TRUE;
}

void mail_index_write(struct mail_index *index, bool want_rotate,
		      const char *reason)
{
	struct mail_index_header *hdr = &index->map->hdr;
	bool rotated = FALSE;

	i_assert(index->log_sync_locked);

	if (index->readonly)
		return;

	/* rotate the .log before writing index, so the index will point to
	   the latest log. Note that it's the caller's responsibility to make
	   sure that the .log can be safely rotated (i.e. everything has been
	   synced). */
	if (want_rotate) {
		if (mail_transaction_log_rotate(index->log, FALSE) == 0) {
			struct mail_transaction_log_file *file =
				index->log->head;
			/* Log rotation refreshes the index, which may cause the
			   map to change. Because we're locked, it's not
			   supposed to happen and will likely lead to an
			   assert-crash below, but we still need to make sure
			   we're using the latest map to do the checks. */
			hdr = &index->map->hdr;
			i_assert(file->hdr.prev_file_seq == hdr->log_file_seq);
			i_assert(file->hdr.prev_file_offset == hdr->log_file_head_offset);
			hdr->log_file_seq = file->hdr.file_seq;
			hdr->log_file_head_offset =
				hdr->log_file_tail_offset = file->hdr.hdr_size;
			/* Assume .log.2 was created successfully. If it
			   wasn't, it just causes an extra stat() and gets
			   fixed later on. */
			hdr->log2_rotate_time = ioloop_time32;
			rotated = TRUE;
		}
	}

	if (MAIL_INDEX_IS_IN_MEMORY(index))
		;
	else if (!rotated && !mail_index_should_recreate(index)) {
		/* make sure we don't keep getting back in here */
		index->reopen_main_index = TRUE;
	} else {
		if (mail_index_recreate(index) < 0) {
			(void)mail_index_move_to_memory(index);
			return;
		}
		event_set_name(index->event, "mail_index_recreated");
		e_debug(index->event, "Recreated %s (file_seq=%u) because: %s",
			index->filepath, hdr->log_file_seq, reason);
	}

	index->main_index_hdr_log_file_seq = hdr->log_file_seq;
	index->main_index_hdr_log_file_tail_offset = hdr->log_file_tail_offset;
}
