/*
   +----------------------------------------------------------------------+
   | 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                                |
   +----------------------------------------------------------------------+
   | Authors: Wez Furlong <wez@thebrainroom.com>                          |
   +----------------------------------------------------------------------+
 */

#include "php.h"
#include "php_network.h"
#include "ext/standard/file.h"  /* For FG(stream_filters) */
#include <stddef.h>
#include <fcntl.h>

#include "php_streams_int.h"

/* Global filter hash, copied to FG(stream_filters) on registration of volatile filter */
static HashTable stream_filters_hash;

/* Should only be used during core initialization */
PHPAPI HashTable *php_get_stream_filters_hash_global(void)
{
	return &stream_filters_hash;
}

/* Normal hash selection/retrieval call */
PHPAPI HashTable *_php_get_stream_filters_hash(void)
{
	return (FG(stream_filters) ? FG(stream_filters) : &stream_filters_hash);
}

/* API for registering GLOBAL filters */
PHPAPI zend_result php_stream_filter_register_factory(const char *filterpattern, const php_stream_filter_factory *factory)
{
	zend_result ret;
	zend_string *str = zend_string_init_interned(filterpattern, strlen(filterpattern), true);
	ret = zend_hash_add_ptr(&stream_filters_hash, str, (void*)factory) ? SUCCESS : FAILURE;
	zend_string_release_ex(str, true);
	return ret;
}

PHPAPI zend_result php_stream_filter_unregister_factory(const char *filterpattern)
{
	return zend_hash_str_del(&stream_filters_hash, filterpattern, strlen(filterpattern));
}

/* API for registering VOLATILE wrappers */
PHPAPI zend_result php_stream_filter_register_factory_volatile(zend_string *filterpattern, const php_stream_filter_factory *factory)
{
	if (!FG(stream_filters)) {
		ALLOC_HASHTABLE(FG(stream_filters));
		zend_hash_init(FG(stream_filters), zend_hash_num_elements(&stream_filters_hash) + 1, NULL, NULL, 0);
		zend_hash_copy(FG(stream_filters), &stream_filters_hash, NULL);
	}

	return zend_hash_add_ptr(FG(stream_filters), filterpattern, (void*)factory) ? SUCCESS : FAILURE;
}

/* Buckets */

PHPAPI php_stream_bucket *php_stream_bucket_new(const php_stream *stream, char *buf, size_t buflen, uint8_t own_buf, uint8_t buf_persistent)
{
	bool is_persistent = php_stream_is_persistent(stream);
	php_stream_bucket *bucket;

	bucket = (php_stream_bucket*)pemalloc(sizeof(php_stream_bucket), is_persistent);
	bucket->next = bucket->prev = NULL;

	if (is_persistent && !buf_persistent) {
		/* all data in a persistent bucket must also be persistent */
		bucket->buf = pemalloc(buflen, true);
		memcpy(bucket->buf, buf, buflen);
		bucket->buflen = buflen;
		bucket->own_buf = true;
	} else {
		bucket->buf = buf;
		bucket->buflen = buflen;
		bucket->own_buf = own_buf;
	}
	bucket->is_persistent = is_persistent;
	bucket->refcount = 1;
	bucket->brigade = NULL;

	return bucket;
}

/* Given a bucket, returns a version of that bucket with a writeable buffer.
 * If the original bucket has a refcount of 1 and owns its buffer, then it
 * is returned unchanged.
 * Otherwise, a copy of the buffer is made.
 * In both cases, the original bucket is unlinked from its brigade.
 * If a copy is made, the original bucket is delref'd.
 * */
PHPAPI php_stream_bucket *php_stream_bucket_make_writeable(php_stream_bucket *bucket)
{
	php_stream_bucket *retval;

	php_stream_bucket_unlink(bucket);

	if (bucket->refcount == 1 && bucket->own_buf) {
		return bucket;
	}

	retval = (php_stream_bucket*)pemalloc(sizeof(php_stream_bucket), bucket->is_persistent);
	memcpy(retval, bucket, sizeof(*retval));

	retval->buf = pemalloc(retval->buflen, retval->is_persistent);
	memcpy(retval->buf, bucket->buf, retval->buflen);

	retval->refcount = 1;
	retval->own_buf = true;

	php_stream_bucket_delref(bucket);

	return retval;
}

PHPAPI int php_stream_bucket_split(php_stream_bucket *in, php_stream_bucket **left, php_stream_bucket **right, size_t length)
{
	*left = (php_stream_bucket*)pecalloc(1, sizeof(php_stream_bucket), in->is_persistent);
	*right = (php_stream_bucket*)pecalloc(1, sizeof(php_stream_bucket), in->is_persistent);

	(*left)->buf = pemalloc(length, in->is_persistent);
	(*left)->buflen = length;
	memcpy((*left)->buf, in->buf, length);
	(*left)->refcount = 1;
	(*left)->own_buf = true;
	(*left)->is_persistent = in->is_persistent;

	(*right)->buflen = in->buflen - length;
	(*right)->buf = pemalloc((*right)->buflen, in->is_persistent);
	memcpy((*right)->buf, in->buf + length, (*right)->buflen);
	(*right)->refcount = 1;
	(*right)->own_buf = true;
	(*right)->is_persistent = in->is_persistent;

	return SUCCESS;
}

