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

#include "lib.h"
#include "array.h"
#include "mail-transaction-log-private.h"
#include "mail-index-private.h"

int mail_index_map_parse_extensions(struct mail_index_map *map)
{
	struct mail_index *index = map->index;
	const struct mail_index_ext_header *ext_hdr;
	unsigned int i, old_count, offset;
	const char *name, *error;
	uint32_t ext_id, ext_map_idx, ext_offset;

	/* extension headers always start from 64bit offsets, so if base header
	   doesn't happen to be 64bit aligned we'll skip some bytes */
	offset = MAIL_INDEX_HEADER_SIZE_ALIGN(map->hdr.base_header_size);
	if (offset >= map->hdr.header_size && map->extension_pool == NULL) {
		/* nothing to do, skip allocations and all */
		return 0;
	}

	old_count = array_count(&index->extensions);
	mail_index_map_init_extbufs(map, old_count + 5);

	ext_id = (uint32_t)-1;
	for (i = 0; i < old_count; i++)
		array_push_back(&map->ext_id_map, &ext_id);

	for (i = 0; offset < map->hdr.header_size; i++) {
		ext_offset = offset;

		if (mail_index_map_ext_get_next(map, &offset,
						&ext_hdr, &name) < 0) {
			mail_index_set_error(index, "Corrupted index file %s: "
				"Header extension #%d (%s) goes outside header",
				index->filepath, i, name);
			return -1;
		}

		if (mail_index_map_ext_hdr_check(&map->hdr, ext_hdr,
						 name, &error) < 0) {
			mail_index_set_error(index, "Corrupted index file %s: "
					     "Broken extension #%d (%s): %s",
					     index->filepath, i, name, error);
			return -1;
		}
		if (mail_index_map_lookup_ext(map, name, &ext_map_idx)) {
			mail_index_set_error(index, "Corrupted index file %s: "
				"Duplicate header extension %s",
				index->filepath, name);
			return -1;
		}

		(void)mail_index_map_register_ext(map, name, ext_offset, ext_hdr);
	}
	return 0;
}

int mail_index_map_parse_keywords(struct mail_index_map *map)
{
	struct mail_index *index = map->index;
	const struct mail_index_ext *ext;
	const struct mail_index_keyword_header *kw_hdr;
	const struct mail_index_keyword_header_rec *kw_rec;
	const char *name;
	unsigned int i, name_area_end_offset, old_count;
	uint32_t idx;

	if (!mail_index_map_lookup_ext(map, MAIL_INDEX_EXT_KEYWORDS, &idx)) {
		if (array_is_created(&map->keyword_idx_map))
			array_clear(&map->keyword_idx_map);
		return 0;
	}
	ext = array_idx(&map->extensions, idx);

	/* Extension header contains:
	   - struct mail_index_keyword_header
	   - struct mail_index_keyword_header_rec * keywords_count
	   - const char names[] * keywords_count

	   The mail_index_keyword_header_rec are rather unnecessary nowadays.
	   They were originally an optimization when dovecot.index header kept
	   changing constantly, but nowadays the changes are usually read from
	   the .log changes, so re-reading dovecot.index header isn't common.
	   In a later version we could even remove it.
	*/
	i_assert(ext->hdr_offset < map->hdr.header_size);
	kw_hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, ext->hdr_offset);
	kw_rec = (const void *)(kw_hdr + 1);
	name = (const char *)(kw_rec + kw_hdr->keywords_count);

	old_count = !array_is_created(&map->keyword_idx_map) ? 0 :
		array_count(&map->keyword_idx_map);

	/* make sure the header is valid */
	if (kw_hdr->keywords_count < old_count) {
		mail_index_set_error(index, "Corrupted index file %s: "
				     "Keywords removed unexpectedly",
				     index->filepath);
		return -1;
	}

	if ((size_t)(name - (const char *)kw_hdr) > ext->hdr_size) {
		mail_index_set_error(index, "Corrupted index file %s: "
				     "keywords_count larger than header size",
				     index->filepath);
		return -1;
	}

	name_area_end_offset = (const char *)kw_hdr + ext->hdr_size - name;
	for (i = 0; i < kw_hdr->keywords_count; i++) {
		if (kw_rec[i].name_offset > name_area_end_offset) {
			mail_index_set_error(index, "Corrupted index file %s: "
				"name_offset points outside allocated header",
				index->filepath);
			return -1;
		}
	}
	if (name[name_area_end_offset-1] != '\0') {
		mail_index_set_error(index, "Corrupted index file %s: "
				     "Keyword header doesn't end with NUL",
				     index->filepath);
		return -1;
	}

	/* create file -> index mapping */
	if (!array_is_created(&map->keyword_idx_map))
		i_array_init(&map->keyword_idx_map, kw_hdr->keywords_count);

	size_t name_offset = 0;
	/* Check that existing headers are still the same. */
	for (i = 0; i < array_count(&map->keyword_idx_map); i++) {
		const char *keyword = name + kw_rec[i].name_offset;
		const unsigned int *old_idx;
		unsigned int kw_idx;

		if (kw_rec[i].name_offset != name_offset) {
			/* this shouldn't happen, but the old code didn't check
			   for this so for safety keep this as a warning. */
			e_warning(index->event,
				  "Corrupted index file %s: "
				  "Mismatching keyword name_offset",
				  index->filepath);
		}
		name_offset += strlen(keyword) + 1;

		old_idx = array_idx(&map->keyword_idx_map, i);
		if (!mail_index_keyword_lookup(index, keyword, &kw_idx) ||
		    kw_idx != *old_idx) {
			mail_index_set_error(index, "Corrupted index file %s: "
					     "Keywords changed unexpectedly",
					     index->filepath);
			return -1;
		}
	}

	/* Register the newly seen keywords */
	i = array_count(&map->keyword_idx_map);
	for (; i < kw_hdr->keywords_count; i++) {
		const char *keyword = name + kw_rec[i].name_offset;
		unsigned int kw_idx;

		if (kw_rec[i].name_offset != name_offset) {
			/* this shouldn't happen, but the old code didn't check
			   for this so for safety keep this as a warning. */
			e_warning(index->event,
				  "Corrupted index file %s: "
				  "Mismatching keyword name_offset",
				  index->filepath);
		}
		name_offset += strlen(keyword) + 1;

		if (*keyword == '\0') {
			mail_index_set_error(index, "Corrupted index file %s: "
				"Empty keyword name in header",
				index->filepath);
			return -1;
		}
		mail_index_keyword_lookup_or_create(index, keyword, &kw_idx);
		array_push_back(&map->keyword_idx_map, &kw_idx);
	}
	return 0;
}

