json_object_array_move_cb() added one to the array length when "from"
and "path" are in the same array, so that the old length counted as a
valid target index. A move is a remove followed by an add (RFC 6902
section 4.4), and the add's index must not be greater than the number
of elements left after the remove. With the extra slot,
json_object_array_insert_idx() was called one past the end and padded
the array with a null:
doc: {"foo": [1, 2, 3]}
patch: [{"op": "move", "from": "/foo/0", "path": "/foo/3"}]
returned 0 with {"foo": [2, 3, null, 1]}. It now fails with EINVAL,
like an add to /foo/3 on a two-element array. Moving to /foo/2 or
/foo/- still puts the element last.
Since the token-boundary check for the parent/child test (1b504e2), an
empty "from" (the root) only counts as a prefix of "path" when "path"
starts with '/'. With an invalid "path" like "a", a move from "" went
on to remove the root: __json_patch_apply_remove() dropped a reference
to the document, the following set failed on the bad path, and the
error path dropped the last reference, so *base was freed while the
caller still owned it (use-after-free / double free in
json_object_put()).
The root is a parent of every other location, so an empty "from" now
always takes the parent/child branch: a no-op for "path": "" and
EINVAL otherwise. This also restores the invariant the comment below
relies on, that json_pointer can't find the root here.
Added move and copy cases to json_patch_tests.json and regenerated
test_json_patch.expected.
RFC 6902 section 4.6 compares JSON numbers by value. The test operation
used json_object_equal(), which treats json_type_int and json_type_double
as different types, so a test of 1.0 against 1 failed.
Use a private comparison for JSON Patch, including numbers nested inside
objects and arrays. Check floating-point range before converting, and
require both integer equality and an exact round trip to reject fractional
values and large integers that would round to the same double.
Add regression cases for both argument orders, nested values, signed and
unsigned limits, fractional values, precision loss and non-number types.
https://www.rfc-editor.org/rfc/rfc6902.txt section 4.6
json_patch_apply_move_copy() checks that "from" is not a proper prefix
of "path" per RFC 6902 section 4.4 (a location cannot be moved into one
of its own children), but it did so with a plain strncmp() over the raw
pointer strings, without checking that the shared run of characters
actually ends on a JSON Pointer reference-token boundary in "path".
As a result, moving or copying between two sibling object keys where
one name is a textual prefix of the other, e.g. "/aa" to "/aab", was
rejected with "Invalid attempt to move parent under a child" even
though "/aa" is not an ancestor pointer of "/aab" at all, just an
unrelated key that happens to share a few leading characters.
The fix requires that the character in "path" right after the shared
"from" prefix be either the end of the string (equal paths, a no-op)
or a '/' (a genuine nested reference token), before treating it as an
attempt to move a location under its own descendant.
Added two cases to json_patch_tests.json covering the move and copy
operations between such sibling keys, and refreshed the corresponding
golden test_json_patch.expected output.
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 <ardeleanalex@gmail.com>