/*
   +----------------------------------------------------------------------+
   | 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>                          |
   |          Sara Golemon <pollita@php.net>                              |
   +----------------------------------------------------------------------+
*/

#include "php.h"
#include "php_globals.h"
#include "ext/standard/file.h"
#include "ext/standard/flock_compat.h"
#ifdef HAVE_SYS_FILE_H
#include <sys/file.h>
#endif
#include <stddef.h>

#ifdef HAVE_UTIME
# ifdef PHP_WIN32
#  include <sys/utime.h>
# else
#  include <utime.h>
# endif
#endif
#include "userspace_arginfo.h"

static int le_protocols;

struct php_user_stream_wrapper {
	php_stream_wrapper wrapper;
	zend_class_entry *ce;
	zend_resource *resource;
};

static php_stream *user_wrapper_opener(php_stream_wrapper *wrapper, const char *filename, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC);
static int user_wrapper_close(php_stream_wrapper *wrapper, php_stream *stream);
static int user_wrapper_stat_url(php_stream_wrapper *wrapper, const char *url, int flags, php_stream_statbuf *ssb, php_stream_context *context);
static int user_wrapper_unlink(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context);
static int user_wrapper_rename(php_stream_wrapper *wrapper, const char *url_from, const char *url_to, int options, php_stream_context *context);
static int user_wrapper_mkdir(php_stream_wrapper *wrapper, const char *url, int mode, int options, php_stream_context *context);
static int user_wrapper_rmdir(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context);
static int user_wrapper_metadata(php_stream_wrapper *wrapper, const char *url, int option, void *value, php_stream_context *context);
static php_stream *user_wrapper_opendir(php_stream_wrapper *wrapper, const char *filename, const char *mode,
		int options, zend_string **opened_path, php_stream_context *context STREAMS_DC);

static const php_stream_wrapper_ops user_stream_wops = {
	user_wrapper_opener,
	user_wrapper_close,
	NULL, /* stat - the streams themselves know how */
	user_wrapper_stat_url,
	user_wrapper_opendir,
	"user-space",
	user_wrapper_unlink,
	user_wrapper_rename,
	user_wrapper_mkdir,
	user_wrapper_rmdir,
	user_wrapper_metadata
};


static void stream_wrapper_dtor(zend_resource *rsrc)
{
	struct php_user_stream_wrapper * uwrap = (struct php_user_stream_wrapper*)rsrc->ptr;

	efree(uwrap);
}


PHP_MINIT_FUNCTION(user_streams)
{
	le_protocols = zend_register_list_destructors_ex(stream_wrapper_dtor, NULL, "stream factory", 0);
	if (le_protocols == FAILURE)
		return FAILURE;

	register_userspace_symbols(module_number);

	return SUCCESS;
}

struct _php_userstream_data {
	struct php_user_stream_wrapper * wrapper;
	zval object;
};
typedef struct _php_userstream_data php_userstream_data_t;

/* names of methods */
#define USERSTREAM_OPEN		"stream_open"
#define USERSTREAM_CLOSE	"stream_close"
#define USERSTREAM_READ		"stream_read"
#define USERSTREAM_WRITE	"stream_write"
#define USERSTREAM_FLUSH	"stream_flush"
#define USERSTREAM_SEEK		"stream_seek"
#define USERSTREAM_TELL		"stream_tell"
#define USERSTREAM_EOF		"stream_eof"
#define USERSTREAM_STAT		"stream_stat"
#define USERSTREAM_STATURL	"url_stat"
#define USERSTREAM_UNLINK	"unlink"
#define USERSTREAM_RENAME	"rename"
#define USERSTREAM_MKDIR	"mkdir"
#define USERSTREAM_RMDIR	"rmdir"
#define USERSTREAM_DIR_OPEN		"dir_opendir"
#define USERSTREAM_DIR_READ		"dir_readdir"
#define USERSTREAM_DIR_REWIND	"dir_rewinddir"
#define USERSTREAM_DIR_CLOSE	"dir_closedir"
#define USERSTREAM_LOCK     "stream_lock"
#define USERSTREAM_CAST		"stream_cast"
#define USERSTREAM_SET_OPTION	"stream_set_option"
#define USERSTREAM_TRUNCATE	"stream_truncate"
#define USERSTREAM_METADATA	"stream_metadata"

/* {{{ class should have methods like these:

	function stream_open($path, $mode, $options, &$opened_path)
	{
	  	return true/false;
	}

	function stream_read($count)
	{
	   	return false on error;
		else return string;
	}

	function stream_write($data)
	{
	   	return false on error;
		else return count written;
	}

	function stream_close()
	{
	}

	function stream_flush()
	{
		return true/false;
	}

	function stream_seek($offset, $whence)
	{
		return true/false;
	}

	function stream_tell()
	{
		return (int)$position;
	}

	function stream_eof()
	{
		return true/false;
	}

	function stream_stat()
	{
		return array( just like that returned by fstat() );
	}

	function stream_cast($castas)
	{
		if ($castas == STREAM_CAST_FOR_SELECT) {
			return $this->underlying_stream;
		}
		return false;
	}

	function stream_set_option($option, $arg1, $arg2)
	{
		switch($option) {
		case STREAM_OPTION_BLOCKING:
			$blocking = $arg1;
			...
		case STREAM_OPTION_READ_TIMEOUT:
			$sec = $arg1;
			$usec = $arg2;
			...
		case STREAM_OPTION_WRITE_BUFFER:
			$mode = $arg1;
			$size = $arg2;
			...
		default:
			return false;
		}
	}

	function url_stat(string $url, int $flags)
	{
		return array( just like that returned by stat() );
	}

	function unlink(string $url)
	{
		return true / false;
	}

	function rename(string $from, string $to)
	{
		return true / false;
	}

	function mkdir($dir, $mode, $options)
	{
		return true / false;
	}

	function rmdir($dir, $options)
	{
		return true / false;
	}

	function dir_opendir(string $url, int $options)
	{
		return true / false;
	}

	function dir_readdir()
	{
		return string next filename in dir ;
	}

	function dir_closedir()
	{
		release dir related resources;
	}

	function dir_rewinddir()
	{
		reset to start of dir list;
	}

	function stream_lock($operation)
	{
		return true / false;
	}

 	function stream_truncate($new_size)
	{
		return true / false;
	}

	}}} **/

