From 3f02f88a17d0d6b0891497a65ed190b89157416d Mon Sep 17 00:00:00 2001 From: Remi Collet Date: Fri, 11 Sep 2020 16:48:26 +0200 Subject: add patches for PHP 8 from upstream and https://github.com/phpredis/phpredis/pull/1845 disable msgpack serializer with PHP 8 --- php-pecl-redis5.spec | 43 +++- redis-php8.patch | 658 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 695 insertions(+), 6 deletions(-) create mode 100644 redis-php8.patch diff --git a/php-pecl-redis5.spec b/php-pecl-redis5.spec index 7e6563b..e6f5389 100644 --- a/php-pecl-redis5.spec +++ b/php-pecl-redis5.spec @@ -21,6 +21,13 @@ %global with_zts 0%{!?_without_zts:%{?__ztsphp:1}} %bcond_without tests %bcond_without igbinary +# NOTICE: this only work when php-devel in buildroot (so only for SCL) +# use mock... --without msgpack for base package +%if "%{php_version}" > "8.0" +%bcond_with msgpack +%else +%bcond_without msgpack +%endif # after 20-json, 40-igbinary and 40-msgpack %global ini_name 50-%{pecl_name}.ini @@ -30,11 +37,13 @@ Summary: Extension for communicating with the Redis key-value store Name: %{?sub_prefix}php-pecl-redis5 Version: %{upstream_version}%{?upstream_prever:~%{upstream_prever}} -Release: 1%{?dist}%{!?scl:%{!?nophptag:%(%{__php} -r 'echo ".".PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')}} +Release: 2%{?dist}%{!?scl:%{!?nophptag:%(%{__php} -r 'echo ".".PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')}} License: PHP URL: https://pecl.php.net/package/redis Source0: https://pecl.php.net/get/%{pecl_name}-%{upstream_version}%{?upstream_prever}.tgz +Patch0: %{pecl_name}-php8.patch + BuildRequires: %{?dtsprefix}gcc BuildRequires: %{?scl_prefix}php-devel > 7 BuildRequires: %{?scl_prefix}php-pear @@ -42,7 +51,9 @@ BuildRequires: %{?scl_prefix}php-json %if %{with igbinary} BuildRequires: %{?scl_prefix}php-pecl-igbinary-devel %endif +%if %{with msgpack} BuildRequires: %{?scl_prefix}php-pecl-msgpack-devel >= 2.0.3 +%endif BuildRequires: pkgconfig(liblzf) BuildRequires: pkgconfig(libzstd) >= 1.3.0 BuildRequires: pkgconfig(liblz4) @@ -57,7 +68,9 @@ Requires: %{?scl_prefix}php-json%{?_isa} %if %{with igbinary} Requires: %{?scl_prefix}php-pecl(igbinary)%{?_isa} %endif +%if %{with msgpack} Requires: %{?scl_prefix}php-pecl-msgpack%{?_isa} +%endif %{?_sclreq:Requires: %{?scl_prefix}runtime%{?_sclreq}%{?_isa}} Obsoletes: %{?scl_prefix}php-%{pecl_name} < 3 @@ -79,7 +92,7 @@ Conflicts: %{?scl_prefix}php-pecl-%{pecl_name} < 5 Conflicts: %{?scl_prefix}php-pecl-%{pecl_name}4 < 5 %endif -%if "%{?vendor}" == "Remi Collet" && 0%{!?scl:1} && 0%{?rhel} +%if "%{?packager}" == "Remi Collet" && 0%{!?scl:1} && 0%{?rhel} # Other third party repo stuff %if "%{php_version}" > "7.1" Obsoletes: php71u-pecl-%{pecl_name} <= %{version} @@ -95,7 +108,9 @@ Obsoletes: php73w-pecl-%{pecl_name} <= %{version} %endif %if "%{php_version}" > "7.4" Obsoletes: php74-pecl-%{pecl_name} <= %{version} -Obsoletes: php74w-pecl-%{pecl_name} <= %{version} +%endif +%if "%{php_version}" > "8.0" +Obsoletes: php80-pecl-%{pecl_name} <= %{version} %endif %endif @@ -129,6 +144,10 @@ sed -e 's/role="test"/role="src"/' \ -i package.xml cd NTS +%if "%{php_version}" > "8.0" +%patch0 -p1 -b.php8 +%endif + # Use system library rm -r liblzf @@ -203,7 +222,9 @@ peclconf() { %if %{with igbinary} --enable-redis-igbinary \ %endif +%if %{with msgpack} --enable-redis-msgpack \ +%endif --enable-redis-lzf \ --with-liblzf \ --enable-redis-zstd \ @@ -251,20 +272,25 @@ done %check # simple module load test -DEPS="--no-php-ini --define extension=json.so" +DEPS="--no-php-ini" +%if "%{php_version}" < "8.0" +DEPS="$DEPS --define extension=json.so" +%endif %if %{with igbinary} DEPS="$DEPS --define extension=igbinary.so" %endif +%if %{with msgpack} DEPS="$DEPS --define extension=msgpack.so" +%endif %{__php} $DEPS \ --define extension=%{buildroot}%{php_extdir}/%{pecl_name}.so \ - --modules | grep %{pecl_name} + --modules | grep '^%{pecl_name}$' %if %{with_zts} %{__ztsphp} $DEPS \ --define extension=%{buildroot}%{php_ztsextdir}/%{pecl_name}.so \ - --modules | grep %{pecl_name} + --modules | grep '^%{pecl_name}$' %endif %if %{with tests} @@ -339,6 +365,11 @@ fi %changelog +* Fri Sep 11 2020 Remi Collet - 5.3.1-2 +- add patches for PHP 8 from upstream and + https://github.com/phpredis/phpredis/pull/1845 +- disable msgpack serializer with PHP 8 + * Wed Jul 8 2020 Remi Collet - 5.3.1-1 - update to 5.3.1 diff --git a/redis-php8.patch b/redis-php8.patch new file mode 100644 index 0000000..1227349 --- /dev/null +++ b/redis-php8.patch @@ -0,0 +1,658 @@ +diff --git a/cluster_library.c b/cluster_library.c +index 98ba9c2..3720f72 100644 +--- a/cluster_library.c ++++ b/cluster_library.c +@@ -634,8 +634,12 @@ cluster_node_create(redisCluster *c, char *host, size_t host_len, + zend_llist_init(&node->slots, sizeof(redisSlotRange), NULL, 0); + + // Attach socket +- node->sock = redis_sock_create(host, host_len, port, c->timeout, +- c->read_timeout, c->persistent, NULL, 0); ++ node->sock = redis_sock_create(host, host_len, port, ++ c->flags->timeout, c->flags->read_timeout, ++ c->flags->persistent, NULL, 0); ++ ++ /* Stream context */ ++ node->sock->stream_ctx = c->flags->stream_ctx; + + redis_sock_set_auth(node->sock, c->flags->user, c->flags->pass); + +@@ -818,12 +822,12 @@ PHP_REDIS_API redisCluster *cluster_create(double timeout, double read_timeout, + + /* Initialize flags and settings */ + c->flags = ecalloc(1, sizeof(RedisSock)); ++ c->flags->timeout = timeout; ++ c->flags->read_timeout = read_timeout; ++ c->flags->persistent = persistent; + c->subscribed_slot = -1; + c->clusterdown = 0; +- c->timeout = timeout; +- c->read_timeout = read_timeout; + c->failover = failover; +- c->persistent = persistent; + c->err = NULL; + + /* Set up our waitms based on timeout */ +@@ -993,9 +997,12 @@ void cluster_init_cache(redisCluster *c, redisCachedCluster *cc) { + + /* Create socket */ + sock = redis_sock_create(ZSTR_VAL(cm->host.addr), ZSTR_LEN(cm->host.addr), cm->host.port, +- c->timeout, c->read_timeout, c->persistent, ++ c->flags->timeout, c->flags->read_timeout, c->flags->persistent, + NULL, 0); + ++ /* Stream context */ ++ sock->stream_ctx = c->flags->stream_ctx; ++ + /* Add to seed nodes */ + zend_hash_str_update_ptr(c->seeds, key, keylen, sock); + +@@ -1027,7 +1034,8 @@ void cluster_init_cache(redisCluster *c, redisCachedCluster *cc) { + * seeds array and know we have a non-empty array of strings all in + * host:port format. */ + PHP_REDIS_API void +-cluster_init_seeds(redisCluster *cluster, zend_string **seeds, uint32_t nseeds) { ++cluster_init_seeds(redisCluster *c, zend_string **seeds, uint32_t nseeds) ++{ + RedisSock *sock; + char *seed, *sep, key[1024]; + int key_len, i, *map; +@@ -1044,19 +1052,22 @@ cluster_init_seeds(redisCluster *cluster, zend_string **seeds, uint32_t nseeds) + ZEND_ASSERT(sep != NULL); + + // Allocate a structure for this seed +- sock = redis_sock_create(seed, sep - seed, +- (unsigned short)atoi(sep+1), cluster->timeout, +- cluster->read_timeout, cluster->persistent, NULL, 0); ++ sock = redis_sock_create(seed, sep - seed, atoi(sep + 1), ++ c->flags->timeout, c->flags->read_timeout, ++ c->flags->persistent, NULL, 0); ++ ++ /* Stream context */ ++ sock->stream_ctx = c->flags->stream_ctx; + + /* Credentials */ +- redis_sock_set_auth(sock, cluster->flags->user, cluster->flags->pass); ++ redis_sock_set_auth(sock, c->flags->user, c->flags->pass); + + // Index this seed by host/port + key_len = snprintf(key, sizeof(key), "%s:%u", ZSTR_VAL(sock->host), + sock->port); + + // Add to our seed HashTable +- zend_hash_str_update_ptr(cluster->seeds, key, key_len, sock); ++ zend_hash_str_update_ptr(c->seeds, key, key_len, sock); + } + + efree(map); +@@ -1807,7 +1818,6 @@ PHP_REDIS_API void cluster_sub_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster * + zval z_ret, z_args[4]; + sctx->cb.retval = &z_ret; + sctx->cb.params = z_args; +- sctx->cb.no_separation = 0; + + /* We're in a subscribe loop */ + c->subscribed_slot = c->cmd_slot; +diff --git a/cluster_library.h b/cluster_library.h +index de9d171..98e9b0e 100644 +--- a/cluster_library.h ++++ b/cluster_library.h +@@ -186,12 +186,8 @@ typedef struct clusterFoldItem clusterFoldItem; + /* RedisCluster implementation structure */ + typedef struct redisCluster { + +- /* Timeout and read timeout (for normal operations) */ +- double timeout; +- double read_timeout; +- +- /* Are we using persistent connections */ +- int persistent; ++ /* One RedisSock struct for serialization and prefix information */ ++ RedisSock *flags; + + /* How long in milliseconds should we wait when being bounced around */ + long waitms; +@@ -241,9 +237,6 @@ typedef struct redisCluster { + /* The slot where we're subscribed */ + short subscribed_slot; + +- /* One RedisSock struct for serialization and prefix information */ +- RedisSock *flags; +- + /* The first line of our last reply, not including our reply type byte + * or the trailing \r\n */ + char line_reply[1024]; +diff --git a/library.c b/library.c +index 2f95aea..6aef627 100644 +--- a/library.c ++++ b/library.c +@@ -464,7 +464,6 @@ PHP_REDIS_API int redis_subscribe_response(INTERNAL_FUNCTION_PARAMETERS, + zval z_ret, z_args[4]; + sctx->cb.retval = &z_ret; + sctx->cb.params = z_args; +- sctx->cb.no_separation = 0; + + /* Multibulk response, {[pattern], type, channel, payload } */ + while(1) { +@@ -686,19 +685,6 @@ redis_sock_read(RedisSock *redis_sock, int *buf_len) + return NULL; + } + +-static int redis_sock_read_cmp(RedisSock *redis_sock, const char *cmp, int cmplen) { +- char *resp; +- int len, rv = FAILURE; +- +- if ((resp = redis_sock_read(redis_sock, &len)) == NULL) +- return FAILURE; +- +- rv = len == cmplen && !memcmp(resp, cmp, cmplen) ? SUCCESS : FAILURE; +- +- efree(resp); +- return rv; +-} +- + /* A simple union to store the various arg types we might handle in our + * redis_spprintf command formatting function */ + union resparg { +@@ -1519,17 +1505,21 @@ redis_read_stream_messages(RedisSock *redis_sock, int count, zval *z_ret + * the multi-bulk header for field and values */ + if ((read_mbulk_header(redis_sock, &mhdr) < 0 || mhdr != 2) || + ((id = redis_sock_read(redis_sock, &idlen)) == NULL) || +- (read_mbulk_header(redis_sock, &fields) < 0 || fields % 2 != 0)) ++ (read_mbulk_header(redis_sock, &fields) < 0 || ++ (fields > 0 && fields % 2 != 0))) + { + if (id) efree(id); + return -1; + } + +- array_init(&z_message); +- +- redis_mbulk_reply_loop(redis_sock, &z_message, fields, UNSERIALIZE_VALS); +- array_zip_values_and_scores(redis_sock, &z_message, SCORE_DECODE_NONE); +- add_assoc_zval_ex(z_ret, id, idlen, &z_message); ++ if (fields < 0) { ++ add_assoc_null_ex(z_ret, id, idlen); ++ } else { ++ array_init(&z_message); ++ redis_mbulk_reply_loop(redis_sock, &z_message, fields, UNSERIALIZE_VALS); ++ array_zip_values_and_scores(redis_sock, &z_message, SCORE_DECODE_NONE); ++ add_assoc_zval_ex(z_ret, id, idlen, &z_message); ++ } + efree(id); + } + +@@ -1727,7 +1717,10 @@ redis_read_xinfo_response(RedisSock *redis_sock, zval *z_ret, int elements) + switch (type) { + case TYPE_BULK: + if ((data = redis_sock_read_bulk_reply(redis_sock, li)) == NULL) { +- goto failure; ++ if (!key) goto failure; ++ add_assoc_null_ex(z_ret, key, len); ++ efree(key); ++ key = NULL; + } else if (key) { + add_assoc_stringl_ex(z_ret, key, len, data, li); + efree(data); +@@ -2130,16 +2123,21 @@ static int redis_stream_liveness_check(php_stream *stream) { + static int + redis_sock_check_liveness(RedisSock *redis_sock) + { +- char id[64], ok; ++ char id[64], inbuf[4096]; + int idlen, auth; + smart_string cmd = {0}; ++ size_t len; + + /* Short circuit if we detect the stream has gone bad or if the user has + * configured persistent connection "YOLO mode". */ +- if (redis_stream_liveness_check(redis_sock->stream) != SUCCESS) +- return FAILURE; +- else if (!INI_INT("redis.pconnect.echo_check_liveness")) ++ if (redis_stream_liveness_check(redis_sock->stream) != SUCCESS) { ++ goto failure; ++ } ++ ++ redis_sock->status = REDIS_SOCK_STATUS_CONNECTED; ++ if (!INI_INT("redis.pconnect.echo_check_liveness")) { + return SUCCESS; ++ } + + /* AUTH (if we need it) */ + auth = redis_sock_append_auth(redis_sock, &cmd); +@@ -2150,12 +2148,55 @@ redis_sock_check_liveness(RedisSock *redis_sock) + redis_cmd_append_sstr(&cmd, id, idlen); + + /* Send command(s) and make sure we can consume reply(ies) */ +- ok = (redis_sock_write(redis_sock, cmd.c, cmd.len) >= 0) && +- (!auth || redis_sock_read_ok(redis_sock) == SUCCESS) && +- (redis_sock_read_cmp(redis_sock, id, idlen) == SUCCESS); +- ++ if (redis_sock_write(redis_sock, cmd.c, cmd.len) < 0) { ++ smart_string_free(&cmd); ++ goto failure; ++ } + smart_string_free(&cmd); +- return ok ? SUCCESS : FAILURE; ++ ++ if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0) { ++ goto failure; ++ } ++ ++ if (auth) { ++ if (strncmp(inbuf, "+OK", 3) == 0 || strncmp(inbuf, "-ERR Client sent AUTH", 21) == 0) { ++ /* successfully authenticated or authentication isn't required */ ++ if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0) { ++ goto failure; ++ } ++ } else if (strncmp(inbuf, "-NOAUTH", 7) == 0) { ++ /* connection is fine but authentication failed, next command must fails too */ ++ if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0 || strncmp(inbuf, "-NOAUTH", 7) != 0) { ++ goto failure; ++ } ++ return SUCCESS; ++ } else { ++ goto failure; ++ } ++ redis_sock->status = REDIS_SOCK_STATUS_READY; ++ } else { ++ if (strncmp(inbuf, "-NOAUTH", 7) == 0) { ++ /* connection is fine but authentication required */ ++ return SUCCESS; ++ } ++ } ++ ++ /* check echo response */ ++ if (*inbuf != TYPE_BULK || atoi(inbuf + 1) != idlen || ++ redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0 || ++ strncmp(inbuf, id, idlen) != 0 ++ ) { ++ goto failure; ++ } ++ ++ return SUCCESS; ++failure: ++ redis_sock->status = REDIS_SOCK_STATUS_DISCONNECTED; ++ if (redis_sock->stream) { ++ php_stream_pclose(redis_sock->stream); ++ redis_sock->stream = NULL; ++ } ++ return FAILURE; + } + + /** +@@ -2165,9 +2206,9 @@ PHP_REDIS_API int redis_sock_connect(RedisSock *redis_sock) + { + struct timeval tv, read_tv, *tv_ptr = NULL; + zend_string *persistent_id = NULL, *estr = NULL; +- char host[1024], *pos, *address, *schema = NULL; ++ char host[1024], *pos, *address, *scheme = NULL; + const char *fmtstr = "%s://%s:%d"; +- int host_len, usocket = 0, err = 0, tcp_flag = 1; ++ int host_len, usocket = 0, err = 0, tcp_flag = 1, scheme_free = 0; + ConnectionPool *p = NULL; + + if (redis_sock->stream != NULL) { +@@ -2175,9 +2216,12 @@ PHP_REDIS_API int redis_sock_connect(RedisSock *redis_sock) + } + + address = ZSTR_VAL(redis_sock->host); +- if ((pos = strstr(address, "://")) != NULL) { +- schema = estrndup(address, pos - address); ++ if ((pos = strstr(address, "://")) == NULL) { ++ scheme = redis_sock->stream_ctx ? "ssl" : "tcp"; ++ } else { ++ scheme = estrndup(address, pos - address); + address = pos + sizeof("://") - 1; ++ scheme_free = 1; + } + if (address[0] == '/' && redis_sock->port < 1) { + host_len = snprintf(host, sizeof(host), "unix://%s", address); +@@ -2193,9 +2237,9 @@ PHP_REDIS_API int redis_sock_connect(RedisSock *redis_sock) + fmtstr = "%s://[%s]:%d"; + } + #endif +- host_len = snprintf(host, sizeof(host), fmtstr, schema ? schema : "tcp", address, redis_sock->port); +- if (schema) efree(schema); ++ host_len = snprintf(host, sizeof(host), fmtstr, scheme, address, redis_sock->port); + } ++ if (scheme_free) efree(scheme); + + if (redis_sock->persistent) { + if (INI_INT("redis.pconnect.pooling_enabled")) { +@@ -2205,11 +2249,7 @@ PHP_REDIS_API int redis_sock_connect(RedisSock *redis_sock) + zend_llist_remove_tail(&p->list); + + if (redis_sock_check_liveness(redis_sock) == SUCCESS) { +- redis_sock->status = REDIS_SOCK_STATUS_READY; + return SUCCESS; +- } else if (redis_sock->stream) { +- php_stream_pclose(redis_sock->stream); +- redis_sock->stream = NULL; + } + p->nb_active--; + } +diff --git a/redis_array_impl.c b/redis_array_impl.c +index a427f67..8d8cece 100644 +--- a/redis_array_impl.c ++++ b/redis_array_impl.c +@@ -1134,7 +1134,6 @@ zval_rehash_callback(zend_fcall_info *z_cb, zend_fcall_info_cache *z_cb_cache, + z_cb->params = z_args; + z_cb->retval = z_ret; + +- z_cb->no_separation = 0; + z_cb->param_count = 2; + + /* run cb(hostname, count) */ +diff --git a/redis_cluster.c b/redis_cluster.c +index 3233b65..5cb453b 100644 +--- a/redis_cluster.c ++++ b/redis_cluster.c +@@ -351,7 +351,7 @@ void free_cluster_context(zend_object *object) { + /* Attempt to connect to a Redis cluster provided seeds and timeout options */ + static void redis_cluster_init(redisCluster *c, HashTable *ht_seeds, double timeout, + double read_timeout, int persistent, zend_string *user, +- zend_string *pass) ++ zend_string *pass, zval *context) + { + zend_string *hash = NULL, **seeds; + redisCachedCluster *cc; +@@ -369,10 +369,13 @@ static void redis_cluster_init(redisCluster *c, HashTable *ht_seeds, double time + c->flags->user = zend_string_copy(user); + if (pass && ZSTR_LEN(pass)) + c->flags->pass = zend_string_copy(pass); ++ if (context) { ++ redis_sock_set_stream_context(c->flags, context); ++ } + +- c->timeout = timeout; +- c->read_timeout = read_timeout; +- c->persistent = persistent; ++ c->flags->timeout = timeout; ++ c->flags->read_timeout = read_timeout; ++ c->flags->persistent = persistent; + c->waitms = timeout * 1000L; + + /* Attempt to load slots from cache if caching is enabled */ +@@ -450,7 +453,7 @@ void redis_cluster_load(redisCluster *c, char *name, int name_len) { + } + + /* Attempt to create/connect to the cluster */ +- redis_cluster_init(c, ht_seeds, timeout, read_timeout, persistent, user, pass); ++ redis_cluster_init(c, ht_seeds, timeout, read_timeout, persistent, user, pass, NULL); + + /* Clean up */ + zval_dtor(&z_seeds); +@@ -464,38 +467,36 @@ void redis_cluster_load(redisCluster *c, char *name, int name_len) { + + /* Create a RedisCluster Object */ + PHP_METHOD(RedisCluster, __construct) { +- zval *object, *z_seeds = NULL, *z_auth = NULL; ++ zval *object, *z_seeds = NULL, *z_auth = NULL, *context = NULL; + zend_string *user = NULL, *pass = NULL; + double timeout = 0.0, read_timeout = 0.0; + size_t name_len; + zend_bool persistent = 0; +- redisCluster *context = GET_CONTEXT(); ++ redisCluster *c = GET_CONTEXT(); + char *name; + + // Parse arguments + if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), +- "Os!|addbz", &object, redis_cluster_ce, &name, ++ "Os!|addbza", &object, redis_cluster_ce, &name, + &name_len, &z_seeds, &timeout, &read_timeout, +- &persistent, &z_auth) == FAILURE) ++ &persistent, &z_auth, &context) == FAILURE) + { + RETURN_FALSE; + } + +- // Require a name +- if (name_len == 0 && ZEND_NUM_ARGS() < 2) { +- CLUSTER_THROW_EXCEPTION("You must specify a name or pass seeds!", 0); +- } +- + /* If we've got a string try to load from INI */ + if (ZEND_NUM_ARGS() < 2) { +- redis_cluster_load(context, name, name_len); ++ if (name_len == 0) { // Require a name ++ CLUSTER_THROW_EXCEPTION("You must specify a name or pass seeds!", 0); ++ } ++ redis_cluster_load(c, name, name_len); + return; + } + + /* The normal case, loading from arguments */ + redis_extract_auth_info(z_auth, &user, &pass); +- redis_cluster_init(context, Z_ARRVAL_P(z_seeds), timeout, read_timeout, +- persistent, user, pass); ++ redis_cluster_init(c, Z_ARRVAL_P(z_seeds), timeout, read_timeout, ++ persistent, user, pass, context); + + if (user) zend_string_release(user); + if (pass) zend_string_release(pass); +diff --git a/redis_commands.c b/redis_commands.c +index 3b19194..d6bb2bc 100644 +--- a/redis_commands.c ++++ b/redis_commands.c +@@ -793,7 +793,7 @@ int redis_subscribe_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + zval *z_arr, *z_chan; + HashTable *ht_chan; + smart_string cmdstr = {0}; +- subscribeContext *sctx = emalloc(sizeof(subscribeContext)); ++ subscribeContext *sctx = ecalloc(1, sizeof(*sctx)); + size_t key_len; + int key_free; + char *key; +@@ -854,7 +854,7 @@ int redis_unsubscribe_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + zval *z_arr, *z_chan; + HashTable *ht_arr; + smart_string cmdstr = {0}; +- subscribeContext *sctx = emalloc(sizeof(subscribeContext)); ++ subscribeContext *sctx = ecalloc(1, sizeof(*sctx)); + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "a", &z_arr) == FAILURE) { + efree(sctx); +@@ -1299,6 +1299,34 @@ int redis_blocking_pop_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + * have specific processing (argument validation, etc) that make them unique + */ + ++/* Attempt to pull a long expiry from a zval. We're more restrictave than zval_get_long ++ * because that function will return integers from things like open file descriptors ++ * which should simply fail as a TTL */ ++static int redis_try_get_expiry(zval *zv, zend_long *lval) { ++ double dval; ++ ++ /* Success on an actual long or double */ ++ if (Z_TYPE_P(zv) == IS_LONG || Z_TYPE_P(zv) == IS_DOUBLE) { ++ *lval = zval_get_long(zv); ++ return SUCCESS; ++ } ++ ++ /* Automatically fail if we're not a string */ ++ if (Z_TYPE_P(zv) != IS_STRING) ++ return FAILURE; ++ ++ /* Attempt to get a long from the string */ ++ switch (is_numeric_string(Z_STRVAL_P(zv), Z_STRLEN_P(zv), lval, &dval, 0)) { ++ case IS_DOUBLE: ++ *lval = dval; ++ /* fallthrough */ ++ case IS_LONG: ++ return SUCCESS; ++ default: ++ return FAILURE; ++ } ++} ++ + /* SET */ + int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char **cmd, int *cmd_len, short *slot, void **ctx) +@@ -1306,7 +1334,8 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + smart_string cmdstr = {0}; + zval *z_value, *z_opts=NULL; + char *key = NULL, *exp_type = NULL, *set_type = NULL; +- long expire = -1, exp_set = 0, keep_ttl = 0; ++ long exp_set = 0, keep_ttl = 0; ++ zend_long expire = -1; + size_t key_len; + + // Make sure the function is being called correctly +@@ -1316,14 +1345,6 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + return FAILURE; + } + +- /* Our optional argument can either be a long (to support legacy SETEX */ +- /* redirection), or an array with Redis >= 2.6.12 set options */ +- if (z_opts && Z_TYPE_P(z_opts) != IS_LONG && Z_TYPE_P(z_opts) != IS_ARRAY +- && Z_TYPE_P(z_opts) != IS_NULL) +- { +- return FAILURE; +- } +- + // Check for an options array + if (z_opts && Z_TYPE_P(z_opts) == IS_ARRAY) { + HashTable *kt = Z_ARRVAL_P(z_opts); +@@ -1355,8 +1376,12 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + set_type = Z_STRVAL_P(v); + } + } ZEND_HASH_FOREACH_END(); +- } else if (z_opts && Z_TYPE_P(z_opts) == IS_LONG) { +- expire = Z_LVAL_P(z_opts); ++ } else if (z_opts && Z_TYPE_P(z_opts) != IS_NULL) { ++ if (redis_try_get_expiry(z_opts, &expire) == FAILURE) { ++ php_error_docref(NULL, E_WARNING, "Expire must be a long, double, or a numeric string"); ++ return FAILURE; ++ } ++ + exp_set = 1; + } + +@@ -1386,7 +1411,7 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + + if (exp_type) { + redis_cmd_append_sstr(&cmdstr, exp_type, strlen(exp_type)); +- redis_cmd_append_sstr_long(&cmdstr, expire); ++ redis_cmd_append_sstr_long(&cmdstr, (long)expire); + } + + if (set_type) +diff --git a/redis_session.c b/redis_session.c +index 6f2c5c5..4165fff 100644 +--- a/redis_session.c ++++ b/redis_session.c +@@ -854,7 +854,7 @@ static char *cluster_session_key(redisCluster *c, const char *key, int keylen, + + PS_OPEN_FUNC(rediscluster) { + redisCluster *c; +- zval z_conf, *zv; ++ zval z_conf, *zv, *context; + HashTable *ht_conf, *ht_seeds; + double timeout = 0, read_timeout = 0; + int persistent = 0, failover = REDIS_FAILOVER_NONE; +@@ -907,6 +907,7 @@ PS_OPEN_FUNC(rediscluster) { + + #define CLUSTER_SESSION_CLEANUP() \ + if (hash) zend_string_release(hash); \ ++ if (failstr) zend_string_release(failstr); \ + if (prefix) zend_string_release(prefix); \ + if (user) zend_string_release(user); \ + if (pass) zend_string_release(pass); \ +@@ -918,7 +919,6 @@ PS_OPEN_FUNC(rediscluster) { + if (seeds == NULL) { + php_error_docref(NULL, E_WARNING, "No valid seeds detected"); + CLUSTER_SESSION_CLEANUP(); +- zval_dtor(&z_conf); + return FAILURE; + } + +@@ -932,6 +932,10 @@ PS_OPEN_FUNC(rediscluster) { + + redis_sock_set_auth(c->flags, user, pass); + ++ if ((context = REDIS_HASH_STR_FIND_TYPE_STATIC(ht_conf, "stream", IS_ARRAY)) != NULL) { ++ redis_sock_set_stream_context(c->flags, context); ++ } ++ + /* First attempt to load from cache */ + if (CLUSTER_CACHING_ENABLED()) { + hash = cluster_hash_seeds(seeds, nseeds); +diff --git a/tests/RedisTest.php b/tests/RedisTest.php +index c7a403f..3c84885 100644 +--- a/tests/RedisTest.php ++++ b/tests/RedisTest.php +@@ -371,9 +371,14 @@ class Redis_Test extends TestSuite + $this->assertEquals($this->redis->get('foo'), 'bar'); + $this->assertEquals($this->redis->ttl('foo'), 20); + ++ /* Should coerce doubles into long */ ++ $this->assertTrue($this->redis->set('foo', 'bar-20.5', 20.5)); ++ $this->assertEquals($this->redis->ttl('foo'), 20); ++ $this->assertEquals($this->redis->get('foo'), 'bar-20.5'); ++ + /* Invalid third arguments */ +- $this->assertFalse($this->redis->set('foo','bar','baz')); +- $this->assertFalse($this->redis->set('foo','bar',new StdClass())); ++ $this->assertFalse(@$this->redis->set('foo','bar','baz')); ++ $this->assertFalse(@$this->redis->set('foo','bar',new StdClass())); + + /* Set if not exist */ + $this->redis->del('foo'); +@@ -6064,6 +6069,21 @@ class Redis_Test extends TestSuite + } + } + ++ /* Regression test for issue-1831 (XINFO STREAM on an empty stream) */ ++ public function testXInfoEmptyStream() { ++ /* Configure an empty stream */ ++ $this->redis->del('s'); ++ $this->redis->xAdd('s', '*', ['foo' => 'bar']); ++ $this->redis->xTrim('s', 0); ++ ++ $arr_info = $this->redis->xInfo('STREAM', 's'); ++ ++ $this->assertTrue(is_array($arr_info)); ++ $this->assertEquals(0, $arr_info['length']); ++ $this->assertEquals(NULL, $arr_info['first-entry']); ++ $this->assertEquals(NULL, $arr_info['last-entry']); ++ } ++ + public function testInvalidAuthArgs() { + $obj_new = $this->newInstance(); + +@@ -6082,9 +6102,15 @@ class Redis_Test extends TestSuite + + foreach ($arr_args as $arr_arg) { + try { +- @call_user_func_array([$obj_new, 'auth'], $arr_arg); ++ if (is_array($arr_arg)) { ++ @call_user_func_array([$obj_new, 'auth'], $arr_arg); ++ } else { ++ call_user_func([$obj_new, 'auth']); ++ } + } catch (Exception $ex) { + unset($ex); /* Suppress intellisense warning */ ++ } catch (ArgumentCountError $ex) { ++ unset($ex); /* Suppress intellisense warning */ + } + } + } +diff --git a/tests/startSession.php b/tests/startSession.php +index c0ae188..f82c17e 100644 +--- a/tests/startSession.php ++++ b/tests/startSession.php +@@ -22,11 +22,11 @@ ini_set('redis.session.lock_retries', $lock_retries); + ini_set('redis.session.lock_expire', $lock_expire); + ini_set('session.gc_maxlifetime', $sessionLifetime); + +-if (isset($argv[8])) { ++if (isset($argv[10])) { + ini_set('redis.session.locking_enabled', $argv[10]); + } + +-if (isset($argv[9])) { ++if (isset($argv[11])) { + ini_set('redis.session.lock_wait_time', $argv[11]); + } + -- cgit