/*
   +----------------------------------------------------------------------+
   | 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: Anatol Belski <ab@php.net>                                   |
   +----------------------------------------------------------------------+
*/

#include "php_hash.h"
#include "php_hash_xxhash.h"

static hash_spec_result php_hash_xxh32_unserialize(
		php_hashcontext_object *hash, zend_long magic, const zval *zv);
static hash_spec_result php_hash_xxh64_unserialize(
		php_hashcontext_object *hash, zend_long magic, const zval *zv);

const php_hash_ops php_hash_xxh32_ops = {
	"xxh32",
	(php_hash_init_func_t) PHP_XXH32Init,
	(php_hash_update_func_t) PHP_XXH32Update,
	(php_hash_final_func_t) PHP_XXH32Final,
	(php_hash_copy_func_t) PHP_XXH32Copy,
	php_hash_serialize,
	php_hash_xxh32_unserialize,
	PHP_XXH32_SPEC,
	4,
	4,
	sizeof(PHP_XXH32_CTX),
	0,
	0
};

PHP_HASH_API void PHP_XXH32Init(PHP_XXH32_CTX *ctx, HashTable *args)
{
	/* XXH32_createState() is not used intentionally. */
	memset(&ctx->s, 0, sizeof ctx->s);

	if (args) {
		zval *seed = zend_hash_str_find_deref(args, "seed", sizeof("seed") - 1);
		/* This might be a bit too restrictive, but thinking that a seed might be set
			once and for all, it should be done a clean way. */
		if (seed) {
			if (IS_LONG == Z_TYPE_P(seed)) {
				XXH32_reset(&ctx->s, (XXH32_hash_t)Z_LVAL_P(seed));
				return;
			} else {
				php_error_docref(NULL, E_DEPRECATED, "Passing a seed of a type other than int is deprecated because it is the same as setting the seed to 0");
			}
		}
	}

	XXH32_reset(&ctx->s, 0);
}

PHP_HASH_API void PHP_XXH32Update(PHP_XXH32_CTX *ctx, const unsigned char *in, size_t len)
{
	XXH32_update(&ctx->s, in, len);
}

PHP_HASH_API void PHP_XXH32Final(unsigned char digest[4], PHP_XXH32_CTX *ctx)
{
	XXH32_canonicalFromHash((XXH32_canonical_t*)digest, XXH32_digest(&ctx->s));
}

PHP_HASH_API zend_result PHP_XXH32Copy(const php_hash_ops *ops, const PHP_XXH32_CTX *orig_context, PHP_XXH32_CTX *copy_context)
{
	copy_context->s = orig_context->s;
	return SUCCESS;
}

static hash_spec_result php_hash_xxh32_unserialize(
		php_hashcontext_object *hash, zend_long magic, const zval *zv)
{
	PHP_XXH32_CTX *ctx = (PHP_XXH32_CTX *) hash->context;
	hash_spec_result r = HASH_SPEC_FAILURE;
	if (magic == PHP_HASH_SERIALIZE_MAGIC_SPEC
		&& (r = php_hash_unserialize_spec(hash, zv, PHP_XXH32_SPEC)) == HASH_SPEC_SUCCESS
		&& ctx->s.memsize < 16) {
		return HASH_SPEC_SUCCESS;
	}

    return r != HASH_SPEC_SUCCESS ? r : CONTEXT_VALIDATION_FAILURE;
}

const php_hash_ops php_hash_xxh64_ops = {
	"xxh64",
	(php_hash_init_func_t) PHP_XXH64Init,
	(php_hash_update_func_t) PHP_XXH64Update,
	(php_hash_final_func_t) PHP_XXH64Final,
	(php_hash_copy_func_t) PHP_XXH64Copy,
	php_hash_serialize,
	php_hash_xxh64_unserialize,
	PHP_XXH64_SPEC,
	8,
	8,
	sizeof(PHP_XXH64_CTX),
	0,
	0
};