static void user_stream_create_object(struct php_user_stream_wrapper *uwrap, php_stream_context *context, zval *object)
{
	ZEND_ASSERT((uwrap->ce->ce_flags & ZEND_ACC_UNINSTANTIABLE) == 0);

	/* create an instance of our class */
	if (object_init_ex(object, uwrap->ce) == FAILURE) {
		ZVAL_UNDEF(object);
		return;
	}

	if (context) {
		GC_ADDREF(context->res);
		add_property_resource(object, "context", context->res);
	} else {
		add_property_null(object, "context");
	}

	if (EG(exception) != NULL) {
		zval_ptr_dtor(object);
		ZVAL_UNDEF(object);
		return;
	}

	if (uwrap->ce->constructor) {
		zend_call_known_instance_method_with_0_params(
			uwrap->ce->constructor, Z_OBJ_P(object), NULL);
	}
}

static php_stream *user_wrapper_opener(php_stream_wrapper *wrapper, const char *filename, const char *mode,
									   int options, zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	php_userstream_data_t *us;
	zval zretval;
	zval args[4];
	php_stream *stream = NULL;
	bool old_in_user_include;

	/* Try to catch bad usage without preventing flexibility */
	if (FG(user_stream_current_filename) != NULL && strcmp(filename, FG(user_stream_current_filename)) == 0) {
		php_stream_wrapper_log_warn(wrapper, context, options,
				RecursionDetected, "infinite recursion prevented");
		return NULL;
	}
	FG(user_stream_current_filename) = filename;

	/* if the user stream was registered as local and we are in include context,
		we add allow_url_include restrictions to allow_url_fopen ones */
	/* we need only is_url == 0 here since if is_url == 1 and remote wrappers
		were restricted we wouldn't get here */
	old_in_user_include = PG(in_user_include);
	if(uwrap->wrapper.is_url == 0 &&
		(options & STREAM_OPEN_FOR_INCLUDE) &&
		!PG(allow_url_include)) {
		PG(in_user_include) = 1;
	}

	us = emalloc(sizeof(*us));
	us->wrapper = uwrap;
	/* zend_call_method_if_exists() may unregister the stream wrapper. Hold on to it. */
	GC_ADDREF(us->wrapper->resource);

	user_stream_create_object(uwrap, context, &us->object);
	if (Z_ISUNDEF(us->object)) {
		goto end;
	}

	/* call its stream_open method - set up params first */
	ZVAL_STRING(&args[0], filename);
	ZVAL_STRING(&args[1], mode);
	ZVAL_LONG(&args[2], options);
	ZVAL_NEW_REF(&args[3], &EG(uninitialized_zval));

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_OPEN, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &zretval, 4, args);
	zend_string_release_ex(func_name, false);

	/* Keep arg3 alive if it has assigned the reference */
	zval_ptr_dtor(&args[1]);
	zval_ptr_dtor(&args[0]);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_wrapper_log_warn(wrapper, context, options,NotImplemented,
				"\"%s::" USERSTREAM_OPEN "\" is not implemented", ZSTR_VAL(us->wrapper->ce->name));
		zval_ptr_dtor(&args[3]);
		goto end;
	}
	/* Exception occurred */
	if (UNEXPECTED(Z_ISUNDEF(zretval))) {
		zval_ptr_dtor(&args[3]);
		goto end;
	}
	if (zend_is_true(&zretval)) {
		/* the stream is now open! */
		stream = php_stream_alloc_rel(&php_stream_userspace_ops, us, 0, mode);

		/* if the opened path is set, copy it out */
		if (Z_ISREF(args[3]) && Z_TYPE_P(Z_REFVAL(args[3])) == IS_STRING && opened_path) {
			*opened_path = zend_string_copy(Z_STR_P(Z_REFVAL(args[3])));
		}
		// TODO Warn when assigning a non string value to the reference?

		/* set wrapper data to be a reference to our object */
		ZVAL_COPY(&stream->wrapperdata, &us->object);
	} else {
		php_stream_wrapper_log_warn(wrapper, context, options,
				UserspaceCallFailed,
				"\"%s::" USERSTREAM_OPEN "\" call failed", ZSTR_VAL(us->wrapper->ce->name));
	}

	zval_ptr_dtor(&zretval);
	zval_ptr_dtor(&args[3]);

end:
	FG(user_stream_current_filename) = NULL;
	PG(in_user_include) = old_in_user_include;
	if (stream == NULL) {
		zval_ptr_dtor(&us->object);
		zend_list_delete(us->wrapper->resource);
		efree(us);
	}
	return stream;
}

static int user_wrapper_close(php_stream_wrapper *wrapper, php_stream *stream)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	zend_list_delete(uwrap->resource);
	// FIXME: Unused?
	return 0;
}

