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

#include "lib.h"
#include "array.h"
#include "str-sanitize.h"
#include "mmap-util.h"
#include "mail-index-private.h"
#include "mail-index-modseq.h"

void mail_index_map_init_extbufs(struct mail_index_map *map,
				 unsigned int initial_count)
{
#define EXTENSION_NAME_APPROX_LEN 20
#define EXT_GLOBAL_ALLOC_SIZE \
	((sizeof(map->extensions) + sizeof(buffer_t)) * 2)
#define EXT_PER_ALLOC_SIZE \
	(EXTENSION_NAME_APPROX_LEN + \
	 sizeof(struct mail_index_ext) + sizeof(uint32_t))
	size_t size;

	if (map->extension_pool == NULL) {
		size = EXT_GLOBAL_ALLOC_SIZE +
			initial_count * EXT_PER_ALLOC_SIZE;
		map->extension_pool =
			pool_alloconly_create(MEMPOOL_GROWING"map extensions",
					      nearest_power(size));
	} else {
		p_clear(map->extension_pool);

		/* try to use the existing pool's size for initial_count so
		   we don't grow it needlessly */
		size = p_get_max_easy_alloc_size(map->extension_pool);
		if (size > EXT_GLOBAL_ALLOC_SIZE + EXT_PER_ALLOC_SIZE) {
			initial_count = (size - EXT_GLOBAL_ALLOC_SIZE) /
				EXT_PER_ALLOC_SIZE;
		}
	}

	p_array_init(&map->extensions, map->extension_pool, initial_count);
	p_array_init(&map->ext_id_map, map->extension_pool, initial_count);
}

bool mail_index_map_lookup_ext(struct mail_index_map *map, const char *name,
			       uint32_t *idx_r)
{
	const struct mail_index_ext *ext;

	if (!array_is_created(&map->extensions))
		return FALSE;

	array_foreach(&map->extensions, ext) {
		if (strcmp(ext->name, name) == 0) {
			*idx_r = array_foreach_idx(&map->extensions, ext);
			return TRUE;
		}
	}
	return FALSE;
}

unsigned int mail_index_map_ext_hdr_offset(unsigned int name_len)
{
	size_t size = sizeof(struct mail_index_ext_header) + name_len;
	return MAIL_INDEX_HEADER_SIZE_ALIGN(size);
}

uint32_t
mail_index_map_register_ext(struct mail_index_map *map,
			    const char *name, uint32_t ext_offset,
			    const struct mail_index_ext_header *ext_hdr)
{
	struct mail_index_ext *ext;
	uint32_t idx, ext_map_idx, empty_idx = (uint32_t)-1;

	i_assert(mail_index_ext_name_is_valid(name));

	if (!array_is_created(&map->extensions)) {
                mail_index_map_init_extbufs(map, 5);
		idx = 0;
	} else {
		idx = array_count(&map->extensions);
	}
	i_assert(!mail_index_map_lookup_ext(map, name, &ext_map_idx));

	ext = array_append_space(&map->extensions);
	ext->name = p_strdup(map->extension_pool, name);
	ext->ext_offset = ext_offset;
	ext->hdr_offset = ext_offset == (uint32_t)-1 ? (uint32_t)-1 :
		ext_offset + mail_index_map_ext_hdr_offset(strlen(name));
	ext->hdr_size = ext_hdr->hdr_size;
	ext->record_offset = ext_hdr->record_offset;
	ext->record_size = ext_hdr->record_size;
	ext->record_align = ext_hdr->record_align;
	ext->reset_id = ext_hdr->reset_id;

	ext->index_idx = mail_index_ext_register(map->index, name,
						 ext_hdr->hdr_size,
						 ext_hdr->record_size,
						 ext_hdr->record_align);

	/* Update index ext_id -> map ext_id mapping. Fill non-used
	   ext_ids with (uint32_t)-1 */
	while (array_count(&map->ext_id_map) < ext->index_idx)
		array_push_back(&map->ext_id_map, &empty_idx);
	array_idx_set(&map->ext_id_map, ext->index_idx, &idx);
	return idx;
}

