From: Rusty Russell Date: Tue, 21 May 2019 04:21:12 +0000 (+0930) Subject: json_escape: new module to escape JSON strings. X-Git-Url: http://git.ozlabs.org/?p=ccan;a=commitdiff_plain;h=45f9ce170fe09bca014d874ffaa13b900c8285ba;hp=7623d0829a63e7f95ed6f116b7e9f2f4a023e8e7 json_escape: new module to escape JSON strings. Signed-off-by: Rusty Russell --- diff --git a/ccan/json_escape/LICENSE b/ccan/json_escape/LICENSE new file mode 120000 index 00000000..2354d129 --- /dev/null +++ b/ccan/json_escape/LICENSE @@ -0,0 +1 @@ +../../licenses/BSD-MIT \ No newline at end of file diff --git a/ccan/json_escape/_info b/ccan/json_escape/_info new file mode 100644 index 00000000..934857f8 --- /dev/null +++ b/ccan/json_escape/_info @@ -0,0 +1,39 @@ +#include "config.h" +#include +#include + +/** + * json_escape - Escape sequences for JSON strings + * + * This code helps you format strings into forms useful for JSON. + * + * Author: Rusty Russell + * License: BSD-MIT + * Example: + * // Print arguments as a JSON array. + * #include + * + * int main(int argc, char *argv[]) + * { + * printf("["); + * for (int i = 1; i < argc; i++) { + * struct json_escape *e = json_escape(NULL, argv[i]); + * printf("%s\"%s\"", i == 1 ? "" : ",", e->s); + * } + * printf("]\n"); + * return 0; + * } + */ +int main(int argc, char *argv[]) +{ + /* Expect exactly one argument */ + if (argc != 2) + return 1; + + if (strcmp(argv[1], "depends") == 0) { + printf("ccan/tal\n"); + return 0; + } + + return 1; +} diff --git a/ccan/json_escape/json_escape.c b/ccan/json_escape/json_escape.c new file mode 100644 index 00000000..d344940d --- /dev/null +++ b/ccan/json_escape/json_escape.c @@ -0,0 +1,157 @@ +/* MIT (BSD) license - see LICENSE file for details */ +#include +#include + +struct json_escape *json_escape_string_(const tal_t *ctx, + const void *bytes, size_t len) +{ + struct json_escape *esc; + + esc = (void *)tal_arr_label(ctx, char, len + 1, + TAL_LABEL(struct json_escape, "")); + memcpy(esc->s, bytes, len); + esc->s[len] = '\0'; + return esc; +} + +bool json_escape_eq(const struct json_escape *a, const struct json_escape *b) +{ + return streq(a->s, b->s); +} + +static struct json_escape *escape(const tal_t *ctx, + const char *str TAKES, + size_t len, + bool partial) +{ + struct json_escape *esc; + size_t i, n; + + /* Worst case: all \uXXXX */ + esc = (struct json_escape *)tal_arr(ctx, char, len * 6 + 1); + + for (i = n = 0; i < len; i++, n++) { + char escape = 0; + switch (str[i]) { + case '\n': + escape = 'n'; + break; + case '\b': + escape = 'b'; + break; + case '\f': + escape = 'f'; + break; + case '\t': + escape = 't'; + break; + case '\r': + escape = 'r'; + break; + case '\\': + if (partial) { + /* Don't double-escape standard escapes. */ + if (str[i+1] == 'n' + || str[i+1] == 'b' + || str[i+1] == 'f' + || str[i+1] == 't' + || str[i+1] == 'r' + || str[i+1] == '/' + || str[i+1] == '\\' + || str[i+1] == '"') { + escape = str[i+1]; + i++; + break; + } + if (str[i+1] == 'u' + && cisxdigit(str[i+2]) + && cisxdigit(str[i+3]) + && cisxdigit(str[i+4]) + && cisxdigit(str[i+5])) { + memcpy(esc->s + n, str + i, 6); + n += 5; + i += 5; + continue; + } + } /* fall thru */ + case '"': + escape = str[i]; + break; + default: + if ((unsigned)str[i] < ' ' || str[i] == 127) { + snprintf(esc->s + n, 7, "\\u%04X", str[i]); + n += 5; + continue; + } + } + if (escape) { + esc->s[n++] = '\\'; + esc->s[n] = escape; + } else + esc->s[n] = str[i]; + } + + esc->s[n] = '\0'; + if (taken(str)) + tal_free(str); + return esc; +} + +struct json_escape *json_partial_escape(const tal_t *ctx, const char *str TAKES) +{ + return escape(ctx, str, strlen(str), true); +} + +struct json_escape *json_escape(const tal_t *ctx, const char *str TAKES) +{ + return escape(ctx, str, strlen(str), false); +} + +struct json_escape *json_escape_len(const tal_t *ctx, const char *str TAKES, + size_t len) +{ + return escape(ctx, str, len, false); +} + +/* By policy, we don't handle \u. Use UTF-8. */ +const char *json_escape_unescape(const tal_t *ctx, const struct json_escape *esc) +{ + char *unesc = tal_arr(ctx, char, strlen(esc->s) + 1); + size_t i, n; + + for (i = n = 0; esc->s[i]; i++, n++) { + if (esc->s[i] != '\\') { + unesc[n] = esc->s[i]; + continue; + } + + i++; + switch (esc->s[i]) { + case 'n': + unesc[n] = '\n'; + break; + case 'b': + unesc[n] = '\b'; + break; + case 'f': + unesc[n] = '\f'; + break; + case 't': + unesc[n] = '\t'; + break; + case 'r': + unesc[n] = '\r'; + break; + case '/': + case '\\': + case '"': + unesc[n] = esc->s[i]; + break; + default: + return tal_free(unesc); + } + } + + unesc[n] = '\0'; + return unesc; +} diff --git a/ccan/json_escape/json_escape.h b/ccan/json_escape/json_escape.h new file mode 100644 index 00000000..b8e0dfca --- /dev/null +++ b/ccan/json_escape/json_escape.h @@ -0,0 +1,43 @@ +/* MIT (BSD) license - see LICENSE file for details */ +#ifndef CCAN_JSON_ESCAPE_H +#define CCAN_JSON_ESCAPE_H +#include "config.h" +#include + +/* Type differentiation for a correctly-escaped JSON string */ +struct json_escape { + /* NUL terminated string. */ + char s[1]; +}; + +/** + * json_escape - escape a valid UTF-8 string. + * @ctx: tal context to allocate from. + * @str: the string to escape. + * + * Allocates and returns a valid JSON string (without surrounding quotes). + */ +struct json_escape *json_escape(const tal_t *ctx, const char *str TAKES); + +/* Version with @len */ +struct json_escape *json_escape_len(const tal_t *ctx, + const char *str TAKES, size_t len); + +/* @str is a valid UTF-8 string which may already contain escapes. */ +struct json_escape *json_partial_escape(const tal_t *ctx, + const char *str TAKES); + +/* Extract a JSON-escaped string. */ + +/* Are two escape json strings identical? */ +bool json_escape_eq(const struct json_escape *a, + const struct json_escape *b); + +/* Internal routine for creating json_escape from bytes. */ +struct json_escape *json_escape_string_(const tal_t *ctx, + const void *bytes, size_t len); + +/* Be very careful here! Can fail! Doesn't handle \u: use UTF-8 please. */ +const char *json_escape_unescape(const tal_t *ctx, + const struct json_escape *esc); +#endif /* CCAN_JSON_ESCAPE_H */ diff --git a/ccan/json_escape/test/run-partial.c b/ccan/json_escape/test/run-partial.c new file mode 100644 index 00000000..25764df0 --- /dev/null +++ b/ccan/json_escape/test/run-partial.c @@ -0,0 +1,41 @@ +#include +/* Include the C files directly. */ +#include +#include + +int main(void) +{ + const tal_t *ctx = tal(NULL, char); + + /* This is how many tests you plan to run */ + plan_tests(21); + + ok1(!strcmp(json_partial_escape(ctx, "\\")->s, "\\\\")); + ok1(!strcmp(json_partial_escape(ctx, "\\\\")->s, "\\\\")); + ok1(!strcmp(json_partial_escape(ctx, "\\\\\\")->s, "\\\\\\\\")); + ok1(!strcmp(json_partial_escape(ctx, "\\\\\\\\")->s, "\\\\\\\\")); + ok1(!strcmp(json_partial_escape(ctx, "\\n")->s, "\\n")); + ok1(!strcmp(json_partial_escape(ctx, "\n")->s, "\\n")); + ok1(!strcmp(json_partial_escape(ctx, "\\\"")->s, "\\\"")); + ok1(!strcmp(json_partial_escape(ctx, "\"")->s, "\\\"")); + ok1(!strcmp(json_partial_escape(ctx, "\\t")->s, "\\t")); + ok1(!strcmp(json_partial_escape(ctx, "\t")->s, "\\t")); + ok1(!strcmp(json_partial_escape(ctx, "\\b")->s, "\\b")); + ok1(!strcmp(json_partial_escape(ctx, "\b")->s, "\\b")); + ok1(!strcmp(json_partial_escape(ctx, "\\r")->s, "\\r")); + ok1(!strcmp(json_partial_escape(ctx, "\r")->s, "\\r")); + ok1(!strcmp(json_partial_escape(ctx, "\\f")->s, "\\f")); + ok1(!strcmp(json_partial_escape(ctx, "\f")->s, "\\f")); + /* You're allowed to escape / according to json.org. */ + ok1(!strcmp(json_partial_escape(ctx, "\\/")->s, "\\/")); + ok1(!strcmp(json_partial_escape(ctx, "/")->s, "/")); + + ok1(!strcmp(json_partial_escape(ctx, "\\u0FFF")->s, "\\u0FFF")); + ok1(!strcmp(json_partial_escape(ctx, "\\u0FFFx")->s, "\\u0FFFx")); + + /* Unknown escapes should be escaped. */ + ok1(!strcmp(json_partial_escape(ctx, "\\x")->s, "\\\\x")); + tal_free(ctx); + + return 0; +} diff --git a/ccan/json_escape/test/run.c b/ccan/json_escape/test/run.c new file mode 100644 index 00000000..9d8b049c --- /dev/null +++ b/ccan/json_escape/test/run.c @@ -0,0 +1,44 @@ +#include +/* Include the C files directly. */ +#include +#include + +int main(void) +{ + const tal_t *ctx = tal(NULL, char); + struct json_escape *e; + + /* This is how many tests you plan to run */ + plan_tests(6); + + e = json_escape(ctx, "Hello"); + ok1(!strcmp(e->s, "Hello")); + ok1(!strcmp(json_escape_unescape(ctx, e), + "Hello")); + + e = json_escape(ctx, + "\\\b\f\n\r\t\"" + "\\\\\\b\\f\\n\\r\\t\\\""); + ok1(!strcmp(e->s, + "\\\\\\b\\f\\n\\r\\t\\\"" + "\\\\\\\\\\\\b\\\\f\\\\n\\\\r\\\\t\\\\\\\"")); + ok1(!strcmp(json_escape_unescape(ctx, e), + "\\\b\f\n\r\t\"" + "\\\\\\b\\f\\n\\r\\t\\\"")); + + /* This one doesn't escape the already-escaped chars */ + e = json_partial_escape(ctx, + "\\\b\f\n\r\t\"" + "\\\\\\b\\f\\n\\r\\t\\\""); + ok1(!strcmp(e->s, + "\\\\\\b\\f\\n\\r\\t\\\"" + "\\\\\\b\\f\\n\\r\\t\\\"")); + ok1(!strcmp(json_escape_unescape(ctx, e), + "\\\b\f\n\r\t\"" + "\\\b\f\n\r\t\"")); + + tal_free(ctx); + + /* This exits depending on whether all tests passed */ + return exit_status(); +}