diff --git a/src/cli_common.c b/src/cli_common.c index 7064a096b..6d627d2b4 100644 --- a/src/cli_common.c +++ b/src/cli_common.c @@ -371,3 +371,28 @@ void freeCliConnInfo(cliConnInfo connInfo){ if (connInfo.auth) sdsfree(connInfo.auth); if (connInfo.user) sdsfree(connInfo.user); } + +/* + * Escape a Unicode string for JSON output (--json), following RFC 7159: + * https://datatracker.ietf.org/doc/html/rfc7159#section-7 +*/ +sds escapeJsonString(sds s, const char *p, size_t len) { + s = sdscatlen(s,"\"",1); + while(len--) { + switch(*p) { + case '\\': + case '"': + s = sdscatprintf(s,"\\%c",*p); + break; + case '\n': s = sdscatlen(s,"\\n",2); break; + case '\f': s = sdscatlen(s,"\\f",2); break; + case '\r': s = sdscatlen(s,"\\r",2); break; + case '\t': s = sdscatlen(s,"\\t",2); break; + case '\b': s = sdscatlen(s,"\\b",2); break; + default: + s = sdscatprintf(s,(*p >= 0 && *p <= 0x1f) ? "\\u%04x" : "%c",*p); + } + p++; + } + return sdscatlen(s,"\"",1); +} diff --git a/src/cli_common.h b/src/cli_common.h index 1cb76c6b9..c5c4c11aa 100644 --- a/src/cli_common.h +++ b/src/cli_common.h @@ -48,4 +48,7 @@ sds unquoteCString(char *str); void parseRedisUri(const char *uri, const char* tool_name, cliConnInfo *connInfo, int *tls_flag); void freeCliConnInfo(cliConnInfo connInfo); + +sds escapeJsonString(sds s, const char *p, size_t len); + #endif /* __CLICOMMON_H */ diff --git a/src/redis-cli.c b/src/redis-cli.c index bbbe6d6ec..809f0ce78 100644 --- a/src/redis-cli.c +++ b/src/redis-cli.c @@ -70,6 +70,7 @@ #define OUTPUT_RAW 1 #define OUTPUT_CSV 2 #define OUTPUT_JSON 3 +#define OUTPUT_QUOTED_JSON 4 #define REDIS_CLI_KEEPALIVE_INTERVAL 15 /* seconds */ #define REDIS_CLI_DEFAULT_PIPE_TIMEOUT 30 /* seconds */ #define REDIS_CLI_HISTFILE_ENV "REDISCLI_HISTFILE" @@ -1486,16 +1487,39 @@ static sds cliFormatReplyCSV(redisReply *r) { return out; } -static sds cliFormatReplyJson(sds out, redisReply *r) { +/* Append specified buffer to out and return it, using required JSON output + * mode. */ +static sds jsonStringOutput(sds out, const char *p, int len, int mode) { + if (mode == OUTPUT_JSON) { + return escapeJsonString(out, p, len); + } else if (mode == OUTPUT_QUOTED_JSON) { + /* Need to double-quote backslashes */ + sds tmp = sdscatrepr(sdsempty(), p, len); + int tmplen = sdslen(tmp); + char *n = tmp; + while (tmplen--) { + if (*n == '\\') out = sdscatlen(out, "\\\\", 2); + else out = sdscatlen(out, n, 1); + n++; + } + + sdsfree(tmp); + return out; + } else { + assert(0); + } +} + +static sds cliFormatReplyJson(sds out, redisReply *r, int mode) { unsigned int i; switch (r->type) { case REDIS_REPLY_ERROR: out = sdscat(out,"error:"); - out = sdscatrepr(out,r->str,strlen(r->str)); + out = jsonStringOutput(out,r->str,strlen(r->str),mode); break; case REDIS_REPLY_STATUS: - out = sdscatrepr(out,r->str,r->len); + out = jsonStringOutput(out,r->str,r->len,mode); break; case REDIS_REPLY_INTEGER: out = sdscatprintf(out,"%lld",r->integer); @@ -1505,7 +1529,7 @@ static sds cliFormatReplyJson(sds out, redisReply *r) { break; case REDIS_REPLY_STRING: case REDIS_REPLY_VERB: - out = sdscatrepr(out,r->str,r->len); + out = jsonStringOutput(out,r->str,r->len,mode); break; case REDIS_REPLY_NIL: out = sdscat(out,"null"); @@ -1518,7 +1542,7 @@ static sds cliFormatReplyJson(sds out, redisReply *r) { case REDIS_REPLY_PUSH: out = sdscat(out,"["); for (i = 0; i < r->elements; i++ ) { - out = cliFormatReplyJson(out, r->element[i]); + out = cliFormatReplyJson(out,r->element[i],mode); if (i != r->elements-1) out = sdscat(out,","); } out = sdscat(out,"]"); @@ -1527,20 +1551,25 @@ static sds cliFormatReplyJson(sds out, redisReply *r) { out = sdscat(out,"{"); for (i = 0; i < r->elements; i += 2) { redisReply *key = r->element[i]; - if (key->type == REDIS_REPLY_STATUS || + if (key->type == REDIS_REPLY_ERROR || + key->type == REDIS_REPLY_STATUS || key->type == REDIS_REPLY_STRING || - key->type == REDIS_REPLY_VERB) { - out = cliFormatReplyJson(out, key); + key->type == REDIS_REPLY_VERB) + { + out = cliFormatReplyJson(out,key,mode); } else { - /* According to JSON spec, JSON map keys must be strings, */ - /* and in RESP3, they can be other types. */ - sds tmp = cliFormatReplyJson(sdsempty(), key); - out = sdscatrepr(out,tmp,sdslen(tmp)); - sdsfree(tmp); + /* According to JSON spec, JSON map keys must be strings, + * and in RESP3, they can be other types. + * The first one(cliFormatReplyJson) is to convert non string type to string + * The Second one(escapeJsonString) is to escape the converted string */ + sds keystr = cliFormatReplyJson(sdsempty(),key,mode); + if (keystr[0] == '"') out = sdscatsds(out,keystr); + else out = sdscatfmt(out,"\"%S\"",keystr); + sdsfree(keystr); } out = sdscat(out,":"); - out = cliFormatReplyJson(out, r->element[i+1]); + out = cliFormatReplyJson(out,r->element[i+1],mode); if (i != r->elements-2) out = sdscat(out,","); } out = sdscat(out,"}"); @@ -1566,8 +1595,8 @@ static sds cliFormatReply(redisReply *reply, int mode, int verbatim) { } else if (mode == OUTPUT_CSV) { out = cliFormatReplyCSV(reply); out = sdscatlen(out, "\n", 1); - } else if (mode == OUTPUT_JSON) { - out = cliFormatReplyJson(sdsempty(), reply); + } else if (mode == OUTPUT_JSON || mode == OUTPUT_QUOTED_JSON) { + out = cliFormatReplyJson(sdsempty(), reply, mode); out = sdscatlen(out, "\n", 1); } else { fprintf(stderr, "Error: Unknown output encoding %d\n", mode); @@ -1953,11 +1982,17 @@ static int parseOptions(int argc, char **argv) { } else if (!strcmp(argv[i],"--csv")) { config.output = OUTPUT_CSV; } else if (!strcmp(argv[i],"--json")) { - /* Not overwrite explicit value by -3*/ + /* Not overwrite explicit value by -3 */ if (config.resp3 == 0) { config.resp3 = 2; } config.output = OUTPUT_JSON; + } else if (!strcmp(argv[i],"--quoted-json")) { + /* Not overwrite explicit value by -3*/ + if (config.resp3 == 0) { + config.resp3 = 2; + } + config.output = OUTPUT_QUOTED_JSON; } else if (!strcmp(argv[i],"--latency")) { config.latency_mode = 1; } else if (!strcmp(argv[i],"--latency-dist")) { @@ -2289,6 +2324,7 @@ static void usage(int err) { " --quoted-input Force input to be handled as quoted strings.\n" " --csv Output in CSV format.\n" " --json Output in JSON format (default RESP3, use -2 if you want to use with RESP2).\n" +" --quoted-json Same as --json, but produce ASCII-safe quoted strings, not Unicode.\n" " --show-pushes Whether to print RESP3 PUSH messages. Enabled by default when\n" " STDOUT is a tty but can be overridden with --show-pushes no.\n" " --stat Print rolling stats about server: mem, clients, ...\n",version); diff --git a/tests/integration/redis-cli.tcl b/tests/integration/redis-cli.tcl index 88b189ac0..c26722961 100644 --- a/tests/integration/redis-cli.tcl +++ b/tests/integration/redis-cli.tcl @@ -228,6 +228,30 @@ start_server {tags {"cli"}} { file delete $tmpfile } + test_tty_cli "Escape character in JSON mode" { + # reverse solidus + r hset solidus \/ \/ + assert_equal \/ \/ [run_cli hgetall solidus] + set escaped_reverse_solidus \"\\" + assert_equal $escaped_reverse_solidus $escaped_reverse_solidus [run_cli --json hgetall \/] + # non printable (0xF0 in ISO-8859-1, not UTF-8(0xC3 0xB0)) + set eth "\xf0\x65" + r hset eth test $eth + assert_equal \"\\xf0e\" [run_cli hget eth test] + assert_equal \"\xf0e\" [run_cli --json hget eth test] + assert_equal \"\\\\xf0e\" [run_cli --quoted-json hget eth test] + # control characters + r hset control test "Hello\x00\x01\x02\x03World" + assert_equal \"Hello\\u0000\\u0001\\u0002\\u0003World" [run_cli --json hget control test] + # non-string keys + r hset numkey 1 One + assert_equal \{\"1\":\"One\"\} [run_cli --json hgetall numkey] + # non-string, non-printable keys + r hset npkey "K\x00\x01ey" "V\x00\x01alue" + assert_equal \{\"K\\u0000\\u0001ey\":\"V\\u0000\\u0001alue\"\} [run_cli --json hgetall npkey] + assert_equal \{\"K\\\\x00\\\\x01ey\":\"V\\\\x00\\\\x01alue\"\} [run_cli --quoted-json hgetall npkey] + } + test_nontty_cli "Status reply" { assert_equal "OK" [run_cli set key bar] assert_equal "bar" [r get key]