PHPAPI void php_stream_bucket_delref(php_stream_bucket *bucket)
{
	if (--bucket->refcount == 0) {
		if (bucket->own_buf) {
			pefree(bucket->buf, bucket->is_persistent);
		}
		pefree(bucket, bucket->is_persistent);
	}
}

PHPAPI void php_stream_bucket_prepend(php_stream_bucket_brigade *brigade, php_stream_bucket *bucket)
{
	bucket->next = brigade->head;
	bucket->prev = NULL;

	if (brigade->head) {
		brigade->head->prev = bucket;
	} else {
		brigade->tail = bucket;
	}
	brigade->head = bucket;
	bucket->brigade = brigade;
}

PHPAPI void php_stream_bucket_append(php_stream_bucket_brigade *brigade, php_stream_bucket *bucket)
{
	/* TODO: this was added as a bad workaround for bug #35916 and should be removed in the future. */
	if (brigade->tail == bucket) {
		return;
	}

	bucket->prev = brigade->tail;
	bucket->next = NULL;

	if (brigade->tail) {
		brigade->tail->next = bucket;
	} else {
		brigade->head = bucket;
	}
	brigade->tail = bucket;
	bucket->brigade = brigade;
}

PHPAPI void php_stream_bucket_unlink(php_stream_bucket *bucket)
{
	if (bucket->prev) {
		bucket->prev->next = bucket->next;
	} else if (bucket->brigade) {
		bucket->brigade->head = bucket->next;
	}
	if (bucket->next) {
		bucket->next->prev = bucket->prev;
	} else if (bucket->brigade) {
		bucket->brigade->tail = bucket->prev;
	}
	bucket->brigade = NULL;
	bucket->next = bucket->prev = NULL;
}








/* We allow very simple pattern matching for filter factories:
 * if "convert.charset.utf-8/sjis" is requested, we search first for an exact
 * match. If that fails, we try "convert.charset.*", then "convert.*"
 * This means that we don't need to clog up the hashtable with a zillion
 * charsets (for example) but still be able to provide them all as filters */
PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, bool persistent)
{
	HashTable *filter_hash = (FG(stream_filters) ? FG(stream_filters) : &stream_filters_hash);
	const php_stream_filter_factory *factory = NULL;
	php_stream_filter *filter = NULL;
	size_t n;
	const char *period;

	n = strlen(filtername);

	if (NULL != (factory = zend_hash_str_find_ptr(filter_hash, filtername, n))) {
		filter = factory->create_filter(filtername, filterparams, persistent);
	} else if ((period = strrchr(filtername, '.'))) {
		/* try a wildcard */
		char *wildname;

		wildname = safe_emalloc(1, n, 3);
		memcpy(wildname, filtername, n+1);
		char *new_period = wildname + (period - filtername);
		while (new_period && !filter) {
			ZEND_ASSERT(new_period[0] == '.');
			new_period[1] = '*';
			new_period[2] = '\0';
			if (NULL != (factory = zend_hash_str_find_ptr(filter_hash, wildname, strlen(wildname)))) {
				filter = factory->create_filter(filtername, filterparams, persistent);
			}

			*new_period = '\0';
			new_period = strrchr(wildname, '.');
		}
		efree(wildname);
	}

	if (filter == NULL) {
		/* TODO: these need correct docrefs */
		if (factory == NULL)
			php_error_docref(NULL, E_WARNING, "Unable to locate filter \"%s\"", filtername);
		else
			php_error_docref(NULL, E_WARNING, "Unable to create or locate filter \"%s\"", filtername);
	}

	return filter;
}

PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops,
		void *abstract, bool persistent, php_stream_filter_seekable_t read_seekable,
		php_stream_filter_seekable_t write_seekable STREAMS_DC)
{
	php_stream_filter *filter;

	filter = (php_stream_filter*) pemalloc_rel_orig(sizeof(php_stream_filter), persistent);
	memset(filter, 0, sizeof(php_stream_filter));

	filter->fops = fops;
	filter->read_seekable = read_seekable;
	filter->write_seekable = write_seekable;
	Z_PTR(filter->abstract) = abstract;
	filter->is_persistent = persistent;

	return filter;
}

