diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml new file mode 100644 index 0000000000..d85e8762cb --- /dev/null +++ b/.github/workflows/meson.yml @@ -0,0 +1,231 @@ +name: meson + +on: [push, pull_request] + +concurrency: + group: ${{github.workflow}}-${{github.head_ref}} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + Linux-GCC: + runs-on: ubuntu-22.04 + strategy: + matrix: + cc: ['9', '13'] + steps: + - uses: actions/checkout@v4 + - uses: egor-tensin/setup-gcc@v1 + with: + version: ${{matrix.cc}} + + - name: Install packages + run: | + python3 -m pip install meson ninja + + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + Linux-Clang: + runs-on: ubuntu-22.04 + strategy: + matrix: + cc: ['9', '22'] + steps: + - uses: actions/checkout@v4 + - uses: egor-tensin/setup-clang@v1 + + - name: Install packages + run: | + python3 -m pip install meson ninja + + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + VisualStudio: + runs-on: windows-latest + strategy: + matrix: + platform: ['x64', 'x86'] + steps: + - uses: actions/checkout@v4 + + - name: Install packages + run: | + python -m pip install meson ninja + + - uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{matrix.platform}} + + - name: Compile and Test + env: + CFLAGS: '/analyze' + run: | + meson setup "${{github.workspace}}/build" --wrap-mode=forcefallback -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + VisualStudio-clang-cl: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Install packages + run: | + python -m pip install meson ninja + + - uses: ilammy/msvc-dev-cmd@v1 + + - name: Compile and Test + env: + CC: clang-cl + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + MSYS2: + runs-on: windows-latest + strategy: + matrix: + platform: ['UCRT64', 'CLANG64'] + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v4 + + - uses: msys2/setup-msys2@v2 + with: + msystem: ${{matrix.platform}} + pacboy: >- + cc:p + cmake:p + meson:p + ninja:p + pkgconf:p + + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + Cygwin: + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v4 + - uses: msys2/setup-msys2@v2 + with: + msystem: 'MSYS' + install: >- + gcc + meson + ninja + - name: Compile and Test + run: | + meson setup build -Dwarning_level=3 -Dcpp_std=gnu++20 -Dbuildtype=debug + meson compile -C build --verbose + meson test -C build --verbose + MacOS: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Install packages + run: | + brew install meson + + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + DragonflyBSD: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: vmactions/dragonflybsd-vm@v1 + with: + prepare: | + pkg install -y ninja meson pkgconf + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + FreeBSD: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: vmactions/freebsd-vm@v1 + with: + prepare: | + pkg install -y ninja meson pkgconf + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + NetBSD: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: vmactions/netbsd-vm@v1 + with: + prepare: | + /usr/sbin/pkg_add meson pkgconf + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + OmniOS: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: vmactions/omnios-vm@v1 + with: + prepare: | + pkg install gcc14 meson-313 ninja pkg-config python-313 + run: | + /usr/lib/python3.13/bin/meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + /usr/lib/python3.13/bin/meson compile -C "${{github.workspace}}/build" --verbose + /usr/lib/python3.13/bin/meson test -C "${{github.workspace}}/build" --verbose + OpenBSD: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: vmactions/openbsd-vm@v1 + with: + prepare: | + pkg_add ninja meson pkgconf + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + Alpine: + runs-on: ubuntu-latest + strategy: + matrix: + platform: ['x86_64', 'x86', 'armhf', 'armv7', 'aarch64', 'ppc64le', 'riscv64', 's390x'] + defaults: + run: + shell: alpine.sh {0} + steps: + - uses: actions/checkout@v4 + - uses: jirutka/setup-alpine@v1 + with: + branch: edge + arch: ${{matrix.platform}} + packages: > + build-base pkgconf meson samurai + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 -Dbuildtype=debug + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose diff --git a/.travis.yml b/.travis.yml index 769bf4f18d..f006b09513 100644 --- a/.travis.yml +++ b/.travis.yml @@ -71,11 +71,6 @@ matrix: - cmake env: CHECK="true" -# osx - - os: osx - osx_image: xcode13.4 - env: XCODE="true" CHECK="true" - # run coveralls - os: linux dist: xenial @@ -109,9 +104,6 @@ matrix: - lcov -r build/all_coverage.info '/usr/*' '*CMakeFiles*' '*fuzz*' '*test*' -o build/coverage.info - coveralls-lcov --verbose build/coverage.info -# allow_failures: -# - os: osx - before_install: - echo $CC - echo $LANG @@ -125,8 +117,6 @@ before_install: fi before_script: - # XXX osx on travis doesn't work w/ set -e, so turn it off :( - - set +e - mkdir -p build || echo "Failed to mkdir build" - cd build || echo "Failed to cd build" - cmake .. || echo "Failed to run cmake" @@ -135,12 +125,8 @@ script: - make # when using bionic, Travis seems to ignore the "addons" section, so installing the packages with apt-get... - if [ -n "$CHECK" ]; then - if [ "$TRAVIS_OS_NAME" = "osx" ]; then - brew install doxygen; - else - if [ "$TRAVIS_DIST" = "bionic" ]; then - sudo apt-get install -y valgrind cppcheck doxygen; - fi; + if [ "$TRAVIS_DIST" = "bionic" ]; then + sudo apt-get install -y valgrind cppcheck doxygen; fi; make distcheck; if type cppcheck &> /dev/null ; then cppcheck --error-exitcode=1 --quiet *.h *.c tests/ ; fi; diff --git a/AUTHORS b/AUTHORS index 80ad6d6265..b66c19a36a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,5 +1,6 @@ Alan Coopersmith Alan Urmancheev <108410815+alurm@users.noreply.github.com> +Alb3e3 <74142887+Alb3e3@users.noreply.github.com> Alexander Dahl Alexandru Ardelean An7ar35 @@ -10,7 +11,7 @@ Awildidiot <69577313+0xfadead@users.noreply.github.com> Björn Esser BonsaY Bruno Haible -Cameron Armstrong +Cameron Armstrong (Nightfox) Careles changyong guo chenguoping @@ -24,6 +25,7 @@ David McCann DeX77 Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> dota17 +dxbjavid Eric Haszlakiewicz Eric Hawicz Evan Si @@ -40,6 +42,7 @@ hofnarr ihsinme <61293369+ihsinme@users.noreply.github.com> Ivan Romanov Jaap Keuter +JaisonZheng <1254186821@qq.com> Jakov Smolic janczer Ján Tomko @@ -56,6 +59,7 @@ Kizuna-Meraki Lakshmi-Surekha Leon Gross Liang, Gao +lone Luca Mannella Marc <34656315+MarcT512@users.noreply.github.com> Matthias Gatto @@ -63,6 +67,7 @@ max Micah Snyder Michael Clark Michele Locati +Milan Broz myd7349 Pascal Cuoq Pavel Zwerschke diff --git a/CMakeLists.txt b/CMakeLists.txt index 58ee9ede6d..d46a3dcc40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_minimum_required(VERSION 3.9...3.12) # JSON-C library is C only project. # PROJECT_VERSION{,_MAJOR,_MINOR,_PATCH} set by project(): -project(json-c LANGUAGES C VERSION 0.18.99) +project(json-c LANGUAGES C VERSION 0.19.99) # set default build type if not specified by user if(NOT CMAKE_BUILD_TYPE) @@ -267,6 +267,7 @@ if (HAVE_SYS_RANDOM_H) endif() if (HAVE_SYS_RESOURCE_H) check_symbol_exists(getrusage "sys/resource.h" HAVE_GETRUSAGE) + check_symbol_exists(setrlimit "sys/resource.h" HAVE_SETRLIMIT) endif() check_symbol_exists(strtoll "stdlib.h" HAVE_STRTOLL) @@ -543,7 +544,7 @@ add_library(${PROJECT_NAME} ${JSON_C_RESOURCES} ) set_target_properties(${PROJECT_NAME} PROPERTIES - VERSION 5.4.0 + VERSION 5.5.0 SOVERSION 5) list(APPEND CMAKE_TARGETS ${PROJECT_NAME}) # If json-c is used as subroject it set to target correct interface -I flags and allow diff --git a/ChangeLog b/ChangeLog index 6304eef7d3..a8cbbc48f3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,5 @@ -0.19 (up to commit 8948568, 2026-01-18) +0.19 (up to commit 3c1cf2b, 2026-06-27) ======================================== Deprecated and removed features: @@ -32,6 +32,14 @@ Significant changes and bug fixes * Fix bug involving supplemental code points that look like high surrogates * Fix runtime issue with test_util_file.c in Windows (add O_BINARY) * Fix macro WIN32_LEAN_AND_MEAN redefined +* Issue #914: Fix Memory usage regression due to newlocale() on macOS +* Issue #916: Fix OOM via large array index in json_pointer_set +* Issue #923: Avoid stack recursion in json_object_put() +* Issue #927: CVE-2026-9146 - update json_object_iterator documentation +* Issue #929: CVE-2026-11322 - fix information disclosure bug in apps/json_parse (not installed by default) +* Issue #930: fix locale-dependent strtod in json_object_get_double +* Issue #931: deep copy values in json_patch copy op to avoid aliasing and cycles + 0.18 (up to commit 6bfab90, 2024-09-15) ======================================== diff --git a/RELEASE_CHECKLIST.txt b/RELEASE_CHECKLIST.txt index a9df1b51cc..d69a5dff2f 100644 --- a/RELEASE_CHECKLIST.txt +++ b/RELEASE_CHECKLIST.txt @@ -186,4 +186,15 @@ Copy and paste this output into the wiki page at: ------------ +Create a release on GitHub + +* https://github.com/json-c/json-c/releases +* Click on "Draft a new release" +* Choose the appropriate tag +* Fill in title, refer to the ChangeLog in the release notes +* Attach the same tarballs as were uploaded to S3 +* When satisfied, click Publish release. + +------------ + Send an email to the mailing list. diff --git a/apps/json_parse.c b/apps/json_parse.c index 3f8ef00339..ebb0f257d9 100644 --- a/apps/json_parse.c +++ b/apps/json_parse.c @@ -39,6 +39,7 @@ static int formatted_output = JSON_C_TO_STRING_SPACED; static int show_output = 1; +static int show_diag = 1; static int strict_mode = 0; static int validate_utf8 = 0; static int tokener_flags = 0; @@ -56,6 +57,8 @@ static int showobj(struct json_object *new_obj); static void showmem(void) { + if (!show_diag) + return; #ifdef HAVE_GETRUSAGE struct rusage rusage; memset(&rusage, 0, sizeof(rusage)); @@ -111,12 +114,12 @@ static int parseit(int fd, int (*callback)(struct json_object *)) size_t parse_end = json_tokener_get_parse_end(tok); if (obj == NULL && jerr != json_tokener_continue) { - const char *aterr = (start_pos + parse_end < (int)sizeof(buf)) ? + const char *aterr = (start_pos + parse_end < retu) ? &buf[start_pos + parse_end] : ""; fflush(stdout); size_t fail_offset = total_read - retu + start_pos + parse_end; - fprintf(stderr, "Failed at offset %lu: %s %c\n", (unsigned long)fail_offset, - json_tokener_error_desc(jerr), aterr[0]); + fprintf(stderr, "Failed at offset %lu: %s: char=0x%02x\n", (unsigned long)fail_offset, + json_tokener_error_desc(jerr), (unsigned char)aterr[0]); json_tokener_free(tok); return 1; } @@ -171,10 +174,11 @@ static void usage(const char *argv0, int exitval, const char *errmsg) fp = stderr; if (errmsg != NULL) fprintf(fp, "ERROR: %s\n\n", errmsg); - fprintf(fp, "Usage: %s [-f|-F ] [-n] [-s] [-u] [filename]\n", argv0); + fprintf(fp, "Usage: %s [-f|-F ] [-n] [-s] [-u] [-N] [filename]\n", argv0); fprintf(fp, " -f - Format the output to stdout with JSON_C_TO_STRING_PRETTY (default is JSON_C_TO_STRING_SPACED)\n"); fprintf(fp, " -F - Format the output to stdout with , e.g. 0 for JSON_C_TO_STRING_PLAIN\n"); fprintf(fp, " -n - No output\n"); + fprintf(fp, " -N - Omit diagnostic information, such as memory usage\n"); fprintf(fp, " -c - Set JSON_C_TO_STRING_COLOR to colorize the output\n"); fprintf(fp, " -P - Initialize tokener flags to the given value\n"); fprintf(fp, " -s - Parse in strict mode, add flags:\n"); @@ -191,7 +195,7 @@ int main(int argc, char **argv) { int opt; - while ((opt = getopt(argc, argv, "cfF:hnP:su")) != -1) + while ((opt = getopt(argc, argv, "cfF:hnNP:su")) != -1) { switch (opt) { @@ -199,6 +203,7 @@ int main(int argc, char **argv) case 'f': formatted_output = JSON_C_TO_STRING_PRETTY; break; case 'F': formatted_output = atoi(optarg); break; case 'n': show_output = 0; break; + case 'N': show_diag = 0; break; case 'P': tokener_flags = atoi(optarg); break; case 's': strict_mode = 1; break; case 'u': validate_utf8 = 1; break; diff --git a/arraylist.c b/arraylist.c index bfc1425197..88c6d230bc 100644 --- a/arraylist.c +++ b/arraylist.c @@ -172,6 +172,14 @@ int array_list_put_idx(struct array_list *arr, size_t idx, void *data) return 0; } +int array_list_set_idx(struct array_list *arr, size_t idx, void *data) +{ + if (idx >= arr->length) + return -1; + arr->array[idx] = data; + return 0; +} + int array_list_add(struct array_list *arr, void *data) { /* Repeat some of array_list_put_idx() so we can skip several diff --git a/arraylist.h b/arraylist.h index a12f27f54f..84438ab55c 100644 --- a/arraylist.h +++ b/arraylist.h @@ -68,6 +68,12 @@ extern int array_list_put_idx(struct array_list *al, size_t i, void *data); extern int array_list_add(struct array_list *al, void *data); +/** + * Set the value at index i. Caller is responsible for freeing the previous value. + * To automatically free the existing value, use array_list_put_idx() instead. + */ +extern int array_list_set_idx(struct array_list *al, size_t i, void *data); + extern size_t array_list_length(struct array_list *al); extern void array_list_sort(struct array_list *arr, int (*compar)(const void *, const void *)); diff --git a/issues_closed_for_0.19.md b/issues_closed_for_0.19.md index d6a9345a08..3bb0c645e9 100644 --- a/issues_closed_for_0.19.md +++ b/issues_closed_for_0.19.md @@ -2,54 +2,13 @@ This list was created with: ``` PREV=2024-09-15 -NOW=2026-01-19 +NOW=2026-06-27 curl "https://api.github.com/search/issues?q=repo%3Ajson-c%2Fjson-c+closed%3A>${PREV}+created%3A<${NOW}&sort=created&order=asc&per_page=100&page=1" > issues1.out jq -r '.items[] | "[" + .title + "](" + .url + ")" | tostring' issues?.out > issues.md sed -e's,^\[ *\(.*\)\](https://api.github.com/.*/\([0-9].*\)),* [Issue #\2](https://github.com/json-c/json-c/issues/\2) - \1,' -i issues.md cat issues.md >> issues_closed_for_0.19.md ``` -* [Issue #826](https://github.com/json-c/json-c/issues/826) - CMake: remove needless policy setting -* [Issue #827](https://github.com/json-c/json-c/issues/827) - Resolve build issue in tokener_parse_ex_fuzzer.c -* [Issue #828](https://github.com/json-c/json-c/issues/828) - Put arc4random check under DISABLE_EXTRA_LIBS=OFF in CMakeLists.txt. -* [Issue #829](https://github.com/json-c/json-c/issues/829) - Cannot build with clang-cl -* [Issue #830](https://github.com/json-c/json-c/issues/830) - I am looking whether this CVE - CVE-2021-32292 is impacted by json-c version v0.13.1or not ? -* [Issue #831](https://github.com/json-c/json-c/issues/831) - rename WIN32 to _WIN32 -* [Issue #833](https://github.com/json-c/json-c/issues/833) - memory leak issue in 0.13 -* [Issue #834](https://github.com/json-c/json-c/issues/834) - fix error des of 'error_memory' -* [Issue #835](https://github.com/json-c/json-c/issues/835) - fix error des of 'error_memory' -* [Issue #836](https://github.com/json-c/json-c/issues/836) - Null pointer dereference in tokener_parse_ex_fuzzer.cc -* [Issue #837](https://github.com/json-c/json-c/issues/837) - Add type checking for parsed objects in tokener_parse_ex_fuzzer -* [Issue #838](https://github.com/json-c/json-c/issues/838) - Cannot link properly using cmake -* [Issue #839](https://github.com/json-c/json-c/issues/839) - json_pointer.c: initialize idx -* [Issue #840](https://github.com/json-c/json-c/issues/840) - Memory leak when enable HAVE_SETLOCALE and HAVE_USELOCALE -* [Issue #841](https://github.com/json-c/json-c/issues/841) - Random crash in json_tokener_parse -* [Issue #842](https://github.com/json-c/json-c/issues/842) - Assignment bug in json_pointer.c -* [Issue #843](https://github.com/json-c/json-c/issues/843) - _WIN32 should be used, not WIN32 in source code ifdefs. -* [Issue #844](https://github.com/json-c/json-c/issues/844) - Doxygen: mis-wording in `json_object_put` -* [Issue #845](https://github.com/json-c/json-c/issues/845) - [CMP0153](https://cmake.org/cmake/help/latest/policy/CMP0153.html) -* [Issue #846](https://github.com/json-c/json-c/issues/846) - CMP0153 -* [Issue #848](https://github.com/json-c/json-c/issues/848) - json_object_from_file caused calloc SIGSEGV -* [Issue #849](https://github.com/json-c/json-c/issues/849) - random_seed.c: add a Coverity Scan suppression -* [Issue #850](https://github.com/json-c/json-c/issues/850) - Missing return description -* [Issue #851](https://github.com/json-c/json-c/issues/851) - Allow NULL in json_object_new_string() and json_object_new_string_len() -* [Issue #852](https://github.com/json-c/json-c/issues/852) - docs: micro typo in json_object_put doc -* [Issue #853](https://github.com/json-c/json-c/issues/853) - json_tokener_parse_verbose does not set the error indicator when it fails to allocate the tokener -* [Issue #854](https://github.com/json-c/json-c/issues/854) - json_tokener_parse_verbose sets the error indicator to a wrong value when there is a memory allocation failure -* [Issue #856](https://github.com/json-c/json-c/issues/856) - Using libjson-c.so or compiling from source seg faults on json_tokener_parse_ex in alpine docker container -* [Issue #857](https://github.com/json-c/json-c/issues/857) - json_tokener_parse_verbose sets the error indicator to a wrong value when there is a memory allocation failure -* [Issue #858](https://github.com/json-c/json-c/issues/858) - json_tokener.h: json_tokener_parse_verbose: fix typo -* [Issue #859](https://github.com/json-c/json-c/issues/859) - Develop -* [Issue #860](https://github.com/json-c/json-c/issues/860) - Develop -* [Issue #861](https://github.com/json-c/json-c/issues/861) - Handle yet another out-of-memory condition. -* [Issue #862](https://github.com/json-c/json-c/issues/862) - Missing `json-c-targets.cmake` during build cmake build step -* [Issue #863](https://github.com/json-c/json-c/issues/863) - build ios simulator error -* [Issue #864](https://github.com/json-c/json-c/issues/864) - Build pkg-config for msvc as well -* [Issue #865](https://github.com/json-c/json-c/issues/865) - Assertion usage -* [Issue #866](https://github.com/json-c/json-c/issues/866) - Added github actions -* [Issue #868](https://github.com/json-c/json-c/issues/868) - I meet compile error in harmony -* [Issue #869](https://github.com/json-c/json-c/issues/869) - At a high level how are jsons parses? Are they terminated by the new line? -* [Issue #870](https://github.com/json-c/json-c/issues/870) - Non-Compliant features should be optional (and disabled) * [Issue #825](https://github.com/json-c/json-c/issues/825) - Add support for WASI * [Issue #847](https://github.com/json-c/json-c/issues/847) - Add support for Commodore Amiga * [Issue #867](https://github.com/json-c/json-c/issues/867) - Parsing of control characters @@ -66,6 +25,7 @@ cat issues.md >> issues_closed_for_0.19.md * [Issue #882](https://github.com/json-c/json-c/issues/882) - Explicitly handle NaN values when converting to integer * [Issue #883](https://github.com/json-c/json-c/issues/883) - Add fuzz tests for json_object/point/array apis * [Issue #884](https://github.com/json-c/json-c/issues/884) - fix macro WIN32_LEAN_AND_MEAN redefined +* [Issue #885](https://github.com/json-c/json-c/issues/885) - A core dump occurs when executing json_object_put. * [Issue #886](https://github.com/json-c/json-c/issues/886) - Linking to json-c using cmake's find_package error * [Issue #887](https://github.com/json-c/json-c/issues/887) - Can json-c accept invalid JSON strings? * [Issue #888](https://github.com/json-c/json-c/issues/888) - Allow build with cmake 4.0.0 @@ -79,6 +39,8 @@ cat issues.md >> issues_closed_for_0.19.md * [Issue #899](https://github.com/json-c/json-c/issues/899) - Meson support * [Issue #900](https://github.com/json-c/json-c/issues/900) - meson cleanups * [Issue #901](https://github.com/json-c/json-c/issues/901) - miscellanious gitignore updates +* [Issue #902](https://github.com/json-c/json-c/issues/902) - json_object_get_int() doesn't support INT32_MIN or INT32_MAX. +* [Issue #903](https://github.com/json-c/json-c/issues/903) - meson: use override_dependency * [Issue #904](https://github.com/json-c/json-c/issues/904) - Include getopt.h header file if the platform is not AIX. * [Issue #905](https://github.com/json-c/json-c/issues/905) - Fix typo * [Issue #906](https://github.com/json-c/json-c/issues/906) - json-c: fix calloc argument order to avoid GCC14 -Wcalloc-transposed-args in arraylist.c and json_object.c @@ -89,3 +51,19 @@ cat issues.md >> issues_closed_for_0.19.md * [Issue #911](https://github.com/json-c/json-c/issues/911) - Building libjson-c DLL for Windows under Cygwin: why it's named cygjson-c? * [Issue #912](https://github.com/json-c/json-c/issues/912) - libjson-c.dll is missing VERSIONINFO * [Issue #913](https://github.com/json-c/json-c/issues/913) - Add VERSIONINFO to libjson-c.dll +* [Issue #914](https://github.com/json-c/json-c/issues/914) - Memory usage regression due to newlocale() on macOS +* [Issue #915](https://github.com/json-c/json-c/issues/915) - Fix memory leak on macOS by auto-enabling NEWLOCALE_NEEDS_FREELOCALE +* [Issue #916](https://github.com/json-c/json-c/issues/916) - OOM via large array index in json_pointer_set +* [Issue #917](https://github.com/json-c/json-c/issues/917) - Fix: OOM vulnerability cause by is_valid_index +* [Issue #918](https://github.com/json-c/json-c/issues/918) - Assertion failed has occurred in function json_object_object_add_ex at json_object.c +* [Issue #919](https://github.com/json-c/json-c/issues/919) - Compiling json-c in Windows for a non x86 target fails +* [Issue #920](https://github.com/json-c/json-c/issues/920) - Broken link at wiki/List-of-json-c-tutorials +* [Issue #922](https://github.com/json-c/json-c/issues/922) - ninja install target does not install all headers needed +* [Issue #923](https://github.com/json-c/json-c/issues/923) - json-c seems to be stack recursive, which can cause stack overflows +* [Issue #924](https://github.com/json-c/json-c/issues/924) - avoid recursion during json_object_put +* [Issue #925](https://github.com/json-c/json-c/issues/925) - Fix build with clang-21 +* [Issue #927](https://github.com/json-c/json-c/issues/927) - Coordinated Vulnerability Disclosure - json-c 0.18.99 Use-After-Free via json_object_iterator Concurrent Modification +* [Issue #928](https://github.com/json-c/json-c/issues/928) - Build error from current top of tree: d1018cf +* [Issue #929](https://github.com/json-c/json-c/issues/929) - Coordinated Vulnerability Disclosure - json-c 0.18.99 +* [Issue #930](https://github.com/json-c/json-c/issues/930) - fix locale-dependent strtod in json_object_get_double +* [Issue #931](https://github.com/json-c/json-c/issues/931) - deep copy values in json_patch copy op to avoid aliasing and cycles diff --git a/json-c.sym b/json-c.sym index 47b76e4efd..30fb3b61f2 100644 --- a/json-c.sym +++ b/json-c.sym @@ -17,6 +17,7 @@ JSONC_PRIVATE { array_list_free; array_list_new; array_list_put_idx; + array_list_set_idx; array_list_sort; json_hex_chars; json_parse_double; @@ -24,6 +25,7 @@ JSONC_PRIVATE { json_parse_uint64; lh_table_delete; lh_table_delete_entry; + lh_table_delete_entry_to_tail; lh_table_free; lh_table_insert; lh_table_insert_w_hash; @@ -182,5 +184,12 @@ JSONC_0.18 { } JSONC_0.17; JSONC_0.19 { -# global: + global: + json_pointer_set_with_limit_index; + json_pointer_set_with_cb; + json_object_array_put_with_idx_limit_cb; } JSONC_0.18; + +JSONC_0.20 { +# global: +} JSONC_0.19; diff --git a/json_c_version.h b/json_c_version.h index 2bfb6a6342..801ca78237 100644 --- a/json_c_version.h +++ b/json_c_version.h @@ -17,11 +17,11 @@ extern "C" { #endif #define JSON_C_MAJOR_VERSION 0 -#define JSON_C_MINOR_VERSION 18 +#define JSON_C_MINOR_VERSION 19 #define JSON_C_MICRO_VERSION 99 #define JSON_C_VERSION_NUM \ ((JSON_C_MAJOR_VERSION << 16) | (JSON_C_MINOR_VERSION << 8) | JSON_C_MICRO_VERSION) -#define JSON_C_VERSION "0.18.99" +#define JSON_C_VERSION "0.19.99" #ifndef JSON_EXPORT #if defined(_MSC_VER) && defined(JSON_C_DLL) diff --git a/json_object.c b/json_object.c index d67a61a4b1..13d3826d97 100644 --- a/json_object.c +++ b/json_object.c @@ -21,6 +21,9 @@ #include #include #include +#ifdef HAVE_LOCALE_H +#include +#endif /* HAVE_LOCALE_H */ #include "arraylist.h" #include "debug.h" @@ -269,11 +272,33 @@ struct json_object *json_object_get(struct json_object *jso) return jso; } -int json_object_put(struct json_object *jso) -{ - if (!jso) - return 0; +/** + * Return values for _json_object_put_maybe_free(). + * json_object_put_still_refd and json_object_put_freed match the documented + * return values of json_object_put(), so they can be returned directly. + */ +enum json_object_put_result +{ + json_object_put_still_refd = 0, /* refcount decremented, object not freed */ + json_object_put_freed = 1, /* refcount reached zero, memory released */ + json_object_put_container = 2 /* refcount reached zero, but the object is a + non-empty container: the caller must free + the contents, then the object itself */ +}; + +/** + * Internal json_object_put function + * Returns json_object_put_still_refd if a reference remains, and the object + * was not freed. + * Returns json_object_put_freed if the object's memory was released, either + * because it holds no other objects, or because free_containers was set. + * Returns json_object_put_container when the refcount reached zero but the + * object is a non-empty container; the caller must free the contents, then + * call this function again with free_containers set to free the object itself. + */ +static inline enum json_object_put_result _json_object_put_maybe_free(struct json_object *jso, int free_containers) +{ /* Avoid invalid free and crash explicitly instead of (silently) * segfaulting. */ @@ -287,21 +312,169 @@ int json_object_put(struct json_object *jso) * operating on an already-freed object. */ if (__sync_sub_and_fetch(&jso->_ref_count, 1) > 0) - return 0; #else if (--jso->_ref_count > 0) - return 0; #endif + { + return json_object_put_still_refd; + } if (jso->_user_delete) jso->_user_delete(jso, jso->_userdata); + jso->_user_delete = NULL; + jso->_userdata = NULL; // aka _delete_parent, but json_object_put() will + // have already grabbed it if it needs it. + switch (jso->o_type) { - case json_type_object: json_object_object_delete(jso); break; - case json_type_array: json_object_array_delete(jso); break; - case json_type_string: json_object_string_delete(jso); break; - default: json_object_generic_delete(jso); break; + case json_type_object: + if (free_containers || lh_table_length(JC_OBJECT(jso)->c_object) == 0) + { + json_object_object_delete(jso); + break; + } + return json_object_put_container; + case json_type_array: + // container objects are handled by the caller + if (free_containers || array_list_length(JC_ARRAY(jso)->c_array) == 0) + { + json_object_array_delete(jso); + break; + } + return json_object_put_container; + case json_type_string: + json_object_string_delete(jso); + break; + default: + json_object_generic_delete(jso); + break; } + return json_object_put_freed; +} + +int json_object_put(struct json_object *jso) +{ + enum json_object_put_result rc; + + if (!jso) + return 0; + + rc = _json_object_put_maybe_free(jso, 0); + if (rc != json_object_put_container) + return (int)rc; + // else, it's a non-empty container object, handle it below + + // Note: jso is now a "zombie" object, _ref_count == 0 but memory not yet released + + /* + * Handle container objects with minimal stack usage. + * Perform depth-first iteration, decrementing ref counts on way down + * and freeing actual memory on the way up. + * Iterate backwards through each container so we can use the tail + * pointer/array length to know where to pick up upon popping up to + * the parent. + */ + + while(jso != NULL) + { + size_t total_slots; + size_t slots_left; + struct lh_entry *cur_entry = NULL; + int retry_main_loop = 0; + + if (jso->o_type == json_type_object) + { + total_slots = lh_table_length(JC_OBJECT(jso)->c_object); + cur_entry = JC_OBJECT(jso)->c_object->tail; + } + else + { + total_slots = array_list_length(JC_ARRAY(jso)->c_array); + } + slots_left = total_slots; + + while (slots_left > 0) + { + size_t cur_slot = slots_left - 1; + json_object *child = NULL; + + // First, clear the slot in the current jso object + // The slot itself will be freed when jso is freed, or + // if the child object in the slot is a container too and + // and we "recurse" into it. + switch (jso->o_type) + { + case json_type_object: + child = (json_object *)lh_entry_v(cur_entry); + // We're going to free child, so detach it from the entry + lh_entry_set_val(cur_entry, NULL); + break; + case json_type_array: + child = (struct json_object *)array_list_get_idx(JC_ARRAY(jso)->c_array, cur_slot); + // We're going to free child, so detach it from the entry + array_list_set_idx(JC_ARRAY(jso)->c_array, cur_slot, NULL); + break; + default: + assert(!"jso->o_type is not object or array"); + break; + } + + // Now, handle actually freeing the json_object in that slot + if (!child || _json_object_put_maybe_free(child, 0) != json_object_put_container) + { + // child is either freed, or still referenced somewhere else + // leave it as-is and handle the previous slot + slots_left--; + if (jso->o_type == json_type_object) + cur_entry = cur_entry->prev; + continue; + } + // _ref_count == 0 now, and _user_delete has been called so we can re-use _userdata + child->_delete_parent = jso; // aka _userdata + child->_user_delete = NULL; // make sure it's not called again + + // Clear the slot entries whose json_object have been freed so when we pop + // back up to this jso we can continue where we left off. + // Note: since we set each entry to NULL above, clearing the slot + // is a noop wrt releasing a json_object. + if (jso->o_type == json_type_object) + { + lh_table_delete_entry_to_tail(JC_OBJECT(jso)->c_object, cur_entry); + } + else // json_type_array + { + array_list_del_idx(JC_ARRAY(jso)->c_array, cur_slot, total_slots - cur_slot); + } + // Iterate down through the child, it will be freed once all + // of *its* children are freed + jso = child; + retry_main_loop = 1; + break; + } + + if (retry_main_loop) + // Iterating down, don't free jso yet + continue; + + // All slots are cleared, now pop back up to the parent + { + // jso is a child that's already been detached from its parent + // so we need to actually free it now + // Be sure to grab _delete_parent *before* freeing jso. + json_object *parent = jso->_delete_parent; + enum json_object_put_result rc; + assert(jso->_ref_count == 0); + jso->_ref_count++; // We're the exclusive owner of jso, non-atomic add is ok. + // Note: the call must not be inside assert(), or it gets + // compiled out when NDEBUG is defined and the memory leaks. + rc = _json_object_put_maybe_free(jso, 1); + assert(rc == json_object_put_freed); + (void)rc; + jso = parent; + // iteration will be reset at the top of the loop + } + } + return 1; } @@ -516,9 +689,11 @@ static int json_object_object_to_json_string(struct json_object *jso, struct pri static void json_object_lh_entry_free(struct lh_entry *ent) { + struct json_object *jso = (struct json_object *)lh_entry_v(ent); if (!lh_entry_k_is_constant(ent)) free(lh_entry_k(ent)); - json_object_put((struct json_object *)lh_entry_v(ent)); + if (jso) // micro-opt, skip func call on null object + json_object_put(jso); } static void json_object_object_delete(struct json_object *jso_base) @@ -842,9 +1017,10 @@ int64_t json_object_get_int64(const struct json_object *jso) } } case json_type_double: - // INT64_MAX can't be exactly represented as a double - // so cast to tell the compiler it's ok to round up. - if (JC_DOUBLE_C(jso)->c_double > (double)INT64_MAX) + // INT64_MAX can't be exactly represented as a double, so it + // rounds up to (double)(INT64_MAX+1). Use >= so that value is + // rejected rather than cast to int64_t, which would be UB. + if (JC_DOUBLE_C(jso)->c_double >= (double)INT64_MAX) { errno = ERANGE; return INT64_MAX; @@ -895,9 +1071,10 @@ uint64_t json_object_get_uint64(const struct json_object *jso) } } case json_type_double: - // UINT64_MAX can't be exactly represented as a double - // so cast to tell the compiler it's ok to round up. - if (JC_DOUBLE_C(jso)->c_double > (double)UINT64_MAX) + // UINT64_MAX can't be exactly represented as a double, so it + // rounds up to (double)(UINT64_MAX+1). Use >= so that value is + // rejected rather than cast to uint64_t, which would be UB. + if (JC_DOUBLE_C(jso)->c_double >= (double)UINT64_MAX) { errno = ERANGE; return UINT64_MAX; @@ -968,14 +1145,14 @@ int json_object_int_inc(struct json_object *jso, int64_t val) { jsoint->cint.c_uint64 = UINT64_MAX; } - else if (val < 0 && jsoint->cint.c_uint64 < (uint64_t)(-val)) + else if (val < 0 && jsoint->cint.c_uint64 < (0 - (uint64_t)val)) { jsoint->cint.c_int64 = (int64_t)jsoint->cint.c_uint64 + val; jsoint->cint_type = json_object_int_type_int64; } - else if (val < 0 && jsoint->cint.c_uint64 >= (uint64_t)(-val)) + else if (val < 0 && jsoint->cint.c_uint64 >= (0 - (uint64_t)val)) { - jsoint->cint.c_uint64 -= (uint64_t)(-val); + jsoint->cint.c_uint64 -= (0 - (uint64_t)val); } else { @@ -1239,12 +1416,42 @@ double json_object_get_double(const struct json_object *jso) } case json_type_boolean: return JC_BOOL_C(jso)->c_boolean; case json_type_string: + { + const char *cstr = get_string_component(jso); + const char *parse = cstr; + char *radixconv = NULL; +#ifdef HAVE_LOCALE_H + /* A json_type_string holds the value with a '.' radix, as it + * appears in JSON, but strtod() honours the current locale. In a + * locale whose decimal point is not '.' the '.' is treated as a + * stray character, so e.g. "19.95" is read as 0. Parse a copy + * with the radix translated so the value comes back unchanged. */ + const char *decimal_point = localeconv()->decimal_point; + if (decimal_point && decimal_point[0] != '.' && decimal_point[1] == '\0') + { + const char *dot = strchr(cstr, '.'); + if (dot) + { + size_t slen = strlen(cstr); + radixconv = (char *)malloc(slen + 1); + if (radixconv == NULL) + { + errno = ENOMEM; + return 0.0; + } + memcpy(radixconv, cstr, slen + 1); + radixconv[dot - cstr] = decimal_point[0]; + parse = radixconv; + } + } +#endif errno = 0; - cdouble = strtod(get_string_component(jso), &errPtr); + cdouble = strtod(parse, &errPtr); /* if conversion stopped at the first character, return 0.0 */ - if (errPtr == get_string_component(jso)) + if (errPtr == parse) { + free(radixconv); errno = EINVAL; return 0.0; } @@ -1256,6 +1463,7 @@ double json_object_get_double(const struct json_object *jso) */ if (*errPtr != '\0') { + free(radixconv); errno = EINVAL; return 0.0; } @@ -1273,7 +1481,9 @@ double json_object_get_double(const struct json_object *jso) */ if ((HUGE_VAL == cdouble || -HUGE_VAL == cdouble) && (ERANGE == errno)) cdouble = 0.0; + free(radixconv); return cdouble; + } default: errno = EINVAL; return 0.0; } } @@ -1497,7 +1707,9 @@ static int json_object_array_to_json_string(struct json_object *jso, struct prin static void json_object_array_entry_free(void *data) { - json_object_put((struct json_object *)data); + struct json_object *jso = (struct json_object *)data; + if (jso) // micro-opt, skip func call on null object + json_object_put(jso); } static void json_object_array_delete(struct json_object *jso) diff --git a/json_object.h b/json_object.h index e21138ecf1..2cd94906e7 100644 --- a/json_object.h +++ b/json_object.h @@ -259,7 +259,7 @@ JSON_EXPORT void *json_object_get_userdata(json_object *jso); * The user_delete parameter is optional and may be passed as NULL, even if * the userdata parameter is non-NULL. It will be called just before the * json_object is deleted, after it's reference count goes to zero - * (see json_object_put()). + * (see json_object_put()) but before any child objects are freed. * If this is not provided, it is up to the caller to free the userdata at * an appropriate time. (i.e. after the json_object is deleted) * @@ -293,7 +293,7 @@ JSON_EXPORT void json_object_set_userdata(json_object *jso, void *userdata, * The user_delete parameter is optional and may be passed as NULL, even if * the userdata parameter is non-NULL. It will be called just before the * json_object is deleted, after it's reference count goes to zero - * (see json_object_put()). + * (see json_object_put()) but before any child objects are freed. * If this is not provided, it is up to the caller to free the userdata at * an appropriate time. (i.e. after the json_object is deleted) * @@ -488,7 +488,7 @@ JSON_EXPORT void json_object_object_del(struct json_object *obj, const char *key * @param val the local name for the json_object* object variable defined in * the body */ -#if defined(__GNUC__) && !defined(__STRICT_ANSI__) && (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) +#if defined(__GNUC__) && !defined(__STRICT_ANSI__) && (defined(__cplusplus) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L)) #define json_object_object_foreach(obj, key, val) \ char *key = NULL; \ @@ -520,7 +520,7 @@ JSON_EXPORT void json_object_object_del(struct json_object *obj, const char *key : 0); \ entry##key = entry_next##key) -#endif /* defined(__GNUC__) && !defined(__STRICT_ANSI__) && (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) */ +#endif /** Iterate through all keys and values of an object (ANSI C Safe) * @param obj the json_object instance diff --git a/json_object_iterator.h b/json_object_iterator.h index a9b1433c74..d05f572a29 100644 --- a/json_object_iterator.h +++ b/json_object_iterator.h @@ -3,6 +3,7 @@ * @file json_object_iterator.h * * Copyright (c) 2009-2012 Hewlett-Packard Development Company, L.P. +* Copyright (c) 2026 Eric Hawicz * * This library is free software; you can redistribute it and/or modify * it under the terms of the MIT license. See COPYING for details. @@ -30,11 +31,6 @@ extern "C" { #endif -/** - * Forward declaration for the opaque iterator information. - */ -struct json_object_iter_info_; - /** * The opaque iterator that references a name/value pair within * a JSON Object instance or the "end" iterator value. @@ -42,6 +38,12 @@ struct json_object_iter_info_; struct json_object_iterator { const void *opaque_; + // These could be used to make an interator that fails "faster" + // in the face of certain changes to the underlying object, but + // would still be vulnerable to skipping entries. + // Changing this structure size is an ABI change + // const lh_table *table; + // size_t begin_size; }; /** @@ -73,10 +75,20 @@ struct json_object; */ JSON_EXPORT struct json_object_iterator json_object_iter_init_default(void); -/** Retrieves an iterator to the first pair of the JSON Object. - * - * @warning Any modification of the underlying pair invalidates all - * iterators to that pair. +/** Retrieves an iterator to the first key/value pair of the JSON Object. + * + * @warning Any modification of the fields present on the object + * invalidates all iterators on that object, regardless of + * what key/value pair they are pointing to, other than the + * end iterator. + * Modifications include adding or removing any pair, even if + * set of keys appears unchanged after the modification. + * Modifying *values* is allowed, including replacing a value + * by calling json_object_add(), provided the key already exists. + * Attempting to use an invalidated iterator in any way, other + * than comparing it to the end iterator, is *undefined behavior*. + * Passing such an iterator to any of the the json_object_iterator + * functions is not even guaranteed to fail in any consistent way. * * @param obj JSON Object instance (MUST be of type json_object) * @@ -138,8 +150,9 @@ JSON_EXPORT struct json_object_iterator json_object_iter_end(const struct json_o /** Returns an iterator to the next pair, if any * - * @warning Any modification of the underlying pair - * invalidates all iterators to that pair. + * @warning Any modification of the fields of the object invalidates + * the iterator, see the warning on json_object_iter_begin() + * for details. * * @param iter [IN/OUT] Pointer to iterator that references a * name/value pair; MUST be a valid, non-end iterator. @@ -159,8 +172,9 @@ JSON_EXPORT void json_object_iter_next(struct json_object_iterator *iter); * @param iter pointer to iterator that references a name/value * pair; MUST be a valid, non-end iterator. * - * @warning bad things will happen if an invalid or - * "end" iterator is passed. + * @warning bad things will happen if an invalid or "end" iterator + * is passed. See the warning on json_object_iter_begin() + * for details. * * @return const char* Pointer to the name of the referenced * name/value pair. The name memory belongs to the @@ -177,8 +191,9 @@ JSON_EXPORT const char *json_object_iter_peek_name(const struct json_object_iter * @param iter pointer to iterator that references a name/value * pair; MUST be a valid, non-end iterator. * - * @warning bad things will happen if invalid or - * "end" iterator is passed. + * @warning bad things will happen if an invalid or "end" iterator + * is passed. See the warning on json_object_iter_begin() + * for details. * * @return struct json_object* Pointer to the json-c value * instance of the referenced name/value pair; the diff --git a/json_object_private.h b/json_object_private.h index f81af881f6..b96c74ba81 100644 --- a/json_object_private.h +++ b/json_object_private.h @@ -47,7 +47,10 @@ struct json_object json_object_to_json_string_fn *_to_json_string; struct printbuf *_pb; json_object_delete_fn *_user_delete; - void *_userdata; + union { + void *_userdata; + void *_delete_parent; // Used during json_object_put + }; // Actually longer, always malloc'd as some more-specific type. // The rest of a struct json_object_${o_type} follows }; diff --git a/json_patch.c b/json_patch.c index 5bbc308a99..90e9d19e23 100644 --- a/json_patch.c +++ b/json_patch.c @@ -108,8 +108,9 @@ static int json_patch_apply_remove(struct json_object **res, const char *path, s return rc; } -// callback for json_pointer_set_with_array_cb() -static int json_object_array_insert_idx_cb(struct json_object *parent, size_t idx, +// callback for json_pointer_set_with_cb() +// key is ignored because this callback is only for arrays +static int json_object_array_insert_idx_cb(struct json_object *parent, const char *key, size_t idx, struct json_object *value, void *priv) { int rc; @@ -117,7 +118,7 @@ static int json_object_array_insert_idx_cb(struct json_object *parent, size_t id if (idx > json_object_array_length(parent)) { - // Note: will propagate back out through json_pointer_set_with_array_cb() + // Note: will propagate back out through json_pointer_set_with_cb() errno = EINVAL; return -1; } @@ -148,8 +149,8 @@ static int json_patch_apply_add_replace(struct json_object **res, return -1; } - rc = json_pointer_set_with_array_cb(res, path, json_object_get(value), - json_object_array_insert_idx_cb, &add); + rc = json_pointer_set_with_cb(res, path, json_object_get(value), + json_object_array_insert_idx_cb, 0, &add); if (rc) { _set_err(errno, "Failed to set value at path referenced by 'path' field"); @@ -159,8 +160,9 @@ static int json_patch_apply_add_replace(struct json_object **res, return rc; } -// callback for json_pointer_set_with_array_cb() -static int json_object_array_move_cb(struct json_object *parent, size_t idx, +// callback for json_pointer_set_with_cb() +// key is ignored because this callback is only for arrays +static int json_object_array_move_cb(struct json_object *parent, const char *key, size_t idx, struct json_object *value, void *priv) { int rc; @@ -178,7 +180,7 @@ static int json_object_array_move_cb(struct json_object *parent, size_t idx, if (idx > len) { - // Note: will propagate back out through json_pointer_set_with_array_cb() + // Note: will propagate back out through json_pointer_set_with_cb() errno = EINVAL; return -1; } @@ -193,9 +195,10 @@ static int json_patch_apply_move_copy(struct json_object **res, struct json_object *patch_elem, const char *path, int move, struct json_patch_error *patch_error) { - json_pointer_array_set_cb array_set_cb; + json_pointer_set_cb array_set_cb; struct json_pointer_get_result from; struct json_object *jfrom; + struct json_object *value; const char *from_s; size_t from_s_len; int rc; @@ -205,7 +208,11 @@ static int json_patch_apply_move_copy(struct json_object **res, return -1; } - from_s = json_object_get_string(jfrom); + from_s = json_object_get_type(jfrom) == json_type_string ? json_object_get_string(jfrom) : NULL; + if (from_s == NULL) { + _set_err(EINVAL, "Patch object 'from' field is not a string"); + return -1; + } from_s_len = strlen(from_s); if (strncmp(from_s, path, from_s_len) == 0) { @@ -231,24 +238,39 @@ static int json_patch_apply_move_copy(struct json_object **res, // Note: it's impossible for json_pointer to find the root obj, due // to the path check above, so from.parent is guaranteed non-NULL - json_object_get(from.obj); if (!move) { + /* RFC 6902 section 4.5: "copy" duplicates the value, it does + * not share it. Sharing the source via a reference (as a plain + * json_object_get() would) lets a later op insert the value + * beneath itself through a different pointer path, which the + * from/path string check above can't detect because both paths + * resolve to the same object, producing a reference cycle that + * loops forever on serialisation and breaks teardown. An + * independent deep copy can never alias an existing node. */ + value = NULL; + if (from.obj != NULL && + json_object_deep_copy(from.obj, &value, NULL) < 0) { + _set_err(ENOMEM, "Unable to copy value referenced by 'from' field"); + return -1; + } array_set_cb = json_object_array_insert_idx_cb; } else { + json_object_get(from.obj); + value = from.obj; rc = __json_patch_apply_remove(&from); if (rc < 0) { - json_object_put(from.obj); + json_object_put(value); return rc; } array_set_cb = json_object_array_move_cb; } - rc = json_pointer_set_with_array_cb(res, path, from.obj, array_set_cb, &from); + rc = json_pointer_set_with_cb(res, path, value, array_set_cb, 0, &from); if (rc) { _set_err(errno, "Failed to set value at path referenced by 'path' field"); - json_object_put(from.obj); + json_object_put(value); } return rc; @@ -301,12 +323,22 @@ int json_patch_apply(struct json_object *copy_from, struct json_object *patch, _set_err(EINVAL, "Patch object does not contain 'op' field"); return -1; } - op = json_object_get_string(jop); + op = json_object_get_type(jop) == json_type_string ? json_object_get_string(jop) : NULL; + if (op == NULL) { + _set_err(EINVAL, "Patch object 'op' field is not a string"); + return -1; + } if (!json_object_object_get_ex(patch_elem, "path", &jpath)) { _set_err(EINVAL, "Patch object does not contain 'path' field"); return -1; } - path = json_object_get_string(jpath); // Note: empty string is ok! + // Note: empty string is ok! + path = json_object_get_type(jpath) == json_type_string ? json_object_get_string(jpath) + : NULL; + if (path == NULL) { + _set_err(EINVAL, "Patch object 'path' field is not a string"); + return -1; + } if (!strcmp(op, "test")) rc = json_patch_apply_test(base, patch_elem, path, patch_error); diff --git a/json_pointer.c b/json_pointer.c index 6e5609d700..9d7ad185f1 100644 --- a/json_pointer.c +++ b/json_pointer.c @@ -46,6 +46,12 @@ static void string_replace_all_occurrences_with_char(char *s, const char *occur, static int is_valid_index(const char *path, size_t *idx) { size_t i, len = strlen(path); + /* an empty reference token is never a valid array index */ + if (len == 0) + { + errno = EINVAL; + return 0; + } /* this code-path optimizes a bit, for when we reference the 0-9 index range * in a JSON array and because leading zeros not allowed */ @@ -120,15 +126,9 @@ static int json_pointer_get_single_path(struct json_object *obj, char *path, return 0; } -static int json_object_array_put_idx_cb(struct json_object *parent, size_t idx, - struct json_object *value, void *priv) -{ - return json_object_array_put_idx(parent, idx, value); -} - static int json_pointer_set_single_path(struct json_object *parent, const char *path, struct json_object *value, - json_pointer_array_set_cb array_set_cb, void *priv) + json_pointer_set_cb set_cb, int cb_handles_obj, void *priv) { if (json_object_is_type(parent, json_type_array)) { @@ -138,14 +138,23 @@ static int json_pointer_set_single_path(struct json_object *parent, const char * return json_object_array_add(parent, value); if (!is_valid_index(path, &idx)) return -1; - return array_set_cb(parent, idx, value, priv); + return set_cb(parent, NULL, idx, value, priv); } /* path replacements should have been done in json_pointer_get_single_path(), * and we should still be good here */ if (json_object_is_type(parent, json_type_object)) - return json_object_object_add(parent, path, value); + { + if (cb_handles_obj) + { + return set_cb(parent, path, (size_t)-1, value, priv); + } + else + { + return json_object_object_add(parent, path, value); + } + } /* Getting here means that we tried to "dereference" a primitive JSON type * (like string, int, bool).i.e. add a sub-object to it @@ -298,9 +307,9 @@ int json_pointer_getf(struct json_object *obj, struct json_object **res, const c return rc; } -int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, +int json_pointer_set_with_cb(struct json_object **obj, const char *path, struct json_object *value, - json_pointer_array_set_cb array_set_cb, void *priv) + json_pointer_set_cb set_cb, int cb_handles_obj, void *priv) { const char *endp; char *path_copy = NULL; @@ -330,7 +339,7 @@ int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, if ((endp = strrchr(path, '/')) == path) { path++; - return json_pointer_set_single_path(*obj, path, value, array_set_cb, priv); + return json_pointer_set_single_path(*obj, path, value, set_cb, cb_handles_obj, priv); } /* pass a working copy to the recursive call */ @@ -347,12 +356,21 @@ int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, return rc; endp++; - return json_pointer_set_single_path(set, endp, value, array_set_cb, priv); + return json_pointer_set_single_path(set, endp, value, set_cb, cb_handles_obj, priv); +} + +static int default_put_cb(struct json_object *parent, const char *key, size_t idx, + struct json_object *value, void *priv) +{ + if (key == NULL) + return json_object_array_put_idx(parent, idx, value); + else + return json_object_object_add(parent, key, value); } int json_pointer_set(struct json_object **obj, const char *path, struct json_object *value) { - return json_pointer_set_with_array_cb(obj, path, value, json_object_array_put_idx_cb, NULL); + return json_pointer_set_with_cb(obj, path, value, default_put_cb, 1, NULL); } int json_pointer_setf(struct json_object **obj, struct json_object *value, const char *path_fmt, @@ -407,9 +425,57 @@ int json_pointer_setf(struct json_object **obj, struct json_object *value, const set_single_path: endp++; - rc = json_pointer_set_single_path(set, endp, value, - json_object_array_put_idx_cb, NULL); + rc = json_pointer_set_single_path(set, endp, value, default_put_cb, 1, NULL); out: free(path_copy); return rc; } + +int json_pointer_set_with_limit_index(struct json_object **obj, const char *path, + struct json_object *value, size_t limit_index) +{ + // -1 means no limits + if (limit_index == (size_t)-1) + { + return json_pointer_set_with_cb(obj, path, value, default_put_cb, 1, NULL); + } + return json_pointer_set_with_cb(obj, path, value, + json_object_array_put_with_idx_limit_cb, 0, &limit_index); +} + +/* safe callback for array index limit */ +int json_object_array_put_with_idx_limit_cb(struct json_object *jso, const char *key, size_t idx, + struct json_object *jso_new, void *priv) +{ + size_t max_idx; + + if (key == NULL) + { + // array operation + // use priv as a size_t pointer to pass in the maximum allowed index. + // The priv is required context for this callback and must not be NULL. + if (!priv) + { + errno = EFAULT; + return -1; + } + + max_idx = *(size_t*)priv; + + // Check against a maximum to prevent excessive memory allocations. + // An extremely large index, even if it doesn't overflow size_t, + // will cause a huge memory allocation request via realloc, + // leading to an OOM. + if (idx > max_idx) + { + errno = EINVAL; + return -1; + } + return json_object_array_put_idx(jso, idx, jso_new); + } + else + { + // object operation + return json_object_object_add(jso, key, jso_new); + } +} diff --git a/json_pointer.h b/json_pointer.h index dfe1185474..49c8579ab6 100644 --- a/json_pointer.h +++ b/json_pointer.h @@ -81,6 +81,10 @@ JSON_EXPORT int json_pointer_getf(struct json_object *obj, struct json_object ** * That also implies that 'json_pointer_set()' does not do any refcount incrementing. * (Just that single decrement that was mentioned above). * + * @warning This function is vulnerable to an OOM. + * To prevent this, use the safer variant 'json_pointer_set_with_limit_index()' + * or the flexible 'json_pointer_set_with_array_cb()' with a custom callback. + * * @param obj the json_object instance/tree to which to add a sub-object * @param path a (RFC6901) string notation for the sub-object to set in the tree * @param value object to set at path @@ -110,6 +114,73 @@ JSON_EXPORT int json_pointer_set(struct json_object **obj, const char *path, JSON_EXPORT int json_pointer_setf(struct json_object **obj, struct json_object *value, const char *path_fmt, ...); +/** + * A convenient and safe variant of 'json_pointer_set()' that prevents excessive memory allocations + * by enforcing a limit on array indices. + * + * @param obj the json_object instance/tree to which to add a sub-object + * @param path a (RFC6901) string notation for the sub-object to set in the tree + * @param value object to set at path + * @param limit_index The maximum allowed value for an array index. If a path + * contains an index larger than this, the function will fail + * with errno set to EINVAL. A value of -1 can be used to specify + * no limit, reverting to the original behavior + * + * @return negative if an error (or not found), or 0 if succeeded + */ +JSON_EXPORT int json_pointer_set_with_limit_index(struct json_object **obj, const char *path, + struct json_object *value, size_t limit_index); + +/** + * Callback function type. + * + * When setting an array element, 'key' will be NULL and 'idx' will be the + * target index. + * When setting an object field, 'key' will be the target key and 'idx' will + * be -1. + */ +typedef int(*json_pointer_set_cb)(json_object *parent, const char *key, size_t idx, + json_object *value, void *priv); + +/** + * Variant of 'json_pointer_set()' that allows specifying a custom callback + * + * @param obj the json_object instance/tree to which to add a sub-object + * @param path a (RFC6901) string notation for the sub-object to set in the tree + * @param value object to set at path + * @param set_cb A custom callback function to handle setting the element + * @param cb_handles_obj If 0, the callback is only invoked for array modifications. + * If 1, the callback is invoked for both array and object + * modifications. + * @param priv A private pointer passed through to the set_cb callback, + * for user-defined context + * + * @return negative if an error (or not found), or 0 if succeeded + */ +JSON_EXPORT int json_pointer_set_with_cb(struct json_object **obj, const char *path, + struct json_object *value, + json_pointer_set_cb set_cb, int cb_handles_obj, void *priv); + +/** + * A safer callback for 'json_pointer_set_with_cb()' that enforces a + * maximum array index. + * + * This function can be used as the 'set_cb' argument to prevent OOM. + * It expects the 'priv' argument to be a valid pointer to a 'size_t' variable + * that holds the maximum allowed index. + * + * @param jso the parent json_object array. + * @param key the object field where the element is to be placed, should be NULL here. + * @param idx the index where the element is to be placed. + * @param jso_new the new json_object to place at the index. + * @param priv A pointer to a 'size_t' variable specifying the maximum index. + * This pointer must not be NULL. + * + * @return 0 on success, or a negative value if idx exceeds the limit or 'priv' is NULL. + */ +JSON_EXPORT int json_object_array_put_with_idx_limit_cb(struct json_object *jso, const char *key, size_t idx, + struct json_object *jso_new, void *priv); + #ifdef __cplusplus } #endif diff --git a/json_pointer_private.h b/json_pointer_private.h index 537cabd36e..e7ddf017c3 100644 --- a/json_pointer_private.h +++ b/json_pointer_private.h @@ -29,12 +29,19 @@ struct json_pointer_get_result { int json_pointer_get_internal(struct json_object *obj, const char *path, struct json_pointer_get_result *res); -typedef int(*json_pointer_array_set_cb)(json_object *parent, size_t idx, - json_object *value, void *priv); - -int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, +// replaced by json_pointer_set_cb +// typedef int(*json_pointer_array_set_cb)(json_object *parent, size_t idx, +// json_object *value, void *priv); +typedef int(*json_pointer_set_cb)(json_object *parent, const char *key, size_t idx, + json_object *value, void *priv); + +// replaced by json_pointer_set_with_cb +// int json_pointer_set_with_array_cb(struct json_object **obj, const char *path, +// struct json_object *value, +// json_pointer_array_set_cb array_set_cb, void *priv); +int json_pointer_set_with_cb(struct json_object **obj, const char *path, struct json_object *value, - json_pointer_array_set_cb array_set_cb, void *priv); + json_pointer_set_cb set_cb, int cb_handles_obj, void *priv); #ifdef __cplusplus } diff --git a/json_tokener.c b/json_tokener.c index 8412eaeabb..b723f139b2 100644 --- a/json_tokener.c +++ b/json_tokener.c @@ -46,7 +46,7 @@ #define jt_hexdigit(x) (((x) <= '9') ? (x) - '0' : ((x)&7) + 9) -#if !HAVE_STRNCASECMP && defined(_MSC_VER) +#if !HAVE_STRNCASECMP && defined(_WIN32) /* MSC has the version as _strnicmp */ #define strncasecmp _strnicmp #elif !HAVE_STRNCASECMP @@ -1043,6 +1043,22 @@ struct json_object *json_tokener_parse_ex(struct json_tokener *tok, const char * tok->st_pos = 0; goto redo_char; } + if (tok->flags & JSON_TOKENER_STRICT) + { + /* RFC 8259 forbids leading zeros in the integer part: + * a '0' may only be followed by '.', 'e'/'E' or the end + * of the number, so "01", "00" and "-0123" are invalid + * while "0", "-0" and "0.5" remain valid. + */ + const char *num = tok->pb->buf; + if (*num == '-') + num++; + if (num[0] == '0' && num[1] >= '0' && num[1] <= '9') + { + tok->err = json_tokener_error_parse_number; + goto out; + } + } if (tok->is_double && !(tok->flags & JSON_TOKENER_STRICT)) { /* Trim some chars off the end, to allow things @@ -1087,12 +1103,6 @@ struct json_object *json_tokener_parse_ex(struct json_tokener *tok, const char * tok->err = json_tokener_error_parse_number; goto out; } - if (numuint64 && tok->pb->buf[0] == '0' && - (tok->flags & JSON_TOKENER_STRICT)) - { - tok->err = json_tokener_error_parse_number; - goto out; - } if (numuint64 <= INT64_MAX) { num64 = (uint64_t)numuint64; diff --git a/linkhash.c b/linkhash.c index 58e1313091..04d1ff5d4b 100644 --- a/linkhash.c +++ b/linkhash.c @@ -668,6 +668,8 @@ int lh_table_delete_entry(struct lh_table *t, struct lh_entry *e) /* CAW: fixed to be 64bit nice, still need the crazy negative case... */ ptrdiff_t n = (ptrdiff_t)(e - t->table); + assert(n >= 0 && n < t->size); + /* CAW: this is bad, really bad, maybe stack goes other direction on this machine... */ if (n < 0) { @@ -704,6 +706,22 @@ int lh_table_delete_entry(struct lh_table *t, struct lh_entry *e) return 0; } +int lh_table_delete_entry_to_tail(struct lh_table *t, struct lh_entry *first_entry) +{ + struct lh_entry *del_entry = t->tail; + do + { + struct lh_entry *prev = del_entry->prev; + // Could probably micro-optimize this, but better to avoid code duplication for now + if (lh_table_delete_entry(t, del_entry) != 0) + return -1; + if (del_entry == first_entry) + break; + del_entry = prev; + } while (del_entry != NULL); + return 0; +} + int lh_table_delete(struct lh_table *t, const void *k) { struct lh_entry *e = lh_table_lookup_entry(t, k); diff --git a/linkhash.h b/linkhash.h index 65c4909683..48fb7c0b0a 100644 --- a/linkhash.h +++ b/linkhash.h @@ -303,6 +303,17 @@ extern json_bool lh_table_lookup_ex(struct lh_table *t, const void *k, void **v) */ extern int lh_table_delete_entry(struct lh_table *t, struct lh_entry *e); +/** + * Delete all entries from the specified one to the tail of the list. + * Same as calling lh_table_delete_entry() on each of them. + * + * @param t the table to delete from. + * @param e a pointer to the first entry to delete. + * @return 0 if the item was deleted. + * @return -1 if it was not found. + */ +extern int lh_table_delete_entry_to_tail(struct lh_table *t, struct lh_entry *e); + /** * Delete a record from the table. * @@ -334,7 +345,7 @@ int lh_table_resize(struct lh_table *t, int new_size); /** * @deprecated Don't use this outside of linkhash.h: */ -#if !defined (__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) +#if !defined(__cplusplus) && (!defined (__STDC_VERSION__) || (__STDC_VERSION__ < 199901L)) /* C89 compilers like VS2010 can't handle inline funcs, so skip it there, note: this also applies to -std=c89 in GCC! */ #define _LH_INLINE diff --git a/meson.build b/meson.build index b0709efd18..ebc9e462dc 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,7 @@ project('json-c', 'c', version: '0.18.99', + meson_version: '>=0.54.0', + license: 'MIT', default_options: ['buildtype=release', 'warning_level=2']) cc = meson.get_compiler('c') @@ -72,14 +74,16 @@ funcs = [ 'vsyslog', 'getrandom', 'getrusage', - 'strcasecmp', - 'strncasecmp', 'strtoll', 'strtoull', 'arc4random', 'vasprintf', ] +if conf_data.has('HAVE_STRINGS_H') + funcs += ['strcasecmp', 'strncasecmp'] +endif + foreach f : funcs if cc.has_function(f) conf_data.set('HAVE_@0@'.format(f.to_upper()), 1, description : 'Define to 1 if you have the `@0@` function.'.format(f)) @@ -123,16 +127,12 @@ foreach d, h : decls endif endforeach -check_thread = cc.compiles(''' - __thread int x = 0; - int main() { return x; } - ''', - name: 'Check for __thread support') +check_thread = cc.compiles('__thread int x;', name: 'Check for __thread support') if check_thread conf_data.set('HAVE___THREAD', 1) conf_data.set('SPEC___THREAD', '__thread') -elif cc.get_id().contains('msvc') +elif cc.get_argument_syntax() == 'msvc' conf_data.set('SPEC___THREAD', '__declspec(thread)') endif @@ -211,7 +211,6 @@ json_header = configure_file( configuration: jhconf_data ) - # Platform-specific flags add_project_arguments('-D_GNU_SOURCE', language: 'c') @@ -228,6 +227,12 @@ endif add_project_arguments(cc.get_supported_arguments('-Wno-unused-parameter'), language : 'c') +sym = cc.get_supported_link_arguments( + '-Wl,--version-script,@0@/json-c.sym'.format( + meson.current_source_dir(), + ), +) + # Source files sources = files( 'arraylist.c', 'debug.c', 'json_c_version.c', 'json_object.c', @@ -252,18 +257,45 @@ libjson = library('json-c', include_directories: inc, dependencies: bsd_dep, install: true, + link_args: sym, version: '5.4.0', soversion: '5', ) jsonc_dep = declare_dependency(link_with: libjson, include_directories: inc) +meson.override_dependency('json-c', jsonc_dep) -# Install headers -install_headers( +# Install headers into json-c/ subdirectory (matches CMake layout) +installed_headers = [ 'arraylist.h', 'debug.h', 'json_c_version.h', 'json_inttypes.h', 'json_object.h', 'json_object_iterator.h', 'json_tokener.h', 'json_types.h', 'json_util.h', 'json_visit.h', 'linkhash.h', 'printbuf.h', json_configure_header, json_header +] + +if not get_option('disable_json_pointer') + installed_headers += ['json_pointer.h'] + if not get_option('disable_json_patch') + installed_headers += ['json_patch.h'] + endif +endif + +install_headers(installed_headers, subdir: 'json-c') + +# pkg-config file +configure_file( + input: 'json-c.pc.in', + output: 'json-c.pc', + install: true, + install_dir: get_option('libdir') / 'pkgconfig', + configuration: { + 'prefix': get_option('prefix'), + 'exec_prefix': get_option('prefix'), + 'libdir': get_option('libdir'), + 'includedir': get_option('includedir'), + 'VERSION': meson.project_version(), + 'LIBS': '', + } ) # Optional apps @@ -274,4 +306,4 @@ endif # Optional tests if get_option('buildtype') == 'debug' subdir('tests') -endif +endif diff --git a/random_seed.c b/random_seed.c index a93c8b979a..0ea57e0678 100644 --- a/random_seed.c +++ b/random_seed.c @@ -46,7 +46,7 @@ static void do_cpuid(int regs[], int h) /* clang-format on */ } -#elif defined _MSC_VER +#elif defined _MSC_VER && (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64)) #define HAS_X86_CPUID 1 #define do_cpuid __cpuid @@ -129,7 +129,7 @@ static int get_rdrand_seed(void) #endif -#if defined _MSC_VER +#if defined _MSC_VER && (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64)) #if _MSC_VER >= 1700 #define HAVE_RDRAND 1 diff --git a/strerror_override.c b/strerror_override.c index a3dd377a3d..dda4a5ba32 100644 --- a/strerror_override.c +++ b/strerror_override.c @@ -69,6 +69,7 @@ char *_json_c_strerror(int errno_in) int start_idx; char digbuf[20]; int ii, jj; + unsigned int uerr; if (!_json_c_strerror_enable) _json_c_strerror_enable = (getenv("_JSON_C_STRERROR_ENABLE") == NULL) ? -1 : 1; @@ -94,14 +95,22 @@ char *_json_c_strerror(int errno_in) } // It's not one of the known errno values, return the numeric value. - for (ii = 0; errno_in >= 10; errno_in /= 10, ii++) + // Work on the magnitude in unsigned space: a negative errno_in makes + // errno_in % 10 negative, which would index before the "0123456789" + // literal (an out-of-bounds read). Computing 0u - errno_in in unsigned + // is also well defined for INT_MIN, where -errno_in would overflow. + uerr = (errno_in < 0) ? (0u - (unsigned int)errno_in) : (unsigned int)errno_in; + for (ii = 0; uerr >= 10; uerr /= 10, ii++) { - digbuf[ii] = "0123456789"[(errno_in % 10)]; + digbuf[ii] = "0123456789"[(uerr % 10)]; } - digbuf[ii] = "0123456789"[(errno_in % 10)]; + digbuf[ii] = "0123456789"[(uerr % 10)]; - // Reverse the digits - for (start_idx = sizeof(PREFIX) - 1; ii >= 0; ii--, start_idx++) + // Reverse the digits, keeping the sign for negative values + start_idx = sizeof(PREFIX) - 1; + if (errno_in < 0) + errno_buf[start_idx++] = '-'; + for (; ii >= 0; ii--, start_idx++) { errno_buf[start_idx] = digbuf[ii]; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 345f93da2a..5d6217131d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,6 +21,7 @@ set(ALL_TEST_NAMES test_charcase test_compare test_deep_copy + test_deep_nesting test_double_serializer test_float test_int_add @@ -37,13 +38,22 @@ set(ALL_TEST_NAMES test_visit test_object_iterator) +set(NOBUILD_TEST_NAMES + test_json_parse_cli +) + if (NOT DISABLE_JSON_POINTER) set(ALL_TEST_NAMES ${ALL_TEST_NAMES} test_json_pointer) + set(ALL_TEST_NAMES ${ALL_TEST_NAMES} test_safe_json_pointer_set) if (NOT DISABLE_JSON_PATCH) set(ALL_TEST_NAMES ${ALL_TEST_NAMES} test_json_patch) endif() endif() +foreach(TESTNAME ${NOBUILD_TEST_NAMES}) +add_test(NAME ${TESTNAME} COMMAND ${PROJECT_SOURCE_DIR}/tests/${TESTNAME}.test) +endforeach(TESTNAME) + foreach(TESTNAME ${ALL_TEST_NAMES}) add_executable(${TESTNAME} ${TESTNAME}.c) diff --git a/tests/json_patch_tests.json b/tests/json_patch_tests.json index 8cafb93c96..80103c0835 100644 --- a/tests/json_patch_tests.json +++ b/tests/json_patch_tests.json @@ -535,6 +535,31 @@ "doc": {"foo":"bar"}, "patch": [{"op": "add", "path": "/FOO", "value": "BAR"}], "expected": {"foo": "bar", "FOO": "BAR"} + }, + + { "comment": "copy must duplicate the value, not alias it, so no cycle can form", + "doc": {"a": {"p": {}}}, + "patch": [{"op": "copy", "from": "/a", "path": "/b"}, + {"op": "copy", "from": "/b", "path": "/a/p/x"}], + "expected": {"a": {"p": {"x": {"p": {}}}}, "b": {"p": {}}} + }, + + { "comment": "null op field must be rejected, not dereferenced", + "doc": {"foo": "bar"}, + "patch": [{"op": null, "path": "/foo"}], + "error": "a null op field should fail rather than crash" + }, + + { "comment": "null from field must be rejected, not dereferenced", + "doc": {"foo": "bar"}, + "patch": [{"op": "move", "from": null, "path": "/foo"}], + "error": "a null from field should fail rather than crash" + }, + + { "comment": "null path field must be rejected, not dereferenced", + "doc": {"foo": "bar"}, + "patch": [{"op": "remove", "path": null}], + "error": "a null path field should fail rather than crash" } ] diff --git a/tests/meson.build b/tests/meson.build index 9580e4b46d..be8b084280 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,5 +1,16 @@ -test_includes = include_directories('.') -test_deps = [jsonc_dep] +#This hack is needed for Windows tests +test_lib = static_library( + 'jsonctest', + objects: libjson.extract_all_objects( + recursive: false, + ), + install: false, +) + +test_deps = declare_dependency( + include_directories: '..', + link_with: test_lib, +) # List of test sources and expected output files test_cases = [ @@ -27,6 +38,7 @@ test_cases = [ ['test_visit', 'test_visit.expected'], ['test_object_iterator', 'test_object_iterator.expected'], ['test_json_pointer', 'test_json_pointer.expected'], + ['test_safe_json_pointer_set', 'test_safe_json_pointer_set.expected'], ['test_json_patch', 'test_json_patch.expected'], ] @@ -53,7 +65,6 @@ foreach t : test_cases name = t[0] expected = t[1] exe = executable(name, name + '.c', - include_directories: test_includes, dependencies: test_deps ) @@ -62,4 +73,4 @@ foreach t : test_cases env: ['EXPECTED_FILE=' + meson.current_build_dir() / expected], workdir: testdir ) -endforeach \ No newline at end of file +endforeach diff --git a/tests/test-defs.sh b/tests/test-defs.sh index 8c5b521f01..79843095e5 100755 --- a/tests/test-defs.sh +++ b/tests/test-defs.sh @@ -83,10 +83,14 @@ fi # run_output_test() { + EXPECTED_EXITVAL=0 if [ "$1" = "-o" ] ; then TEST_OUTPUT="$2" - shift - shift + shift 2 + fi + if [ "$1" = "--exit" ] ; then + EXPECTED_EXITVAL="$2" + shift 2 fi TEST_COMMAND="$1" shift @@ -94,9 +98,9 @@ run_output_test() TEST_OUTPUT=${TEST_COMMAND} fi - REDIR_OUTPUT="> \"${TEST_OUTPUT}.out\"" + REDIR_OUTPUT="> \"${TEST_OUTPUT}.out\" 2>&1" if [ $VERBOSE -gt 1 ] ; then - REDIR_OUTPUT="| tee \"${TEST_OUTPUT}.out\"" + REDIR_OUTPUT="2>&1 | tee \"${TEST_OUTPUT}.out\"" fi if [ $use_valgrind -eq 1 ] ; then @@ -108,15 +112,16 @@ run_output_test() --show-reachable=yes \ --run-libc-freeres=yes \ "\"${top_builddir}/${TEST_COMMAND}\"" \"\$@\" ${REDIR_OUTPUT} - err=$? + exitval=$? else eval "\"${top_builddir}/${TEST_COMMAND}"\" \"\$@\" ${REDIR_OUTPUT} - err=$? + exitval=$? fi - if [ $err -ne 0 ] ; then - echo "ERROR: \"${TEST_COMMAND} $@\" exited with non-zero exit status: $err" 1>&2 + if [ $exitval -ne $EXPECTED_EXITVAL ] ; then + echo "ERROR: \"${TEST_COMMAND} $@\" exited with incorrect exit status: $err != $EXPECTED_EXITVAL" 1>&2 + err=1 fi if [ $use_valgrind -eq 1 ] ; then diff --git a/tests/test_deep_nesting.c b/tests/test_deep_nesting.c new file mode 100644 index 0000000000..4bf24578da --- /dev/null +++ b/tests/test_deep_nesting.c @@ -0,0 +1,140 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#ifdef HAVE_SYS_RESOURCE_H +#include +#endif + +#include "config.h" + +#include "json.h" + +#define NESTING_DEPTH 100000 + +static char *generate_json_string(void) +{ + char *str; + int depth = NESTING_DEPTH; + str = malloc(depth * 2 + 1); + memset(str, '[', depth); + memset(str + depth, ']', depth); + str[depth * 2] = '\0'; + return str; +} + +static void test_deep_nesting_put(const char *str) +{ + json_object *my_array; + + struct json_tokener *tok = json_tokener_new_ex(NESTING_DEPTH); + my_array = json_tokener_parse_ex(tok, str, strlen(str) + 1); + printf("Parsed depth %d string to json_object: %s\n", NESTING_DEPTH, (my_array == NULL) ? "NO" : "yes"); + + json_object_put(my_array); + printf("Freed json_object\n"); + + json_tokener_free(tok); +} + +static void test_deep_nesting_tostring(const char *str) +{ + json_object *my_array; + + struct json_tokener *tok = json_tokener_new_ex(NESTING_DEPTH); + my_array = json_tokener_parse_ex(tok, str, strlen(str) + 1); + printf("Parsed depth %d string to json_object: %s\n", NESTING_DEPTH, (my_array == NULL) ? "NO" : "yes"); + + const char *res = json_object_to_json_string_ext(my_array, JSON_C_TO_STRING_PLAIN); + printf("Serialized to string of length %ld\n", (long)strlen(res)); + json_object_put(my_array); + printf("Freed json_object\n"); + + json_tokener_free(tok); +} + +struct userdata_test { + int userdata_val; + char *p; +}; +/* + * Check that the user_delete function is only called once, even with the + * newer code to avoid deeply nested calls during json_object_put(). + */ +static void user_delete_test(struct json_object *jso, void *userdata_in) +{ + struct userdata_test *userdata = (struct userdata_test *)userdata_in; + printf("in user_delete, userdata_val=%d\n", userdata->userdata_val); + fflush(stdout); + userdata->userdata_val = 0; + userdata->p[0] = 'x'; + userdata->p[8191] = 'x'; + free(userdata->p); +} +static void test_nesting_with_user_delete(void) +{ + json_object *jso; + struct userdata_test userdata_val = { + 1, malloc(8192) + }; + + jso = json_object_new_object(); + json_object_set_userdata(jso, &userdata_val, user_delete_test); + json_object_object_add(jso, "somekey", json_object_new_string("foo")); + json_object_put(jso); +} + +/* + * Check that json_object_put() returns 1 whenever the object is freed, + * including for scalars and empty containers, and 0 only when a + * reference remains. + */ +static void test_put_return_values(void) +{ + json_object *jso; + + printf("put(empty object) returned %d\n", json_object_put(json_object_new_object())); + printf("put(empty array) returned %d\n", json_object_put(json_object_new_array())); + printf("put(string) returned %d\n", json_object_put(json_object_new_string("foo"))); + printf("put(int) returned %d\n", json_object_put(json_object_new_int(42))); + + jso = json_object_new_object(); + json_object_object_add(jso, "somekey", json_object_new_string("foo")); + printf("put(non-empty object) returned %d\n", json_object_put(jso)); + + jso = json_object_new_object(); + json_object_get(jso); + printf("put(still-referenced object) returned %d\n", json_object_put(jso)); + printf("put(last reference) returned %d\n", json_object_put(jso)); +} + +int main(int argc, char **argv) +{ + char *str; +#ifdef HAVE_SETRLIMIT + struct rlimit rl; + rl.rlim_cur = 2048; + rl.rlim_max = 2048; + setrlimit(RLIMIT_STACK, &rl); +#endif + + str = generate_json_string(); + + MC_SET_DEBUG(1); + + test_deep_nesting_put(str); + + if (0) // TODO: make json_object_to_json_string non-recursive + test_deep_nesting_tostring(str); + + free(str); + + test_nesting_with_user_delete(); + + test_put_return_values(); + + return EXIT_SUCCESS; +} diff --git a/tests/test_deep_nesting.expected b/tests/test_deep_nesting.expected new file mode 100644 index 0000000000..2449ede326 --- /dev/null +++ b/tests/test_deep_nesting.expected @@ -0,0 +1,10 @@ +Parsed depth 100000 string to json_object: yes +Freed json_object +in user_delete, userdata_val=1 +put(empty object) returned 1 +put(empty array) returned 1 +put(string) returned 1 +put(int) returned 1 +put(non-empty object) returned 1 +put(still-referenced object) returned 0 +put(last reference) returned 1 diff --git a/tests/test_deep_nesting.test b/tests/test_deep_nesting.test new file mode 120000 index 0000000000..58a13f4f32 --- /dev/null +++ b/tests/test_deep_nesting.test @@ -0,0 +1 @@ +test_basic.test \ No newline at end of file diff --git a/tests/test_int_add.c b/tests/test_int_add.c index 142feacb6e..be3f5c06ce 100644 --- a/tests/test_int_add.c +++ b/tests/test_int_add.c @@ -68,6 +68,17 @@ int main(int argc, char **argv) assert(json_object_get_uint64(tmp) == 0); json_object_put(tmp); printf("UINT64 ADD UNDERFLOW PASSED\n"); + // Decrement by INT64_MIN: -val would overflow int64_t, so the magnitude + // has to be taken in unsigned space. + tmp = json_object_new_uint64(100); + json_object_int_inc(tmp, INT64_MIN); + assert(json_object_get_int64(tmp) == INT64_MIN + 100); + json_object_put(tmp); + tmp = json_object_new_uint64(UINT64_MAX); + json_object_int_inc(tmp, INT64_MIN); + assert(json_object_get_uint64(tmp) == (uint64_t)INT64_MAX); + json_object_put(tmp); + printf("UINT64 ADD INT64_MIN PASSED\n"); printf("PASSED\n"); return 0; diff --git a/tests/test_int_add.expected b/tests/test_int_add.expected index f9348d4b09..34b8edfbb8 100644 --- a/tests/test_int_add.expected +++ b/tests/test_int_add.expected @@ -7,4 +7,5 @@ INT64 ADD UNDERFLOW PASSED UINT64 ADD PASSED UINT64 ADD OVERFLOW PASSED UINT64 ADD UNDERFLOW PASSED +UINT64 ADD INT64_MIN PASSED PASSED diff --git a/tests/test_int_get.c b/tests/test_int_get.c index e694fb36a4..3c41a828e2 100644 --- a/tests/test_int_get.c +++ b/tests/test_int_get.c @@ -65,6 +65,8 @@ int main(int argc, char **argv) CHECK_GET_INT64(N_DBL(INFINITY), INT64_MAX && errno == ERANGE); CHECK_GET_INT64(N_DBL(-INFINITY), INT64_MIN && errno == ERANGE); CHECK_GET_INT64(N_DBL(NAN), INT64_MIN && errno == EINVAL); + // (double)INT64_MAX rounds up to 2^63, which is one past INT64_MAX + CHECK_GET_INT64(N_DBL(9223372036854775808.0), INT64_MAX && errno == ERANGE); printf("INT64 GET PASSED\n"); CHECK_GET_UINT64(N_U64(UINT64_MAX), UINT64_MAX && errno == 0); @@ -73,6 +75,8 @@ int main(int argc, char **argv) CHECK_GET_UINT64(N_DBL(INFINITY), UINT64_MAX && errno == ERANGE); CHECK_GET_UINT64(N_DBL(-INFINITY), 0 && errno == ERANGE); CHECK_GET_UINT64(N_DBL(NAN), 0 && errno == EINVAL); + // (double)UINT64_MAX rounds up to 2^64, which is one past UINT64_MAX + CHECK_GET_UINT64(N_DBL(18446744073709551616.0), UINT64_MAX && errno == ERANGE); printf("UINT64 GET PASSED\n"); printf("PASSED\n"); diff --git a/tests/test_json_parse_cli.expected b/tests/test_json_parse_cli.expected new file mode 100644 index 0000000000..c1eeca4b3c --- /dev/null +++ b/tests/test_json_parse_cli.expected @@ -0,0 +1,2 @@ +Successfully parsed object from stdin +Failed at offset 71: invalid utf-8 string: char=0x00 diff --git a/tests/test_json_parse_cli.test b/tests/test_json_parse_cli.test new file mode 100755 index 0000000000..0982afe3c8 --- /dev/null +++ b/tests/test_json_parse_cli.test @@ -0,0 +1,21 @@ +#!/bin/sh + +# Common definitions +if test -z "$srcdir"; then + srcdir="${0%/*}" + test "$srcdir" = "$0" && srcdir=. + test -z "$srcdir" && srcdir=. +fi +. "$srcdir/test-defs.sh" + +set -x -v + +echo -n '"tenant=blue;note=CANARY_STACK_WINDOW_2026;status=ok"' > file1.dat +(printf '"' ; printf 'A%.0s' $(seq 1 16) ; echo 'e2' | xxd -r -p) > file2.dat + +TESTNAME="${0##*/}" +TESTNAME="${TESTNAME%.test}" +( cat file1.dat ; cat file2.dat ) | run_output_test -o "${TESTNAME}" --exit 1 ../apps/json_parse -u -N -n - +_err=$? + +exit $_err diff --git a/tests/test_json_patch.expected b/tests/test_json_patch.expected index 523d276b25..8882779ff4 100644 --- a/tests/test_json_patch.expected +++ b/tests/test_json_patch.expected @@ -123,7 +123,7 @@ Testing 'test add with bad number should fail', doc '[ "foo", "sil" ]' patch '[ Testing 'missing 'path' parameter', doc '{ }' patch '[ { "op": "add", "value": "bar" } ]' : OK => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object does not contain 'path' field Testing ''path' parameter with null value', doc '{ }' patch '[ { "op": "add", "path": null, "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 + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object 'path' field is not a string Testing 'invalid JSON Pointer token', doc '{ }' patch '[ { "op": "add", "path": "foo", "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 'missing 'value' parameter to add', doc '[ 1 ]' patch '[ { "op": "add", "path": "\/-" } ]' : OK @@ -156,3 +156,10 @@ Testing 'Removing deep nonexistent path', doc '{ "foo": "bar" }' patch '[ { "op" Testing 'Removing nonexistent index', doc '[ "foo", "bar" ]' patch '[ { "op": "remove", "path": "\/2" } ]' : OK => json_patch_apply failed as expected: ERRNO=ENOENT at patch idx 0: Did not find element referenced by path field Testing 'Patch with different capitalisation than doc', doc '{ "foo": "bar" }' patch '[ { "op": "add", "path": "\/FOO", "value": "BAR" } ]' : OK +Testing 'copy must duplicate the value, not alias it, so no cycle can form', doc '{ "a": { "p": { } } }' patch '[ { "op": "copy", "from": "\/a", "path": "\/b" }, { "op": "copy", "from": "\/b", "path": "\/a\/p\/x" } ]' : OK +Testing 'null op field must be rejected, not dereferenced', doc '{ "foo": "bar" }' patch '[ { "op": null, "path": "\/foo" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object 'op' field is not a string +Testing 'null from field must be rejected, not dereferenced', doc '{ "foo": "bar" }' patch '[ { "op": "move", "from": null, "path": "\/foo" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object 'from' field is not a string +Testing 'null path field must be rejected, not dereferenced', doc '{ "foo": "bar" }' patch '[ { "op": "remove", "path": null } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Patch object 'path' field is not a string diff --git a/tests/test_json_pointer.c b/tests/test_json_pointer.c index 09c195a2fb..b18c98a95c 100644 --- a/tests/test_json_pointer.c +++ b/tests/test_json_pointer.c @@ -203,6 +203,10 @@ static void test_wrong_inputs_get(void) assert(0 != json_pointer_get(jo1, "/foo/01", NULL)); assert(errno == EINVAL); errno = 0; + /* An empty reference token is not a valid array index */ + assert(0 != json_pointer_get(jo1, "/foo/", NULL)); + assert(errno == EINVAL); + errno = 0; assert(0 != json_pointer_getf(jo1, NULL, "/%s/a", "foo")); assert(errno == EINVAL); errno = 0; @@ -314,6 +318,10 @@ static void test_wrong_inputs_set(void) printf("PASSED - SET - failed with invalid array index'\n"); json_object_put(jo2); + assert(0 != json_pointer_set(&jo1, "/foo/", (jo2 = json_object_new_string("cod")))); + printf("PASSED - SET - failed with empty array index'\n"); + json_object_put(jo2); + jo2 = json_object_new_string("whatever"); assert(0 != json_pointer_set(&jo1, "/fud/gaw", jo2)); assert(0 == json_pointer_set(&jo1, "/fud", json_object_new_object())); diff --git a/tests/test_json_pointer.expected b/tests/test_json_pointer.expected index 7cb158edf5..3b363a5087 100644 --- a/tests/test_json_pointer.expected +++ b/tests/test_json_pointer.expected @@ -36,4 +36,5 @@ PASSED - SET - failed with NULL params for input json & path PASSED - SET - failed 'cod' with path 'foo/bar' PASSED - SET - failed 'cod' with path 'foo/bar' PASSED - SET - failed with invalid array index' +PASSED - SET - failed with empty array index' PASSED - SET - failed to set index to non-array diff --git a/tests/test_locale.c b/tests/test_locale.c index 79f6c2c6bd..efbd425142 100644 --- a/tests/test_locale.c +++ b/tests/test_locale.c @@ -40,6 +40,18 @@ int main(int argc, char **argv) if (strcmp(buf1, buf2) != 0) printf("ERROR: Original locale not restored \"%s\" != \"%s\"", buf1, buf2); + // json_object_get_double() on a json_type_string must not depend on the + // locale's decimal point. Under a comma-radix locale such as de_DE the + // '.' used to be treated as a stray char, so "19.95" came back as 0. + // (If the de_DE locale isn't installed we're still in "C" here, where + // this passes anyway, so the check is safe to always run.) + { + json_object *numstr = json_object_new_string("19.95"); + double d = json_object_get_double(numstr); + assert(d > 19.94 && d < 19.96); + json_object_put(numstr); + } + #ifdef HAVE_SETLOCALE setlocale(LC_NUMERIC, "C"); #endif diff --git a/tests/test_object_iterator.c b/tests/test_object_iterator.c index 4928088e09..2202ed5d7f 100644 --- a/tests/test_object_iterator.c +++ b/tests/test_object_iterator.c @@ -2,6 +2,7 @@ #undef NDEBUG #endif #include "config.h" +#include #include #include #include @@ -20,6 +21,7 @@ int main(int atgc, char **argv) \"boolean_false\": false,\n\ \"big_number\": 2147483649,\n\ \"a_null\": null,\n\ + \"an_object\": { \"foo\": \"bar\" },\n\ }"; struct json_object *new_obj; @@ -31,10 +33,41 @@ int main(int atgc, char **argv) it = json_object_iter_begin(new_obj); itEnd = json_object_iter_end(new_obj); + // If this changes size on any platform, it'd break the ABI + printf("sizeof(struct json_object_iterator)/sizeof(void *)=%zu\n", sizeof(struct json_object_iterator) / sizeof(void *)); + while (!json_object_iter_equal(&it, &itEnd)) { - printf("%s\n", json_object_iter_peek_name(&it)); - printf("%s\n", json_object_to_json_string(json_object_iter_peek_value(&it))); + const char *key = json_object_iter_peek_name(&it); + json_object *value = json_object_iter_peek_value(&it); + printf("key: %s\n", key); + printf("pre change: %s\n", json_object_to_json_string(value)); + if (json_object_get_type(value) == json_type_object) + { + // Changing sub-objects is ok + assert(json_object_object_add(value, "foo", json_object_new_string("change foo")) == 0); + assert(json_object_object_add(value, "newkey", json_object_new_string("new value")) == 0); + } + else + { + // Changing the value of pairs is ok + assert(json_object_object_add(new_obj, key, json_object_new_string("switched")) == 0); + } + + if (0) // DO NOT DO THIS + { + // Adding new keys is NOT ok, and invalidates the iterator + json_object_object_add(value, "somenewkey", json_object_new_string("foo")); + + // Deleting any keys ALSO invalidates the iterator, even if + // the key is immediately re-added. + json_object_object_del(value, key); + json_object_object_add(value, key, json_object_new_string("switched")); + } + + value = json_object_iter_peek_value(&it); + printf("post change: %s\n", json_object_to_json_string(value)); + json_object_iter_next(&it); } diff --git a/tests/test_object_iterator.expected b/tests/test_object_iterator.expected index e56e288b04..ea764da45f 100644 --- a/tests/test_object_iterator.expected +++ b/tests/test_object_iterator.expected @@ -1,14 +1,25 @@ -string_of_digits -"123" -regular_number -222 -decimal_number -99.55 -boolean_true -true -boolean_false -false -big_number -2147483649 -a_null -null +sizeof(struct json_object_iterator)/sizeof(void *)=1 +key: string_of_digits +pre change: "123" +post change: "switched" +key: regular_number +pre change: 222 +post change: "switched" +key: decimal_number +pre change: 99.55 +post change: "switched" +key: boolean_true +pre change: true +post change: "switched" +key: boolean_false +pre change: false +post change: "switched" +key: big_number +pre change: 2147483649 +post change: "switched" +key: a_null +pre change: null +post change: "switched" +key: an_object +pre change: { "foo": "bar" } +post change: { "foo": "change foo", "newkey": "new value" } diff --git a/tests/test_parse.c b/tests/test_parse.c index 9bcf278cb5..897863af41 100644 --- a/tests/test_parse.c +++ b/tests/test_parse.c @@ -366,6 +366,15 @@ struct incremental_step {"12{", 3, 2, json_tokener_success, 1, 0}, /* Parse number in strict mode */ {"[02]", -1, 3, json_tokener_error_parse_number, 1, JSON_TOKENER_STRICT}, + /* Leading zeros are rejected in strict mode, for every sign and type ... */ + {"[00]", -1, 3, json_tokener_error_parse_number, 1, JSON_TOKENER_STRICT}, + {"[-00]", -1, 4, json_tokener_error_parse_number, 1, JSON_TOKENER_STRICT}, + {"[-01]", -1, 4, json_tokener_error_parse_number, 1, JSON_TOKENER_STRICT}, + {"[-0123]", -1, 6, json_tokener_error_parse_number, 1, JSON_TOKENER_STRICT}, + {"[01.5]", -1, 5, json_tokener_error_parse_number, 1, JSON_TOKENER_STRICT}, + /* ... but a lone zero, "-0" and a zero before the fraction stay valid. */ + {"[-0]", -1, -1, json_tokener_success, 1, JSON_TOKENER_STRICT}, + {"[0.5]", -1, -1, json_tokener_success, 1, JSON_TOKENER_STRICT}, {"0e+0", 5, 4, json_tokener_success, 1, 0}, {"[0e+0]", -1, -1, json_tokener_success, 1, 0}, diff --git a/tests/test_parse.expected b/tests/test_parse.expected index 8d3961f51d..acf0594d11 100644 --- a/tests/test_parse.expected +++ b/tests/test_parse.expected @@ -156,6 +156,13 @@ json_tokener_parse_ex(tok, 1 , 1) ... OK: got correct error: continu json_tokener_parse_ex(tok, 2 , 2) ... OK: got object of type [int]: 12 json_tokener_parse_ex(tok, 12{ , 3) ... OK: got object of type [int]: 12 json_tokener_parse_ex(tok, [02] , 4) ... OK: got correct error: number expected +json_tokener_parse_ex(tok, [00] , 4) ... OK: got correct error: number expected +json_tokener_parse_ex(tok, [-00] , 5) ... OK: got correct error: number expected +json_tokener_parse_ex(tok, [-01] , 5) ... OK: got correct error: number expected +json_tokener_parse_ex(tok, [-0123] , 7) ... OK: got correct error: number expected +json_tokener_parse_ex(tok, [01.5] , 6) ... OK: got correct error: number expected +json_tokener_parse_ex(tok, [-0] , 4) ... OK: got object of type [array]: [ 0 ] +json_tokener_parse_ex(tok, [0.5] , 5) ... OK: got object of type [array]: [ 0.5 ] json_tokener_parse_ex(tok, 0e+0 , 5) ... OK: got object of type [double]: 0e+0 json_tokener_parse_ex(tok, [0e+0] , 6) ... OK: got object of type [array]: [ 0e+0 ] json_tokener_parse_ex(tok, 0e , 2) ... OK: got correct error: continue @@ -368,5 +375,5 @@ json_tokener_parse_ex(tok, {"":1} , 7) ... OK: got correct error: invalid json_tokener_parse_ex(tok, {"":1} , 7) ... OK: got correct error: invalid string sequence json_tokener_parse_ex(tok, {"":1} , 7) ... OK: got correct error: invalid string sequence json_tokener_parse_ex(tok, {"":1} , 7) ... OK: got correct error: invalid string sequence -End Incremental Tests OK=272 ERROR=0 +End Incremental Tests OK=279 ERROR=0 ================================== diff --git a/tests/test_safe_json_pointer_set.c b/tests/test_safe_json_pointer_set.c new file mode 100644 index 0000000000..cebe1faf8c --- /dev/null +++ b/tests/test_safe_json_pointer_set.c @@ -0,0 +1,249 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include + +#include "json.h" + +static const char *input_json_str = "{ " + "'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 " + "}"; + +static void test_example_set_with_limit_index(void) +{ + struct json_object *jo2, *jo1 = json_tokener_parse(input_json_str); + size_t limit_index = 10; + + // testing if json_pointer_set_with_limit_index() works as json_pointer_set() + assert(jo1 != NULL); + printf("PASSED - SET_WITH_LIMIT - LOADED TEST JSON\n"); + printf("%s\n", json_object_get_string(jo1)); + + assert(0 == json_pointer_set_with_limit_index(&jo1, "/foo/1", json_object_new_string("cod"), limit_index)); + assert(0 == strcmp("cod", json_object_get_string(json_object_array_get_idx( + json_object_object_get(jo1, "foo"), 1)))); + printf("PASSED - SET_WITH_LIMIT - 'cod' in /foo/1\n"); + assert(0 != json_pointer_set_with_limit_index(&jo1, "/fud/gaw", (jo2 = json_tokener_parse("[1,2,3]")), limit_index)); + assert(errno == ENOENT); + printf("PASSED - SET_WITH_LIMIT - non-existing /fud/gaw\n"); + assert(0 == json_pointer_set_with_limit_index(&jo1, "/fud", json_object_new_object(), limit_index)); + printf("PASSED - SET_WITH_LIMIT - /fud == {}\n"); + assert(0 == json_pointer_set_with_limit_index(&jo1, "/fud/gaw", jo2, limit_index)); /* re-using jo2 from above */ + printf("PASSED - SET_WITH_LIMIT - /fug/gaw == [1,2,3]\n"); + assert(0 == json_pointer_set_with_limit_index(&jo1, "/fud/gaw/0", json_object_new_int(0), limit_index)); + assert(0 == json_pointer_setf(&jo1, json_object_new_int(0), "%s%s/%d", "/fud", "/gaw", 0)); + printf("PASSED - SET_WITH_LIMIT - /fug/gaw == [0,2,3]\n"); + assert(0 == json_pointer_set_with_limit_index(&jo1, "/fud/gaw/-", json_object_new_int(4), limit_index)); + printf("PASSED - SET_WITH_LIMIT - /fug/gaw == [0,2,3,4]\n"); + assert(0 == json_pointer_set_with_limit_index(&jo1, "/", json_object_new_int(9), limit_index)); + printf("PASSED - SET_WITH_LIMIT - / == 9\n"); + + jo2 = json_tokener_parse( + "{ 'foo': [ 'bar', 'cod' ], '': 9, 'a/b': 1, 'c%d': 2, 'e^f': 3, 'g|h': 4, 'i\\\\j': " + "5, 'k\\\"l': 6, ' ': 7, 'm~n': 8, 'fud': { 'gaw': [ 0, 2, 3, 4 ] } }"); + assert(json_object_equal(jo2, jo1)); + printf("PASSED - SET_WITH_LIMIT - Final JSON is: %s\n", json_object_get_string(jo1)); + json_object_put(jo2); + + assert(0 == json_pointer_set_with_limit_index(&jo1, "", json_object_new_int(10), limit_index)); + assert(10 == json_object_get_int(jo1)); + printf("%s\n", json_object_get_string(jo1)); + + json_object_put(jo1); + + jo1 = json_tokener_parse("[0, 1, 2, 3]"); + jo2 = json_tokener_parse("[0, 1, 2, 3, null, null, null, 7]"); + + assert(0 == json_pointer_set_with_limit_index(&jo1, "/7", json_object_new_int(7), limit_index)); + assert(1 == json_object_equal(jo1, jo2)); + + json_object_put(jo1); + + jo1 = json_tokener_parse("[0, 1, 2, 3]"); + + assert(0 == json_pointer_setf(&jo1, json_object_new_int(7), "/%u", 7)); + assert(1 == json_object_equal(jo1, jo2)); + + json_object_put(jo1); + json_object_put(jo2); + + // testing with limit_index + jo1 = json_tokener_parse("{'foo': ['bar', 'baz']}"); + jo2 = json_tokener_parse("{'foo': ['bar', 'cod']}"); + + assert(0 == json_pointer_set_with_limit_index(&jo1, "/foo/1", json_object_new_string("cod"), limit_index)); + assert(json_object_equal(jo1, jo2)); + printf("PASSED - SET_LIMIT - Set value within limit (/foo/1 with limit 10)\n"); + + assert(0 == json_pointer_set_with_limit_index(&jo1, "/bar", json_object_new_string("new_field"), limit_index)); + printf("PASSED - SET_LIMIT - Set value on an object (limit is ignored)\n"); + + assert(0 == json_pointer_set_with_limit_index(&jo1, "/foo/20", json_object_new_string("big_index"), (size_t)-1)); + printf("PASSED - SET_LIMIT - Set value with limit_index = -1 (no limit)\n"); + + json_object_put(jo1); + json_object_put(jo2); +} + +static void test_wrong_inputs_set_with_limit_index(void) +{ + struct json_object *jo2, *jo1 = json_tokener_parse(input_json_str); + size_t limit_index = 10; + + // testing if json_pointer_set_with_limit_index() works as json_pointer_set() + assert(jo1 != NULL); + printf("PASSED - SET_WITH_LIMIT - LOADED TEST JSON\n"); + printf("%s\n", json_object_get_string(jo1)); + + assert(0 != json_pointer_set_with_limit_index(NULL, NULL, NULL, limit_index)); + assert(0 != json_pointer_set_with_limit_index(&jo1, NULL, NULL, limit_index)); + printf("PASSED - SET_WITH_LIMIT - failed with NULL params for input json & path\n"); + + assert(0 != json_pointer_set_with_limit_index(&jo1, "foo/bar", (jo2 = json_object_new_string("cod")), limit_index)); + printf("PASSED - SET_WITH_LIMIT - failed 'cod' with path 'foo/bar'\n"); + json_object_put(jo2); + + // assert(0 != + // json_pointer_setf(&jo1, (jo2 = json_object_new_string("cod")), "%s", "foo/bar")); + // printf("PASSED - SET_WITH_LIMIT - failed 'cod' with path 'foo/bar'\n"); + // json_object_put(jo2); + + assert(0 != json_pointer_set_with_limit_index(&jo1, "0", (jo2 = json_object_new_string("cod")), limit_index)); + printf("PASSED - SET_WITH_LIMIT - failed with invalid array index'\n"); + json_object_put(jo2); + + jo2 = json_object_new_string("whatever"); + assert(0 != json_pointer_set_with_limit_index(&jo1, "/fud/gaw", jo2, limit_index)); + assert(0 == json_pointer_set_with_limit_index(&jo1, "/fud", json_object_new_object(), limit_index)); + assert(0 == json_pointer_set_with_limit_index(&jo1, "/fud/gaw", jo2, limit_index)); /* re-using jo2 from above */ + // ownership of jo2 transferred into jo1 + + jo2 = json_object_new_int(0); + assert(0 != json_pointer_set_with_limit_index(&jo1, "/fud/gaw/0", jo2, limit_index)); + json_object_put(jo2); + jo2 = json_object_new_int(0); + assert(0 != json_pointer_set_with_limit_index(&jo1, "/fud/gaw/", jo2, limit_index)); + json_object_put(jo2); + printf("PASSED - SET_WITH_LIMIT - failed to set index to non-array\n"); + + // assert(0 == json_pointer_setf(&jo1, json_object_new_string("cod"), "%s", "\0")); + + json_object_put(jo1); + + // testing with limit_index + jo1 = json_tokener_parse("{'foo': ['bar', 'baz']}"); + + errno = 0; + jo2 = json_object_new_string("out_of_bounds"); + assert(0 != json_pointer_set_with_limit_index(&jo1, "/foo/20", jo2, limit_index)); + assert(errno == EINVAL); + printf("PASSED - SET_LIMIT - Failed to set index 20 with limit 10\n"); + // The value object was not consumed, so we must put it. + json_object_put(jo2); + + // corner case: setting an index that equals the limit (should be ok, as it's idx > max_idx) + errno = 0; + assert(0 == json_pointer_set_with_limit_index(&jo1, "/foo/10", json_object_new_string("at_the_limit"), limit_index)); + printf("PASSED - SET_LIMIT - Succeeded to set index 10 with limit 10\n"); + + json_object_put(jo1); +} + +// callback for testing json_pointer_set_with_cb() +static int test_cb_print_msg(json_object *parent, const char *key, size_t idx, + json_object *value, void *priv) +{ + printf("PASSED - SET_WITH_CB - This callback is called\n"); + json_object_put(value); + return 0; +} + +// callback for testing json_pointer_set_with_cb() with rejection logic +static int test_cb_reject_logic(json_object *parent, const char *key, size_t idx, + json_object *value, void *priv) +{ + // Reject any operation if the key is "reject" + if (key && strcmp(key, "reject") == 0) + { + printf("PASSED - SET_WITH_CB - Callback correctly identified key 'reject' to reject\n"); + // Note: caller of json_pointer_set must free value + return -1; + } + json_object_put(value); + return 0; +} + +static void test_set_with_cb(void) +{ + struct json_object *jo2, *jo1 = json_tokener_parse(input_json_str); + size_t limit_index = 5; + + assert(jo1 != NULL); + printf("PASSED - SET_WITH_CB - LOADED TEST JSON\n"); + printf("%s\n", json_object_get_string(jo1)); + + + assert(0 == json_pointer_set_with_cb(&jo1, "/foo/1", json_object_new_string("cod"), test_cb_print_msg, 1, NULL)); + printf("PASSED - SET_WITH_CB - callback test_cb_print_msg for /foo/1\n"); + + assert(0 == json_pointer_set_with_cb(&jo1, "/foo/4", json_object_new_string("in"), json_object_array_put_with_idx_limit_cb, 0, &limit_index)); + printf("PASSED - SET_WITH_CB - callback json_object_array_put_with_idx_limit_cb for /foo/4 with limit_index 5\n"); + + assert(0 == json_pointer_set_with_cb(&jo1, "/foo/5", json_object_new_string("border"), json_object_array_put_with_idx_limit_cb, 0, &limit_index)); + printf("PASSED - SET_WITH_CB - failed with callback json_object_array_put_with_idx_limit_cb for /foo/5 with limit_index 5\n"); + + assert(0 != json_pointer_set_with_cb(&jo1, "/foo/10", (jo2 = json_object_new_string("out")), json_object_array_put_with_idx_limit_cb, 0, &limit_index)); + json_object_put(jo2); + assert(errno == EINVAL); + printf("PASSED - SET_WITH_CB - failed with callback json_object_array_put_with_idx_limit_cb for /foo/10 with limit_index 5\n"); + + assert(0 != json_pointer_set_with_cb(&jo1, "/foo/2", (jo2 = json_object_new_string("null_priv")), json_object_array_put_with_idx_limit_cb, 0, NULL)); + json_object_put(jo2); + assert(errno == EFAULT); + printf("PASSED - SET_WITH_CB - failed with callback json_object_array_put_with_idx_limit_cb with NULL priv\n"); + + json_object_put(jo1); + + jo1 = json_tokener_parse("{'foo': 'bar'}"); + assert(jo1 != NULL); + + assert(0 == json_pointer_set_with_cb(&jo1, "/foo", json_object_new_string("cod"), test_cb_print_msg, 1, NULL)); + printf("PASSED - SET_WITH_CB - cb_handles_obj=1: callback was triggered for object operation\n"); + + assert(0 == json_pointer_set_with_cb(&jo1, "/new_key", json_object_new_string("new_value"), test_cb_print_msg, 0, NULL)); + printf("PASSED - SET_WITH_CB - cb_handles_obj=0: callback was NOT triggered for object operation, default logic was used\n"); + json_object_put(jo1); + + // testing rejection logic callback + jo1 = json_tokener_parse("{'data': {} }"); + assert(jo1 != NULL); + + assert(0 == json_pointer_set_with_cb(&jo1, "/data/accept", json_object_new_string("accepted_value"), test_cb_reject_logic, 1, NULL)); + printf("PASSED - SET_WITH_CB - Rejection callback approved an allowed key\n"); + + assert(0 != json_pointer_set_with_cb(&jo1, "/data/reject", (jo2 = json_object_new_string("rejected_value")), test_cb_reject_logic, 1, NULL)); + json_object_put(jo2); + printf("PASSED - SET_WITH_CB - Rejection callback rejected a forbidden key\n"); + + json_object_put(jo1); +} + +int main(int argc, char **argv) +{ + test_example_set_with_limit_index(); + test_wrong_inputs_set_with_limit_index(); + test_set_with_cb(); + return 0; +} diff --git a/tests/test_safe_json_pointer_set.expected b/tests/test_safe_json_pointer_set.expected new file mode 100644 index 0000000000..aa5dc558d2 --- /dev/null +++ b/tests/test_safe_json_pointer_set.expected @@ -0,0 +1,36 @@ +PASSED - SET_WITH_LIMIT - LOADED TEST JSON +{ "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 } +PASSED - SET_WITH_LIMIT - 'cod' in /foo/1 +PASSED - SET_WITH_LIMIT - non-existing /fud/gaw +PASSED - SET_WITH_LIMIT - /fud == {} +PASSED - SET_WITH_LIMIT - /fug/gaw == [1,2,3] +PASSED - SET_WITH_LIMIT - /fug/gaw == [0,2,3] +PASSED - SET_WITH_LIMIT - /fug/gaw == [0,2,3,4] +PASSED - SET_WITH_LIMIT - / == 9 +PASSED - SET_WITH_LIMIT - Final JSON is: { "foo": [ "bar", "cod" ], "": 9, "a\/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8, "fud": { "gaw": [ 0, 2, 3, 4 ] } } +10 +PASSED - SET_LIMIT - Set value within limit (/foo/1 with limit 10) +PASSED - SET_LIMIT - Set value on an object (limit is ignored) +PASSED - SET_LIMIT - Set value with limit_index = -1 (no limit) +PASSED - SET_WITH_LIMIT - LOADED TEST JSON +{ "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 } +PASSED - SET_WITH_LIMIT - failed with NULL params for input json & path +PASSED - SET_WITH_LIMIT - failed 'cod' with path 'foo/bar' +PASSED - SET_WITH_LIMIT - failed with invalid array index' +PASSED - SET_WITH_LIMIT - failed to set index to non-array +PASSED - SET_LIMIT - Failed to set index 20 with limit 10 +PASSED - SET_LIMIT - Succeeded to set index 10 with limit 10 +PASSED - SET_WITH_CB - LOADED TEST JSON +{ "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 } +PASSED - SET_WITH_CB - This callback is called +PASSED - SET_WITH_CB - callback test_cb_print_msg for /foo/1 +PASSED - SET_WITH_CB - callback json_object_array_put_with_idx_limit_cb for /foo/4 with limit_index 5 +PASSED - SET_WITH_CB - failed with callback json_object_array_put_with_idx_limit_cb for /foo/5 with limit_index 5 +PASSED - SET_WITH_CB - failed with callback json_object_array_put_with_idx_limit_cb for /foo/10 with limit_index 5 +PASSED - SET_WITH_CB - failed with callback json_object_array_put_with_idx_limit_cb with NULL priv +PASSED - SET_WITH_CB - This callback is called +PASSED - SET_WITH_CB - cb_handles_obj=1: callback was triggered for object operation +PASSED - SET_WITH_CB - cb_handles_obj=0: callback was NOT triggered for object operation, default logic was used +PASSED - SET_WITH_CB - Rejection callback approved an allowed key +PASSED - SET_WITH_CB - Callback correctly identified key 'reject' to reject +PASSED - SET_WITH_CB - Rejection callback rejected a forbidden key diff --git a/tests/test_safe_json_pointer_set.test b/tests/test_safe_json_pointer_set.test new file mode 120000 index 0000000000..58a13f4f32 --- /dev/null +++ b/tests/test_safe_json_pointer_set.test @@ -0,0 +1 @@ +test_basic.test \ No newline at end of file diff --git a/tests/test_set_value.c b/tests/test_set_value.c index a8ebbfec79..dac25770ab 100644 --- a/tests/test_set_value.c +++ b/tests/test_set_value.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "json.h" @@ -106,7 +107,9 @@ int main(int argc, char **argv) tmp = json_tokener_parse("1.234"); json_object_set_double(tmp, 12.3); const char *serialized = json_object_to_json_string(tmp); - fprintf(stderr, "%s\n", serialized); + if (getenv("JSONC_TEST_TRACE") != NULL) + // This output might be different on different systems + fprintf(stderr, "%s\n", serialized); assert(strncmp(serialized, "12.3", 4) == 0); json_object_put(tmp); printf("PARSE AND SET PASSED\n"); diff --git a/tests/test_strerror.c b/tests/test_strerror.c index 773b1aa10b..121f4e0a95 100644 --- a/tests/test_strerror.c +++ b/tests/test_strerror.c @@ -9,5 +9,9 @@ int main(int argc, char **argv) { puts(strerror(10000)); puts(strerror(999)); + /* Negative values must not index before the digit table (out-of-bounds + * read); INT_MIN also exercises the unsigned magnitude handling. */ + puts(strerror(-5)); + puts(strerror(-2147483647 - 1)); return 0; } diff --git a/tests/test_strerror.expected b/tests/test_strerror.expected index b6b3bb6554..8b527df09e 100644 --- a/tests/test_strerror.expected +++ b/tests/test_strerror.expected @@ -1,2 +1,4 @@ ERRNO=10000 ERRNO=999 +ERRNO=-5 +ERRNO=-2147483648 diff --git a/tests/test_visit.c b/tests/test_visit.c index 3283a559a4..48fb60e8ff 100644 --- a/tests/test_visit.c +++ b/tests/test_visit.c @@ -77,6 +77,7 @@ static int emit_object(json_object *jso, int flags, json_object *parent_jso, con printf("flags: 0x%x, key: %s, index: %ld, value: %s\n", flags, (jso_key ? jso_key : "(null)"), (jso_index ? (long)*jso_index : -1L), json_object_to_json_string(jso)); + fflush(stdout); return JSON_C_VISIT_RETURN_CONTINUE; } @@ -148,5 +149,6 @@ static int err_return(json_object *jso, int flags, json_object *parent_jso, cons printf("flags: 0x%x, key: %s, index: %ld, value: %s\n", flags, (jso_key ? jso_key : "(null)"), (jso_index ? (long)*jso_index : -1L), json_object_to_json_string(jso)); + fflush(stdout); return 100; } diff --git a/tests/test_visit.expected b/tests/test_visit.expected index 5f323174bb..34da92a462 100644 --- a/tests/test_visit.expected +++ b/tests/test_visit.expected @@ -84,6 +84,7 @@ json_c_visit(stop_array)=0 ================================ flags: 0x0, key: (null), index: -1, value: { "obj1": 123, "obj2": { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] }, "obj3": 1.234, "obj4": [ true, false, null ] } +ERROR: invalid return value from json_c_visit userfunc: 100 json_c_visit(err_return)=-1 ================================