static php_stream *user_wrapper_opendir(php_stream_wrapper *wrapper, const char *filename, const char *mode,
		int options, zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	php_userstream_data_t *us;
	zval zretval;
	zval args[2];
	php_stream *stream = NULL;

	/* Try to catch bad usage without preventing flexibility */
	if (FG(user_stream_current_filename) != NULL && strcmp(filename, FG(user_stream_current_filename)) == 0) {
		php_stream_wrapper_log_warn(wrapper, context, options,
				RecursionDetected, "infinite recursion prevented");
		return NULL;
	}
	FG(user_stream_current_filename) = filename;

	us = emalloc(sizeof(*us));
	us->wrapper = uwrap;
	/* zend_call_method_if_exists() may unregister the stream wrapper. Hold on to it. */
	GC_ADDREF(us->wrapper->resource);

	user_stream_create_object(uwrap, context, &us->object);
	if (Z_TYPE(us->object) == IS_UNDEF) {
		goto end;
	}

	/* call its dir_open method - set up params first */
	ZVAL_STRING(&args[0], filename);
	ZVAL_LONG(&args[1], options);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_DIR_OPEN, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &zretval, 2, args);
	zend_string_release_ex(func_name, false);
	zval_ptr_dtor(&args[0]);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_wrapper_log_warn(wrapper, context, options, NotImplemented,
				"\"%s::" USERSTREAM_DIR_OPEN "\" is not implemented",
				ZSTR_VAL(us->wrapper->ce->name));
		goto end;
	}
	/* Exception occurred in call */
	if (UNEXPECTED(Z_ISUNDEF(zretval))) {
		goto end;
	}

	if (zend_is_true(&zretval)) {
		/* the stream is now open! */
		stream = php_stream_alloc_rel(&php_stream_userspace_dir_ops, us, 0, mode);

		/* set wrapper data to be a reference to our object */
		ZVAL_COPY(&stream->wrapperdata, &us->object);
	} else {
		php_stream_wrapper_log_warn(wrapper, context, options,
				UserspaceCallFailed,
				"\"%s::" USERSTREAM_DIR_OPEN "\" call failed", ZSTR_VAL(us->wrapper->ce->name));
	}
	zval_ptr_dtor(&zretval);

end:
	FG(user_stream_current_filename) = NULL;
	if (stream == NULL) {
		zval_ptr_dtor(&us->object);
		zend_list_delete(us->wrapper->resource);
		efree(us);
	}
	return stream;
}


/* {{{ Registers a custom URL protocol handler class */
PHP_FUNCTION(stream_wrapper_register)
{
	zend_string *protocol;
	struct php_user_stream_wrapper *uwrap;
	zend_class_entry *ce = NULL;
	zend_resource *rsrc;
	zend_long flags = 0;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "SC|l", &protocol, &ce, &flags) == FAILURE) {
		RETURN_THROWS();
	}

	if (UNEXPECTED(ce->ce_flags & ZEND_ACC_UNINSTANTIABLE)) {
		zend_argument_value_error(2, "must be a concrete class");
		RETURN_THROWS();
	}

	uwrap = (struct php_user_stream_wrapper *)ecalloc(1, sizeof(*uwrap));
	uwrap->ce = ce;
	uwrap->wrapper.wops = &user_stream_wops;
	uwrap->wrapper.abstract = uwrap;
	uwrap->wrapper.is_url = ((flags & PHP_STREAM_IS_URL) != 0);

	rsrc = zend_register_resource(uwrap, le_protocols);

	if (php_register_url_stream_wrapper_volatile(protocol, &uwrap->wrapper) == SUCCESS) {
		uwrap->resource = rsrc;
		RETURN_TRUE;
	}

	/* We failed.  But why? */
	if (zend_hash_exists(php_stream_get_url_stream_wrappers_hash(), protocol)) {
		php_stream_wrapper_warn(&uwrap->wrapper, NULL, REPORT_ERRORS,
				WrapperRegistrationFailed,
				"Protocol %s:// is already defined.", ZSTR_VAL(protocol));
	} else {
		/* Hash doesn't exist so it must have been an invalid protocol scheme */
		php_stream_wrapper_warn(&uwrap->wrapper, NULL, REPORT_ERRORS,
				WrapperRegistrationFailed,
				"Invalid protocol scheme specified. Unable to register wrapper class %s to %s://",
				ZSTR_VAL(uwrap->ce->name), ZSTR_VAL(protocol));
	}

	zend_list_delete(rsrc);
	RETURN_FALSE;
}
/* }}} */

/* {{{ Unregister a wrapper for the life of the current request. */
PHP_FUNCTION(stream_wrapper_unregister)
{
	zend_string *protocol;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &protocol) == FAILURE) {
		RETURN_THROWS();
	}

	php_stream_wrapper *wrapper = zend_hash_find_ptr(php_stream_get_url_stream_wrappers_hash(), protocol);
	if (php_unregister_url_stream_wrapper_volatile(protocol) == FAILURE) {
		/* We failed */
		php_stream_wrapper_warn(wrapper, NULL, REPORT_ERRORS,
				WrapperUnregistrationFailed,
				"Unable to unregister protocol %s://", ZSTR_VAL(protocol));
		RETURN_FALSE;
	}

	ZEND_ASSERT(wrapper != NULL);
	if (wrapper->wops == &user_stream_wops) {
		struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper *)wrapper;
		// uwrap will be released by resource destructor
		zend_list_delete(uwrap->resource);
	}

	RETURN_TRUE;
}
/* }}} */