bool mail_index_hdr_check_indexid(struct mail_index *index,
				  const struct mail_index_header *hdr)
{
	const char *reason;

	if (index->indexid == 0) {
		/* this happens only when .log file is lost/corrupted */
		i_assert(index->log->head == NULL);
		index->indexid = hdr->indexid;
	}
	if (hdr->indexid == index->indexid)
		return TRUE;

	if (mail_transaction_log_has_changed(index->log, TRUE,
					     &reason) != 0) {
		/* Transaction log has either changed, or there was some IO
		   error when doing the check. Either way, we can't continue. */
		return FALSE;
	}

	/* indexid has a permanent mismatch - delete the index. */
	mail_index_set_error(index, "Index file %s: "
			     "indexid changed: %u -> %u - deleting",
			     index->filepath, index->indexid, hdr->indexid);
	if (!index->readonly)
		i_unlink_if_exists(index->filepath);
	return FALSE;
}

bool mail_index_check_header_compat(const struct mail_index_header *hdr,
				    uoff_t file_size, const char **error_r)
{
        enum mail_index_header_compat_flags compat_flags = 0;

#ifndef WORDS_BIGENDIAN
	compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
#endif

	if (hdr->major_version != MAIL_INDEX_MAJOR_VERSION) {
		/* major version change */
		*error_r = t_strdup_printf("Major version changed (%u != %u)",
			hdr->major_version, MAIL_INDEX_MAJOR_VERSION);
		return FALSE;
	}
	if ((hdr->flags & MAIL_INDEX_HDR_FLAG_CORRUPTED) != 0) {
		/* we've already complained about it */
		*error_r = "Header's corrupted flag is set";
		return FALSE;
	}

	if (hdr->compat_flags != compat_flags) {
		/* architecture change */
		*error_r = "CPU architecture changed";
		return FALSE;
	}

	if (hdr->base_header_size < MAIL_INDEX_HEADER_MIN_SIZE ||
	    hdr->header_size < hdr->base_header_size) {
		*error_r = t_strdup_printf(
			"Corrupted header sizes (base %u, full %u)",
			hdr->base_header_size, hdr->header_size);
		return FALSE;
	}
	if (hdr->header_size > file_size) {
		*error_r = t_strdup_printf(
			"Header size is larger than file (%u > %"PRIuUOFF_T")",
			hdr->header_size, file_size);
		return FALSE;
	}
	return TRUE;
}

