json_pointer_get_single_path() returned ENOENT when the array element
at an in-range index was NULL, which is how json-c stores a JSON null.
A null object member was already found, but on [0, null, 2] "/1" failed
with "not found", although RFC 6901 says it references the null value.
The array index is bounds-checked just before, so a NULL element now
resolves like a null member: json_pointer_get() returns 0 and sets
*res to NULL. This also fixes json_patch_apply(), which failed every
"test", "replace" and "remove" on a null array element, and every
"move" and "copy" from one, with "Did not find element referenced by
path field".
Added a json_pointer_get() test, and json_patch_tests.json cases that
mirror the existing null object member ones.
json_pointer_get_internal() decodes the last reference token in a
private copy of the path, then pointed key_in_parent at the same offset
in the caller's path, where the token is still escaped. For a path like
"/a~1b" that gave "a~1b" instead of "a/b", so json_patch's remove,
and the remove step of move, called json_object_object_del() with a key
that doesn't exist. Nothing was removed and the patch still reported
success; a move left the source member in place.
Point key_in_parent at the key as it is stored in the parent object
instead, which is always the decoded name.
Added remove and move cases with escaped member names to
json_patch_tests.json and refreshed test_json_patch.expected.
In lld, a symbol version with undefined symbol is rejected by
default. In previous CMake, we use a wildcard for all symbol, which is
fine for empty program. Add --undefined-version to the detection to
fix it.
Discard the previous document's high surrogate when resetting the tokener.
Otherwise the next Unicode escape can add a replacement character or
combine with a surrogate from the abandoned document.
Extend the existing incremental parse tests for incomplete input and a
fatal error, using independent next-document string expectations.
Fixes#984.
Reported-by: GeekQAQ (GitHub)
Assisted-by: OpenAI Codex (AI)
Reviewed-by: GPT-6 Luna (AI)
json-c uses duplocale+newlocale+uselocale() to temporarily switch LC_NUMERIC
to "C" during JSON number parsing.
On AIX, every call to json_tokener_parse_ex permanently leaks memory.
This was confirmed with dbx malloc allocation.
0x20008af8 24 0 YORKTOWN
0x20008e18 5128 0 YORKTOWN
0x2000a458 776 0 YORKTOWN
0x2000a768 776 0 YORKTOWN
0x2000aa78 776 0 YORKTOWN
0x2000ad88 776 0 YORKTOWN
0x2000b098 776 0 YORKTOWN
0x2000b6b8 776 0 YORKTOWN
dbx malloc allocation showing new allocations surviving after each parse call with ~776
bytes left after invocations.
The root cause is that on AIX, locale category objects allocated by duplocale() are not
released back to the process heap, even when freelocale() is called correctly.
This causes unbounded heap growth in any long-running process that parses JSON repeatedly.
The fix it to replace the per-call duplocale+newlocale+reelocale() with a single
file-scope static locale_t created once on first call using
newlocale(LC_NUMERIC_MASK, "C", (locale_t)0) and reused for the lifetime of the library.
This reduces newlocale calls on all platforms.
__attribute__((destructor)) function json_tokener_locale_fini() frees the static locale
when the library is unloaded via dlclose() or process exit.
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.
Some test_json_patch test cases have neither a comment field and
nor a error field, so the expression
"comment ? comment : error_s" evaluates to NULL.
The printf statement
printf("Testing '%s', ...", comment ? comment : error_s, ...);
passes NULL as a %s argument. GNU libc handles this gracefully and prints (null).
On AIX, passing NULL for %s prints an empty string, causing the test output
to differ from the expected file.
The fix adds a simple macro:
define STR_OR_NULL(s) ((s) ? (s) : "(null)")
that retruns "(null)" when s is NULL.
still waiting for more data, and also after read() failures.
Finalize the tokener at EOF with the terminating NUL so valid scalar
values can complete, while rejecting genuinely truncated input.
Also propagate read errors to the caller and add CLI regression tests.
json_tokener.c assigned to an int64_t through a (uint64_t) cast. The
guarded range makes it harmless, but the cast names the wrong type.
array_list_expand_internal() reallocated when max == arr->size even though
the array can already hold max entries at that point, since callers only
index up to max - 1. Skip the realloc.
lseek() expects (fd, offset, whence). The test passed (fd, SEEK_SET, 0),
which does not conform to the function signature. It should be corrected
to (fd, 0, SEEK_SET).
Keep Amiga-specific files under amiga/, cache the pinned LHa binary in
CI, and make it explicit that the workflow artifact is not an official
release.
Co-authored-by: Cursor <cursoragent@cursor.com>