/* {{{ Restore the original protocol handler, overriding if necessary */
PHP_FUNCTION(stream_wrapper_restore)
{
	zend_string *protocol;
	php_stream_wrapper *wrapper;
	HashTable *global_wrapper_hash, *wrapper_hash;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &protocol) == FAILURE) {
		RETURN_THROWS();
	}

	global_wrapper_hash = php_stream_get_url_stream_wrappers_hash_global();
	if ((wrapper = zend_hash_find_ptr(global_wrapper_hash, protocol)) == NULL) {
		php_stream_wrapper_warn_name(user_stream_wops.label, NULL, REPORT_ERRORS,
				WrapperNotFound,
				"%s:// never existed, nothing to restore", ZSTR_VAL(protocol));
		RETURN_FALSE;
	}

	wrapper_hash = php_stream_get_url_stream_wrappers_hash();
	if (wrapper_hash == global_wrapper_hash || zend_hash_find_ptr(wrapper_hash, protocol) == wrapper) {
		php_stream_wrapper_notice(wrapper, NULL, REPORT_ERRORS,
				WrapperRestorationFailed,
				"%s:// was never changed, nothing to restore", ZSTR_VAL(protocol));
		RETURN_TRUE;
	}

	/* A failure here could be okay given that the protocol might have been merely unregistered */
	php_unregister_url_stream_wrapper_volatile(protocol);

	if (php_register_url_stream_wrapper_volatile(protocol, wrapper) == FAILURE) {
		php_stream_wrapper_warn(wrapper, NULL, REPORT_ERRORS,
			WrapperRestorationFailed,
			"Unable to restore original %s:// wrapper", ZSTR_VAL(protocol));
		RETURN_FALSE;
	}

	RETURN_TRUE;
}
/* }}} */

static ssize_t php_userstreamop_write(php_stream *stream, const char *buf, size_t count)
{
	zval retval;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;
	zval args[1];
	ssize_t didwrite;

	assert(us != NULL);

	ZVAL_STRINGL(&args[0], (char*)buf, count);

	uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_WRITE, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
	zend_string_release_ex(func_name, false);
	zval_ptr_dtor(&args[0]);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_WRITE " is not implemented!", ZSTR_VAL(us->wrapper->ce->name));
	}

	stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= orig_no_fclose;

	/* Exception occurred */
	if (Z_ISUNDEF(retval)) {
		return -1;
	}

	if (Z_TYPE(retval) == IS_FALSE) {
		didwrite = -1;
	} else {
		convert_to_long(&retval);
		didwrite = Z_LVAL(retval);
	}

	/* don't allow strange buffer overruns due to bogus return */
	if (didwrite > 0 && didwrite > count) {
		php_stream_warn_nt(stream, UserspaceInvalidReturn,
				"%s::" USERSTREAM_WRITE " wrote " ZEND_LONG_FMT " bytes more data than requested ("
						ZEND_LONG_FMT " written, " ZEND_LONG_FMT " max)",
				ZSTR_VAL(us->wrapper->ce->name),
				(zend_long)(didwrite - count), (zend_long)didwrite, (zend_long)count);
		didwrite = count;
	}

	return didwrite;
}

static ssize_t php_userstreamop_read(php_stream *stream, char *buf, size_t count)
{
	zval retval;
	zval args[1];
	size_t didread = 0;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;

	assert(us != NULL);

	uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;

	ZVAL_LONG(&args[0], count);
	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_READ, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(Z_ISUNDEF(retval))) {
		goto err;
	}

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_READ " is not implemented!", ZSTR_VAL(us->wrapper->ce->name));
		goto err;
	}

	if (Z_TYPE(retval) == IS_FALSE) {
		goto err;
	}

	if (!try_convert_to_string(&retval)) {
		zval_ptr_dtor(&retval);
		goto err;
	}

	didread = Z_STRLEN(retval);
	if (didread > 0) {
		if (didread > count) {
			php_stream_warn_nt(stream, UserspaceInvalidReturn,
					"%s::" USERSTREAM_READ " - read " ZEND_LONG_FMT
							" bytes more data than requested (" ZEND_LONG_FMT " read, "
							ZEND_LONG_FMT " max) - excess data will be lost",
					ZSTR_VAL(us->wrapper->ce->name), (zend_long)(didread - count),
					(zend_long)didread, (zend_long)count);
			didread = count;
		}
		memcpy(buf, Z_STRVAL(retval), didread);
	}

	zval_ptr_dtor(&retval);
	ZVAL_UNDEF(&retval);

	/* since the user stream has no way of setting the eof flag directly, we need to ask it if we hit eof */

	func_name = ZSTR_INIT_LITERAL(USERSTREAM_EOF, false);
	call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_EOF " is not implemented! Assuming EOF",
				ZSTR_VAL(us->wrapper->ce->name));
		stream->eof = 1;
		goto err;
	}
	if (UNEXPECTED(Z_ISUNDEF(retval))) {
		stream->eof = 1;
		goto err;
	}

	if (zend_is_true(&retval)) {
		stream->eof = 1;
	}
	zval_ptr_dtor(&retval);

	stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= orig_no_fclose;

	return didread;

err:
	stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= orig_no_fclose;
	return -1;
}

static int php_userstreamop_close(php_stream *stream, int close_handle)
{
	zval retval;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;

	assert(us != NULL);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_CLOSE, false);
	zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	zval_ptr_dtor(&retval);

	zval_ptr_dtor(&us->object);
	ZVAL_UNDEF(&us->object);

	efree(us);

	return 0;
}

static int php_userstreamop_flush(php_stream *stream)
{
	zval retval;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;

	assert(us != NULL);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_FLUSH, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	int ret = call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF && zend_is_true(&retval) ? 0 : -1;

	zval_ptr_dtor(&retval);

	return ret;
}

static int php_userstreamop_seek(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffs)
{
	zval retval;
	int ret;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;
	zval args[2];

	assert(us != NULL);

	ZVAL_LONG(&args[0], offset);
	ZVAL_LONG(&args[1], whence);

	uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_SEEK, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 2, args);
	zend_string_release_ex(func_name, false);

	if (call_result == FAILURE) {
		/* stream_seek is not implemented, so disable seeks for this stream */
		stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
		/* there should be no retval to clean up */

		zval_ptr_dtor(&retval);

		ret = -1;
		goto out;
	} else if (call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF && zend_is_true(&retval)) {
		ret = 0;
	} else {
		ret = -1;
	}

	zval_ptr_dtor(&retval);
	ZVAL_UNDEF(&retval);

	if (ret) {
		goto out;
	}

	/* now determine where we are */
	func_name = ZSTR_INIT_LITERAL(USERSTREAM_TELL, false);
	call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	if (call_result == SUCCESS && Z_TYPE(retval) == IS_LONG) {
		*newoffs = Z_LVAL(retval);
		ret = 0;
	} else if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_TELL " is not implemented!", ZSTR_VAL(us->wrapper->ce->name));
		ret = -1;
	} else {
		ret = -1;
	}

	zval_ptr_dtor(&retval);

