diff --git a/cJSON.c b/cJSON.c index bc343ba3a84fbf4cdb4237e4a3370b4f955cddae..a25556d5d36488d0f7bdcc882c169a4b241d83de 100644 --- a/cJSON.c +++ b/cJSON.c @@ -2585,3 +2585,13 @@ CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * cons return false; } } + +CJSON_PUBLIC(void *) cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) cJSON_free(void *object) +{ + global_hooks.deallocate(object); +} diff --git a/cJSON.h b/cJSON.h index c29ba172cdb02f8cbca5a9355787f41e3d47d85f..0ddab6f34ca76f832bcb5e8a448fb886469f57da 100644 --- a/cJSON.h +++ b/cJSON.h @@ -241,6 +241,10 @@ CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); /* Macro for iterating over an array */ #define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) +/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ +CJSON_PUBLIC(void *) cJSON_malloc(size_t size); +CJSON_PUBLIC(void) cJSON_free(void *object); + #ifdef __cplusplus } #endif diff --git a/cJSON_Utils.c b/cJSON_Utils.c index 92bb0b4ede8c741eee7e54c921b5fd7132c8eb18..aac2b07b3b806e1698ded57bccd3cf32a7959807 100644 --- a/cJSON_Utils.c +++ b/cJSON_Utils.c @@ -194,6 +194,46 @@ CJSON_PUBLIC(char *) cJSONUtils_FindPointerFromObjectTo(cJSON *object, cJSON *ta return NULL; } +/* non broken version of cJSON_GetArrayItem */ +static cJSON *get_array_item(const cJSON *array, size_t item) +{ + cJSON *child = array ? array->child : NULL; + while ((child != NULL) && (item > 0)) + { + item--; + child = child->next; + } + + return child; +} + +static cJSON_bool decode_array_index_from_pointer(const unsigned char * const pointer, size_t * const index) +{ + size_t parsed_index = 0; + size_t position = 0; + + if ((pointer[0] == '0') && ((pointer[1] != '\0') && (pointer[1] != '/'))) + { + /* leading zeroes are not permitted */ + return 0; + } + + for (position = 0; (pointer[position] >= '0') && (*pointer <= '9'); position++) + { + parsed_index = (10 * parsed_index) + (size_t)(pointer[position] - '0'); + + } + + if ((pointer[position] != '\0') && (pointer[position] != '/')) + { + return 0; + } + + *index = parsed_index; + + return 1; +} + CJSON_PUBLIC(cJSON *) cJSONUtils_GetPointer(cJSON *object, const char *pointer) { /* follow path of the pointer */ @@ -201,22 +241,13 @@ CJSON_PUBLIC(cJSON *) cJSONUtils_GetPointer(cJSON *object, const char *pointer) { if (cJSON_IsArray(object)) { - size_t which = 0; - /* parse array index */ - while ((*pointer >= '0') && (*pointer <= '9')) - { - which = (10 * which) + (size_t)(*pointer++ - '0'); - } - if (*pointer && (*pointer != '/')) - { - /* not end of string or new path token */ - return NULL; - } - if (which > INT_MAX) + size_t index = 0; + if (!decode_array_index_from_pointer((const unsigned char*)pointer, &index)) { return NULL; } - object = cJSON_GetArrayItem(object, (int)which); + + object = get_array_item(object, index); } else if (cJSON_IsObject(object)) { @@ -262,6 +293,39 @@ static void cJSONUtils_InplaceDecodePointerString(unsigned char *string) *s2 = '\0'; } +/* non-broken cJSON_DetachItemFromArray */ +static cJSON *detach_item_from_array(cJSON *array, size_t which) +{ + cJSON *c = array->child; + while (c && (which > 0)) + { + c = c->next; + which--; + } + if (!c) + { + /* item doesn't exist */ + return NULL; + } + if (c->prev) + { + /* not the first element */ + c->prev->next = c->next; + } + if (c->next) + { + c->next->prev = c->prev; + } + if (c==array->child) + { + array->child = c->next; + } + /* make sure the detached item doesn't point anywhere anymore */ + c->prev = c->next = NULL; + + return c; +} + static cJSON *cJSONUtils_PatchDetach(cJSON *object, const unsigned char *path) { unsigned char *parentptr = NULL; @@ -294,7 +358,13 @@ static cJSON *cJSONUtils_PatchDetach(cJSON *object, const unsigned char *path) } else if (cJSON_IsArray(parent)) { - ret = cJSON_DetachItemFromArray(parent, atoi((char*)childptr)); + size_t index = 0; + if (!decode_array_index_from_pointer(childptr, &index)) + { + free(parentptr); + return NULL; + } + ret = detach_item_from_array(parent, index); } else if (cJSON_IsObject(parent)) { @@ -364,19 +434,59 @@ static int cJSONUtils_Compare(cJSON *a, cJSON *b) return 0; } +/* non broken version of cJSON_InsertItemInArray */ +static cJSON_bool insert_item_in_array(cJSON *array, size_t which, cJSON *newitem) +{ + cJSON *child = array->child; + while (child && (which > 0)) + { + child = child->next; + which--; + } + if (which > 0) + { + /* item is after the end of the array */ + return 0; + } + if (child == NULL) + { + cJSON_AddItemToArray(array, newitem); + return 1; + } + + /* insert into the linked list */ + newitem->next = child; + newitem->prev = child->prev; + child->prev = newitem; + + /* was it at the beginning */ + if (child == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + + return 1; +} + +enum patch_operation { INVALID, ADD, REMOVE, REPLACE, MOVE, COPY, TEST }; + static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch) { cJSON *op = NULL; cJSON *path = NULL; cJSON *value = NULL; cJSON *parent = NULL; - int opcode = 0; + enum patch_operation opcode = INVALID; unsigned char *parentptr = NULL; unsigned char *childptr = NULL; op = cJSON_GetObjectItem(patch, "op"); path = cJSON_GetObjectItem(patch, "path"); - if (!op || !path) + if (!cJSON_IsString(op) || !cJSON_IsString(path)) { /* malformed patch. */ return 2; @@ -385,23 +495,23 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch) /* decode operation */ if (!strcmp(op->valuestring, "add")) { - opcode = 0; + opcode = ADD; } else if (!strcmp(op->valuestring, "remove")) { - opcode = 1; + opcode = REMOVE; } else if (!strcmp(op->valuestring, "replace")) { - opcode = 2; + opcode = REPLACE; } else if (!strcmp(op->valuestring, "move")) { - opcode = 3; + opcode = MOVE; } else if (!strcmp(op->valuestring, "copy")) { - opcode = 4; + opcode = COPY; } else if (!strcmp(op->valuestring, "test")) { @@ -414,20 +524,99 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch) return 3; } - /* Remove/Replace */ - if ((opcode == 1) || (opcode == 2)) + /* special case for replacing the root */ + if (path->valuestring[0] == '\0') + { + if (opcode == REMOVE) + { + /* remove possible children */ + if (object->child != NULL) + { + cJSON_Delete(object->child); + } + + /* remove other allocated resources */ + if (object->string != NULL) + { + cJSON_free(object->string); + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + + /* make it invalid */ + memset(object, '\0', sizeof(cJSON)); + + return 0; + } + + if ((opcode == REPLACE) || (opcode == ADD)) + { + /* remove possible children */ + if (object->child != NULL) + { + cJSON_Delete(object->child); + } + + /* remove other allocated resources */ + if (object->string != NULL) + { + cJSON_free(object->string); + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + + value = cJSON_GetObjectItem(patch, "value"); + if (value == NULL) + { + /* missing "value" for add/replace. */ + return 7; + } + + value = cJSON_Duplicate(value, 1); + if (value == NULL) + { + /* out of memory for add/replace. */ + return 8; + } + /* the string "value" isn't needed */ + if (value->string != NULL) + { + cJSON_free(value->string); + value->string = NULL; + } + + /* copy over the value object */ + memcpy(object, value, sizeof(cJSON)); + + /* delete the duplicated value */ + cJSON_free(value); + + return 0; + } + } + + if ((opcode == REMOVE) || (opcode == REPLACE)) { /* Get rid of old. */ - cJSON_Delete(cJSONUtils_PatchDetach(object, (unsigned char*)path->valuestring)); - if (opcode == 1) + cJSON *old_item = cJSONUtils_PatchDetach(object, (unsigned char*)path->valuestring); + if (old_item == NULL) { - /* For Remove, this is job done. */ + return 13; + } + cJSON_Delete(old_item); + if (opcode == REMOVE) + { + /* For Remove, this job is done. */ return 0; } } /* Copy/Move uses "from". */ - if ((opcode == 3) || (opcode == 4)) + if ((opcode == MOVE) || (opcode == COPY)) { cJSON *from = cJSON_GetObjectItem(patch, "from"); if (!from) @@ -436,14 +625,12 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch) return 4; } - if (opcode == 3) + if (opcode == MOVE) { - /* move */ value = cJSONUtils_PatchDetach(object, (unsigned char*)from->valuestring); } - if (opcode == 4) + if (opcode == COPY) { - /* copy */ value = cJSONUtils_GetPointer(object, from->valuestring); } if (!value) @@ -451,7 +638,7 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch) /* missing "from" for copy/move. */ return 5; } - if (opcode == 4) + if (opcode == COPY) { value = cJSON_Duplicate(value, 1); } @@ -505,7 +692,20 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch) } else { - cJSON_InsertItemInArray(parent, atoi((char*)childptr), value); + size_t index = 0; + if (!decode_array_index_from_pointer(childptr, &index)) + { + free(parentptr); + cJSON_Delete(value); + return 11; + } + + if (!insert_item_in_array(parent, index, value)) + { + free(parentptr); + cJSON_Delete(value); + return 10; + } } } else if (cJSON_IsObject(parent)) @@ -526,12 +726,7 @@ CJSON_PUBLIC(int) cJSONUtils_ApplyPatches(cJSON *object, cJSON *patches) { int err = 0; - if (patches == NULL) - { - return 1; - } - - if (cJSON_IsArray(patches)) + if (!cJSON_IsArray(patches)) { /* malformed patches. */ return 1; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bb7c6fa33418be04c91147ef92bc20042194421a..ff56a92bb1e6f6288431d950d2830805f28253a2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -75,4 +75,28 @@ if(ENABLE_CJSON_TEST) endforeach() add_dependencies(check ${unity_tests}) + + if (ENABLE_CJSON_UTILS) + #copy test files + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/json-patch-tests") + file(GLOB test_files "json-patch-tests/*") + file(COPY ${test_files} DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/json-patch-tests/") + + set (cjson_utils_tests + json_patch_tests) + + foreach (cjson_utils_test ${cjson_utils_tests}) + add_executable("${cjson_utils_test}" "${cjson_utils_test}.c") + target_link_libraries("${cjson_utils_test}" "${CJSON_LIB}" "${CJSON_UTILS_LIB}" unity test-common) + if(MEMORYCHECK_COMMAND) + add_test(NAME "${cjson_utils_test}" + COMMAND "${MEMORYCHECK_COMMAND}" ${MEMORYCHECK_COMMAND_OPTIONS} "${CMAKE_CURRENT_BINARY_DIR}/${cjson_utils_test}") + else() + add_test(NAME "${cjson_utils_test}" + COMMAND "./${cjson_utils_test}") + endif() + endforeach() + + add_dependencies(check ${cjson_utils_tests}) + endif() endif() diff --git a/tests/json-patch-tests/.editorconfig b/tests/json-patch-tests/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..dc79da48b226ddc33db083e848dfe9acf00ad4fe --- /dev/null +++ b/tests/json-patch-tests/.editorconfig @@ -0,0 +1,10 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space diff --git a/tests/json-patch-tests/.gitignore b/tests/json-patch-tests/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..cd2b23de6028052b067ea6fb8b46c1a523cd9caa --- /dev/null +++ b/tests/json-patch-tests/.gitignore @@ -0,0 +1,4 @@ +*~ +\#* +!.editorconfig +!.gitignore diff --git a/tests/json-patch-tests/.npmignore b/tests/json-patch-tests/.npmignore new file mode 100644 index 0000000000000000000000000000000000000000..2d6cdcb26eafb56260afe15bd38d8707808ac4d8 --- /dev/null +++ b/tests/json-patch-tests/.npmignore @@ -0,0 +1,2 @@ +.editorconfig +.gitignore diff --git a/tests/json-patch-tests/README.md b/tests/json-patch-tests/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fb9e44784b3e1431e9ce6e74f7b73017911744da --- /dev/null +++ b/tests/json-patch-tests/README.md @@ -0,0 +1,75 @@ +JSON Patch Tests +================ + +These are test cases for implementations of [IETF JSON Patch (RFC6902)](http://tools.ietf.org/html/rfc6902). + +Some implementations can be found at [jsonpatch.com](http://jsonpatch.com). + + +Test Format +----------- + +Each test file is a JSON document that contains an array of test records. A +test record is an object with the following members: + +- doc: The JSON document to test against +- patch: The patch(es) to apply +- expected: The expected resulting document, OR +- error: A string describing an expected error +- comment: A string describing the test +- disabled: True if the test should be skipped + +All fields except 'doc' and 'patch' are optional. Test records consisting only +of a comment are also OK. + + +Files +----- + +- tests.json: the main test file +- spec_tests.json: tests from the RFC6902 spec + + +Writing Tests +------------- + +All tests should have a descriptive comment. Tests should be as +simple as possible - just what's required to test a specific piece of +behavior. If you want to test interacting behaviors, create tests for +each behavior as well as the interaction. + +If an 'error' member is specified, the error text should describe the +error the implementation should raise - *not* what's being tested. +Implementation error strings will vary, but the suggested error should +be easily matched to the implementation error string. Try to avoid +creating error tests that might pass because an incorrect error was +reported. + +Please feel free to contribute! + + +Credits +------- + +The seed test set was adapted from Byron Ruth's +[jsonpatch-js](https://github.com/bruth/jsonpatch-js/blob/master/test.js) and +extended by [Mike McCabe](https://github.com/mikemccabe). + + +License +------- + + Copyright 2014 The Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/tests/json-patch-tests/package.json b/tests/json-patch-tests/package.json new file mode 100644 index 0000000000000000000000000000000000000000..256f9368e277356bb8fc6aead9af7817efed9903 --- /dev/null +++ b/tests/json-patch-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "json-patch-test-suite", + "version": "1.1.0", + "description": "JSON Patch RFC 6902 test suite", + "repository": "github:json-patch/json-patch-tests", + "homepage": "https://github.com/json-patch/json-patch-tests", + "bugs": "https://github.com/json-patch/json-patch-tests/issues", + "keywords": [ + "JSON", + "Patch", + "test", + "suite" + ], + "license": "Apache-2.0" +} diff --git a/tests/json-patch-tests/spec_tests.json b/tests/json-patch-tests/spec_tests.json new file mode 100644 index 0000000000000000000000000000000000000000..c160535b340f7c21388a463616ef0bfb4db5f9ca --- /dev/null +++ b/tests/json-patch-tests/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/tests.json b/tests/json-patch-tests/tests.json new file mode 100644 index 0000000000000000000000000000000000000000..3a42eab70f153d4af0258a97a8feb05b8adfab8f --- /dev/null +++ b/tests/json-patch-tests/tests.json @@ -0,0 +1,408 @@ +[ + { "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": "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": "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}], + "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}}], + "comment": "test should pass despite rearrangement" }, + + { "doc": {"foo": [{"foo": 1, "bar": 2}]}, + "patch": [{"op": "test", "path": "/foo", "value": [{"bar": 2, "foo": 1}]}], + "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]}}], + "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}] }, + + { "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}] }, + + { "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 '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 parameter to move", + "doc": { "foo": 1 }, + "patch": [ { "op": "move", "path": "" } ], + "error": "missing 'from' parameter" }, + + { "comment": "duplicate ops", + "doc": { "foo": "bar" }, + "patch": [ { "op": "add", "path": "/baz", "value": "qux", + "op": "move", "from":"/foo" } ], + "error": "patch has two 'op' members", + "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 nonexistent index", + "doc": ["foo", "bar"], + "patch": [{"op": "remove", "path": "/2"}], + "error": "removing a nonexistent index should fail" } + +] diff --git a/tests/json_patch_tests.c b/tests/json_patch_tests.c new file mode 100644 index 0000000000000000000000000000000000000000..a1c03ebbf4ecda514e6b80365c79e47c4804d3f9 --- /dev/null +++ b/tests/json_patch_tests.c @@ -0,0 +1,162 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#include +#include +#include + +#include "unity/examples/unity_config.h" +#include "unity/src/unity.h" +#include "common.h" +#include "../cJSON_Utils.h" + +static cJSON *parse_test_file(const char * const filename) +{ + char *file = NULL; + cJSON *json = NULL; + + file = read_file(filename); + TEST_ASSERT_NOT_NULL_MESSAGE(file, "Failed to read file."); + + json = cJSON_Parse(file); + TEST_ASSERT_NOT_NULL_MESSAGE(json, "Failed to parse test json."); + TEST_ASSERT_TRUE_MESSAGE(cJSON_IsArray(json), "Json is not an array."); + + free(file); + + return json; +} + +static cJSON_bool test_apply_patch(const cJSON * const test) +{ + cJSON *doc = NULL; + cJSON *patch = NULL; + cJSON *expected = NULL; + cJSON *error_element = NULL; + cJSON *comment = NULL; + cJSON *disabled = NULL; + + cJSON *object = NULL; + cJSON_bool successful = false; + + /* extract all the data out of the test */ + comment = cJSON_GetObjectItem(test, "comment"); + if (cJSON_IsString(comment)) + { + printf("Testing \"%s\"\n", comment->valuestring); + } + else + { + printf("Testing unkown\n"); + } + + disabled = cJSON_GetObjectItem(test, "disabled"); + if (cJSON_IsTrue(disabled)) + { + printf("SKIPPED\n"); + return true; + } + + doc = cJSON_GetObjectItem(test, "doc"); + TEST_ASSERT_NOT_NULL_MESSAGE(doc, "No \"doc\" in the test."); + patch = cJSON_GetObjectItem(test, "patch"); + TEST_ASSERT_NOT_NULL_MESSAGE(patch, "No \"patch\"in the test."); + /* Make a working copy of 'doc' */ + object = cJSON_Duplicate(doc, true); + TEST_ASSERT_NOT_NULL(object); + + expected = cJSON_GetObjectItem(test, "expected"); + error_element = cJSON_GetObjectItem(test, "error"); + if (error_element != NULL) + { + /* excepting an error */ + TEST_ASSERT_TRUE_MESSAGE(0 != cJSONUtils_ApplyPatches(object, patch), "Test didn't fail as it's supposed to."); + + successful = true; + } + else + { + /* apply the patch */ + TEST_ASSERT_EQUAL_INT_MESSAGE(0, cJSONUtils_ApplyPatches(object, patch), "Failed to apply patches."); + successful = true; + + if (expected != NULL) + { + successful = cJSON_Compare(object, expected, true); + } + } + + cJSON_Delete(object); + + if (successful) + { + printf("OK\n"); + } + else + { + printf("FAILED\n"); + } + + return successful; +} + +static void cjson_utils_should_pass_json_patch_test_tests(void) +{ + cJSON *tests = parse_test_file("json-patch-tests/tests.json"); + cJSON *test = NULL; + + cJSON_bool failed = false; + cJSON_ArrayForEach(test, tests) + { + failed |= !test_apply_patch(test); + } + + cJSON_Delete(tests); + + TEST_ASSERT_FALSE_MESSAGE(failed, "Some tests failed."); +} + +static void cjson_utils_should_pass_json_patch_test_spec_tests(void) +{ + cJSON *tests = parse_test_file("json-patch-tests/spec_tests.json"); + cJSON *test = NULL; + + cJSON_bool failed = false; + cJSON_ArrayForEach(test, tests) + { + failed |= !test_apply_patch(test); + } + + cJSON_Delete(tests); + + TEST_ASSERT_FALSE_MESSAGE(failed, "Some tests failed."); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(cjson_utils_should_pass_json_patch_test_tests); + RUN_TEST(cjson_utils_should_pass_json_patch_test_spec_tests); + + return UNITY_END(); +}