static void mail_index_map_clear_recent_flags(struct mail_index_map *map)
{
	struct mail_index_record *rec;
	uint32_t seq;

	for (seq = 1; seq <= map->hdr.messages_count; seq++) {
		rec = MAIL_INDEX_REC_AT_SEQ(map, seq);
		rec->flags &= ENUM_NEGATE(MAIL_RECENT);
	}
}

int mail_index_map_check_header(struct mail_index_map *map,
				const char **error_r)
{
	struct mail_index *index = map->index;
	const struct mail_index_header *hdr = &map->hdr;

	if (!mail_index_check_header_compat(hdr, UOFF_T_MAX, error_r))
		return 0;

	/* following some extra checks that only take a bit of CPU */
	if (hdr->record_size < sizeof(struct mail_index_record)) {
		*error_r = t_strdup_printf(
			"record_size too small (%u < %zu)",
			hdr->record_size, sizeof(struct mail_index_record));
		return -1;
	}

	if (hdr->uid_validity == 0 && hdr->next_uid != 1) {
		*error_r = t_strdup_printf(
			"uidvalidity=0, but next_uid=%u", hdr->next_uid);
		return 0;
	}
	if (hdr->next_uid == 0) {
		*error_r = "next_uid=0";
		return 0;
	}
	if (hdr->messages_count > map->rec_map->records_count) {
		*error_r = t_strdup_printf(
			"messages_count is higher in header than record map (%u > %u)",
			hdr->messages_count, map->rec_map->records_count);
		return 0;
	}

	if (hdr->seen_messages_count > hdr->messages_count) {
		*error_r = t_strdup_printf(
			"seen_messages_count %u > messages_count %u",
			hdr->seen_messages_count, hdr->messages_count);
		return 0;
	}
	if (hdr->deleted_messages_count > hdr->messages_count) {
		*error_r = t_strdup_printf(
			"deleted_messages_count %u > messages_count %u",
			hdr->deleted_messages_count, hdr->messages_count);
		return 0;
	}
	switch (hdr->minor_version) {
	case 0:
		/* upgrade silently from v1.0 */
		map->hdr.unused_old_recent_messages_count = 0;
		if (hdr->first_recent_uid == 0)
			map->hdr.first_recent_uid = 1;
		if (index->need_recreate == NULL)
			index->need_recreate = i_strdup("Upgrading from index version 1.0");
		/* fall through */
	case 1:
		/* pre-v1.1.rc6: make sure the \Recent flags are gone */
		mail_index_map_clear_recent_flags(map);
		/* fall through */
	case 2:
		/* pre-v2.2 (although should have been done in v2.1 already):
		   make sure the old unused fields are cleared */
		map->hdr.unused_old_sync_size_part1 = 0;
		map->hdr.log2_rotate_time = 0;
		map->hdr.last_temp_file_scan = 0;
	}
	/* The old index format is now updated, set minor_version to latest
	   value. However, if minor_version is larger than what we know of,
	   keep it as it is to avoid breaking anything. */
	if (map->hdr.minor_version < MAIL_INDEX_MINOR_VERSION)
		map->hdr.minor_version = MAIL_INDEX_MINOR_VERSION;
	if (hdr->first_recent_uid == 0) {
		*error_r = "first_recent_uid=0";
		return 0;
	}
	if (hdr->first_recent_uid > hdr->next_uid) {
		*error_r = t_strdup_printf(
			"first_recent_uid %u > next_uid %u",
			hdr->first_recent_uid, hdr->next_uid);
		return 0;
	}
	if (hdr->first_unseen_uid_lowwater > hdr->next_uid) {
		*error_r = t_strdup_printf(
			"first_unseen_uid_lowwater %u > next_uid %u",
			hdr->first_unseen_uid_lowwater, hdr->next_uid);
		return 0;
	}
	if (hdr->first_deleted_uid_lowwater > hdr->next_uid) {
		*error_r = t_strdup_printf(
			"first_deleted_uid_lowwater %u > next_uid %u",
			hdr->first_deleted_uid_lowwater, hdr->next_uid);
		return 0;
	}

	if (hdr->messages_count > 0) {
		/* last message's UID must be smaller than next_uid.
		   also make sure it's not zero. */
		const struct mail_index_record *rec;

		rec = MAIL_INDEX_REC_AT_SEQ(map, hdr->messages_count);
		if (rec->uid == 0) {
			*error_r = "last message has uid=0";
			return -1;
		}
		if (rec->uid >= hdr->next_uid) {
			*error_r = t_strdup_printf(
				"last message uid %u >= next_uid %u",
				rec->uid, hdr->next_uid);
			return 0;
		}
	}
	return 1;
}
