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: + *
    + *
  1. https://github.com/redis/redis-specifications/tree/master/protocol
  2. + *
+ * * \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; +} + +