out:
	stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= orig_no_fclose;

	return ret;
}

/* parse the return value from one of the stat functions and store the
 * relevant fields into the statbuf provided */
static void statbuf_from_array(const HashTable *array, php_stream_statbuf *ssb)
{
	zval *elem;

#define STAT_PROP_ENTRY_EX(name, name2)                        \
	if (NULL != (elem = zend_hash_str_find(array, #name, sizeof(#name)-1))) {     \
		ssb->sb.st_##name2 = zval_get_long(elem);                                                      \
	}

#define STAT_PROP_ENTRY(name) STAT_PROP_ENTRY_EX(name,name)

	memset(ssb, 0, sizeof(php_stream_statbuf));
	STAT_PROP_ENTRY(dev);
	STAT_PROP_ENTRY(ino);
	STAT_PROP_ENTRY(mode);
	STAT_PROP_ENTRY(nlink);
	STAT_PROP_ENTRY(uid);
	STAT_PROP_ENTRY(gid);
#ifdef HAVE_STRUCT_STAT_ST_RDEV
	STAT_PROP_ENTRY(rdev);
#endif
	STAT_PROP_ENTRY(size);
	STAT_PROP_ENTRY(atime);
	STAT_PROP_ENTRY(mtime);
	STAT_PROP_ENTRY(ctime);
#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
	STAT_PROP_ENTRY(blksize);
#endif
#ifdef HAVE_STRUCT_STAT_ST_BLOCKS
	STAT_PROP_ENTRY(blocks);
#endif

#undef STAT_PROP_ENTRY
#undef STAT_PROP_ENTRY_EX
}

static int php_userstreamop_stat(php_stream *stream, php_stream_statbuf *ssb)
{
	zval retval;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;
	int ret = -1;

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_STAT, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_STAT " is not implemented!", ZSTR_VAL(us->wrapper->ce->name));
		return -1;
	}
	if (UNEXPECTED(Z_ISUNDEF(retval))) {
		return -1;
	}

	if (EXPECTED(Z_TYPE(retval) == IS_ARRAY)) {
		statbuf_from_array(Z_ARR(retval), ssb);
		ret = 0;
	}
	// TODO: Warning on incorrect return type?

	zval_ptr_dtor(&retval);

	return ret;
}

static int user_stream_set_check_liveliness(php_stream *stream, const php_userstream_data_t *us)
{
	zval retval;

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_EOF, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_EOF " is not implemented! Assuming EOF",
				ZSTR_VAL(us->wrapper->ce->name));
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
	if (UNEXPECTED(Z_ISUNDEF(retval))) {
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
	if (EXPECTED(Z_TYPE(retval) == IS_FALSE || Z_TYPE(retval) == IS_TRUE)) {
		return Z_TYPE(retval) == IS_TRUE ? PHP_STREAM_OPTION_RETURN_ERR : PHP_STREAM_OPTION_RETURN_OK;
	} else {
		php_stream_warn(stream, UserspaceInvalidReturn,
				"%s::" USERSTREAM_EOF " value must be of type bool, %s given",
				ZSTR_VAL(us->wrapper->ce->name), zend_zval_value_name(&retval));
		zval_ptr_dtor(&retval);
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
}

static int user_stream_set_locking(php_stream *stream, const php_userstream_data_t *us, int value)
{
	zval retval;
	zval zlock;
	zend_long lock = 0;

	if (value & LOCK_NB) {
		lock |= PHP_LOCK_NB;
	}
	switch (value & ~LOCK_NB) {
		case LOCK_SH:
			lock |= PHP_LOCK_SH;
			break;
		case LOCK_EX:
			lock |= PHP_LOCK_EX;
			break;
		case LOCK_UN:
			lock |= PHP_LOCK_UN;
			break;
		default:
			// TODO: Warn on invalid option value?
			;
	}
	ZVAL_LONG(&zlock, lock);

	/* TODO wouldblock */
	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_LOCK, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, &zlock);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(call_result == FAILURE)) {
		if (value == 0) {
			/* lock support test (TODO: more check) */
			return PHP_STREAM_OPTION_RETURN_OK;
		}
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_LOCK " is not implemented!", ZSTR_VAL(us->wrapper->ce->name));
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
	if (UNEXPECTED(Z_ISUNDEF(retval))) {
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
	if (EXPECTED(Z_TYPE(retval) == IS_FALSE || Z_TYPE(retval) == IS_TRUE)) {
		// This is somewhat confusing and relies on magic numbers.
		return Z_TYPE(retval) == IS_FALSE;
	}
	// TODO: ext/standard/tests/file/userstreams_004.phpt returns null implicitly for function
	// Should this warn or not? And should this be considered an error?
	//php_stream_warn(stream, UserspaceInvalidReturn,
	//		"%s::" USERSTREAM_LOCK " value must be of type bool, %s given",
	//		ZSTR_VAL(us->wrapper->ce->name), zend_zval_value_name(&retval));
	zval_ptr_dtor(&retval);
	return PHP_STREAM_OPTION_RETURN_NOTIMPL;
}

