From 5568916eb14e7e56daee08123d2f2ccbe5114583 Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Thu, 22 Apr 2021 11:19:07 +0300 Subject: [PATCH 01/15] json_pointer: fix comments about printf() variants of set/get() These were wrong. Some details about the json_pointer_setf() & json_pointer_getf() were added in the json_pointer_set() & json_pointer_get() doc-strings. This change removes them. Signed-off-by: Alexandru Ardelean --- json_pointer.h | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/json_pointer.h b/json_pointer.h index 06c395b..dfe1185 100644 --- a/json_pointer.h +++ b/json_pointer.h @@ -32,11 +32,6 @@ extern "C" { * Internally, this is equivalent to doing a series of 'json_object_object_get()' * and 'json_object_array_get_idx()' along the given 'path'. * - * Note that the 'path' string supports 'printf()' type arguments, so, whatever - * is added after the 'res' param will be treated as an argument for 'path' - * Example: json_pointer_get(obj, "/foo/%d/%s", &res, 0, bar) - * This means, that you need to escape '%' with '%%' (just like in printf()) - * * @param obj the json_object instance/tree from where to retrieve sub-objects * @param path a (RFC6901) string notation for the sub-object to retrieve * @param res a pointer that stores a reference to the json_object @@ -50,7 +45,9 @@ JSON_EXPORT int json_pointer_get(struct json_object *obj, const char *path, /** * This is a variant of 'json_pointer_get()' that supports printf() style arguments. * - * Example: json_pointer_getf(obj, res, "/foo/%d/%s", 0, bak) + * Variable arguments go after the 'path_fmt' parameter. + * + * Example: json_pointer_getf(obj, res, "/foo/%d/%s", 0, "bar") * This also means that you need to escape '%' with '%%' (just like in printf()) * * Please take into consideration all recommended 'printf()' format security @@ -84,11 +81,6 @@ JSON_EXPORT int json_pointer_getf(struct json_object *obj, struct json_object ** * That also implies that 'json_pointer_set()' does not do any refcount incrementing. * (Just that single decrement that was mentioned above). * - * Note that the 'path' string supports 'printf()' type arguments, so, whatever - * is added after the 'value' param will be treated as an argument for 'path' - * Example: json_pointer_set(obj, "/foo/%d/%s", value, 0, bak) - * This means, that you need to escape '%' with '%%' (just like in printf()) - * * @param obj the json_object instance/tree to which to add a sub-object * @param path a (RFC6901) string notation for the sub-object to set in the tree * @param value object to set at path @@ -101,7 +93,9 @@ JSON_EXPORT int json_pointer_set(struct json_object **obj, const char *path, /** * This is a variant of 'json_pointer_set()' that supports printf() style arguments. * - * Example: json_pointer_setf(obj, value, "/foo/%d/%s", 0, bak) + * Variable arguments go after the 'path_fmt' parameter. + * + * Example: json_pointer_setf(obj, value, "/foo/%d/%s", 0, "bar") * This also means that you need to escape '%' with '%%' (just like in printf()) * * Please take into consideration all recommended 'printf()' format security From a86d7a8f5aa5d47a315931fe11e9316a2be5045f Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Fri, 16 Apr 2021 16:12:22 +0300 Subject: [PATCH 02/15] json_object: introduce json_object_array_insert_idx() API function The behavior of the json_object_array_put_idx() is that, if a user wants to insert an element inside a JSON array, the element will be replaced. For some cases, a user would want to insert an element into the JSON array and shift the elements to the right. For indexes that are outside the length of the current array this behaves like json_object_array_put_idx(). If a user wants to enforce that the JSON array is not expanded, then the json_object_array_length() function can be used to guard against that. The main driver for this change is JSON patch, where the 'add' operation in an array means inserting a value at a certain index and shifting everything by one. Signed-off-by: Alexandru Ardelean --- arraylist.c | 21 +++++++++++++++++++++ arraylist.h | 2 ++ json-c.sym | 4 ++-- json_object.c | 6 ++++++ json_object.h | 19 +++++++++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/arraylist.c b/arraylist.c index d8e12d1..bfc1425 100644 --- a/arraylist.c +++ b/arraylist.c @@ -125,6 +125,27 @@ int array_list_shrink(struct array_list *arr, size_t empty_slots) return 0; } +int array_list_insert_idx(struct array_list *arr, size_t idx, void *data) +{ + size_t move_amount; + + if (idx >= arr->length) + return array_list_put_idx(arr, idx, data); + + /* we're at full size, what size_t can support */ + if (arr->length == SIZE_T_MAX) + return -1; + + if (array_list_expand_internal(arr, arr->length + 1)) + return -1; + + move_amount = (arr->length - idx) * sizeof(void *); + memmove(arr->array + idx + 1, arr->array + idx, move_amount); + arr->array[idx] = data; + arr->length++; + return 0; +} + //static inline int _array_list_put_idx(struct array_list *arr, size_t idx, void *data) int array_list_put_idx(struct array_list *arr, size_t idx, void *data) { diff --git a/arraylist.h b/arraylist.h index f541706..a12f27f 100644 --- a/arraylist.h +++ b/arraylist.h @@ -62,6 +62,8 @@ extern void array_list_free(struct array_list *al); extern void *array_list_get_idx(struct array_list *al, size_t i); +extern int array_list_insert_idx(struct array_list *al, size_t i, void *data); + extern int array_list_put_idx(struct array_list *al, size_t i, void *data); extern int array_list_add(struct array_list *al, void *data); diff --git a/json-c.sym b/json-c.sym index 2867c80..de1fdeb 100644 --- a/json-c.sym +++ b/json-c.sym @@ -167,8 +167,8 @@ JSONC_0.15 { } JSONC_0.14; JSONC_0.16 { -# global: -# ...new symbols here... + global: + json_object_array_insert_idx; } JSONC_0.15; JSONC_0.17 { diff --git a/json_object.c b/json_object.c index 6894a93..c3974a0 100644 --- a/json_object.c +++ b/json_object.c @@ -1519,6 +1519,12 @@ int json_object_array_add(struct json_object *jso, struct json_object *val) return array_list_add(JC_ARRAY(jso)->c_array, val); } +int json_object_array_insert_idx(struct json_object *jso, size_t idx, struct json_object *val) +{ + assert(json_object_get_type(jso) == json_type_array); + return array_list_insert_idx(JC_ARRAY(jso)->c_array, idx, val); +} + int json_object_array_put_idx(struct json_object *jso, size_t idx, struct json_object *val) { assert(json_object_get_type(jso) == json_type_array); diff --git a/json_object.h b/json_object.h index 7633e64..97ef84c 100644 --- a/json_object.h +++ b/json_object.h @@ -622,6 +622,25 @@ JSON_EXPORT int json_object_array_add(struct json_object *obj, struct json_objec JSON_EXPORT int json_object_array_put_idx(struct json_object *obj, size_t idx, struct json_object *val); +/** Insert an element at a specified index in an array (a json_object of type json_type_array) + * + * The reference count will *not* be incremented. This is to make adding + * fields to objects in code more compact. If you want to retain a reference + * to an added object you must wrap the passed object with json_object_get + * + * The array size will be automatically be expanded to the size of the + * index if the index is larger than the current size. + * If the index is within the existing array limits, then the element will be + * inserted and all elements will be shifted. This is the only difference between + * this function and json_object_array_put_idx(). + * + * @param obj the json_object instance + * @param idx the index to insert the element at + * @param val the json_object to be added + */ +JSON_EXPORT int json_object_array_insert_idx(struct json_object *obj, size_t idx, + struct json_object *val); + /** Get the element at specified index of array `obj` (which must be a json_object of type json_type_array) * * *No* reference counts will be changed, and ownership of the returned From d5c5b2caec671dd2235dddac5b9c790e36cb0671 Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Tue, 20 Apr 2021 16:07:26 +0300 Subject: [PATCH 03/15] tests: test1: add test cases for json_object_array_insert_idx() This change adds a few test cases to test the behavior of the new json_object_array_insert_idx() function, to make sure it behaves according to specification in doc-string. This test uses assert() vs the old method of comparing outputs. This will cause the test to fail because the outputs won't match, since the assert() will kick in. Signed-off-by: Alexandru Ardelean --- tests/test1.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test1.c b/tests/test1.c index d28811b..986861b 100644 --- a/tests/test1.c +++ b/tests/test1.c @@ -189,6 +189,41 @@ void test_array_list_expand_internal(void) json_object_put(my_array); } +void test_array_insert_idx() +{ + json_object *my_string, *my_int, *my_null, *my_object, *my_array; + struct json_object *jo1; + + my_array = json_object_new_array(); + json_object_array_add(my_array, json_object_new_int(1)); + json_object_array_add(my_array, json_object_new_int(2)); + json_object_array_add(my_array, json_object_new_int(5)); + + json_object_array_insert_idx(my_array, 2, json_object_new_int(4)); + jo1 = json_tokener_parse("[1, 2, 4, 5]"); + assert(1 == json_object_equal(my_array, jo1)); + json_object_put(jo1); + + json_object_array_insert_idx(my_array, 2, json_object_new_int(3)); + + jo1 = json_tokener_parse("[1, 2, 3, 4, 5]"); + assert(1 == json_object_equal(my_array, jo1)); + json_object_put(jo1); + + json_object_array_insert_idx(my_array, 5, json_object_new_int(6)); + + jo1 = json_tokener_parse("[1, 2, 3, 4, 5, 6]"); + assert(1 == json_object_equal(my_array, jo1)); + json_object_put(jo1); + + json_object_array_insert_idx(my_array, 7, json_object_new_int(8)); + jo1 = json_tokener_parse("[1, 2, 3, 4, 5, 6, null, 8]"); + assert(1 == json_object_equal(my_array, jo1)); + json_object_put(jo1); + + json_object_put(my_array); +} + int main(int argc, char **argv) { json_object *my_string, *my_int, *my_null, *my_object, *my_array; @@ -253,6 +288,8 @@ int main(int argc, char **argv) json_object_put(my_array); + test_array_insert_idx(); + test_array_del_idx(); test_array_list_expand_internal(); From 43d311893516aa56ef2935cb335e169d2671669c Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Sat, 24 Apr 2021 17:06:17 +0300 Subject: [PATCH 04/15] json_pointer: convert index to size_t type The index cannot be negative when parsing in is_valid_index(), because we don't allow the '-' character in a string before we get to the strtol() function. So, might as well remove the negative check (for idx) in is_valid_index() and convert it to size_t. That may allow for higher values for the index (which can be insane, but some people may want to try it). Signed-off-by: Alexandru Ardelean --- json_pointer.c | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/json_pointer.c b/json_pointer.c index 5abccdb..15f5026 100644 --- a/json_pointer.c +++ b/json_pointer.c @@ -44,7 +44,6 @@ static void string_replace_all_occurrences_with_char(char *s, const char *occur, static int is_valid_index(struct json_object *jo, const char *path, size_t *idx) { size_t i, len = strlen(path); - long int idx_val = -1; /* this code-path optimizes a bit, for when we reference the 0-9 index range * in a JSON array and because leading zeros not allowed */ @@ -74,14 +73,9 @@ static int is_valid_index(struct json_object *jo, const char *path, size_t *idx) } } - idx_val = strtol(path, NULL, 10); - if (idx_val < 0) - { - errno = EINVAL; - return 0; - } - *idx = idx_val; - + // We know it's all digits, so the only error case here is overflow, + // but ULLONG_MAX will be longer than any array length so that's ok. + *idx = strtoull(path, NULL, 10); check_oob: len = json_object_array_length(jo); if (*idx >= len) From 5a46a3b76d774ab3f2d884462d4bd4c382f3e835 Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Tue, 20 Apr 2021 16:23:47 +0300 Subject: [PATCH 05/15] json_pointer: introduce json_pointer_get_internal() for internal usage For JSON patch, we require that we get access to the parent of a JSON object as well in order to do some operations via the API. For example, given the object: { "foo": "bar", "array", [ 1, 2, 3] } Using JSON pointer with the path * '/foo' will return 'bar' of type string * '/array/0' will return '1', of type integer The problem is, that if we do 'json_object_put()' on any of the objects above, this will not detach them from the parent, because there is no information back to the parent. One way to fix this, is to introduce links back to the parent, and have these links be made by 'json_object_array_{put,insert}_idx()' and 'json_object_object_add{_ex}()'[1]. [1] For json_object_object_add_ex() we would need to de-constify the second parameter, as we need to change it's internal state when being added to a parent object. It may break some applications, but who knows. But, since this information is needed mostly for JSON patch, another way to address this, is to also retrieve the parent of an object via JSON pointer and use json_object_object_del() and json_object_array_del_idx() on the object's parent. Signed-off-by: Alexandru Ardelean --- json-c.sym | 1 + json_object_private.h | 12 +++++++ json_pointer.c | 80 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/json-c.sym b/json-c.sym index de1fdeb..ab96ecc 100644 --- a/json-c.sym +++ b/json-c.sym @@ -18,6 +18,7 @@ JSONC_PRIVATE { array_list_new; array_list_put_idx; array_list_sort; + json_pointer_get_internal; json_hex_chars; json_parse_double; json_parse_int64; diff --git a/json_object_private.h b/json_object_private.h index e143b46..3e60f64 100644 --- a/json_object_private.h +++ b/json_object_private.h @@ -100,6 +100,18 @@ void _json_c_set_last_err(const char *err_fmt, ...); extern const char *json_hex_chars; +struct json_pointer_get_result { + struct json_object *parent; + struct json_object *obj; + union { + const char *key; + uint32_t index; + } id; +}; + +int json_pointer_get_internal(struct json_object *obj, const char *path, + struct json_pointer_get_result *res); + #ifdef __cplusplus } #endif diff --git a/json_pointer.c b/json_pointer.c index 15f5026..f04b487 100644 --- a/json_pointer.c +++ b/json_pointer.c @@ -15,6 +15,7 @@ #include #include +#include "json_object_private.h" #include "json_pointer.h" #include "strdup_compat.h" #include "vasprintf_compat.h" @@ -88,14 +89,13 @@ check_oob: } static int json_pointer_get_single_path(struct json_object *obj, char *path, - struct json_object **value) + struct json_object **value, size_t *idx) { if (json_object_is_type(obj, json_type_array)) { - size_t idx; - if (!is_valid_index(obj, path, &idx)) + if (!is_valid_index(obj, path, idx)) return -1; - obj = json_object_array_get_idx(obj, idx); + obj = json_object_array_get_idx(obj, *idx); if (obj) { if (value) @@ -147,9 +147,11 @@ static int json_pointer_set_single_path(struct json_object *parent, const char * return -1; } -static int json_pointer_get_recursive(struct json_object *obj, char *path, - struct json_object **value) +static int json_pointer_result_get_recursive(struct json_object *obj, char *path, + struct json_pointer_get_result *res) { + struct json_object *parent_obj = obj; + size_t idx; char *endp; int rc; @@ -166,24 +168,47 @@ static int json_pointer_get_recursive(struct json_object *obj, char *path, *endp = '\0'; /* If we err-ed here, return here */ - if ((rc = json_pointer_get_single_path(obj, path, &obj))) + if ((rc = json_pointer_get_single_path(obj, path, &obj, &idx))) return rc; if (endp) { /* Put the slash back, so that the sanity check passes on next recursion level */ *endp = '/'; - return json_pointer_get_recursive(obj, endp, value); + return json_pointer_result_get_recursive(obj, endp, res); } /* We should be at the end of the recursion here */ - if (value) - *value = obj; + if (res) { + res->parent = parent_obj; + res->obj = obj; + if (json_object_is_type(res->parent, json_type_array)) + res->id.index = idx; + else + res->id.key = path; + } return 0; } -int json_pointer_get(struct json_object *obj, const char *path, struct json_object **res) +static int json_pointer_object_get_recursive(struct json_object *obj, char *path, + struct json_object **value) +{ + struct json_pointer_get_result res; + int rc; + + rc = json_pointer_result_get_recursive(obj, path, &res); + if (rc) + return rc; + + if (value) + *value = res.obj; + + return 0; +} + +int json_pointer_get_internal(struct json_object *obj, const char *path, + struct json_pointer_get_result *res) { char *path_copy = NULL; int rc; @@ -196,8 +221,11 @@ int json_pointer_get(struct json_object *obj, const char *path, struct json_obje if (path[0] == '\0') { - if (res) - *res = obj; + if (res) { + res->parent = NULL; + res->obj = obj; + } + res->id.key = NULL; return 0; } @@ -207,12 +235,30 @@ int json_pointer_get(struct json_object *obj, const char *path, struct json_obje errno = ENOMEM; return -1; } - rc = json_pointer_get_recursive(obj, path_copy, res); + rc = json_pointer_result_get_recursive(obj, path_copy, res); + /* re-map the path string to the const-path string */ + if (rc == 0 && res->id.key && !json_object_is_type(res->parent, json_type_array)) + res->id.key = path + (res->id.key - path_copy); free(path_copy); return rc; } +int json_pointer_get(struct json_object *obj, const char *path, struct json_object **res) +{ + struct json_pointer_get_result jpres; + int rc; + + rc = json_pointer_get_internal(obj, path, &jpres); + if (rc) + return rc; + + if (res) + *res = jpres.obj; + + return 0; +} + int json_pointer_getf(struct json_object *obj, struct json_object **res, const char *path_fmt, ...) { char *path_copy = NULL; @@ -239,7 +285,7 @@ int json_pointer_getf(struct json_object *obj, struct json_object **res, const c goto out; } - rc = json_pointer_get_recursive(obj, path_copy, res); + rc = json_pointer_object_get_recursive(obj, path_copy, res); out: free(path_copy); @@ -286,7 +332,7 @@ int json_pointer_set(struct json_object **obj, const char *path, struct json_obj return -1; } path_copy[endp - path] = '\0'; - rc = json_pointer_get_recursive(*obj, path_copy, &set); + rc = json_pointer_object_get_recursive(*obj, path_copy, &set); free(path_copy); if (rc) @@ -341,7 +387,7 @@ int json_pointer_setf(struct json_object **obj, struct json_object *value, const } *endp = '\0'; - rc = json_pointer_get_recursive(*obj, path_copy, &set); + rc = json_pointer_object_get_recursive(*obj, path_copy, &set); if (rc) goto out; From 1c38dea651447fcaf4ec5930d622a7d0e6384e43 Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Fri, 23 Apr 2021 21:19:49 +0300 Subject: [PATCH 06/15] json_pointer: move array out-of-bounds check outside of is_valid_index() The out-of-bounds check is useful when trying to index/obtain a value from an array. However, when we set a value to a specific JSON pointer, we can allow values that are outside the length of the current array. The RFC6901 doc isn't clear on that aspect, and doing so is a bit more in-line with how json_object_array_{put,insert}_idx() functions behave. This changes the behavior of json_pointer_set{f}() because now a value can be set anywhere in the array. Also, added a test-case for this behavior change. Signed-off-by: Alexandru Ardelean --- json_pointer.c | 21 ++++++++++----------- tests/test_json_pointer.c | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/json_pointer.c b/json_pointer.c index f04b487..e834c3b 100644 --- a/json_pointer.c +++ b/json_pointer.c @@ -42,7 +42,7 @@ static void string_replace_all_occurrences_with_char(char *s, const char *occur, } } -static int is_valid_index(struct json_object *jo, const char *path, size_t *idx) +static int is_valid_index(const char *path, size_t *idx) { size_t i, len = strlen(path); /* this code-path optimizes a bit, for when we reference the 0-9 index range @@ -53,7 +53,7 @@ static int is_valid_index(struct json_object *jo, const char *path, size_t *idx) if (is_plain_digit(path[0])) { *idx = (path[0] - '0'); - goto check_oob; + return 1; } errno = EINVAL; return 0; @@ -77,13 +77,6 @@ static int is_valid_index(struct json_object *jo, const char *path, size_t *idx) // We know it's all digits, so the only error case here is overflow, // but ULLONG_MAX will be longer than any array length so that's ok. *idx = strtoull(path, NULL, 10); -check_oob: - len = json_object_array_length(jo); - if (*idx >= len) - { - errno = ENOENT; - return 0; - } return 1; } @@ -93,8 +86,14 @@ static int json_pointer_get_single_path(struct json_object *obj, char *path, { if (json_object_is_type(obj, json_type_array)) { - if (!is_valid_index(obj, path, idx)) + if (!is_valid_index(path, idx)) return -1; + if (*idx >= json_object_array_length(obj)) + { + errno = ENOENT; + return -1; + } + obj = json_object_array_get_idx(obj, *idx); if (obj) { @@ -129,7 +128,7 @@ static int json_pointer_set_single_path(struct json_object *parent, const char * /* RFC (Chapter 4) states that '-' may be used to add new elements to an array */ if (path[0] == '-' && path[1] == '\0') return json_object_array_add(parent, value); - if (!is_valid_index(parent, path, &idx)) + if (!is_valid_index(path, &idx)) return -1; return json_object_array_put_idx(parent, idx, value); } diff --git a/tests/test_json_pointer.c b/tests/test_json_pointer.c index 4ac78cb..09c195a 100644 --- a/tests/test_json_pointer.c +++ b/tests/test_json_pointer.c @@ -269,6 +269,22 @@ static void test_example_set(void) printf("%s\n", json_object_get_string(jo1)); json_object_put(jo1); + + jo1 = json_tokener_parse("[0, 1, 2, 3]"); + jo2 = json_tokener_parse("[0, 1, 2, 3, null, null, null, 7]"); + + assert(0 == json_pointer_set(&jo1, "/7", json_object_new_int(7))); + assert(1 == json_object_equal(jo1, jo2)); + + json_object_put(jo1); + + jo1 = json_tokener_parse("[0, 1, 2, 3]"); + + assert(0 == json_pointer_setf(&jo1, json_object_new_int(7), "/%u", 7)); + assert(1 == json_object_equal(jo1, jo2)); + + json_object_put(jo1); + json_object_put(jo2); } static void test_wrong_inputs_set(void) From e4d9fbd52a56ab108630647cf7992be5c73a37a6 Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Fri, 23 Apr 2021 21:56:29 +0300 Subject: [PATCH 07/15] json_pointer: split json_pointer_set_with_array_cb() JSON patch is a bit more clear on how some array operations should be handled. Unfortunately, handling them on a case-by-case is a bit tricky because it's difficult to satisfy properly an 'add' operating with a 'move' operation and the basic json_pointer_set(). With json_pointer_set{f}() we use json_object_array_put_idx() to insert a value at a certain index. With JSON patch: * for the 'add' operation, we need to insert a value at a given index, which means shifting existing values by one to the right - also, we cannot allow values to be inserted/added outside the bounds of the array * a 'move' operation, is described as a 'remove' and then an 'add'; for arrays this complicates things, because when we want to a move a value within the array, we have to remove it first (during which the size of the array is reduced by one); when the size of the array is reduced by one, we can't add it to the last position in the array (before the remove) The only sane method to handle this (after a few considerations) is to provide a callback to the function that does the final put/insert into the array. That way, we can do some final checks where these are needed to handle each corner-case. Signed-off-by: Alexandru Ardelean --- json_object_private.h | 7 +++++++ json_pointer.c | 27 +++++++++++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/json_object_private.h b/json_object_private.h index 3e60f64..ff7bbef 100644 --- a/json_object_private.h +++ b/json_object_private.h @@ -112,6 +112,13 @@ struct json_pointer_get_result { int json_pointer_get_internal(struct json_object *obj, const char *path, struct json_pointer_get_result *res); +typedef int(*json_pointer_array_set_cb)(json_object *parent, size_t idx, + json_object *value, void *priv); + +int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, + struct json_object *value, + json_pointer_array_set_cb array_set_cb, void *priv); + #ifdef __cplusplus } #endif diff --git a/json_pointer.c b/json_pointer.c index e834c3b..8260256 100644 --- a/json_pointer.c +++ b/json_pointer.c @@ -119,8 +119,15 @@ static int json_pointer_get_single_path(struct json_object *obj, char *path, return 0; } +static int json_object_array_put_idx_cb(struct json_object *parent, size_t idx, + struct json_object *value, void *priv) +{ + return json_object_array_put_idx(parent, idx, value); +} + static int json_pointer_set_single_path(struct json_object *parent, const char *path, - struct json_object *value) + struct json_object *value, + json_pointer_array_set_cb array_set_cb, void *priv) { if (json_object_is_type(parent, json_type_array)) { @@ -130,7 +137,7 @@ static int json_pointer_set_single_path(struct json_object *parent, const char * return json_object_array_add(parent, value); if (!is_valid_index(path, &idx)) return -1; - return json_object_array_put_idx(parent, idx, value); + return array_set_cb(parent, idx, value, priv); } /* path replacements should have been done in json_pointer_get_single_path(), @@ -291,7 +298,9 @@ out: return rc; } -int json_pointer_set(struct json_object **obj, const char *path, struct json_object *value) +int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, + struct json_object *value, + json_pointer_array_set_cb array_set_cb, void *priv) { const char *endp; char *path_copy = NULL; @@ -321,7 +330,7 @@ int json_pointer_set(struct json_object **obj, const char *path, struct json_obj if ((endp = strrchr(path, '/')) == path) { path++; - return json_pointer_set_single_path(*obj, path, value); + return json_pointer_set_single_path(*obj, path, value, array_set_cb, priv); } /* pass a working copy to the recursive call */ @@ -338,7 +347,12 @@ int json_pointer_set(struct json_object **obj, const char *path, struct json_obj return rc; endp++; - return json_pointer_set_single_path(set, endp, value); + return json_pointer_set_single_path(set, endp, value, array_set_cb, priv); +} + +int json_pointer_set(struct json_object **obj, const char *path, struct json_object *value) +{ + return json_pointer_set_with_array_cb(obj, path, value, json_object_array_put_idx_cb, NULL); } int json_pointer_setf(struct json_object **obj, struct json_object *value, const char *path_fmt, @@ -393,7 +407,8 @@ int json_pointer_setf(struct json_object **obj, struct json_object *value, const set_single_path: endp++; - rc = json_pointer_set_single_path(set, endp, value); + rc = json_pointer_set_single_path(set, endp, value, + json_object_array_put_idx_cb, NULL); out: free(path_copy); return rc; From 538b0468847660d52191ad202edb4ab585b6fd19 Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Sat, 24 Apr 2021 17:00:13 +0300 Subject: [PATCH 08/15] json_patch: add first implementation only with patch application Initially I wanted to also do a function that generates the JSON patch from two JSON documents, but even just applying the JSON patch was a bit of work, especially when needing to satisfy all the test-cases. This change defines all the operation in the RFC6902. The addition isn't too big (for the json_patch_apply() function), as part of the heavy lifting is also done by JSON pointer logic. All the ops were tested with the test-cases defined at: https://github.com/json-patch/json-patch-tests RFC6902: https://tools.ietf.org/html/rfc6902 Signed-off-by: Alexandru Ardelean --- CMakeLists.txt | 10 +- json-c.sym | 1 + json.h.cmakein | 1 + json_patch.c | 249 +++++++++++++++++++++++++++++++++++++++++++++++++ json_patch.h | 46 +++++++++ 5 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 json_patch.c create mode 100644 json_patch.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ada8b0c..964c174 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,7 +88,8 @@ option(ENABLE_RDRAND "Enable RDRAND Hardware RNG Hash Seed." option(ENABLE_THREADING "Enable partial threading support." OFF) option(OVERRIDE_GET_RANDOM_SEED "Override json_c_get_random_seed() with custom code." OFF) option(DISABLE_EXTRA_LIBS "Avoid linking against extra libraries, such as libbsd." OFF) -option(DISABLE_JSON_POINTER "Disable JSON pointer (RFC6901) support." OFF) +option(DISABLE_JSON_POINTER "Disable JSON pointer (RFC6901) and JSON patch support." OFF) +option(DISABLE_JSON_PATCH "Disable JSON patch (RFC6902) support." OFF) option(NEWLOCALE_NEEDS_FREELOCALE "Work around newlocale bugs in old FreeBSD by calling freelocale" OFF) @@ -429,8 +430,15 @@ if (NOT DISABLE_JSON_POINTER) set(JSON_C_PUBLIC_HEADERS ${JSON_C_PUBLIC_HEADERS} ${PROJECT_SOURCE_DIR}/json_pointer.h) set(JSON_C_SOURCES ${JSON_C_SOURCES} ${PROJECT_SOURCE_DIR}/json_pointer.c) set(JSON_H_JSON_POINTER "#include \"json_pointer.h\"") + + if (NOT DISABLE_JSON_PATCH) + set(JSON_C_PUBLIC_HEADERS ${JSON_C_PUBLIC_HEADERS} ${PROJECT_SOURCE_DIR}/json_patch.h) + set(JSON_C_SOURCES ${JSON_C_SOURCES} ${PROJECT_SOURCE_DIR}/json_patch.c) + set(JSON_H_JSON_PATCH "#include \"json_patch.h\"") + endif() else() set(JSON_H_JSON_POINTER "") + set(JSON_H_JSON_PATCH "") endif() configure_file(json.h.cmakein ${PROJECT_BINARY_DIR}/json.h @ONLY) diff --git a/json-c.sym b/json-c.sym index ab96ecc..9b5933b 100644 --- a/json-c.sym +++ b/json-c.sym @@ -170,6 +170,7 @@ JSONC_0.15 { JSONC_0.16 { global: json_object_array_insert_idx; + json_patch_apply; } JSONC_0.15; JSONC_0.17 { diff --git a/json.h.cmakein b/json.h.cmakein index 4fed013..2271320 100644 --- a/json.h.cmakein +++ b/json.h.cmakein @@ -26,6 +26,7 @@ extern "C" { #include "json_c_version.h" #include "json_object.h" #include "json_object_iterator.h" +@JSON_H_JSON_PATCH@ @JSON_H_JSON_POINTER@ #include "json_tokener.h" #include "json_util.h" diff --git a/json_patch.c b/json_patch.c new file mode 100644 index 0000000..296985c --- /dev/null +++ b/json_patch.c @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2021 Alexandru Ardelean. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See COPYING for details. + * + */ + +#include "config.h" + +#include +#include +#include + +#include "json_patch.h" +#include "json_object_private.h" + +/** + * JavaScript Object Notation (JSON) Patch + * RFC 6902 - https://tools.ietf.org/html/rfc6902 + */ + +static int json_patch_apply_test(struct json_object **res, + struct json_object *patch_elem, + const char *path) +{ + struct json_object *value1, *value2; + + if (!json_object_object_get_ex(patch_elem, "value", &value1)) { + errno = EINVAL; + return -1; + } + + /* errno should be set by json_pointer_get() */ + if (json_pointer_get(*res, path, &value2)) + return -1; + + if (!json_object_equal(value1, value2)) { + json_object_put(*res); + *res = NULL; + errno = ENOENT; + return -1; + } + + return 0; +} + +static int __json_patch_apply_remove(struct json_pointer_get_result *jpres) +{ + if (json_object_is_type(jpres->parent, json_type_array)) { + return json_object_array_del_idx(jpres->parent, jpres->id.index, 1); + } else if (jpres->parent && jpres->id.key) { + json_object_object_del(jpres->parent, jpres->id.key); + return 0; + } else { + return json_object_put(jpres->obj); + } +} + +static int json_patch_apply_remove(struct json_object **res, const char *path) +{ + struct json_pointer_get_result jpres; + + if (json_pointer_get_internal(*res, path, &jpres)) + return -1; + + return __json_patch_apply_remove(&jpres); +} + +static int json_object_array_insert_idx_cb(struct json_object *parent, size_t idx, + struct json_object *value, void *priv) +{ + int *add = priv; + + if (idx > json_object_array_length(parent)) + { + errno = EINVAL; + return -1; + } + + if (*add) + return json_object_array_insert_idx(parent, idx, value); + else + return json_object_array_put_idx(parent, idx, value); +} + +static int json_patch_apply_add_replace(struct json_object **res, + struct json_object *patch_elem, + const char *path, int add) +{ + struct json_object *value; + int rc; + + if (!json_object_object_get_ex(patch_elem, "value", &value)) { + errno = EINVAL; + return -1; + } + /* if this is a replace op, then we need to make sure it exists before replacing */ + if (!add && json_pointer_get(*res, path, NULL)) { + errno = ENOENT; + return -1; + } + + rc = json_pointer_set_with_array_cb(res, path, json_object_get(value), + json_object_array_insert_idx_cb, &add); + if (rc) + json_object_put(value); + + return rc; +} + +static int json_object_array_move_cb(struct json_object *parent, size_t idx, + struct json_object *value, void *priv) +{ + struct json_pointer_get_result *from = priv; + size_t len = json_object_array_length(parent); + + /** + * If it's the same array parent, it means that we removed + * and element from it, so the length is temporarily reduced + * by 1, which means that if we try to move an element to + * the last position, we need to check the current length + 1 + */ + if (parent == from->parent) + len++; + + if (idx > len) + { + errno = EINVAL; + return -1; + } + + return json_object_array_insert_idx(parent, idx, value); +} + +static int json_patch_apply_move_copy(struct json_object **res, + struct json_object *patch_elem, + const char *path, int move) +{ + json_pointer_array_set_cb array_set_cb; + struct json_pointer_get_result from; + struct json_object *jfrom; + const char *from_s; + size_t from_s_len; + int rc; + + if (!json_object_object_get_ex(patch_elem, "from", &jfrom)) { + errno = EINVAL; + return -1; + } + + from_s = json_object_get_string(jfrom); + + from_s_len = strlen(from_s); + if (strncmp(from_s, path, from_s_len) == 0) { + /** + * If lengths match, it's a noop, if they don't, + * then we're trying to move a parent under a child + * which is not allowed as per RFC 6902 section 4.4 + * The "from" location MUST NOT be a proper prefix of the "path" + * location; i.e., a location cannot be moved into one of its children. + */ + if (from_s_len == strlen(path)) + return 0; + errno = EINVAL; + return -1; + } + + rc = json_pointer_get_internal(*res, from_s, &from); + if (rc) + return rc; + + json_object_get(from.obj); + + if (!move) { + array_set_cb = json_object_array_insert_idx_cb; + } else { + rc = __json_patch_apply_remove(&from); + if (rc < 0) { + json_object_put(from.obj); + return rc; + } + array_set_cb = json_object_array_move_cb; + } + + rc = json_pointer_set_with_array_cb(res, path, from.obj, array_set_cb, &from); + if (rc) + json_object_put(from.obj); + + return rc; +} + +int json_patch_apply(struct json_object *base, struct json_object *patch, + struct json_object **res) +{ + size_t i; + int rc = 0; + + if (!base || !json_object_is_type(patch, json_type_array)) { + errno = EINVAL; + return -1; + } + + /* errno should be set inside json_object_deep_copy() */ + if (json_object_deep_copy(base, res, NULL) < 0) + return -1; + + /* Go through all operations ; apply them on res */ + for (i = 0; i < json_object_array_length(patch); i++) { + struct json_object *jop, *jpath; + struct json_object *patch_elem = json_object_array_get_idx(patch, i); + const char *op, *path; + if (!json_object_object_get_ex(patch_elem, "op", &jop)) { + errno = EINVAL; + rc = -1; + break; + } + op = json_object_get_string(jop); + json_object_object_get_ex(patch_elem, "path", &jpath); + path = json_object_get_string(jpath); + + if (!strcmp(op, "test")) + rc = json_patch_apply_test(res, patch_elem, path); + else if (!strcmp(op, "remove")) + rc = json_patch_apply_remove(res, path); + else if (!strcmp(op, "add")) + rc = json_patch_apply_add_replace(res, patch_elem, path, 1); + else if (!strcmp(op, "replace")) + rc = json_patch_apply_add_replace(res, patch_elem, path, 0); + else if (!strcmp(op, "move")) + rc = json_patch_apply_move_copy(res, patch_elem, path, 1); + else if (!strcmp(op, "copy")) + rc = json_patch_apply_move_copy(res, patch_elem, path, 0); + else { + errno = EINVAL; + rc = -1; + break; + } + if (rc < 0) + break; + } + + if (rc < 0) { + json_object_put(*res); + *res = NULL; + } + + return rc; +} diff --git a/json_patch.h b/json_patch.h new file mode 100644 index 0000000..80dc8b9 --- /dev/null +++ b/json_patch.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Alexadru Ardelean. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See COPYING for details. + * + */ + +/** + * @file + * @brief JSON Patch (RFC 6902) implementation for manipulating JSON objects + */ +#ifndef _json_patch_h_ +#define _json_patch_h_ + +#include "json_pointer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Apply the JSON patch to the base object. + * The patch object must be formatted as per RFC 6902. + * If the patch is not correctly formatted, an error will + * be returned. + * + * The original `base` object will first be copied, and then + * the patch will be applied. + * If anything fails during patching, the `res` object will be + * NULL and the function will return a negative result. + * + * @param base the JSON object which to patch + * @param patch the JSON object that describes the patch to be applied + * @param the resulting patched JSON object + * + * @return negative if an error (or not found), or 0 if succeeded + */ +JSON_EXPORT int json_patch_apply(struct json_object *base, struct json_object *patch, + struct json_object **res); + +#ifdef __cplusplus +} +#endif + +#endif From 3b8363fcdc21a80a6eead3cf3651a59185f4a78d Mon Sep 17 00:00:00 2001 From: Alexandru Ardelean Date: Tue, 20 Apr 2021 15:47:18 +0300 Subject: [PATCH 09/15] tests: test_json_patch: add test suite for JSON patch Essentially, this change adds the test cases from this repo: https://github.com/json-patch/json-patch-tests Specifically: https://github.com/json-patch/json-patch-tests/blob/master/spec_tests.json https://github.com/json-patch/json-patch-tests/blob/master/tests.json The files were taken at the date of this commit, at git hash ea3af85790cb72893d0676597814b7532019c24e Some tests may not have an 'expected' or 'error' field. Those are ignored. One test was disabled manually via "disabled_in_json_c", because it tries an impossible test, i.e. to add 2 "op" fields in the same patch entry, something which is impossible in a JSON object. For the 'error' cases, right now we only test that an error happens. Later, we can extend this to check the error codes. Signed-off-by: Alexandru Ardelean --- tests/CMakeLists.txt | 3 + tests/json_patch_spec_tests.json | 233 +++++++++++++++ tests/json_patch_tests.json | 485 +++++++++++++++++++++++++++++++ tests/test_json_patch.c | 119 ++++++++ tests/test_json_patch.expected | 110 +++++++ tests/test_json_patch.test | 17 ++ 6 files changed, 967 insertions(+) create mode 100644 tests/json_patch_spec_tests.json create mode 100644 tests/json_patch_tests.json create mode 100644 tests/test_json_patch.c create mode 100644 tests/test_json_patch.expected create mode 100755 tests/test_json_patch.test diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bdee375..f54840d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,9 @@ set(ALL_TEST_NAMES if (NOT DISABLE_JSON_POINTER) set(ALL_TEST_NAMES ${ALL_TEST_NAMES} test_json_pointer) + if (NOT DISABLE_JSON_PATCH) + set(ALL_TEST_NAMES ${ALL_TEST_NAMES} test_json_patch) + endif() endif() foreach(TESTNAME ${ALL_TEST_NAMES}) diff --git a/tests/json_patch_spec_tests.json b/tests/json_patch_spec_tests.json new file mode 100644 index 0000000..c160535 --- /dev/null +++ b/tests/json_patch_spec_tests.json @@ -0,0 +1,233 @@ +[ + { + "comment": "4.1. add with missing object", + "doc": { "q": { "bar": 2 } }, + "patch": [ {"op": "add", "path": "/a/b", "value": 1} ], + "error": + "path /a does not exist -- missing objects are not created recursively" + }, + + { + "comment": "A.1. Adding an Object Member", + "doc": { + "foo": "bar" +}, + "patch": [ + { "op": "add", "path": "/baz", "value": "qux" } +], + "expected": { + "baz": "qux", + "foo": "bar" +} + }, + + { + "comment": "A.2. Adding an Array Element", + "doc": { + "foo": [ "bar", "baz" ] +}, + "patch": [ + { "op": "add", "path": "/foo/1", "value": "qux" } +], + "expected": { + "foo": [ "bar", "qux", "baz" ] +} + }, + + { + "comment": "A.3. Removing an Object Member", + "doc": { + "baz": "qux", + "foo": "bar" +}, + "patch": [ + { "op": "remove", "path": "/baz" } +], + "expected": { + "foo": "bar" +} + }, + + { + "comment": "A.4. Removing an Array Element", + "doc": { + "foo": [ "bar", "qux", "baz" ] +}, + "patch": [ + { "op": "remove", "path": "/foo/1" } +], + "expected": { + "foo": [ "bar", "baz" ] +} + }, + + { + "comment": "A.5. Replacing a Value", + "doc": { + "baz": "qux", + "foo": "bar" +}, + "patch": [ + { "op": "replace", "path": "/baz", "value": "boo" } +], + "expected": { + "baz": "boo", + "foo": "bar" +} + }, + + { + "comment": "A.6. Moving a Value", + "doc": { + "foo": { + "bar": "baz", + "waldo": "fred" + }, + "qux": { + "corge": "grault" + } +}, + "patch": [ + { "op": "move", "from": "/foo/waldo", "path": "/qux/thud" } +], + "expected": { + "foo": { + "bar": "baz" + }, + "qux": { + "corge": "grault", + "thud": "fred" + } +} + }, + + { + "comment": "A.7. Moving an Array Element", + "doc": { + "foo": [ "all", "grass", "cows", "eat" ] +}, + "patch": [ + { "op": "move", "from": "/foo/1", "path": "/foo/3" } +], + "expected": { + "foo": [ "all", "cows", "eat", "grass" ] +} + + }, + + { + "comment": "A.8. Testing a Value: Success", + "doc": { + "baz": "qux", + "foo": [ "a", 2, "c" ] +}, + "patch": [ + { "op": "test", "path": "/baz", "value": "qux" }, + { "op": "test", "path": "/foo/1", "value": 2 } +], + "expected": { + "baz": "qux", + "foo": [ "a", 2, "c" ] + } + }, + + { + "comment": "A.9. Testing a Value: Error", + "doc": { + "baz": "qux" +}, + "patch": [ + { "op": "test", "path": "/baz", "value": "bar" } +], + "error": "string not equivalent" + }, + + { + "comment": "A.10. Adding a nested Member Object", + "doc": { + "foo": "bar" +}, + "patch": [ + { "op": "add", "path": "/child", "value": { "grandchild": { } } } +], + "expected": { + "foo": "bar", + "child": { + "grandchild": { + } + } +} + }, + + { + "comment": "A.11. Ignoring Unrecognized Elements", + "doc": { + "foo":"bar" +}, + "patch": [ + { "op": "add", "path": "/baz", "value": "qux", "xyz": 123 } +], + "expected": { + "foo":"bar", + "baz":"qux" +} + }, + + { + "comment": "A.12. Adding to a Non-existent Target", + "doc": { + "foo": "bar" +}, + "patch": [ + { "op": "add", "path": "/baz/bat", "value": "qux" } +], + "error": "add to a non-existent target" + }, + + { + "comment": "A.13 Invalid JSON Patch Document", + "doc": { + "foo": "bar" + }, + "patch": [ + { "op": "add", "path": "/baz", "value": "qux", "op": "remove" } +], + "error": "operation has two 'op' members", + "disabled": true + }, + + { + "comment": "A.14. ~ Escape Ordering", + "doc": { + "/": 9, + "~1": 10 + }, + "patch": [{"op": "test", "path": "/~01", "value": 10}], + "expected": { + "/": 9, + "~1": 10 + } + }, + + { + "comment": "A.15. Comparing Strings and Numbers", + "doc": { + "/": 9, + "~1": 10 + }, + "patch": [{"op": "test", "path": "/~01", "value": "10"}], + "error": "number is not equal to string" + }, + + { + "comment": "A.16. Adding an Array Value", + "doc": { + "foo": ["bar"] + }, + "patch": [{ "op": "add", "path": "/foo/-", "value": ["abc", "def"] }], + "expected": { + "foo": ["bar", ["abc", "def"]] + } + } + +] diff --git a/tests/json_patch_tests.json b/tests/json_patch_tests.json new file mode 100644 index 0000000..4124d51 --- /dev/null +++ b/tests/json_patch_tests.json @@ -0,0 +1,485 @@ +[ + { "comment": "empty list, empty docs", + "doc": {}, + "patch": [], + "expected": {} }, + + { "comment": "empty patch list", + "doc": {"foo": 1}, + "patch": [], + "expected": {"foo": 1} }, + + { "comment": "rearrangements OK?", + "doc": {"foo": 1, "bar": 2}, + "patch": [], + "expected": {"bar":2, "foo": 1} }, + + { "comment": "rearrangements OK? How about one level down ... array", + "doc": [{"foo": 1, "bar": 2}], + "patch": [], + "expected": [{"bar":2, "foo": 1}] }, + + { "comment": "rearrangements OK? How about one level down...", + "doc": {"foo":{"foo": 1, "bar": 2}}, + "patch": [], + "expected": {"foo":{"bar":2, "foo": 1}} }, + + { "comment": "add replaces any existing field", + "doc": {"foo": null}, + "patch": [{"op": "add", "path": "/foo", "value":1}], + "expected": {"foo": 1} }, + + { "comment": "toplevel array", + "doc": [], + "patch": [{"op": "add", "path": "/0", "value": "foo"}], + "expected": ["foo"] }, + + { "comment": "toplevel array, no change", + "doc": ["foo"], + "patch": [], + "expected": ["foo"] }, + + { "comment": "toplevel object, numeric string", + "doc": {}, + "patch": [{"op": "add", "path": "/foo", "value": "1"}], + "expected": {"foo":"1"} }, + + { "comment": "toplevel object, integer", + "doc": {}, + "patch": [{"op": "add", "path": "/foo", "value": 1}], + "expected": {"foo":1} }, + + { "comment": "Toplevel scalar values OK?", + "doc": "foo", + "patch": [{"op": "replace", "path": "", "value": "bar"}], + "expected": "bar", + "disabled": true }, + + { "comment": "replace object document with array document?", + "doc": {}, + "patch": [{"op": "add", "path": "", "value": []}], + "expected": [] }, + + { "comment": "replace array document with object document?", + "doc": [], + "patch": [{"op": "add", "path": "", "value": {}}], + "expected": {} }, + + { "comment": "append to root array document?", + "doc": [], + "patch": [{"op": "add", "path": "/-", "value": "hi"}], + "expected": ["hi"] }, + + { "comment": "Add, / target", + "doc": {}, + "patch": [ {"op": "add", "path": "/", "value":1 } ], + "expected": {"":1} }, + + { "comment": "Add, /foo/ deep target (trailing slash)", + "doc": {"foo": {}}, + "patch": [ {"op": "add", "path": "/foo/", "value":1 } ], + "expected": {"foo":{"": 1}} }, + + { "comment": "Add composite value at top level", + "doc": {"foo": 1}, + "patch": [{"op": "add", "path": "/bar", "value": [1, 2]}], + "expected": {"foo": 1, "bar": [1, 2]} }, + + { "comment": "Add into composite value", + "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, + "patch": [{"op": "add", "path": "/baz/0/foo", "value": "world"}], + "expected": {"foo": 1, "baz": [{"qux": "hello", "foo": "world"}]} }, + + { "doc": {"bar": [1, 2]}, + "patch": [{"op": "add", "path": "/bar/8", "value": "5"}], + "error": "Out of bounds (upper)" }, + + { "doc": {"bar": [1, 2]}, + "patch": [{"op": "add", "path": "/bar/-1", "value": "5"}], + "error": "Out of bounds (lower)" }, + + { "doc": {"foo": 1}, + "patch": [{"op": "add", "path": "/bar", "value": true}], + "expected": {"foo": 1, "bar": true} }, + + { "doc": {"foo": 1}, + "patch": [{"op": "add", "path": "/bar", "value": false}], + "expected": {"foo": 1, "bar": false} }, + + { "doc": {"foo": 1}, + "patch": [{"op": "add", "path": "/bar", "value": null}], + "expected": {"foo": 1, "bar": null} }, + + { "comment": "0 can be an array index or object element name", + "doc": {"foo": 1}, + "patch": [{"op": "add", "path": "/0", "value": "bar"}], + "expected": {"foo": 1, "0": "bar" } }, + + { "doc": ["foo"], + "patch": [{"op": "add", "path": "/1", "value": "bar"}], + "expected": ["foo", "bar"] }, + + { "doc": ["foo", "sil"], + "patch": [{"op": "add", "path": "/1", "value": "bar"}], + "expected": ["foo", "bar", "sil"] }, + + { "doc": ["foo", "sil"], + "patch": [{"op": "add", "path": "/0", "value": "bar"}], + "expected": ["bar", "foo", "sil"] }, + + { "comment": "push item to array via last index + 1", + "doc": ["foo", "sil"], + "patch": [{"op":"add", "path": "/2", "value": "bar"}], + "expected": ["foo", "sil", "bar"] }, + + { "comment": "add item to array at index > length should fail", + "doc": ["foo", "sil"], + "patch": [{"op":"add", "path": "/3", "value": "bar"}], + "error": "index is greater than number of items in array" }, + + { "comment": "test against implementation-specific numeric parsing", + "doc": {"1e0": "foo"}, + "patch": [{"op": "test", "path": "/1e0", "value": "foo"}], + "expected": {"1e0": "foo"} }, + + { "comment": "test with bad number should fail", + "doc": ["foo", "bar"], + "patch": [{"op": "test", "path": "/1e0", "value": "bar"}], + "error": "test op shouldn't get array element 1" }, + + { "doc": ["foo", "sil"], + "patch": [{"op": "add", "path": "/bar", "value": 42}], + "error": "Object operation on array target" }, + + { "doc": ["foo", "sil"], + "patch": [{"op": "add", "path": "/1", "value": ["bar", "baz"]}], + "expected": ["foo", ["bar", "baz"], "sil"], + "comment": "value in array add not flattened" }, + + { "doc": {"foo": 1, "bar": [1, 2, 3, 4]}, + "patch": [{"op": "remove", "path": "/bar"}], + "expected": {"foo": 1} }, + + { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, + "patch": [{"op": "remove", "path": "/baz/0/qux"}], + "expected": {"foo": 1, "baz": [{}]} }, + + { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, + "patch": [{"op": "replace", "path": "/foo", "value": [1, 2, 3, 4]}], + "expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]} }, + + { "doc": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]}, + "patch": [{"op": "replace", "path": "/baz/0/qux", "value": "world"}], + "expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "world"}]} }, + + { "doc": ["foo"], + "patch": [{"op": "replace", "path": "/0", "value": "bar"}], + "expected": ["bar"] }, + + { "doc": [""], + "patch": [{"op": "replace", "path": "/0", "value": 0}], + "expected": [0] }, + + { "doc": [""], + "patch": [{"op": "replace", "path": "/0", "value": true}], + "expected": [true] }, + + { "doc": [""], + "patch": [{"op": "replace", "path": "/0", "value": false}], + "expected": [false] }, + + { "doc": [""], + "patch": [{"op": "replace", "path": "/0", "value": null}], + "expected": [null] }, + + { "doc": ["foo", "sil"], + "patch": [{"op": "replace", "path": "/1", "value": ["bar", "baz"]}], + "expected": ["foo", ["bar", "baz"]], + "comment": "value in array replace not flattened" }, + + { "comment": "replace whole document", + "doc": {"foo": "bar"}, + "patch": [{"op": "replace", "path": "", "value": {"baz": "qux"}}], + "expected": {"baz": "qux"} }, + + { "comment": "test replace with missing parent key should fail", + "doc": {"bar": "baz"}, + "patch": [{"op": "replace", "path": "/foo/bar", "value": false}], + "error": "replace op should fail with missing parent key" }, + + { "comment": "spurious patch properties", + "doc": {"foo": 1}, + "patch": [{"op": "test", "path": "/foo", "value": 1, "spurious": 1}], + "expected": {"foo": 1} }, + + { "doc": {"foo": null}, + "patch": [{"op": "test", "path": "/foo", "value": null}], + "expected": {"foo": null}, + "comment": "null value should be valid obj property" }, + + { "doc": {"foo": null}, + "patch": [{"op": "replace", "path": "/foo", "value": "truthy"}], + "expected": {"foo": "truthy"}, + "comment": "null value should be valid obj property to be replaced with something truthy" }, + + { "doc": {"foo": null}, + "patch": [{"op": "move", "from": "/foo", "path": "/bar"}], + "expected": {"bar": null}, + "comment": "null value should be valid obj property to be moved" }, + + { "doc": {"foo": null}, + "patch": [{"op": "copy", "from": "/foo", "path": "/bar"}], + "expected": {"foo": null, "bar": null}, + "comment": "null value should be valid obj property to be copied" }, + + { "doc": {"foo": null}, + "patch": [{"op": "remove", "path": "/foo"}], + "expected": {}, + "comment": "null value should be valid obj property to be removed" }, + + { "doc": {"foo": "bar"}, + "patch": [{"op": "replace", "path": "/foo", "value": null}], + "expected": {"foo": null}, + "comment": "null value should still be valid obj property replace other value" }, + + { "doc": {"foo": {"foo": 1, "bar": 2}}, + "patch": [{"op": "test", "path": "/foo", "value": {"bar": 2, "foo": 1}}], + "expected": {"foo": {"foo": 1, "bar": 2}}, + "comment": "test should pass despite rearrangement" }, + + { "doc": {"foo": [{"foo": 1, "bar": 2}]}, + "patch": [{"op": "test", "path": "/foo", "value": [{"bar": 2, "foo": 1}]}], + "expected": {"foo": [{"foo": 1, "bar": 2}]}, + "comment": "test should pass despite (nested) rearrangement" }, + + { "doc": {"foo": {"bar": [1, 2, 5, 4]}}, + "patch": [{"op": "test", "path": "/foo", "value": {"bar": [1, 2, 5, 4]}}], + "expected": {"foo": {"bar": [1, 2, 5, 4]}}, + "comment": "test should pass - no error" }, + + { "doc": {"foo": {"bar": [1, 2, 5, 4]}}, + "patch": [{"op": "test", "path": "/foo", "value": [1, 2]}], + "error": "test op should fail" }, + + { "comment": "Whole document", + "doc": { "foo": 1 }, + "patch": [{"op": "test", "path": "", "value": {"foo": 1}}], + "disabled": true }, + + { "comment": "Empty-string element", + "doc": { "": 1 }, + "patch": [{"op": "test", "path": "/", "value": 1}], + "expected": { "": 1 } }, + + { "doc": { + "foo": ["bar", "baz"], + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 + }, + "patch": [{"op": "test", "path": "/foo", "value": ["bar", "baz"]}, + {"op": "test", "path": "/foo/0", "value": "bar"}, + {"op": "test", "path": "/", "value": 0}, + {"op": "test", "path": "/a~1b", "value": 1}, + {"op": "test", "path": "/c%d", "value": 2}, + {"op": "test", "path": "/e^f", "value": 3}, + {"op": "test", "path": "/g|h", "value": 4}, + {"op": "test", "path": "/i\\j", "value": 5}, + {"op": "test", "path": "/k\"l", "value": 6}, + {"op": "test", "path": "/ ", "value": 7}, + {"op": "test", "path": "/m~0n", "value": 8}], + "expected": { + "": 0, + " ": 7, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "foo": [ + "bar", + "baz" + ], + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + "m~n": 8 + } + }, + { "comment": "Move to same location has no effect", + "doc": {"foo": 1}, + "patch": [{"op": "move", "from": "/foo", "path": "/foo"}], + "expected": {"foo": 1} }, + + { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, + "patch": [{"op": "move", "from": "/foo", "path": "/bar"}], + "expected": {"baz": [{"qux": "hello"}], "bar": 1} }, + + { "doc": {"baz": [{"qux": "hello"}], "bar": 1}, + "patch": [{"op": "move", "from": "/baz/0/qux", "path": "/baz/1"}], + "expected": {"baz": [{}, "hello"], "bar": 1} }, + + { "doc": {"baz": [{"qux": "hello"}], "bar": 1}, + "patch": [{"op": "copy", "from": "/baz/0", "path": "/boo"}], + "expected": {"baz":[{"qux":"hello"}],"bar":1,"boo":{"qux":"hello"}} }, + + { "comment": "replacing the root of the document is possible with add", + "doc": {"foo": "bar"}, + "patch": [{"op": "add", "path": "", "value": {"baz": "qux"}}], + "expected": {"baz":"qux"}}, + + { "comment": "Adding to \"/-\" adds to the end of the array", + "doc": [ 1, 2 ], + "patch": [ { "op": "add", "path": "/-", "value": { "foo": [ "bar", "baz" ] } } ], + "expected": [ 1, 2, { "foo": [ "bar", "baz" ] } ]}, + + { "comment": "Adding to \"/-\" adds to the end of the array, even n levels down", + "doc": [ 1, 2, [ 3, [ 4, 5 ] ] ], + "patch": [ { "op": "add", "path": "/2/1/-", "value": { "foo": [ "bar", "baz" ] } } ], + "expected": [ 1, 2, [ 3, [ 4, 5, { "foo": [ "bar", "baz" ] } ] ] ]}, + + { "comment": "test remove with bad number should fail", + "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, + "patch": [{"op": "remove", "path": "/baz/1e0/qux"}], + "error": "remove op shouldn't remove from array with bad number" }, + + { "comment": "test remove on array", + "doc": [1, 2, 3, 4], + "patch": [{"op": "remove", "path": "/0"}], + "expected": [2, 3, 4] }, + + { "comment": "test repeated removes", + "doc": [1, 2, 3, 4], + "patch": [{ "op": "remove", "path": "/1" }, + { "op": "remove", "path": "/2" }], + "expected": [1, 3] }, + + { "comment": "test remove with bad index should fail", + "doc": [1, 2, 3, 4], + "patch": [{"op": "remove", "path": "/1e0"}], + "error": "remove op shouldn't remove from array with bad number" }, + + { "comment": "test replace with bad number should fail", + "doc": [""], + "patch": [{"op": "replace", "path": "/1e0", "value": false}], + "error": "replace op shouldn't replace in array with bad number" }, + + { "comment": "test copy with bad number should fail", + "doc": {"baz": [1,2,3], "bar": 1}, + "patch": [{"op": "copy", "from": "/baz/1e0", "path": "/boo"}], + "error": "copy op shouldn't work with bad number" }, + + { "comment": "test move with bad number should fail", + "doc": {"foo": 1, "baz": [1,2,3,4]}, + "patch": [{"op": "move", "from": "/baz/1e0", "path": "/foo"}], + "error": "move op shouldn't work with bad number" }, + + { "comment": "test add with bad number should fail", + "doc": ["foo", "sil"], + "patch": [{"op": "add", "path": "/1e0", "value": "bar"}], + "error": "add op shouldn't add to array with bad number" }, + + { "comment": "missing 'path' parameter", + "doc": {}, + "patch": [ { "op": "add", "value": "bar" } ], + "error": "missing 'path' parameter" }, + + { "comment": "'path' parameter with null value", + "doc": {}, + "patch": [ { "op": "add", "path": null, "value": "bar" } ], + "error": "null is not valid value for 'path'" }, + + { "comment": "invalid JSON Pointer token", + "doc": {}, + "patch": [ { "op": "add", "path": "foo", "value": "bar" } ], + "error": "JSON Pointer should start with a slash" }, + + { "comment": "missing 'value' parameter to add", + "doc": [ 1 ], + "patch": [ { "op": "add", "path": "/-" } ], + "error": "missing 'value' parameter" }, + + { "comment": "missing 'value' parameter to replace", + "doc": [ 1 ], + "patch": [ { "op": "replace", "path": "/0" } ], + "error": "missing 'value' parameter" }, + + { "comment": "missing 'value' parameter to test", + "doc": [ null ], + "patch": [ { "op": "test", "path": "/0" } ], + "error": "missing 'value' parameter" }, + + { "comment": "missing value parameter to test - where undef is falsy", + "doc": [ false ], + "patch": [ { "op": "test", "path": "/0" } ], + "error": "missing 'value' parameter" }, + + { "comment": "missing from parameter to copy", + "doc": [ 1 ], + "patch": [ { "op": "copy", "path": "/-" } ], + "error": "missing 'from' parameter" }, + + { "comment": "missing from location to copy", + "doc": { "foo": 1 }, + "patch": [ { "op": "copy", "from": "/bar", "path": "/foo" } ], + "error": "missing 'from' location" }, + + { "comment": "missing from parameter to move", + "doc": { "foo": 1 }, + "patch": [ { "op": "move", "path": "" } ], + "error": "missing 'from' parameter" }, + + { "comment": "missing from location to move", + "doc": { "foo": 1 }, + "patch": [ { "op": "move", "from": "/bar", "path": "/foo" } ], + "error": "missing 'from' location" }, + + { "comment": "duplicate ops", + "doc": { "foo": "bar" }, + "patch": [ { "op": "add", "path": "/baz", "value": "qux", + "op": "move", "from":"/foo" } ], + "error": "patch has two 'op' members", + "disabled_in_json_c": true, + "disabled": true }, + + { "comment": "unrecognized op should fail", + "doc": {"foo": 1}, + "patch": [{"op": "spam", "path": "/foo", "value": 1}], + "error": "Unrecognized op 'spam'" }, + + { "comment": "test with bad array number that has leading zeros", + "doc": ["foo", "bar"], + "patch": [{"op": "test", "path": "/00", "value": "foo"}], + "error": "test op should reject the array value, it has leading zeros" }, + + { "comment": "test with bad array number that has leading zeros", + "doc": ["foo", "bar"], + "patch": [{"op": "test", "path": "/01", "value": "bar"}], + "error": "test op should reject the array value, it has leading zeros" }, + + { "comment": "Removing nonexistent field", + "doc": {"foo" : "bar"}, + "patch": [{"op": "remove", "path": "/baz"}], + "error": "removing a nonexistent field should fail" }, + + { "comment": "Removing deep nonexistent path", + "doc": {"foo" : "bar"}, + "patch": [{"op": "remove", "path": "/missing1/missing2"}], + "error": "removing a nonexistent field should fail" }, + + { "comment": "Removing nonexistent index", + "doc": ["foo", "bar"], + "patch": [{"op": "remove", "path": "/2"}], + "error": "removing a nonexistent index should fail" }, + + { "comment": "Patch with different capitalisation than doc", + "doc": {"foo":"bar"}, + "patch": [{"op": "add", "path": "/FOO", "value": "BAR"}], + "expected": {"foo": "bar", "FOO": "BAR"} + } + +] diff --git a/tests/test_json_patch.c b/tests/test_json_patch.c new file mode 100644 index 0000000..dad7521 --- /dev/null +++ b/tests/test_json_patch.c @@ -0,0 +1,119 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include + +#include "json.h" + +#ifndef PATH_MAX +#define PATH_MAX 256 +#endif + +void test_json_patch_op(struct json_object *jo) +{ + const char *comment = json_object_get_string(json_object_object_get(jo, "comment")); + struct json_object *doc = json_object_object_get(jo, "doc"); + struct json_object *patch = json_object_object_get(jo, "patch"); + struct json_object *expected = json_object_object_get(jo, "expected"); + struct json_object *error = json_object_object_get(jo, "error"); + int disabled_test = json_object_get_boolean(json_object_object_get(jo, "disabled_in_json_c")); + const char *error_s = json_object_get_string(error); + struct json_object *res = NULL; + int ret; + + printf("Testing '%s', doc '%s' patch '%s' : ", + comment ? comment : error_s, + json_object_get_string(doc), + json_object_get_string(patch)); + if (disabled_test) { + printf("SKIPPING - disabled in the test spec\n"); + return; + } + if (!error && !expected) { + printf("SKIPPING - no expected or error conditions in test\n"); + return; + } + fflush(stdout); + if (error) { + assert(-1 == json_patch_apply(doc, patch, &res)); + assert(res == NULL); + } else { + ret = json_patch_apply(doc, patch, &res); + if (ret) { + fprintf(stderr, "json_patch_apply() returned '%d'\n", ret); + fprintf(stderr, "Expected: %s\n", json_object_get_string(expected)); + fprintf(stderr, "Got: %s\n", json_object_get_string(res)); + fflush(stderr); + assert(0); + } + assert(res != NULL); + ret = json_object_equal(expected, res); + if (ret == 0) { + fprintf(stderr, "json_object_equal() returned '%d'\n", ret); + fprintf(stderr, "Expected: %s\n", json_object_get_string(expected)); + fprintf(stderr, "Got: %s\n", json_object_get_string(res)); + fflush(stderr); + assert(0); + } + json_object_put(res); + res = NULL; + } + + printf("OK\n"); +} + +void test_json_patch_using_file(const char *testdir, const char *filename) +{ + char full_filename[PATH_MAX]; + (void)snprintf(full_filename, sizeof(full_filename), "%s/%s", testdir, filename); + int i; + + json_object *jo = json_object_from_file(full_filename); + if (!jo) { + fprintf(stderr, "FAIL: unable to open %s: %s\n", full_filename, strerror(errno)); + exit(EXIT_FAILURE); + } + + for (i = 0; i < json_object_array_length(jo); i++) { + struct json_object *jo1 = json_object_array_get_idx(jo, i); + test_json_patch_op(jo1); + } + + json_object_put(jo); +} + +int main(int argc, char **argv) +{ + const char *testdir; + if (argc < 2) + { + fprintf(stderr, + "Usage: %s \n" + " is the location of input files\n", + argv[0]); + return EXIT_FAILURE; + } + testdir = argv[1]; + + // Test json_c_version.c + if (strncmp(json_c_version(), JSON_C_VERSION, sizeof(JSON_C_VERSION))) + { + printf("FAIL: Output from json_c_version(): %s does not match %s", + json_c_version(), JSON_C_VERSION); + return EXIT_FAILURE; + } + if (json_c_version_num() != JSON_C_VERSION_NUM) + { + printf("FAIL: Output from json_c_version_num(): %d does not match %d", + json_c_version_num(), JSON_C_VERSION_NUM); + return EXIT_FAILURE; + } + + test_json_patch_using_file(testdir, "json_patch_spec_tests.json"); + test_json_patch_using_file(testdir, "json_patch_tests.json"); + return 0; +} diff --git a/tests/test_json_patch.expected b/tests/test_json_patch.expected new file mode 100644 index 0000000..64e59b8 --- /dev/null +++ b/tests/test_json_patch.expected @@ -0,0 +1,110 @@ +Testing '4.1. add with missing object', doc '{ "q": { "bar": 2 } }' patch '[ { "op": "add", "path": "\/a\/b", "value": 1 } ]' : OK +Testing 'A.1. Adding an Object Member', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/baz", "value": "qux" } ]' : OK +Testing 'A.2. Adding an Array Element', doc '{ "foo": [ "bar", "baz" ] }' patch '[ { "op": "add", "path": "\/foo\/1", "value": "qux" } ]' : OK +Testing 'A.3. Removing an Object Member', doc '{ "baz": "qux", "foo": "bar" }' patch '[ { "op": "remove", "path": "\/baz" } ]' : OK +Testing 'A.4. Removing an Array Element', doc '{ "foo": [ "bar", "qux", "baz" ] }' patch '[ { "op": "remove", "path": "\/foo\/1" } ]' : OK +Testing 'A.5. Replacing a Value', doc '{ "baz": "qux", "foo": "bar" }' patch '[ { "op": "replace", "path": "\/baz", "value": "boo" } ]' : OK +Testing 'A.6. Moving a Value', doc '{ "foo": { "bar": "baz", "waldo": "fred" }, "qux": { "corge": "grault" } }' patch '[ { "op": "move", "from": "\/foo\/waldo", "path": "\/qux\/thud" } ]' : OK +Testing 'A.7. Moving an Array Element', doc '{ "foo": [ "all", "grass", "cows", "eat" ] }' patch '[ { "op": "move", "from": "\/foo\/1", "path": "\/foo\/3" } ]' : OK +Testing 'A.8. Testing a Value: Success', doc '{ "baz": "qux", "foo": [ "a", 2, "c" ] }' patch '[ { "op": "test", "path": "\/baz", "value": "qux" }, { "op": "test", "path": "\/foo\/1", "value": 2 } ]' : OK +Testing 'A.9. Testing a Value: Error', doc '{ "baz": "qux" }' patch '[ { "op": "test", "path": "\/baz", "value": "bar" } ]' : OK +Testing 'A.10. Adding a nested Member Object', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/child", "value": { "grandchild": { } } } ]' : OK +Testing 'A.11. Ignoring Unrecognized Elements', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/baz", "value": "qux", "xyz": 123 } ]' : OK +Testing 'A.12. Adding to a Non-existent Target', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/baz\/bat", "value": "qux" } ]' : OK +Testing 'A.13 Invalid JSON Patch Document', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": "\/baz", "value": "qux" } ]' : OK +Testing 'A.14. ~ Escape Ordering', doc '{ "\/": 9, "~1": 10 }' patch '[ { "op": "test", "path": "\/~01", "value": 10 } ]' : OK +Testing 'A.15. Comparing Strings and Numbers', doc '{ "\/": 9, "~1": 10 }' patch '[ { "op": "test", "path": "\/~01", "value": "10" } ]' : OK +Testing 'A.16. Adding an Array Value', doc '{ "foo": [ "bar" ] }' patch '[ { "op": "add", "path": "\/foo\/-", "value": [ "abc", "def" ] } ]' : OK +Testing 'empty list, empty docs', doc '{ }' patch '[ ]' : OK +Testing 'empty patch list', doc '{ "foo": 1 }' patch '[ ]' : OK +Testing 'rearrangements OK?', doc '{ "foo": 1, "bar": 2 }' patch '[ ]' : OK +Testing 'rearrangements OK? How about one level down ... array', doc '[ { "foo": 1, "bar": 2 } ]' patch '[ ]' : OK +Testing 'rearrangements OK? How about one level down...', doc '{ "foo": { "foo": 1, "bar": 2 } }' patch '[ ]' : OK +Testing 'add replaces any existing field', doc '{ "foo": null }' patch '[ { "op": "add", "path": "\/foo", "value": 1 } ]' : OK +Testing 'toplevel array', doc '[ ]' patch '[ { "op": "add", "path": "\/0", "value": "foo" } ]' : OK +Testing 'toplevel array, no change', doc '[ "foo" ]' patch '[ ]' : OK +Testing 'toplevel object, numeric string', doc '{ }' patch '[ { "op": "add", "path": "\/foo", "value": "1" } ]' : OK +Testing 'toplevel object, integer', doc '{ }' patch '[ { "op": "add", "path": "\/foo", "value": 1 } ]' : OK +Testing 'Toplevel scalar values OK?', doc 'foo' patch '[ { "op": "replace", "path": "", "value": "bar" } ]' : OK +Testing 'replace object document with array document?', doc '{ }' patch '[ { "op": "add", "path": "", "value": [ ] } ]' : OK +Testing 'replace array document with object document?', doc '[ ]' patch '[ { "op": "add", "path": "", "value": { } } ]' : OK +Testing 'append to root array document?', doc '[ ]' patch '[ { "op": "add", "path": "\/-", "value": "hi" } ]' : OK +Testing 'Add, / target', doc '{ }' patch '[ { "op": "add", "path": "\/", "value": 1 } ]' : OK +Testing 'Add, /foo/ deep target (trailing slash)', doc '{ "foo": { } }' patch '[ { "op": "add", "path": "\/foo\/", "value": 1 } ]' : OK +Testing 'Add composite value at top level', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/bar", "value": [ 1, 2 ] } ]' : OK +Testing 'Add into composite value', doc '{ "foo": 1, "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "add", "path": "\/baz\/0\/foo", "value": "world" } ]' : OK +Testing 'Out of bounds (upper)', doc '{ "bar": [ 1, 2 ] }' patch '[ { "op": "add", "path": "\/bar\/8", "value": "5" } ]' : OK +Testing 'Out of bounds (lower)', doc '{ "bar": [ 1, 2 ] }' patch '[ { "op": "add", "path": "\/bar\/-1", "value": "5" } ]' : OK +Testing '(null)', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/bar", "value": true } ]' : OK +Testing '(null)', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/bar", "value": false } ]' : OK +Testing '(null)', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/bar", "value": null } ]' : OK +Testing '0 can be an array index or object element name', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/0", "value": "bar" } ]' : OK +Testing '(null)', doc '[ "foo" ]' patch '[ { "op": "add", "path": "\/1", "value": "bar" } ]' : OK +Testing '(null)', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/1", "value": "bar" } ]' : OK +Testing '(null)', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/0", "value": "bar" } ]' : OK +Testing 'push item to array via last index + 1', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/2", "value": "bar" } ]' : OK +Testing 'add item to array at index > length should fail', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/3", "value": "bar" } ]' : OK +Testing 'test against implementation-specific numeric parsing', doc '{ "1e0": "foo" }' patch '[ { "op": "test", "path": "\/1e0", "value": "foo" } ]' : OK +Testing 'test with bad number should fail', doc '[ "foo", "bar" ]' patch '[ { "op": "test", "path": "\/1e0", "value": "bar" } ]' : OK +Testing 'Object operation on array target', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/bar", "value": 42 } ]' : OK +Testing 'value in array add not flattened', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/1", "value": [ "bar", "baz" ] } ]' : OK +Testing '(null)', doc '{ "foo": 1, "bar": [ 1, 2, 3, 4 ] }' patch '[ { "op": "remove", "path": "\/bar" } ]' : OK +Testing '(null)', doc '{ "foo": 1, "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "remove", "path": "\/baz\/0\/qux" } ]' : OK +Testing '(null)', doc '{ "foo": 1, "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "replace", "path": "\/foo", "value": [ 1, 2, 3, 4 ] } ]' : OK +Testing '(null)', doc '{ "foo": [ 1, 2, 3, 4 ], "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "replace", "path": "\/baz\/0\/qux", "value": "world" } ]' : OK +Testing '(null)', doc '[ "foo" ]' patch '[ { "op": "replace", "path": "\/0", "value": "bar" } ]' : OK +Testing '(null)', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/0", "value": 0 } ]' : OK +Testing '(null)', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/0", "value": true } ]' : OK +Testing '(null)', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/0", "value": false } ]' : OK +Testing '(null)', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/0", "value": null } ]' : OK +Testing 'value in array replace not flattened', doc '[ "foo", "sil" ]' patch '[ { "op": "replace", "path": "\/1", "value": [ "bar", "baz" ] } ]' : OK +Testing 'replace whole document', doc '{ "foo": "bar" }' patch '[ { "op": "replace", "path": "", "value": { "baz": "qux" } } ]' : OK +Testing 'test replace with missing parent key should fail', doc '{ "bar": "baz" }' patch '[ { "op": "replace", "path": "\/foo\/bar", "value": false } ]' : OK +Testing 'spurious patch properties', doc '{ "foo": 1 }' patch '[ { "op": "test", "path": "\/foo", "value": 1, "spurious": 1 } ]' : OK +Testing 'null value should be valid obj property', doc '{ "foo": null }' patch '[ { "op": "test", "path": "\/foo", "value": null } ]' : OK +Testing 'null value should be valid obj property to be replaced with something truthy', doc '{ "foo": null }' patch '[ { "op": "replace", "path": "\/foo", "value": "truthy" } ]' : OK +Testing 'null value should be valid obj property to be moved', doc '{ "foo": null }' patch '[ { "op": "move", "from": "\/foo", "path": "\/bar" } ]' : OK +Testing 'null value should be valid obj property to be copied', doc '{ "foo": null }' patch '[ { "op": "copy", "from": "\/foo", "path": "\/bar" } ]' : OK +Testing 'null value should be valid obj property to be removed', doc '{ "foo": null }' patch '[ { "op": "remove", "path": "\/foo" } ]' : OK +Testing 'null value should still be valid obj property replace other value', doc '{ "foo": "bar" }' patch '[ { "op": "replace", "path": "\/foo", "value": null } ]' : OK +Testing 'test should pass despite rearrangement', doc '{ "foo": { "foo": 1, "bar": 2 } }' patch '[ { "op": "test", "path": "\/foo", "value": { "bar": 2, "foo": 1 } } ]' : OK +Testing 'test should pass despite (nested) rearrangement', doc '{ "foo": [ { "foo": 1, "bar": 2 } ] }' patch '[ { "op": "test", "path": "\/foo", "value": [ { "bar": 2, "foo": 1 } ] } ]' : OK +Testing 'test should pass - no error', doc '{ "foo": { "bar": [ 1, 2, 5, 4 ] } }' patch '[ { "op": "test", "path": "\/foo", "value": { "bar": [ 1, 2, 5, 4 ] } } ]' : OK +Testing 'test op should fail', doc '{ "foo": { "bar": [ 1, 2, 5, 4 ] } }' patch '[ { "op": "test", "path": "\/foo", "value": [ 1, 2 ] } ]' : OK +Testing 'Whole document', doc '{ "foo": 1 }' patch '[ { "op": "test", "path": "", "value": { "foo": 1 } } ]' : SKIPPING - no expected or error conditions in test +Testing 'Empty-string element', doc '{ "": 1 }' patch '[ { "op": "test", "path": "\/", "value": 1 } ]' : OK +Testing '(null)', doc '{ "foo": [ "bar", "baz" ], "": 0, "a\/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }' patch '[ { "op": "test", "path": "\/foo", "value": [ "bar", "baz" ] }, { "op": "test", "path": "\/foo\/0", "value": "bar" }, { "op": "test", "path": "\/", "value": 0 }, { "op": "test", "path": "\/a~1b", "value": 1 }, { "op": "test", "path": "\/c%d", "value": 2 }, { "op": "test", "path": "\/e^f", "value": 3 }, { "op": "test", "path": "\/g|h", "value": 4 }, { "op": "test", "path": "\/i\\j", "value": 5 }, { "op": "test", "path": "\/k\"l", "value": 6 }, { "op": "test", "path": "\/ ", "value": 7 }, { "op": "test", "path": "\/m~0n", "value": 8 } ]' : OK +Testing 'Move to same location has no effect', doc '{ "foo": 1 }' patch '[ { "op": "move", "from": "\/foo", "path": "\/foo" } ]' : OK +Testing '(null)', doc '{ "foo": 1, "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "move", "from": "\/foo", "path": "\/bar" } ]' : OK +Testing '(null)', doc '{ "baz": [ { "qux": "hello" } ], "bar": 1 }' patch '[ { "op": "move", "from": "\/baz\/0\/qux", "path": "\/baz\/1" } ]' : OK +Testing '(null)', doc '{ "baz": [ { "qux": "hello" } ], "bar": 1 }' patch '[ { "op": "copy", "from": "\/baz\/0", "path": "\/boo" } ]' : OK +Testing 'replacing the root of the document is possible with add', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "", "value": { "baz": "qux" } } ]' : OK +Testing 'Adding to "/-" adds to the end of the array', doc '[ 1, 2 ]' patch '[ { "op": "add", "path": "\/-", "value": { "foo": [ "bar", "baz" ] } } ]' : OK +Testing 'Adding to "/-" adds to the end of the array, even n levels down', doc '[ 1, 2, [ 3, [ 4, 5 ] ] ]' patch '[ { "op": "add", "path": "\/2\/1\/-", "value": { "foo": [ "bar", "baz" ] } } ]' : OK +Testing 'test remove with bad number should fail', doc '{ "foo": 1, "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "remove", "path": "\/baz\/1e0\/qux" } ]' : OK +Testing 'test remove on array', doc '[ 1, 2, 3, 4 ]' patch '[ { "op": "remove", "path": "\/0" } ]' : OK +Testing 'test repeated removes', doc '[ 1, 2, 3, 4 ]' patch '[ { "op": "remove", "path": "\/1" }, { "op": "remove", "path": "\/2" } ]' : OK +Testing 'test remove with bad index should fail', doc '[ 1, 2, 3, 4 ]' patch '[ { "op": "remove", "path": "\/1e0" } ]' : OK +Testing 'test replace with bad number should fail', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/1e0", "value": false } ]' : OK +Testing 'test copy with bad number should fail', doc '{ "baz": [ 1, 2, 3 ], "bar": 1 }' patch '[ { "op": "copy", "from": "\/baz\/1e0", "path": "\/boo" } ]' : OK +Testing 'test move with bad number should fail', doc '{ "foo": 1, "baz": [ 1, 2, 3, 4 ] }' patch '[ { "op": "move", "from": "\/baz\/1e0", "path": "\/foo" } ]' : OK +Testing 'test add with bad number should fail', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/1e0", "value": "bar" } ]' : OK +Testing 'missing 'path' parameter', doc '{ }' patch '[ { "op": "add", "value": "bar" } ]' : OK +Testing ''path' parameter with null value', doc '{ }' patch '[ { "op": "add", "path": null, "value": "bar" } ]' : OK +Testing 'invalid JSON Pointer token', doc '{ }' patch '[ { "op": "add", "path": "foo", "value": "bar" } ]' : OK +Testing 'missing 'value' parameter to add', doc '[ 1 ]' patch '[ { "op": "add", "path": "\/-" } ]' : OK +Testing 'missing 'value' parameter to replace', doc '[ 1 ]' patch '[ { "op": "replace", "path": "\/0" } ]' : OK +Testing 'missing 'value' parameter to test', doc '[ null ]' patch '[ { "op": "test", "path": "\/0" } ]' : OK +Testing 'missing value parameter to test - where undef is falsy', doc '[ false ]' patch '[ { "op": "test", "path": "\/0" } ]' : OK +Testing 'missing from parameter to copy', doc '[ 1 ]' patch '[ { "op": "copy", "path": "\/-" } ]' : OK +Testing 'missing from location to copy', doc '{ "foo": 1 }' patch '[ { "op": "copy", "from": "\/bar", "path": "\/foo" } ]' : OK +Testing 'missing from parameter to move', doc '{ "foo": 1 }' patch '[ { "op": "move", "path": "" } ]' : OK +Testing 'missing from location to move', doc '{ "foo": 1 }' patch '[ { "op": "move", "from": "\/bar", "path": "\/foo" } ]' : OK +Testing 'duplicate ops', doc '{ "foo": "bar" }' patch '[ { "op": "move", "path": "\/baz", "value": "qux", "from": "\/foo" } ]' : SKIPPING - disabled in the test spec +Testing 'unrecognized op should fail', doc '{ "foo": 1 }' patch '[ { "op": "spam", "path": "\/foo", "value": 1 } ]' : OK +Testing 'test with bad array number that has leading zeros', doc '[ "foo", "bar" ]' patch '[ { "op": "test", "path": "\/00", "value": "foo" } ]' : OK +Testing 'test with bad array number that has leading zeros', doc '[ "foo", "bar" ]' patch '[ { "op": "test", "path": "\/01", "value": "bar" } ]' : OK +Testing 'Removing nonexistent field', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": "\/baz" } ]' : OK +Testing 'Removing deep nonexistent path', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": "\/missing1\/missing2" } ]' : OK +Testing 'Removing nonexistent index', doc '[ "foo", "bar" ]' patch '[ { "op": "remove", "path": "\/2" } ]' : OK +Testing 'Patch with different capitalisation than doc', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/FOO", "value": "BAR" } ]' : OK diff --git a/tests/test_json_patch.test b/tests/test_json_patch.test new file mode 100755 index 0000000..0dc5aff --- /dev/null +++ b/tests/test_json_patch.test @@ -0,0 +1,17 @@ +#!/bin/sh + +export _JSON_C_STRERROR_ENABLE=1 + +# Common definitions +if test -z "$srcdir"; then + srcdir="${0%/*}" + test "$srcdir" = "$0" && srcdir=. + test -z "$srcdir" && srcdir=. +fi +. "$srcdir/test-defs.sh" + +filename=$(basename "$0") +filename="${filename%.*}" + +run_output_test $filename "$srcdir" +exit $? From efc530594bc1578cb8f8b1e2cd8a1c1b395a6326 Mon Sep 17 00:00:00 2001 From: Eric Hawicz Date: Sun, 16 Jul 2023 10:48:20 -0400 Subject: [PATCH 10/15] Create a json_pointer_private.h and move a few things there, fix test warnings, note array_list_insert_idx is private. --- CMakeLists.txt | 1 + json-c.sym | 1 + json_object_private.h | 19 ------------------- json_patch.c | 7 ++++--- json_pointer.c | 1 + json_pointer_private.h | 42 +++++++++++++++++++++++++++++++++++++++++ tests/test1.c | 3 ++- tests/test_json_patch.c | 8 +++++--- 8 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 json_pointer_private.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 964c174..64e1260 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -403,6 +403,7 @@ set(JSON_C_PUBLIC_HEADERS set(JSON_C_HEADERS ${JSON_C_PUBLIC_HEADERS} ${PROJECT_SOURCE_DIR}/json_object_private.h + ${PROJECT_SOURCE_DIR}/json_pointer_private.h ${PROJECT_SOURCE_DIR}/random_seed.h ${PROJECT_SOURCE_DIR}/strerror_override.h ${PROJECT_SOURCE_DIR}/math_compat.h diff --git a/json-c.sym b/json-c.sym index 9b5933b..15440bf 100644 --- a/json-c.sym +++ b/json-c.sym @@ -176,4 +176,5 @@ JSONC_0.16 { JSONC_0.17 { # global: # ...new symbols here... +# array_list_insert_idx is intentionally not exported } JSONC_0.16; diff --git a/json_object_private.h b/json_object_private.h index ff7bbef..e143b46 100644 --- a/json_object_private.h +++ b/json_object_private.h @@ -100,25 +100,6 @@ void _json_c_set_last_err(const char *err_fmt, ...); extern const char *json_hex_chars; -struct json_pointer_get_result { - struct json_object *parent; - struct json_object *obj; - union { - const char *key; - uint32_t index; - } id; -}; - -int json_pointer_get_internal(struct json_object *obj, const char *path, - struct json_pointer_get_result *res); - -typedef int(*json_pointer_array_set_cb)(json_object *parent, size_t idx, - json_object *value, void *priv); - -int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, - struct json_object *value, - json_pointer_array_set_cb array_set_cb, void *priv); - #ifdef __cplusplus } #endif diff --git a/json_patch.c b/json_patch.c index 296985c..97d9dd8 100644 --- a/json_patch.c +++ b/json_patch.c @@ -14,6 +14,7 @@ #include "json_patch.h" #include "json_object_private.h" +#include "json_pointer_private.h" /** * JavaScript Object Notation (JSON) Patch @@ -193,7 +194,7 @@ static int json_patch_apply_move_copy(struct json_object **res, int json_patch_apply(struct json_object *base, struct json_object *patch, struct json_object **res) { - size_t i; + size_t ii; int rc = 0; if (!base || !json_object_is_type(patch, json_type_array)) { @@ -206,9 +207,9 @@ int json_patch_apply(struct json_object *base, struct json_object *patch, return -1; /* Go through all operations ; apply them on res */ - for (i = 0; i < json_object_array_length(patch); i++) { + for (ii = 0; ii < json_object_array_length(patch); ii++) { struct json_object *jop, *jpath; - struct json_object *patch_elem = json_object_array_get_idx(patch, i); + struct json_object *patch_elem = json_object_array_get_idx(patch, ii); const char *op, *path; if (!json_object_object_get_ex(patch_elem, "op", &jop)) { errno = EINVAL; diff --git a/json_pointer.c b/json_pointer.c index 8260256..e6e5f91 100644 --- a/json_pointer.c +++ b/json_pointer.c @@ -17,6 +17,7 @@ #include "json_object_private.h" #include "json_pointer.h" +#include "json_pointer_private.h" #include "strdup_compat.h" #include "vasprintf_compat.h" diff --git a/json_pointer_private.h b/json_pointer_private.h new file mode 100644 index 0000000..40ec76d --- /dev/null +++ b/json_pointer_private.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Eric Hawicz + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See COPYING for details. + */ + +/** + * @file + * @brief Do not use, json-c internal, may be changed or removed at any time. + */ +#ifndef _json_pointer_private_h_ +#define _json_pointer_private_h_ + +#ifdef __cplusplus +extern "C" { +#endif + +struct json_pointer_get_result { + struct json_object *parent; + struct json_object *obj; + union { + const char *key; + uint32_t index; + } id; +}; + +int json_pointer_get_internal(struct json_object *obj, const char *path, + struct json_pointer_get_result *res); + +typedef int(*json_pointer_array_set_cb)(json_object *parent, size_t idx, + json_object *value, void *priv); + +int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, + struct json_object *value, + json_pointer_array_set_cb array_set_cb, void *priv); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/tests/test1.c b/tests/test1.c index 986861b..12097b2 100644 --- a/tests/test1.c +++ b/tests/test1.c @@ -189,9 +189,10 @@ void test_array_list_expand_internal(void) json_object_put(my_array); } +void test_array_insert_idx(void); void test_array_insert_idx() { - json_object *my_string, *my_int, *my_null, *my_object, *my_array; + json_object *my_array; struct json_object *jo1; my_array = json_object_new_array(); diff --git a/tests/test_json_patch.c b/tests/test_json_patch.c index dad7521..8dd593f 100644 --- a/tests/test_json_patch.c +++ b/tests/test_json_patch.c @@ -7,7 +7,9 @@ #include #include +#include "config.h" #include "json.h" +#include "snprintf_compat.h" #ifndef PATH_MAX #define PATH_MAX 256 @@ -70,7 +72,7 @@ void test_json_patch_using_file(const char *testdir, const char *filename) { char full_filename[PATH_MAX]; (void)snprintf(full_filename, sizeof(full_filename), "%s/%s", testdir, filename); - int i; + size_t ii; json_object *jo = json_object_from_file(full_filename); if (!jo) { @@ -78,8 +80,8 @@ void test_json_patch_using_file(const char *testdir, const char *filename) exit(EXIT_FAILURE); } - for (i = 0; i < json_object_array_length(jo); i++) { - struct json_object *jo1 = json_object_array_get_idx(jo, i); + for (ii = 0; ii < json_object_array_length(jo); ii++) { + struct json_object *jo1 = json_object_array_get_idx(jo, ii); test_json_patch_op(jo1); } From a14a3a680c862321a88b8fb9fc9da922e4dcda93 Mon Sep 17 00:00:00 2001 From: Eric Hawicz Date: Wed, 26 Jul 2023 18:15:07 -0400 Subject: [PATCH 11/15] Fix an uninitialized memory access in json_pointer. Add comments describing when the fields of the internal struct json_pointer_get_result are valid. --- json_patch.c | 6 +++--- json_pointer.c | 17 ++++++++--------- json_pointer_private.h | 9 +++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/json_patch.c b/json_patch.c index 97d9dd8..b48eed8 100644 --- a/json_patch.c +++ b/json_patch.c @@ -49,9 +49,9 @@ static int json_patch_apply_test(struct json_object **res, static int __json_patch_apply_remove(struct json_pointer_get_result *jpres) { if (json_object_is_type(jpres->parent, json_type_array)) { - return json_object_array_del_idx(jpres->parent, jpres->id.index, 1); - } else if (jpres->parent && jpres->id.key) { - json_object_object_del(jpres->parent, jpres->id.key); + return json_object_array_del_idx(jpres->parent, jpres->index_in_parent, 1); + } else if (jpres->parent && jpres->key_in_parent) { + json_object_object_del(jpres->parent, jpres->key_in_parent); return 0; } else { return json_object_put(jpres->obj); diff --git a/json_pointer.c b/json_pointer.c index e6e5f91..89e9e21 100644 --- a/json_pointer.c +++ b/json_pointer.c @@ -190,9 +190,9 @@ static int json_pointer_result_get_recursive(struct json_object *obj, char *path res->parent = parent_obj; res->obj = obj; if (json_object_is_type(res->parent, json_type_array)) - res->id.index = idx; + res->index_in_parent = idx; else - res->id.key = path; + res->key_in_parent = path; } return 0; @@ -228,11 +228,10 @@ int json_pointer_get_internal(struct json_object *obj, const char *path, if (path[0] == '\0') { - if (res) { - res->parent = NULL; - res->obj = obj; - } - res->id.key = NULL; + res->parent = NULL; + res->obj = obj; + res->key_in_parent = NULL; + res->index_in_parent = -1; return 0; } @@ -244,8 +243,8 @@ int json_pointer_get_internal(struct json_object *obj, const char *path, } rc = json_pointer_result_get_recursive(obj, path_copy, res); /* re-map the path string to the const-path string */ - if (rc == 0 && res->id.key && !json_object_is_type(res->parent, json_type_array)) - res->id.key = path + (res->id.key - path_copy); + if (rc == 0 && json_object_is_type(res->parent, json_type_object) && res->key_in_parent) + res->key_in_parent = path + (res->key_in_parent - path_copy); free(path_copy); return rc; diff --git a/json_pointer_private.h b/json_pointer_private.h index 40ec76d..537cabd 100644 --- a/json_pointer_private.h +++ b/json_pointer_private.h @@ -19,10 +19,11 @@ extern "C" { struct json_pointer_get_result { struct json_object *parent; struct json_object *obj; - union { - const char *key; - uint32_t index; - } id; + // The key of the found object; only valid when parent is json_type_object + // Caution: re-uses tail end of the `path` argument to json_pointer_get_internal + const char *key_in_parent; + // the index of the found object; only valid when parent is json_type_array + uint32_t index_in_parent; }; int json_pointer_get_internal(struct json_object *obj, const char *path, From 9dbf2880cc07138753699636d00c4d043ebd0a2f Mon Sep 17 00:00:00 2001 From: Eric Hawicz Date: Wed, 26 Jul 2023 22:01:04 -0400 Subject: [PATCH 12/15] Adjust the behavior of the args passed to json_patch_apply to make it easier to do in place modifications, and add a struct json_patch_error to report more details on failures. --- json_patch.c | 162 ++++++++++++++++++++++++--------- json_patch.h | 54 +++++++++-- tests/test_json_patch.c | 17 +++- tests/test_json_patch.expected | 35 +++++++ 4 files changed, 210 insertions(+), 58 deletions(-) diff --git a/json_patch.c b/json_patch.c index b48eed8..dbc54ec 100644 --- a/json_patch.c +++ b/json_patch.c @@ -1,9 +1,9 @@ /* * Copyright (c) 2021 Alexandru Ardelean. + * Copyright (c) 2023 Eric Hawicz * * This is free software; you can redistribute it and/or modify * it under the terms of the MIT license. See COPYING for details. - * */ #include "config.h" @@ -16,6 +16,33 @@ #include "json_object_private.h" #include "json_pointer_private.h" +#include +#ifndef SIZE_T_MAX +#if SIZEOF_SIZE_T == SIZEOF_INT +#define SIZE_T_MAX UINT_MAX +#elif SIZEOF_SIZE_T == SIZEOF_LONG +#define SIZE_T_MAX ULONG_MAX +#elif SIZEOF_SIZE_T == SIZEOF_LONG_LONG +#define SIZE_T_MAX ULLONG_MAX +#else +#error Unable to determine size of size_t +#endif +#endif + +#define _set_err(_errval, _errmsg) do { \ + patch_error->errno_code = (_errval); \ + patch_error->errmsg = (_errmsg); \ + errno = 0; /* To avoid confusion */ \ +} while (0) + +#define _set_err_from_ptrget(_errval, _fieldname) do { \ + patch_error->errno_code = (_errval); \ + patch_error->errmsg = (_errval) == ENOENT ? \ + "Did not find element referenced by " _fieldname " field" : \ + "Invalid " _fieldname " field"; \ + errno = 0; /* To avoid confusion */ \ +} while(0) + /** * JavaScript Object Notation (JSON) Patch * RFC 6902 - https://tools.ietf.org/html/rfc6902 @@ -23,23 +50,23 @@ static int json_patch_apply_test(struct json_object **res, struct json_object *patch_elem, - const char *path) + const char *path, struct json_patch_error *patch_error) { struct json_object *value1, *value2; if (!json_object_object_get_ex(patch_elem, "value", &value1)) { - errno = EINVAL; + _set_err(EINVAL, "Patch object does not contain a 'value' field"); return -1; } - /* errno should be set by json_pointer_get() */ if (json_pointer_get(*res, path, &value2)) + { + _set_err_from_ptrget(errno, "path"); return -1; + } if (!json_object_equal(value1, value2)) { - json_object_put(*res); - *res = NULL; - errno = ENOENT; + _set_err(ENOENT, "Value of element referenced by 'path' field did not match 'value' field"); return -1; } @@ -58,61 +85,79 @@ static int __json_patch_apply_remove(struct json_pointer_get_result *jpres) } } -static int json_patch_apply_remove(struct json_object **res, const char *path) +static int json_patch_apply_remove(struct json_object **res, const char *path, struct json_patch_error *patch_error) { struct json_pointer_get_result jpres; + int rc; if (json_pointer_get_internal(*res, path, &jpres)) + { + _set_err_from_ptrget(errno, "path"); return -1; + } - return __json_patch_apply_remove(&jpres); + rc = __json_patch_apply_remove(&jpres); + if (rc < 0) + _set_err(EINVAL, "Unable to remove path referenced by 'path' field"); + return rc; } +// callback for json_pointer_set_with_array_cb() static int json_object_array_insert_idx_cb(struct json_object *parent, size_t idx, struct json_object *value, void *priv) { + int rc; int *add = priv; if (idx > json_object_array_length(parent)) { + // Note: will propagate back out through json_pointer_set_with_array_cb() errno = EINVAL; return -1; } if (*add) - return json_object_array_insert_idx(parent, idx, value); + rc = json_object_array_insert_idx(parent, idx, value); else - return json_object_array_put_idx(parent, idx, value); + rc = json_object_array_put_idx(parent, idx, value); + if (rc < 0) + errno = EINVAL; + return rc; } static int json_patch_apply_add_replace(struct json_object **res, struct json_object *patch_elem, - const char *path, int add) + const char *path, int add, struct json_patch_error *patch_error) { struct json_object *value; int rc; if (!json_object_object_get_ex(patch_elem, "value", &value)) { - errno = EINVAL; + _set_err(EINVAL, "Patch object does not contain a 'value' field"); return -1; } /* if this is a replace op, then we need to make sure it exists before replacing */ if (!add && json_pointer_get(*res, path, NULL)) { - errno = ENOENT; + _set_err_from_ptrget(errno, "path"); return -1; } rc = json_pointer_set_with_array_cb(res, path, json_object_get(value), json_object_array_insert_idx_cb, &add); if (rc) + { + _set_err(errno, "Failed to set value at path referenced by 'path' field"); json_object_put(value); + } return rc; } +// callback for json_pointer_set_with_array_cb() static int json_object_array_move_cb(struct json_object *parent, size_t idx, struct json_object *value, void *priv) { + int rc; struct json_pointer_get_result *from = priv; size_t len = json_object_array_length(parent); @@ -127,16 +172,20 @@ static int json_object_array_move_cb(struct json_object *parent, size_t idx, if (idx > len) { + // Note: will propagate back out through json_pointer_set_with_array_cb() errno = EINVAL; return -1; } - return json_object_array_insert_idx(parent, idx, value); + rc = json_object_array_insert_idx(parent, idx, value); + if (rc < 0) + errno = EINVAL; + return rc; } static int json_patch_apply_move_copy(struct json_object **res, struct json_object *patch_elem, - const char *path, int move) + const char *path, int move, struct json_patch_error *patch_error) { json_pointer_array_set_cb array_set_cb; struct json_pointer_get_result from; @@ -146,7 +195,7 @@ static int json_patch_apply_move_copy(struct json_object **res, int rc; if (!json_object_object_get_ex(patch_elem, "from", &jfrom)) { - errno = EINVAL; + _set_err(EINVAL, "Patch does not contain a 'from' field"); return -1; } @@ -163,13 +212,16 @@ static int json_patch_apply_move_copy(struct json_object **res, */ if (from_s_len == strlen(path)) return 0; - errno = EINVAL; + _set_err(EINVAL, "Invalid attempt to move parent under a child"); return -1; } rc = json_pointer_get_internal(*res, from_s, &from); if (rc) + { + _set_err_from_ptrget(errno, "from"); return rc; + } json_object_get(from.obj); @@ -186,65 +238,87 @@ static int json_patch_apply_move_copy(struct json_object **res, rc = json_pointer_set_with_array_cb(res, path, from.obj, array_set_cb, &from); if (rc) + { + _set_err(errno, "Failed to set value at path referenced by 'path' field"); json_object_put(from.obj); + } return rc; } -int json_patch_apply(struct json_object *base, struct json_object *patch, - struct json_object **res) +int json_patch_apply(struct json_object *copy_from, struct json_object *patch, + struct json_object **base, struct json_patch_error *patch_error) { size_t ii; int rc = 0; + struct json_patch_error placeholder; - if (!base || !json_object_is_type(patch, json_type_array)) { - errno = EINVAL; + if (!patch_error) + patch_error = &placeholder; + + patch_error->patch_failure_idx = SIZE_T_MAX; + patch_error->errno_code = 0; + + if (base == NULL|| + (*base == NULL && copy_from == NULL) || + (*base != NULL && copy_from != NULL)) + { + _set_err(EFAULT, "Exactly one of *base or copy_from must be non-NULL"); + return -1; + } + + if (!json_object_is_type(patch, json_type_array)) { + _set_err(EFAULT, "Patch object is not of type json_type_array"); return -1; } - /* errno should be set inside json_object_deep_copy() */ - if (json_object_deep_copy(base, res, NULL) < 0) - return -1; + if (copy_from != NULL) + { + if (json_object_deep_copy(copy_from, base, NULL) < 0) + { + _set_err(ENOMEM, "Unable to copy copy_from using json_object_deep_copy()"); + return -1; + } + } /* Go through all operations ; apply them on res */ for (ii = 0; ii < json_object_array_length(patch); ii++) { struct json_object *jop, *jpath; struct json_object *patch_elem = json_object_array_get_idx(patch, ii); const char *op, *path; + + patch_error->patch_failure_idx = ii; + if (!json_object_object_get_ex(patch_elem, "op", &jop)) { - errno = EINVAL; - rc = -1; - break; + _set_err(EINVAL, "Patch object does not contain 'op' field"); + return -1; } op = json_object_get_string(jop); - json_object_object_get_ex(patch_elem, "path", &jpath); - path = json_object_get_string(jpath); + if (!json_object_object_get_ex(patch_elem, "path", &jpath)) { + _set_err(EINVAL, "Patch object does not contain 'path' field"); + return -1; + } + path = json_object_get_string(jpath); // Note: empty string is ok! if (!strcmp(op, "test")) - rc = json_patch_apply_test(res, patch_elem, path); + rc = json_patch_apply_test(base, patch_elem, path, patch_error); else if (!strcmp(op, "remove")) - rc = json_patch_apply_remove(res, path); + rc = json_patch_apply_remove(base, path, patch_error); else if (!strcmp(op, "add")) - rc = json_patch_apply_add_replace(res, patch_elem, path, 1); + rc = json_patch_apply_add_replace(base, patch_elem, path, 1, patch_error); else if (!strcmp(op, "replace")) - rc = json_patch_apply_add_replace(res, patch_elem, path, 0); + rc = json_patch_apply_add_replace(base, patch_elem, path, 0, patch_error); else if (!strcmp(op, "move")) - rc = json_patch_apply_move_copy(res, patch_elem, path, 1); + rc = json_patch_apply_move_copy(base, patch_elem, path, 1, patch_error); else if (!strcmp(op, "copy")) - rc = json_patch_apply_move_copy(res, patch_elem, path, 0); + rc = json_patch_apply_move_copy(base, patch_elem, path, 0, patch_error); else { - errno = EINVAL; - rc = -1; - break; + _set_err(EINVAL, "Patch object has invalid 'op' field"); + return -1; } if (rc < 0) break; } - if (rc < 0) { - json_object_put(*res); - *res = NULL; - } - return rc; } diff --git a/json_patch.h b/json_patch.h index 80dc8b9..79de5f9 100644 --- a/json_patch.h +++ b/json_patch.h @@ -19,25 +19,59 @@ extern "C" { #endif +/** + * Details of an error that occurred during json_patch_apply() + */ +struct json_patch_error { + /** + * An errno value indicating what kind of error occurred. + * Possible values include: + * - ENOENT - A path referenced in the operation does not exist. + * - EINVAL - An invalid operation or with invalid path was attempted + * - ENOMEM - Unable to allocate memory + * - EFAULT - Invalid arguments were passed to json_patch_apply() + * (i.e. a C API error, vs. a data error like EINVAL) + */ + int errno_code; + + /** + * The index into the patch array of the operation that failed, + * or SIZE_T_MAX for overall errors. + */ + size_t patch_failure_idx; + + /** + * A human readable error message. + * Allocated from static storage, does not need to be freed. + */ + const char *errmsg; +}; + /** * Apply the JSON patch to the base object. - * The patch object must be formatted as per RFC 6902. + * The patch object must be formatted as per RFC 6902, i.e. + * a json_type_array containing patch operations. * If the patch is not correctly formatted, an error will * be returned. * - * The original `base` object will first be copied, and then - * the patch will be applied. - * If anything fails during patching, the `res` object will be - * NULL and the function will return a negative result. + * The json_object at *base will be modified in place. + * Exactly one of *base or copy_from must be non-NULL. + * If *base is NULL, a new copy of copy_from will allocated and populated + * using json_object_deep_copy(). In this case json_object_put() _must_ be + * used to free *base even if the overall patching operation fails. * - * @param base the JSON object which to patch + * If anything fails during patching a negative value will be returned, + * and patch_error (if non-NULL) will be populated with error details. + * + * @param base a pointer to the JSON object which to patch * @param patch the JSON object that describes the patch to be applied - * @param the resulting patched JSON object + * @param copy_from a JSON object to copy to *base + * @param patch_error optional, details about errors * - * @return negative if an error (or not found), or 0 if succeeded + * @return negative if an error (or not found), or 0 if patch completely applied */ -JSON_EXPORT int json_patch_apply(struct json_object *base, struct json_object *patch, - struct json_object **res); +JSON_EXPORT int json_patch_apply(struct json_object *copy_from, struct json_object *patch, + struct json_object **base, struct json_patch_error *patch_error); #ifdef __cplusplus } diff --git a/tests/test_json_patch.c b/tests/test_json_patch.c index 8dd593f..d53c980 100644 --- a/tests/test_json_patch.c +++ b/tests/test_json_patch.c @@ -1,6 +1,7 @@ #ifdef NDEBUG #undef NDEBUG #endif +#include "strerror_override.h" #include #include #include @@ -40,19 +41,27 @@ void test_json_patch_op(struct json_object *jo) return; } fflush(stdout); + struct json_patch_error jperr; if (error) { - assert(-1 == json_patch_apply(doc, patch, &res)); - assert(res == NULL); + assert(-1 == json_patch_apply(doc, patch, &res, &jperr)); + assert(jperr.errno_code != 0); + printf("OK\n"); + printf(" => json_patch_apply failed as expected: %s at patch idx %zu: %s\n", + strerror(jperr.errno_code), jperr.patch_failure_idx, jperr.errmsg); + json_object_put(res); } else { - ret = json_patch_apply(doc, patch, &res); + ret = json_patch_apply(doc, patch, &res, &jperr); if (ret) { fprintf(stderr, "json_patch_apply() returned '%d'\n", ret); fprintf(stderr, "Expected: %s\n", json_object_get_string(expected)); fprintf(stderr, "Got: %s\n", json_object_get_string(res)); + fprintf(stderr, "json_patch_apply failed: %s at patch idx %zu: %s\n", + strerror(jperr.errno_code), jperr.patch_failure_idx, jperr.errmsg); fflush(stderr); assert(0); } assert(res != NULL); + assert(jperr.errno_code == 0); ret = json_object_equal(expected, res); if (ret == 0) { fprintf(stderr, "json_object_equal() returned '%d'\n", ret); @@ -63,9 +72,9 @@ void test_json_patch_op(struct json_object *jo) } json_object_put(res); res = NULL; + printf("OK\n"); } - printf("OK\n"); } void test_json_patch_using_file(const char *testdir, const char *filename) diff --git a/tests/test_json_patch.expected b/tests/test_json_patch.expected index 64e59b8..9d2887a 100644 --- a/tests/test_json_patch.expected +++ b/tests/test_json_patch.expected @@ -1,4 +1,5 @@ Testing '4.1. add with missing object', doc '{ "q": { "bar": 2 } }' patch '[ { "op": "add", "path": "\/a\/b", "value": 1 } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'A.1. Adding an Object Member', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/baz", "value": "qux" } ]' : OK Testing 'A.2. Adding an Array Element', doc '{ "foo": [ "bar", "baz" ] }' patch '[ { "op": "add", "path": "\/foo\/1", "value": "qux" } ]' : OK Testing 'A.3. Removing an Object Member', doc '{ "baz": "qux", "foo": "bar" }' patch '[ { "op": "remove", "path": "\/baz" } ]' : OK @@ -8,12 +9,16 @@ Testing 'A.6. Moving a Value', doc '{ "foo": { "bar": "baz", "waldo": "fred" }, Testing 'A.7. Moving an Array Element', doc '{ "foo": [ "all", "grass", "cows", "eat" ] }' patch '[ { "op": "move", "from": "\/foo\/1", "path": "\/foo\/3" } ]' : OK Testing 'A.8. Testing a Value: Success', doc '{ "baz": "qux", "foo": [ "a", 2, "c" ] }' patch '[ { "op": "test", "path": "\/baz", "value": "qux" }, { "op": "test", "path": "\/foo\/1", "value": 2 } ]' : OK Testing 'A.9. Testing a Value: Error', doc '{ "baz": "qux" }' patch '[ { "op": "test", "path": "\/baz", "value": "bar" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Value of element referenced by 'path' field did not match 'value' field Testing 'A.10. Adding a nested Member Object', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/child", "value": { "grandchild": { } } } ]' : OK Testing 'A.11. Ignoring Unrecognized Elements', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/baz", "value": "qux", "xyz": 123 } ]' : OK Testing 'A.12. Adding to a Non-existent Target', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/baz\/bat", "value": "qux" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'A.13 Invalid JSON Patch Document', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": "\/baz", "value": "qux" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by path field Testing 'A.14. ~ Escape Ordering', doc '{ "\/": 9, "~1": 10 }' patch '[ { "op": "test", "path": "\/~01", "value": 10 } ]' : OK Testing 'A.15. Comparing Strings and Numbers', doc '{ "\/": 9, "~1": 10 }' patch '[ { "op": "test", "path": "\/~01", "value": "10" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Value of element referenced by 'path' field did not match 'value' field Testing 'A.16. Adding an Array Value', doc '{ "foo": [ "bar" ] }' patch '[ { "op": "add", "path": "\/foo\/-", "value": [ "abc", "def" ] } ]' : OK Testing 'empty list, empty docs', doc '{ }' patch '[ ]' : OK Testing 'empty patch list', doc '{ "foo": 1 }' patch '[ ]' : OK @@ -34,7 +39,9 @@ Testing 'Add, /foo/ deep target (trailing slash)', doc '{ "foo": { } }' patch '[ Testing 'Add composite value at top level', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/bar", "value": [ 1, 2 ] } ]' : OK Testing 'Add into composite value', doc '{ "foo": 1, "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "add", "path": "\/baz\/0\/foo", "value": "world" } ]' : OK Testing 'Out of bounds (upper)', doc '{ "bar": [ 1, 2 ] }' patch '[ { "op": "add", "path": "\/bar\/8", "value": "5" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'Out of bounds (lower)', doc '{ "bar": [ 1, 2 ] }' patch '[ { "op": "add", "path": "\/bar\/-1", "value": "5" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field Testing '(null)', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/bar", "value": true } ]' : OK Testing '(null)', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/bar", "value": false } ]' : OK Testing '(null)', doc '{ "foo": 1 }' patch '[ { "op": "add", "path": "\/bar", "value": null } ]' : OK @@ -44,9 +51,12 @@ Testing '(null)', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/1", Testing '(null)', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/0", "value": "bar" } ]' : OK Testing 'push item to array via last index + 1', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/2", "value": "bar" } ]' : OK Testing 'add item to array at index > length should fail', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/3", "value": "bar" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'test against implementation-specific numeric parsing', doc '{ "1e0": "foo" }' patch '[ { "op": "test", "path": "\/1e0", "value": "foo" } ]' : OK Testing 'test with bad number should fail', doc '[ "foo", "bar" ]' patch '[ { "op": "test", "path": "\/1e0", "value": "bar" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Invalid path field Testing 'Object operation on array target', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/bar", "value": 42 } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'value in array add not flattened', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/1", "value": [ "bar", "baz" ] } ]' : OK Testing '(null)', doc '{ "foo": 1, "bar": [ 1, 2, 3, 4 ] }' patch '[ { "op": "remove", "path": "\/bar" } ]' : OK Testing '(null)', doc '{ "foo": 1, "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "remove", "path": "\/baz\/0\/qux" } ]' : OK @@ -60,6 +70,7 @@ Testing '(null)', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/0", "value Testing 'value in array replace not flattened', doc '[ "foo", "sil" ]' patch '[ { "op": "replace", "path": "\/1", "value": [ "bar", "baz" ] } ]' : OK Testing 'replace whole document', doc '{ "foo": "bar" }' patch '[ { "op": "replace", "path": "", "value": { "baz": "qux" } } ]' : OK Testing 'test replace with missing parent key should fail', doc '{ "bar": "baz" }' patch '[ { "op": "replace", "path": "\/foo\/bar", "value": false } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by path field Testing 'spurious patch properties', doc '{ "foo": 1 }' patch '[ { "op": "test", "path": "\/foo", "value": 1, "spurious": 1 } ]' : OK Testing 'null value should be valid obj property', doc '{ "foo": null }' patch '[ { "op": "test", "path": "\/foo", "value": null } ]' : OK Testing 'null value should be valid obj property to be replaced with something truthy', doc '{ "foo": null }' patch '[ { "op": "replace", "path": "\/foo", "value": "truthy" } ]' : OK @@ -71,6 +82,7 @@ Testing 'test should pass despite rearrangement', doc '{ "foo": { "foo": 1, "bar Testing 'test should pass despite (nested) rearrangement', doc '{ "foo": [ { "foo": 1, "bar": 2 } ] }' patch '[ { "op": "test", "path": "\/foo", "value": [ { "bar": 2, "foo": 1 } ] } ]' : OK Testing 'test should pass - no error', doc '{ "foo": { "bar": [ 1, 2, 5, 4 ] } }' patch '[ { "op": "test", "path": "\/foo", "value": { "bar": [ 1, 2, 5, 4 ] } } ]' : OK Testing 'test op should fail', doc '{ "foo": { "bar": [ 1, 2, 5, 4 ] } }' patch '[ { "op": "test", "path": "\/foo", "value": [ 1, 2 ] } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Value of element referenced by 'path' field did not match 'value' field Testing 'Whole document', doc '{ "foo": 1 }' patch '[ { "op": "test", "path": "", "value": { "foo": 1 } } ]' : SKIPPING - no expected or error conditions in test Testing 'Empty-string element', doc '{ "": 1 }' patch '[ { "op": "test", "path": "\/", "value": 1 } ]' : OK Testing '(null)', doc '{ "foo": [ "bar", "baz" ], "": 0, "a\/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }' patch '[ { "op": "test", "path": "\/foo", "value": [ "bar", "baz" ] }, { "op": "test", "path": "\/foo\/0", "value": "bar" }, { "op": "test", "path": "\/", "value": 0 }, { "op": "test", "path": "\/a~1b", "value": 1 }, { "op": "test", "path": "\/c%d", "value": 2 }, { "op": "test", "path": "\/e^f", "value": 3 }, { "op": "test", "path": "\/g|h", "value": 4 }, { "op": "test", "path": "\/i\\j", "value": 5 }, { "op": "test", "path": "\/k\"l", "value": 6 }, { "op": "test", "path": "\/ ", "value": 7 }, { "op": "test", "path": "\/m~0n", "value": 8 } ]' : OK @@ -82,29 +94,52 @@ Testing 'replacing the root of the document is possible with add', doc '{ "foo": Testing 'Adding to "/-" adds to the end of the array', doc '[ 1, 2 ]' patch '[ { "op": "add", "path": "\/-", "value": { "foo": [ "bar", "baz" ] } } ]' : OK Testing 'Adding to "/-" adds to the end of the array, even n levels down', doc '[ 1, 2, [ 3, [ 4, 5 ] ] ]' patch '[ { "op": "add", "path": "\/2\/1\/-", "value": { "foo": [ "bar", "baz" ] } } ]' : OK Testing 'test remove with bad number should fail', doc '{ "foo": 1, "baz": [ { "qux": "hello" } ] }' patch '[ { "op": "remove", "path": "\/baz\/1e0\/qux" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Invalid path field Testing 'test remove on array', doc '[ 1, 2, 3, 4 ]' patch '[ { "op": "remove", "path": "\/0" } ]' : OK Testing 'test repeated removes', doc '[ 1, 2, 3, 4 ]' patch '[ { "op": "remove", "path": "\/1" }, { "op": "remove", "path": "\/2" } ]' : OK Testing 'test remove with bad index should fail', doc '[ 1, 2, 3, 4 ]' patch '[ { "op": "remove", "path": "\/1e0" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Invalid path field Testing 'test replace with bad number should fail', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/1e0", "value": false } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Invalid path field Testing 'test copy with bad number should fail', doc '{ "baz": [ 1, 2, 3 ], "bar": 1 }' patch '[ { "op": "copy", "from": "\/baz\/1e0", "path": "\/boo" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Invalid from field Testing 'test move with bad number should fail', doc '{ "foo": 1, "baz": [ 1, 2, 3, 4 ] }' patch '[ { "op": "move", "from": "\/baz\/1e0", "path": "\/foo" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Invalid from field Testing 'test add with bad number should fail', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/1e0", "value": "bar" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'missing 'path' parameter', doc '{ }' patch '[ { "op": "add", "value": "bar" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object does not contain 'path' field Testing ''path' parameter with null value', doc '{ }' patch '[ { "op": "add", "path": null, "value": "bar" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'invalid JSON Pointer token', doc '{ }' patch '[ { "op": "add", "path": "foo", "value": "bar" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'missing 'value' parameter to add', doc '[ 1 ]' patch '[ { "op": "add", "path": "\/-" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object does not contain a 'value' field Testing 'missing 'value' parameter to replace', doc '[ 1 ]' patch '[ { "op": "replace", "path": "\/0" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object does not contain a 'value' field Testing 'missing 'value' parameter to test', doc '[ null ]' patch '[ { "op": "test", "path": "\/0" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object does not contain a 'value' field Testing 'missing value parameter to test - where undef is falsy', doc '[ false ]' patch '[ { "op": "test", "path": "\/0" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object does not contain a 'value' field Testing 'missing from parameter to copy', doc '[ 1 ]' patch '[ { "op": "copy", "path": "\/-" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch does not contain a 'from' field Testing 'missing from location to copy', doc '{ "foo": 1 }' patch '[ { "op": "copy", "from": "\/bar", "path": "\/foo" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by from field Testing 'missing from parameter to move', doc '{ "foo": 1 }' patch '[ { "op": "move", "path": "" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch does not contain a 'from' field Testing 'missing from location to move', doc '{ "foo": 1 }' patch '[ { "op": "move", "from": "\/bar", "path": "\/foo" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by from field Testing 'duplicate ops', doc '{ "foo": "bar" }' patch '[ { "op": "move", "path": "\/baz", "value": "qux", "from": "\/foo" } ]' : SKIPPING - disabled in the test spec Testing 'unrecognized op should fail', doc '{ "foo": 1 }' patch '[ { "op": "spam", "path": "\/foo", "value": 1 } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object has invalid 'op' field Testing 'test with bad array number that has leading zeros', doc '[ "foo", "bar" ]' patch '[ { "op": "test", "path": "\/00", "value": "foo" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Invalid path field Testing 'test with bad array number that has leading zeros', doc '[ "foo", "bar" ]' patch '[ { "op": "test", "path": "\/01", "value": "bar" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Invalid path field Testing 'Removing nonexistent field', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": "\/baz" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by path field Testing 'Removing deep nonexistent path', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": "\/missing1\/missing2" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by path field Testing 'Removing nonexistent index', doc '[ "foo", "bar" ]' patch '[ { "op": "remove", "path": "\/2" } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by path field Testing 'Patch with different capitalisation than doc', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/FOO", "value": "BAR" } ]' : OK From ce3184243a20af59a5f5ad796c76acc8d0726ff1 Mon Sep 17 00:00:00 2001 From: Eric Hawicz Date: Fri, 28 Jul 2023 22:12:51 -0400 Subject: [PATCH 13/15] Fix json_patch_apply handling of removing the whole document (i.e. "path":""). Enable all disabled tests, add a few more including some with null documents. --- json_patch.c | 10 ++++- tests/json_patch_spec_tests.json | 4 +- tests/json_patch_tests.json | 71 ++++++++++++++++++++++++++++---- tests/test_json_patch.c | 19 ++++----- tests/test_json_patch.expected | 17 +++++++- 5 files changed, 97 insertions(+), 24 deletions(-) diff --git a/json_patch.c b/json_patch.c index dbc54ec..5bbc308 100644 --- a/json_patch.c +++ b/json_patch.c @@ -81,7 +81,10 @@ static int __json_patch_apply_remove(struct json_pointer_get_result *jpres) json_object_object_del(jpres->parent, jpres->key_in_parent); return 0; } else { - return json_object_put(jpres->obj); + // We're removing the root object + (void)json_object_put(jpres->obj); + jpres->obj = NULL; + return 0; } } @@ -99,6 +102,9 @@ static int json_patch_apply_remove(struct json_object **res, const char *path, s rc = __json_patch_apply_remove(&jpres); if (rc < 0) _set_err(EINVAL, "Unable to remove path referenced by 'path' field"); + // This means we removed and freed the root object, i.e. *res + if (jpres.parent == NULL) + *res = NULL; return rc; } @@ -223,6 +229,8 @@ static int json_patch_apply_move_copy(struct json_object **res, return rc; } + // Note: it's impossible for json_pointer to find the root obj, due + // to the path check above, so from.parent is guaranteed non-NULL json_object_get(from.obj); if (!move) { diff --git a/tests/json_patch_spec_tests.json b/tests/json_patch_spec_tests.json index c160535..d7d407d 100644 --- a/tests/json_patch_spec_tests.json +++ b/tests/json_patch_spec_tests.json @@ -192,8 +192,8 @@ "patch": [ { "op": "add", "path": "/baz", "value": "qux", "op": "remove" } ], - "error": "operation has two 'op' members", - "disabled": true + "error_wont_happen_in_jsonc": "operation has two 'op' members", + "error": "Did not find element referenced by path field" }, { diff --git a/tests/json_patch_tests.json b/tests/json_patch_tests.json index 4124d51..8cafb93 100644 --- a/tests/json_patch_tests.json +++ b/tests/json_patch_tests.json @@ -52,8 +52,8 @@ { "comment": "Toplevel scalar values OK?", "doc": "foo", "patch": [{"op": "replace", "path": "", "value": "bar"}], - "expected": "bar", - "disabled": true }, + "expected": "bar" + }, { "comment": "replace object document with array document?", "doc": {}, @@ -202,6 +202,55 @@ "patch": [{"op": "replace", "path": "", "value": {"baz": "qux"}}], "expected": {"baz": "qux"} }, + { "comment": "add whole document, null", + "doc": {}, + "Note1": "We can't pass null in to json_patch_apply, so start with _something_ and remove it", + "patch": [ + {"op": "remove", "path": ""}, + {"op": "add", "path": "", "value": {"baz": "qux"}} + ], + "expected": {"baz": "qux"} }, + + { "comment": "replace whole document, null", + "doc": {}, + "Note1": "We can't pass null in to json_patch_apply, so start with _something_ and remove it", + "patch": [ + {"op": "remove", "path": ""}, + {"op": "replace", "path": "", "value": {"baz": "qux"}} + ], + "error": "The spec says the target location must exist, so replacing a null document fails" + }, + + { "comment": "remove whole document", + "doc": {"foo": "bar"}, + "patch": [{"op": "remove", "path": ""}], + "expected": null }, + + { "comment": "remove whole document", + "doc": {"foo": "bar"}, + "patch": [{"op": "remove", "path": ""}], + "expected": null }, + + { "comment": "remove whole document, array", + "doc": ["foo", "bar"], + "patch": [{"op": "remove", "path": ""}], + "expected": null }, + + { "comment": "remove whole document, string", + "doc": "foo", + "patch": [{"op": "remove", "path": ""}], + "expected": null }, + + { "comment": "remove whole document, null", + "doc": {}, + "Note1": "We can't pass null in to json_patch_apply, so start with _something_ and remove it", + "patch": [ + {"op": "remove", "path": ""}, + {"op": "remove", "path": ""}, + ], + "error": "The spec says the target location must exist, so removing a null document fails" + }, + { "comment": "test replace with missing parent key should fail", "doc": {"bar": "baz"}, "patch": [{"op": "replace", "path": "/foo/bar", "value": false}], @@ -261,10 +310,16 @@ "patch": [{"op": "test", "path": "/foo", "value": [1, 2]}], "error": "test op should fail" }, - { "comment": "Whole document", + { "comment": "Test the whole document", "doc": { "foo": 1 }, "patch": [{"op": "test", "path": "", "value": {"foo": 1}}], - "disabled": true }, + "expected": { "foo": 1 } }, + + { "comment": "Test the whole document, no match", + "doc": { "foo": 1 }, + "patch": [{"op": "test", "path": "", "value": {"foo": 2}}], + "expected": { "foo": 1 }, + "error": "Tested value does not match original doc" }, { "comment": "Empty-string element", "doc": { "": 1 }, @@ -438,13 +493,13 @@ "patch": [ { "op": "move", "from": "/bar", "path": "/foo" } ], "error": "missing 'from' location" }, - { "comment": "duplicate ops", + { "comment": "duplicate ops, json-c parses this as op:move", "doc": { "foo": "bar" }, "patch": [ { "op": "add", "path": "/baz", "value": "qux", "op": "move", "from":"/foo" } ], - "error": "patch has two 'op' members", - "disabled_in_json_c": true, - "disabled": true }, + "error_wont_happen_in_jsonc": "patch has two 'op' members", + "expected": { "baz": "bar" } + }, { "comment": "unrecognized op should fail", "doc": {"foo": 1}, diff --git a/tests/test_json_patch.c b/tests/test_json_patch.c index d53c980..eeb791c 100644 --- a/tests/test_json_patch.c +++ b/tests/test_json_patch.c @@ -21,9 +21,9 @@ void test_json_patch_op(struct json_object *jo) const char *comment = json_object_get_string(json_object_object_get(jo, "comment")); struct json_object *doc = json_object_object_get(jo, "doc"); struct json_object *patch = json_object_object_get(jo, "patch"); - struct json_object *expected = json_object_object_get(jo, "expected"); + struct json_object *expected = NULL; + json_bool have_expected = json_object_object_get_ex(jo, "expected", &expected); struct json_object *error = json_object_object_get(jo, "error"); - int disabled_test = json_object_get_boolean(json_object_object_get(jo, "disabled_in_json_c")); const char *error_s = json_object_get_string(error); struct json_object *res = NULL; int ret; @@ -32,13 +32,9 @@ void test_json_patch_op(struct json_object *jo) comment ? comment : error_s, json_object_get_string(doc), json_object_get_string(patch)); - if (disabled_test) { - printf("SKIPPING - disabled in the test spec\n"); - return; - } - if (!error && !expected) { - printf("SKIPPING - no expected or error conditions in test\n"); - return; + if (!error && !have_expected) { + printf("BAD TEST - no expected or error conditions in test: %s\n", json_object_to_json_string(jo)); + assert(0); } fflush(stdout); struct json_patch_error jperr; @@ -54,13 +50,13 @@ void test_json_patch_op(struct json_object *jo) if (ret) { fprintf(stderr, "json_patch_apply() returned '%d'\n", ret); fprintf(stderr, "Expected: %s\n", json_object_get_string(expected)); - fprintf(stderr, "Got: %s\n", json_object_get_string(res)); + fprintf(stderr, "Got: %s\n", res ? json_object_get_string(res) : "(null)"); fprintf(stderr, "json_patch_apply failed: %s at patch idx %zu: %s\n", strerror(jperr.errno_code), jperr.patch_failure_idx, jperr.errmsg); fflush(stderr); assert(0); } - assert(res != NULL); + // Note: res might be NULL if the whole document was removed assert(jperr.errno_code == 0); ret = json_object_equal(expected, res); if (ret == 0) { @@ -83,6 +79,7 @@ void test_json_patch_using_file(const char *testdir, const char *filename) (void)snprintf(full_filename, sizeof(full_filename), "%s/%s", testdir, filename); size_t ii; + printf("Testing using file %s\n", filename); json_object *jo = json_object_from_file(full_filename); if (!jo) { fprintf(stderr, "FAIL: unable to open %s: %s\n", full_filename, strerror(errno)); diff --git a/tests/test_json_patch.expected b/tests/test_json_patch.expected index 9d2887a..523d276 100644 --- a/tests/test_json_patch.expected +++ b/tests/test_json_patch.expected @@ -1,3 +1,4 @@ +Testing using file json_patch_spec_tests.json Testing '4.1. add with missing object', doc '{ "q": { "bar": 2 } }' patch '[ { "op": "add", "path": "\/a\/b", "value": 1 } ]' : OK => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Failed to set value at path referenced by 'path' field Testing 'A.1. Adding an Object Member', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/baz", "value": "qux" } ]' : OK @@ -20,6 +21,7 @@ Testing 'A.14. ~ Escape Ordering', doc '{ "\/": 9, "~1": 10 }' patch '[ { "op": Testing 'A.15. Comparing Strings and Numbers', doc '{ "\/": 9, "~1": 10 }' patch '[ { "op": "test", "path": "\/~01", "value": "10" } ]' : OK => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Value of element referenced by 'path' field did not match 'value' field Testing 'A.16. Adding an Array Value', doc '{ "foo": [ "bar" ] }' patch '[ { "op": "add", "path": "\/foo\/-", "value": [ "abc", "def" ] } ]' : OK +Testing using file json_patch_tests.json Testing 'empty list, empty docs', doc '{ }' patch '[ ]' : OK Testing 'empty patch list', doc '{ "foo": 1 }' patch '[ ]' : OK Testing 'rearrangements OK?', doc '{ "foo": 1, "bar": 2 }' patch '[ ]' : OK @@ -69,6 +71,15 @@ Testing '(null)', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/0", "value Testing '(null)', doc '[ "" ]' patch '[ { "op": "replace", "path": "\/0", "value": null } ]' : OK Testing 'value in array replace not flattened', doc '[ "foo", "sil" ]' patch '[ { "op": "replace", "path": "\/1", "value": [ "bar", "baz" ] } ]' : OK Testing 'replace whole document', doc '{ "foo": "bar" }' patch '[ { "op": "replace", "path": "", "value": { "baz": "qux" } } ]' : OK +Testing 'add whole document, null', doc '{ }' patch '[ { "op": "remove", "path": "" }, { "op": "add", "path": "", "value": { "baz": "qux" } } ]' : OK +Testing 'replace whole document, null', doc '{ }' patch '[ { "op": "remove", "path": "" }, { "op": "replace", "path": "", "value": { "baz": "qux" } } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 1: Invalid path field +Testing 'remove whole document', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": "" } ]' : OK +Testing 'remove whole document', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": "" } ]' : OK +Testing 'remove whole document, array', doc '[ "foo", "bar" ]' patch '[ { "op": "remove", "path": "" } ]' : OK +Testing 'remove whole document, string', doc 'foo' patch '[ { "op": "remove", "path": "" } ]' : OK +Testing 'remove whole document, null', doc '{ }' patch '[ { "op": "remove", "path": "" }, { "op": "remove", "path": "" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 1: Invalid path field Testing 'test replace with missing parent key should fail', doc '{ "bar": "baz" }' patch '[ { "op": "replace", "path": "\/foo\/bar", "value": false } ]' : OK => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by path field Testing 'spurious patch properties', doc '{ "foo": 1 }' patch '[ { "op": "test", "path": "\/foo", "value": 1, "spurious": 1 } ]' : OK @@ -83,7 +94,9 @@ Testing 'test should pass despite (nested) rearrangement', doc '{ "foo": [ { "fo Testing 'test should pass - no error', doc '{ "foo": { "bar": [ 1, 2, 5, 4 ] } }' patch '[ { "op": "test", "path": "\/foo", "value": { "bar": [ 1, 2, 5, 4 ] } } ]' : OK Testing 'test op should fail', doc '{ "foo": { "bar": [ 1, 2, 5, 4 ] } }' patch '[ { "op": "test", "path": "\/foo", "value": [ 1, 2 ] } ]' : OK => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Value of element referenced by 'path' field did not match 'value' field -Testing 'Whole document', doc '{ "foo": 1 }' patch '[ { "op": "test", "path": "", "value": { "foo": 1 } } ]' : SKIPPING - no expected or error conditions in test +Testing 'Test the whole document', doc '{ "foo": 1 }' patch '[ { "op": "test", "path": "", "value": { "foo": 1 } } ]' : OK +Testing 'Test the whole document, no match', doc '{ "foo": 1 }' patch '[ { "op": "test", "path": "", "value": { "foo": 2 } } ]' : OK + => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Value of element referenced by 'path' field did not match 'value' field Testing 'Empty-string element', doc '{ "": 1 }' patch '[ { "op": "test", "path": "\/", "value": 1 } ]' : OK Testing '(null)', doc '{ "foo": [ "bar", "baz" ], "": 0, "a\/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }' patch '[ { "op": "test", "path": "\/foo", "value": [ "bar", "baz" ] }, { "op": "test", "path": "\/foo\/0", "value": "bar" }, { "op": "test", "path": "\/", "value": 0 }, { "op": "test", "path": "\/a~1b", "value": 1 }, { "op": "test", "path": "\/c%d", "value": 2 }, { "op": "test", "path": "\/e^f", "value": 3 }, { "op": "test", "path": "\/g|h", "value": 4 }, { "op": "test", "path": "\/i\\j", "value": 5 }, { "op": "test", "path": "\/k\"l", "value": 6 }, { "op": "test", "path": "\/ ", "value": 7 }, { "op": "test", "path": "\/m~0n", "value": 8 } ]' : OK Testing 'Move to same location has no effect', doc '{ "foo": 1 }' patch '[ { "op": "move", "from": "\/foo", "path": "\/foo" } ]' : OK @@ -129,7 +142,7 @@ Testing 'missing from parameter to move', doc '{ "foo": 1 }' patch '[ { "op": "m => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch does not contain a 'from' field Testing 'missing from location to move', doc '{ "foo": 1 }' patch '[ { "op": "move", "from": "\/bar", "path": "\/foo" } ]' : OK => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by from field -Testing 'duplicate ops', doc '{ "foo": "bar" }' patch '[ { "op": "move", "path": "\/baz", "value": "qux", "from": "\/foo" } ]' : SKIPPING - disabled in the test spec +Testing 'duplicate ops, json-c parses this as op:move', doc '{ "foo": "bar" }' patch '[ { "op": "move", "path": "\/baz", "value": "qux", "from": "\/foo" } ]' : OK Testing 'unrecognized op should fail', doc '{ "foo": 1 }' patch '[ { "op": "spam", "path": "\/foo", "value": 1 } ]' : OK => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object has invalid 'op' field Testing 'test with bad array number that has leading zeros', doc '[ "foo", "bar" ]' patch '[ { "op": "test", "path": "\/00", "value": "foo" } ]' : OK From 469bc0e4bb4a33c05f03cbf7ea57ad1d0f78af82 Mon Sep 17 00:00:00 2001 From: Eric Hawicz Date: Sat, 29 Jul 2023 11:22:12 -0400 Subject: [PATCH 14/15] Work around a somewhat misleading warning about "a function declaration without a prototype is deprecated in all versions of C" in test1.c --- tests/test1.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test1.c b/tests/test1.c index 12097b2..b45f752 100644 --- a/tests/test1.c +++ b/tests/test1.c @@ -190,7 +190,7 @@ void test_array_list_expand_internal(void) } void test_array_insert_idx(void); -void test_array_insert_idx() +void test_array_insert_idx(void) { json_object *my_array; struct json_object *jo1; From 612ba56f063f9607da3cbdd64863efc896ad0bba Mon Sep 17 00:00:00 2001 From: Eric Hawicz Date: Sat, 29 Jul 2023 21:45:16 -0400 Subject: [PATCH 15/15] Don't export json_pointer_get_internal, move json_object_array_insert_idx and json_patch_apply to the JSONC_0.17 section in json-c.sym --- json-c.sym | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/json-c.sym b/json-c.sym index 15440bf..c7501bb 100644 --- a/json-c.sym +++ b/json-c.sym @@ -18,7 +18,6 @@ JSONC_PRIVATE { array_list_new; array_list_put_idx; array_list_sort; - json_pointer_get_internal; json_hex_chars; json_parse_double; json_parse_int64; @@ -168,13 +167,12 @@ JSONC_0.15 { } JSONC_0.14; JSONC_0.16 { - global: - json_object_array_insert_idx; - json_patch_apply; +# No new symbols in 0.16 } JSONC_0.15; JSONC_0.17 { -# global: -# ...new symbols here... + global: + json_object_array_insert_idx; + json_patch_apply; # array_list_insert_idx is intentionally not exported } JSONC_0.16;