int mail_index_map_ext_get_next(struct mail_index_map *map,
				unsigned int *offset_p,
				const struct mail_index_ext_header **ext_hdr_r,
				const char **name_r)
{
	const struct mail_index_ext_header *ext_hdr;
	unsigned int offset, name_offset;

	offset = *offset_p;
	*name_r = "";

	/* Extension header contains:
	   - struct mail_index_ext_header
	   - name (not 0-terminated)
	   - 64bit alignment padding
	   - extension header contents
	   - 64bit alignment padding
	*/
	name_offset = offset + sizeof(*ext_hdr);
	ext_hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, offset);
	if (offset + sizeof(*ext_hdr) >= map->hdr.header_size)
		return -1;

	offset += mail_index_map_ext_hdr_offset(ext_hdr->name_size);
	if (offset > map->hdr.header_size)
		return -1;

	*name_r = t_strndup(MAIL_INDEX_MAP_HDR_OFFSET(map, name_offset),
			    ext_hdr->name_size);
	if (strcmp(*name_r, str_sanitize(*name_r, SIZE_MAX)) != 0) {
		/* we allow only plain ASCII names, so this extension
		   is most likely broken */
		*name_r = "";
	}

	/* finally make sure that the hdr_size is small enough.
	   do this last so that we could return a usable name. */
	offset += MAIL_INDEX_HEADER_SIZE_ALIGN(ext_hdr->hdr_size);
	if (offset > map->hdr.header_size)
		return -1;

	*offset_p = offset;
	*ext_hdr_r = ext_hdr;
	return 0;
}

static int
mail_index_map_ext_hdr_check_record(const struct mail_index_header *hdr,
				    const struct mail_index_ext_header *ext_hdr,
				    const char **error_r)
{
	if (ext_hdr->record_align == 0) {
		*error_r = "Record field alignment is zero";
		return -1;
	}

	/* until we get 128 bit CPUs having a larger alignment is pointless */
	if (ext_hdr->record_align > sizeof(uint64_t)) {
		*error_r = "Record alignment is too large";
		return -1;
	}
	/* a large record size is most likely a bug somewhere. the maximum
	   record size is limited to 64k anyway, so try to fail earlier. */
	if (ext_hdr->record_size >= 32768) {
		*error_r = "Record size is too large";
		return -1;
	}

	if (ext_hdr->record_offset == 0) {
		/* if we get here from extension introduction, record_offset=0
		   and hdr->record_size hasn't been updated yet */
		return 0;
	}

	if (ext_hdr->record_offset + ext_hdr->record_size > hdr->record_size) {
		*error_r = t_strdup_printf("Record field points "
					   "outside record size (%u+%u > %u)",
					   ext_hdr->record_offset,
					   ext_hdr->record_size,
					   hdr->record_size);
		return -1;
	}

	if ((ext_hdr->record_offset % ext_hdr->record_align) != 0) {
		*error_r = t_strdup_printf("Record field alignment %u "
					   "not used", ext_hdr->record_align);
		return -1;
	}
	if ((hdr->record_size % ext_hdr->record_align) != 0) {
		*error_r = t_strdup_printf("Record size not aligned by %u "
					   "as required by extension",
					   ext_hdr->record_align);
		return -1;
	}
	return 0;
}

int mail_index_map_ext_hdr_check(const struct mail_index_header *hdr,
				 const struct mail_index_ext_header *ext_hdr,
				 const char *name, const char **error_r)
{
	if (ext_hdr->record_size == 0 && ext_hdr->hdr_size == 0) {
		*error_r = "Invalid field values";
		return -1;
	}
	if (!mail_index_ext_name_is_valid(name)) {
		*error_r = "Invalid name";
		return -1;
	}

	if (ext_hdr->record_size != 0) {
		if (mail_index_map_ext_hdr_check_record(hdr, ext_hdr,
							error_r) < 0)
			return -1;
	}
	if (ext_hdr->hdr_size > MAIL_INDEX_EXT_HEADER_MAX_SIZE) {
		*error_r = t_strdup_printf("Headersize too large (%u)",
					   ext_hdr->hdr_size);
		return -1;
	}
	return 0;
}