static int user_stream_set_truncation(php_stream *stream, const php_userstream_data_t *us,
		int value, void *ptrparam) {
	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_TRUNCATE, false);

	if (value == PHP_STREAM_TRUNCATE_SUPPORTED) {
		zval zstr;
		ZVAL_STR(&zstr, func_name);
		bool is_callable = zend_is_callable_ex(&zstr, Z_OBJ(us->object), IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, NULL, NULL);
		// Frees func_name
		zval_ptr_dtor(&zstr);
		return is_callable ? PHP_STREAM_OPTION_RETURN_OK : PHP_STREAM_OPTION_RETURN_ERR;
	}
	ZEND_ASSERT(value == PHP_STREAM_TRUNCATE_SET_SIZE);
	ptrdiff_t new_size = *(ptrdiff_t*) ptrparam;

	if (UNEXPECTED(new_size < 0 || new_size > (ptrdiff_t)LONG_MAX)) {
		/* bad new size */
		zend_string_release_ex(func_name, false);
		return PHP_STREAM_OPTION_RETURN_ERR;
	}

	zval retval;
	zval size;

	ZVAL_LONG(&size, (zend_long)new_size);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, &size);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_TRUNCATE " is not implemented!", ZSTR_VAL(us->wrapper->ce->name));
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
	if (UNEXPECTED(Z_ISUNDEF(retval))) {
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
	if (EXPECTED(Z_TYPE(retval) == IS_FALSE || Z_TYPE(retval) == IS_TRUE)) {
		return Z_TYPE(retval) == IS_TRUE ? PHP_STREAM_OPTION_RETURN_OK : PHP_STREAM_OPTION_RETURN_ERR;
	} else {
		php_stream_warn(stream, UserspaceInvalidReturn,
				"%s::" USERSTREAM_TRUNCATE " value must be of type bool, %s given",
				ZSTR_VAL(us->wrapper->ce->name), zend_zval_value_name(&retval));
		zval_ptr_dtor(&retval);
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
}

static int user_stream_set_option(php_stream *stream, const php_userstream_data_t *us, int option,
		int value, void *ptrparam)
{
	zval args[3];
	ZVAL_LONG(&args[0], option);
	ZVAL_LONG(&args[1], value);
	ZVAL_NULL(&args[2]);

	if (option == PHP_STREAM_OPTION_READ_TIMEOUT) {
		struct timeval tv = *(struct timeval*)ptrparam;
		ZVAL_LONG(&args[1], tv.tv_sec);
		ZVAL_LONG(&args[2], tv.tv_usec);
	} else if (option == PHP_STREAM_OPTION_READ_BUFFER || option == PHP_STREAM_OPTION_WRITE_BUFFER) {
		if (ptrparam) {
			ZVAL_LONG(&args[2], *(long *)ptrparam);
		} else {
			ZVAL_LONG(&args[2], BUFSIZ);
		}
	}

	zval retval;
	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_SET_OPTION, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 3, args);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
				"%s::" USERSTREAM_SET_OPTION " is not implemented!",
				ZSTR_VAL(us->wrapper->ce->name));
		return PHP_STREAM_OPTION_RETURN_ERR;
	}
	if (UNEXPECTED(Z_ISUNDEF(retval))) {
		return PHP_STREAM_OPTION_RETURN_ERR;
	}

	int ret;
	if (zend_is_true(&retval)) {
		ret = PHP_STREAM_OPTION_RETURN_OK;
	} else {
		ret = PHP_STREAM_OPTION_RETURN_ERR;
	}

	zval_ptr_dtor(&retval);
	return ret;
}

static int php_userstreamop_set_option(php_stream *stream, int option, int value, void *ptrparam) {
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;

	switch (option) {
		case PHP_STREAM_OPTION_CHECK_LIVENESS:
			return user_stream_set_check_liveliness(stream, us);

		case PHP_STREAM_OPTION_LOCKING:
			return user_stream_set_locking(stream, us, value);

		case PHP_STREAM_OPTION_TRUNCATE_API:
			return user_stream_set_truncation(stream, us, value, ptrparam);

		case PHP_STREAM_OPTION_READ_BUFFER:
		case PHP_STREAM_OPTION_WRITE_BUFFER:
		case PHP_STREAM_OPTION_READ_TIMEOUT:
		case PHP_STREAM_OPTION_BLOCKING:
			return user_stream_set_option(stream, us, option, value, ptrparam);

		default:
			return PHP_STREAM_OPTION_RETURN_NOTIMPL;
	}
}


static int user_wrapper_unlink(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	zval zretval;
	zval args[1];
	zval object;
	int ret = 0;

	/* create an instance of our class */
	user_stream_create_object(uwrap, context, &object);
	if (Z_TYPE(object) == IS_UNDEF) {
		return ret;
	}

	/* call the unlink method */
	ZVAL_STRING(&args[0], url);


	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_UNLINK, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(object), func_name, &zretval, 1, args);
	zend_string_release_ex(func_name, false);
	zval_ptr_dtor(&args[0]);
	zval_ptr_dtor(&object);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_wrapper_warn(wrapper, context, REPORT_ERRORS, NotImplemented,
				"%s::" USERSTREAM_UNLINK " is not implemented!", ZSTR_VAL(uwrap->ce->name));
	} else if (Z_TYPE(zretval) == IS_FALSE || Z_TYPE(zretval) == IS_TRUE) {
		ret = Z_TYPE(zretval) == IS_TRUE;
	}
	// TODO: Warn on invalid return type, or use zend_is_true()?

	zval_ptr_dtor(&zretval);

	return ret;
}

