diff --git a/cJSON.c b/cJSON.c index 471cdf3..bc343ba 100644 --- a/cJSON.c +++ b/cJSON.c @@ -2484,3 +2484,104 @@ CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) return (item->type & 0xFF) == cJSON_Raw; } + +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF)) || cJSON_IsInvalid(a)) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (a->valuedouble == b->valuedouble) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + for (a_element = a->child, b_element = b->child; + (a_element != NULL) && (b_element != NULL); + a_element = a_element->next, b_element = b_element->next) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + cJSON *b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} diff --git a/cJSON.h b/cJSON.h index 7199520..c29ba17 100644 --- a/cJSON.h +++ b/cJSON.h @@ -212,6 +212,10 @@ CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); /* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will need to be released. With recurse!=0, it will duplicate any children connected to the item. The item->next and ->prev pointers are always zero on return from Duplicate. */ +/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); + /* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ /* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error. If not, then cJSON_GetErrorPtr() does the job. */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0cf17d3..bb7c6fa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -46,6 +46,7 @@ if(ENABLE_CJSON_TEST) print_value misc_tests parse_with_opts + compare_tests ) add_library(test-common common.c) diff --git a/tests/compare_tests.c b/tests/compare_tests.c new file mode 100644 index 0000000..654e14e --- /dev/null +++ b/tests/compare_tests.c @@ -0,0 +1,192 @@ +/* + 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 "unity/examples/unity_config.h" +#include "unity/src/unity.h" +#include "common.h" + +static cJSON_bool compare_from_string(const char * const a, const char * const b, const cJSON_bool case_sensitive) +{ + cJSON *a_json = NULL; + cJSON *b_json = NULL; + cJSON_bool result = false; + + a_json = cJSON_Parse(a); + TEST_ASSERT_NOT_NULL_MESSAGE(a_json, "Failed to parse a."); + b_json = cJSON_Parse(b); + TEST_ASSERT_NOT_NULL_MESSAGE(b_json, "Failed to parse b."); + + result = cJSON_Compare(a_json, b_json, case_sensitive); + + cJSON_Delete(a_json); + cJSON_Delete(b_json); + + return result; +} + +static void cjson_compare_should_compare_null_pointer_as_not_equal(void) +{ + TEST_ASSERT_FALSE(cJSON_Compare(NULL, NULL, true)); + TEST_ASSERT_FALSE(cJSON_Compare(NULL, NULL, false)); +} + +static void cjson_compare_should_compare_invalid_as_not_equal(void) +{ + cJSON invalid[1]; + memset(invalid, '\0', sizeof(invalid)); + + TEST_ASSERT_FALSE(cJSON_Compare(invalid, invalid, false)); + TEST_ASSERT_FALSE(cJSON_Compare(invalid, invalid, true)); +} + +static void cjson_compare_should_compare_numbers(void) +{ + TEST_ASSERT_TRUE(compare_from_string("1", "1", true)); + TEST_ASSERT_TRUE(compare_from_string("1", "1", false)); + TEST_ASSERT_TRUE(compare_from_string("0.0001", "0.0001", true)); + TEST_ASSERT_TRUE(compare_from_string("0.0001", "0.0001", false)); + + TEST_ASSERT_FALSE(compare_from_string("1", "2", true)); + TEST_ASSERT_FALSE(compare_from_string("1", "2", false)); +} + +static void cjson_compare_should_compare_booleans(void) +{ + /* true */ + TEST_ASSERT_TRUE(compare_from_string("true", "true", true)); + TEST_ASSERT_TRUE(compare_from_string("true", "true", false)); + + /* false */ + TEST_ASSERT_TRUE(compare_from_string("false", "false", true)); + TEST_ASSERT_TRUE(compare_from_string("false", "false", false)); + + /* mixed */ + TEST_ASSERT_FALSE(compare_from_string("true", "false", true)); + TEST_ASSERT_FALSE(compare_from_string("true", "false", false)); + TEST_ASSERT_FALSE(compare_from_string("false", "true", true)); + TEST_ASSERT_FALSE(compare_from_string("false", "true", false)); +} + +static void cjson_compare_should_compare_null(void) +{ + TEST_ASSERT_TRUE(compare_from_string("null", "null", true)); + TEST_ASSERT_TRUE(compare_from_string("null", "null", false)); + + TEST_ASSERT_FALSE(compare_from_string("null", "true", true)); + TEST_ASSERT_FALSE(compare_from_string("null", "true", false)); +} + +static void cjson_compare_should_not_accept_invalid_types(void) +{ + cJSON invalid[1]; + memset(invalid, '\0', sizeof(invalid)); + + invalid->type = cJSON_Number | cJSON_String; + + TEST_ASSERT_FALSE(cJSON_Compare(invalid, invalid, true)); + TEST_ASSERT_FALSE(cJSON_Compare(invalid, invalid, false)); +} + +static void cjson_compare_should_compare_strings(void) +{ + TEST_ASSERT_TRUE(compare_from_string("\"abcdefg\"", "\"abcdefg\"", true)); + TEST_ASSERT_TRUE(compare_from_string("\"abcdefg\"", "\"abcdefg\"", false)); + + TEST_ASSERT_FALSE(compare_from_string("\"ABCDEFG\"", "\"abcdefg\"", true)); + TEST_ASSERT_FALSE(compare_from_string("\"ABCDEFG\"", "\"abcdefg\"", false)); +} + +static void cjson_compare_should_compare_raw(void) +{ + cJSON *raw1 = NULL; + cJSON *raw2 = NULL; + + raw1 = cJSON_Parse("\"[true, false]\""); + TEST_ASSERT_NOT_NULL(raw1); + raw2 = cJSON_Parse("\"[true, false]\""); + TEST_ASSERT_NOT_NULL(raw2); + + raw1->type = cJSON_Raw; + raw2->type = cJSON_Raw; + + TEST_ASSERT_TRUE(cJSON_Compare(raw1, raw2, true)); + TEST_ASSERT_TRUE(cJSON_Compare(raw1, raw2, false)); + + cJSON_Delete(raw1); + cJSON_Delete(raw2); +} + +static void cjson_compare_should_compare_arrays(void) +{ + TEST_ASSERT_TRUE(compare_from_string("[]", "[]", true)); + TEST_ASSERT_TRUE(compare_from_string("[]", "[]", false)); + + TEST_ASSERT_TRUE(compare_from_string("[false,true,null,42,\"string\",[],{}]", "[false, true, null, 42, \"string\", [], {}]", true)); + TEST_ASSERT_TRUE(compare_from_string("[false,true,null,42,\"string\",[],{}]", "[false, true, null, 42, \"string\", [], {}]", false)); + + TEST_ASSERT_TRUE(compare_from_string("[[[1], 2]]", "[[[1], 2]]", true)); + TEST_ASSERT_TRUE(compare_from_string("[[[1], 2]]", "[[[1], 2]]", false)); + + TEST_ASSERT_FALSE(compare_from_string("[true,null,42,\"string\",[],{}]", "[false, true, null, 42, \"string\", [], {}]", true)); + TEST_ASSERT_FALSE(compare_from_string("[true,null,42,\"string\",[],{}]", "[false, true, null, 42, \"string\", [], {}]", false)); +} + +static void cjson_compare_should_compare_objects(void) +{ + TEST_ASSERT_TRUE(compare_from_string("{}", "{}", true)); + TEST_ASSERT_TRUE(compare_from_string("{}", "{}", false)); + + TEST_ASSERT_TRUE(compare_from_string( + "{\"false\": false, \"true\": true, \"null\": null, \"number\": 42, \"string\": \"string\", \"array\": [], \"object\": {}}", + "{\"true\": true, \"false\": false, \"null\": null, \"number\": 42, \"string\": \"string\", \"array\": [], \"object\": {}}", + true)); + TEST_ASSERT_FALSE(compare_from_string( + "{\"False\": false, \"true\": true, \"null\": null, \"number\": 42, \"string\": \"string\", \"array\": [], \"object\": {}}", + "{\"true\": true, \"false\": false, \"null\": null, \"number\": 42, \"string\": \"string\", \"array\": [], \"object\": {}}", + true)); + TEST_ASSERT_TRUE(compare_from_string( + "{\"False\": false, \"true\": true, \"null\": null, \"number\": 42, \"string\": \"string\", \"array\": [], \"object\": {}}", + "{\"true\": true, \"false\": false, \"null\": null, \"number\": 42, \"string\": \"string\", \"array\": [], \"object\": {}}", + false)); + TEST_ASSERT_FALSE(compare_from_string( + "{\"Flse\": false, \"true\": true, \"null\": null, \"number\": 42, \"string\": \"string\", \"array\": [], \"object\": {}}", + "{\"true\": true, \"false\": false, \"null\": null, \"number\": 42, \"string\": \"string\", \"array\": [], \"object\": {}}", + false)); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(cjson_compare_should_compare_null_pointer_as_not_equal); + RUN_TEST(cjson_compare_should_compare_invalid_as_not_equal); + RUN_TEST(cjson_compare_should_compare_numbers); + RUN_TEST(cjson_compare_should_compare_booleans); + RUN_TEST(cjson_compare_should_compare_null); + RUN_TEST(cjson_compare_should_not_accept_invalid_types); + RUN_TEST(cjson_compare_should_compare_strings); + RUN_TEST(cjson_compare_should_compare_raw); + RUN_TEST(cjson_compare_should_compare_arrays); + RUN_TEST(cjson_compare_should_compare_objects); + + return UNITY_END(); +}