diff --git a/Makefile b/Makefile
index 5ca19ea..2e05788 100644
--- a/Makefile
+++ b/Makefile
@@ -69,7 +69,7 @@ clean:
# Remove all generated files
.PHONY: distclean
-distclean: clean
+distclean:
rm -f Doxyfile.local $(LIB)/libredisx.so* $(LIB)/libredisx.a
@@ -77,7 +77,7 @@ distclean: clean
# The nitty-gritty stuff below
# ----------------------------------------------------------------------------
-SOURCES = $(SRC)/redisx.c $(SRC)/redisx-net.c $(SRC)/redisx-hooks.c $(SRC)/redisx-client.c \
+SOURCES = $(SRC)/redisx.c $(SRC)/resp.c $(SRC)/redisx-net.c $(SRC)/redisx-hooks.c $(SRC)/redisx-client.c \
$(SRC)/redisx-tab.c $(SRC)/redisx-sub.c $(SRC)/redisx-script.c
# Generate a list of object (obj/*.o) files from the input sources
diff --git a/README.md b/README.md
index b045152..439c117 100644
--- a/README.md
+++ b/README.md
@@ -205,6 +205,17 @@ the default 6379), and the database authentication (if any):
redisxSetPassword(redis, mySecretPasswordString);
```
+You can also set the RESP protocol to use (provided your server is compatible with Redis 6 or later):
+
+```c
+ // (optional) Use RESP3 (provided the server supports it)
+ redisxSetProtocol(redis, REDISX_RESP3);
+```
+
+The above call will use the `HELLO` command (since Redis 6) upon connecting. If you do not set the protocol, `HELLO`
+will not be used, and RESP2 will be assumed -- which is best for older servers. (Note, that you can always check the
+actual protocol used after connecting, using `redisxGetProtocol()`).
+
You might also tweak the send/receive buffer sizes to use for clients, if you find the socket defaults sub-optimal for
your application (note, that this setting is common to all `Redis` instances managed by the library):
@@ -307,9 +318,9 @@ The same goes for disconnect hooks, using `redisxAddDisconnectHook()` instead.
- [RESP data type](#resp-data-type)
Redis queries are sent as strings, according the the specification of the Redis protocol. All responses sent back by
-the server using the RESP protocol. Specifically, Redis uses version 2.0 of the RESP protocol (a.k.a. RESP2) by
-default, with optional support for the newer RESP3 introduced in Redis version 6.0. The __RedisX__ library currently
-processes the standard RESP2 replies only. RESP3 support to the library may be added in the future (stay tuned...)
+the server using the RESP protocol. Specifically, Redis uses version 2 of the RESP protocol (a.k.a. RESP2) by
+default, with optional support for the newer RESP3 introduced in Redis version 6.0. The __RedisX__ library provides
+support for both RESP2 and RESP3.
@@ -378,9 +389,19 @@ whose contents are:
| `RESP_ARRAY` | `*` | number of `RESP *` pointers | `(RESP **)` |
| `RESP_INT` | `:` | integer return value | `(void)` |
| `RESP_SIMPLE_STRING` | `+` | string length | `(char *)` |
- | `RESP_ERROR` | `-` | string length | `(char *)` |
+ | `RESP_ERROR` | `-` | total string length | `(char *)` |
| `RESP_BULK_STRING` | `$` | string length or -1 if `NULL` | `(char *)` |
-
+ | `RESP3_NULL` | `_` | 0 | `(void)` |
+ | `RESP3_BOOLEAN` | `#` | 1 if _true_, 0 if _false_ | `(void)` |
+ | `RESP3_DOUBLE` | `,` | _unused_ | `(double *)` |
+ | `RESP3_BIG_NUMBER` | `(` | string representation length | `(char *)` |
+ | `RESP3_BLOB_ERROR` | `!` | total string length | `(char *)` |
+ | `RESP3_VERBATIM_TEXT` | `=` | text length (incl. type) | `(char *)` |
+ | `RESP3_SET` | `~` | number of `RESP *` pointers | `(RESP *)` |
+ | `RESP3_MAP` | `%` | number of key / value pairs | `(RedisMapEntry *)` |
+ | `RESP3_ATTRIBUTE` | `|` | number of key / value pairs | `(RedisMapEntry *)` |
+ | `RESP3_PUSH` | `>` | number of `RESP *` pointers | `(RESP **)` |
+
Each `RESP` has a type (e.g. `RESP_SIMPLE_STRING`), an integer value `n`, and a `value` pointer
to further data. If the type is `RESP_INT`, then `n` represents the actual return value (and the `value` pointer is
@@ -388,9 +409,16 @@ not used). For string type values `n` is the number of characters in the string
while for `RESP_ARRAY` types the `value` is a pointer to an embedded `RESP` array and `n` is the number of elements
in that.
-You may check the integrity of a `RESP` using `redisxCheckRESP()`. Since `RESP` data is dynamically allocated, the
-user is responsible for discarding them once they are no longer needed, e.g. by calling `redisxDestroyRESP()`. The
-two steps may be combined to automatically discard invalid or unexpected `RESP` data in a single step by calling
+To help with deciding what cast to use for a given `value` field of the RESP data structure, we provide the
+convenience methods `redisxIsScalarType()` when cast is `(void)` or else `(double *)`), `redisxIsArrayType()` if the
+cast is `(RESP **)`, `redisxIsStringTupe()` if the cast should be `(char *)`, and `redisxIsMapType()` if the cast
+should be to `(RedisMapEntry *)`.
+
+You can check that two `RESP` data structures are equivalent with `redisxIsEqualRESP(RESP *a, RESP *b)`.
+
+You may also check the integrity of a `RESP` using `redisxCheckRESP()`. Since `RESP` data is dynamically allocated,
+the user is responsible for discarding them once they are no longer needed, e.g. by calling `redisxDestroyRESP()`.
+The two steps may be combined to automatically discard invalid or unexpected `RESP` data in a single step by calling
`redisxCheckDestroyRESP()`.
```c
@@ -1045,7 +1073,6 @@ Some obvious ways the library could evolve and grow in the not too distant futur
- Automated regression testing and coverage tracking.
- Keep track of subscription patterns, and automatically resubscribe to them on reconnecting.
- - Support for the [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) standard and Redis `HELLO`.
- Support for [Redis Sentinel](https://redis.io/docs/latest/develop/reference/sentinel-clients/) clients, for
high-availability server configurations.
- TLS support (perhaps...)
diff --git a/build.mk b/build.mk
index a6ace54..28a196d 100644
--- a/build.mk
+++ b/build.mk
@@ -46,7 +46,7 @@ clean: clean-local
# Remove intermediate files (general)
.PHONY: distclean
-distclean: distclean-local
+distclean: clean distclean-local
# Static code analysis using 'cppcheck'
.PHONY: analyze
diff --git a/include/redisx-priv.h b/include/redisx-priv.h
index 735f1f3..7ec89b6 100644
--- a/include/redisx-priv.h
+++ b/include/redisx-priv.h
@@ -57,8 +57,10 @@ typedef struct {
uint32_t addr; ///< The 32-bit inet address
int port; ///< port number (usually 6379)
int dbIndex; ///< the zero-based database index
- char *username; ///< REdis user name (if any)
+ char *username; ///< Redis user name (if any)
char *password; ///< Redis password (if any)
+ int protocol; ///< RESP version to use
+ boolean hello; ///< whether to use HELLO (introduced in Redis 6.0.0 only)
RedisClient *clients;
@@ -93,6 +95,9 @@ int rConnectClient(Redis *redis, enum redisx_channel channel);
void rCloseClient(RedisClient *cl);
boolean rIsLowLatency(const ClientPrivate *cp);
+// in resp.c ------------------------------>
+int redisxAppendRESP(RESP *resp, RESP *part);
+
/// \endcond
#endif /* REDISX_PRIV_H_ */
diff --git a/include/redisx.h b/include/redisx.h
index 5350703..001aa09 100644
--- a/include/redisx.h
+++ b/include/redisx.h
@@ -90,6 +90,19 @@
#define RESP_ERROR '-' ///< \hideinitializer RESP error message type
#define RESP_BULK_STRING '$' ///< \hideinitializer RESP bulk string type
+// RESP3 types
+#define RESP3_NULL '_' ///< \hideinitializer RESP3 null value
+#define RESP3_DOUBLE ',' ///< \hideinitializer RESP3 floating-point value
+#define RESP3_BOOLEAN '#' ///< \hideinitializer RESP3 boolean value
+#define RESP3_BLOB_ERROR '!' ///< \hideinitializer RESP3 blob error
+#define RESP3_VERBATIM_STRING '=' ///< \hideinitializer RESP3 verbatim string (with type)
+#define RESP3_BIG_NUMBER '(' ///< \hideinitializer RESP3 big integer / decimal
+#define RESP3_MAP '%' ///< \hideinitializer RESP3 dictionary of key / value
+#define RESP3_SET '~' ///< \hideinitializer RESP3 unordered set of elements
+#define RESP3_ATTRIBUTE '|' ///< \hideinitializer RESP3 dictionary of attributes (metadata)
+#define RESP3_PUSH '>' ///< \hideinitializer RESP3 dictionary of attributes (metadata)
+#define RESP3_SNIPPET ';' ///< \hideinitializer RESP3 dictionary of attributes (metadata)
+
#define REDIS_INVALID_CHANNEL (-101) ///< \hideinitializer There is no such channel in the Redis instance.
#define REDIS_NULL (-102) ///< \hideinitializer Redis returned NULL
#define REDIS_ERROR (-103) ///< \hideinitializer Redis returned an error
@@ -112,11 +125,29 @@ enum redisx_channel {
#define REDISX_CHANNELS (REDISX_SUBSCRIPTION_CHANNEL + 1) ///< \hideinitializer The number of channels a Redis instance has.
+/**
+ * The RESP protocol to use for a Redis instance. Redis originally used RESP2, but later releases added
+ * support for RESP3.
+ *
+ */
+enum redisx_protocol {
+ REDISX_RESP2 = 2, ///< \hideinitializer RESP2 protocol
+ REDISX_RESP3 ///< \hideinitializer RESP3 protocol (since Redis version 6.0.0)
+};
/**
* \brief Structure that represents a Redis response (RESP format).
*
+ * REFERENCES:
+ *
+ * - https://github.com/redis/redis-specifications/tree/master/protocol
+ *
+ *
* \sa redisxDestroyRESP()
+ * \sa redisxIsScalarType()
+ * \sa redisxIsStringType()
+ * \sa redisxIsArrayType()
+ * \sa redisxIsMapType()
*/
typedef struct RESP {
char type; ///< RESP type RESP_ARRAY, RESP_INT ...
@@ -126,6 +157,19 @@ typedef struct RESP {
///< (RESP**)...
} RESP;
+/**
+ * Structure that represents a key/value mapping in RESP3.
+ *
+ * @sa redisxIsMapType()
+ * @sa RESP3_MAP
+ * @sa RESP3_ATTRIBUTE
+ *
+ */
+typedef struct {
+ RESP *key; ///< The keyword component
+ RESP *value; ///< The associated value component
+} RedisMapEntry;
+
/**
* \brief A single key / value entry, or field, in the Redis database.
@@ -228,6 +272,8 @@ int redisxSetPort(Redis *redis, int port);
int redisxSetUser(Redis *redis, const char *username);
int redisxSetPassword(Redis *redis, const char *passwd);
int redisxSelectDB(Redis *redis, int idx);
+int redisxSetProtocol(Redis *redis, enum redisx_protocol protocol);
+enum redisx_protocol redisxGetProtocol(const Redis *redis);
Redis *redisxInit(const char *server);
void redisxDestroy(Redis *redis);
@@ -286,6 +332,15 @@ int redisxGetTime(Redis *redis, struct timespec *t);
int redisxCheckRESP(const RESP *resp, char expectedType, int expectedSize);
int redisxCheckDestroyRESP(RESP *resp, char expectedType, int expectedSize);
void redisxDestroyRESP(RESP *resp);
+boolean redisxIsScalarType(const RESP *r);
+boolean redisxIsStringType(const RESP *r);
+boolean redisxIsArrayType(const RESP *r);
+boolean redisxIsMapType(const RESP *r);
+boolean redisxIsEqualRESP(const RESP *a, const RESP *b);
+int redisxSplitText(RESP *resp, char **text);
+
+RedisMapEntry *redisxGetMapEntry(const RESP *map, const RESP *key);
+RedisMapEntry *redisxGetKeywordEntry(const RESP *map, const char *key);
// Locks for async calls
int redisxLockClient(RedisClient *cl);
diff --git a/src/redisx-client.c b/src/redisx-client.c
index 0e0ac95..62c2889 100644
--- a/src/redisx-client.c
+++ b/src/redisx-client.c
@@ -12,6 +12,7 @@
#include
#include
#include
+#include
#if __Lynx__
# include
#else
@@ -668,6 +669,46 @@ int redisxIgnoreReplyAsync(RedisClient *cl) {
return X_SUCCESS;
}
+static int rTypeIsParametrized(char type) {
+ switch(type) {
+ case RESP_INT:
+ case RESP_BULK_STRING:
+ case RESP_ARRAY:
+ case RESP3_SET:
+ case RESP3_PUSH:
+ case RESP3_MAP:
+ case RESP3_ATTRIBUTE:
+ case RESP3_BLOB_ERROR:
+ case RESP3_VERBATIM_STRING:
+ case RESP3_SNIPPET:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static void rPushMessage(RedisClient *cl, RESP *resp) {
+ int i;
+ RESP **array;
+
+ if(resp->n < 0) return;
+
+ array = (RESP **) calloc(resp->n, sizeof(RESP *));
+ if(!array) fprintf(stderr, "WARNING! Redis-X : not enough memory for push message (%d elements). Skipping.\n", resp->n);
+
+ for(i = 0; i < resp->n; i++) {
+ RESP *r = redisxReadReplyAsync(cl);
+ if(array) array[i] = r;
+ else redisxDestroyRESP(r);
+ }
+
+ resp->value = array;
+
+ // TODO push to consumer.
+
+ redisxDestroyRESP(resp);
+}
+
/**
* Reads a response from Redis and returns it.
*
@@ -702,61 +743,145 @@ RESP *redisxReadReplyAsync(RedisClient *cl) {
return NULL;
}
- size = rReadToken(cp, buf, REDIS_SIMPLE_STRING_SIZE + 1);
- if(size < 0) {
- // Either read/recv had an error, or we got garbage...
- if(cp->isEnabled) x_trace_null(fn, NULL);
- cp->isEnabled = FALSE; // Disable this client so we don't attempt to read from it again...
- return NULL;
- }
+ for(;;) {
+ size = rReadToken(cp, buf, REDIS_SIMPLE_STRING_SIZE + 1);
+ if(size < 0) {
+ // Either read/recv had an error, or we got garbage...
+ if(cp->isEnabled) x_trace_null(fn, NULL);
+ cp->isEnabled = FALSE; // Disable this client so we don't attempt to read from it again...
+ return NULL;
+ }
- resp = (RESP *) calloc(1, sizeof(RESP));
- x_check_alloc(resp);
- resp->type = buf[0];
-
- // Get the integer / size value...
- if(resp->type == RESP_ARRAY || resp->type == RESP_INT || resp->type == RESP_BULK_STRING) {
- char *tail;
- errno = 0;
- resp->n = (int) strtol(&buf[1], &tail, 10);
- if(errno) {
- fprintf(stderr, "WARNING! Redis-X : unparseable dimension '%s'\n", &buf[1]);
- status = X_PARSE_ERROR;
+ resp = (RESP *) calloc(1, sizeof(RESP));
+ x_check_alloc(resp);
+ resp->type = buf[0];
+
+ // Parametrized type.
+ if(rTypeIsParametrized(resp->type)) {
+
+ if(buf[1] == '?') {
+ // Streaming RESP in parts...
+ for(;;) {
+ RESP *r = redisxReadReplyAsync(cl);
+ if(r->type != RESP3_SNIPPET) {
+ int type = r->type;
+ redisxDestroyRESP(r);
+ fprintf(stderr, "WARNING! expected type '%c', got type '%c'.", resp->type, type);
+ return resp;
+ }
+
+ if(r->n == 0) {
+ if(resp->type == RESP3_PUSH) break;
+ return resp;
+ }
+
+ r->type = resp->type;
+ redisxAppendRESP(resp, r);
+ }
+ }
+ else {
+ // Get the integer / size value...
+ char *tail;
+ errno = 0;
+ resp->n = (int) strtol(&buf[1], &tail, 10);
+ if(errno) {
+ fprintf(stderr, "WARNING! Redis-X : unparseable dimension '%s'\n", &buf[1]);
+ status = X_PARSE_ERROR;
+ }
+ }
}
+
+ // Deal with push messages...
+ if(resp->type == RESP3_PUSH) rPushMessage(cl, resp);
+ else break;
}
+
// Now get the body of the response...
if(!status) switch(resp->type) {
+ case RESP3_NULL:
+ resp->n = 0;
+ break;
+
+ case RESP3_BOOLEAN: {
+ resp->n = 1;
+ switch(tolower(buf[1])) {
+ case 't': resp->n = TRUE; break;
+ case 'f': resp->n = FALSE; break;
+ default:
+ fprintf(stderr, "WARNING! Redis-X : invalid boolean value '%c'\n", buf[1]);
+ status = X_PARSE_ERROR;
+ }
+ break;
+ }
+
+ case RESP3_DOUBLE: {
+ // TODO inf / -inf?
+ double *dval = (double *) calloc(1, sizeof(double));
+ x_check_alloc(dval);
+
+ *dval = xParseDouble(&buf[1], NULL);
+ if(errno) {
+ fprintf(stderr, "WARNING! Redis-X : invalid double value '%s'\n", &buf[1]);
+ status = X_PARSE_ERROR;
+ }
+ resp->value = dval;
+ break;
+ }
+
+ case RESP3_SET:
+ case RESP3_PUSH:
case RESP_ARRAY: {
RESP **component;
int i;
if(resp->n <= 0) break;
- resp->value = (RESP **) malloc(resp->n * sizeof(RESP *));
- if(resp->value == NULL) {
+ component = (RESP **) malloc(resp->n * sizeof(RESP *));
+ if(component == NULL) {
status = x_error(X_FAILURE, errno, fn, "malloc() error (%d RESP)", resp->n);
// We should get the data from the input even if we have nowhere to store...
}
- component = (RESP **) resp->value;
for(i=0; in; i++) {
- RESP* r = redisxReadReplyAsync(cl); // Always read RESP even if we don't have storage for it...
- if(resp->value) component[i] = r;
+ RESP *r = redisxReadReplyAsync(cl); // Always read RESP even if we don't have storage for it...
+ if(component) component[i] = r;
+ else redisxDestroyRESP(r);
}
// Consistency check. Discard response if incomplete (because of read errors...)
- if(resp->value) for(i = 0; i < resp->n; i++) if(component[i] == NULL) {
+ if(component) for(i = 0; i < resp->n; i++) if(component[i] == NULL || component[i]->type != RESP3_NULL) {
fprintf(stderr, "WARNING! Redis-X : incomplete array received (index %d of %d).\n", (i+1), resp->n);
if(!status) status = REDIS_INCOMPLETE_TRANSFER;
break;
}
+ resp->value = component;
+
+ break;
+ }
+
+ case RESP3_MAP:
+ case RESP3_ATTRIBUTE: {
+ RedisMapEntry *component;
+ int i;
+ if(resp->n <= 0) break;
+
+ component = (RedisMapEntry *) calloc(resp->n, sizeof(RedisMapEntry));
+ x_check_alloc(component);
+
+ for(i=0; in; i++) {
+ RedisMapEntry *e = &component[i];
+ e->key = redisxReadReplyAsync(cl);
+ e->value = redisxReadReplyAsync(cl);
+ }
break;
}
+ case RESP3_BLOB_ERROR:
+ case RESP3_VERBATIM_STRING:
case RESP_BULK_STRING:
if(resp->n < 0) break; // no string token following!
@@ -781,6 +906,7 @@ RESP *redisxReadReplyAsync(RedisClient *cl) {
case RESP_SIMPLE_STRING:
case RESP_ERROR:
+ case RESP3_BIG_NUMBER:
resp->value = malloc(size);
if(resp->value == NULL) {
diff --git a/src/redisx-net.c b/src/redisx-net.c
index 4e276a4..596af0b 100644
--- a/src/redisx-net.c
+++ b/src/redisx-net.c
@@ -521,13 +521,13 @@ int rConnectClient(Redis *redis, enum redisx_channel channel) {
struct sockaddr_in serverAddress;
struct utsname u;
- const RedisPrivate *p;
+ RedisPrivate *p;
RedisClient *cl;
ClientPrivate *cp;
const char *channelID;
char host[200], *id;
- int status;
+ int status = X_SUCCESS;
int sock;
cl = redisxGetClient(redis, channel);
@@ -557,13 +557,34 @@ int rConnectClient(Redis *redis, enum redisx_channel channel) {
cp->socket = sock;
cp->isEnabled = TRUE;
- if(p->password) {
- status = rAuthAsync(cl);
- if(status) {
- rCloseClientAsync(cl);
- redisxUnlockClient(cl);
- return status;
+ if(p->hello) {
+ char proto[20];
+ RESP *reply;
+
+ // Try HELLO and see what we get back...
+ sprintf(proto, "%d", (int) p->protocol);
+
+ reply = redisxRequest(redis, "HELLO", proto, p->password, NULL, &status);
+ if(redisxCheckRESP(reply, RESP3_MAP, 0)) {
+ // OK, it looks like HELLO worked...
+ RedisMapEntry *e = redisxGetKeywordEntry(reply, "proto");
+ if(e && e->value->type == RESP_INT) p->protocol = e->value->n;
}
+ else p->hello = FALSE;
+
+ redisxDestroyRESP(reply);
+ }
+
+ if(p->hello) {
+ // No HELLO, go the old way...
+ p->protocol = REDISX_RESP2;
+ if(p->password) status = rAuthAsync(cl);
+ }
+
+ if(status) {
+ rCloseClientAsync(cl);
+ redisxUnlockClient(cl);
+ return status;
}
// Set the client name in Redis.
@@ -657,6 +678,7 @@ Redis *redisxInit(const char *server) {
p->addr = inet_addr((char *) ipAddress);
p->port = REDISX_TCP_PORT;
+ p->protocol = REDISX_RESP2; // Default
l = (ServerLink *) calloc(1, sizeof(ServerLink));
x_check_alloc(l);
diff --git a/src/redisx-tab.c b/src/redisx-tab.c
index 41638bf..ce56cca 100644
--- a/src/redisx-tab.c
+++ b/src/redisx-tab.c
@@ -73,7 +73,13 @@ RedisEntry *redisxGetTable(Redis *redis, const char *table, int *n) {
return x_trace_null(fn, NULL);
}
- *n = redisxCheckDestroyRESP(reply, RESP_ARRAY, 0);
+ // Cast RESP2 array respone to RESP3 map also...
+ if(reply && reply->type == RESP_ARRAY) {
+ reply->type = RESP3_MAP;
+ reply->n >>= 1;
+ }
+
+ *n = redisxCheckDestroyRESP(reply, RESP3_MAP, 0);
if(*n) {
return x_trace_null(fn, NULL);
}
@@ -81,6 +87,7 @@ RedisEntry *redisxGetTable(Redis *redis, const char *table, int *n) {
*n = reply->n / 2;
if(*n > 0) {
+ RedisMapEntry *dict = (RedisMapEntry *) reply->value;
entries = (RedisEntry *) calloc(*n, sizeof(RedisEntry));
if(entries == NULL) {
@@ -90,21 +97,19 @@ RedisEntry *redisxGetTable(Redis *redis, const char *table, int *n) {
int i;
for(i=0; in; i+=2) {
- RedisEntry *e = &entries[i>>1];
- RESP **component = (RESP **) reply->value;
-
- e->key = (char *) component[i]->value;
- e->value = (char *) component[i+1]->value;
- e->length = component[i+1]->n;
-
- // Dereference the values from the RESP
- component[i]->value = NULL;
- component[i+1]->value = NULL;
+ RedisEntry *e = &entries[i];
+ RedisMapEntry *component = &dict[i];
+ e->key = component->key->value;
+ e->value = component->value->value;
+
+ // Dereference the key/value so we don't destroy them with the reply.
+ component->key->value = NULL;
+ component->value->value = NULL;
}
}
}
- // Free the Reply container, but not the strings inside, which are returned.
+ // Free the reply container, but not the strings inside, which are returned.
redisxDestroyRESP(reply);
return entries;
}
@@ -344,7 +349,7 @@ int redisxMultiSetAsync(RedisClient *cl, const char *table, const RedisEntry *en
return x_trace(fn, NULL, X_FAILURE);
}
- req[0] = "HMSET";
+ req[0] = "HMSET"; // TODO, as of Redis 4.0.0, just use HSET...
req[1] = (char *) table;
for(i=0; ipriv) return x_error(X_NO_INIT, EINVAL, fn, "redis is not initialized");
+ if(redisxIsConnected(redis)) return x_error(X_ALREADY_OPEN, EALREADY, fn, "already connected");
+
+ p = (RedisPrivate *) redis->priv;
+ p->hello = TRUE;
+ p->protocol = protocol;
+
+ return X_SUCCESS;
+}
+
+/**
+ * Returns the actual protocol used with the Redis server. If HELLO was used during connection it will
+ * be the protocol that was confirmed in the response of HELLO (and which hopefully matches the
+ * protocol requested). Otherwise, RedisX will default to RESP2.
+ *
+ * @param redis The Redis server instance
+ * @return REDISX_RESP2 or REDISX_RESP3, or else an error code, such as X_NULL if the
+ * argument is NULL, or X_NO_INIT if the Redis server instance was not initialized.
+ *
+ * @sa redisxSetProtocol()
+ */
+enum redisx_protocol redisxGetProtocol(const Redis *redis) {
+ static const char *fn = "redisxGetProtocol";
+ const RedisPrivate *p;
+
+ if(!redis) return x_error(X_NULL, EINVAL, fn, "redis is NULL");
+ if(!redis->priv) return x_error(X_NO_INIT, EINVAL, fn, "redis is not initialized");
+
+ p = (RedisPrivate *) redis->priv;
+ return p->protocol;
+}
+
/**
* Sets the user-specific error handler to call if a socket level trasmit error occurs.
* It replaces any prior handlers set earlier.
@@ -382,74 +434,6 @@ int redisxSelectDB(Redis *redis, int idx) {
return status;
}
-/**
- * Frees up the resources used by a RESP structure that was dynamically allocated.
- * The call will segfault if the same RESP is destroyed twice or if the argument
- * is a static allocation.
- *
- * \param resp Pointer to the RESP structure to be destroyed, which may be NULL (no action taken).
- */
-void redisxDestroyRESP(RESP *resp) {
- if(resp == NULL) return;
- if(resp->type == RESP_ARRAY) while(--resp->n >= 0) {
- RESP **component = (RESP **) resp->value;
- redisxDestroyRESP(component[resp->n]);
- }
- if(resp->value != NULL) free(resp->value);
- free(resp);
-}
-
-
-/**
- * Checks a Redis RESP for NULL values or unexpected values.
- *
- * \param resp Pointer to the RESP structure from Redis.
- * \param expectedType The RESP type expected (e.g. RESP_ARRAY) or 0 if not checking type.
- * \param expectedSize The expected size of the RESP (array or bytes) or <=0 to skip checking
- *
- * \return X_SUCCESS (0) if the RESP passes the tests, or
- * X_NULL if the RESP is NULL (garbled response).
- * REDIS_NULL if Redis returned (nil),
- * REDIS_UNEXPECTED_TYPE if got a reply of a different type than expected
- * REDIS_UNEXPECTED_ARRAY_SIZE if got a reply of different size than expected.
- *
- * or the error returned in resp->n.
- *
- */
-int redisxCheckRESP(const RESP *resp, char expectedType, int expectedSize) {
- static const char *fn = "redisxCheckRESP";
-
- if(resp == NULL) return x_error(X_NULL, EINVAL, fn, "RESP is NULL");
- if(resp->type != RESP_INT) {
- if(resp->n < 0) return x_error(X_FAILURE, EBADMSG, fn, "RESP error code: %d", resp->n);
- if(resp->value == NULL) if(resp->n) return x_error(REDIS_NULL, ENOMSG, fn, "RESP with NULL value, n=%d", resp->n);
- }
- if(expectedType) if(resp->type != expectedType)
- return x_error(REDIS_UNEXPECTED_RESP, ENOMSG, fn, "unexpected RESP type: expected '%c', got '%c'", expectedType, resp->type);
- if(expectedSize > 0) if(resp->n != expectedSize)
- return x_error(REDIS_UNEXPECTED_RESP, ENOMSG, fn, "unexpected RESP size: expected %d, got %d", expectedSize, resp->n);
- return X_SUCCESS;
-}
-
-/**
- * Like redisxCheckRESP(), but it also destroys the RESP in case of an error.
- *
- * \param resp Pointer to the RESP structure from Redis.
- * \param expectedType The RESP type expected (e.g. RESP_ARRAY) or 0 if not checking type.
- * \param expectedSize The expected size of the RESP (array or bytes) or <=0 to skip checking
- *
- * \return The return value of redisxCheckRESP().
- *
- * \sa redisxCheckRESP()
- *
- */
-int redisxCheckDestroyRESP(RESP *resp, char expectedType, int expectedSize) {
- int status = redisxCheckRESP(resp, expectedType, expectedSize);
- if(status) redisxDestroyRESP(resp);
- prop_error("redisxCheckDestroyRESP", status);
- return status;
-}
-
/**
* Prints a descriptive error message to stderr, and returns the error code.
diff --git a/src/resp.c b/src/resp.c
new file mode 100644
index 0000000..d56d24a
--- /dev/null
+++ b/src/resp.c
@@ -0,0 +1,424 @@
+/**
+ * @file
+ *
+ * @date Created on Dec 6, 2024
+ * @author Attila Kovacs
+ */
+
+// We'll use gcc major version as a proxy for the glibc library to decide which feature macro to use.
+// gcc 5.1 was released 2015-04-22...
+#ifndef __GNUC__
+# define _DEFAULT_SOURCE ///< strcasecmp() feature macro starting glibc 2.20 (2014-09-08)
+#elif __GNUC__ >= 5 || __clang__
+# define _DEFAULT_SOURCE ///< strcasecmp() feature macro starting glibc 2.20 (2014-09-08)
+#else
+# define _BSD_SOURCE ///< strcasecmp() feature macro for glibc <= 2.19
+#endif
+
+
+#include
+#include
+#include
+#include
+#include
+
+#include "redisx-priv.h"
+
+/**
+ * Frees up the resources used by a RESP structure that was dynamically allocated.
+ * The call will segfault if the same RESP is destroyed twice or if the argument
+ * is a static allocation.
+ *
+ * \param resp Pointer to the RESP structure to be destroyed, which may be NULL (no action taken).
+ */
+void redisxDestroyRESP(RESP *resp) {
+ if(resp == NULL) return;
+
+ switch(resp->type) {
+ case RESP_ARRAY:
+ case RESP3_SET:
+ case RESP3_PUSH: {
+ RESP **component = (RESP **) resp->value;
+ while(--resp->n >= 0) redisxDestroyRESP(component[resp->n]);
+ break;
+ }
+ case RESP3_MAP:
+ case RESP3_ATTRIBUTE: {
+ RedisMapEntry *component = (RedisMapEntry *) resp->value;
+ while(--resp->n >= 0) {
+ RedisMapEntry *e = &component[resp->n];
+ redisxDestroyRESP(e->key);
+ redisxDestroyRESP(e->value);
+ }
+ break;
+ }
+ }
+
+ if(resp->value != NULL) free(resp->value);
+ free(resp);
+}
+
+
+/**
+ * Checks a Redis RESP for NULL values or unexpected values.
+ *
+ * \param resp Pointer to the RESP structure from Redis.
+ * \param expectedType The RESP type expected (e.g. RESP_ARRAY) or 0 if not checking type.
+ * \param expectedSize The expected size of the RESP (array or bytes) or <=0 to skip checking
+ *
+ * \return X_SUCCESS (0) if the RESP passes the tests, or
+ * X_NULL if the RESP is NULL (garbled response).
+ * REDIS_NULL if Redis returned (nil),
+ * REDIS_UNEXPECTED_TYPE if got a reply of a different type than expected
+ * REDIS_UNEXPECTED_ARRAY_SIZE if got a reply of different size than expected.
+ *
+ * or the error returned in resp->n.
+ *
+ */
+int redisxCheckRESP(const RESP *resp, char expectedType, int expectedSize) {
+ static const char *fn = "redisxCheckRESP";
+
+ if(resp == NULL) return x_error(X_NULL, EINVAL, fn, "RESP is NULL");
+ if(resp->type != RESP_INT) {
+ if(resp->n < 0) return x_error(X_FAILURE, EBADMSG, fn, "RESP error code: %d", resp->n);
+ if(resp->value == NULL) if(resp->n) return x_error(REDIS_NULL, ENOMSG, fn, "RESP with NULL value, n=%d", resp->n);
+ }
+ if(expectedType) if(resp->type != expectedType)
+ return x_error(REDIS_UNEXPECTED_RESP, ENOMSG, fn, "unexpected RESP type: expected '%c', got '%c'", expectedType, resp->type);
+ if(expectedSize > 0) if(resp->n != expectedSize)
+ return x_error(REDIS_UNEXPECTED_RESP, ENOMSG, fn, "unexpected RESP size: expected %d, got %d", expectedSize, resp->n);
+ return X_SUCCESS;
+}
+
+/**
+ * Like redisxCheckRESP(), but it also destroys the RESP in case of an error.
+ *
+ * \param resp Pointer to the RESP structure from Redis.
+ * \param expectedType The RESP type expected (e.g. RESP_ARRAY) or 0 if not checking type.
+ * \param expectedSize The expected size of the RESP (array or bytes) or <=0 to skip checking
+ *
+ * \return The return value of redisxCheckRESP().
+ *
+ * \sa redisxCheckRESP()
+ *
+ */
+int redisxCheckDestroyRESP(RESP *resp, char expectedType, int expectedSize) {
+ int status = redisxCheckRESP(resp, expectedType, expectedSize);
+ if(status) redisxDestroyRESP(resp);
+ prop_error("redisxCheckDestroyRESP", status);
+ return status;
+}
+
+
+/**
+ * Splits the string value of a RESP into two components, by terminating the first component with a null
+ * byte and optionally returning the remaining part and length in the output parameters. Only RESP_ERROR
+ * RESP_BLOB_ERROR and RESP_VERBATIM_STRING types can be split this way. All others will return
+ * REDIS_UNEXPECTED_RESP.
+ *
+ * @param resp The input RESP.
+ * @param[out] text (optional) pointer in which to return the start of the remnant text component.
+ * @return n the length of the remnant text (<=0), or else X_NULL if the input RESP was NULL,
+ * or REDIS_UNEXPEXCTED_RESP if the input RESP does not contain a two-component string
+ * value.
+ *
+ * @sa RESP_ERROR
+ * @sa RESP3_BLOB_ERROR
+ * @sa RESP3_VERBATIM_STRING
+ */
+int redisxSplitText(RESP *resp, char **text) {
+ static const char *fn = "redisxSplitText";
+ char *str;
+
+ if(!resp) return x_error(X_NULL, EINVAL, fn, "input RESP is NULL");
+
+ if(!resp->value) {
+ if(text) *text = NULL;
+ return 0;
+ }
+
+ str = (char *) resp->value;
+
+ switch(resp->type) {
+
+
+ case RESP3_VERBATIM_STRING:
+ if(resp->n < 4)
+ return x_error(X_PARSE_ERROR, ERANGE, fn, "value '%s' is too short (%d bytes) for verbatim string type", str, resp->n);
+ str[3] = '\0';
+ if(text) *text = &str[4];
+ return resp->n - 4;
+
+ case RESP_ERROR:
+ case RESP3_BLOB_ERROR: {
+ const char *code = strtok(str, " \t\r\n");
+ int offset = strlen(code) + 1;
+
+ if(offset < resp->n) {
+ if(text) *text = &str[offset];
+ return resp->n - offset - 1;
+ }
+ else {
+ if(text) *text = NULL;
+ return 0;
+ }
+ }
+ }
+
+ return x_error(REDIS_UNEXPECTED_RESP, EINVAL, fn, "RESP type '%c' does not have a two-component string value", resp->type);
+}
+
+/**
+ * Checks if a RESP holds a scalar type value, such as an integer, a boolean or a double-precision value, or a null value.
+ *
+ * @param r Pointer to a RESP data structure
+ * @return TRUE (1) if the data holds a scalar-type value, or else FALSE (0).
+ *
+ * @sa redisxIsStringType()
+ * @sa redisxIsArrayType()
+ * @sa redisxIsMapType()
+ * @sa RESP_INT
+ * @sa RESP3_BOOLEAN
+ * @sa RESP3_DOUBLE
+ * @sa RESP3_NULL
+ *
+ */
+boolean redisxIsScalarType(const RESP *r) {
+ if(!r) return FALSE;
+
+ switch(r->type) {
+ case RESP_INT:
+ case RESP3_BOOLEAN:
+ case RESP3_DOUBLE:
+ case RESP3_NULL:
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Checks if a RESP holds a string type value, whose `value` can be cast to `(char *)` to use.
+ *
+ * @param r Pointer to a RESP data structure
+ * @return TRUE (1) if the data holds a string type value, or else FALSE (0).
+ *
+ * @sa redisxIsScalarType()
+ * @sa redisxIsArrayType()
+ * @sa redisxIsMapType()
+ * @sa RESP_SIMPLE_STRING
+ * @sa RESP_ERROR
+ * @sa RESP_BULK_STRING
+ * @sa RESP3_BLOB_ERROR
+ * @sa RESP3_VERBATIM_STRING
+ *
+ */
+boolean redisxIsStringType(const RESP *r) {
+ if(!r) return FALSE;
+
+ switch(r->type) {
+ case RESP_SIMPLE_STRING:
+ case RESP_ERROR:
+ case RESP_BULK_STRING:
+ case RESP3_BLOB_ERROR:
+ case RESP3_VERBATIM_STRING:
+ case RESP3_BIG_NUMBER:
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Checks if a RESP holds an array of RESP pointers, and whose `value` can be cast to `(RESP **)` to use.
+ *
+ * @param r Pointer to a RESP data structure
+ * @return TRUE (1) if the data holds an array of `RESP *` pointers, or else FALSE (0).
+ *
+ * @sa redisxIsScalarType()
+ * @sa redisxIsStringType()
+ * @sa redisxIsMapType()
+ * @sa RESP_ARRAY
+ * @sa RESP3_SET
+ * @sa RESP3_PUSH
+ *
+ */
+boolean redisxIsArrayType(const RESP *r) {
+ if(!r) return FALSE;
+
+ switch(r->type) {
+ case RESP_ARRAY:
+ case RESP3_SET:
+ case RESP3_PUSH:
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Checks if a RESP holds a dictionary, and whose `value` can be cast to `(RedisMapEntry *)` to use.
+ *
+ * @param r Pointer to a RESP data structure
+ * @return TRUE (1) if the data holds a dictionary (a RedisMapEntry array), or else FALSE (0).
+ *
+ * @sa redisxIsScalarType()
+ * @sa redisxIsStringType()
+ * @sa redisxIsMapType()
+ * @sa RESP3_MAP
+ * @sa RESP3_ATTRIBUTE
+ *
+ */
+boolean redisxIsMapType(const RESP *r) {
+ if(!r) return FALSE;
+
+ switch(r->type) {
+ case RESP3_MAP:
+ case RESP3_ATTRIBUTE:
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+/**
+ * Appends a part to an existing RESP of the same type, before discarding the part.
+ *
+ * @param[in, out] resp The RESP to which the part is appended
+ * @param part The part, which is destroyed after the content is appended to the first RESP argument.
+ * @return X_SUCCESS (0) if successful, or else X_NULL if the first argument is NULL, or
+ * REDIS_UNEXPECTED_RESP if the types do not match, or X_FAILURE if there was an allocation
+ * error.
+ */
+int redisxAppendRESP(RESP *resp, RESP *part) {
+ static const char *fn = "redisxAppendRESP";
+ char *old, *extend;
+ size_t eSize;
+
+ if(!resp)
+ return x_error(X_NULL, EINVAL, fn, "NULL resp");
+ if(!part || part->type == RESP3_NULL || part->n <= 0)
+ return 0;
+ if(resp->type != part->type) {
+ int err = x_error(REDIS_UNEXPECTED_RESP, EINVAL, fn, "Mismatched types: '%c' vs. '%c'", resp->type, part->type);
+ redisxDestroyRESP(part);
+ return err;
+ }
+ if(redisxIsScalarType(resp))
+ return x_error(REDIS_UNEXPECTED_RESP, EINVAL, fn, "Cannot append to RESP type '%c'", resp->type);
+
+ if(redisxIsArrayType(resp))
+ eSize = sizeof(RESP *);
+ else if(redisxIsMapType(resp))
+ eSize = sizeof(RedisMapEntry);
+ else
+ eSize = 1;
+
+ old = resp->value;
+ extend = (char *) realloc(resp->value, resp->n + part->n);
+ if(!extend) {
+ free(old);
+ return x_error(X_FAILURE, errno, fn, "alloc RESP array (%d components)", resp->n + part->n);
+ }
+
+ memcpy(extend + resp->n * eSize, part->value, part->n * eSize);
+ resp->n += part->n;
+ resp->value = extend;
+ free(part);
+
+ return X_SUCCESS;
+}
+
+/**
+ * Checks if two RESP are equal, that is they hold the same type of data, have the same 'n' value,
+ * and the values match byte-for-byte, or are both NULL.
+ *
+ * @param a Ponter to a RESP data structure.
+ * @param b Pointer to another RESP data structure.
+ * @return TRUE (1) if the two RESP structures match, or else FALSE (0).
+ */
+boolean redisxIsEqualRESP(const RESP *a, const RESP *b) {
+ if(a == b) return TRUE;
+ if(!a || !b) return FALSE;
+
+
+ if(a->type != b->type) return FALSE;
+ if(a->n != b->n) return FALSE;
+ if(a->value == NULL) return (b->value == NULL);
+ if(!b->value) return FALSE;
+
+ return (memcmp(a->value, b->value, a->n) == 0);
+}
+
+/**
+ * Retrieves a keyed entry from a map-type RESP data structure.
+ *
+ * @param map The map-type REST data structure containing a dictionary
+ * @param key The RESP key to match
+ * @return The matching map entry or NULL if the map contains no such entry.
+ *
+ * @sa RESP3_MAP
+ * @sa RESP3_ATTRIBUTE
+ *
+ * @sa redisxGetKeywordEntry()
+ */
+RedisMapEntry *redisxGetMapEntry(const RESP *map, const RESP *key) {
+ int i;
+ RedisMapEntry *entries;
+
+ if(!key) return NULL;
+ if(!redisxIsMapType(map)) return NULL;
+ if(!map->value) return NULL;
+
+ entries = (RedisMapEntry *) map->value;
+
+ for(i = 0; i < map->n; i++) {
+ RedisMapEntry *e = &entries[i];
+
+ if(e->key->type != key->type) continue;
+ if(e->key->n != key->n) continue;
+ if(key->value == NULL) {
+ if(e->key->value == NULL) return e;
+ continue;
+ }
+ if(e->key->value == NULL) continue;
+ if(memcmp(e->key->value, key->value, key->n) == 0) return e;
+ }
+
+ return NULL;
+}
+
+/**
+ * Retrieves a entry, by its string keyword, from a map-type RESP data structure.
+ *
+ * @param map The map-type REST data structure containing a dictionary
+ * @param key The string keyword to match
+ * @return The matching map entry or NULL if the map contains no such entry.
+ *
+ * @sa RESP3_MAP
+ * @sa RESP3_ATTRIBUTE
+ *
+ * @sa redisxGetMapEntry()
+ */
+RedisMapEntry *redisxGetKeywordEntry(const RESP *map, const char *key) {
+ int i;
+ RedisMapEntry *entries;
+
+ if(!key) return NULL;
+ if(!redisxIsMapType(map)) return NULL;
+ if(!map->value) return NULL;
+
+ entries = (RedisMapEntry *) map->value;
+
+ for(i = 0; i < map->n; i++) {
+ RedisMapEntry *e = &entries[i];
+
+ if(!redisxIsStringType(e->key)) continue;
+ if(strcmp(e->key->value, key) == 0) return e;
+ }
+
+ return NULL;
+}
+
+