PHPAPI zend_result php_stream_filter_parse_write_seek_mode(
		const zval *filterparams,
		php_stream_filter_seekable_t *write_seekable)
{
	*write_seekable = PSFS_SEEKABLE_ALWAYS;

	if (filterparams == NULL) {
		return SUCCESS;
	}
	if (Z_TYPE_P(filterparams) != IS_ARRAY) {
		return SUCCESS;
	}

	const zval *write_seek_mode = zend_hash_str_find(Z_ARR_P(filterparams), ZEND_STRL("write_seek_mode"));
	if (write_seek_mode == NULL) {
		return SUCCESS;
	}

	zend_string *tmp_str;
	const zend_string *str = zval_get_tmp_string(write_seek_mode, &tmp_str);
	zend_result result = SUCCESS;

	if (zend_string_equals_literal(str, "preserve")) {
		*write_seekable = PSFS_SEEKABLE_ALWAYS;
	} else if (zend_string_equals_literal(str, "reset")) {
		*write_seekable = PSFS_SEEKABLE_START;
	} else if (zend_string_equals_literal(str, "strict")) {
		*write_seekable = PSFS_SEEKABLE_NEVER;
	} else {
		php_error_docref(NULL, E_WARNING,
			"\"write_seek_mode\" filter parameter must be one of "
			"\"preserve\", \"reset\", or \"strict\"");
		result = FAILURE;
	}

	zend_tmp_string_release(tmp_str);
	return result;
}

PHPAPI int php_stream_filter_get_chain_type(php_stream *stream, php_stream_filter *filter)
{
	return filter->chain == &stream->readfilters ?
			PHP_STREAM_FILTER_READ : PHP_STREAM_FILTER_WRITE;
}

PHPAPI void php_stream_filter_free(php_stream_filter *filter)
{
	if (filter->fops->dtor)
		filter->fops->dtor(filter);
	pefree(filter, filter->is_persistent);
}

PHPAPI void php_stream_filter_prepend_ex(php_stream_filter_chain *chain, php_stream_filter *filter)
{
	filter->next = chain->head;
	filter->prev = NULL;

	if (chain->head) {
		chain->head->prev = filter;
	} else {
		chain->tail = filter;
	}
	chain->head = filter;
	filter->chain = chain;
}

PHPAPI void _php_stream_filter_prepend(php_stream_filter_chain *chain, php_stream_filter *filter)
{
	php_stream_filter_prepend_ex(chain, filter);
}

PHPAPI zend_result php_stream_filter_append_ex(php_stream_filter_chain *chain, php_stream_filter *filter)
{
	php_stream *stream = chain->stream;

	filter->prev = chain->tail;
	filter->next = NULL;
	if (chain->tail) {
		chain->tail->next = filter;
	} else {
		chain->head = filter;
	}
	chain->tail = filter;
	filter->chain = chain;

	if (&(stream->readfilters) == chain && (stream->writepos - stream->readpos) > 0) {
		/* Let's going ahead and wind anything in the buffer through this filter */
		php_stream_bucket_brigade brig_in = { NULL, NULL }, brig_out = { NULL, NULL };
		php_stream_bucket_brigade *brig_inp = &brig_in, *brig_outp = &brig_out;
		php_stream_filter_status_t status;
		php_stream_bucket *bucket;
		size_t consumed = 0;

		bucket = php_stream_bucket_new(stream, (char*) stream->readbuf + stream->readpos, stream->writepos - stream->readpos, 0, 0);
		php_stream_bucket_append(brig_inp, bucket);
		status = filter->fops->filter(stream, filter, brig_inp, brig_outp, &consumed, PSFS_FLAG_NORMAL);

		if (stream->readpos + consumed > (uint32_t)stream->writepos) {
			/* No behaving filter should cause this. */
			status = PSFS_ERR_FATAL;
		}

		switch (status) {
			case PSFS_ERR_FATAL:
				while (brig_in.head) {
					bucket = brig_in.head;
					php_stream_bucket_unlink(bucket);
					php_stream_bucket_delref(bucket);
				}
				while (brig_out.head) {
					bucket = brig_out.head;
					php_stream_bucket_unlink(bucket);
					php_stream_bucket_delref(bucket);
				}
				php_stream_warn(stream, FilterFailed,
						"Filter failed to process pre-buffered data");
				return FAILURE;
			case PSFS_FEED_ME:
				/* We don't actually need data yet,
				   leave this filter in a feed me state until data is needed.
				   Reset stream's internal read buffer since the filter is "holding" it. */
				stream->readpos = 0;
				stream->writepos = 0;

				/* Filter could have added buckets anyway, but signalled that it did not return any. Discard them. */
				while (brig_out.head) {
					bucket = brig_out.head;
					php_stream_bucket_unlink(bucket);
					php_stream_bucket_delref(bucket);
				}
				break;
			case PSFS_PASS_ON:
				/* If any data is consumed, we cannot rely upon the existing read buffer,
				   as the filtered data must replace the existing data, so invalidate the cache */
				stream->writepos = 0;
				stream->readpos = 0;

				while (brig_outp->head) {
					bucket = brig_outp->head;
					/* Grow buffer to hold this bucket if need be.
					   TODO: See warning in main/stream/streams.c::php_stream_fill_read_buffer */
					if (stream->readbuflen - stream->writepos < bucket->buflen) {
						stream->readbuflen += bucket->buflen;
						stream->readbuf = perealloc(stream->readbuf, stream->readbuflen, stream->is_persistent);
					}
					memcpy(stream->readbuf + stream->writepos, bucket->buf, bucket->buflen);
					stream->writepos += bucket->buflen;

					php_stream_bucket_unlink(bucket);
					php_stream_bucket_delref(bucket);
				}
				break;
		}
	}

	return SUCCESS;
}