static void mail_index_header_init(struct mail_index *index,
				   struct mail_index_header *hdr)
{
	i_assert((sizeof(*hdr) % sizeof(uint64_t)) == 0);

	i_zero(hdr);

	hdr->major_version = MAIL_INDEX_MAJOR_VERSION;
	hdr->minor_version = MAIL_INDEX_MINOR_VERSION;
	hdr->base_header_size = sizeof(*hdr);
	hdr->header_size = sizeof(*hdr);
	hdr->record_size = sizeof(struct mail_index_record);

#ifndef WORDS_BIGENDIAN
	hdr->compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
#endif

	hdr->indexid = index->indexid;
	hdr->log_file_seq = 1;
	hdr->next_uid = 1;
	hdr->first_recent_uid = 1;
}

struct mail_index_map *mail_index_map_alloc(struct mail_index *index)
{
	struct mail_index_map tmp_map;

	i_zero(&tmp_map);
	mail_index_header_init(index, &tmp_map.hdr);
	tmp_map.hdr_copy_buf = t_buffer_create(sizeof(tmp_map.hdr));
	buffer_append(tmp_map.hdr_copy_buf, &tmp_map.hdr, sizeof(tmp_map.hdr));
	tmp_map.index = index;

	/* a bit kludgy way to do this, but it initializes everything
	   nicely and correctly */
	return mail_index_map_clone(&tmp_map);
}

static void mail_index_record_map_free(struct mail_index_map *map,
				       struct mail_index_record_map *rec_map)
{
	if (rec_map->buffer != NULL) {
		i_assert(rec_map->mmap_base == NULL);
		buffer_free(&rec_map->buffer);
	} else if (rec_map->mmap_base != NULL) {
		i_assert(rec_map->buffer == NULL);
		if (munmap(rec_map->mmap_base, rec_map->mmap_size) < 0)
			mail_index_set_syscall_error(map->index, "munmap()");
		rec_map->mmap_base = NULL;
	}
	array_free(&rec_map->maps);
	i_free(rec_map);
}

static void mail_index_record_map_unlink(struct mail_index_map *map)
{
	unsigned int idx;

	if (!array_lsearch_ptr_idx(&map->rec_map->maps, map, &idx))
		i_unreached();
	array_delete(&map->rec_map->maps, idx, 1);

	if (array_count(&map->rec_map->maps) == 0) {
		mail_index_record_map_free(map, map->rec_map);
		map->rec_map = NULL;
	}
}

void mail_index_unmap(struct mail_index_map **_map)
{
	struct mail_index_map *map = *_map;

	*_map = NULL;
	if (--map->refcount > 0)
		return;

	i_assert(map->refcount == 0);
	mail_index_record_map_unlink(map);

	pool_unref(&map->extension_pool);
	if (array_is_created(&map->keyword_idx_map))
		array_free(&map->keyword_idx_map);
	buffer_free(&map->hdr_copy_buf);
	i_free(map);
}

static void mail_index_map_copy_records(struct mail_index_record_map *dest,
					const struct mail_index_record_map *src,
					unsigned int record_size)
{
	size_t size;

	size = src->records_count * record_size;
	/* +1% so we have a bit of space to grow. useful for huge mailboxes. */
	dest->buffer = buffer_create_dynamic(default_pool,
					     size + I_MAX(size/100, 1024));
	buffer_append(dest->buffer, src->records, size);

	dest->records = buffer_get_modifiable_data(dest->buffer, NULL);
	dest->records_count = src->records_count;
}

