diff --git a/json_patch.c b/json_patch.c index 8fb7f08..904f609 100644 --- a/json_patch.c +++ b/json_patch.c @@ -15,6 +15,7 @@ #include "json_patch.h" #include "json_object_private.h" #include "json_pointer_private.h" +#include "linkhash.h" #include #ifndef SIZE_T_MAX @@ -48,6 +49,72 @@ * RFC 6902 - https://tools.ietf.org/html/rfc6902 */ +/* RFC 6902 compares numbers by value, including inside arrays and objects. + * json_object_equal() deliberately distinguishes integer and double objects. + */ +static int json_patch_equal(struct json_object *a, struct json_object *b) +{ + enum json_type type_a = json_object_get_type(a); + enum json_type type_b = json_object_get_type(b); + + if (a == b) + return 1; + + if ((type_a == json_type_int && type_b == json_type_double) || + (type_a == json_type_double && type_b == json_type_int)) + { + struct json_object *integer = type_a == json_type_int ? a : b; + struct json_object *floating = type_a == json_type_double ? a : b; + double d = json_object_get_double(floating); + int64_t i = json_object_get_int64(integer); + uint64_t u; + + /* Check the range before casting, then reject fractional values and + * integers that would lose precision when converted to double. + */ + if (i < 0) + return d >= (double)INT64_MIN && d < 0 && (int64_t)d == i && (double)i == d; + + u = json_object_get_uint64(integer); + /* As in json_object_get_uint64(), the rounded upper bound is exclusive. */ + return d >= 0 && d < (double)UINT64_MAX && (uint64_t)d == u && (double)u == d; + } + + if (type_a != type_b) + return 0; + + if (type_a == json_type_array) + { + size_t i, len = json_object_array_length(a); + if (len != json_object_array_length(b)) + return 0; + for (i = 0; i < len; i++) + { + if (!json_patch_equal(json_object_array_get_idx(a, i), + json_object_array_get_idx(b, i))) + return 0; + } + return 1; + } + + if (type_a == json_type_object) + { + struct json_object_iter iter; + struct json_object *value; + if (json_object_object_length(a) != json_object_object_length(b)) + return 0; + json_object_object_foreachC(a, iter) + { + if (!json_object_object_get_ex(b, iter.key, &value) || + !json_patch_equal(iter.val, value)) + return 0; + } + return 1; + } + + return json_object_equal(a, b); +} + static int json_patch_apply_test(struct json_object **res, struct json_object *patch_elem, const char *path, struct json_patch_error *patch_error) @@ -65,7 +132,7 @@ static int json_patch_apply_test(struct json_object **res, return -1; } - if (!json_object_equal(value1, value2)) { + if (!json_patch_equal(value1, value2)) { _set_err(ENOENT, "Value of element referenced by 'path' field did not match 'value' field"); return -1; } diff --git a/tests/json_patch_tests.json b/tests/json_patch_tests.json index eb964a8..e2d62d6 100644 --- a/tests/json_patch_tests.json +++ b/tests/json_patch_tests.json @@ -137,6 +137,81 @@ "patch": [{"op":"add", "path": "/3", "value": "bar"}], "error": "index is greater than number of items in array" }, + { "comment": "test integer against equal double", + "doc": {"n": 1}, + "patch": [{"op": "test", "path": "/n", "value": 1.0}], + "expected": {"n": 1} }, + + { "comment": "test double against equal integer", + "doc": {"n": 1.0}, + "patch": [{"op": "test", "path": "/n", "value": 1}], + "expected": {"n": 1.0} }, + + { "comment": "test numeric equality inside arrays and objects", + "doc": {"a": [1, {"n": -2.0}], "b": null}, + "patch": [{"op": "test", "path": "", "value": {"b": null, "a": [1.0, {"n": -2}]}}], + "expected": {"a": [1, {"n": -2.0}], "b": null} }, + + { "comment": "test integer against negative zero", + "doc": 0, + "patch": [{"op": "test", "path": "", "value": -0.0}], + "expected": 0 }, + + { "comment": "test unsigned integer against equal double", + "doc": 9223372036854775808, + "patch": [{"op": "test", "path": "", "value": 9223372036854775808.0}], + "expected": 9223372036854775808 }, + + { "comment": "test minimum signed integer against equal double", + "doc": -9223372036854775808, + "patch": [{"op": "test", "path": "", "value": -9223372036854775808.0}], + "expected": -9223372036854775808 }, + + { "comment": "test does not round integers to doubles", + "doc": 9007199254740993, + "patch": [{"op": "test", "path": "", "value": 9007199254740992.0}], + "error": "different integers must not compare equal after rounding" }, + + { "comment": "test does not round negative integers to doubles", + "doc": -9007199254740993, + "patch": [{"op": "test", "path": "", "value": -9007199254740992.0}], + "error": "different negative integers must not compare equal after rounding" }, + + { "comment": "test does not truncate fractional numbers", + "doc": 1, + "patch": [{"op": "test", "path": "", "value": 1.5}], + "error": "fractional number must not compare equal to an integer" }, + + { "comment": "test does not truncate negative fractional numbers", + "doc": -1, + "patch": [{"op": "test", "path": "", "value": -1.5}], + "error": "negative fractional number must not compare equal to an integer" }, + + { "comment": "test does not clamp out of range unsigned numbers", + "doc": 18446744073709551615, + "patch": [{"op": "test", "path": "", "value": 18446744073709551616.0}], + "error": "out of range double must not compare equal to UINT64_MAX" }, + + { "comment": "test does not clamp out of range negative numbers", + "doc": -9223372036854775808, + "patch": [{"op": "test", "path": "", "value": -18446744073709551616.0}], + "error": "out of range double must not compare equal to INT64_MIN" }, + + { "comment": "test does not coerce boolean values to numbers", + "doc": true, + "patch": [{"op": "test", "path": "", "value": 1.0}], + "error": "boolean is not a number" }, + + { "comment": "test does not coerce strings to numbers", + "doc": "1", + "patch": [{"op": "test", "path": "", "value": 1.0}], + "error": "string is not a number" }, + + { "comment": "test checks numeric equality recursively", + "doc": {"a": [1, {"n": -2}]}, + "patch": [{"op": "test", "path": "", "value": {"a": [1.0, {"n": -3.0}]}}], + "error": "nested numbers differ" }, + { "comment": "test against implementation-specific numeric parsing", "doc": {"1e0": "foo"}, "patch": [{"op": "test", "path": "/1e0", "value": "foo"}], diff --git a/tests/test_json_patch.expected b/tests/test_json_patch.expected index 041c014..c2b5db7 100644 --- a/tests/test_json_patch.expected +++ b/tests/test_json_patch.expected @@ -54,6 +54,30 @@ Testing '(null)', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/0", 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 integer against equal double', doc '{ "n": 1 }' patch '[ { "op": "test", "path": "\/n", "value": 1.0 } ]' : OK +Testing 'test double against equal integer', doc '{ "n": 1.0 }' patch '[ { "op": "test", "path": "\/n", "value": 1 } ]' : OK +Testing 'test numeric equality inside arrays and objects', doc '{ "a": [ 1, { "n": -2.0 } ], "b": null }' patch '[ { "op": "test", "path": "", "value": { "b": null, "a": [ 1.0, { "n": -2 } ] } } ]' : OK +Testing 'test integer against negative zero', doc '0' patch '[ { "op": "test", "path": "", "value": -0.0 } ]' : OK +Testing 'test unsigned integer against equal double', doc '9223372036854775808' patch '[ { "op": "test", "path": "", "value": 9223372036854775808.0 } ]' : OK +Testing 'test minimum signed integer against equal double', doc '-9223372036854775808' patch '[ { "op": "test", "path": "", "value": -9223372036854775808.0 } ]' : OK +Testing 'test does not round integers to doubles', doc '9007199254740993' patch '[ { "op": "test", "path": "", "value": 9007199254740992.0 } ]' : 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 'test does not round negative integers to doubles', doc '-9007199254740993' patch '[ { "op": "test", "path": "", "value": -9007199254740992.0 } ]' : 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 'test does not truncate fractional numbers', doc '1' patch '[ { "op": "test", "path": "", "value": 1.5 } ]' : 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 'test does not truncate negative fractional numbers', doc '-1' patch '[ { "op": "test", "path": "", "value": -1.5 } ]' : 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 'test does not clamp out of range unsigned numbers', doc '18446744073709551615' patch '[ { "op": "test", "path": "", "value": 18446744073709551616.0 } ]' : 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 'test does not clamp out of range negative numbers', doc '-9223372036854775808' patch '[ { "op": "test", "path": "", "value": -18446744073709551616.0 } ]' : 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 'test does not coerce boolean values to numbers', doc 'true' patch '[ { "op": "test", "path": "", "value": 1.0 } ]' : 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 'test does not coerce strings to numbers', doc '1' patch '[ { "op": "test", "path": "", "value": 1.0 } ]' : 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 'test checks numeric equality recursively', doc '{ "a": [ 1, { "n": -2 } ] }' patch '[ { "op": "test", "path": "", "value": { "a": [ 1.0, { "n": -3.0 } ] } } ]' : 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 '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