PHP_HASH_API void PHP_XXH64Init(PHP_XXH64_CTX *ctx, HashTable *args)
{
	/* XXH64_createState() is not used intentionally. */
	memset(&ctx->s, 0, sizeof ctx->s);

	if (args) {
		zval *seed = zend_hash_str_find_deref(args, "seed", sizeof("seed") - 1);
		/* This might be a bit too restrictive, but thinking that a seed might be set
			once and for all, it should be done a clean way. */
		if (seed && IS_LONG == Z_TYPE_P(seed)) {
			XXH64_reset(&ctx->s, (XXH64_hash_t)Z_LVAL_P(seed));
			return;
		} else {
			php_error_docref(NULL, E_DEPRECATED, "Passing a seed of a type other than int is deprecated because it is the same as setting the seed to 0");
		}
	}

	XXH64_reset(&ctx->s, 0);
}

PHP_HASH_API void PHP_XXH64Update(PHP_XXH64_CTX *ctx, const unsigned char *in, size_t len)
{
	XXH64_update(&ctx->s, in, len);
}

PHP_HASH_API void PHP_XXH64Final(unsigned char digest[8], PHP_XXH64_CTX *ctx)
{
	XXH64_canonicalFromHash((XXH64_canonical_t*)digest, XXH64_digest(&ctx->s));
}

PHP_HASH_API zend_result PHP_XXH64Copy(const php_hash_ops *ops, const PHP_XXH64_CTX *orig_context, PHP_XXH64_CTX *copy_context)
{
	copy_context->s = orig_context->s;
	return SUCCESS;
}

const php_hash_ops php_hash_xxh3_64_ops = {
	"xxh3",
	(php_hash_init_func_t) PHP_XXH3_64_Init,
	(php_hash_update_func_t) PHP_XXH3_64_Update,
	(php_hash_final_func_t) PHP_XXH3_64_Final,
	(php_hash_copy_func_t) PHP_XXH3_64_Copy,
	php_hash_serialize,
	php_hash_unserialize,
	NULL,
	8,
	8,
	sizeof(PHP_XXH3_64_CTX),
	0,
	64
};

typedef XXH_errorcode (*xxh3_reset_with_secret_func_t)(XXH3_state_t*, const void*, size_t);
typedef XXH_errorcode (*xxh3_reset_with_seed_func_t)(XXH3_state_t*, XXH64_hash_t);

static void _PHP_XXH3_Init(PHP_XXH3_64_CTX *ctx, HashTable *args,
		xxh3_reset_with_seed_func_t func_init_seed, xxh3_reset_with_secret_func_t func_init_secret, const char* algo_name)
{
	memset(&ctx->s, 0, sizeof ctx->s);

	if (args) {
		zval *_seed = zend_hash_str_find_deref(args, "seed", sizeof("seed") - 1);
		zval *_secret = zend_hash_str_find_deref(args, "secret", sizeof("secret") - 1);

		if (_seed && _secret) {
			zend_throw_error(NULL, "%s: Only one of seed or secret is to be passed for initialization", algo_name);
			return;
		}

		if (_seed && IS_LONG != Z_TYPE_P(_seed)) {
			php_error_docref(NULL, E_DEPRECATED, "Passing a seed of a type other than int is deprecated because it is ignored");
		}

		if (_seed && IS_LONG == Z_TYPE_P(_seed)) {
			/* This might be a bit too restrictive, but thinking that a seed might be set
				once and for all, it should be done a clean way. */
			func_init_seed(&ctx->s, (XXH64_hash_t)Z_LVAL_P(_seed));
			return;
		} else if (_secret) {
			if (IS_STRING != Z_TYPE_P(_secret)) {
				php_error_docref(NULL, E_DEPRECATED, "Passing a secret of a type other than string is deprecated because it implicitly converts to a string, potentially hiding bugs");
			}
			zend_string *secret_string = zval_try_get_string(_secret);
			if (UNEXPECTED(!secret_string)) {
				ZEND_ASSERT(EG(exception));
				return;
			}
			size_t len = ZSTR_LEN(secret_string);
			if (len < PHP_XXH3_SECRET_SIZE_MIN) {
				zend_string_release(secret_string);
				zend_throw_error(NULL, "%s: Secret length must be >= %u bytes, %zu bytes passed", algo_name, XXH3_SECRET_SIZE_MIN, len);
				return;
			}
			if (len > sizeof(ctx->secret)) {
				len = sizeof(ctx->secret);
				php_error_docref(NULL, E_WARNING, "%s: Secret content exceeding %zu bytes discarded", algo_name, sizeof(ctx->secret));
			}
			memcpy((unsigned char *)ctx->secret, ZSTR_VAL(secret_string), len);
			zend_string_release(secret_string);
			func_init_secret(&ctx->s, ctx->secret, len);
			return;
		}
	}

	func_init_seed(&ctx->s, 0);
}