static void mail_index_map_copy_header(struct mail_index_map *dest,
				       const struct mail_index_map *src)
{
	/* use src->hdr copy directly, because if we got here
	   from syncing it has the latest changes. */
	if (src != dest)
		dest->hdr = src->hdr;
	if (dest->hdr_copy_buf != NULL) {
		if (src == dest)
			return;

		buffer_set_used_size(dest->hdr_copy_buf, 0);
	} else {
		dest->hdr_copy_buf =
			buffer_create_dynamic(default_pool,
					      dest->hdr.header_size);
	}
	buffer_append(dest->hdr_copy_buf, &dest->hdr,
		      I_MIN(sizeof(dest->hdr), src->hdr.base_header_size));
	if (src != dest) {
		buffer_write(dest->hdr_copy_buf, src->hdr.base_header_size,
			     MAIL_INDEX_MAP_HDR_OFFSET(src, src->hdr.base_header_size),
			     src->hdr.header_size - src->hdr.base_header_size);
	}
	i_assert(dest->hdr_copy_buf->used == dest->hdr.header_size);
}

static struct mail_index_record_map *
mail_index_record_map_alloc(struct mail_index_map *map)
{
	struct mail_index_record_map *rec_map;

	rec_map = i_new(struct mail_index_record_map, 1);
	i_array_init(&rec_map->maps, 4);
	array_push_back(&rec_map->maps, &map);
	return rec_map;
}

struct mail_index_map *mail_index_map_clone(const struct mail_index_map *map)
{
	struct mail_index_map *mem_map;
	struct mail_index_ext *ext;
	unsigned int count;

	mem_map = i_new(struct mail_index_map, 1);
	mem_map->index = map->index;
	mem_map->refcount = 1;
	if (map->rec_map == NULL) {
		mem_map->rec_map = mail_index_record_map_alloc(mem_map);
		mem_map->rec_map->buffer =
			buffer_create_dynamic(default_pool, 1024);
	} else {
		mem_map->rec_map = map->rec_map;
		array_push_back(&mem_map->rec_map->maps, &mem_map);
	}

	mail_index_map_copy_header(mem_map, map);

	/* copy extensions */
	if (array_is_created(&map->ext_id_map)) {
		count = array_count(&map->ext_id_map);
		mail_index_map_init_extbufs(mem_map, count + 2);

		array_append_array(&mem_map->extensions, &map->extensions);
		array_append_array(&mem_map->ext_id_map, &map->ext_id_map);

		/* fix the name pointers to use our own pool */
		array_foreach_modifiable(&mem_map->extensions, ext) {
			i_assert(ext->record_offset + ext->record_size <=
				 mem_map->hdr.record_size);
			ext->name = p_strdup(mem_map->extension_pool,
					     ext->name);
		}
	}

	/* copy keyword map */
	if (array_is_created(&map->keyword_idx_map)) {
		i_array_init(&mem_map->keyword_idx_map,
			     array_count(&map->keyword_idx_map) + 4);
		array_append_array(&mem_map->keyword_idx_map,
				   &map->keyword_idx_map);
	}

	return mem_map;
}

void mail_index_record_map_move_to_private(struct mail_index_map *map)
{
	struct mail_index_record_map *new_map;
	const struct mail_index_record *rec;

	if (array_count(&map->rec_map->maps) > 1) {
		/* Multiple references to the rec_map. Create a clone of the
		   rec_map, which is in memory. */
		new_map = mail_index_record_map_alloc(map);
		mail_index_map_copy_records(new_map, map->rec_map,
					    map->hdr.record_size);
		mail_index_record_map_unlink(map);
		map->rec_map = new_map;
	} else {
		new_map = map->rec_map;
	}

	if (new_map->records_count != map->hdr.messages_count) {
		/* The rec_map has more messages than what map contains.
		   These messages aren't necessary (and may confuse the caller),
		   so truncate them away. */
		i_assert(new_map->records_count > map->hdr.messages_count);
		new_map->records_count = map->hdr.messages_count;
		if (new_map->records_count == 0)
			new_map->last_appended_uid = 0;
		else {
			rec = MAIL_INDEX_REC_AT_SEQ(map, new_map->records_count);
			new_map->last_appended_uid = rec->uid;
		}
		buffer_set_used_size(new_map->buffer, new_map->records_count *
				     map->hdr.record_size);
	}
}