PHPAPI void _php_stream_filter_append(php_stream_filter_chain *chain, php_stream_filter *filter)
{
	if (php_stream_filter_append_ex(chain, filter) != SUCCESS) {
		if (chain->head == filter) {
			chain->head = NULL;
			chain->tail = NULL;
		} else {
			filter->prev->next = NULL;
			chain->tail = filter->prev;
		}
	}
}

PHPAPI zend_result _php_stream_filter_flush(php_stream_filter *filter, bool finish)
{
	php_stream_bucket_brigade brig_a = { NULL, NULL }, brig_b = { NULL, NULL }, *inp = &brig_a, *outp = &brig_b, *brig_temp;
	php_stream_bucket *bucket;
	php_stream_filter_chain *chain;
	php_stream_filter *current;
	php_stream *stream;
	size_t flushed_size = 0;
	int flags = (finish ? PSFS_FLAG_FLUSH_CLOSE : PSFS_FLAG_FLUSH_INC);

	if (!filter->chain || !filter->chain->stream) {
		/* Filter is not attached to a chain, or chain is somehow not part of a stream */
		return FAILURE;
	}

	chain = filter->chain;
	stream = chain->stream;

	for(current = filter; current; current = current->next) {
		php_stream_filter_status_t status;

		status = current->fops->filter(stream, current, inp, outp, NULL, flags);
		if (status == PSFS_FEED_ME) {
			/* We've flushed the data far enough */
			return SUCCESS;
		}
		if (status == PSFS_ERR_FATAL) {
			return FAILURE;
		}
		/* Otherwise we have data available to PASS_ON
			Swap the brigades and continue */
		brig_temp = inp;
		inp = outp;
		outp = brig_temp;
		outp->head = NULL;
		outp->tail = NULL;

		flags = PSFS_FLAG_NORMAL;
	}

	/* Last filter returned data via PSFS_PASS_ON
		Do something with it */

	for(bucket = inp->head; bucket; bucket = bucket->next) {
		flushed_size += bucket->buflen;
	}

	if (flushed_size == 0) {
		/* Unlikely, but possible */
		return SUCCESS;
	}

	if (chain == &(stream->readfilters)) {
		/* Dump any newly flushed data to the read buffer */
		if (stream->readpos > 0) {
			/* Back the buffer up */
			memcpy(stream->readbuf, stream->readbuf + stream->readpos, stream->writepos - stream->readpos);
			stream->readpos = 0;
			stream->writepos -= stream->readpos;
		}
		if (flushed_size > (stream->readbuflen - stream->writepos)) {
			/* Grow the buffer */
			stream->readbuf = perealloc(stream->readbuf, stream->writepos + flushed_size + stream->chunk_size, stream->is_persistent);
		}
		while ((bucket = inp->head)) {
			memcpy(stream->readbuf + stream->writepos, bucket->buf, bucket->buflen);
			stream->writepos += bucket->buflen;
			php_stream_bucket_unlink(bucket);
			php_stream_bucket_delref(bucket);
		}
	} else if (chain == &(stream->writefilters)) {
		/* Send flushed data to the stream */
		while ((bucket = inp->head)) {
			ssize_t count = stream->ops->write(stream, bucket->buf, bucket->buflen);
			if (count > 0) {
				stream->position += count;
			}
			php_stream_bucket_unlink(bucket);
			php_stream_bucket_delref(bucket);
		}
	}

	return SUCCESS;
}

PHPAPI php_stream_filter *php_stream_filter_remove(php_stream_filter *filter, bool call_dtor)
{
	if (filter->prev) {
		filter->prev->next = filter->next;
	} else {
		filter->chain->head = filter->next;
	}
	if (filter->next) {
		filter->next->prev = filter->prev;
	} else {
		filter->chain->tail = filter->prev;
	}

	if (filter->res) {
		zend_list_delete(filter->res);
	}

	if (call_dtor) {
		php_stream_filter_free(filter);
		return NULL;
	}
	return filter;
}