PHP_HASH_API void PHP_XXH3_64_Init(PHP_XXH3_64_CTX *ctx, HashTable *args)
{
	_PHP_XXH3_Init(ctx, args, XXH3_64bits_reset_withSeed, XXH3_64bits_reset_withSecret, "xxh3");
}

PHP_HASH_API void PHP_XXH3_64_Update(PHP_XXH3_64_CTX *ctx, const unsigned char *in, size_t len)
{
	XXH3_64bits_update(&ctx->s, in, len);
}

PHP_HASH_API void PHP_XXH3_64_Final(unsigned char digest[8], PHP_XXH3_64_CTX *ctx)
{
	XXH64_canonicalFromHash((XXH64_canonical_t*)digest, XXH3_64bits_digest(&ctx->s));
}

PHP_HASH_API zend_result PHP_XXH3_64_Copy(const php_hash_ops *ops, const PHP_XXH3_64_CTX *orig_context, PHP_XXH3_64_CTX *copy_context)
{
	copy_context->s = orig_context->s;
	return SUCCESS;
}

static hash_spec_result php_hash_xxh64_unserialize(
		php_hashcontext_object *hash, zend_long magic, const zval *zv)
{
	PHP_XXH64_CTX *ctx = (PHP_XXH64_CTX *) hash->context;
	hash_spec_result r = HASH_SPEC_FAILURE;
	if (magic == PHP_HASH_SERIALIZE_MAGIC_SPEC
		&& (r = php_hash_unserialize_spec(hash, zv, PHP_XXH64_SPEC)) == HASH_SPEC_SUCCESS
		&& ctx->s.memsize < 32) {
		return HASH_SPEC_SUCCESS;
	}

    return r != HASH_SPEC_SUCCESS ? r : CONTEXT_VALIDATION_FAILURE;
}

const php_hash_ops php_hash_xxh3_128_ops = {
	"xxh128",
	(php_hash_init_func_t) PHP_XXH3_128_Init,
	(php_hash_update_func_t) PHP_XXH3_128_Update,
	(php_hash_final_func_t) PHP_XXH3_128_Final,
	(php_hash_copy_func_t) PHP_XXH3_128_Copy,
	php_hash_serialize,
	php_hash_unserialize,
	NULL,
	16,
	8,
	sizeof(PHP_XXH3_128_CTX),
	0,
	64
};

PHP_HASH_API void PHP_XXH3_128_Init(PHP_XXH3_128_CTX *ctx, HashTable *args)
{
	_PHP_XXH3_Init(ctx, args, XXH3_128bits_reset_withSeed, XXH3_128bits_reset_withSecret, "xxh128");
}

PHP_HASH_API void PHP_XXH3_128_Update(PHP_XXH3_128_CTX *ctx, const unsigned char *in, size_t len)
{
	XXH3_128bits_update(&ctx->s, in, len);
}

PHP_HASH_API void PHP_XXH3_128_Final(unsigned char digest[16], PHP_XXH3_128_CTX *ctx)
{
	XXH128_canonicalFromHash((XXH128_canonical_t*)digest, XXH3_128bits_digest(&ctx->s));
}

PHP_HASH_API zend_result PHP_XXH3_128_Copy(const php_hash_ops *ops, const PHP_XXH3_128_CTX *orig_context, PHP_XXH3_128_CTX *copy_context)
{
	copy_context->s = orig_context->s;
	return SUCCESS;
}

