mirror of
https://github.com/phpredis/phpredis.git
synced 2026-06-19 07:35:31 +00:00
2c5ef19257
* Introduce a new `RedisCmd` struct to dynamically append RESP arguments such that we don't have to precalculate the number of arguments the command will have up front. Additionally the new `RedisCmd allows both a `void *` context pointer but also can attach a `void (*ctx_dtor)(void*)` destructor so we are still able to clean up any allocated context when commands fail. This moves the context cleanup out of every individual reply handler and into the generic processing wrappers. * Create a small group of `resp_str` helper functions for lower level concatination of RESP protocol data over the wire. * Lots of small modernization of the codebase such as using `zend_string*` instead of (`char *`, `size_t`) pairs. * Greatly simplify `crosslot` handling logic
4875 lines
143 KiB
C
4875 lines
143 KiB
C
#include "php_redis.h"
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "common.h"
|
|
#include "php_network.h"
|
|
#include <sys/types.h>
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
|
|
#ifdef HAVE_REDIS_IGBINARY
|
|
#include "igbinary/igbinary.h"
|
|
#endif
|
|
#ifdef HAVE_REDIS_MSGPACK
|
|
#include "msgpack/php_msgpack.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_REDIS_LZF
|
|
#include <lzf.h>
|
|
|
|
#ifndef LZF_MARGIN
|
|
#define LZF_MARGIN 128
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef HAVE_REDIS_ZSTD
|
|
#include <zstd.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_REDIS_LZ4
|
|
#include <lz4.h>
|
|
#include <lz4hc.h>
|
|
|
|
/* uint8_t crf + int length */
|
|
#define REDIS_LZ4_HDR_SIZE (sizeof(uint8_t) + sizeof(int))
|
|
#if defined(LZ4HC_CLEVEL_MAX)
|
|
/* version >= 1.7.5 */
|
|
#define REDIS_LZ4_MAX_CLEVEL LZ4HC_CLEVEL_MAX
|
|
|
|
#elif defined (LZ4HC_MAX_CLEVEL)
|
|
/* version >= 1.7.3 */
|
|
#define REDIS_LZ4_MAX_CLEVEL LZ4HC_MAX_CLEVEL
|
|
|
|
#else
|
|
/* older versions */
|
|
#define REDIS_LZ4_MAX_CLEVEL 12
|
|
#endif
|
|
#endif
|
|
|
|
#include <zend_exceptions.h>
|
|
#include "php_redis.h"
|
|
#include "library.h"
|
|
#include "redis_commands.h"
|
|
|
|
#ifdef HAVE_REDIS_JSON
|
|
#include <ext/json/php_json.h>
|
|
#endif
|
|
|
|
#include <ext/hash/php_hash.h>
|
|
|
|
#if PHP_VERSION_ID < 80400
|
|
#include <ext/standard/php_rand.h>
|
|
#else
|
|
#include <ext/random/php_random.h>
|
|
#endif
|
|
|
|
#define UNSERIALIZE_NONE 0
|
|
#define UNSERIALIZE_KEYS 1
|
|
#define UNSERIALIZE_VALS 2
|
|
#define UNSERIALIZE_ALL 3
|
|
|
|
#define SCORE_DECODE_NONE 0
|
|
#define SCORE_DECODE_INT 1
|
|
#define SCORE_DECODE_DOUBLE 2
|
|
|
|
#define REDIS_CALLBACKS_INIT_SIZE 8
|
|
#define REDIS_CALLBACKS_MAX_DOUBLE 32768
|
|
#define REDIS_CALLBACKS_ADD_SIZE 4096
|
|
|
|
/* PhpRedis often returns either FALSE or NULL depending on whether we have
|
|
* an option set, so this macro just wraps that often repeated logic */
|
|
#define REDIS_ZVAL_NULL(sock_, zv_) \
|
|
do { \
|
|
if ((sock_)->null_mbulk_as_null) { \
|
|
ZVAL_NULL((zv_)); \
|
|
} else { \
|
|
ZVAL_FALSE((zv_)); \
|
|
} \
|
|
} while (0)
|
|
|
|
/** Set return value to false in case of we are in atomic mode or add FALSE to output array in pipeline mode */
|
|
#define REDIS_RESPONSE_ERROR(redis_sock, z_tab) \
|
|
do { \
|
|
if (IS_ATOMIC(redis_sock)) { \
|
|
RETVAL_FALSE; \
|
|
} else { \
|
|
add_next_index_bool(z_tab, 0); \
|
|
} \
|
|
} while (0)
|
|
|
|
/** Set return value to `zval` in case of we are in atomic mode or add `zval` to output array in pipeline mode */
|
|
#define REDIS_RETURN_ZVAL(redis_sock, z_tab, zval) \
|
|
do { \
|
|
if (IS_ATOMIC(redis_sock)) { \
|
|
/* Move value of `zval` to `return_value` */ \
|
|
ZVAL_COPY_VALUE(return_value, &zval); \
|
|
} else { \
|
|
zend_hash_next_index_insert_new(Z_ARRVAL_P(z_tab), &zval); \
|
|
} \
|
|
} while (0)
|
|
|
|
#ifndef PHP_WIN32
|
|
#include <netinet/tcp.h> /* TCP_NODELAY */
|
|
#include <sys/socket.h> /* SO_KEEPALIVE */
|
|
#else
|
|
#include <winsock.h>
|
|
#endif
|
|
|
|
extern zend_class_entry *redis_ce;
|
|
extern zend_class_entry *redis_exception_ce;
|
|
|
|
extern int le_redis_pconnect;
|
|
|
|
RedisCmdCtx redis_empty_ctx = {0};
|
|
|
|
static int redis_mbulk_reply_zipped_raw_variant(RedisSock *redis_sock, zval *zret, int count);
|
|
|
|
static int redis_bulk_resp_to_zval(RedisSock *redis_sock, zval *zdst,
|
|
int *dstlen) ;
|
|
|
|
static ConnectionPool *
|
|
redis_sock_get_connection_pool(RedisSock *redis_sock)
|
|
{
|
|
ConnectionPool *pool;
|
|
zend_resource *le;
|
|
zend_string *persistent_id;
|
|
|
|
/* Generate our unique pool id depending on configuration */
|
|
persistent_id = redis_pool_spprintf(redis_sock, INI_STR("redis.pconnect.pool_pattern"));
|
|
|
|
/* Return early if we can find the pool */
|
|
if ((le = zend_hash_find_ptr(&EG(persistent_list), persistent_id))) {
|
|
zend_string_release(persistent_id);
|
|
return le->ptr;
|
|
}
|
|
|
|
/* Create the pool and store it in our persistent list */
|
|
pool = pecalloc(1, sizeof(*pool), 1);
|
|
zend_llist_init(&pool->list, sizeof(php_stream *), NULL, 1);
|
|
zend_register_persistent_resource(ZSTR_VAL(persistent_id),
|
|
ZSTR_LEN(persistent_id),
|
|
pool, le_redis_pconnect);
|
|
|
|
zend_string_release(persistent_id);
|
|
|
|
return pool;
|
|
}
|
|
|
|
static int redis_sock_response_ok(RedisSock *redis_sock, char *buf, int buf_size) {
|
|
size_t len;
|
|
if (UNEXPECTED(redis_sock_gets(redis_sock, buf, buf_size - 1, &len) < 0)) {
|
|
return 0;
|
|
}
|
|
if (UNEXPECTED(redis_strncmp(buf, ZEND_STRL("+OK")))) {
|
|
if (buf[0] == '-') {
|
|
redis_sock_set_err(redis_sock, buf + 1, len - 1);
|
|
}
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* Helper to select the proper DB number */
|
|
static int redis_select_db(RedisSock *redis_sock) {
|
|
char response[4096];
|
|
RedisCmd *cmd;
|
|
|
|
cmd = redis_cmd_create_literal(redis_sock, "SELECT");
|
|
|
|
redis_cmd_cat_long(cmd, redis_sock->dbNumber);
|
|
|
|
if (redis_sock_write(redis_sock, redis_cmd_str(cmd), redis_cmd_len(cmd)) < 0) {
|
|
redis_cmd_free(cmd);
|
|
return -1;
|
|
}
|
|
|
|
redis_cmd_free(cmd);
|
|
|
|
if (!redis_sock_response_ok(redis_sock, response, sizeof(response))) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Append an AUTH command to a smart string if necessary. This will either
|
|
* append the new style AUTH <user> <password>, old style AUTH <password>, or
|
|
* append no command at all. Function returns 1 if we appended a command
|
|
* and 0 otherwise. */
|
|
static int redis_sock_append_auth(RedisSock *redis_sock, smart_str *str) {
|
|
int argc;
|
|
|
|
/* We need a password at least */
|
|
if (redis_sock->pass == NULL)
|
|
return 0;
|
|
|
|
argc = !!redis_sock->user + !!redis_sock->pass;
|
|
|
|
resp_str_cat_cmd_literal(str, "AUTH", argc);
|
|
|
|
if (redis_sock->user)
|
|
resp_str_cat_zstr(str, redis_sock->user);
|
|
|
|
resp_str_cat_zstr(str, redis_sock->pass);
|
|
|
|
/* We appended a command */
|
|
return 1;
|
|
}
|
|
|
|
PHP_REDIS_API void
|
|
redis_sock_set_auth(RedisSock *redis_sock, zend_string *user, zend_string *pass)
|
|
{
|
|
/* Release existing user/pass */
|
|
redis_sock_free_auth(redis_sock);
|
|
|
|
/* Set new user/pass */
|
|
redis_sock->user = user ? zend_string_copy(user) : NULL;
|
|
redis_sock->pass = pass ? zend_string_copy(pass) : NULL;
|
|
}
|
|
|
|
PHP_REDIS_API void
|
|
redis_sock_set_auth_zval(RedisSock *redis_sock, zval *zv) {
|
|
zend_string *user, *pass;
|
|
|
|
if (redis_extract_auth_info(zv, &user, &pass) == FAILURE)
|
|
return;
|
|
|
|
redis_sock_set_auth(redis_sock, user, pass);
|
|
|
|
if (user) zend_string_release(user);
|
|
if (pass) zend_string_release(pass);
|
|
}
|
|
|
|
PHP_REDIS_API void
|
|
redis_sock_free_auth(RedisSock *redis_sock) {
|
|
if (redis_sock->user) {
|
|
zend_string_release(redis_sock->user);
|
|
redis_sock->user = NULL;
|
|
}
|
|
|
|
if (redis_sock->pass) {
|
|
zend_string_release(redis_sock->pass);
|
|
redis_sock->pass = NULL;
|
|
}
|
|
}
|
|
|
|
#if PHP_VERSION_ID < 80000
|
|
static void redis_array_release(HashTable *ht) {
|
|
if (!ht || (GC_FLAGS(ht) & GC_IMMUTABLE)) {
|
|
return;
|
|
}
|
|
|
|
if (GC_DELREF(ht) == 0) {
|
|
zend_array_destroy(ht);
|
|
}
|
|
}
|
|
#else
|
|
static inline void redis_array_release(HashTable *ht) {
|
|
zend_array_release(ht);
|
|
}
|
|
#endif
|
|
|
|
|
|
void redis_sock_free_context(RedisSock *redis_sock) {
|
|
if (redis_sock->context == NULL)
|
|
return;
|
|
|
|
redis_array_release(redis_sock->context);
|
|
redis_sock->context = NULL;
|
|
}
|
|
|
|
PHP_REDIS_API zend_string *
|
|
redis_sock_auth_cmd(RedisSock *redis_sock) {
|
|
smart_str cmd = {0};
|
|
|
|
if (redis_sock_append_auth(redis_sock, &cmd) == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
return cmd.s;
|
|
}
|
|
|
|
/* Send Redis AUTH and process response */
|
|
PHP_REDIS_API int redis_sock_auth(RedisSock *redis_sock) {
|
|
zend_string *cmd;
|
|
char inbuf[4096];
|
|
|
|
if ((cmd = redis_sock_auth_cmd(redis_sock)) == NULL)
|
|
return SUCCESS;
|
|
|
|
if (redis_sock_write(redis_sock, ZSTR_VAL(cmd), ZSTR_LEN(cmd)) < 0) {
|
|
zend_string_release(cmd);
|
|
return FAILURE;
|
|
}
|
|
|
|
zend_string_release(cmd);
|
|
|
|
if (!redis_sock_response_ok(redis_sock, inbuf, sizeof(inbuf))) {
|
|
return FAILURE;
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/* Helper function and macro to test a RedisSock error prefix. */
|
|
#define REDIS_SOCK_ERRCMP_STATIC(rs, s) redis_sock_errcmp(rs, s, sizeof(s)-1)
|
|
static int redis_sock_errcmp(RedisSock *redis_sock, const char *err, size_t errlen) {
|
|
return ZSTR_LEN(redis_sock->err) >= errlen &&
|
|
memcmp(ZSTR_VAL(redis_sock->err), err, errlen) == 0;
|
|
}
|
|
|
|
/* Helper function that will throw an exception for a small number of ERR codes
|
|
* returned by Redis. Typically we just return FALSE to the caller in the event
|
|
* of an ERROR reply, but for the following error types:
|
|
* 1) MASTERDOWN
|
|
* 2) AUTH
|
|
* 3) LOADING
|
|
*/
|
|
static void
|
|
redis_error_throw(RedisSock *redis_sock)
|
|
{
|
|
/* Short circuit if we have no redis_sock or any error */
|
|
if (redis_sock == NULL || redis_sock->err == NULL)
|
|
return;
|
|
|
|
/* Redis 6 decided to add 'ERR AUTH' which has a normal 'ERR' prefix
|
|
* but is actually an authentication error that we will want to throw
|
|
* an exception for, so just short circuit if this is any other 'ERR'
|
|
* prefixed error. */
|
|
if (REDIS_SOCK_ERRCMP_STATIC(redis_sock, "ERR") &&
|
|
!REDIS_SOCK_ERRCMP_STATIC(redis_sock, "ERR AUTH")) return;
|
|
|
|
/* We may want to flip this logic and check for MASTERDOWN, AUTH,
|
|
* and LOADING but that may have side effects (esp for things like
|
|
* Disque) */
|
|
if (!REDIS_SOCK_ERRCMP_STATIC(redis_sock, "NOSCRIPT") &&
|
|
!REDIS_SOCK_ERRCMP_STATIC(redis_sock, "NOQUORUM") &&
|
|
!REDIS_SOCK_ERRCMP_STATIC(redis_sock, "NOGOODSLAVE") &&
|
|
!REDIS_SOCK_ERRCMP_STATIC(redis_sock, "WRONGTYPE") &&
|
|
!REDIS_SOCK_ERRCMP_STATIC(redis_sock, "BUSYGROUP") &&
|
|
!REDIS_SOCK_ERRCMP_STATIC(redis_sock, "NOGROUP"))
|
|
{
|
|
REDIS_THROW_EXCEPTION(ZSTR_VAL(redis_sock->err), 0);
|
|
}
|
|
}
|
|
|
|
static int
|
|
read_mbulk_header(RedisSock *redis_sock, int *nelem)
|
|
{
|
|
char line[4096];
|
|
size_t len;
|
|
|
|
/* Throws exception on failure */
|
|
if (redis_sock_gets(redis_sock, line, sizeof(line) - 1, &len) < 0) {
|
|
return FAILURE;
|
|
}
|
|
|
|
if (*line != TYPE_MULTIBULK) {
|
|
if (*line == TYPE_ERR) {
|
|
redis_sock_set_err(redis_sock, line + 1, len - 1);
|
|
}
|
|
return FAILURE;
|
|
}
|
|
|
|
{
|
|
char *endptr;
|
|
long long n;
|
|
|
|
errno = 0;
|
|
n = strtoll(line + 1, &endptr, 10);
|
|
if (endptr == line + 1 || errno == ERANGE || n < -1 || n > INT_MAX) {
|
|
return FAILURE;
|
|
}
|
|
*nelem = (int)n;
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_check_eof(RedisSock *redis_sock, zend_bool no_retry, zend_bool no_throw)
|
|
{
|
|
unsigned int retry_index;
|
|
char *errmsg;
|
|
|
|
if (!redis_sock || !redis_sock->stream || redis_sock->status == REDIS_SOCK_STATUS_FAILED) {
|
|
if (!no_throw) {
|
|
REDIS_THROW_EXCEPTION( "Connection closed", 0);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* NOITCE: set errno = 0 here
|
|
*
|
|
* There is a bug in php socket stream to check liveness of a connection:
|
|
* if (0 >= recv(sock->socket, &buf, sizeof(buf), MSG_PEEK) && php_socket_errno() != EWOULDBLOCK) {
|
|
* alive = 0;
|
|
* }
|
|
* If last errno is EWOULDBLOCK and recv returns 0 because of connection closed, alive would not be
|
|
* set to 0. However, the connection is close indeed. The php_stream_eof is not reliable. This will
|
|
* cause a "read error on connection" exception when use a closed persistent connection.
|
|
*
|
|
* We work around this by set errno = 0 first.
|
|
*
|
|
* Bug fix of php: https://github.com/php/php-src/pull/1456
|
|
* */
|
|
errno = 0;
|
|
if (php_stream_eof(redis_sock->stream) == 0) {
|
|
/* Success */
|
|
return 0;
|
|
} else if (redis_sock->mode == MULTI || redis_sock->watching) {
|
|
errmsg = "Connection lost and socket is in MULTI/watching mode";
|
|
} else {
|
|
errmsg = "Connection lost";
|
|
redis_backoff_reset(&redis_sock->backoff);
|
|
for (retry_index = 0; !no_retry && retry_index < redis_sock->max_retries; ++retry_index) {
|
|
/* close existing stream before reconnecting */
|
|
if (redis_sock->stream) {
|
|
/* reconnect no need to reset mode, it will cause pipeline mode socket exception */
|
|
redis_sock_disconnect(redis_sock, 1, 0);
|
|
}
|
|
/* Sleep based on our backoff algorithm */
|
|
zend_ulong delay = redis_backoff_compute(&redis_sock->backoff, retry_index);
|
|
if (delay != 0)
|
|
usleep(delay);
|
|
|
|
/* reconnect */
|
|
if (redis_sock_connect(redis_sock) == 0) {
|
|
/* check for EOF again. */
|
|
errno = 0;
|
|
if (php_stream_eof(redis_sock->stream) == 0) {
|
|
if (redis_sock_auth(redis_sock) != SUCCESS) {
|
|
errmsg = "AUTH failed while reconnecting";
|
|
break;
|
|
}
|
|
redis_sock->status = REDIS_SOCK_STATUS_AUTHENTICATED;
|
|
|
|
/* If we're using a non-zero db, reselect it */
|
|
if (redis_sock->dbNumber && redis_select_db(redis_sock) != 0) {
|
|
errmsg = "SELECT failed while reconnecting";
|
|
break;
|
|
}
|
|
redis_sock->status = REDIS_SOCK_STATUS_READY;
|
|
/* Success */
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* close stream and mark socket as failed */
|
|
redis_sock_disconnect(redis_sock, 1, 1);
|
|
redis_sock->status = REDIS_SOCK_STATUS_FAILED;
|
|
if (!no_throw) {
|
|
REDIS_THROW_EXCEPTION( errmsg, 0);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
REDIS_SCAN_TYPE type, uint64_t *cursor)
|
|
{
|
|
REDIS_REPLY_TYPE reply_type;
|
|
char err[4096], *p_iter;
|
|
RedisCmdCtx ctx = {0};
|
|
long reply_info;
|
|
size_t errlen;
|
|
|
|
/* Our response should have two multibulk replies */
|
|
if(redis_read_reply_type(redis_sock, &reply_type, &reply_info)<0
|
|
|| reply_type != TYPE_MULTIBULK || reply_info != 2)
|
|
{
|
|
if (reply_type == TYPE_ERR) {
|
|
if (redis_sock_gets(redis_sock, err, sizeof(err), &errlen) == 0) {
|
|
redis_sock_set_err(redis_sock, err, errlen);
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* The BULK response iterator */
|
|
if(redis_read_reply_type(redis_sock, &reply_type, &reply_info)<0
|
|
|| reply_type != TYPE_BULK)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
/* Attempt to read the iterator */
|
|
if(!(p_iter = redis_sock_read_bulk_reply(redis_sock, reply_info))) {
|
|
return -1;
|
|
}
|
|
|
|
/* Push the iterator out to the caller */
|
|
*cursor = strtoull(p_iter, NULL, 10);
|
|
efree(p_iter);
|
|
|
|
/* Read our actual keys/members/etc differently depending on what kind of
|
|
scan command this is. They all come back in slightly different ways */
|
|
switch(type) {
|
|
case TYPE_SCAN:
|
|
return redis_mbulk_reply_raw(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, NULL, ctx);
|
|
case TYPE_SSCAN:
|
|
return redis_sock_read_multibulk_reply(
|
|
INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, ctx);
|
|
case TYPE_ZSCAN:
|
|
return redis_mbulk_reply_zipped_keys_dbl(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, NULL, ctx);
|
|
case TYPE_HSCAN:
|
|
return redis_mbulk_reply_zipped_vals(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, NULL, ctx);
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_pubsub_response(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_long_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_read_variant_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 1) {
|
|
return redis_mbulk_reply_zipped_keys_int(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
static void
|
|
ht_free_subs(zval *data)
|
|
{
|
|
efree(Z_PTR_P(data));
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_subscribe_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
HashTable *subs;
|
|
subscribeCallback *cb;
|
|
subscribeContext *sctx = ctx.ptr;
|
|
zval *z_tmp, z_resp;
|
|
int i;
|
|
|
|
ALLOC_HASHTABLE(subs);
|
|
zend_hash_init(subs, 0, NULL, ht_free_subs, 0);
|
|
// Consume response(s) from subscribe, which will vary on argc
|
|
while(sctx->argc--) {
|
|
ZVAL_NULL(&z_resp);
|
|
if (!redis_sock_read_multibulk_reply_zval(redis_sock, &z_resp)) {
|
|
goto error;
|
|
}
|
|
|
|
// We'll need to find the command response
|
|
if ((z_tmp = zend_hash_index_find(Z_ARRVAL(z_resp), 0)) == NULL) {
|
|
goto error;
|
|
}
|
|
|
|
// Make sure the command response matches the command we called
|
|
if(strcasecmp(Z_STRVAL_P(z_tmp), sctx->kw) !=0) {
|
|
goto error;
|
|
}
|
|
|
|
if ((z_tmp = zend_hash_index_find(Z_ARRVAL(z_resp), 1)) == NULL) {
|
|
goto error;
|
|
}
|
|
|
|
zend_hash_str_update_mem(subs, Z_STRVAL_P(z_tmp), Z_STRLEN_P(z_tmp),
|
|
&sctx->cb, sizeof(sctx->cb));
|
|
|
|
zval_ptr_dtor_nogc(&z_resp);
|
|
}
|
|
|
|
if (strcasecmp(sctx->kw, "ssubscribe") == 0) {
|
|
i = REDIS_SSUBSCRIBE_IDX;
|
|
} else if (strcasecmp(sctx->kw, "psubscribe") == 0) {
|
|
i = REDIS_PSUBSCRIBE_IDX;
|
|
} else {
|
|
i = REDIS_SUBSCRIBE_IDX;
|
|
}
|
|
|
|
if (redis_sock->subs[i]) {
|
|
zend_string *zkey;
|
|
|
|
ZEND_HASH_FOREACH_STR_KEY_PTR(subs, zkey, cb) {
|
|
zend_hash_update_mem(redis_sock->subs[i], zkey, cb, sizeof(*cb));
|
|
} ZEND_HASH_FOREACH_END();
|
|
zend_hash_destroy(subs);
|
|
efree(subs);
|
|
|
|
RETVAL_TRUE;
|
|
return SUCCESS;
|
|
}
|
|
|
|
redis_sock->subs[i] = subs;
|
|
/* Multibulk response, {[pattern], type, channel, payload } */
|
|
while (redis_sock->subs[i]) {
|
|
zval z_ret, z_args[4], *z_type, *z_chan, *z_pat = NULL, *z_data;
|
|
int tab_idx = 1, is_pmsg = 0;
|
|
HashTable *ht_tab;
|
|
zend_string *zs;
|
|
|
|
ZVAL_NULL(&z_resp);
|
|
if (!redis_sock_read_multibulk_reply_zval(redis_sock, &z_resp)) {
|
|
goto failure;
|
|
}
|
|
|
|
ht_tab = Z_ARRVAL(z_resp);
|
|
|
|
if ((z_type = zend_hash_index_find(ht_tab, 0)) == NULL ||
|
|
Z_TYPE_P(z_type) != IS_STRING
|
|
) {
|
|
goto failure;
|
|
}
|
|
|
|
// Check for message or pmessage
|
|
if (zend_string_equals_literal_ci(Z_STR_P(z_type), "message") ||
|
|
zend_string_equals_literal_ci(Z_STR_P(z_type), "pmessage") ||
|
|
zend_string_equals_literal_ci(Z_STR_P(z_type), "smessage")
|
|
) {
|
|
is_pmsg = *Z_STRVAL_P(z_type)=='p';
|
|
} else {
|
|
zval_ptr_dtor_nogc(&z_resp);
|
|
continue;
|
|
}
|
|
|
|
// Extract pattern if it's a pmessage
|
|
if (is_pmsg) {
|
|
z_pat = zend_hash_index_find(ht_tab, tab_idx++);
|
|
if (z_pat == NULL || Z_TYPE_P(z_pat) != IS_STRING)
|
|
goto failure;
|
|
}
|
|
|
|
/* Extract channel */
|
|
z_chan = zend_hash_index_find(ht_tab, tab_idx++);
|
|
if (z_chan == NULL || Z_TYPE_P(z_chan) != IS_STRING)
|
|
goto failure;
|
|
|
|
/* Finally, extract data */
|
|
z_data = zend_hash_index_find(ht_tab, tab_idx++);
|
|
if (z_data == NULL)
|
|
goto failure;
|
|
|
|
/* Find our callback, either by channel or pattern string */
|
|
zs = z_pat != NULL ? Z_STR_P(z_pat) : Z_STR_P(z_chan);
|
|
if ((cb = zend_hash_find_ptr(redis_sock->subs[i], zs)) == NULL)
|
|
goto failure;
|
|
|
|
// Different args for SUBSCRIBE and PSUBSCRIBE
|
|
z_args[0] = *getThis();
|
|
if(is_pmsg) {
|
|
z_args[1] = *z_pat;
|
|
z_args[2] = *z_chan;
|
|
z_args[3] = *z_data;
|
|
} else {
|
|
z_args[1] = *z_chan;
|
|
z_args[2] = *z_data;
|
|
}
|
|
|
|
// Set arg count
|
|
cb->fci.param_count = tab_idx;
|
|
cb->fci.retval = &z_ret;
|
|
cb->fci.params = z_args;
|
|
|
|
// Execute callback
|
|
if (zend_call_function(&cb->fci, &cb->fci_cache) != SUCCESS) {
|
|
goto failure;
|
|
}
|
|
|
|
// If we have a return value free it
|
|
zval_ptr_dtor(&z_ret);
|
|
zval_ptr_dtor_nogc(&z_resp);
|
|
}
|
|
|
|
RETVAL_TRUE;
|
|
return SUCCESS;
|
|
|
|
// This is an error state, clean up
|
|
error:
|
|
zend_hash_destroy(subs);
|
|
efree(subs);
|
|
failure:
|
|
zval_ptr_dtor_nogc(&z_resp);
|
|
RETVAL_FALSE;
|
|
return FAILURE;
|
|
}
|
|
|
|
PHP_REDIS_API int redis_unsubscribe_response(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
subscribeContext *sctx = ctx.ptr;
|
|
zval *z_chan, z_ret, z_resp;
|
|
int i;
|
|
|
|
if (strcasecmp(sctx->kw, "sunsubscribe") == 0) {
|
|
i = REDIS_SSUBSCRIBE_IDX;
|
|
} else if (strcasecmp(sctx->kw, "punsubscribe") == 0) {
|
|
i = REDIS_PSUBSCRIBE_IDX;
|
|
} else {
|
|
i = REDIS_SUBSCRIBE_IDX;
|
|
}
|
|
if (!sctx->argc && redis_sock->subs[i]) {
|
|
sctx->argc = zend_hash_num_elements(redis_sock->subs[i]);
|
|
}
|
|
|
|
array_init(&z_ret);
|
|
|
|
while (sctx->argc--) {
|
|
ZVAL_NULL(&z_resp);
|
|
if (!redis_sock_read_multibulk_reply_zval(redis_sock, &z_resp) ||
|
|
(z_chan = zend_hash_index_find(Z_ARRVAL(z_resp), 1)) == NULL
|
|
) {
|
|
zval_ptr_dtor_nogc(&z_resp);
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
RETVAL_FALSE;
|
|
return FAILURE;
|
|
}
|
|
|
|
if (!redis_sock->subs[i] ||
|
|
!zend_hash_str_exists(redis_sock->subs[i], Z_STRVAL_P(z_chan), Z_STRLEN_P(z_chan))
|
|
) {
|
|
add_assoc_bool_ex(&z_ret, Z_STRVAL_P(z_chan), Z_STRLEN_P(z_chan), 0);
|
|
} else {
|
|
zend_hash_str_del(redis_sock->subs[i], Z_STRVAL_P(z_chan), Z_STRLEN_P(z_chan));
|
|
add_assoc_bool_ex(&z_ret, Z_STRVAL_P(z_chan), Z_STRLEN_P(z_chan), 1);
|
|
}
|
|
|
|
zval_ptr_dtor_nogc(&z_resp);
|
|
}
|
|
|
|
if (redis_sock->subs[i] && !zend_hash_num_elements(redis_sock->subs[i])) {
|
|
zend_hash_destroy(redis_sock->subs[i]);
|
|
efree(redis_sock->subs[i]);
|
|
redis_sock->subs[i] = NULL;
|
|
}
|
|
|
|
RETVAL_ZVAL(&z_ret, 0, 1);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API zval *
|
|
redis_sock_read_multibulk_reply_zval(RedisSock *redis_sock, zval *z_tab)
|
|
{
|
|
int numElems;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0) {
|
|
ZVAL_NULL(z_tab);
|
|
return NULL;
|
|
}
|
|
array_init_size(z_tab, numElems > 0 ? numElems : 0);
|
|
redis_mbulk_reply_loop(redis_sock, z_tab, numElems, UNSERIALIZE_ALL);
|
|
|
|
return z_tab;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_read_bulk_reply
|
|
*/
|
|
PHP_REDIS_API char *
|
|
redis_sock_read_bulk_reply(RedisSock *redis_sock, int bytes)
|
|
{
|
|
int offset = 0, nbytes;
|
|
char *reply;
|
|
ssize_t got;
|
|
|
|
if (bytes < -1 || bytes > INT_MAX - 2) {
|
|
zend_throw_exception_ex(redis_exception_ce, 0,
|
|
"protocol error, invalid bulk length: %d", bytes);
|
|
return NULL;
|
|
}
|
|
|
|
/* The caller reaches this only after successfully reading the bulk
|
|
* header on this socket, which already proved the stream live, so the
|
|
* php_stream_eof() liveness probe redis_check_eof() performs is
|
|
* redundant here. A mid-read disconnect is still caught by the read
|
|
* loop below. Keep a cheap NULL-stream guard for safety. */
|
|
if (-1 == bytes || redis_sock->stream == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
/* + 2 for \r\n */
|
|
nbytes = bytes + 2;
|
|
|
|
/* Allocate memory for string */
|
|
reply = emalloc(nbytes);
|
|
|
|
/* Consume bulk string */
|
|
while (offset < nbytes) {
|
|
got = redis_sock_read_raw(redis_sock, reply + offset, nbytes - offset);
|
|
if (got < 0 || (got == 0 && php_stream_eof(redis_sock->stream)))
|
|
break;
|
|
|
|
offset += got;
|
|
}
|
|
|
|
/* Protect against reading too few bytes */
|
|
if (offset < nbytes) {
|
|
/* Error or EOF */
|
|
REDIS_THROW_EXCEPTION("socket error on read socket", 0);
|
|
efree(reply);
|
|
return NULL;
|
|
}
|
|
|
|
/* Null terminate reply string */
|
|
reply[bytes] = '\0';
|
|
|
|
return reply;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_read
|
|
*/
|
|
PHP_REDIS_API char *
|
|
redis_sock_read(RedisSock *redis_sock, int *buf_len)
|
|
{
|
|
char inbuf[4096];
|
|
size_t len;
|
|
|
|
*buf_len = 0;
|
|
if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0 || len < 1) {
|
|
return NULL;
|
|
}
|
|
|
|
switch(inbuf[0]) {
|
|
case '-':
|
|
redis_sock_set_err(redis_sock, inbuf + 1, len - 1);
|
|
|
|
/* Filter our ERROR through the few that should actually throw */
|
|
redis_error_throw(redis_sock);
|
|
|
|
return NULL;
|
|
case '$':
|
|
{
|
|
char *endptr;
|
|
long long n;
|
|
|
|
errno = 0;
|
|
n = strtoll(inbuf + 1, &endptr, 10);
|
|
if (endptr == inbuf + 1 || errno == ERANGE || n < -1 || n > INT_MAX) {
|
|
return NULL;
|
|
}
|
|
*buf_len = (int)n;
|
|
}
|
|
return redis_sock_read_bulk_reply(redis_sock, *buf_len);
|
|
|
|
case '*':
|
|
/* For null multi-bulk replies (like timeouts from brpoplpush): */
|
|
if(len > 2 && memcmp(inbuf + 1, "-1", 2) == 0) {
|
|
return NULL;
|
|
}
|
|
REDIS_FALLTHROUGH;
|
|
case '+':
|
|
case ':':
|
|
/* Single Line Reply */
|
|
/* +OK or :123 */
|
|
if (len > 1) {
|
|
*buf_len = len;
|
|
return estrndup(inbuf, *buf_len);
|
|
}
|
|
REDIS_FALLTHROUGH;
|
|
default:
|
|
zend_throw_exception_ex(redis_exception_ce, 0,
|
|
"protocol error, got '%c' as reply type byte\n",
|
|
inbuf[0]
|
|
);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static zend_string *redis_hash_auth(zend_string *user, zend_string *pass) {
|
|
zend_string *algo, *hex;
|
|
smart_str salted = {0};
|
|
const php_hash_ops *ops;
|
|
unsigned char *digest;
|
|
void *ctx;
|
|
|
|
/* No op if there is not username/password */
|
|
if (user == NULL && pass == NULL)
|
|
return NULL;
|
|
|
|
/* Theoretically impossible but check anyway */
|
|
algo = zend_string_init("sha256", sizeof("sha256") - 1, 0);
|
|
if ((ops = redis_hash_fetch_ops(algo)) == NULL) {
|
|
zend_string_release(algo);
|
|
return NULL;
|
|
}
|
|
|
|
/* Hash username + password with our salt global */
|
|
smart_str_alloc(&salted, 256, 0);
|
|
if (user) smart_str_append_ex(&salted, user, 0);
|
|
if (pass) smart_str_append_ex(&salted, pass, 0);
|
|
smart_str_appendl_ex(&salted, REDIS_G(salt), sizeof(REDIS_G(salt)), 0);
|
|
|
|
ctx = emalloc(ops->context_size);
|
|
#if PHP_VERSION_ID >= 80100
|
|
ops->hash_init(ctx,NULL);
|
|
#else
|
|
ops->hash_init(ctx);
|
|
#endif
|
|
ops->hash_update(ctx, (const unsigned char *)ZSTR_VAL(salted.s), ZSTR_LEN(salted.s));
|
|
|
|
digest = emalloc(ops->digest_size);
|
|
ops->hash_final(digest, ctx);
|
|
efree(ctx);
|
|
|
|
hex = zend_string_safe_alloc(ops->digest_size, 2, 0, 0);
|
|
php_hash_bin2hex(ZSTR_VAL(hex), digest, ops->digest_size);
|
|
ZSTR_VAL(hex)[2 * ops->digest_size] = 0;
|
|
|
|
efree(digest);
|
|
zend_string_release(algo);
|
|
smart_str_free(&salted);
|
|
|
|
return hex;
|
|
}
|
|
|
|
static void append_auth_hash(smart_str *dst, zend_string *user, zend_string *pass) {
|
|
zend_string *s;
|
|
|
|
if ((s = redis_hash_auth(user, pass)) != NULL) {
|
|
smart_str_appendc(dst, ':');
|
|
smart_str_append_ex(dst, s, 0);
|
|
zend_string_release(s);
|
|
}
|
|
}
|
|
|
|
/* A printf like function to generate our connection pool hash value. */
|
|
PHP_REDIS_API zend_string *
|
|
redis_pool_spprintf(RedisSock *redis_sock, char *fmt, ...) {
|
|
smart_str str = {0};
|
|
|
|
smart_str_alloc(&str, 128, 0);
|
|
|
|
/* We always include phpredis_<host>:<port> */
|
|
smart_str_appendl(&str, "phpredis_", sizeof("phpredis_") - 1);
|
|
smart_str_append_ex(&str, redis_sock->host, 0);
|
|
smart_str_appendc(&str, ':');
|
|
smart_str_append_long(&str, (zend_long)redis_sock->port);
|
|
|
|
/* Short circuit if we don't have a pattern */
|
|
if (fmt == NULL) {
|
|
smart_str_0(&str);
|
|
return str.s;
|
|
}
|
|
|
|
while (*fmt) {
|
|
switch (*fmt) {
|
|
case 'i':
|
|
if (redis_sock->persistent_id) {
|
|
smart_str_appendc(&str, ':');
|
|
smart_str_append_ex(&str, redis_sock->persistent_id, 0);
|
|
}
|
|
break;
|
|
case 'u':
|
|
smart_str_appendc(&str, ':');
|
|
if (redis_sock->user) {
|
|
smart_str_append_ex(&str, redis_sock->user, 0);
|
|
}
|
|
break;
|
|
case 'p':
|
|
append_auth_hash(&str, NULL, redis_sock->pass);
|
|
break;
|
|
case 'a':
|
|
append_auth_hash(&str, redis_sock->user, redis_sock->pass);
|
|
break;
|
|
default:
|
|
/* Maybe issue a php_error_docref? */
|
|
break;
|
|
}
|
|
|
|
fmt++;
|
|
}
|
|
|
|
smart_str_0(&str);
|
|
return str.s;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_bulk_double_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
|
|
char *response;
|
|
int response_len;
|
|
double ret;
|
|
|
|
if ((response = redis_sock_read(redis_sock, &response_len)) == NULL) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
ret = atof(response);
|
|
efree(response);
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
RETVAL_DOUBLE(ret);
|
|
} else {
|
|
add_next_index_double(z_tab, ret);
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_type_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
char *response;
|
|
int response_len;
|
|
long l;
|
|
|
|
if ((response = redis_sock_read(redis_sock, &response_len)) == NULL) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
if (redis_strncmp(response, ZEND_STRL("+string")) == 0) {
|
|
l = REDIS_STRING;
|
|
} else if (redis_strncmp(response, ZEND_STRL("+set")) == 0){
|
|
l = REDIS_SET;
|
|
} else if (redis_strncmp(response, ZEND_STRL("+list")) == 0){
|
|
l = REDIS_LIST;
|
|
} else if (redis_strncmp(response, ZEND_STRL("+zset")) == 0){
|
|
l = REDIS_ZSET;
|
|
} else if (redis_strncmp(response, ZEND_STRL("+hash")) == 0){
|
|
l = REDIS_HASH;
|
|
} else if (redis_strncmp(response, ZEND_STRL("+stream")) == 0) {
|
|
l = REDIS_STREAM;
|
|
} else if (redis_strncmp(response, ZEND_STRL("+vectorset")) == 0) {
|
|
l = REDIS_VECTORSET;
|
|
} else {
|
|
l = REDIS_NOT_FOUND;
|
|
}
|
|
|
|
efree(response);
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
RETVAL_LONG(l);
|
|
} else {
|
|
add_next_index_long(z_tab, l);
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_config_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
FailableResultCallback cb = ctx.ptr;
|
|
|
|
ZEND_ASSERT(cb == redis_boolean_response ||
|
|
cb == redis_mbulk_reply_zipped_raw);
|
|
|
|
return cb(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, ctx);
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_zrange_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
FailableResultCallback cb;
|
|
|
|
/* Whether or not we have WITHSCORES */
|
|
ZEND_ASSERT(ctx.ptr == NULL || ctx.ptr == PHPREDIS_CTX_PTR);
|
|
|
|
cb = ctx.ptr ? redis_mbulk_reply_zipped_keys_dbl :
|
|
redis_sock_read_multibulk_reply;
|
|
|
|
return cb(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, ctx);
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_randmember_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
FailableResultCallback cb;
|
|
|
|
/* Whether or not we have a COUNT argument */
|
|
ZEND_ASSERT(ctx.ptr == NULL || ctx.ptr == PHPREDIS_CTX_PTR);
|
|
|
|
cb = ctx.ptr ? redis_sock_read_multibulk_reply : redis_string_response;
|
|
|
|
return cb(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, ctx);
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_info_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
char *response;
|
|
int response_len;
|
|
zval z_ret;
|
|
|
|
/* Read bulk response */
|
|
if ((response = redis_sock_read(redis_sock, &response_len)) == NULL) {
|
|
RETVAL_FALSE;
|
|
return FAILURE;
|
|
}
|
|
|
|
/* Parse it into a zval array */
|
|
ZVAL_UNDEF(&z_ret);
|
|
redis_parse_info_response(response, &z_ret);
|
|
|
|
/* Free source response */
|
|
efree(response);
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API void
|
|
redis_parse_info_response(char *response, zval *z_ret)
|
|
{
|
|
char *p1, *s1 = NULL;
|
|
|
|
ZVAL_FALSE(z_ret);
|
|
if ((p1 = php_strtok_r(response, _NL, &s1)) != NULL) {
|
|
array_init(z_ret);
|
|
do {
|
|
if (*p1 == '#') continue;
|
|
char *p;
|
|
zend_uchar type;
|
|
zend_long lval;
|
|
double dval;
|
|
if ((p = strchr(p1, ':')) != NULL) {
|
|
type = is_numeric_string(p + 1, strlen(p + 1), &lval, &dval, 0);
|
|
switch (type) {
|
|
case IS_LONG:
|
|
add_assoc_long_ex(z_ret, p1, p - p1, lval);
|
|
break;
|
|
case IS_DOUBLE:
|
|
if (dval < HUGE_VAL) {
|
|
add_assoc_double_ex(z_ret, p1, p - p1, dval);
|
|
break;
|
|
}
|
|
// fall through
|
|
default:
|
|
add_assoc_string_ex(z_ret, p1, p - p1, p + 1);
|
|
}
|
|
} else {
|
|
add_next_index_string(z_ret, p1);
|
|
}
|
|
} while ((p1 = php_strtok_r(NULL, _NL, &s1)) != NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
redis_parse_client_info(char *info, zval *z_ret)
|
|
{
|
|
char *p1, *s1 = NULL;
|
|
|
|
ZVAL_FALSE(z_ret);
|
|
if ((p1 = php_strtok_r(info, " ", &s1)) != NULL) {
|
|
array_init(z_ret);
|
|
do {
|
|
char *p;
|
|
zend_uchar type;
|
|
zend_long lval;
|
|
double dval;
|
|
if ((p = strchr(p1, '=')) != NULL) {
|
|
type = is_numeric_string(p + 1, strlen(p + 1), &lval, &dval, 0);
|
|
switch (type) {
|
|
case IS_LONG:
|
|
add_assoc_long_ex(z_ret, p1, p - p1, lval);
|
|
break;
|
|
case IS_DOUBLE:
|
|
add_assoc_double_ex(z_ret, p1, p - p1, dval);
|
|
break;
|
|
default:
|
|
add_assoc_string_ex(z_ret, p1, p - p1, p + 1);
|
|
}
|
|
} else {
|
|
add_next_index_string(z_ret, p1);
|
|
}
|
|
} while ((p1 = php_strtok_r(NULL, " ", &s1)) != NULL);
|
|
}
|
|
}
|
|
|
|
int
|
|
redis_client_info_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
char *resp;
|
|
int resp_len;
|
|
zval z_ret;
|
|
|
|
/* Make sure we can read the bulk response from Redis */
|
|
if ((resp = redis_sock_read(redis_sock, &resp_len)) == NULL) {
|
|
RETVAL_FALSE;
|
|
return FAILURE;
|
|
}
|
|
|
|
/* Parse it out */
|
|
redis_parse_client_info(resp, &z_ret);
|
|
|
|
/* Free our response */
|
|
efree(resp);
|
|
|
|
/* Return or append depending if we're atomic */
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/*
|
|
* Specialized handling of the CLIENT LIST output so it comes out in a simple way for PHP userland code
|
|
* to handle.
|
|
*/
|
|
PHP_REDIS_API int
|
|
redis_client_list_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
char *resp;
|
|
int resp_len;
|
|
zval z_ret;
|
|
|
|
/* Make sure we can read the bulk response from Redis */
|
|
if ((resp = redis_sock_read(redis_sock, &resp_len)) == NULL) {
|
|
RETVAL_FALSE;
|
|
return FAILURE;
|
|
} else if (resp_len > 0) {
|
|
/* Parse it out */
|
|
redis_parse_client_list_response(resp, &z_ret);
|
|
} else {
|
|
array_init(&z_ret);
|
|
}
|
|
|
|
/* Free our response */
|
|
efree(resp);
|
|
|
|
/* Return or append depending if we're atomic */
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API void
|
|
redis_parse_client_list_response(char *response, zval *z_ret)
|
|
{
|
|
char *p, *s = NULL;
|
|
|
|
ZVAL_FALSE(z_ret);
|
|
if ((p = php_strtok_r(response, _NL, &s)) != NULL) {
|
|
array_init(z_ret);
|
|
do {
|
|
zval z_sub;
|
|
redis_parse_client_info(p, &z_sub);
|
|
add_next_index_zval(z_ret, &z_sub);
|
|
} while ((p = php_strtok_r(NULL, _NL, &s)) != NULL);
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_zadd_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
FailableResultCallback cb;
|
|
|
|
ZEND_ASSERT(ctx.ptr == NULL || ctx.ptr == PHPREDIS_CTX_PTR);
|
|
|
|
cb = ctx.ptr ? redis_bulk_double_response : redis_long_response;
|
|
|
|
return cb(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, redis_empty_ctx);
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_zrandmember_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_mbulk_reply_raw(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 1) {
|
|
return redis_mbulk_reply_zipped_keys_dbl(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_zdiff_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_mbulk_reply_raw(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_mbulk_reply_zipped_keys_dbl(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_set_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_boolean_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_hrandfield_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_mbulk_reply_raw(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 1) {
|
|
return redis_mbulk_reply_zipped_raw(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_pop_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_sock_read_multibulk_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_object_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
ZEND_ASSERT(ctx.ptr == PHPREDIS_CTX_PTR || ctx.ptr == PHPREDIS_CTX_PTR + 1);
|
|
|
|
if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_long_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, redis_empty_ctx);
|
|
} else {
|
|
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_lpos_response(zval *zdst, RedisSock *redis_sock, char reply_type,
|
|
long long elements, RedisCmdCtx ctx)
|
|
{
|
|
char inbuf[4096];
|
|
size_t len;
|
|
int i;
|
|
|
|
if (ctx.ptr == NULL) {
|
|
if (reply_type != TYPE_INT && reply_type != TYPE_BULK)
|
|
return FAILURE;
|
|
|
|
if (elements > -1) {
|
|
ZVAL_LONG(zdst, elements);
|
|
} else {
|
|
REDIS_ZVAL_NULL(redis_sock, zdst);
|
|
}
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
if (reply_type != TYPE_MULTIBULK)
|
|
return FAILURE;
|
|
|
|
array_init_size(zdst, elements > 0 ? elements : 0);
|
|
|
|
for (i = 0; i < elements; ++i) {
|
|
if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf), &len) < 0) {
|
|
zval_ptr_dtor_nogc(zdst);
|
|
return FAILURE;
|
|
}
|
|
add_next_index_long(zdst, atol(inbuf + 1));
|
|
}
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
PHP_REDIS_API int
|
|
redis_lpos_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
char inbuf[1024] = {0};
|
|
int res = SUCCESS;
|
|
zval zdst = {0};
|
|
size_t len;
|
|
|
|
/* Attempt to read the LPOS response */
|
|
if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf), &len) < 0 ||
|
|
redis_read_lpos_response(&zdst, redis_sock, *inbuf, atoll(inbuf+1), ctx) < 0)
|
|
{
|
|
ZVAL_FALSE(&zdst);
|
|
res = FAILURE;
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, zdst);
|
|
|
|
return res;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_select_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (redis_boolean_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, redis_empty_ctx) < 0)
|
|
return FAILURE;
|
|
|
|
redis_sock->dbNumber = (long)(uintptr_t)ctx.ptr;
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_boolean_response_impl(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx,
|
|
SuccessCallback success_callback)
|
|
{
|
|
|
|
char *response;
|
|
int response_len;
|
|
zend_bool ret = 0;
|
|
|
|
if ((response = redis_sock_read(redis_sock, &response_len)) != NULL) {
|
|
ret = (*response == '+');
|
|
efree(response);
|
|
}
|
|
|
|
if (ret && success_callback != NULL) {
|
|
success_callback(redis_sock);
|
|
}
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
RETVAL_BOOL(ret);
|
|
} else {
|
|
add_next_index_bool(z_tab, ret);
|
|
}
|
|
|
|
return ret ? SUCCESS : FAILURE;
|
|
}
|
|
|
|
PHP_REDIS_API int redis_boolean_response(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
return redis_boolean_response_impl(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, redis_empty_ctx, NULL);
|
|
}
|
|
|
|
PHP_REDIS_API int redis_long_response(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval * z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
|
|
char *response;
|
|
int response_len;
|
|
|
|
if ((response = redis_sock_read(redis_sock, &response_len)) == NULL ||
|
|
*response != TYPE_INT)
|
|
{
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
if (response) efree(response);
|
|
return FAILURE;
|
|
}
|
|
|
|
int64_t ret = phpredis_atoi64(response + 1);
|
|
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
if (ret > LONG_MAX) { /* overflow */
|
|
RETVAL_STRINGL(response + 1, response_len - 1);
|
|
} else {
|
|
RETVAL_LONG((long)ret);
|
|
}
|
|
} else {
|
|
if (ret > LONG_MAX) { /* overflow */
|
|
add_next_index_stringl(z_tab, response + 1, response_len - 1);
|
|
} else {
|
|
add_next_index_long(z_tab, (long)ret);
|
|
}
|
|
}
|
|
|
|
efree(response);
|
|
return SUCCESS;
|
|
}
|
|
|
|
/* Helper method to convert [key, value, key, value] into [key => value,
|
|
* key => value] when returning data to the caller. Depending on our decode
|
|
* flag we'll convert the value data types */
|
|
static void array_zip_values_and_scores(RedisSock *redis_sock, zval *z_tab,
|
|
int decode)
|
|
{
|
|
|
|
HashTable *keytable = Z_ARRVAL_P(z_tab);
|
|
zend_string *hkey, *tmp;
|
|
zval z_ret, z_sub;
|
|
|
|
array_init_size(&z_ret, zend_hash_num_elements(keytable) / 2);
|
|
|
|
for(zend_hash_internal_pointer_reset(keytable);
|
|
zend_hash_has_more_elements(keytable) == SUCCESS;
|
|
zend_hash_move_forward(keytable)) {
|
|
|
|
zval *z_key_p, *z_value_p;
|
|
|
|
if ((z_key_p = zend_hash_get_current_data(keytable)) == NULL) {
|
|
continue; /* this should never happen, according to the PHP people. */
|
|
}
|
|
|
|
/* get current value, a key */
|
|
hkey = zval_get_tmp_string(z_key_p, &tmp);
|
|
|
|
/* move forward */
|
|
zend_hash_move_forward(keytable);
|
|
|
|
/* fetch again */
|
|
if ((z_value_p = zend_hash_get_current_data(keytable)) == NULL) {
|
|
zend_tmp_string_release(tmp);
|
|
continue; /* this should never happen, according to the PHP people. */
|
|
}
|
|
|
|
/* get current value, a hash value now. */
|
|
char *hval = Z_STRVAL_P(z_value_p);
|
|
|
|
/* Decode the score depending on flag */
|
|
if (decode == SCORE_DECODE_INT && Z_STRLEN_P(z_value_p) > 0) {
|
|
ZVAL_LONG(&z_sub, atoi(hval+1));
|
|
} else if (decode == SCORE_DECODE_DOUBLE) {
|
|
ZVAL_DOUBLE(&z_sub, atof(hval));
|
|
} else {
|
|
ZVAL_ZVAL(&z_sub, z_value_p, 1, 0);
|
|
}
|
|
|
|
zend_symtable_update(Z_ARRVAL_P(&z_ret), hkey, &z_sub);
|
|
|
|
zend_tmp_string_release(tmp);
|
|
}
|
|
|
|
/* replace */
|
|
zval_ptr_dtor_nogc(z_tab);
|
|
ZVAL_ZVAL(z_tab, &z_ret, 0, 0);
|
|
}
|
|
|
|
static int
|
|
array_zip_values_recursive(zval *z_tab)
|
|
{
|
|
zend_string *zkey;
|
|
zval z_ret, z_sub, *zv;
|
|
|
|
array_init(&z_ret);
|
|
for (zend_hash_internal_pointer_reset(Z_ARRVAL_P(z_tab));
|
|
zend_hash_has_more_elements(Z_ARRVAL_P(z_tab)) == SUCCESS;
|
|
zend_hash_move_forward(Z_ARRVAL_P(z_tab))
|
|
) {
|
|
if ((zv = zend_hash_get_current_data(Z_ARRVAL_P(z_tab))) == NULL) {
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
return FAILURE;
|
|
}
|
|
if (Z_TYPE_P(zv) == IS_STRING) {
|
|
zkey = zval_get_string(zv);
|
|
zend_hash_move_forward(Z_ARRVAL_P(z_tab));
|
|
if ((zv = zend_hash_get_current_data(Z_ARRVAL_P(z_tab))) == NULL) {
|
|
zend_string_release(zkey);
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
return FAILURE;
|
|
}
|
|
if (Z_TYPE_P(zv) == IS_ARRAY && array_zip_values_recursive(zv) != SUCCESS) {
|
|
zend_string_release(zkey);
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
return FAILURE;
|
|
}
|
|
ZVAL_ZVAL(&z_sub, zv, 1, 0);
|
|
add_assoc_zval_ex(&z_ret, ZSTR_VAL(zkey), ZSTR_LEN(zkey), &z_sub);
|
|
zend_string_release(zkey);
|
|
} else {
|
|
if (Z_TYPE_P(zv) == IS_ARRAY && array_zip_values_recursive(zv) != SUCCESS) {
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
return FAILURE;
|
|
}
|
|
ZVAL_ZVAL(&z_sub, zv, 1, 0);
|
|
add_next_index_zval(&z_ret, &z_sub);
|
|
}
|
|
}
|
|
zval_ptr_dtor_nogc(z_tab);
|
|
ZVAL_ZVAL(z_tab, &z_ret, 0, 0);
|
|
return SUCCESS;
|
|
}
|
|
|
|
static int
|
|
redis_mbulk_reply_zipped(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, int unserialize, int decode)
|
|
{
|
|
int numElems;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
zval z_multi_result;
|
|
|
|
if (numElems < 1) {
|
|
ZVAL_EMPTY_ARRAY(&z_multi_result);
|
|
} else {
|
|
array_init_size(&z_multi_result, numElems); /* pre-allocate array for multi's results. */
|
|
|
|
/* Grab our key, value, key, value array */
|
|
redis_mbulk_reply_loop(redis_sock, &z_multi_result, numElems, unserialize);
|
|
|
|
/* Zip keys and values */
|
|
array_zip_values_and_scores(redis_sock, &z_multi_result, decode);
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_multi_result);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
geosearch_cast(zval *zv)
|
|
{
|
|
if (Z_TYPE_P(zv) == IS_ARRAY) {
|
|
zend_hash_apply(Z_ARRVAL_P(zv), geosearch_cast);
|
|
} else if (Z_TYPE_P(zv) == IS_STRING) {
|
|
convert_to_double(zv);
|
|
}
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_mpop_response(RedisSock *redis_sock, zval *zdst, int elements,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
int subele, keylen;
|
|
zval zele = {0};
|
|
char *key;
|
|
|
|
ZEND_ASSERT(ctx.ptr == NULL || ctx.ptr == PHPREDIS_CTX_PTR);
|
|
|
|
if (elements < 0) {
|
|
REDIS_ZVAL_NULL(redis_sock, zdst);
|
|
return SUCCESS;
|
|
}
|
|
|
|
/* Invariant: We should have two elements */
|
|
ZEND_ASSERT(elements == 2);
|
|
|
|
array_init(zdst);
|
|
|
|
/* Key name and number of entries */
|
|
if ((key = redis_sock_read(redis_sock, &keylen)) == NULL ||
|
|
read_mbulk_header(redis_sock, &elements) < 0 || elements < 0)
|
|
{
|
|
if (key) efree(key);
|
|
goto fail;
|
|
}
|
|
|
|
add_next_index_stringl(zdst, key, keylen);
|
|
efree(key);
|
|
|
|
array_init_size(&zele, elements);
|
|
|
|
if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
int i;
|
|
for (i = 0; i < elements; i++) {
|
|
if (read_mbulk_header(redis_sock, &subele) < 0 || subele != 2) {
|
|
zval_ptr_dtor_nogc(&zele);
|
|
goto fail;
|
|
}
|
|
redis_mbulk_reply_loop(redis_sock, &zele, subele, UNSERIALIZE_KEYS);
|
|
}
|
|
|
|
array_zip_values_and_scores(redis_sock, &zele, SCORE_DECODE_DOUBLE);
|
|
} else {
|
|
redis_mbulk_reply_loop(redis_sock, &zele, elements, UNSERIALIZE_ALL);
|
|
}
|
|
|
|
add_next_index_zval(zdst, &zele);
|
|
|
|
return SUCCESS;
|
|
|
|
fail:
|
|
zval_ptr_dtor_nogc(zdst);
|
|
ZVAL_FALSE(zdst);
|
|
|
|
return FAILURE;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_mpop_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
int elements, res = SUCCESS;
|
|
zval zret = {0};
|
|
|
|
if (read_mbulk_header(redis_sock, &elements) == FAILURE ||
|
|
redis_read_mpop_response(redis_sock, &zret, elements, ctx) == FAILURE)
|
|
{
|
|
res = FAILURE;
|
|
ZVAL_FALSE(&zret);
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, zret);
|
|
|
|
return res;
|
|
}
|
|
|
|
#if PHP_VERSION_ID < 80200
|
|
static HashTable *zend_array_to_list(HashTable *arr) {
|
|
zval zret = {0}, *zv;
|
|
|
|
array_init_size(&zret, zend_hash_num_elements(arr));
|
|
|
|
ZEND_HASH_FOREACH_VAL(arr, zv) {
|
|
Z_TRY_ADDREF_P(zv);
|
|
add_next_index_zval(&zret, zv);
|
|
} ZEND_HASH_FOREACH_END();
|
|
|
|
return Z_ARRVAL(zret);
|
|
}
|
|
#endif
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_geosearch_response(zval *zdst, RedisSock *redis_sock,
|
|
long long elements, int with_aux_data)
|
|
{
|
|
zval z_multi_result, z_sub, *z_ele, *zv;
|
|
zend_string *zkey;
|
|
|
|
/* Handle the trivial "empty" result first */
|
|
if (elements < 0 && redis_sock->null_mbulk_as_null) {
|
|
ZVAL_NULL(zdst);
|
|
return SUCCESS;
|
|
}
|
|
|
|
array_init(zdst);
|
|
|
|
if (with_aux_data == 0) {
|
|
redis_mbulk_reply_loop(redis_sock, zdst, elements, UNSERIALIZE_NONE);
|
|
} else {
|
|
array_init(&z_multi_result);
|
|
|
|
redis_read_multibulk_recursive(redis_sock, elements, 0, &z_multi_result);
|
|
|
|
ZEND_HASH_FOREACH_VAL(Z_ARRVAL(z_multi_result), z_ele) {
|
|
// The first item in the sub-array is always the name of the returned item
|
|
zv = zend_hash_index_find(Z_ARRVAL_P(z_ele), 0);
|
|
zkey = zval_get_string(zv);
|
|
|
|
zend_hash_index_del(Z_ARRVAL_P(z_ele), 0);
|
|
|
|
// The other information is returned in the following order as successive
|
|
// elements of the sub-array: distance, geohash, coordinates
|
|
zend_hash_apply(Z_ARRVAL_P(z_ele), geosearch_cast);
|
|
|
|
// Reindex elements so they start at zero */
|
|
ZVAL_ARR(&z_sub, zend_array_to_list(Z_ARRVAL_P(z_ele)));
|
|
|
|
add_assoc_zval_ex(zdst, ZSTR_VAL(zkey), ZSTR_LEN(zkey), &z_sub);
|
|
zend_string_release(zkey);
|
|
} ZEND_HASH_FOREACH_END();
|
|
|
|
// Cleanup
|
|
zval_ptr_dtor_nogc(&z_multi_result);
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_geosearch_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval zret = {0};
|
|
int elements;
|
|
|
|
if (read_mbulk_header(redis_sock, &elements) < 0 ||
|
|
redis_read_geosearch_response(&zret, redis_sock, elements,
|
|
ctx.ptr != NULL) < 0)
|
|
{
|
|
ZVAL_FALSE(&zret);
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, zret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
int redis_client_trackinginfo_reply(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
int numElems;
|
|
zval z_ret;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
array_init_size(&z_ret, numElems > 0 ? numElems : 0);
|
|
redis_read_multibulk_recursive(redis_sock, numElems, 0, &z_ret);
|
|
array_zip_values_and_scores(redis_sock, &z_ret, 0);
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_client_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_client_info_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_client_list_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 1) {
|
|
return redis_boolean_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 2) {
|
|
return redis_long_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 3) {
|
|
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 4) {
|
|
return redis_client_trackinginfo_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
static int
|
|
redis_hello_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval z_ret, *zv;
|
|
int numElems;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0)
|
|
goto fail;
|
|
|
|
array_init_size(&z_ret, numElems > 0 ? numElems : 0);
|
|
|
|
if (redis_read_multibulk_recursive(redis_sock, numElems, 0, &z_ret) != SUCCESS ||
|
|
array_zip_values_recursive(&z_ret) != SUCCESS)
|
|
{
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
goto fail;
|
|
}
|
|
|
|
if (redis_sock->hello.server) {
|
|
zend_string_release(redis_sock->hello.server);
|
|
}
|
|
|
|
if ((zv = zend_hash_str_find(Z_ARRVAL(z_ret), ZEND_STRL("dragonfly_version")))) {
|
|
redis_sock->hello.server = zend_string_init(ZEND_STRL("dragonfly"), 0);
|
|
} else {
|
|
zv = zend_hash_str_find(Z_ARRVAL(z_ret), ZEND_STRL("server"));
|
|
redis_sock->hello.server = zv ? zval_get_string(zv) : ZSTR_EMPTY_ALLOC();
|
|
}
|
|
|
|
if (redis_sock->hello.version) {
|
|
zend_string_release(redis_sock->hello.version);
|
|
}
|
|
zv = zend_hash_str_find(Z_ARRVAL(z_ret), ZEND_STRL("version"));
|
|
redis_sock->hello.version = zv ? zval_get_string(zv) : ZSTR_EMPTY_ALLOC();
|
|
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
|
|
if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
ZVAL_STR_COPY(&z_ret, redis_sock->hello.server);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 1) {
|
|
ZVAL_STR_COPY(&z_ret, redis_sock->hello.version);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
RETVAL_ZVAL(&z_ret, 0, 1);
|
|
} else {
|
|
add_next_index_zval(z_tab, &z_ret);
|
|
}
|
|
|
|
return SUCCESS;
|
|
|
|
fail:
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
RETVAL_FALSE;
|
|
} else {
|
|
add_next_index_bool(z_tab, 0);
|
|
}
|
|
return FAILURE;
|
|
}
|
|
|
|
|
|
PHP_REDIS_API int
|
|
redis_hello_server_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
static RedisCmdCtx hello_ctx = { .ptr = PHPREDIS_CTX_PTR };
|
|
|
|
return redis_hello_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, hello_ctx);
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_hello_version_response(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
static RedisCmdCtx hello_ctx = { .ptr = PHPREDIS_CTX_PTR + 1 };
|
|
|
|
return redis_hello_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, hello_ctx);
|
|
}
|
|
|
|
int
|
|
redis_function_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
int numElems;
|
|
zval z_ret;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
array_init(&z_ret);
|
|
redis_read_multibulk_recursive(redis_sock, numElems, 0, &z_ret);
|
|
array_zip_values_recursive(&z_ret);
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_function_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_boolean_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 1) {
|
|
return redis_function_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
int
|
|
redis_command_info_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
int numElems;
|
|
zval z_ret;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
array_init(&z_ret);
|
|
redis_read_multibulk_recursive(redis_sock, numElems, 0, &z_ret);
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_command_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_command_info_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_long_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 1) {
|
|
return redis_mbulk_reply_raw(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 2) {
|
|
return redis_read_variant_reply_strings(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
/* Helper function to consume Redis stream message data. This is useful for
|
|
* multiple stream callers (e.g. XREAD[GROUP], and X[REV]RANGE handlers). */
|
|
PHP_REDIS_API int
|
|
redis_read_stream_messages(RedisSock *redis_sock, int count, zval *z_ret) {
|
|
zval z_message;
|
|
int i, mhdr, fields;
|
|
char *id = NULL;
|
|
int idlen;
|
|
|
|
/* Iterate over each message */
|
|
for (i = 0; i < count; i++) {
|
|
/* Consume inner multi-bulk header, message ID itself and finally
|
|
* 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 > 0 && fields % 2 != 0)))
|
|
{
|
|
if (id) efree(id);
|
|
return -1;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_xrange_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval z_messages;
|
|
int messages;
|
|
|
|
array_init(&z_messages);
|
|
|
|
if (read_mbulk_header(redis_sock, &messages) < 0 ||
|
|
redis_read_stream_messages(redis_sock, messages, &z_messages) < 0)
|
|
{
|
|
zval_ptr_dtor_nogc(&z_messages);
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return -1;
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_messages);
|
|
|
|
return 0;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_stream_messages_multi(RedisSock *redis_sock, int count,
|
|
zval *z_streams)
|
|
{
|
|
zval z_messages;
|
|
int i, shdr, messages;
|
|
char *id = NULL;
|
|
int idlen;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
if ((read_mbulk_header(redis_sock, &shdr) < 0 || shdr != 2) ||
|
|
(id = redis_sock_read(redis_sock, &idlen)) == NULL ||
|
|
read_mbulk_header(redis_sock, &messages) < 0)
|
|
{
|
|
if (id) efree(id);
|
|
return -1;
|
|
}
|
|
|
|
array_init(&z_messages);
|
|
|
|
if (redis_read_stream_messages(redis_sock, messages, &z_messages) < 0)
|
|
goto failure;
|
|
|
|
add_assoc_zval_ex(z_streams, id, idlen, &z_messages);
|
|
efree(id);
|
|
}
|
|
|
|
return 0;
|
|
failure:
|
|
efree(id);
|
|
zval_ptr_dtor_nogc(&z_messages);
|
|
return -1;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_xread_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval z_rv;
|
|
int streams;
|
|
|
|
if (read_mbulk_header(redis_sock, &streams) < 0)
|
|
goto failure;
|
|
|
|
if (streams == -1 && redis_sock->null_mbulk_as_null) {
|
|
ZVAL_NULL(&z_rv);
|
|
} else {
|
|
array_init(&z_rv);
|
|
if (redis_read_stream_messages_multi(redis_sock, streams, &z_rv) < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_rv);
|
|
return 0;
|
|
|
|
cleanup:
|
|
zval_ptr_dtor_nogc(&z_rv);
|
|
failure:
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return -1;
|
|
}
|
|
|
|
/* A helper method to read X[AUTO]CLAIM messages into an array. */
|
|
static int
|
|
redis_read_xclaim_ids(RedisSock *redis_sock, int count, zval *rv) {
|
|
zval z_msg;
|
|
REDIS_REPLY_TYPE type;
|
|
char *id = NULL;
|
|
int i, fields, idlen;
|
|
long li;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
id = NULL;
|
|
|
|
/* Consume inner reply type */
|
|
if (redis_read_reply_type(redis_sock, &type, &li) < 0 ||
|
|
(type != TYPE_BULK && type != TYPE_MULTIBULK) ||
|
|
(type == TYPE_BULK && li <= 0)) return -1;
|
|
|
|
/* TYPE_BULK is the JUSTID variant, otherwise it's standard xclaim response */
|
|
if (type == TYPE_BULK) {
|
|
if ((id = redis_sock_read_bulk_reply(redis_sock, (size_t)li)) == NULL)
|
|
return -1;
|
|
|
|
add_next_index_stringl(rv, id, li);
|
|
efree(id);
|
|
} else {
|
|
if ((li != 2 || (id = redis_sock_read(redis_sock, &idlen)) == NULL) ||
|
|
(read_mbulk_header(redis_sock, &fields) < 0 || fields % 2 != 0))
|
|
{
|
|
if (id) efree(id);
|
|
return -1;
|
|
}
|
|
|
|
array_init(&z_msg);
|
|
|
|
redis_mbulk_reply_loop(redis_sock, &z_msg, fields, UNSERIALIZE_VALS);
|
|
array_zip_values_and_scores(redis_sock, &z_msg, SCORE_DECODE_NONE);
|
|
add_assoc_zval_ex(rv, id, idlen, &z_msg);
|
|
efree(id);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Read an X[AUTO]CLAIM reply having already consumed the reply-type byte. */
|
|
PHP_REDIS_API int
|
|
redis_read_xclaim_reply(RedisSock *redis_sock, int count, int is_xautoclaim,
|
|
zval *rv)
|
|
{
|
|
REDIS_REPLY_TYPE type;
|
|
zval z_msgs = {0};
|
|
char *id = NULL;
|
|
long id_len = 0;
|
|
int messages = 0;
|
|
|
|
ZEND_ASSERT(!is_xautoclaim || (count == 2 || count == 3));
|
|
|
|
ZVAL_UNDEF(rv);
|
|
|
|
/* If this is XAUTOCLAIM consume the BULK ID and then the actual number of IDs.
|
|
* Otherwise, our 'count' argument is the number of IDs. */
|
|
if (is_xautoclaim) {
|
|
if (redis_read_reply_type(redis_sock, &type, &id_len) < 0 || type != TYPE_BULK)
|
|
goto failure;
|
|
if ((id = redis_sock_read_bulk_reply(redis_sock, id_len)) == NULL)
|
|
goto failure;
|
|
if (read_mbulk_header(redis_sock, &messages) < 0)
|
|
goto failure;
|
|
} else {
|
|
messages = count;
|
|
}
|
|
|
|
array_init(&z_msgs);
|
|
|
|
if (redis_read_xclaim_ids(redis_sock, messages, &z_msgs) < 0)
|
|
goto failure;
|
|
|
|
/* If XAUTOCLAIM we now need to consume the final array of message IDs */
|
|
if (is_xautoclaim) {
|
|
zval z_deleted = {0};
|
|
|
|
if (count == 3 && redis_sock_read_multibulk_reply_zval(redis_sock, &z_deleted) == NULL)
|
|
goto failure;
|
|
|
|
array_init(rv);
|
|
|
|
// Package up ID and message
|
|
add_next_index_stringl(rv, id, id_len);
|
|
add_next_index_zval(rv, &z_msgs);
|
|
|
|
// Add deleted messages if they exist
|
|
if (count == 3)
|
|
add_next_index_zval(rv, &z_deleted);
|
|
|
|
efree(id);
|
|
} else {
|
|
// We just want the messages
|
|
ZVAL_COPY_VALUE(rv, &z_msgs);
|
|
}
|
|
|
|
return 0;
|
|
|
|
failure:
|
|
zval_ptr_dtor_nogc(&z_msgs);
|
|
zval_ptr_dtor_nogc(rv);
|
|
if (id) efree(id);
|
|
|
|
return -1;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_xclaim_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval z_ret = {0};
|
|
int count;
|
|
|
|
ZEND_ASSERT(ctx.ptr == NULL || ctx.ptr == PHPREDIS_CTX_PTR);
|
|
|
|
if (read_mbulk_header(redis_sock, &count) < 0)
|
|
goto failure;
|
|
|
|
if (redis_read_xclaim_reply(redis_sock, count, ctx.ptr == PHPREDIS_CTX_PTR, &z_ret) < 0)
|
|
goto failure;
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return 0;
|
|
|
|
failure:
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return -1;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_xinfo_response(RedisSock *redis_sock, zval *z_ret, int elements)
|
|
{
|
|
zval zv;
|
|
int i, len = 0;
|
|
char *key = NULL, *data;
|
|
REDIS_REPLY_TYPE type;
|
|
long li;
|
|
|
|
for (i = 0; i < elements; ++i) {
|
|
if (redis_read_reply_type(redis_sock, &type, &li) < 0) {
|
|
goto failure;
|
|
}
|
|
switch (type) {
|
|
case TYPE_BULK:
|
|
if ((data = redis_sock_read_bulk_reply(redis_sock, li)) == NULL) {
|
|
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);
|
|
efree(key);
|
|
key = NULL;
|
|
} else {
|
|
key = data;
|
|
len = li;
|
|
}
|
|
break;
|
|
case TYPE_INT:
|
|
if (key) {
|
|
add_assoc_long_ex(z_ret, key, len, li);
|
|
efree(key);
|
|
key = NULL;
|
|
} else {
|
|
len = spprintf(&key, 0, "%ld", li);
|
|
}
|
|
break;
|
|
case TYPE_MULTIBULK:
|
|
array_init(&zv);
|
|
if (redis_read_xinfo_response(redis_sock, &zv, li) != SUCCESS) {
|
|
zval_ptr_dtor_nogc(&zv);
|
|
goto failure;
|
|
}
|
|
if (key) {
|
|
add_assoc_zval_ex(z_ret, key, len, &zv);
|
|
efree(key);
|
|
key = NULL;
|
|
} else {
|
|
add_next_index_zval(z_ret, &zv);
|
|
}
|
|
break;
|
|
default:
|
|
goto failure;
|
|
}
|
|
}
|
|
|
|
return SUCCESS;
|
|
|
|
failure:
|
|
if (key) efree(key);
|
|
return FAILURE;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_vinfo_response(RedisSock *redis_sock, zval *z_ret, long long count) {
|
|
char kbuf[256], vbuf[256];
|
|
REDIS_REPLY_TYPE type;
|
|
size_t klen, vlen;
|
|
long lval;
|
|
|
|
if (count < 0 || count % 2 != 0 || Z_TYPE_P(z_ret) != IS_ARRAY) {
|
|
zend_error_noreturn(E_ERROR, "Internal vinfo handler error");
|
|
}
|
|
|
|
for (long long i = 0; i < count; i += 2) {
|
|
if (redis_read_reply_type(redis_sock, &type, &lval) < 0 ||
|
|
type != TYPE_LINE ||
|
|
redis_sock_gets(redis_sock, kbuf, sizeof(kbuf), &klen) < 0)
|
|
{
|
|
return FAILURE;
|
|
}
|
|
|
|
if (redis_read_reply_type(redis_sock, &type, &lval) < 0) {
|
|
return FAILURE;
|
|
}
|
|
|
|
switch (type) {
|
|
case TYPE_LINE:
|
|
if (redis_sock_gets(redis_sock, vbuf, sizeof(vbuf), &vlen) < 0) {
|
|
return FAILURE;
|
|
}
|
|
add_assoc_stringl_ex(z_ret, kbuf, klen, vbuf, vlen);
|
|
break;
|
|
case TYPE_INT:
|
|
add_assoc_long_ex(z_ret, kbuf, klen, lval);
|
|
break;
|
|
default:
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
PHP_REDIS_API int
|
|
redis_vinfo_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval z_ret;
|
|
int count;
|
|
|
|
if (read_mbulk_header(redis_sock, &count) < 0 ||
|
|
count < 0 || count % 2 != 0)
|
|
{
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
array_init_size(&z_ret, count / 2);
|
|
|
|
if (redis_read_vinfo_response(redis_sock, &z_ret, count) != SUCCESS) {
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/* Unfortunately VEMB decided to use +string\r\n for the encoding when RAW is
|
|
* sent which PhpRedis will parse as `(true)` so we need a specific handler for
|
|
* it */
|
|
PHP_REDIS_API int
|
|
redis_read_vemb_response(RedisSock *redis_sock, zval *z_ret, long long count) {
|
|
REDIS_REPLY_TYPE type;
|
|
char kbuf[256], *str;
|
|
size_t klen;
|
|
double dval;
|
|
long tlen;
|
|
|
|
if (count < 0 || Z_TYPE_P(z_ret) != IS_ARRAY) {
|
|
zend_error_noreturn(E_ERROR, "Internal vemb handler error");
|
|
}
|
|
|
|
for (long long i = 0; i < count; i++) {
|
|
if (redis_read_reply_type(redis_sock, &type, &tlen) < 0) {
|
|
return FAILURE;
|
|
}
|
|
|
|
if (type == TYPE_LINE) {
|
|
if (redis_sock_gets(redis_sock, kbuf, sizeof(kbuf), &klen) < 0)
|
|
return FAILURE;
|
|
add_next_index_stringl(z_ret, kbuf, klen);
|
|
} else if (type == TYPE_BULK) {
|
|
if ((str = redis_sock_read_bulk_reply(redis_sock, tlen)) == NULL)
|
|
return FAILURE;
|
|
|
|
if (is_numeric_string(str, tlen, NULL, &dval, 0) == IS_DOUBLE) {
|
|
add_next_index_double(z_ret, dval);
|
|
} else {
|
|
add_next_index_stringl(z_ret, str, tlen);
|
|
}
|
|
|
|
efree(str);
|
|
} else {
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_vemb_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval z_ret;
|
|
int count;
|
|
|
|
if (read_mbulk_header(redis_sock, &count) < 0 || count < 1) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
array_init_size(&z_ret, count);
|
|
|
|
if (redis_read_vemb_response(redis_sock, &z_ret, count) != SUCCESS) {
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_vlinks_response(RedisSock *redis_sock, zval *z_ret,
|
|
long long elements, RedisCmdCtx ctx)
|
|
{
|
|
long long i;
|
|
zval z_ele;
|
|
int links;
|
|
|
|
array_init_size(z_ret, elements);
|
|
|
|
for (i = 0; i < elements; i++) {
|
|
if (read_mbulk_header(redis_sock, &links) < 0)
|
|
return FAILURE;
|
|
|
|
array_init(&z_ele);
|
|
redis_mbulk_reply_loop(redis_sock, &z_ele, links, UNSERIALIZE_KEYS);
|
|
|
|
if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
array_zip_values_and_scores(redis_sock, &z_ele, SCORE_DECODE_DOUBLE);
|
|
}
|
|
|
|
add_next_index_zval(z_ret, &z_ele);
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_vlinks_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
int elements;
|
|
zval z_ret;
|
|
|
|
ZVAL_FALSE(&z_ret);
|
|
|
|
if (read_mbulk_header(redis_sock, &elements) < 0)
|
|
goto fail;
|
|
|
|
/* TODO: Figure out if we want `false` or `[]` here */
|
|
if (elements < 0) {
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
return SUCCESS;
|
|
}
|
|
|
|
if (redis_read_vlinks_response(redis_sock, &z_ret, elements, ctx) != SUCCESS)
|
|
goto fail;
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
return SUCCESS;
|
|
|
|
fail:
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_deserialize_vgetattr_reply(zval *zret, const char *str, size_t len) {
|
|
#ifdef HAVE_REDIS_JSON
|
|
#if PHP_VERSION_ID < 80000
|
|
#define PHP_JSON_IN(s) ((char*)s)
|
|
#else
|
|
#define PHP_JSON_IN(s) (s)
|
|
#endif
|
|
|
|
if (php_json_decode(zret, PHP_JSON_IN(str), len, 1,
|
|
PHP_JSON_PARSER_DEFAULT_DEPTH) == FAILURE)
|
|
{
|
|
php_error_docref(NULL, E_WARNING, "Failed to deserialize attributes");
|
|
return FAILURE;
|
|
}
|
|
|
|
return SUCCESS;
|
|
|
|
#undef PHP_JSON_IN
|
|
#else
|
|
php_error_docref(NULL, E_WARNING,
|
|
"PhpRedis is not compiled with JSON support");
|
|
return FAILURE;
|
|
#endif
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_vgetattr_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval z_ret;
|
|
char *attr;
|
|
int len;
|
|
|
|
attr = redis_sock_read(redis_sock, &len);
|
|
if (attr == NULL) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
if (len == 0) {
|
|
ZVAL_FALSE(&z_ret);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
if (redis_deserialize_vgetattr_reply(&z_ret, attr, len) != SUCCESS) {
|
|
ZVAL_STRINGL(&z_ret, attr, len);
|
|
}
|
|
} else {
|
|
ZVAL_STRINGL(&z_ret, attr, len);
|
|
}
|
|
|
|
efree(attr);
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_xinfo_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval z_ret;
|
|
int elements;
|
|
|
|
if (read_mbulk_header(redis_sock, &elements) == SUCCESS) {
|
|
array_init(&z_ret);
|
|
if (redis_read_xinfo_response(redis_sock, &z_ret, elements) == SUCCESS) {
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
return SUCCESS;
|
|
}
|
|
zval_ptr_dtor_nogc(&z_ret);
|
|
}
|
|
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_acl_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
if (ctx.ptr == NULL) {
|
|
return redis_read_variant_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR) {
|
|
return redis_boolean_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 1) {
|
|
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 2) {
|
|
return redis_long_response(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 3) {
|
|
return redis_acl_getuser_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else if (ctx.ptr == PHPREDIS_CTX_PTR + 4) {
|
|
return redis_acl_log_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, redis_empty_ctx);
|
|
} else {
|
|
ZEND_ASSERT(!"memory corruption?");
|
|
return FAILURE;
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_acl_log_reply(RedisSock *redis_sock, zval *zret, long count) {
|
|
zval zsub;
|
|
int i, nsub;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
if (read_mbulk_header(redis_sock, &nsub) < 0 || nsub % 2 != 0)
|
|
return FAILURE;
|
|
|
|
array_init(&zsub);
|
|
if (redis_mbulk_reply_zipped_raw_variant(redis_sock, &zsub, nsub) == FAILURE)
|
|
return FAILURE;
|
|
|
|
add_next_index_zval(zret, &zsub);
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_acl_getuser_reply(RedisSock *redis_sock, zval *zret, long count) {
|
|
REDIS_REPLY_TYPE type;
|
|
zval zv;
|
|
char *key, *val;
|
|
long vlen;
|
|
int klen, i;
|
|
|
|
for (i = 0; i < count; i += 2) {
|
|
if (!(key = redis_sock_read(redis_sock, &klen)) ||
|
|
redis_read_reply_type(redis_sock, &type, &vlen) < 0 ||
|
|
(type != TYPE_BULK && type != TYPE_MULTIBULK) ||
|
|
vlen > INT_MAX)
|
|
{
|
|
if (key) efree(key);
|
|
return FAILURE;
|
|
}
|
|
|
|
if (type == TYPE_BULK) {
|
|
if (!(val = redis_sock_read_bulk_reply(redis_sock, (int)vlen)))
|
|
return FAILURE;
|
|
add_assoc_stringl_ex(zret, key, klen, val, vlen);
|
|
efree(val);
|
|
} else {
|
|
array_init(&zv);
|
|
redis_mbulk_reply_loop(redis_sock, &zv, (int)vlen, UNSERIALIZE_NONE);
|
|
add_assoc_zval_ex(zret, key, klen, &zv);
|
|
}
|
|
|
|
efree(key);
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
int redis_acl_custom_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx,
|
|
int (*cb)(RedisSock*, zval*, long))
|
|
{
|
|
REDIS_REPLY_TYPE type;
|
|
int res = FAILURE;
|
|
zval zret;
|
|
long len;
|
|
|
|
if (redis_read_reply_type(redis_sock, &type, &len) == 0 && type == TYPE_MULTIBULK) {
|
|
array_init(&zret);
|
|
|
|
res = cb(redis_sock, &zret, len);
|
|
if (res == FAILURE) {
|
|
zval_ptr_dtor_nogc(&zret);
|
|
ZVAL_FALSE(&zret);
|
|
}
|
|
} else {
|
|
ZVAL_FALSE(&zret);
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, zret);
|
|
|
|
return res;
|
|
}
|
|
|
|
int redis_acl_getuser_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
return redis_acl_custom_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, ctx, redis_read_acl_getuser_reply);
|
|
}
|
|
|
|
int redis_acl_log_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
return redis_acl_custom_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, ctx, redis_read_acl_log_reply);
|
|
}
|
|
|
|
/* Zipped key => value reply but we don't touch anything (e.g. CONFIG GET) */
|
|
PHP_REDIS_API int
|
|
redis_mbulk_reply_zipped_raw(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
return redis_mbulk_reply_zipped(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, UNSERIALIZE_NONE, SCORE_DECODE_NONE);
|
|
}
|
|
|
|
/* Zipped key => value reply unserializing keys and decoding the score as an integer (PUBSUB) */
|
|
PHP_REDIS_API int
|
|
redis_mbulk_reply_zipped_keys_int(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
return redis_mbulk_reply_zipped(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, UNSERIALIZE_KEYS,
|
|
SCORE_DECODE_INT);
|
|
}
|
|
|
|
/* Zipped key => value reply unserializing keys and decoding the score as a
|
|
* double (ZSET commands) */
|
|
PHP_REDIS_API int
|
|
redis_mbulk_reply_zipped_keys_dbl(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
return redis_mbulk_reply_zipped(INTERNAL_FUNCTION_PARAM_PASSTHRU,
|
|
redis_sock, z_tab, UNSERIALIZE_KEYS,
|
|
SCORE_DECODE_DOUBLE);
|
|
}
|
|
|
|
/* Zipped key => value reply where only the values are unserialized (e.g.
|
|
* HMGET) */
|
|
PHP_REDIS_API int
|
|
redis_mbulk_reply_zipped_vals(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
return redis_mbulk_reply_zipped(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
z_tab, UNSERIALIZE_VALS, SCORE_DECODE_NONE);
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_1_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
char *response;
|
|
int response_len;
|
|
zend_bool ret = 0;
|
|
|
|
if ((response = redis_sock_read(redis_sock, &response_len)) != NULL) {
|
|
ret = (response[1] == '1');
|
|
efree(response);
|
|
}
|
|
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
RETVAL_BOOL(ret);
|
|
} else {
|
|
add_next_index_bool(z_tab, ret);
|
|
}
|
|
|
|
return ret ? SUCCESS : FAILURE;
|
|
}
|
|
|
|
static int redis_bulk_resp_to_zval(RedisSock *redis_sock, zval *zdst, int *dstlen) {
|
|
char *resp;
|
|
int len;
|
|
|
|
resp = redis_sock_read(redis_sock, &len);
|
|
if (dstlen) *dstlen = len;
|
|
|
|
if (resp == NULL) {
|
|
ZVAL_FALSE(zdst);
|
|
return FAILURE;
|
|
}
|
|
|
|
redis_unpack(redis_sock, resp, len, zdst);
|
|
|
|
efree(resp);
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_string_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
zval zret;
|
|
int ret;
|
|
|
|
ret = redis_bulk_resp_to_zval(redis_sock, &zret, NULL);
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, zret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_bulk_withmeta_response(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
zval zret, zbulk;
|
|
int len, ret;
|
|
|
|
ret = redis_bulk_resp_to_zval(redis_sock, &zbulk, &len);
|
|
|
|
redis_with_metadata(&zret, &zbulk, len);
|
|
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
RETVAL_ZVAL(&zret, 0, 1);
|
|
} else {
|
|
add_next_index_zval(z_tab, &zret);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* like string response, but never unserialized. */
|
|
PHP_REDIS_API int
|
|
redis_ping_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
|
|
char *response;
|
|
int response_len;
|
|
|
|
if ((response = redis_sock_read(redis_sock, &response_len))
|
|
== NULL)
|
|
{
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
if (IS_ATOMIC(redis_sock)) {
|
|
RETVAL_STRINGL(response, response_len);
|
|
} else {
|
|
add_next_index_stringl(z_tab, response, response_len);
|
|
}
|
|
|
|
efree(response);
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_sock_configure(RedisSock *redis_sock, HashTable *opts)
|
|
{
|
|
zend_string *zkey;
|
|
zval *val;
|
|
|
|
ZEND_HASH_FOREACH_STR_KEY_VAL(opts, zkey, val) {
|
|
if (zkey == NULL) {
|
|
continue;
|
|
}
|
|
ZVAL_DEREF(val);
|
|
if (zend_string_equals_literal_ci(zkey, "host")) {
|
|
if (Z_TYPE_P(val) != IS_STRING) {
|
|
REDIS_VALUE_EXCEPTION("Invalid host");
|
|
return FAILURE;
|
|
}
|
|
if (redis_sock->host) zend_string_release(redis_sock->host);
|
|
redis_sock->host = zval_get_string(val);
|
|
} else if (zend_string_equals_literal_ci(zkey, "port")) {
|
|
if (Z_TYPE_P(val) != IS_LONG) {
|
|
REDIS_VALUE_EXCEPTION("Invalid port");
|
|
return FAILURE;
|
|
}
|
|
redis_sock->port = zval_get_long(val);
|
|
} else if (zend_string_equals_literal_ci(zkey, "connectTimeout")) {
|
|
if (Z_TYPE_P(val) != IS_LONG && Z_TYPE_P(val) != IS_DOUBLE) {
|
|
REDIS_VALUE_EXCEPTION("Invalid connect timeout");
|
|
return FAILURE;
|
|
}
|
|
redis_sock->timeout = zval_get_double(val);
|
|
} else if (zend_string_equals_literal_ci(zkey, "readTimeout")) {
|
|
if (Z_TYPE_P(val) != IS_LONG && Z_TYPE_P(val) != IS_DOUBLE) {
|
|
REDIS_VALUE_EXCEPTION("Invalid read timeout");
|
|
return FAILURE;
|
|
}
|
|
redis_sock->read_timeout = zval_get_double(val);
|
|
} else if (zend_string_equals_literal_ci(zkey, "persistent")) {
|
|
if (Z_TYPE_P(val) == IS_STRING) {
|
|
if (redis_sock->persistent_id) zend_string_release(redis_sock->persistent_id);
|
|
redis_sock->persistent_id = zval_get_string(val);
|
|
redis_sock->persistent = 1;
|
|
} else {
|
|
redis_sock->persistent = zend_is_true(val);
|
|
}
|
|
} else if (zend_string_equals_literal_ci(zkey, "maxRetries")) {
|
|
if (Z_TYPE_P(val) != IS_LONG || Z_LVAL_P(val) < 0) {
|
|
REDIS_VALUE_EXCEPTION("Max retries must be a non-negative integer");
|
|
return FAILURE;
|
|
}
|
|
redis_sock->max_retries = zval_get_long(val);
|
|
} else if (zend_string_equals_literal_ci(zkey, "retryInterval")) {
|
|
if (Z_TYPE_P(val) != IS_LONG && Z_TYPE_P(val) != IS_DOUBLE) {
|
|
REDIS_VALUE_EXCEPTION("Invalid retry interval");
|
|
return FAILURE;
|
|
}
|
|
redis_sock->retry_interval = zval_get_long(val);
|
|
} else if (zend_string_equals_literal_ci(zkey, "ssl")) {
|
|
if (redis_sock_set_context_zval(redis_sock, val) != SUCCESS) {
|
|
REDIS_VALUE_EXCEPTION("Invalid SSL context options");
|
|
return FAILURE;
|
|
}
|
|
} else if (zend_string_equals_literal_ci(zkey, "auth")) {
|
|
if (Z_TYPE_P(val) != IS_STRING && Z_TYPE_P(val) != IS_ARRAY) {
|
|
REDIS_VALUE_EXCEPTION("Invalid auth credentials");
|
|
return FAILURE;
|
|
}
|
|
redis_sock_set_auth_zval(redis_sock, val);
|
|
} else if (zend_string_equals_literal_ci(zkey, "database")) {
|
|
if (Z_TYPE_P(val) != IS_LONG || Z_LVAL_P(val) < 0 || Z_LVAL_P(val) > INT_MAX) {
|
|
REDIS_VALUE_EXCEPTION("Invalid database number");
|
|
return FAILURE;
|
|
}
|
|
redis_sock->dbNumber = Z_LVAL_P(val);
|
|
} else if (zend_string_equals_literal_ci(zkey, "backoff")) {
|
|
if (redis_sock_set_backoff(redis_sock, val) != SUCCESS) {
|
|
REDIS_VALUE_EXCEPTION("Invalid backoff options");
|
|
return FAILURE;
|
|
}
|
|
} else {
|
|
php_error_docref(NULL, E_WARNING, "Skip unknown option '%s'", ZSTR_VAL(zkey));
|
|
}
|
|
} ZEND_HASH_FOREACH_END();
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_create
|
|
*/
|
|
PHP_REDIS_API RedisSock*
|
|
redis_sock_create(RedisSockType type, const char *host, int host_len, int port,
|
|
double timeout, double read_timeout, int persistent,
|
|
const char *persistent_id, long retry_interval)
|
|
{
|
|
RedisSock *redis_sock;
|
|
|
|
redis_sock = ecalloc(1, sizeof(RedisSock));
|
|
redis_sock->type = type;
|
|
redis_sock->host = zend_string_init(host, host_len, 0);
|
|
redis_sock->status = REDIS_SOCK_STATUS_DISCONNECTED;
|
|
redis_sock->retry_interval = retry_interval * 1000;
|
|
redis_sock->max_retries = 10;
|
|
redis_initialize_backoff(&redis_sock->backoff, redis_sock->retry_interval);
|
|
redis_sock->persistent = persistent;
|
|
|
|
if (persistent && persistent_id != NULL) {
|
|
redis_sock->persistent_id = zend_string_init(persistent_id, strlen(persistent_id), 0);
|
|
}
|
|
|
|
redis_sock->port = port;
|
|
redis_sock->timeout = timeout;
|
|
redis_sock->read_timeout = read_timeout;
|
|
|
|
redis_sock->serializer = REDIS_SERIALIZER_NONE;
|
|
redis_sock->compression = REDIS_COMPRESSION_NONE;
|
|
redis_sock->mode = ATOMIC;
|
|
|
|
return redis_sock;
|
|
}
|
|
|
|
static int redis_uniqid(char *buf, size_t buflen) {
|
|
static unsigned long counter = 0;
|
|
struct timeval tv;
|
|
|
|
gettimeofday(&tv, NULL);
|
|
|
|
return snprintf(buf, buflen, "phpredis:%08lx%05lx:%08lx",
|
|
(long)tv.tv_sec, (long)tv.tv_usec, counter++);
|
|
}
|
|
|
|
static int redis_stream_liveness_check(php_stream *stream) {
|
|
return php_stream_set_option(stream, PHP_STREAM_OPTION_CHECK_LIVENESS,
|
|
0, NULL) == PHP_STREAM_OPTION_RETURN_OK ?
|
|
SUCCESS : FAILURE;
|
|
}
|
|
|
|
/* Try to get the underlying socket FD for use with poll/select.
|
|
* Returns -1 on failure. */
|
|
static php_socket_t redis_stream_fd_for_select(php_stream *stream) {
|
|
php_socket_t fd;
|
|
int flags;
|
|
|
|
flags = PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL;
|
|
if (php_stream_cast(stream, flags, (void*)&fd, 1) == FAILURE)
|
|
return -1;
|
|
|
|
return fd;
|
|
}
|
|
|
|
static int redis_detect_dirty_config(void) {
|
|
int val = INI_INT("redis.pconnect.pool_detect_dirty");
|
|
|
|
if (val >= 0 && val <= 2)
|
|
return val;
|
|
else if (val > 2)
|
|
return 2;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
static int redis_pool_poll_timeout(void) {
|
|
int val = INI_INT("redis.pconnect.pool_poll_timeout");
|
|
if (val >= 0)
|
|
return val;
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define REDIS_POLL_FD_SET(_pfd, _fd, _events) \
|
|
(_pfd).fd = _fd; (_pfd).events = _events; (_pfd).revents = 0
|
|
|
|
/* Try to determine if the socket is out of sync (has unconsumed replies) */
|
|
static int redis_stream_detect_dirty(php_stream *stream) {
|
|
php_socket_t fd;
|
|
php_pollfd pfd;
|
|
int rv, action;
|
|
|
|
/* Short circuit if this is disabled */
|
|
if ((action = redis_detect_dirty_config()) == 0)
|
|
return SUCCESS;
|
|
|
|
/* Seek past unconsumed bytes if we detect them */
|
|
if (stream->readpos < stream->writepos) {
|
|
redisDbgFmt("%s on unconsumed buffer (%ld < %ld)",
|
|
action > 1 ? "Aborting" : "Seeking",
|
|
(long)stream->readpos, (long)stream->writepos);
|
|
|
|
/* Abort if we are configured to immediately fail */
|
|
if (action == 1)
|
|
return FAILURE;
|
|
|
|
/* Seek to the end of buffered data */
|
|
zend_off_t offset = stream->writepos - stream->readpos;
|
|
if (php_stream_seek(stream, offset, SEEK_CUR) == FAILURE)
|
|
return FAILURE;
|
|
}
|
|
|
|
/* Get the underlying FD */
|
|
if ((fd = redis_stream_fd_for_select(stream)) == -1)
|
|
return FAILURE;
|
|
|
|
/* We want to detect a readable socket (it shouldn't be) */
|
|
REDIS_POLL_FD_SET(pfd, fd, PHP_POLLREADABLE);
|
|
rv = php_poll2(&pfd, 1, redis_pool_poll_timeout());
|
|
|
|
/* If we detect the socket is readable, it's dirty which is
|
|
* a failure. Otherwise as best we can tell it's good.
|
|
* TODO: We could attempt to consume up to N bytes */
|
|
redisDbgFmt("Detected %s socket", rv > 0 ? "readable" : "unreadable");
|
|
return rv == 0 ? SUCCESS : FAILURE;
|
|
}
|
|
|
|
static inline zend_bool
|
|
redis_check_echo_response(RedisSock *redis_sock, char *hdr, const char *id,
|
|
size_t idlen)
|
|
{
|
|
char buf[256] = {0};
|
|
size_t len;
|
|
|
|
/* Sentinel doesn't have ECHO but it will contain the ID in the error
|
|
* message so just check that */
|
|
if (redis_sock->sentinel) {
|
|
return redis_strncmp(hdr, ZEND_STRL("-ERR unknown command")) == 0 &&
|
|
strstr(hdr, id) != NULL;
|
|
}
|
|
|
|
/* Non-sentinel: Read and verify the ID */
|
|
return *hdr == TYPE_BULK && atoi(hdr + 1) == idlen &&
|
|
redis_sock_gets(redis_sock, buf, sizeof(buf) - 1, &len) == 0 &&
|
|
redis_strncmp(buf, id, idlen) == 0;
|
|
}
|
|
|
|
static int
|
|
redis_sock_check_liveness(RedisSock *redis_sock)
|
|
{
|
|
char id[64], inbuf[256] = {0};
|
|
smart_str cmd = {0};
|
|
int idlen, auth;
|
|
size_t len;
|
|
|
|
/* Short circuit if PHP detects the stream isn't live */
|
|
if (redis_stream_liveness_check(redis_sock->stream) != SUCCESS)
|
|
goto failure;
|
|
|
|
/* Short circuit if we detect the stream is "dirty", can't or are
|
|
configured not to try and fix it */
|
|
if (redis_stream_detect_dirty(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);
|
|
|
|
/* ECHO challenge/response */
|
|
idlen = redis_uniqid(id, sizeof(id));
|
|
resp_str_cat_cmd_literal(&cmd, "ECHO", 1);
|
|
|
|
resp_str_cat_str(&cmd, id, idlen);
|
|
|
|
/* Send command(s) and make sure we can consume reply(ies) */
|
|
if (redis_sock_write(redis_sock, ZSTR_VAL(cmd.s), ZSTR_LEN(cmd.s)) < 0) {
|
|
smart_str_free(&cmd);
|
|
goto failure;
|
|
}
|
|
|
|
smart_str_free(&cmd);
|
|
|
|
if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0) {
|
|
goto failure;
|
|
}
|
|
|
|
if (auth) {
|
|
if (redis_strncmp(inbuf, ZEND_STRL("+OK")) == 0 ||
|
|
redis_strncmp(inbuf, ZEND_STRL("-ERR Client sent AUTH")) == 0)
|
|
{
|
|
/* successfully authenticated or authentication isn't required */
|
|
if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0) {
|
|
goto failure;
|
|
}
|
|
} else if (redis_strncmp(inbuf, ZEND_STRL("-NOAUTH")) == 0) {
|
|
/* connection is fine but authentication failed, next command must
|
|
* fail too */
|
|
if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0
|
|
|| redis_strncmp(inbuf, ZEND_STRL("-NOAUTH")) != 0)
|
|
{
|
|
goto failure;
|
|
}
|
|
return SUCCESS;
|
|
} else {
|
|
goto failure;
|
|
}
|
|
redis_sock->status = REDIS_SOCK_STATUS_AUTHENTICATED;
|
|
} else {
|
|
if (redis_strncmp(inbuf, ZEND_STRL("-NOAUTH")) == 0) {
|
|
/* connection is fine but authentication required */
|
|
return SUCCESS;
|
|
}
|
|
}
|
|
|
|
if (redis_check_echo_response(redis_sock, inbuf, id, idlen)) {
|
|
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;
|
|
}
|
|
|
|
static php_stream_context *
|
|
alloc_stream_context(RedisSock *redis_sock) {
|
|
php_stream_context *ctx;
|
|
zend_string *zkey;
|
|
zval *z_ele;
|
|
|
|
if (redis_sock->context == NULL)
|
|
return NULL;
|
|
|
|
ctx = php_stream_context_alloc();
|
|
|
|
ZEND_HASH_FOREACH_STR_KEY_VAL(redis_sock->context, zkey, z_ele) {
|
|
if (zkey != NULL) {
|
|
php_stream_context_set_option(ctx, "ssl", ZSTR_VAL(zkey), z_ele);
|
|
}
|
|
} ZEND_HASH_FOREACH_END();
|
|
|
|
return ctx;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_connect
|
|
*/
|
|
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], scheme[8], *pos, *address;
|
|
const char *fmtstr = "%s://%s:%d";
|
|
int host_len, usocket = 0, err = 0, tcp_flag = 1;
|
|
ConnectionPool *p = NULL;
|
|
php_stream_context *ctx;
|
|
|
|
// Monotonically incrementing persistent id counter
|
|
static unsigned long long counter = 0;
|
|
|
|
if (redis_sock->stream != NULL) {
|
|
redis_sock_disconnect(redis_sock, 0, 1);
|
|
}
|
|
|
|
address = ZSTR_VAL(redis_sock->host);
|
|
if ((pos = strstr(address, "://")) == NULL) {
|
|
strcpy(scheme, redis_sock->context ? "ssl" : "tcp");
|
|
} else {
|
|
snprintf(scheme, sizeof(scheme), "%.*s", (int)(pos - address), address);
|
|
address = pos + sizeof("://") - 1;
|
|
}
|
|
if (address[0] == '/' && redis_sock->port < 1) {
|
|
host_len = snprintf(host, sizeof(host), "unix://%s", address);
|
|
usocket = 1;
|
|
} else {
|
|
if(redis_sock->port == 0)
|
|
redis_sock->port = 6379;
|
|
|
|
#ifdef HAVE_IPV6
|
|
/* If we've got IPv6 and find a colon in our address, convert to proper
|
|
* IPv6 [host]:port format */
|
|
if (strchr(address, ':') != NULL && strchr(address, '[') == NULL) {
|
|
fmtstr = "%s://[%s]:%d";
|
|
}
|
|
#endif
|
|
host_len = snprintf(host, sizeof(host), fmtstr, scheme, address, redis_sock->port);
|
|
}
|
|
|
|
if (redis_sock->persistent) {
|
|
if (INI_INT("redis.pconnect.pooling_enabled")) {
|
|
p = redis_sock_get_connection_pool(redis_sock);
|
|
if (zend_llist_count(&p->list) > 0) {
|
|
redis_sock->stream = *(php_stream **)zend_llist_get_last(&p->list);
|
|
zend_llist_remove_tail(&p->list);
|
|
|
|
if (redis_sock_check_liveness(redis_sock) == SUCCESS) {
|
|
return SUCCESS;
|
|
}
|
|
|
|
p->nb_active--;
|
|
}
|
|
|
|
int limit = INI_INT("redis.pconnect.connection_limit");
|
|
if (limit > 0 && p->nb_active >= limit) {
|
|
redis_sock_set_err(redis_sock, ZEND_STRL("Connection limit reached"));
|
|
return FAILURE;
|
|
}
|
|
|
|
gettimeofday(&tv, NULL);
|
|
persistent_id = strpprintf(0, "phpredis_%ld%ld:%llu", tv.tv_sec,
|
|
(long)tv.tv_usec, ++counter);
|
|
} else {
|
|
if (redis_sock->persistent_id) {
|
|
persistent_id = strpprintf(0, "phpredis:%s:%s", host, ZSTR_VAL(redis_sock->persistent_id));
|
|
} else {
|
|
persistent_id = strpprintf(0, "phpredis:%s:%f", host, redis_sock->timeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
tv.tv_sec = (time_t)redis_sock->timeout;
|
|
tv.tv_usec = (int)((redis_sock->timeout - tv.tv_sec) * 1000000);
|
|
if (tv.tv_sec != 0 || tv.tv_usec != 0) {
|
|
tv_ptr = &tv;
|
|
}
|
|
|
|
ctx = alloc_stream_context(redis_sock);
|
|
|
|
redis_sock->stream = php_stream_xport_create(host, host_len,
|
|
0, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT,
|
|
persistent_id ? ZSTR_VAL(persistent_id) : NULL,
|
|
tv_ptr, ctx, &estr, &err);
|
|
|
|
/* On successful connection, the context is moved into the stream so we
|
|
* can disown it. If the connection failed, clean it up ourselves. */
|
|
if (ctx && redis_sock->stream) {
|
|
ZEND_ASSERT(GC_REFCOUNT(ctx->res) == 2);
|
|
GC_DELREF(ctx->res);
|
|
} else if (ctx) {
|
|
ZEND_ASSERT(GC_REFCOUNT(ctx->res) == 1);
|
|
zend_list_delete(ctx->res);
|
|
}
|
|
|
|
if (persistent_id) {
|
|
zend_string_release(persistent_id);
|
|
}
|
|
|
|
if (!redis_sock->stream) {
|
|
if (estr) {
|
|
redis_sock_set_err(redis_sock, ZSTR_VAL(estr), ZSTR_LEN(estr));
|
|
zend_string_release(estr);
|
|
}
|
|
return FAILURE;
|
|
}
|
|
|
|
if (p) p->nb_active++;
|
|
|
|
/* Attempt to set TCP_NODELAY/TCP_KEEPALIVE if we're not using a unix socket. */
|
|
if (!usocket) {
|
|
php_netstream_data_t *sock = (php_netstream_data_t*)redis_sock->stream->abstract;
|
|
err = setsockopt(sock->socket, IPPROTO_TCP, TCP_NODELAY, (char*) &tcp_flag, sizeof(tcp_flag));
|
|
PHPREDIS_NOTUSED(err);
|
|
err = setsockopt(sock->socket, SOL_SOCKET, SO_KEEPALIVE, (char*) &redis_sock->tcp_keepalive, sizeof(redis_sock->tcp_keepalive));
|
|
PHPREDIS_NOTUSED(err);
|
|
}
|
|
|
|
php_stream_auto_cleanup(redis_sock->stream);
|
|
|
|
read_tv.tv_sec = (time_t)redis_sock->read_timeout;
|
|
read_tv.tv_usec = (int)((redis_sock->read_timeout - read_tv.tv_sec) * 1000000);
|
|
|
|
if (read_tv.tv_sec != 0 || read_tv.tv_usec != 0) {
|
|
php_stream_set_option(redis_sock->stream,PHP_STREAM_OPTION_READ_TIMEOUT,
|
|
0, &read_tv);
|
|
}
|
|
php_stream_set_option(redis_sock->stream,
|
|
PHP_STREAM_OPTION_WRITE_BUFFER, PHP_STREAM_BUFFER_NONE, NULL);
|
|
|
|
redis_sock->status = REDIS_SOCK_STATUS_CONNECTED;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_server_open
|
|
*/
|
|
PHP_REDIS_API int
|
|
redis_sock_server_open(RedisSock *redis_sock)
|
|
{
|
|
if (redis_sock) {
|
|
switch (redis_sock->status) {
|
|
case REDIS_SOCK_STATUS_DISCONNECTED:
|
|
if (redis_sock_connect(redis_sock) != SUCCESS) {
|
|
break;
|
|
}
|
|
redis_sock->status = REDIS_SOCK_STATUS_CONNECTED;
|
|
// fall through
|
|
case REDIS_SOCK_STATUS_CONNECTED:
|
|
if (redis_sock_auth(redis_sock) != SUCCESS) {
|
|
break;
|
|
}
|
|
redis_sock->status = REDIS_SOCK_STATUS_AUTHENTICATED;
|
|
// fall through
|
|
case REDIS_SOCK_STATUS_AUTHENTICATED:
|
|
if (redis_sock->dbNumber && redis_select_db(redis_sock) != SUCCESS) {
|
|
break;
|
|
}
|
|
redis_sock->status = REDIS_SOCK_STATUS_READY;
|
|
// fall through
|
|
case REDIS_SOCK_STATUS_READY:
|
|
return SUCCESS;
|
|
default:
|
|
return FAILURE;
|
|
}
|
|
}
|
|
return FAILURE;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_disconnect
|
|
*/
|
|
PHP_REDIS_API int
|
|
redis_sock_disconnect(RedisSock *redis_sock, int force, int is_reset_mode)
|
|
{
|
|
if (redis_sock == NULL) {
|
|
return FAILURE;
|
|
} else if (redis_sock->stream) {
|
|
if (redis_sock->persistent) {
|
|
ConnectionPool *p = NULL;
|
|
if (INI_INT("redis.pconnect.pooling_enabled")) {
|
|
p = redis_sock_get_connection_pool(redis_sock);
|
|
}
|
|
if (force || !IS_ATOMIC(redis_sock)) {
|
|
php_stream_pclose(redis_sock->stream);
|
|
redis_free_reply_callbacks(redis_sock);
|
|
if (p) p->nb_active--;
|
|
} else if (p) {
|
|
zend_llist_prepend_element(&p->list, &redis_sock->stream);
|
|
}
|
|
} else {
|
|
php_stream_close(redis_sock->stream);
|
|
}
|
|
redis_sock->stream = NULL;
|
|
}
|
|
if (is_reset_mode) {
|
|
redis_sock->mode = ATOMIC;
|
|
}
|
|
redis_sock->status = REDIS_SOCK_STATUS_DISCONNECTED;
|
|
redis_sock->watching = 0;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_set_err
|
|
*/
|
|
PHP_REDIS_API void
|
|
redis_sock_set_err(RedisSock *redis_sock, const char *msg, int msg_len)
|
|
{
|
|
redis_sock_clear_err(redis_sock);
|
|
|
|
if (msg != NULL && msg_len > 0) {
|
|
// Copy in our new error message
|
|
redis_sock->err = zend_string_init(msg, msg_len, 0);
|
|
}
|
|
}
|
|
|
|
PHP_REDIS_API void redis_sock_clear_err(RedisSock *redis_sock) {
|
|
if (redis_sock->err == NULL)
|
|
return;
|
|
|
|
zend_string_release(redis_sock->err);
|
|
redis_sock->err = NULL;
|
|
}
|
|
|
|
#if PHP_VERSION_ID < 80000
|
|
#define GC_TRY_ADDREF(ht) do { \
|
|
if (ht && !(GC_FLAGS(ht) & GC_IMMUTABLE)) { \
|
|
GC_ADDREF(ht); \
|
|
} \
|
|
} while(0)
|
|
#endif
|
|
|
|
|
|
void redis_sock_set_context(RedisSock *redis_sock, HashTable *ht) {
|
|
redis_sock_free_context(redis_sock);
|
|
|
|
if (ht == NULL)
|
|
return;
|
|
|
|
GC_TRY_ADDREF(ht);
|
|
redis_sock->context = ht;
|
|
}
|
|
|
|
int redis_sock_set_context_zval(RedisSock *redis_sock, zval *zv) {
|
|
HashTable *ht;
|
|
|
|
ht = zv && Z_TYPE_P(zv) == IS_ARRAY ? Z_ARRVAL_P(zv) : NULL;
|
|
|
|
redis_sock_set_context(redis_sock, ht);
|
|
|
|
return ht ? SUCCESS : FAILURE;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_sock_set_backoff(RedisSock *redis_sock, zval *options)
|
|
{
|
|
zend_string *zkey;
|
|
zend_long val;
|
|
zval *z_ele;
|
|
|
|
if (!redis_sock || Z_TYPE_P(options) != IS_ARRAY) {
|
|
return FAILURE;
|
|
}
|
|
|
|
ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(options), zkey, z_ele) {
|
|
if (zkey != NULL) {
|
|
ZVAL_DEREF(z_ele);
|
|
if (zend_string_equals_literal_ci(zkey, "algorithm")) {
|
|
if ((val = zval_get_long(z_ele)) < 0 || val >= REDIS_BACKOFF_ALGORITHMS) {
|
|
return FAILURE;
|
|
}
|
|
redis_sock->backoff.algorithm = val;
|
|
} else if (zend_string_equals_literal_ci(zkey, "base")) {
|
|
if ((val = zval_get_long(z_ele)) < 0) {
|
|
return FAILURE;
|
|
}
|
|
redis_sock->backoff.base = val * 1000;
|
|
} else if (zend_string_equals_literal_ci(zkey, "cap")) {
|
|
if ((val = zval_get_long(z_ele)) < 0) {
|
|
return FAILURE;
|
|
}
|
|
redis_sock->backoff.cap = val * 1000;
|
|
} else {
|
|
php_error_docref(NULL, E_WARNING, "Skip unknown backoff option '%s'", ZSTR_VAL(zkey));
|
|
}
|
|
}
|
|
} ZEND_HASH_FOREACH_END();
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_read_multibulk_reply
|
|
*/
|
|
PHP_REDIS_API int redis_sock_read_multibulk_reply(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
zval z_multi_result;
|
|
int numElems;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
if (numElems == -1 && redis_sock->null_mbulk_as_null) {
|
|
ZVAL_NULL(&z_multi_result);
|
|
} else if (numElems < 1) {
|
|
ZVAL_EMPTY_ARRAY(&z_multi_result);
|
|
} else {
|
|
array_init_size(&z_multi_result, numElems);
|
|
redis_mbulk_reply_loop(redis_sock, &z_multi_result, numElems, UNSERIALIZE_ALL);
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_multi_result);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Like multibulk reply, but don't touch the values, they won't be unserialized
|
|
* (this is used by HKEYS). */
|
|
PHP_REDIS_API int
|
|
redis_mbulk_reply_raw(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
int numElems;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
zval z_multi_result;
|
|
|
|
if (numElems < 1) {
|
|
ZVAL_EMPTY_ARRAY(&z_multi_result);
|
|
} else {
|
|
array_init_size(&z_multi_result, numElems); /* pre-allocate array for multi's results. */
|
|
redis_mbulk_reply_loop(redis_sock, &z_multi_result, numElems, UNSERIALIZE_NONE);
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_multi_result);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_mbulk_reply_double(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
char *line;
|
|
int i, numElems, len;
|
|
zval z_multi_result;
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0) {
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
if (numElems < 1) {
|
|
ZVAL_EMPTY_ARRAY(&z_multi_result);
|
|
} else {
|
|
array_init_size(&z_multi_result, numElems);
|
|
for (i = 0; i < numElems; ++i) {
|
|
if ((line = redis_sock_read(redis_sock, &len)) == NULL) {
|
|
add_next_index_bool(&z_multi_result, 0);
|
|
continue;
|
|
}
|
|
add_next_index_double(&z_multi_result, atof(line));
|
|
efree(line);
|
|
}
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_multi_result);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API void
|
|
redis_mbulk_reply_loop(RedisSock *redis_sock, zval *z_tab, int count,
|
|
int unserialize)
|
|
{
|
|
zval z_value;
|
|
char *line;
|
|
int i, len;
|
|
|
|
for (i = 0; i < count; ++i) {
|
|
if ((line = redis_sock_read(redis_sock, &len)) == NULL) {
|
|
add_next_index_bool(z_tab, 0);
|
|
continue;
|
|
}
|
|
|
|
/* We will attempt unserialization, if we're unserializing everything,
|
|
* or if we're unserializing keys and we're on a key, or we're
|
|
* unserializing values and we're on a value! */
|
|
int unwrap = (
|
|
(unserialize == UNSERIALIZE_ALL) ||
|
|
(unserialize == UNSERIALIZE_KEYS && i % 2 == 0) ||
|
|
(unserialize == UNSERIALIZE_VALS && i % 2 != 0)
|
|
);
|
|
|
|
if (unwrap) {
|
|
redis_unpack(redis_sock, line, len, &z_value);
|
|
} else {
|
|
ZVAL_STRINGL_FAST(&z_value, line, len);
|
|
}
|
|
zend_hash_next_index_insert_new(Z_ARRVAL_P(z_tab), &z_value);
|
|
|
|
efree(line);
|
|
}
|
|
}
|
|
|
|
static int
|
|
redis_mbulk_reply_zipped_raw_variant(RedisSock *redis_sock, zval *zret, int count) {
|
|
REDIS_REPLY_TYPE type;
|
|
char *key, *val;
|
|
int keylen, i;
|
|
zend_long lval;
|
|
double dval;
|
|
long vallen;
|
|
|
|
for (i = 0; i < count; i+= 2) {
|
|
/* Keys should always be bulk strings */
|
|
if ((key = redis_sock_read(redis_sock, &keylen)) == NULL)
|
|
return FAILURE;
|
|
|
|
/* This can vary */
|
|
if (redis_read_reply_type(redis_sock, &type, &vallen) < 0) {
|
|
efree(key);
|
|
return FAILURE;
|
|
}
|
|
|
|
if (type == TYPE_BULK) {
|
|
if (vallen > INT_MAX || (val = redis_sock_read_bulk_reply(redis_sock, (int)vallen)) == NULL) {
|
|
efree(key);
|
|
return FAILURE;
|
|
}
|
|
|
|
/* Possibly overkill, but provides really nice types */
|
|
switch (is_numeric_string(val, vallen, &lval, &dval, 0)) {
|
|
case IS_LONG:
|
|
add_assoc_long_ex(zret, key, keylen, lval);
|
|
break;
|
|
case IS_DOUBLE:
|
|
add_assoc_double_ex(zret, key, keylen, dval);
|
|
break;
|
|
default:
|
|
add_assoc_stringl_ex(zret, key, keylen, val, vallen);
|
|
}
|
|
|
|
efree(val);
|
|
} else if (type == TYPE_INT) {
|
|
add_assoc_long_ex(zret, key, keylen, (zend_long)vallen);
|
|
} else {
|
|
add_assoc_null_ex(zret, key, keylen);
|
|
}
|
|
|
|
efree(key);
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/* Specialized multibulk processing for HMGET where we need to pair requested
|
|
* keys with their returned values */
|
|
PHP_REDIS_API int
|
|
redis_mbulk_reply_assoc(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
HashTable *htctx = ctx.ptr;
|
|
int numElems;
|
|
zval *zfield;
|
|
char *rresp;
|
|
int len;
|
|
|
|
ZEND_ASSERT(htctx != NULL);
|
|
|
|
if (read_mbulk_header(redis_sock, &numElems) < 0 ||
|
|
zend_hash_num_elements(htctx) != numElems)
|
|
{
|
|
REDIS_RESPONSE_ERROR(redis_sock, z_tab);
|
|
return FAILURE;
|
|
}
|
|
|
|
zval z_multi_result, zunpacked;
|
|
|
|
GC_ADDREF(htctx);
|
|
|
|
ZEND_HASH_FOREACH_VAL(htctx, zfield) {
|
|
rresp = redis_sock_read(redis_sock, &len);
|
|
|
|
if (rresp != NULL) {
|
|
redis_unpack(redis_sock, rresp, len, &zunpacked);
|
|
efree(rresp);
|
|
} else {
|
|
ZVAL_FALSE(&zunpacked);
|
|
}
|
|
|
|
ZVAL_COPY_VALUE(zfield, &zunpacked);
|
|
} ZEND_HASH_FOREACH_END();
|
|
|
|
ZVAL_ARR(&z_multi_result, htctx);
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_multi_result);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* redis_sock_write
|
|
*/
|
|
PHP_REDIS_API int
|
|
redis_sock_write(RedisSock *redis_sock, const char *cmd, size_t sz)
|
|
{
|
|
if (redis_check_eof(redis_sock, 0, 0) == 0 &&
|
|
redis_sock_write_raw(redis_sock, cmd, sz) == sz)
|
|
{
|
|
return sz;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* Grow array to double size if we need more space */
|
|
fold_item*
|
|
redis_add_reply_callback(RedisSock *redis_sock) {
|
|
if (UNEXPECTED(redis_sock->reply_callback_count == redis_sock->reply_callback_capacity)) {
|
|
if (redis_sock->reply_callback_capacity == 0) {
|
|
redis_sock->reply_callback_capacity = REDIS_CALLBACKS_INIT_SIZE;
|
|
} else if (redis_sock->reply_callback_capacity < REDIS_CALLBACKS_MAX_DOUBLE) {
|
|
redis_sock->reply_callback_capacity *= 2;
|
|
} else {
|
|
redis_sock->reply_callback_capacity += REDIS_CALLBACKS_ADD_SIZE;
|
|
}
|
|
redis_sock->reply_callback = erealloc(redis_sock->reply_callback, redis_sock->reply_callback_capacity * sizeof(fold_item));
|
|
}
|
|
return &redis_sock->reply_callback[redis_sock->reply_callback_count++];
|
|
}
|
|
|
|
void
|
|
redis_free_reply_callbacks(RedisSock *redis_sock)
|
|
{
|
|
if (redis_sock->reply_callback != NULL) {
|
|
size_t i;
|
|
|
|
for (i = 0; i < redis_sock->reply_callback_count; i++) {
|
|
redis_cmd_ctx_free(redis_sock->reply_callback[i].ctx);
|
|
}
|
|
|
|
efree(redis_sock->reply_callback);
|
|
redis_sock->reply_callback = NULL;
|
|
redis_sock->reply_callback_count = 0;
|
|
redis_sock->reply_callback_capacity = 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
redis_sock_release_hello(struct RedisHello *hello) {
|
|
if (hello->server) {
|
|
zend_string_release(hello->server);
|
|
hello->server = NULL;
|
|
}
|
|
|
|
if (hello->version) {
|
|
zend_string_release(hello->version);
|
|
hello->version = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* redis_free_socket
|
|
*/
|
|
PHP_REDIS_API void redis_free_socket(RedisSock *redis_sock)
|
|
{
|
|
int i;
|
|
|
|
if (redis_sock->prefix) {
|
|
zend_string_release(redis_sock->prefix);
|
|
}
|
|
smart_string_free(&redis_sock->pipeline_cmd);
|
|
if (redis_sock->err) {
|
|
zend_string_release(redis_sock->err);
|
|
}
|
|
if (redis_sock->persistent_id) {
|
|
zend_string_release(redis_sock->persistent_id);
|
|
}
|
|
if (redis_sock->host) {
|
|
zend_string_release(redis_sock->host);
|
|
}
|
|
for (i = 0; i < REDIS_SUBS_BUCKETS; ++i) {
|
|
if (redis_sock->subs[i]) {
|
|
zend_hash_destroy(redis_sock->subs[i]);
|
|
efree(redis_sock->subs[i]);
|
|
redis_sock->subs[i] = NULL;
|
|
}
|
|
}
|
|
redis_sock_free_auth(redis_sock);
|
|
redis_free_reply_callbacks(redis_sock);
|
|
redis_sock_release_hello(&redis_sock->hello);
|
|
redis_sock_free_context(redis_sock);
|
|
|
|
efree(redis_sock);
|
|
}
|
|
|
|
#ifdef HAVE_REDIS_LZ4
|
|
/* Implementation of CRC8 for our LZ4 checksum value */
|
|
static uint8_t crc8(unsigned char *input, size_t len) {
|
|
size_t i;
|
|
uint8_t crc = 0xFF;
|
|
|
|
while (len--) {
|
|
crc ^= *input++;
|
|
for (i = 0; i < 8; i++) {
|
|
if (crc & 0x80)
|
|
crc = (uint8_t)(crc << 1) ^ 0x31;
|
|
else
|
|
crc <<= 1;
|
|
}
|
|
}
|
|
|
|
return crc;
|
|
}
|
|
#endif
|
|
|
|
PHP_REDIS_API int
|
|
redis_compress(RedisSock *redis_sock, char **dst, size_t *dstlen, char *buf, size_t len) {
|
|
switch (redis_sock->compression) {
|
|
case REDIS_COMPRESSION_LZF:
|
|
#ifdef HAVE_REDIS_LZF
|
|
{
|
|
char *data;
|
|
uint32_t res;
|
|
double size;
|
|
|
|
/* preserve compatibility with PECL lzf_compress margin (greater of 4% and LZF_MARGIN) */
|
|
size = len + MIN(UINT_MAX - len, MAX(LZF_MARGIN, len / 25));
|
|
data = emalloc(size);
|
|
if ((res = lzf_compress(buf, len, data, size)) > 0) {
|
|
*dst = data;
|
|
*dstlen = res;
|
|
return 1;
|
|
}
|
|
efree(data);
|
|
}
|
|
#endif
|
|
break;
|
|
case REDIS_COMPRESSION_ZSTD:
|
|
#ifdef HAVE_REDIS_ZSTD
|
|
{
|
|
char *data;
|
|
size_t size;
|
|
int level;
|
|
|
|
if (redis_sock->compression_level < 1) {
|
|
#ifdef ZSTD_CLEVEL_DEFAULT
|
|
level = ZSTD_CLEVEL_DEFAULT;
|
|
#else
|
|
level = 3;
|
|
#endif
|
|
} else if (redis_sock->compression_level > ZSTD_maxCLevel()) {
|
|
level = ZSTD_maxCLevel();
|
|
} else {
|
|
level = redis_sock->compression_level;
|
|
}
|
|
|
|
size = ZSTD_compressBound(len);
|
|
data = emalloc(size);
|
|
size = ZSTD_compress(data, size, buf, len, level);
|
|
if (!ZSTD_isError(size)) {
|
|
*dst = erealloc(data, size);
|
|
*dstlen = size;
|
|
return 1;
|
|
}
|
|
efree(data);
|
|
}
|
|
#endif
|
|
break;
|
|
case REDIS_COMPRESSION_LZ4:
|
|
#ifdef HAVE_REDIS_LZ4
|
|
{
|
|
/* Compressing empty data is pointless */
|
|
if (len < 1)
|
|
break;
|
|
|
|
/* Compressing more than INT_MAX bytes would require multiple blocks */
|
|
if (len > INT_MAX) {
|
|
php_error_docref(NULL, E_WARNING,
|
|
"LZ4: compressing > %d bytes not supported", INT_MAX);
|
|
break;
|
|
}
|
|
|
|
int old_len = len, lz4len, lz4bound;
|
|
uint8_t crc = crc8((unsigned char*)&old_len, sizeof(old_len));
|
|
char *lz4buf, *lz4pos;
|
|
|
|
lz4bound = LZ4_compressBound(len);
|
|
lz4buf = emalloc(REDIS_LZ4_HDR_SIZE + lz4bound);
|
|
lz4pos = lz4buf;
|
|
|
|
/* Copy and move past crc8 length checksum */
|
|
memcpy(lz4pos, &crc, sizeof(crc));
|
|
lz4pos += sizeof(crc);
|
|
|
|
/* Copy and advance past length */
|
|
memcpy(lz4pos, &old_len, sizeof(old_len));
|
|
lz4pos += sizeof(old_len);
|
|
|
|
if (redis_sock->compression_level <= 0 || redis_sock->compression_level > REDIS_LZ4_MAX_CLEVEL) {
|
|
lz4len = LZ4_compress_default(buf, lz4pos, old_len, lz4bound);
|
|
} else {
|
|
lz4len = LZ4_compress_HC(buf, lz4pos, old_len, lz4bound, redis_sock->compression_level);
|
|
}
|
|
|
|
if (lz4len <= 0) {
|
|
efree(lz4buf);
|
|
break;
|
|
}
|
|
|
|
*dst = lz4buf;
|
|
*dstlen = lz4len + REDIS_LZ4_HDR_SIZE;
|
|
return 1;
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
*dst = buf;
|
|
*dstlen = len;
|
|
return 0;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_uncompress(RedisSock *redis_sock, char **dst, size_t *dstlen, const char *src, size_t len) {
|
|
switch (redis_sock->compression) {
|
|
case REDIS_COMPRESSION_LZF:
|
|
#ifdef HAVE_REDIS_LZF
|
|
{
|
|
char *data = NULL;
|
|
uint32_t res;
|
|
int i;
|
|
|
|
if (len == 0)
|
|
break;
|
|
|
|
/* Grow our buffer until we succeed or get a non E2BIG error */
|
|
errno = E2BIG;
|
|
for (i = 2; errno == E2BIG; i *= 2) {
|
|
data = erealloc(data, len * i);
|
|
if ((res = lzf_decompress(src, len, data, len * i)) > 0) {
|
|
*dst = data;
|
|
*dstlen = res;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
efree(data);
|
|
break;
|
|
}
|
|
#endif
|
|
break;
|
|
case REDIS_COMPRESSION_ZSTD:
|
|
#ifdef HAVE_REDIS_ZSTD
|
|
{
|
|
char *data;
|
|
unsigned long long zlen;
|
|
|
|
zlen = ZSTD_getFrameContentSize(src, len);
|
|
if (zlen == ZSTD_CONTENTSIZE_ERROR || zlen == ZSTD_CONTENTSIZE_UNKNOWN || zlen > INT_MAX)
|
|
break;
|
|
|
|
data = emalloc(zlen);
|
|
*dstlen = ZSTD_decompress(data, zlen, src, len);
|
|
if (ZSTD_isError(*dstlen) || *dstlen != zlen) {
|
|
efree(data);
|
|
break;
|
|
}
|
|
|
|
*dst = data;
|
|
return 1;
|
|
}
|
|
#endif
|
|
break;
|
|
case REDIS_COMPRESSION_LZ4:
|
|
#ifdef HAVE_REDIS_LZ4
|
|
{
|
|
char *data;
|
|
int datalen, res;
|
|
uint8_t lz4crc;
|
|
|
|
/* We must have at least enough bytes for our header, and can't have more than
|
|
* INT_MAX + our header size. */
|
|
if (len < REDIS_LZ4_HDR_SIZE || len > INT_MAX + REDIS_LZ4_HDR_SIZE)
|
|
break;
|
|
|
|
/* Operate on copies in case our CRC fails */
|
|
const char *copy = src;
|
|
size_t copylen = len;
|
|
|
|
/* Read in our header bytes */
|
|
memcpy(&lz4crc, copy, sizeof(uint8_t));
|
|
copy += sizeof(uint8_t); copylen -= sizeof(uint8_t);
|
|
memcpy(&datalen, copy, sizeof(int));
|
|
copy += sizeof(int); copylen -= sizeof(int);
|
|
|
|
if (datalen <= 0)
|
|
break;
|
|
|
|
/* Make sure our CRC matches (TODO: Maybe issue a docref error?) */
|
|
if (crc8((unsigned char*)&datalen, sizeof(datalen)) != lz4crc)
|
|
break;
|
|
|
|
/* Finally attempt decompression */
|
|
data = emalloc(datalen);
|
|
res = LZ4_decompress_safe(copy, data, copylen, datalen);
|
|
if (res == datalen) {
|
|
*dst = data;
|
|
*dstlen = res;
|
|
return 1;
|
|
}
|
|
|
|
efree(data);
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
*dst = (char*)src;
|
|
*dstlen = len;
|
|
return 0;
|
|
}
|
|
|
|
static int serialize_generic_zval(char **dst, size_t *len, zval *zsrc) {
|
|
zend_string *zstr;
|
|
|
|
zstr = zval_get_string_func(zsrc);
|
|
if (ZSTR_IS_INTERNED(zstr)) {
|
|
*dst = ZSTR_VAL(zstr);
|
|
*len = ZSTR_LEN(zstr);
|
|
return 0;
|
|
}
|
|
|
|
*dst = estrndup(ZSTR_VAL(zstr), ZSTR_LEN(zstr));
|
|
*len = ZSTR_LEN(zstr);
|
|
|
|
zend_string_release(zstr);
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
PHP_REDIS_API int
|
|
redis_pack(RedisSock *redis_sock, zval *z, char **val, size_t *val_len) {
|
|
size_t tmplen;
|
|
int tmpfree;
|
|
char *tmp;
|
|
|
|
/* Don't pack actual numbers if the user asked us not to */
|
|
if (UNEXPECTED(redis_sock->pack_ignore_numbers &&
|
|
(Z_TYPE_P(z) == IS_LONG || Z_TYPE_P(z) == IS_DOUBLE)))
|
|
{
|
|
return serialize_generic_zval(val, val_len, z);
|
|
}
|
|
|
|
/* First serialize */
|
|
tmpfree = redis_serialize(redis_sock, z, &tmp, &tmplen);
|
|
|
|
/* Now attempt compression */
|
|
if (redis_compress(redis_sock, val, val_len, tmp, tmplen)) {
|
|
if (tmpfree) efree(tmp);
|
|
return 1;
|
|
}
|
|
|
|
return tmpfree;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_unpack(RedisSock *redis_sock, const char *src, int srclen, zval *zdst) {
|
|
zend_long lval;
|
|
double dval;
|
|
size_t len;
|
|
char *buf;
|
|
|
|
if (UNEXPECTED((redis_sock->serializer != REDIS_SERIALIZER_NONE ||
|
|
redis_sock->compression != REDIS_COMPRESSION_NONE) &&
|
|
redis_sock->pack_ignore_numbers) &&
|
|
srclen > 0 && srclen < 512)
|
|
{
|
|
switch (is_numeric_string(src, srclen, &lval, &dval, 0)) {
|
|
case IS_LONG:
|
|
ZVAL_LONG(zdst, lval);
|
|
return 1;
|
|
case IS_DOUBLE:
|
|
ZVAL_DOUBLE(zdst, dval);
|
|
return 1;
|
|
default:
|
|
/* Fallthrough */
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Input string is empty */
|
|
if (srclen == 0) {
|
|
ZVAL_STR(zdst, ZSTR_EMPTY_ALLOC());
|
|
return 1;
|
|
}
|
|
|
|
/* Uncompress, then unserialize */
|
|
if (redis_uncompress(redis_sock, &buf, &len, src, srclen)) {
|
|
if (!redis_unserialize(redis_sock, buf, len, zdst)) {
|
|
ZVAL_STRINGL_FAST(zdst, buf, len);
|
|
}
|
|
efree(buf);
|
|
return 1;
|
|
}
|
|
|
|
if (!redis_unserialize(redis_sock, src, srclen, zdst)) {
|
|
ZVAL_STRINGL_FAST(zdst, src, srclen);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
|
|
redis_serialize(RedisSock *redis_sock, zval *z, char **val, size_t *val_len)
|
|
{
|
|
php_serialize_data_t ht;
|
|
|
|
smart_str sstr = {0};
|
|
#ifdef HAVE_REDIS_IGBINARY
|
|
size_t sz;
|
|
uint8_t *val8;
|
|
#endif
|
|
|
|
*val = "";
|
|
*val_len = 0;
|
|
switch(redis_sock->serializer) {
|
|
case REDIS_SERIALIZER_NONE:
|
|
switch(Z_TYPE_P(z)) {
|
|
case IS_STRING:
|
|
*val = Z_STRVAL_P(z);
|
|
*val_len = Z_STRLEN_P(z);
|
|
break;
|
|
|
|
case IS_OBJECT:
|
|
*val = "Object";
|
|
*val_len = 6;
|
|
break;
|
|
|
|
case IS_ARRAY:
|
|
*val = "Array";
|
|
*val_len = 5;
|
|
break;
|
|
|
|
default:
|
|
return serialize_generic_zval(val, val_len, z);
|
|
}
|
|
break;
|
|
case REDIS_SERIALIZER_PHP:
|
|
PHP_VAR_SERIALIZE_INIT(ht);
|
|
php_var_serialize(&sstr, z, &ht);
|
|
PHP_VAR_SERIALIZE_DESTROY(ht);
|
|
|
|
if (!sstr.s) {
|
|
break;
|
|
}
|
|
*val = estrndup(ZSTR_VAL(sstr.s), ZSTR_LEN(sstr.s));
|
|
*val_len = ZSTR_LEN(sstr.s);
|
|
|
|
smart_str_free(&sstr);
|
|
|
|
return 1;
|
|
|
|
case REDIS_SERIALIZER_MSGPACK:
|
|
#ifdef HAVE_REDIS_MSGPACK
|
|
php_msgpack_serialize(&sstr, z);
|
|
*val = estrndup(ZSTR_VAL(sstr.s), ZSTR_LEN(sstr.s));
|
|
*val_len = ZSTR_LEN(sstr.s);
|
|
smart_str_free(&sstr);
|
|
|
|
return 1;
|
|
#endif
|
|
break;
|
|
case REDIS_SERIALIZER_IGBINARY:
|
|
#ifdef HAVE_REDIS_IGBINARY
|
|
if(igbinary_serialize(&val8, (size_t *)&sz, z) == 0) {
|
|
*val = (char*)val8;
|
|
*val_len = sz;
|
|
return 1;
|
|
}
|
|
#endif
|
|
break;
|
|
case REDIS_SERIALIZER_JSON:
|
|
#ifdef HAVE_REDIS_JSON
|
|
php_json_encode(&sstr, z, PHP_JSON_OBJECT_AS_ARRAY);
|
|
*val = estrndup(ZSTR_VAL(sstr.s), ZSTR_LEN(sstr.s));
|
|
*val_len = ZSTR_LEN(sstr.s);
|
|
smart_str_free(&sstr);
|
|
return 1;
|
|
#endif
|
|
break;
|
|
EMPTY_SWITCH_DEFAULT_CASE()
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_unserialize(RedisSock* redis_sock, const char *val, int val_len,
|
|
zval *z_ret)
|
|
{
|
|
|
|
php_unserialize_data_t var_hash;
|
|
int ret = 0;
|
|
|
|
switch(redis_sock->serializer) {
|
|
case REDIS_SERIALIZER_NONE:
|
|
/* Nothing to do */
|
|
break;
|
|
case REDIS_SERIALIZER_PHP:
|
|
PHP_VAR_UNSERIALIZE_INIT(var_hash);
|
|
|
|
ret = php_var_unserialize(z_ret, (const unsigned char **)&val,
|
|
(const unsigned char *)val + val_len,
|
|
&var_hash);
|
|
|
|
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
|
|
break;
|
|
|
|
case REDIS_SERIALIZER_MSGPACK:
|
|
#ifdef HAVE_REDIS_MSGPACK
|
|
ret = !php_msgpack_unserialize(z_ret, (char *)val, (size_t)val_len);
|
|
#endif
|
|
break;
|
|
|
|
case REDIS_SERIALIZER_IGBINARY:
|
|
#ifdef HAVE_REDIS_IGBINARY
|
|
/*
|
|
* Check if the given string starts with an igbinary header.
|
|
*
|
|
* A modern igbinary string consists of the following format:
|
|
*
|
|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
|
|
* | header (4) | type (1) | ... (n) | NUL (1) |
|
|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
|
|
*
|
|
* With header being three zero bytes and one non-zero version
|
|
* specifier. At the time of this comment, there is only version
|
|
* 0x01 and 0x02, but newer versions will use subsequent
|
|
* values.
|
|
*
|
|
* Not all versions contain a trailing \x00 so don't check for that.
|
|
*/
|
|
if (val_len < 5 || memcmp(val, "\x00\x00\x00", 3) || val[3] < '\x01' || val[3] > '\x04')
|
|
{
|
|
/* This is most definitely not an igbinary string, so do
|
|
not try to unserialize this as one. */
|
|
break;
|
|
}
|
|
|
|
ret = !igbinary_unserialize((const uint8_t *)val, (size_t)val_len, z_ret);
|
|
#endif
|
|
break;
|
|
case REDIS_SERIALIZER_JSON:
|
|
#ifdef HAVE_REDIS_JSON
|
|
ret = !php_json_decode(z_ret, (char *)val, val_len, 1, PHP_JSON_PARSER_DEFAULT_DEPTH);
|
|
#endif
|
|
break;
|
|
EMPTY_SWITCH_DEFAULT_CASE()
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_key_prefix(RedisSock *redis_sock, char **key, size_t *key_len) {
|
|
int ret_len;
|
|
char *ret;
|
|
|
|
if (redis_sock->prefix == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
ret_len = ZSTR_LEN(redis_sock->prefix) + *key_len;
|
|
ret = emalloc(1 + ret_len);
|
|
memcpy(ret, ZSTR_VAL(redis_sock->prefix), ZSTR_LEN(redis_sock->prefix));
|
|
memcpy(ret + ZSTR_LEN(redis_sock->prefix), *key, *key_len);
|
|
ret[ret_len] = '\0';
|
|
|
|
*key = ret;
|
|
*key_len = ret_len;
|
|
return 1;
|
|
}
|
|
|
|
/* This is very similar to PHP >= 7.4 zend_string_concat2 only we are taking
|
|
* two zend_string arguments rather than two char*, size_t pairs */
|
|
static zend_string *redis_zstr_concat(zend_string *prefix, zend_string *suffix) {
|
|
zend_string *res;
|
|
size_t len;
|
|
|
|
ZEND_ASSERT(prefix != NULL && suffix != NULL);
|
|
|
|
len = ZSTR_LEN(prefix) + ZSTR_LEN(suffix);
|
|
res = zend_string_alloc(len, 0);
|
|
|
|
memcpy(ZSTR_VAL(res), ZSTR_VAL(prefix), ZSTR_LEN(prefix));
|
|
memcpy(ZSTR_VAL(res) + ZSTR_LEN(prefix), ZSTR_VAL(suffix), ZSTR_LEN(suffix));
|
|
ZSTR_VAL(res)[len] = '\0';
|
|
|
|
return res;
|
|
}
|
|
|
|
PHP_REDIS_API zend_string *
|
|
redis_key_prefix_zval(RedisSock *redis_sock, zval *zv) {
|
|
zend_string *zstr, *dup;
|
|
|
|
zstr = zval_get_string(zv);
|
|
if (redis_sock->prefix == NULL)
|
|
return zstr;
|
|
|
|
dup = redis_zstr_concat(redis_sock->prefix, zstr);
|
|
|
|
zend_string_release(zstr);
|
|
|
|
return dup;
|
|
}
|
|
|
|
PHP_REDIS_API zend_string *
|
|
redis_key_prefix_zstr(RedisSock *redis_sock, zend_string *key) {
|
|
if (redis_sock->prefix == NULL)
|
|
return zend_string_copy(key);
|
|
|
|
return redis_zstr_concat(redis_sock->prefix, key);
|
|
}
|
|
|
|
/*
|
|
* Processing for variant reply types (think EVAL)
|
|
*/
|
|
|
|
PHP_REDIS_API int
|
|
redis_sock_gets(RedisSock *redis_sock, char *buf, int buf_size, size_t *line_size) {
|
|
// Handle EOF
|
|
if(-1 == redis_check_eof(redis_sock, 1, 0)) {
|
|
return -1;
|
|
}
|
|
|
|
if(redis_sock_get_line(redis_sock, buf, buf_size, line_size) == NULL) {
|
|
if (redis_sock->port < 0) {
|
|
snprintf(buf, buf_size, "read error on connection to %s", ZSTR_VAL(redis_sock->host));
|
|
} else {
|
|
snprintf(buf, buf_size, "read error on connection to %s:%d", ZSTR_VAL(redis_sock->host), redis_sock->port);
|
|
}
|
|
// Close our socket
|
|
redis_sock_disconnect(redis_sock, 1, 1);
|
|
|
|
// Throw a read error exception
|
|
REDIS_THROW_EXCEPTION(buf, 0);
|
|
return FAILURE;
|
|
}
|
|
|
|
/* We don't need \r\n */
|
|
*line_size-=2;
|
|
buf[*line_size]='\0';
|
|
|
|
/* Success! */
|
|
return 0;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_reply_type(RedisSock *redis_sock, REDIS_REPLY_TYPE *reply_type,
|
|
long *reply_info)
|
|
{
|
|
size_t nread;
|
|
|
|
// Make sure we haven't lost the connection, even trying to reconnect
|
|
if(-1 == redis_check_eof(redis_sock, 1, 0)) {
|
|
// Failure
|
|
*reply_type = EOF;
|
|
return -1;
|
|
}
|
|
|
|
// Attempt to read the reply-type byte
|
|
if((*reply_type = redis_sock_getc(redis_sock)) == EOF) {
|
|
REDIS_THROW_EXCEPTION( "socket error on read socket", 0);
|
|
return -1;
|
|
}
|
|
|
|
// If this is a BULK, MULTI BULK, or simply an INTEGER response, we can
|
|
// extract the value or size info here
|
|
if(*reply_type == TYPE_INT || *reply_type == TYPE_BULK ||
|
|
*reply_type == TYPE_MULTIBULK)
|
|
{
|
|
// Buffer to hold size information
|
|
char inbuf[255];
|
|
|
|
/* Read up to our newline */
|
|
if (redis_sock_get_line(redis_sock, inbuf, sizeof(inbuf), &nread) == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
/* Set our size response */
|
|
{
|
|
char *endptr;
|
|
|
|
errno = 0;
|
|
*reply_info = strtol(inbuf, &endptr, 10);
|
|
if (endptr == inbuf || errno == ERANGE) {
|
|
return -1;
|
|
}
|
|
}
|
|
} else {
|
|
/* Always initialize to prevent UB */
|
|
*reply_info = 0;
|
|
}
|
|
|
|
/* Success! */
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Read a single line response, having already consumed the reply-type byte
|
|
*/
|
|
static int
|
|
redis_read_variant_line(RedisSock *redis_sock, REDIS_REPLY_TYPE reply_type,
|
|
int as_string, zval *z_ret)
|
|
{
|
|
// Buffer to read our single line reply
|
|
char inbuf[4096];
|
|
size_t len;
|
|
|
|
/* Attempt to read our single line reply */
|
|
if(redis_sock_gets(redis_sock, inbuf, sizeof(inbuf), &len) < 0) {
|
|
return -1;
|
|
}
|
|
|
|
/* Throw exception on SYNC error otherwise just set error string */
|
|
if(reply_type == TYPE_ERR) {
|
|
redis_sock_set_err(redis_sock, inbuf, len);
|
|
redis_error_throw(redis_sock);
|
|
ZVAL_FALSE(z_ret);
|
|
} else if (as_string) {
|
|
ZVAL_STRINGL(z_ret, inbuf, len);
|
|
} else {
|
|
ZVAL_TRUE(z_ret);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_variant_bulk(RedisSock *redis_sock, long size, zval *z_ret
|
|
)
|
|
{
|
|
int bytes;
|
|
|
|
if (size < -1 || size > INT_MAX - 2) {
|
|
zend_throw_exception_ex(redis_exception_ce, 0,
|
|
"protocol error, invalid bulk length: %ld", size);
|
|
ZVAL_FALSE(z_ret);
|
|
return -1;
|
|
}
|
|
|
|
bytes = size;
|
|
|
|
// Attempt to read the bulk reply
|
|
char *bulk_resp = redis_sock_read_bulk_reply(redis_sock, bytes);
|
|
|
|
/* Set our reply to FALSE on failure, and the string on success */
|
|
if(bulk_resp == NULL) {
|
|
ZVAL_FALSE(z_ret);
|
|
return -1;
|
|
}
|
|
ZVAL_STRINGL(z_ret, bulk_resp, bytes);
|
|
efree(bulk_resp);
|
|
return 0;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_multibulk_recursive(RedisSock *redis_sock, long long elements, int status_strings,
|
|
zval *z_ret)
|
|
{
|
|
long reply_info;
|
|
REDIS_REPLY_TYPE reply_type;
|
|
zval z_subelem;
|
|
|
|
// Iterate while we have elements
|
|
while(elements > 0) {
|
|
// Attempt to read our reply type
|
|
if(redis_read_reply_type(redis_sock, &reply_type, &reply_info
|
|
) < 0)
|
|
{
|
|
zend_throw_exception_ex(redis_exception_ce, 0,
|
|
"protocol error, couldn't parse MULTI-BULK response\n");
|
|
return FAILURE;
|
|
}
|
|
|
|
// Switch on our reply-type byte
|
|
switch(reply_type) {
|
|
case TYPE_ERR:
|
|
case TYPE_LINE:
|
|
redis_read_variant_line(redis_sock, reply_type, status_strings,
|
|
&z_subelem);
|
|
add_next_index_zval(z_ret, &z_subelem);
|
|
break;
|
|
case TYPE_INT:
|
|
// Add our long value
|
|
add_next_index_long(z_ret, reply_info);
|
|
break;
|
|
case TYPE_BULK:
|
|
// Init a zval for our bulk response, read and add it
|
|
redis_read_variant_bulk(redis_sock, reply_info, &z_subelem);
|
|
add_next_index_zval(z_ret, &z_subelem);
|
|
break;
|
|
case TYPE_MULTIBULK:
|
|
if (reply_info < 0 && redis_sock->null_mbulk_as_null) {
|
|
add_next_index_null(z_ret);
|
|
} else {
|
|
array_init_size(&z_subelem, reply_info > 0 ? reply_info : 0);
|
|
if (reply_info > 0) {
|
|
redis_read_multibulk_recursive(redis_sock, reply_info, status_strings, &z_subelem);
|
|
}
|
|
add_next_index_zval(z_ret, &z_subelem);
|
|
}
|
|
break;
|
|
default:
|
|
// Stop the compiler from whinging
|
|
break;
|
|
}
|
|
|
|
/* Decrement our element counter */
|
|
elements--;
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
static int
|
|
variant_reply_generic(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
int status_strings, int null_mbulk_as_null,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
// Reply type, and reply size vars
|
|
REDIS_REPLY_TYPE reply_type;
|
|
long reply_info;
|
|
zval z_ret;
|
|
|
|
// Attempt to read our header
|
|
if(redis_read_reply_type(redis_sock,&reply_type,&reply_info) < 0) {
|
|
return -1;
|
|
}
|
|
|
|
/* Switch based on our top level reply type */
|
|
switch(reply_type) {
|
|
case TYPE_ERR:
|
|
case TYPE_LINE:
|
|
redis_read_variant_line(redis_sock, reply_type, status_strings, &z_ret);
|
|
break;
|
|
case TYPE_INT:
|
|
ZVAL_LONG(&z_ret, reply_info);
|
|
break;
|
|
case TYPE_BULK:
|
|
redis_read_variant_bulk(redis_sock, reply_info, &z_ret);
|
|
break;
|
|
case TYPE_MULTIBULK:
|
|
if (reply_info > -1) {
|
|
array_init(&z_ret);
|
|
redis_read_multibulk_recursive(redis_sock, reply_info, status_strings, &z_ret);
|
|
} else {
|
|
if (null_mbulk_as_null) {
|
|
ZVAL_NULL(&z_ret);
|
|
} else {
|
|
array_init(&z_ret);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
zend_throw_exception_ex(redis_exception_ce, 0,
|
|
"protocol error, got '%c' as reply-type byte\n", reply_type);
|
|
return FAILURE;
|
|
}
|
|
|
|
REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret);
|
|
|
|
/* Success */
|
|
return 0;
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_raw_variant_reply(INTERNAL_FUNCTION_PARAMETERS,
|
|
RedisSock *redis_sock, zval *z_tab,
|
|
RedisCmdCtx ctx)
|
|
{
|
|
return variant_reply_generic(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
redis_sock->reply_literal,
|
|
redis_sock->null_mbulk_as_null,
|
|
z_tab, ctx);
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_variant_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
return variant_reply_generic(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, 0,
|
|
redis_sock->null_mbulk_as_null, z_tab, ctx);
|
|
}
|
|
|
|
PHP_REDIS_API int
|
|
redis_read_variant_reply_strings(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
|
|
zval *z_tab, RedisCmdCtx ctx)
|
|
{
|
|
return variant_reply_generic(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
|
|
1, 0, z_tab, ctx);
|
|
}
|
|
|
|
/* The user may wish to send us something like [NULL, 'password'] or
|
|
* [false, 'password'] so don't convert NULL or FALSE into "". */
|
|
static int redisTrySetAuthArg(zend_string **dst, zval *zsrc) {
|
|
if (Z_TYPE_P(zsrc) == IS_NULL || Z_TYPE_P(zsrc) == IS_FALSE)
|
|
return FAILURE;
|
|
|
|
*dst = zval_get_string(zsrc);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
PHP_REDIS_API
|
|
int redis_extract_auth_info(zval *ztest, zend_string **user, zend_string **pass) {
|
|
zval *zv;
|
|
HashTable *ht;
|
|
int num;
|
|
|
|
/* Null out user and password */
|
|
*user = *pass = NULL;
|
|
|
|
/* User passed nothing */
|
|
if (ztest == NULL)
|
|
return FAILURE;
|
|
|
|
/* Handle a non-array first */
|
|
if (Z_TYPE_P(ztest) != IS_ARRAY) {
|
|
return redisTrySetAuthArg(pass, ztest);
|
|
}
|
|
|
|
/* Handle the array case */
|
|
ht = Z_ARRVAL_P(ztest);
|
|
num = zend_hash_num_elements(ht);
|
|
|
|
/* Something other than one or two entries makes no sense */
|
|
if (num != 1 && num != 2) {
|
|
php_error_docref(NULL, E_WARNING, "When passing an array as auth it must have one or two elements!");
|
|
return FAILURE;
|
|
}
|
|
|
|
if (num == 2) {
|
|
if ((zv = REDIS_HASH_STR_FIND_STATIC(ht, "user")) ||
|
|
(zv = zend_hash_index_find(ht, 0)))
|
|
{
|
|
redisTrySetAuthArg(user, zv);
|
|
}
|
|
|
|
if ((zv = REDIS_HASH_STR_FIND_STATIC(ht, "pass")) ||
|
|
(zv = zend_hash_index_find(ht, 1)))
|
|
{
|
|
redisTrySetAuthArg(pass, zv);
|
|
}
|
|
} else if ((zv = REDIS_HASH_STR_FIND_STATIC(ht, "pass")) ||
|
|
(zv = zend_hash_index_find(ht, 0)))
|
|
{
|
|
redisTrySetAuthArg(pass, zv);
|
|
}
|
|
|
|
/* If we at least have a password, we're good */
|
|
if (*pass != NULL)
|
|
return SUCCESS;
|
|
|
|
/* Failure, clean everything up so caller doesn't need to care */
|
|
if (*user) zend_string_release(*user);
|
|
*user = NULL;
|
|
|
|
return FAILURE;
|
|
}
|
|
|
|
PHP_REDIS_API void redis_with_metadata(zval *zdst, zval *zsrc, zend_long length) {
|
|
zval z_sub;
|
|
|
|
array_init(zdst);
|
|
add_next_index_zval(zdst, zsrc);
|
|
|
|
array_init(&z_sub);
|
|
add_assoc_long_ex(&z_sub, ZEND_STRL("length"), length);
|
|
add_next_index_zval(zdst, &z_sub);
|
|
}
|
|
|
|
/* Helper methods to extract configuration settings from a hash table */
|
|
|
|
zval *redis_hash_str_find_type(HashTable *ht, const char *key, int keylen, int type) {
|
|
zval *zv = zend_hash_str_find(ht, key, keylen);
|
|
if (zv == NULL || Z_TYPE_P(zv) != type)
|
|
return NULL;
|
|
|
|
return zv;
|
|
}
|
|
|
|
void redis_conf_double(HashTable *ht, const char *key, int keylen, double *dval) {
|
|
zval *zv = zend_hash_str_find(ht, key, keylen);
|
|
if (zv == NULL)
|
|
return;
|
|
|
|
*dval = zval_get_double(zv);
|
|
}
|
|
|
|
void redis_conf_bool(HashTable *ht, const char *key, int keylen, int *ival) {
|
|
zend_string *zstr = NULL;
|
|
|
|
redis_conf_string(ht, key, keylen, &zstr);
|
|
if (zstr == NULL)
|
|
return;
|
|
|
|
*ival = zend_string_equals_literal_ci(zstr, "true") ||
|
|
zend_string_equals_literal_ci(zstr, "yes") ||
|
|
zend_string_equals_literal_ci(zstr, "1");
|
|
|
|
zend_string_release(zstr);
|
|
}
|
|
|
|
void redis_conf_zend_bool(HashTable *ht, const char *key, int keylen, zend_bool *bval) {
|
|
zval *zv = zend_hash_str_find(ht, key, keylen);
|
|
if (zv == NULL)
|
|
return;
|
|
|
|
*bval = zend_is_true(zv);
|
|
}
|
|
|
|
void redis_conf_long(HashTable *ht, const char *key, int keylen, zend_long *lval) {
|
|
zval *zv = zend_hash_str_find(ht, key, keylen);
|
|
if (zv == NULL)
|
|
return;
|
|
|
|
*lval = zval_get_long(zv);
|
|
}
|
|
|
|
void redis_conf_int(HashTable *ht, const char *key, int keylen, int *ival) {
|
|
zval *zv = zend_hash_str_find(ht, key, keylen);
|
|
if (zv == NULL)
|
|
return;
|
|
|
|
*ival = zval_get_long(zv);
|
|
}
|
|
|
|
void redis_conf_string(HashTable *ht, const char *key, size_t keylen,
|
|
zend_string **sval)
|
|
{
|
|
zval *zv = zend_hash_str_find(ht, key, keylen);
|
|
if (zv == NULL)
|
|
return;
|
|
|
|
*sval = zval_get_string(zv);
|
|
}
|
|
|
|
void redis_conf_zval(HashTable *ht, const char *key, size_t keylen, zval *zret,
|
|
int copy, int dtor)
|
|
{
|
|
zval *zv = zend_hash_str_find(ht, key, keylen);
|
|
if (zv == NULL)
|
|
return;
|
|
|
|
ZVAL_ZVAL(zret, zv, copy, dtor);
|
|
}
|
|
|
|
void redis_conf_auth(HashTable *ht, const char *key, size_t keylen,
|
|
zend_string **user, zend_string **pass)
|
|
{
|
|
zval *zv = zend_hash_str_find(ht, key, keylen);
|
|
if (zv == NULL)
|
|
return;
|
|
|
|
redis_extract_auth_info(zv, user, pass);
|
|
}
|
|
|
|
/* Update a zval with the current 64 bit scan cursor. This presents a problem
|
|
* because we can only represent up to 63 bits in a PHP integer. So depending
|
|
* on the cursor value, we may need to represent it as a string. */
|
|
void redisSetScanCursor(zval *zv, uint64_t cursor) {
|
|
char tmp[21];
|
|
size_t len;
|
|
|
|
ZEND_ASSERT(zv != NULL && (Z_TYPE_P(zv) == IS_LONG ||
|
|
Z_TYPE_P(zv) == IS_STRING));
|
|
|
|
if (Z_TYPE_P(zv) == IS_STRING)
|
|
zend_string_release(Z_STR_P(zv));
|
|
|
|
if (cursor > ZEND_LONG_MAX) {
|
|
len = snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)cursor);
|
|
ZVAL_STRINGL(zv, tmp, len);
|
|
} else {
|
|
ZVAL_LONG(zv, cursor);
|
|
}
|
|
}
|
|
|
|
/* Get a Redis SCAN cursor value out of a zval. These are always taken as a
|
|
* reference argument that that must be `null`, `int`, or `string`. */
|
|
uint64_t redisGetScanCursor(zval *zv, zend_bool *was_zero) {
|
|
ZEND_ASSERT(zv != NULL && (Z_TYPE_P(zv) == IS_LONG ||
|
|
Z_TYPE_P(zv) == IS_STRING ||
|
|
Z_TYPE_P(zv) == IS_NULL));
|
|
|
|
if (Z_TYPE_P(zv) == IS_NULL) {
|
|
convert_to_long(zv);
|
|
*was_zero = 0;
|
|
return 0;
|
|
} else if (Z_TYPE_P(zv) == IS_STRING) {
|
|
*was_zero = Z_STRLEN_P(zv) == 1 && Z_STRVAL_P(zv)[0] == '0';
|
|
return strtoull(Z_STRVAL_P(zv), NULL, 10);
|
|
} else {
|
|
*was_zero = Z_LVAL_P(zv) == 0;
|
|
return Z_LVAL_P(zv);
|
|
}
|
|
}
|
|
|
|
void redis_cmd_ctx_efree_dtor(void *ptr) {
|
|
efree(ptr);
|
|
}
|
|
|
|
|
|
/* vim: set tabstop=4 softtabstop=4 expandtab shiftwidth=4: */
|