static int user_wrapper_rename(php_stream_wrapper *wrapper, const char *url_from, const char *url_to,
							   int options, php_stream_context *context)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	zval zretval;
	zval args[2];
	zval object;
	int ret = 0;

	/* create an instance of our class */
	user_stream_create_object(uwrap, context, &object);
	if (Z_TYPE(object) == IS_UNDEF) {
		return ret;
	}

	/* call the rename method */
	ZVAL_STRING(&args[0], url_from);
	ZVAL_STRING(&args[1], url_to);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_RENAME, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(object), func_name, &zretval, 2, args);
	zend_string_release_ex(func_name, false);
	zval_ptr_dtor(&args[1]);
	zval_ptr_dtor(&args[0]);
	zval_ptr_dtor(&object);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_wrapper_warn(wrapper, context, REPORT_ERRORS, NotImplemented,
				"%s::" USERSTREAM_RENAME " is not implemented!", ZSTR_VAL(uwrap->ce->name));
	} else if (Z_TYPE(zretval) == IS_FALSE || Z_TYPE(zretval) == IS_TRUE) {
		ret = Z_TYPE(zretval) == IS_TRUE;
	}
	// TODO: Warn on invalid return type, or use zend_is_true()?

	zval_ptr_dtor(&zretval);

	return ret;
}

static int user_wrapper_mkdir(php_stream_wrapper *wrapper, const char *url, int mode,
							  int options, php_stream_context *context)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	zval zretval;
	zval args[3];
	zval object;
	int ret = 0;

	/* create an instance of our class */
	user_stream_create_object(uwrap, context, &object);
	if (Z_TYPE(object) == IS_UNDEF) {
		return ret;
	}

	/* call the mkdir method */
	ZVAL_STRING(&args[0], url);
	ZVAL_LONG(&args[1], mode);
	ZVAL_LONG(&args[2], options);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_MKDIR, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(object), func_name, &zretval, 3, args);
	zend_string_release_ex(func_name, false);
	zval_ptr_dtor(&args[0]);
	zval_ptr_dtor(&object);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_wrapper_warn(wrapper, context, REPORT_ERRORS, NotImplemented,
				"%s::" USERSTREAM_MKDIR " is not implemented!", ZSTR_VAL(uwrap->ce->name));
	} else if (Z_TYPE(zretval) == IS_FALSE || Z_TYPE(zretval) == IS_TRUE) {
		ret = Z_TYPE(zretval) == IS_TRUE;
	}
	// TODO: Warn on invalid return type, or use zend_is_true()?

	zval_ptr_dtor(&zretval);

	return ret;
}

static int user_wrapper_rmdir(php_stream_wrapper *wrapper, const char *url,
							  int options, php_stream_context *context)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	zval zretval;
	zval args[2];
	zval object;
	int ret = 0;

	/* create an instance of our class */
	user_stream_create_object(uwrap, context, &object);
	if (Z_TYPE(object) == IS_UNDEF) {
		return ret;
	}

	/* call the rmdir method */
	ZVAL_STRING(&args[0], url);
	ZVAL_LONG(&args[1], options);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_RMDIR, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(object), func_name, &zretval, 2, args);
	zend_string_release_ex(func_name, false);
	zval_ptr_dtor(&args[0]);
	zval_ptr_dtor(&object);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_wrapper_warn(wrapper, context, REPORT_ERRORS, NotImplemented,
				"%s::" USERSTREAM_RMDIR " is not implemented!", ZSTR_VAL(uwrap->ce->name));
	} else if (Z_TYPE(zretval) == IS_FALSE || Z_TYPE(zretval) == IS_TRUE) {
		ret = Z_TYPE(zretval) == IS_TRUE;
	}
	// TODO: Warn on invalid return type, or use zend_is_true()?

	zval_ptr_dtor(&zretval);

	return ret;
}

static int user_wrapper_metadata(php_stream_wrapper *wrapper, const char *url, int option,
								 void *value, php_stream_context *context)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	zval zretval;
	zval args[3];
	zval object;
	int ret = 0;

	switch(option) {
		case PHP_STREAM_META_TOUCH:
			array_init(&args[2]);
			if(value) {
				struct utimbuf *newtime = (struct utimbuf *)value;
				add_index_long(&args[2], 0, newtime->modtime);
				add_index_long(&args[2], 1, newtime->actime);
			}
			break;
		case PHP_STREAM_META_GROUP:
		case PHP_STREAM_META_OWNER:
		case PHP_STREAM_META_ACCESS:
			ZVAL_LONG(&args[2], *(long *)value);
			break;
		case PHP_STREAM_META_GROUP_NAME:
		case PHP_STREAM_META_OWNER_NAME:
			ZVAL_STRING(&args[2], value);
			break;
		default:
			php_stream_wrapper_warn(wrapper, context, REPORT_ERRORS,
					InvalidMeta,
					"Unknown option %d for " USERSTREAM_METADATA, option);
			return ret;
	}

	/* create an instance of our class */
	user_stream_create_object(uwrap, context, &object);
	if (Z_TYPE(object) == IS_UNDEF) {
		zval_ptr_dtor(&args[2]);
		return ret;
	}

	/* call the mkdir method */
	ZVAL_STRING(&args[0], url);
	ZVAL_LONG(&args[1], option);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_METADATA, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(object), func_name, &zretval, 3, args);
	zend_string_release_ex(func_name, false);
	zval_ptr_dtor(&args[2]);
	zval_ptr_dtor(&args[0]);
	zval_ptr_dtor(&object);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_wrapper_warn(wrapper, context, REPORT_ERRORS, NotImplemented,
				"%s::" USERSTREAM_METADATA " is not implemented!", ZSTR_VAL(uwrap->ce->name));
	} else if (Z_TYPE(zretval) == IS_FALSE || Z_TYPE(zretval) == IS_TRUE) {
		ret = Z_TYPE(zretval) == IS_TRUE;
	}
	// TODO: Warn on invalid return type, or use zend_is_true()?

	zval_ptr_dtor(&zretval);

	return ret;
}


