common/json: make json_scan return an error string.

This makes for more useful errors.  It prints where it was up to in
the guide, but doesn't print the entire JSON it's scanning.

Suggested-by: Christian Decker
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell
2021-01-07 16:04:43 +10:30
committed by Christian Decker
parent 53582a0f81
commit 3b7d0e7a62
9 changed files with 381 additions and 265 deletions

View File

@@ -720,30 +720,42 @@ void json_tok_remove(jsmntok_t **tokens,
}
/* talfmt take a ctx pointer and return NULL or a valid pointer.
* fmt takes the argument, and returns a bool. */
static bool handle_percent(const char *buffer,
const jsmntok_t *tok,
va_list *ap)
* fmt takes the argument, and returns a bool.
*
* This function returns NULL on success, or errmsg on failure.
*/
static const char *handle_percent(const char *buffer,
const jsmntok_t *tok,
va_list *ap)
{
void *ctx;
const char *fmtname;
/* This is set to (dummy) json_scan if it's a non-tal fmt */
ctx = va_arg(*ap, void *);
fmtname = va_arg(*ap, const char *);
if (ctx != json_scan) {
void *(*talfmt)(void *, const char *, const jsmntok_t *);
void **p;
p = va_arg(*ap, void **);
talfmt = va_arg(*ap, void *(*)(void *, const char *, const jsmntok_t *));
*p = talfmt(ctx, buffer, tok);
return *p != NULL;
if (*p != NULL)
return NULL;
} else {
bool (*fmt)(const char *, const jsmntok_t *, void *);
void *p;
p = va_arg(*ap, void *);
fmt = va_arg(*ap, bool (*)(const char *, const jsmntok_t *, void *));
return fmt(buffer, tok, p);
if (fmt(buffer, tok, p))
return NULL;
}
return tal_fmt(tmpctx, "%s could not parse %.*s",
fmtname,
json_tok_full_len(tok),
json_tok_full(buffer, tok));
}
/* GUIDE := OBJ | ARRAY | '%'
@@ -756,235 +768,280 @@ static bool handle_percent(const char *buffer,
* ARRELEM := NUMBER ':' FIELDVAL
*/
/* Returns NULL on failure, or offset into guide */
static const char *parse_literal(const char *guide,
const char **literal,
size_t *len)
static void parse_literal(const char **guide,
const char **literal,
size_t *len)
{
*literal = guide;
*len = strspn(guide,
*literal = *guide;
*len = strspn(*guide,
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789"
"_-");
return guide + *len;
*guide += *len;
}
static const char *parse_number(const char *guide, u32 *number)
static void parse_number(const char **guide, u32 *number)
{
char *endp;
long int l;
l = strtol(guide, &endp, 10);
if (endp == guide || errno == ERANGE)
return NULL;
l = strtol(*guide, &endp, 10);
assert(endp != *guide);
assert(errno != ERANGE);
/* Test for overflow */
*number = l;
if (*number != l)
return NULL;
assert(*number == l);
return endp;
*guide = endp;
}
/* Recursion */
static char guide_consume_one(const char **guide)
{
char c = **guide;
(*guide)++;
return c;
}
static void guide_must_be(const char **guide, char c)
{
char actual = guide_consume_one(guide);
assert(actual == c);
}
/* Recursion: return NULL on success, errmsg on fail */
static const char *parse_obj(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap);
static const char *parse_arr(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap);
static const char *parse_guide(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap)
{
if (*guide == '{') {
guide = parse_obj(buffer, tok, guide, ap);
if (!guide)
return NULL;
assert(*guide == '}');
return guide + 1;
} else if (*guide == '[') {
guide = parse_arr(buffer, tok, guide, ap);
if (!guide)
return NULL;
assert(*guide == ']');
return guide + 1;
const char *errmsg;
if (**guide == '{') {
errmsg = parse_obj(buffer, tok, guide, ap);
if (errmsg)
return errmsg;
} else if (**guide == '[') {
errmsg = parse_arr(buffer, tok, guide, ap);
if (errmsg)
return errmsg;
} else {
assert(*guide == '%');
if (!handle_percent(buffer, tok, ap))
return NULL;
return guide + 1;
guide_must_be(guide, '%');
errmsg = handle_percent(buffer, tok, ap);
if (errmsg)
return errmsg;
}
return NULL;
}
static const char *parse_fieldval(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap)
{
if (*guide == '{') {
guide = parse_obj(buffer, tok, guide, ap);
if (!guide)
return NULL;
assert(*guide == '}');
return guide + 1;
} else if (*guide == '[') {
guide = parse_arr(buffer, tok, guide, ap);
if (!guide)
return NULL;
assert(*guide == ']');
return guide + 1;
} else if (*guide == '%') {
if (!handle_percent(buffer, tok, ap))
return NULL;
return guide + 1;
const char *errmsg;
if (**guide == '{') {
errmsg = parse_obj(buffer, tok, guide, ap);
if (errmsg)
return errmsg;
} else if (**guide == '[') {
errmsg = parse_arr(buffer, tok, guide, ap);
if (errmsg)
return errmsg;
} else if (**guide == '%') {
guide_consume_one(guide);
errmsg = handle_percent(buffer, tok, ap);
if (errmsg)
return errmsg;
} else {
const char *literal;
size_t len;
/* Literal must match exactly */
guide = parse_literal(guide, &literal, &len);
/* Literal must match exactly (modulo quotes for strings) */
parse_literal(guide, &literal, &len);
if (!memeq(buffer + tok->start, tok->end - tok->start,
literal, len))
return NULL;
return guide;
literal, len)) {
return tal_fmt(tmpctx,
"%.*s does not match expected %.*s",
json_tok_full_len(tok),
json_tok_full(buffer, tok),
(int)len, literal);
}
}
return NULL;
}
static const char *parse_field(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap)
{
const jsmntok_t *member;
size_t len;
const char *memname;
guide = parse_literal(guide, &memname, &len);
assert(*guide == ':');
parse_literal(guide, &memname, &len);
guide_must_be(guide, ':');
member = json_get_membern(buffer, tok, memname, guide - memname);
if (!member)
return NULL;
member = json_get_membern(buffer, tok, memname, len);
if (!member) {
return tal_fmt(tmpctx, "object does not have member %.*s",
(int)len, memname);
}
return parse_fieldval(buffer, member, guide + 1, ap);
return parse_fieldval(buffer, member, guide, ap);
}
static const char *parse_fieldlist(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap)
{
for (;;) {
guide = parse_field(buffer, tok, guide, ap);
if (!guide)
return NULL;
if (*guide != ',')
const char *errmsg;
errmsg = parse_field(buffer, tok, guide, ap);
if (errmsg)
return errmsg;
if (**guide != ',')
break;
guide++;
guide_consume_one(guide);
}
return guide;
return NULL;
}
static const char *parse_obj(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap)
{
assert(*guide == '{');
const char *errmsg;
if (tok->type != JSMN_OBJECT)
return NULL;
guide_must_be(guide, '{');
guide = parse_fieldlist(buffer, tok, guide + 1, ap);
if (!guide)
return NULL;
return guide;
if (tok->type != JSMN_OBJECT) {
return tal_fmt(tmpctx, "token is not an object: %.*s",
json_tok_full_len(tok),
json_tok_full(buffer, tok));
}
errmsg = parse_fieldlist(buffer, tok, guide, ap);
if (errmsg)
return errmsg;
guide_must_be(guide, '}');
return NULL;
}
static const char *parse_arrelem(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap)
{
const jsmntok_t *member;
u32 idx;
guide = parse_number(guide, &idx);
assert(*guide == ':');
parse_number(guide, &idx);
guide_must_be(guide, ':');
member = json_get_arr(tok, idx);
if (!member)
return NULL;
if (!member) {
return tal_fmt(tmpctx, "token has no index %u: %.*s",
idx,
json_tok_full_len(tok),
json_tok_full(buffer, tok));
}
return parse_fieldval(buffer, member, guide + 1, ap);
return parse_fieldval(buffer, member, guide, ap);
}
static const char *parse_arrlist(const char *buffer,
const jsmntok_t *tok,
const char *guide,
va_list *ap)
const jsmntok_t *tok,
const char **guide,
va_list *ap)
{
const char *errmsg;
for (;;) {
guide = parse_arrelem(buffer, tok, guide, ap);
if (!guide)
return NULL;
if (*guide != ',')
errmsg = parse_arrelem(buffer, tok, guide, ap);
if (errmsg)
return errmsg;
if (**guide != ',')
break;
guide++;
guide_consume_one(guide);
}
return guide;
return NULL;
}
static const char *parse_arr(const char *buffer,
const jsmntok_t *tok,
const char *guide,
const char **guide,
va_list *ap)
{
assert(*guide == '[');
const char *errmsg;
if (tok->type != JSMN_ARRAY)
return NULL;
guide_must_be(guide, '[');
guide = parse_arrlist(buffer, tok, guide + 1, ap);
if (!guide)
return NULL;
return guide;
if (tok->type != JSMN_ARRAY) {
return tal_fmt(tmpctx, "token is not an array: %.*s",
json_tok_full_len(tok),
json_tok_full(buffer, tok));
}
errmsg = parse_arrlist(buffer, tok, guide, ap);
if (errmsg)
return errmsg;
guide_must_be(guide, ']');
return NULL;
}
bool json_scanv(const char *buffer,
const jsmntok_t *tok,
const char *guide,
va_list ap)
const char *json_scanv(const tal_t *ctx,
const char *buffer,
const jsmntok_t *tok,
const char *guide,
va_list ap)
{
va_list cpy;
const char *orig_guide = guide, *errmsg;
/* We need this, since &ap doesn't work on some platforms... */
va_copy(cpy, ap);
guide = parse_guide(buffer, tok, guide, &cpy);
errmsg = parse_guide(buffer, tok, &guide, &cpy);
va_end(cpy);
if (!guide)
return false;
if (errmsg) {
return tal_fmt(ctx, "Parsing '%.*s': %s",
(int)(guide - orig_guide), orig_guide,
errmsg);
}
assert(guide[0] == '\0');
return true;
return NULL;
}
bool json_scan(const char *buffer,
const jsmntok_t *tok,
const char *guide,
...)
const char *json_scan(const tal_t *ctx,
const char *buffer,
const jsmntok_t *tok,
const char *guide,
...)
{
va_list ap;
bool ret;
const char *ret;
va_start(ap, guide);
ret = json_scanv(buffer, tok, guide, ap);
ret = json_scanv(ctx, buffer, tok, guide, ap);
va_end(ap);
return ret;
}