void mail_index_map_move_to_memory(struct mail_index_map *map)
{
	struct mail_index_record_map *new_map;

	if (map->rec_map->mmap_base == NULL) {
		/* rec_map is already in memory */
		return;
	}

	/* Move the rec_map contents to memory. If this is the only map that
	   refers to the rec_map, it can be directly replaced and the old
	   content munmap()ed. Otherwise, create a new rec_map for this map. */
	if (array_count(&map->rec_map->maps) == 1)
		new_map = map->rec_map;
	else {
		new_map = mail_index_record_map_alloc(map);
	}

	mail_index_map_copy_records(new_map, map->rec_map,
				    map->hdr.record_size);
	mail_index_map_copy_header(map, map);

	if (new_map != map->rec_map) {
		mail_index_record_map_unlink(map);
		map->rec_map = new_map;
	} else {
		if (munmap(new_map->mmap_base, new_map->mmap_size) < 0)
			mail_index_set_syscall_error(map->index, "munmap()");
		new_map->mmap_base = NULL;
	}
}

bool mail_index_map_get_ext_idx(struct mail_index_map *map,
				uint32_t ext_id, uint32_t *idx_r)
{
	const uint32_t *id;

	if (!array_is_created(&map->ext_id_map) ||
	    ext_id >= array_count(&map->ext_id_map))
		return FALSE;

	id = array_idx(&map->ext_id_map, ext_id);
	*idx_r = *id;
	return *idx_r != (uint32_t)-1;
}

static uint32_t mail_index_bsearch_uid(struct mail_index_map *map,
				       uint32_t uid, uint32_t left_idx,
				       int nearest_side)
{
	const struct mail_index_record *rec_base, *rec;
	uint32_t idx, right_idx, record_size;

	i_assert(map->hdr.messages_count <= map->rec_map->records_count);

	rec_base = map->rec_map->records;
	record_size = map->hdr.record_size;

	idx = left_idx;
	right_idx = I_MIN(map->hdr.messages_count, uid);

	i_assert(right_idx < INT_MAX);
	while (left_idx < right_idx) {
		idx = (left_idx + right_idx) / 2;

                rec = CONST_PTR_OFFSET(rec_base, idx * record_size);
		if (rec->uid < uid)
			left_idx = idx+1;
		else if (rec->uid > uid)
			right_idx = idx;
		else
			break;
	}
	i_assert(idx < map->hdr.messages_count);

	rec = CONST_PTR_OFFSET(rec_base, idx * record_size);
	if (rec->uid != uid) {
		if (nearest_side > 0) {
			/* we want uid or larger */
			return rec->uid > uid ? idx+1 :
				(idx == map->hdr.messages_count-1 ? 0 : idx+2);
		} else {
			/* we want uid or smaller */
			return rec->uid < uid ? idx + 1 : idx;
		}
	}

	return idx+1;
}

void mail_index_map_lookup_seq_range(struct mail_index_map *map,
				     uint32_t first_uid, uint32_t last_uid,
				     uint32_t *first_seq_r,
				     uint32_t *last_seq_r)
{
	i_assert(first_uid > 0);
	i_assert(first_uid <= last_uid);

	if (map->hdr.messages_count == 0) {
		*first_seq_r = *last_seq_r = 0;
		return;
	}

	*first_seq_r = mail_index_bsearch_uid(map, first_uid, 0, 1);
	if (*first_seq_r == 0 ||
	    MAIL_INDEX_REC_AT_SEQ(map, *first_seq_r)->uid > last_uid) {
		*first_seq_r = *last_seq_r = 0;
		return;
	}

	if (last_uid >= map->hdr.next_uid-1) {
		/* we want the last message */
		last_uid = map->hdr.next_uid-1;
		if (first_uid > last_uid) {
			*first_seq_r = *last_seq_r = 0;
			return;
		}

		*last_seq_r = map->hdr.messages_count;
		return;
	}

	if (first_uid == last_uid)
		*last_seq_r = *first_seq_r;
	else {
		/* optimization - binary lookup only from right side: */
		*last_seq_r = mail_index_bsearch_uid(map, last_uid,
						     *first_seq_r - 1, -1);
	}
	i_assert(*last_seq_r >= *first_seq_r);
}
