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

/* @UNSAFE: whole file */

#include "lib.h"
#include "mmap-util.h"

#include <fcntl.h>

#ifndef MAP_ANONYMOUS
#  ifdef MAP_ANON
#    define MAP_ANONYMOUS MAP_ANON
#  else
#    define MAP_ANONYMOUS 0
#  endif
#endif

#ifndef HAVE_LINUX_MREMAP

#include <sys/mman.h>

#define MMAP_SIGNATURE 0xdeadbeef

#define PAGE_ALIGN(size) \
	(((size) + (size_t)page_size-1) & ~(size_t)(page_size-1))

struct anon_header {
	unsigned int signature;
	size_t size;
};

static int page_size = 0;
static int header_size = 0;
static int zero_fd = -1;

static void movable_mmap_init(void)
{
#if MAP_ANONYMOUS == 0
	/* mmap()ing /dev/zero should be the same with some platforms */
	zero_fd = open("/dev/zero", O_RDWR);
	if (zero_fd == -1)
		i_fatal("Can't open /dev/zero for creating anonymous mmap: %m");
	fd_close_on_exec(zero_fd, TRUE);
#endif

	page_size = mmap_get_page_size();
	header_size = page_size;
}

void *mmap_anon(size_t length)
{
	struct anon_header *hdr;
	void *base;

	if (header_size == 0)
		movable_mmap_init();

	/* we need extra page to store the pieces which construct
	   the full mmap. also allocate only page-aligned mmap sizes. */
	length = PAGE_ALIGN(length + header_size);

	base = mmap(NULL, length, PROT_READ | PROT_WRITE,
		    MAP_ANONYMOUS | MAP_PRIVATE, zero_fd, 0);
	if (base == MAP_FAILED)
		return MAP_FAILED;

	/* initialize the header */
	hdr = base;
	hdr->signature = MMAP_SIGNATURE;
	hdr->size = length - header_size;

	return (char *) hdr + header_size;
}

static void *mremap_move(struct anon_header *hdr, size_t new_size)
{
	void *new_base;
	char *p;
	size_t block_size, old_size;

	new_base = mmap_anon(new_size);
	if (new_base == MAP_FAILED)
		return MAP_FAILED;

	/* If we're moving large memory areas, it takes less memory to
	   copy the memory pages in smaller blocks. */
	old_size = hdr->size;
	block_size = 1024*1024;

	p = (char *) hdr + header_size + hdr->size;
	do {
		if (block_size > old_size)
			block_size = old_size;
		p -= block_size;
		old_size -= block_size;

		memcpy((char *) new_base + old_size, p, block_size);
		if (munmap((void *) p, block_size) < 0)
			i_panic("munmap() failed: %m");
	} while (old_size != 0);

	if (munmap((void *) hdr, header_size) < 0)
		i_panic("munmap() failed: %m");

	return new_base;
}

void *mremap_anon(void *old_address, size_t old_size  ATTR_UNUSED,
		  size_t new_size, unsigned long flags)
{
	struct anon_header *hdr;

	if (old_address == NULL || old_address == MAP_FAILED) {
		errno = EINVAL;
		return MAP_FAILED;
	}

	hdr = (struct anon_header *) ((char *) old_address - header_size);
	if (hdr->signature != MMAP_SIGNATURE)
		i_panic("movable_mremap(): Invalid old_address");

	new_size = PAGE_ALIGN(new_size);

	if (new_size > hdr->size) {
		/* grow */
		if ((flags & MREMAP_MAYMOVE) == 0) {
			errno = ENOMEM;
			return MAP_FAILED;
		}

		return mremap_move(hdr, new_size);
	}

	if (new_size < hdr->size) {
		/* shrink */
		if (munmap((void *) ((char *) hdr + header_size + new_size),
			   hdr->size - new_size) < 0)
			i_panic("munmap() failed: %m");
		hdr->size = new_size;
	}

	return old_address;
}

int munmap_anon(void *start, size_t length ATTR_UNUSED)
{
	struct anon_header *hdr;

	if (start == NULL || start == MAP_FAILED) {
		errno = EINVAL;
		return -1;
	}

	hdr = (struct anon_header *) ((char *) start - header_size);
	if (hdr->signature != MMAP_SIGNATURE)
		i_panic("movable_munmap(): Invalid address");

	if (munmap((void *) hdr, hdr->size + header_size) < 0)
		i_panic("munmap() failed: %m");

	return 0;
}

#else

void *mmap_anon(size_t length)
{
	return mmap(NULL, length, PROT_READ | PROT_WRITE,
		    MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
}

void *mremap_anon(void *old_address, size_t old_size, size_t new_size,
		  unsigned long flags)
{
	return mremap(old_address, old_size, new_size, flags);
}

int munmap_anon(void *start, size_t length)
{
	return munmap(start, length);
}

#endif
