diff --git a/include/redisx.h b/include/redisx.h index d823dd9..b9f8018 100644 --- a/include/redisx.h +++ b/include/redisx.h @@ -17,22 +17,22 @@ // Configuration constants -------------------------------------------------------> #ifndef REDISX_TCP_PORT /// Default TCP/IP port on which Redis server listens to clients. -# define REDISX_TCP_PORT 6379 +# define REDISX_TCP_PORT 6379 #endif #ifndef REDISX_TCP_BUF_SIZE /// (bytes) Default TCP buffer size (send/recv) for Redis clients. Values <= 0 will use system default. -# define REDISX_TCP_BUF_SIZE 0 +# define REDISX_TCP_BUF_SIZE 0 #endif #ifndef REDISX_CMDBUF_SIZE /// (bytes) Size of many internal arrays, and the max. send chunk size. At least ~16 bytes... -# define REDISX_CMDBUF_SIZE 8192 +# define REDISX_CMDBUF_SIZE 8192 #endif #ifndef REDISX_RCVBUF_SIZE /// (bytes) Redis receive buffer size (at most that much is read from the socket in a single call). -# define REDISX_RCVBUF_SIZE 8192 +# define REDISX_RCVBUF_SIZE 8192 #endif #ifndef REDISX_SET_LISTENER_PRIORITY @@ -55,7 +55,7 @@ #define REDISX_MINOR_VERSION 9 /// Integer sub version of the release -#define REDISX_PATCHLEVEL 1 +#define REDISX_PATCHLEVEL 2 /// Additional release information in version, e.g. "-1", or "-rc1". #define REDISX_RELEASE_STRING "-devel" @@ -107,9 +107,9 @@ enum resp_type { RESP3_SET = '~', ///< \hideinitializer RESP3 unordered set of elements RESP3_ATTRIBUTE = '|', ///< \hideinitializer RESP3 dictionary of attributes (metadata) RESP3_PUSH = '>', ///< \hideinitializer RESP3 dictionary of attributes (metadata) - RESP3_CONTINUED = ';' ///< \hideinitializer RESP3 dictionary of attributes (metadata) }; +#define RESP3_CONTINUED ';' ///< \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 @@ -158,7 +158,7 @@ enum redisx_protocol { * \sa redisxIsMapType() */ typedef struct RESP { - enum resp_type type; ///< RESP type RESP_ARRAY, RESP_INT ... + enum resp_type type; ///< RESP type; RESP_ARRAY, RESP_INT ... int n; ///< Either the integer value of a RESP_INT or a RESP3_BOOLEAN response, or the ///< dimension of the value field. void *value; ///< Pointer to text (char *) content or to an array of components @@ -395,6 +395,7 @@ boolean redisxIsMapType(const RESP *r); boolean redisxHasComponents(const RESP *r); boolean redisxIsEqualRESP(const RESP *a, const RESP *b); int redisxSplitText(RESP *resp, char **text); +XField *resp2XField(const char *name, const RESP *resp); RedisMapEntry *redisxGetMapEntry(const RESP *map, const RESP *key); RedisMapEntry *redisxGetKeywordEntry(const RESP *map, const char *key); diff --git a/src/redisx-client.c b/src/redisx-client.c index 030bd3a..e43c31a 100644 --- a/src/redisx-client.c +++ b/src/redisx-client.c @@ -921,6 +921,9 @@ RESP *redisxReadReplyAsync(RedisClient *cl) { resp->n = 0; break; + case RESP_INT: // Nothing left to do for INT type response. + break; + case RESP3_BOOLEAN: { switch(tolower(buf[1])) { case 't': resp->n = TRUE; break; @@ -946,9 +949,9 @@ RESP *redisxReadReplyAsync(RedisClient *cl) { break; } + case RESP_ARRAY: case RESP3_SET: - case RESP3_PUSH: - case RESP_ARRAY: { + case RESP3_PUSH: { RESP **component; int i; @@ -999,9 +1002,9 @@ RESP *redisxReadReplyAsync(RedisClient *cl) { break; } + case RESP_BULK_STRING: case RESP3_BLOB_ERROR: case RESP3_VERBATIM_STRING: - case RESP_BULK_STRING: if(resp->n < 0) break; // no string token following! resp->value = malloc(resp->n + 2); // \r\n @@ -1040,9 +1043,6 @@ RESP *redisxReadReplyAsync(RedisClient *cl) { break; - case RESP_INT: // Nothing left to do for INT type response. - break; - default: // FIXME workaround for Redis 4.x improper OK reply to QUIT if(!strcmp(buf, "OK")) { diff --git a/src/resp.c b/src/resp.c index f8cfffd..7f27924 100644 --- a/src/resp.c +++ b/src/resp.c @@ -3,6 +3,9 @@ * * @date Created on Dec 6, 2024 * @author Attila Kovacs + * + * A set of utilities for handling RESP responses from a Redis / Valkey server. + * */ // We'll use gcc major version as a proxy for the glibc library to decide which feature macro to use. @@ -517,4 +520,194 @@ RedisMapEntry *redisxGetKeywordEntry(const RESP *map, const char *key) { return NULL; } +static XType resp2xType(enum resp_type type) { + switch(type) { + case RESP3_NULL: + return X_UNKNOWN; + case RESP3_BOOLEAN: + return X_BOOLEAN; + case RESP_INT: + return X_LONG; + case RESP3_DOUBLE: + return X_DOUBLE; + case RESP_SIMPLE_STRING: + case RESP_BULK_STRING: + case RESP_ERROR: + case RESP3_BLOB_ERROR: + case RESP3_VERBATIM_STRING: + case RESP3_BIG_NUMBER: + return X_STRING; + case RESP_ARRAY: + case RESP3_SET: + case RESP3_PUSH: + return X_FIELD; + case RESP3_MAP: + case RESP3_ATTRIBUTE: + return X_STRUCT; + } + + return X_UNKNOWN; +} + + +static XField *respArrayToXField(const char *name, const RESP **component, int n) { + static const char *fn = "respArrayToXField"; + + XField *f; + enum resp_type type = RESP3_NULL; + int i; + + if(n < 0) return NULL; + + for(i = 0; i < n; i++) { + if(i == 0) type = component[i]->type; + else if(component[i]->type != type) break; + } + + if(i < n) { + // -------------------------------------------------------- + // Heterogeneous array... + + XField *array; + + f = xCreate1DFieldArray(name, n); + + if(!f->value) return x_trace_null(fn, "field array"); + + array = (XField *) f->value; + + for(i = 0; i < n; i++) { + XField *e = resp2XField(array[i].name, component[i]); + if(e) { + array[i] = *e; + free(e); + } + } + } + + else { + // -------------------------------------------------------- + // Homogeneous array... + + XType eType = resp2xType(type); + char *array; + size_t eSize; + + if(eType == X_UNKNOWN) return NULL; + + eSize = xElementSizeOf(eType); + array = (char *) calloc(1, n * eSize); + + f = xCreate1DField(name, eType, n, array); + f->flags = type; + if(!array) return x_trace_null(fn, "field array"); + + for(i = 0; i < n; i++) { + XField *e = resp2XField("", component[i]); + if(e) { + memcpy(&array[i * eSize], e, sizeof(XField)); + free(e); + } + } + } + + return f; +} + + +static XField *respMap2XField(const char *name, const RedisMapEntry *map, int n) { + XStructure *s = xCreateStruct(); + XField *f; + + while(--n >= 0) { + const RedisMapEntry *e = &map[n]; + XField *fi = NULL; + if(redisxIsStringType(e->key)) { + fi = resp2XField((char *) e->key->value, e->value->value); + fi->next = s->firstField; + s->firstField = fi; + } + else { + xvprintf("WARNING! cannot convert RESP map entry with non-string key"); + errno = ENOSYS; + } + } + + f = xCreateScalarField(name, X_STRUCT, s); + return f; +} + +/** + * Converts a RESP to the xchange representation as an appropriate XField. + * + * + * + * @param name + * @param resp + * @return + */ +XField *resp2XField(const char *name, const RESP *resp) { + static const char *fn = "resp2XField"; + + errno = 0; + + if(!resp) { + x_error(0, EINVAL, fn, "input RESP is NULL"); + return NULL; + } + + switch(resp->type) { + case RESP3_NULL: + return NULL; + + case RESP3_BOOLEAN: + return xCreateBooleanField(name, resp->n); + + case RESP_INT: + return xCreateIntField(name, resp->n); + + case RESP3_DOUBLE: + return xCreateDoubleField(name, *(double *) resp->value); + + case RESP_SIMPLE_STRING: + case RESP_BULK_STRING: + case RESP_ERROR: + case RESP3_BLOB_ERROR: + case RESP3_VERBATIM_STRING: + case RESP3_BIG_NUMBER: { + XField *f = xCreateStringField(name, (char *) resp->value); + f->flags = resp->type; + return f; + } + + case RESP_ARRAY: + case RESP3_SET: + case RESP3_PUSH: { + XField *f = respArrayToXField(name, (const RESP **) resp->value, resp->n); + if(!f) return x_trace_null(fn, NULL); + f->flags = resp->type; + return f; + } + + case RESP3_MAP: + case RESP3_ATTRIBUTE: { + XField *f = respMap2XField(name, (const RedisMapEntry *) resp->value, resp->n); + if(!f) return x_trace_null(fn, NULL); + f->flags = resp->type; + return f; + } + + } + + return NULL; +}