/*
   +----------------------------------------------------------------------+
   | Copyright © The PHP Group and Contributors.                          |
   +----------------------------------------------------------------------+
   | This source file is subject to the Modified BSD License that is      |
   | bundled with this package in the file LICENSE, and is available      |
   | through the World Wide Web at <https://www.php.net/license/>.        |
   |                                                                      |
   | SPDX-License-Identifier: BSD-3-Clause                                |
   +----------------------------------------------------------------------+
   | Author: Sascha Schumann <sascha@schumann.cx>                         |
   +----------------------------------------------------------------------+
 */

#include "php.h"

#ifdef HAVE_LIBMM

#include <unistd.h>
#include <mm.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdint.h>

#include "php_session.h"
#include "mod_mm.h"
#include "SAPI.h"

#ifdef ZTS
# error mm is not thread-safe
#endif

#define PS_MM_FILE "session_mm_"

/* This list holds all data associated with one session. */

typedef struct ps_sd {
	struct ps_sd *next;
	uint32_t hv;        /* hash value of key */
	time_t ctime;       /* time of last change */
	void *data;
	size_t datalen;     /* amount of valid data */
	size_t alloclen;    /* amount of allocated memory for data */
	zend_string *key;
} ps_sd;

typedef struct {
	MM *mm;
	ps_sd **hash;
	uint32_t hash_max;
	uint32_t hash_cnt;
	pid_t owner;
} ps_mm;

static ps_mm *ps_mm_instance = NULL;

#if 0
# define ps_mm_debug(a) printf a
#else
# define ps_mm_debug(a)
#endif

static inline uint32_t ps_sd_hash(const zend_string *data)
{
	uint32_t h;
	const char *data_char = ZSTR_VAL(data);
	const char *e = ZSTR_VAL(data) + ZSTR_LEN(data);

	for (h = 2166136261U; data_char < e; ) {
		h *= 16777619;
		h ^= *data_char++;
	}

	return h;
}

static void hash_split(ps_mm *data)
{
	uint32_t nmax;
	ps_sd **nhash;
	ps_sd **ohash, **ehash;
	ps_sd *ps, *next;

	nmax = ((data->hash_max + 1) << 1) - 1;
	nhash = mm_calloc(data->mm, nmax + 1, sizeof(*data->hash));

	if (!nhash) {
		/* no further memory to expand hash table */
		return;
	}

	ehash = data->hash + data->hash_max + 1;
	for (ohash = data->hash; ohash < ehash; ohash++) {
		for (ps = *ohash; ps; ps = next) {
			next = ps->next;
			ps->next = nhash[ps->hv & nmax];
			nhash[ps->hv & nmax] = ps;
		}
	}
	mm_free(data->mm, data->hash);

	data->hash = nhash;
	data->hash_max = nmax;
}

static ps_sd *ps_sd_new(ps_mm *data, zend_string *key)
{
	uint32_t hv, slot;
	ps_sd *sd;

	sd = mm_malloc(data->mm, sizeof(ps_sd) + ZSTR_LEN(key));
	if (!sd) {

		php_error_docref(NULL, E_WARNING, "mm_malloc failed, avail %ld, err %s", mm_available(data->mm), mm_error());
		return NULL;
	}

	hv = ps_sd_hash(key);
	slot = hv & data->hash_max;

	sd->ctime = 0;
	sd->hv = hv;
	sd->data = NULL;
	sd->alloclen = sd->datalen = 0;

	sd->key = zend_string_copy(key);

	sd->next = data->hash[slot];
	data->hash[slot] = sd;

	data->hash_cnt++;

	if (!sd->next) {
		if (data->hash_cnt >= data->hash_max) {
			hash_split(data);
		}
	}

	ps_mm_debug(("inserting %s(%p) into slot %d\n", ZSTR_VAL(key), sd, slot));

	return sd;
}

static void ps_sd_destroy(ps_mm *data, ps_sd *sd)
{
	uint32_t slot;

	slot = ps_sd_hash(sd->key) & data->hash_max;

	if (data->hash[slot] == sd) {
		data->hash[slot] = sd->next;
	} else {
		ps_sd *prev;

		/* There must be some entry before the one we want to delete */
		for (prev = data->hash[slot]; prev->next != sd; prev = prev->next);
		prev->next = sd->next;
	}

	data->hash_cnt--;

	if (sd->data) {
		mm_free(data->mm, sd->data);
	}
	zend_string_release(sd->key);

	mm_free(data->mm, sd);
}