static int user_wrapper_stat_url(php_stream_wrapper *wrapper, const char *url, int flags,
								 php_stream_statbuf *ssb, php_stream_context *context)
{
	struct php_user_stream_wrapper *uwrap = (struct php_user_stream_wrapper*)wrapper->abstract;
	zval zretval;
	zval args[2];
	zval object;
	int ret = -1;

	/* create an instance of our class */
	user_stream_create_object(uwrap, context, &object);
	if (Z_TYPE(object) == IS_UNDEF) {
		return -1;
	}

	/* call it's stat_url method - set up params first */
	ZVAL_STRING(&args[0], url);
	ZVAL_LONG(&args[1], flags);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_STATURL, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(object), func_name, &zretval, 2, args);
	zend_string_release_ex(func_name, false);
	zval_ptr_dtor(&args[0]);
	zval_ptr_dtor(&object);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_wrapper_warn(wrapper, context, REPORT_ERRORS, NotImplemented,
			"%s::" USERSTREAM_STATURL " is not implemented!", ZSTR_VAL(uwrap->ce->name));
		return -1;
	}
	if (UNEXPECTED(Z_ISUNDEF(zretval))) {
		return -1;
	}
	if (EXPECTED(Z_TYPE(zretval) == IS_ARRAY)) {
		statbuf_from_array(Z_ARR(zretval), ssb);
		ret = 0;
	}
	// TODO: Warning on incorrect return type?

	zval_ptr_dtor(&zretval);

	return ret;

}

static ssize_t php_userstreamop_readdir(php_stream *stream, char *buf, size_t count)
{
	zval retval;
	size_t didread = 0;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;
	php_stream_dirent *ent = (php_stream_dirent*)buf;

	/* avoid problems if someone mis-uses the stream */
	if (count != sizeof(php_stream_dirent)) {
		return -1;
	}

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_DIR_READ, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(call_result == FAILURE)) {
		php_stream_warn(stream, NotImplemented,
			"%s::" USERSTREAM_DIR_READ " is not implemented!",
			ZSTR_VAL(us->wrapper->ce->name));
		return -1;
	}
	if (UNEXPECTED(Z_ISUNDEF(retval))) {
		return -1;
	}
	// TODO: Warn/TypeError for invalid returns?
	if (Z_TYPE(retval) != IS_FALSE && Z_TYPE(retval) != IS_TRUE) {
		if (UNEXPECTED(!try_convert_to_string(&retval))) {
			zval_ptr_dtor(&retval);
			return -1;
		}
		PHP_STRLCPY(ent->d_name, Z_STRVAL(retval), sizeof(ent->d_name), Z_STRLEN(retval));
		ent->d_type = DT_UNKNOWN;

		didread = sizeof(php_stream_dirent);
	}

	zval_ptr_dtor(&retval);

	return didread;
}

static int php_userstreamop_closedir(php_stream *stream, int close_handle)
{
	zval retval;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;

	assert(us != NULL);

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_DIR_CLOSE, false);
	zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	zval_ptr_dtor(&retval);
	zval_ptr_dtor(&us->object);
	ZVAL_UNDEF(&us->object);
	efree(us);

	return 0;
}

static int php_userstreamop_rewinddir(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffs)
{
	zval retval;
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_DIR_REWIND, false);
	zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL);
	zend_string_release_ex(func_name, false);

	zval_ptr_dtor(&retval);

	return 0;

}

static int php_userstreamop_cast(php_stream *stream, int castas, void **retptr)
{
	php_userstream_data_t *us = (php_userstream_data_t *)stream->abstract;
	zval retval;
	zval args[1];
	php_stream * intstream = NULL;
	int ret = FAILURE;
	/* If we are checking if the stream can cast, no return pointer is provided, so do not emit errors */
	bool report_errors = retptr;

	switch(castas) {
	case PHP_STREAM_AS_FD_FOR_SELECT:
		ZVAL_LONG(&args[0], PHP_STREAM_AS_FD_FOR_SELECT);
		break;
	default:
		ZVAL_LONG(&args[0], PHP_STREAM_AS_STDIO);
		break;
	}

	uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;

	zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_CAST, false);
	zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
	zend_string_release_ex(func_name, false);

	if (UNEXPECTED(call_result == FAILURE)) {
		if (report_errors) {
			php_stream_warn(stream, NotImplemented,
					"%s::" USERSTREAM_CAST " is not implemented!",
					ZSTR_VAL(us->wrapper->ce->name));
		}
		goto out;
	}

	do {
		if (!zend_is_true(&retval)) {
			break;
		}
		// TODO: Can this emit an exception even with no error reporting?
		php_stream_from_zval_no_verify(intstream, &retval);
		if (!intstream) {
			if (report_errors) {
				php_stream_warn(stream, UserspaceInvalidReturn,
						"%s::" USERSTREAM_CAST " must return a stream resource",
						ZSTR_VAL(us->wrapper->ce->name));
			}
			break;
		}
		if (intstream == stream) {
			if (report_errors) {
				php_stream_warn(stream, UserspaceInvalidReturn,
						"%s::" USERSTREAM_CAST " must not return itself",
						ZSTR_VAL(us->wrapper->ce->name));
			}
			intstream = NULL;
			break;
		}
		ret = php_stream_cast(intstream, castas, retptr, 1);
	} while (0);

	zval_ptr_dtor(&retval);

out:
	stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= orig_no_fclose;

	return ret;
}

const php_stream_ops php_stream_userspace_ops = {
	php_userstreamop_write, php_userstreamop_read,
	php_userstreamop_close, php_userstreamop_flush,
	"user-space",
	php_userstreamop_seek,
	php_userstreamop_cast,
	php_userstreamop_stat,
	php_userstreamop_set_option,
};

const php_stream_ops php_stream_userspace_dir_ops = {
	NULL, /* write */
	php_userstreamop_readdir,
	php_userstreamop_closedir,
	NULL, /* flush */
	"user-space-dir",
	php_userstreamop_rewinddir,
	NULL, /* cast */
	NULL, /* stat */
	NULL  /* set_option */
};