View File

@@ -152,15 +152,18 @@ jsmntok_t *json_tok_copy(const tal_t *ctx, const jsmntok_t *tok);
void json_tok_remove(jsmntok_t **tokens,
jsmntok_t *obj_or_array, const jsmntok_t *tok, size_t num);
/* Guide is % for a token: each must be followed by JSON_SCAN(). */
bool json_scan(const char *buffer,
const jsmntok_t *tok,
const char *guide,
...);
/* Guide is % for a token: each must be followed by JSON_SCAN().
* Returns NULL on error (asserts() on bad guide). */
const char *json_scan(const tal_t *ctx,
const char *buffer,
const jsmntok_t *tok,
const char *guide,
...);
/* eg. JSON_SCAN(json_to_bool, &boolvar) */
#define JSON_SCAN(fmt, var) \
json_scan, \
stringify(fmt), \
((var) + 0*sizeof(fmt((const char *)NULL, \
(const jsmntok_t *)NULL, var) == true)), \
(fmt)
@@ -168,16 +171,18 @@ bool json_scan(const char *buffer,
/* eg. JSON_SCAN_TAL(tmpctx, json_strdup, &charvar) */
#define JSON_SCAN_TAL(ctx, fmt, var) \
(ctx), \
stringify(fmt), \
((var) + 0*sizeof((*var) = fmt((ctx), \
(const char *)NULL, \
(const jsmntok_t *)NULL))), \
(fmt)
/* Already-have-varargs version */
bool json_scanv(const char *buffer,
const jsmntok_t *tok,
const char *guide,
va_list ap);
const char *json_scanv(const tal_t *ctx,
const char *buffer,
const jsmntok_t *tok,
const char *guide,
va_list ap);
/* Iterator macro for array: i is counter, t is token ptr, arr is JSMN_ARRAY */
#define json_for_each_arr(i, t, arr) \

View File

@@ -137,6 +137,7 @@ int main(int argc, char *argv[])
char *s;
u32 u32val;
u8 *hex;
const char *err;
common_setup(argv[0]);
@@ -146,55 +147,70 @@ int main(int argc, char *argv[])
assert(toks->size == 4);
/* These are direct matches, and they work. */
assert(json_scan(buf, toks, "{1:one}"));
assert(json_scan(buf, toks, "{1:one,2:two}"));
assert(json_scan(buf, toks, "{2:two,1:one}"));
assert(json_scan(buf, toks, "{2:two,1:one,3:{three:{deeper:17}}}"));
assert(json_scan(buf, toks, "{2:two,1:one,3:{three:{deeper:17}},arr:[0:{1:arrone}]}"));
assert(json_scan(buf, toks, "{2:two,1:one,3:{three:{deeper:17}},arr:[1:2]}"));
assert(json_scan(buf, toks, "{2:two,1:one,3:{three:{deeper:17}},arr:[1:2,2:[0:3,1:4]]}"));
assert(!json_scan(tmpctx, buf, toks, "{1:one}"));
assert(!json_scan(tmpctx, buf, toks, "{1:one,2:two}"));
assert(!json_scan(tmpctx, buf, toks, "{2:two,1:one}"));
assert(!json_scan(tmpctx, buf, toks, "{2:two,1:one,3:{three:{deeper:17}}}"));
assert(!json_scan(tmpctx, buf, toks, "{2:two,1:one,3:{three:{deeper:17}},arr:[0:{1:arrone}]}"));
assert(!json_scan(tmpctx, buf, toks, "{2:two,1:one,3:{three:{deeper:17}},arr:[1:2]}"));
assert(!json_scan(tmpctx, buf, toks, "{2:two,1:one,3:{three:{deeper:17}},arr:[1:2,2:[0:3,1:4]]}"));
/* These do not match */
assert(!json_scan(buf, toks, "{2:one}"));
assert(!json_scan(buf, toks, "{1:one,2:tw}"));
assert(!json_scan(buf, toks, "{1:one,2:twoo}"));
assert(!json_scan(buf, toks, "{4:one}"));
assert(!json_scan(buf, toks, "{2:two,1:one,3:three}"));
assert(!json_scan(buf, toks, "{3:{three:deeper}}"));
assert(!json_scan(buf, toks, "{3:{three:{deeper:{}}}}"));
assert(!json_scan(buf, toks, "{arr:{}}"));
assert(!json_scan(buf, toks, "{arr:[0:1]}"));
assert(!json_scan(buf, toks, "{arr:[4:0]}"));
assert(!json_scan(buf, toks, "{arr:[0:{1:arrtwo}]}"));
assert(!json_scan(buf, toks, "{arr:[1:3]}"));
assert(!json_scan(buf, toks, "{arr:[2:[0:1]]}"));
err = json_scan(tmpctx, buf, toks, "{2:one}");
assert(streq(err, "Parsing '{2:one': \"two\" does not match expected one"));
err = json_scan(tmpctx, buf, toks, "{1:one,2:tw}");
assert(streq(err, "Parsing '{1:one,2:tw': \"two\" does not match expected tw"));
err = json_scan(tmpctx, buf, toks, "{1:one,2:twoo}");
assert(streq(err, "Parsing '{1:one,2:twoo': \"two\" does not match expected twoo"));
err = json_scan(tmpctx, buf, toks, "{4:one}");
assert(streq(err, "Parsing '{4:': object does not have member 4"));
err = json_scan(tmpctx, buf, toks, "{2:two,1:one,3:three}");
assert(streq(err, "Parsing '{2:two,1:one,3:three': {\"three\": {\"deeper\": 17}} does not match expected three"));
err = json_scan(tmpctx, buf, toks, "{3:{three:deeper}}");
assert(streq(err, "Parsing '{3:{three:deeper': {\"deeper\": 17} does not match expected deeper"));
err = json_scan(tmpctx, buf, toks, "{3:{three:{deeper:{}}}}");
assert(streq(err, "Parsing '{3:{three:{deeper:{': token is not an object: 17"));
err = json_scan(tmpctx, buf, toks, "{arr:{}}");
assert(streq(err, "Parsing '{arr:{': token is not an object: [{\"1\": \"arrone\"}, 2, [3, 4]]"));
err = json_scan(tmpctx, buf, toks, "{arr:[0:1]}");
assert(streq(err, "Parsing '{arr:[0:1': {\"1\": \"arrone\"} does not match expected 1"));
err = json_scan(tmpctx, buf, toks, "{arr:[4:0]}");
assert(streq(err, "Parsing '{arr:[4:': token has no index 4: [{\"1\": \"arrone\"}, 2, [3, 4]]"));
err = json_scan(tmpctx, buf, toks, "{arr:[0:{1:arrtwo}]}");
assert(streq(err, "Parsing '{arr:[0:{1:arrtwo': \"arrone\" does not match expected arrtwo"));
err = json_scan(tmpctx, buf, toks, "{arr:[1:3]}");
assert(streq(err, "Parsing '{arr:[1:3': 2 does not match expected 3"));
err = json_scan(tmpctx, buf, toks, "{arr:[2:[0:1]]}");
assert(streq(err, "Parsing '{arr:[2:[0:1': 3 does not match expected 1"));
/* These capture simple values. */
assert(json_scan(buf, toks, "{3:{three:{deeper:%}}}",
JSON_SCAN(json_to_number, &u32val)));
assert(!json_scan(tmpctx, buf, toks, "{3:{three:{deeper:%}}}",
JSON_SCAN(json_to_number, &u32val)));
assert(u32val == 17);
assert(!json_scan(buf, toks, "{1:%}",
JSON_SCAN(json_to_number, &u32val)));
assert(json_scan(buf, toks, "{1:%}",
JSON_SCAN_TAL(tmpctx, json_strdup, &s)));
err = json_scan(tmpctx, buf, toks, "{1:%}",
JSON_SCAN(json_to_number, &u32val));
assert(streq(err, "Parsing '{1:%': json_to_number could not parse \"one\""));
assert(!json_scan(tmpctx, buf, toks, "{1:%}",
JSON_SCAN_TAL(tmpctx, json_strdup, &s)));
assert(tal_parent(s) == tmpctx);
assert(streq(s, "one"));
assert(!json_scan(buf, toks, "{1:%}",
JSON_SCAN_TAL(tmpctx, json_tok_bin_from_hex, &hex)));
err = json_scan(tmpctx, buf, toks, "{1:%}",
JSON_SCAN_TAL(tmpctx, json_tok_bin_from_hex, &hex));
assert(streq(err, "Parsing '{1:%': json_tok_bin_from_hex could not parse \"one\""));
assert(json_scan(buf, toks, "{3:%}", JSON_SCAN(json_to_tok, &t)));
assert(!json_scan(tmpctx, buf, toks, "{3:%}", JSON_SCAN(json_to_tok, &t)));
assert(t == &toks[6]);
assert(json_scan(buf, toks, "{arr:%}", JSON_SCAN(json_to_tok, &t)));
assert(!json_scan(tmpctx, buf, toks, "{arr:%}", JSON_SCAN(json_to_tok, &t)));
assert(t == &toks[12]);
assert(json_scan(buf, toks, "{arr:[1:%]}",
JSON_SCAN(json_to_number, &u32val)));
assert(!json_scan(tmpctx, buf, toks, "{arr:[1:%]}",
JSON_SCAN(json_to_number, &u32val)));
assert(u32val == 2);
assert(json_scan(buf, toks, "{arr:[2:[1:%]]}",
JSON_SCAN(json_to_number, &u32val)));
assert(!json_scan(tmpctx, buf, toks, "{arr:[2:[1:%]]}",
JSON_SCAN(json_to_number, &u32val)));
assert(u32val == 4);
common_shutdown();