static ps_sd *ps_sd_lookup(ps_mm *data, const zend_string *key, bool rw)
{
	uint32_t hv, slot;
	ps_sd *ret, *prev;

	hv = ps_sd_hash(key);
	slot = hv & data->hash_max;

	for (prev = NULL, ret = data->hash[slot]; ret; prev = ret, ret = ret->next) {
		if (ret->hv == hv && zend_string_equals(ret->key, key)) {
			break;
		}
	}

	if (ret && rw && ret != data->hash[slot]) {
		/* Move the entry to the top of the linked list */
		if (prev) {
			prev->next = ret->next;
		}

		ret->next = data->hash[slot];
		data->hash[slot] = ret;
	}

	ps_mm_debug(("lookup(%s): ret=%p,hv=%u,slot=%d\n", ZSTR_VAL(key), ret, hv, slot));

	return ret;
}

static zend_result ps_mm_key_exists(ps_mm *data, const zend_string *key)
{
	ps_sd *sd;

	if (!key) {
		return FAILURE;
	}
	sd = ps_sd_lookup(data, key, false);
	if (sd) {
		return SUCCESS;
	}
	return FAILURE;
}

const ps_module ps_mod_mm = {
	PS_MOD(mm)
};

#define PS_MM_DATA ps_mm *data = PS_GET_MOD_DATA()

static zend_result ps_mm_initialize(ps_mm *data, const char *path)
{
	data->owner = getpid();
	data->mm = mm_create(0, path);
	if (!data->mm) {
		return FAILURE;
	}

	data->hash_cnt = 0;
	data->hash_max = 511;
	data->hash = mm_calloc(data->mm, data->hash_max + 1, sizeof(ps_sd *));
	if (!data->hash) {
		mm_destroy(data->mm);
		return FAILURE;
	}

	return SUCCESS;
}

static void ps_mm_destroy(ps_mm *data)
{
	ps_sd *sd, *next;

	/* This function is called during each module shutdown,
	   but we must not release the shared memory pool, when
	   an Apache child dies! */
	if (data->owner != getpid()) {
		return;
	}

	for (int h = 0; h < data->hash_max + 1; h++) {
		for (sd = data->hash[h]; sd; sd = next) {
			next = sd->next;
			ps_sd_destroy(data, sd);
		}
	}

	mm_free(data->mm, data->hash);
	mm_destroy(data->mm);
	free(data);
}

