Skip to content

Commit

Permalink
Add location information to decoded JSON objects
Browse files Browse the repository at this point in the history
Aid users in printing semantic errors in JSON input by providing them
with location data for each parsed JSON element. An example usage of
such data is demonstrated by nftables' error messages:

| # nft add chain mytable mychain
| Error: No such file or directory
| add chain mytable mychain
|           ^^^^^^^

To reduce overhead for library users not interested in such data, store
it only if `JSON_STORE_LOCATION' flag was passed to json_load*()
functions. It may then be retrieved using the new API function
json_get_location().

Signed-off-by: Phil Sutter <[email protected]>
  • Loading branch information
SirPhuttel committed Sep 28, 2023
1 parent 9535972 commit 4145222
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 0 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ if (NOT JANSSON_WITHOUT_TESTS)
test_load
test_load_callback
test_loadb
test_location
test_number
test_object
test_pack
Expand Down
33 changes: 33 additions & 0 deletions doc/apiref.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2068,3 +2068,36 @@ And usage::

json_decref(obj);
}

.. _apiref-location-information:

Location Information
====================

Jansson supports storing decoded objects' locations in input for better
reporting of semantic errors in applications. Since this comes with a certain
overhead, location information is stored only if ``JSON_STORE_LOCATION`` flag
was specified during decoding.

.. function:: int json_get_location(json_t *json, int *line, int *column, int *position, int *length);

Retrieve location of *json* writing it to memory locations pointed to by
*line*, *column*, *position* and *length* if not *NULL*. Returns 0 on success
or -1 if no location information is available for *json*.

``line``
The line number on which the object occurred.

``column``
The column on which the object occurred. Note that this is the *character
column*, not the byte column, i.e. a multibyte UTF-8 character counts as one
column.

``position``
The position in bytes from the start of the input. This is useful for
debugging Unicode encoding problems.

``length``
The length of the object in bytes. For arrays and objects, length is always
1. For all other types, the value resembles the actual length as it appears
in input. Note that for strings, this includes the quotes.
1 change: 1 addition & 0 deletions src/jansson.def
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,5 @@ EXPORTS
json_get_alloc_funcs
jansson_version_str
jansson_version_cmp
json_get_location

4 changes: 4 additions & 0 deletions src/jansson.h
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,10 @@ void json_get_alloc_funcs(json_malloc_t *malloc_fn, json_free_t *free_fn);
const char *jansson_version_str(void);
int jansson_version_cmp(int major, int minor, int micro);

/* location information */

int json_get_location(json_t *json, int *line, int *column, int *position, int *length);

#ifdef __cplusplus
}
#endif
Expand Down
2 changes: 2 additions & 0 deletions src/jansson_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ int jsonp_loop_check(hashtable_t *parents, const json_t *json, char *key, size_t

/* Helpers for location information */
json_t *jsonp_simple(json_t *json, size_t flags);
void jsonp_store_location(json_t *json, int line, int column,
int position, int length);

/* Windows compatibility */
#if defined(_WIN32) || defined(WIN32)
Expand Down
24 changes: 24 additions & 0 deletions src/load.c
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,22 @@ static void lex_close(lex_t *lex) {
strbuffer_close(&lex->saved_text);
}

static void store_location_from_lex(json_t *json, size_t flags, const lex_t *lex)
{
int tlen = lex->saved_text.length;

if (!(flags & JSON_STORE_LOCATION))
return;

if (tlen)
tlen--;

jsonp_store_location(json, lex->stream.line,
lex->stream.column - tlen,
lex->stream.position - tlen,
lex->saved_text.length ?: 1);
}

/*** parser ***/

static json_t *parse_value(lex_t *lex, size_t flags, json_error_t *error);
Expand All @@ -664,6 +680,7 @@ static json_t *parse_object(lex_t *lex, size_t flags, json_error_t *error) {
if (!object)
return NULL;

store_location_from_lex(object, flags, lex);
lex_scan(lex, error);
if (lex->token == '}')
return object;
Expand Down Expand Up @@ -741,6 +758,7 @@ static json_t *parse_array(lex_t *lex, size_t flags, json_error_t *error) {
if (!array)
return NULL;

store_location_from_lex(array, flags, lex);
lex_scan(lex, error);
if (lex->token == ']')
return array;
Expand Down Expand Up @@ -796,31 +814,37 @@ static json_t *parse_value(lex_t *lex, size_t flags, json_error_t *error) {
}

json = jsonp_stringn_nocheck_own(value, len);
store_location_from_lex(json, flags, lex);
lex->value.string.val = NULL;
lex->value.string.len = 0;
break;
}

case TOKEN_INTEGER: {
json = json_integer(lex->value.integer);
store_location_from_lex(json, flags, lex);
break;
}

case TOKEN_REAL: {
json = json_real(lex->value.real);
store_location_from_lex(json, flags, lex);
break;
}

case TOKEN_TRUE:
json = jsonp_simple(json_true(), flags);
store_location_from_lex(json, flags, lex);
break;

case TOKEN_FALSE:
json = jsonp_simple(json_false(), flags);
store_location_from_lex(json, flags, lex);
break;

case TOKEN_NULL:
json = jsonp_simple(json_null(), flags);
store_location_from_lex(json, flags, lex);
break;

case '{':
Expand Down
99 changes: 99 additions & 0 deletions src/value.c
Original file line number Diff line number Diff line change
Expand Up @@ -1026,10 +1026,13 @@ static void json_delete_simple(json_simple_t *simple)

/*** deletion ***/

static void delete_location(json_t *json);

void json_delete(json_t *json) {
if (!json)
return;

delete_location(json);
switch (json_typeof(json)) {
case JSON_OBJECT:
json_delete_object(json_to_object(json));
Expand Down Expand Up @@ -1148,3 +1151,99 @@ json_t *do_deep_copy(const json_t *json, hashtable_t *parents) {
return NULL;
}
}

/*** location information ***/

typedef struct {
json_t json; /* just to integrate with hashtable */
int line;
int column;
int position;
int length;
} json_location_t;

static hashtable_t location_hash;
static int location_hash_initialized;

static void location_atexit(void)
{
if (location_hash_initialized)
hashtable_close(&location_hash);
}

void jsonp_store_location(json_t *json, int line, int column,
int position, int length)
{
json_location_t *loc = NULL;

/* not possible to store location for the singleton primitives
* as one can't distinguish them by their memory location */
if (json->refcount == (size_t)-1)
return;

if (!location_hash_initialized) {
if (!hashtable_seed) {
/* Autoseed */
json_object_seed(0);
}
if (hashtable_init(&location_hash))
return;

atexit(location_atexit);
location_hash_initialized = 1;
} else {
loc = hashtable_get(&location_hash, (void *)&json, sizeof(json));
}
if (!loc) {
loc = jsonp_malloc(sizeof(*loc));
if (!loc)
return;

loc->json.refcount = (size_t)-1;

if (hashtable_set(&location_hash,
(void *)&json, sizeof(json), (void *)loc))
return;
}

loc->line = line;
loc->column = column;
loc->position = position;
loc->length = length;
}

int json_get_location(json_t *json, int *line, int *column,
int *position, int *length)
{
json_location_t *loc = NULL;

if (location_hash_initialized)
loc = hashtable_get(&location_hash, (void *)&json, sizeof(json));

if (!loc)
return -1;

if (line)
*line = loc->line;
if (column)
*column = loc->column;
if (position)
*position = loc->position;
if (length)
*length = loc->length;

return 0;
}

static void delete_location(json_t *json)
{
struct json_location_t *loc;

if (!location_hash_initialized)
return;

loc = hashtable_get(&location_hash, (void *)&json, sizeof(json));
hashtable_del(&location_hash, (void *)&json, sizeof(json));
if (loc)
jsonp_free(loc);
}
1 change: 1 addition & 0 deletions test/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ suites/api/test_fixed_size
suites/api/test_load
suites/api/test_load_callback
suites/api/test_loadb
suites/api/test_location
suites/api/test_memory_funcs
suites/api/test_number
suites/api/test_object
Expand Down
2 changes: 2 additions & 0 deletions test/suites/api/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ check_PROGRAMS = \
test_load \
test_load_callback \
test_loadb \
test_location \
test_memory_funcs \
test_number \
test_object \
Expand All @@ -28,6 +29,7 @@ test_dump_callback_SOURCES = test_dump_callback.c util.h
test_fixed_size_SOURCES = test_fixed_size.c util.h
test_load_SOURCES = test_load.c util.h
test_loadb_SOURCES = test_loadb.c util.h
test_location_SOURCES = test_location.c util.h
test_memory_funcs_SOURCES = test_memory_funcs.c util.h
test_number_SOURCES = test_number.c util.h
test_object_SOURCES = test_object.c util.h
Expand Down
Loading

0 comments on commit 4145222

Please sign in to comment.