PHP_MINIT_FUNCTION(ps_mm)
{
	size_t save_path_len = ZSTR_LEN(PS(save_path));
	size_t mod_name_len = strlen(sapi_module.name);
	size_t euid_len;
	char *ps_mm_path, euid[30];
	zend_result ret;

	ps_mm_instance = calloc(sizeof(*ps_mm_instance), 1);
	if (!ps_mm_instance) {
		return FAILURE;
	}

	if (!(euid_len = slprintf(euid, sizeof(euid), "%d", geteuid()))) {
		free(ps_mm_instance);
		ps_mm_instance = NULL;
		return FAILURE;
	}

	/* Directory + '/' + File + Module Name + Effective UID + \0 */
	ps_mm_path = emalloc(save_path_len + 1 + (sizeof(PS_MM_FILE) - 1) + mod_name_len + euid_len + 1);

	memcpy(ps_mm_path, ZSTR_VAL(PS(save_path)), save_path_len);
	if (save_path_len && ZSTR_VAL(PS(save_path))[save_path_len - 1] != DEFAULT_SLASH) {
		ps_mm_path[save_path_len] = DEFAULT_SLASH;
		save_path_len++;
	}
	memcpy(ps_mm_path + save_path_len, PS_MM_FILE, sizeof(PS_MM_FILE) - 1);
	save_path_len += sizeof(PS_MM_FILE) - 1;
	memcpy(ps_mm_path + save_path_len, sapi_module.name, mod_name_len);
	save_path_len += mod_name_len;
	memcpy(ps_mm_path + save_path_len, euid, euid_len);
	ps_mm_path[save_path_len + euid_len] = '\0';

	ret = ps_mm_initialize(ps_mm_instance, ps_mm_path);

	efree(ps_mm_path);

	if (ret == FAILURE) {
		free(ps_mm_instance);
		ps_mm_instance = NULL;
		return FAILURE;
	}

	php_session_register_module(&ps_mod_mm);
	return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(ps_mm)
{
	if (ps_mm_instance) {
		ps_mm_destroy(ps_mm_instance);
		return SUCCESS;
	}
	return FAILURE;
}

PS_OPEN_FUNC(mm)
{
	ps_mm_debug(("open: ps_mm_instance=%p\n", ps_mm_instance));

	if (!ps_mm_instance) {
		return FAILURE;
	}
	PS_SET_MOD_DATA(ps_mm_instance);

	return SUCCESS;
}

PS_CLOSE_FUNC(mm)
{
	PS_SET_MOD_DATA(NULL);

	return SUCCESS;
}

PS_READ_FUNC(mm)
{
	PS_MM_DATA;
	ps_sd *sd;
	zend_result ret = FAILURE;

	mm_lock(data->mm, MM_LOCK_RD);

	sd = ps_sd_lookup(data, key, false);
	if (sd) {
		*val = zend_string_init(sd->data, sd->datalen, false);
		ret = SUCCESS;
	}

	mm_unlock(data->mm);

	return ret;
}

PS_WRITE_FUNC(mm)
{
	PS_MM_DATA;
	ps_sd *sd;

	mm_lock(data->mm, MM_LOCK_RW);

	sd = ps_sd_lookup(data, key, true);
	if (!sd) {
		sd = ps_sd_new(data, key);
		ps_mm_debug(("new entry for %s\n", ZSTR_VAL(key)));
	}

	if (sd) {
		if (val->len >= sd->alloclen) {
			if (data->mm) {
				mm_free(data->mm, sd->data);
			}
			sd->alloclen = val->len + 1;
			sd->data = mm_malloc(data->mm, sd->alloclen);

			if (!sd->data) {
				ps_sd_destroy(data, sd);
				php_error_docref(NULL, E_WARNING, "Cannot allocate new data segment");
				sd = NULL;
			}
		}
		if (sd) {
			sd->datalen = val->len;
			memcpy(sd->data, val->val, val->len);
			time(&sd->ctime);
		}
	}

	mm_unlock(data->mm);

	return sd ? SUCCESS : FAILURE;
}

PS_DESTROY_FUNC(mm)
{
	PS_MM_DATA;
	ps_sd *sd;

	mm_lock(data->mm, MM_LOCK_RW);

	sd = ps_sd_lookup(data, key, false);
	if (sd) {
		ps_sd_destroy(data, sd);
	}

	mm_unlock(data->mm);

	return SUCCESS;
}

PS_GC_FUNC(mm)
{
	PS_MM_DATA;
	time_t limit;
	ps_sd **ohash, **ehash;
	ps_sd *sd, *next;

	*nrdels = 0;
	ps_mm_debug(("gc\n"));

	time(&limit);

	limit -= maxlifetime;

	mm_lock(data->mm, MM_LOCK_RW);

	ehash = data->hash + data->hash_max + 1;
	for (ohash = data->hash; ohash < ehash; ohash++) {
		for (sd = *ohash; sd; sd = next) {
			next = sd->next;
			if (sd->ctime < limit) {
				ps_mm_debug(("purging %s\n", ZSTR_VAL(sd->key)));
				ps_sd_destroy(data, sd);
				(*nrdels)++;
			}
		}
	}

	mm_unlock(data->mm);

	return *nrdels;
}

PS_CREATE_SID_FUNC(mm)
{
	zend_string *sid;
	int maxfail = 3;
	PS_MM_DATA;

	do {
		sid = php_session_create_id((void **)&data);
		/* Check collision */
		if (ps_mm_key_exists(data, sid) == SUCCESS) {
			if (sid) {
				zend_string_release_ex(sid, false);
				sid = NULL;
			}
			if (!(maxfail--)) {
				return NULL;
			}
		}
	} while (!sid);

	return sid;
}

/*
 * Check session ID existence for use_strict_mode support.
 * PARAMETERS: PS_VALIDATE_SID_ARGS in php_session.h
 * RETURN VALUE: SUCCESS or FAILURE.
 *
 * Return SUCCESS for valid key(already existing session).
 * Return FAILURE for invalid key(non-existing session).
 * *mod_data, *key are guaranteed to have non-NULL values.
 */
PS_VALIDATE_SID_FUNC(mm)
{
	PS_MM_DATA;

	mm_lock(data->mm, MM_LOCK_RD);
	zend_result ret = ps_mm_key_exists(data, key)
	mm_unlock(data->mm);
	return ret;
}

#endif
