diff --git a/.clang-format b/.clang-format index 2a8066958..b7cf99793 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,4 @@ BasedOnStyle: LLVM DerivePointerAlignment: false PointerAlignment: Left - +SpacesBeforeTrailingComments: 1 diff --git a/.clang-tidy b/.clang-tidy index b78b3247e..75da71785 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,10 +1,9 @@ --- -Checks: 'google-readability-casting,modernize-use-default-member-init,modernize-use-using,modernize-use-auto,readability-redundant-member-init' +Checks: 'google-readability-casting,modernize-deprecated-headers,modernize-loop-convert,modernize-use-auto,modernize-use-default-member-init,modernize-use-using,readability-else-after-return,readability-redundant-member-init,readability-redundant-string-cstr' WarningsAsErrors: '' HeaderFilterRegex: '' -AnalyzeTemporaryDtors: false FormatStyle: none -CheckOptions: +CheckOptions: - key: modernize-use-using.IgnoreMacros value: '0' ... diff --git a/.github/workflows/abi-compatibility.yml b/.github/workflows/abi-compatibility.yml new file mode 100644 index 000000000..1351c09d4 --- /dev/null +++ b/.github/workflows/abi-compatibility.yml @@ -0,0 +1,88 @@ +name: ABI Compatibility + +on: [check_run, push, pull_request] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + abi-compatibility: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + shared_libs: [ON, OFF] + include: + - jsoncpp_std: 11 + app_std: 17 + - jsoncpp_std: 17 + app_std: 11 + - jsoncpp_std: 11 + app_std: 23 + - jsoncpp_std: 23 + app_std: 11 + - jsoncpp_std: 17 + app_std: 23 + - jsoncpp_std: 23 + app_std: 17 + + steps: + - name: checkout project + uses: actions/checkout@v4 + + - name: build and install JsonCpp (C++${{ matrix.jsoncpp_std }}) + shell: bash + run: | + mkdir build-jsoncpp + cd build-jsoncpp + cmake .. -DCMAKE_CXX_STANDARD=${{ matrix.jsoncpp_std }} \ + -DCMAKE_CXX_STANDARD_REQUIRED=ON \ + -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/install-jsoncpp \ + -DBUILD_SHARED_LIBS=${{ matrix.shared_libs }} \ + -DJSONCPP_WITH_TESTS=OFF + cmake --build . --config Release + cmake --install . --config Release + + - name: create example app + shell: bash + run: | + mkdir example-app + cat << 'EOF' > example-app/CMakeLists.txt + cmake_minimum_required(VERSION 3.10) + project(abi_test) + + find_package(jsoncpp REQUIRED CONFIG) + + add_executable(abi_test jsontest.cpp fuzz.cpp main.cpp) + target_link_libraries(abi_test PRIVATE JsonCpp::JsonCpp) + EOF + + cp src/test_lib_json/*.cpp example-app/ + cp src/test_lib_json/*.h example-app/ + + - name: build example app (C++${{ matrix.app_std }}) + shell: bash + run: | + cd example-app + mkdir build + cd build + cmake .. -DCMAKE_CXX_STANDARD=${{ matrix.app_std }} \ + -DCMAKE_CXX_STANDARD_REQUIRED=ON \ + -DCMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/install-jsoncpp + cmake --build . --config Release + + - name: run example app + shell: bash + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + export PATH=$GITHUB_WORKSPACE/install-jsoncpp/bin:$PATH + ./example-app/build/Release/abi_test.exe + elif [ "$RUNNER_OS" == "macOS" ]; then + export DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/install-jsoncpp/lib:$DYLD_LIBRARY_PATH + ./example-app/build/abi_test + else + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/install-jsoncpp/lib:$LD_LIBRARY_PATH + ./example-app/build/abi_test + fi diff --git a/.github/workflows/amalgamate.yml b/.github/workflows/amalgamate.yml new file mode 100644 index 000000000..e8a55d428 --- /dev/null +++ b/.github/workflows/amalgamate.yml @@ -0,0 +1,39 @@ +name: Amalgamation + +on: [check_run, push, pull_request] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + amalgamation: + runs-on: ubuntu-latest + + steps: + - name: checkout project + uses: actions/checkout@v4 + + - name: setup python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: run amalgamate script + run: | + python amalgamate.py + + - name: test compile amalgamated source + run: | + cat << 'EOF' > test_amalgamation.cpp + #include "json/json.h" + #include + + int main() { + Json::Value root; + root["hello"] = "world"; + std::cout << root.toStyledString() << std::endl; + return 0; + } + EOF + c++ -std=c++11 -I dist dist/jsoncpp.cpp test_amalgamation.cpp -o test_amalgamation + ./test_amalgamation \ No newline at end of file diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 000000000..ae3096302 --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,23 @@ +name: clang-format check +on: [check_run, pull_request, push] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + formatting-check: + name: formatting check + runs-on: ubuntu-latest + strategy: + matrix: + path: + - 'src' + - 'example' + - 'include' + steps: + - uses: actions/checkout@v4 + - name: runs clang-format style check for C/C++/Protobuf programs. + uses: jidicula/clang-format-action@v4.13.0 + with: + clang-format-version: '18' + check-path: ${{ matrix.path }} diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 000000000..2b2666d36 --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,25 @@ +name: cmake +on: [check_run, push, pull_request] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + cmake-publish: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + cxx_standard: [11, 17] + + steps: + - name: checkout project + uses: actions/checkout@v4 + + - name: build project + uses: threeal/cmake-action@v2.0.0 + with: + options: CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }} + diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml new file mode 100644 index 000000000..92d04862f --- /dev/null +++ b/.github/workflows/meson.yml @@ -0,0 +1,72 @@ +name: meson build and test +run-name: update pushed to ${{ github.ref }} +on: [check_run, push, pull_request] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + meson-publish: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: checkout repository + uses: actions/checkout@v4 + + - name: setup python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: meson build + uses: BSFishy/meson-build@v1.0.3 + with: + meson-version: 1.5.1 + ninja-version: 1.11.1.1 + action: build + + - name: meson test + uses: BSFishy/meson-build@v1.0.3 + with: + meson-version: 1.5.1 + ninja-version: 1.11.1.1 + action: test + + meson-coverage: + runs-on: ubuntu-latest + + steps: + - name: checkout repository + uses: actions/checkout@v4 + + - name: setup python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: meson build + uses: BSFishy/meson-build@v1.0.3 + with: + meson-version: 1.5.1 + ninja-version: 1.11.1.1 + setup-options: -Db_coverage=true + action: build + + - name: meson test + uses: BSFishy/meson-build@v1.0.3 + with: + meson-version: 1.5.1 + ninja-version: 1.11.1.1 + setup-options: -Db_coverage=true + action: test + + - name: generate code coverage report + uses: threeal/gcovr-action@v1.0.0 + with: + coveralls-send: true + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-project-version.yml b/.github/workflows/update-project-version.yml new file mode 100644 index 000000000..c47e53790 --- /dev/null +++ b/.github/workflows/update-project-version.yml @@ -0,0 +1,118 @@ +name: "update project version" + +on: + workflow_dispatch: + inputs: + target_version: + description: 'next version (e.g., 1.9.8)' + required: true + default: '1.9.8' + target_soversion: + description: 'next soversion (e.g., 28). leave blank to keep current.' + required: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + bump-and-verify: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: checkout code + uses: actions/checkout@v4 + + - name: update project files + run: | + VER="${{ github.event.inputs.target_version }}" + SOVER="${{ github.event.inputs.target_soversion }}" + echo "bumping to $VER" + + # 1. cmakelists.txt + if [ -f CMakeLists.txt ]; then + sed -i "/project.*jsoncpp/,/)/ s/VERSION [0-9.]*/VERSION $VER/" CMakeLists.txt + if [ -n "$SOVER" ]; then + sed -i "s/set(PROJECT_SOVERSION [0-9]*/set(PROJECT_SOVERSION $SOVER/" CMakeLists.txt + fi + fi + + # 2. meson.build + if [ -f meson.build ]; then + # targeting the version line directly + sed -i "s/version[[:space:]]*:[[:space:]]*['\"][0-9.]*['\"]/version : '$VER'/" meson.build + if [ -n "$SOVER" ]; then + sed -i "s/soversion[[:space:]]*:[[:space:]]*['\"][0-9]*['\"]/soversion : '$SOVER'/" meson.build + fi + fi + + # 3. module.bazel + if [ -f MODULE.bazel ]; then + # match only the first 'version' occurrence in the file (the module version) + sed -i "0,/version[[:space:]]*=[[:space:]]*['\"][0-9.]*['\"]/s//version = \"$VER\"/" MODULE.bazel + fi + + # 4. vcpkg.json + if [ -f vcpkg.json ]; then + jq --arg ver "$VER" '.version = $ver' vcpkg.json > tmp.json && mv tmp.json vcpkg.json + fi + + # 5. include/json/version.h + if [ -f include/json/version.h ]; then + MAJOR=$(echo "$VER" | cut -d. -f1) + MINOR=$(echo "$VER" | cut -d. -f2) + PATCH=$(echo "$VER" | cut -d. -f3) + + sed -i "s/#define JSONCPP_VERSION_STRING \".*\"/#define JSONCPP_VERSION_STRING \"$VER\"/" include/json/version.h + sed -i "s/#define JSONCPP_VERSION_MAJOR [0-9]*/#define JSONCPP_VERSION_MAJOR $MAJOR/" include/json/version.h + sed -i "s/#define JSONCPP_VERSION_MINOR [0-9]*/#define JSONCPP_VERSION_MINOR $MINOR/" include/json/version.h + sed -i "s/#define JSONCPP_VERSION_PATCH [0-9]*/#define JSONCPP_VERSION_PATCH $PATCH/" include/json/version.h + fi + + - name: verify version macros + id: verify + run: | + FILE="include/json/version.h" + if [ -f "$FILE" ]; then + # extract clean values by stripping everything except digits and dots + V_STR=$(grep "JSONCPP_VERSION_STRING" "$FILE" | head -n 1 | cut -d '"' -f 2) + V_MAJ=$(grep "JSONCPP_VERSION_MAJOR" "$FILE" | head -n 1 | awk '{print $3}' | tr -cd '0-9') + V_MIN=$(grep "JSONCPP_VERSION_MINOR" "$FILE" | head -n 1 | awk '{print $3}' | tr -cd '0-9') + V_PAT=$(grep "JSONCPP_VERSION_PATCH" "$FILE" | head -n 1 | awk '{print $3}' | tr -cd '0-9') + + # create a unique delimiter for the multi-line output + DELIM=$(dd if=/dev/urandom bs=15 count=1 2>/dev/null | base64) + echo "report<<$DELIM" >> $GITHUB_OUTPUT + echo "| Macro | Value |" >> $GITHUB_OUTPUT + echo "| :--- | :--- |" >> $GITHUB_OUTPUT + echo "| STRING | \`$V_STR\` |" >> $GITHUB_OUTPUT + echo "| MAJOR | \`$V_MAJ\` |" >> $GITHUB_OUTPUT + echo "| MINOR | \`$V_MIN\` |" >> $GITHUB_OUTPUT + echo "| PATCH | \`$V_PAT\` |" >> $GITHUB_OUTPUT + echo "$DELIM" >> $GITHUB_OUTPUT + fi + + - name: sanity check (cmake configure) + run: | + if [ -f CMakeLists.txt ]; then + mkdir build_check && cd build_check + cmake .. -DJSONCPP_WITH_TESTS=OFF -DJSONCPP_WITH_POST_BUILD_UNITTEST=OFF + cd .. && rm -rf build_check + fi + + - name: create pull request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "chore: bump version to ${{ github.event.inputs.target_version }}" + branch: "bump-to-${{ github.event.inputs.target_version }}" + title: "chore: bump version to ${{ github.event.inputs.target_version }}" + body: | + automated version bump. + - new version: `${{ github.event.inputs.target_version }}` + - new soversion: `${{ github.event.inputs.target_soversion || 'no change' }}` + + ### header verification + ${{ steps.verify.outputs.report }} + labels: "maintenance" diff --git a/.gitignore b/.gitignore index 91121c230..69868f413 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /libs/ /doc/doxyfile /dist/ +/.cache/ # MSVC project files: *.sln @@ -28,9 +29,9 @@ # CMake-generated files: CMakeFiles/ -*.cmake /pkg-config/jsoncpp.pc jsoncpp_lib_static.dir/ +compile_commands.json # In case someone runs cmake in the root-dir: /CMakeCache.txt @@ -51,3 +52,10 @@ jsoncpp_lib_static.dir/ # DS_Store .DS_Store + +# temps +/version + +# Bazel output paths +/bazel-* +/MODULE.bazel.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0554dc93b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Build matrix / environment variables are explained on: -# http://about.travis-ci.org/docs/user/build-configuration/ -# This file can be validated on: http://www.yamllint.com/ -# Or using the Ruby based travel command line tool: -# gem install travis --no-rdoc --no-ri -# travis lint .travis.yml -language: cpp -sudo: false -addons: - homebrew: - packages: - - clang-format - - meson - - ninja - update: false # do not update homebrew by default - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-xenial-8 - packages: - - clang-format-8 - - clang-8 - - valgrind -matrix: - allow_failures: - - os: osx - include: - - name: Mac clang meson static release testing - os: osx - osx_image: xcode11 - compiler: clang - env: - CXX="clang++" - CC="clang" - LIB_TYPE=static - BUILD_TYPE=release - script: ./.travis_scripts/meson_builder.sh - - name: Linux xenial clang meson static release testing - os: linux - dist: xenial - compiler: clang - env: - CXX="clang++" - CC="clang" - LIB_TYPE=static - BUILD_TYPE=release - # before_install and install steps only needed for linux meson builds - before_install: - - source ./.travis_scripts/travis.before_install.${TRAVIS_OS_NAME}.sh - install: - - source ./.travis_scripts/travis.install.${TRAVIS_OS_NAME}.sh - script: ./.travis_scripts/meson_builder.sh - - name: Linux xenial gcc cmake coverage - os: linux - dist: xenial - compiler: gcc - env: - CXX=g++ - CC=gcc - DO_Coverage=ON - BUILD_TOOL="Unix Makefiles" - BUILD_TYPE=Debug - LIB_TYPE=shared - DESTDIR=/tmp/cmake_json_cpp - before_install: - - pip install --user cpp-coveralls - script: ./.travis_scripts/cmake_builder.sh - after_success: - - coveralls --include src/lib_json --include include -notifications: - email: false diff --git a/.travis_scripts/cmake_builder.sh b/.travis_scripts/cmake_builder.sh deleted file mode 100755 index ccb33312e..000000000 --- a/.travis_scripts/cmake_builder.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env sh -# This script can be used on the command line directly to configure several -# different build environments. -# This is called by `.travis.yml` via Travis CI. -# Travis supplies $TRAVIS_OS_NAME. -# http://docs.travis-ci.com/user/multi-os/ -# Our .travis.yml also defines: - -# - BUILD_TYPE=Release/Debug -# - LIB_TYPE=static/shared -# -# Optional environmental variables -# - DESTDIR <- used for setting the install prefix -# - BUILD_TOOL=["Unix Makefile"|"Ninja"] -# - BUILDNAME <- how to identify this build on the dashboard -# - DO_MemCheck <- if set, try to use valgrind -# - DO_Coverage <- if set, try to do dashboard coverage testing -# - -env_set=1 -if ${BUILD_TYPE+false}; then - echo "BUILD_TYPE not set in environment." - env_set=0 -fi -if ${LIB_TYPE+false}; then - echo "LIB_TYPE not set in environment." - env_set=0 -fi -if ${CXX+false}; then - echo "CXX not set in environment." - env_set=0 -fi - - -if [ ${env_set} -eq 0 ]; then - echo "USAGE: CXX=$(which clang++) BUILD_TYPE=[Release|Debug] LIB_TYPE=[static|shared] $0" - echo "" - echo "Examples:" - echo " CXX=$(which clang++) BUILD_TYPE=Release LIB_TYPE=shared DESTDIR=/tmp/cmake_json_cpp $0" - echo " CXX=$(which clang++) BUILD_TYPE=Debug LIB_TYPE=shared DESTDIR=/tmp/cmake_json_cpp $0" - echo " CXX=$(which clang++) BUILD_TYPE=Release LIB_TYPE=static DESTDIR=/tmp/cmake_json_cpp $0" - echo " CXX=$(which clang++) BUILD_TYPE=Debug LIB_TYPE=static DESTDIR=/tmp/cmake_json_cpp $0" - - echo " CXX=$(which g++) BUILD_TYPE=Release LIB_TYPE=shared DESTDIR=/tmp/cmake_json_cpp $0" - echo " CXX=$(which g++) BUILD_TYPE=Debug LIB_TYPE=shared DESTDIR=/tmp/cmake_json_cpp $0" - echo " CXX=$(which g++) BUILD_TYPE=Release LIB_TYPE=static DESTDIR=/tmp/cmake_json_cpp $0" - echo " CXX=$(which g++) BUILD_TYPE=Debug LIB_TYPE=static DESTDIR=/tmp/cmake_json_cpp $0" - - exit -1 -fi - -if ${DESTDIR+false}; then - DESTDIR="/usr/local" -fi - -# -e: fail on error -# -v: show commands -# -x: show expanded commands -set -vex - -env | sort - -which cmake -cmake --version - -echo ${CXX} -${CXX} --version -_COMPILER_NAME=`basename ${CXX}` -if [ "${BUILD_TYPE}" == "shared" ]; then - _CMAKE_BUILD_SHARED_LIBS=ON -else - _CMAKE_BUILD_SHARED_LIBS=OFF -fi - -CTEST_TESTING_OPTION="-D ExperimentalTest" -# - DO_MemCheck <- if set, try to use valgrind -if ! ${DO_MemCheck+false}; then - valgrind --version - CTEST_TESTING_OPTION="-D ExperimentalMemCheck" -else -# - DO_Coverage <- if set, try to do dashboard coverage testing - if ! ${DO_Coverage+false}; then - export CXXFLAGS="-fprofile-arcs -ftest-coverage" - export LDFLAGS="-fprofile-arcs -ftest-coverage" - CTEST_TESTING_OPTION="-D ExperimentalTest -D ExperimentalCoverage" - #gcov --version - fi -fi - -# Ninja = Generates build.ninja files. -if ${BUILD_TOOL+false}; then - BUILD_TOOL="Ninja" - export _BUILD_EXE=ninja - which ninja - ninja --version -else -# Unix Makefiles = Generates standard UNIX makefiles. - export _BUILD_EXE=make -fi - -_BUILD_DIR_NAME="build-cmake_${BUILD_TYPE}_${LIB_TYPE}_${_COMPILER_NAME}_${_BUILD_EXE}" -mkdir -p ${_BUILD_DIR_NAME} -cd "${_BUILD_DIR_NAME}" - if ${BUILDNAME+false}; then - _HOSTNAME=`hostname -s` - BUILDNAME="${_HOSTNAME}_${BUILD_TYPE}_${LIB_TYPE}_${_COMPILER_NAME}_${_BUILD_EXE}" - fi - cmake \ - -G "${BUILD_TOOL}" \ - -DBUILDNAME:STRING="${BUILDNAME}" \ - -DCMAKE_CXX_COMPILER:PATH=${CXX} \ - -DCMAKE_BUILD_TYPE:STRING=${BUILD_TYPE} \ - -DBUILD_SHARED_LIBS:BOOL=${_CMAKE_BUILD_SHARED_LIBS} \ - -DCMAKE_INSTALL_PREFIX:PATH=${DESTDIR} \ - ../ - - ctest -C ${BUILD_TYPE} -D ExperimentalStart -D ExperimentalConfigure -D ExperimentalBuild ${CTEST_TESTING_OPTION} -D ExperimentalSubmit - # Final step is to verify that installation succeeds - cmake --build . --config ${BUILD_TYPE} --target install - - if [ "${DESTDIR}" != "/usr/local" ]; then - ${_BUILD_EXE} install - fi -cd - - -if ${CLEANUP+false}; then - echo "Skipping cleanup: build directory will persist." -else - rm -r "${_BUILD_DIR_NAME}" -fi diff --git a/.travis_scripts/meson_builder.sh b/.travis_scripts/meson_builder.sh deleted file mode 100755 index 1fdd8f65d..000000000 --- a/.travis_scripts/meson_builder.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env sh -# This script can be used on the command line directly to configure several -# different build environments. -# This is called by `.travis.yml` via Travis CI. -# Travis supplies $TRAVIS_OS_NAME. -# http://docs.travis-ci.com/user/multi-os/ -# Our .travis.yml also defines: - -# - BUILD_TYPE=release/debug -# - LIB_TYPE=static/shared - -env_set=1 -if ${BUILD_TYPE+false}; then - echo "BUILD_TYPE not set in environment." - env_set=0 -fi -if ${LIB_TYPE+false}; then - echo "LIB_TYPE not set in environment." - env_set=0 -fi -if ${CXX+false}; then - echo "CXX not set in environment." - env_set=0 -fi - - -if [ ${env_set} -eq 0 ]; then - echo "USAGE: CXX=$(which clang++) BUILD_TYPE=[release|debug] LIB_TYPE=[static|shared] $0" - echo "" - echo "Examples:" - echo " CXX=$(which clang++) BUILD_TYPE=release LIB_TYPE=shared DESTDIR=/tmp/meson_json_cpp $0" - echo " CXX=$(which clang++) BUILD_TYPE=debug LIB_TYPE=shared DESTDIR=/tmp/meson_json_cpp $0" - echo " CXX=$(which clang++) BUILD_TYPE=release LIB_TYPE=static DESTDIR=/tmp/meson_json_cpp $0" - echo " CXX=$(which clang++) BUILD_TYPE=debug LIB_TYPE=static DESTDIR=/tmp/meson_json_cpp $0" - - echo " CXX=$(which g++) BUILD_TYPE=release LIB_TYPE=shared DESTDIR=/tmp/meson_json_cpp $0" - echo " CXX=$(which g++) BUILD_TYPE=debug LIB_TYPE=shared DESTDIR=/tmp/meson_json_cpp $0" - echo " CXX=$(which g++) BUILD_TYPE=release LIB_TYPE=static DESTDIR=/tmp/meson_json_cpp $0" - echo " CXX=$(which g++) BUILD_TYPE=debug LIB_TYPE=static DESTDIR=/tmp/meson_json_cpp $0" - - exit -1 -fi - -if ${DESTDIR+false}; then - DESTDIR="/usr/local" -fi - -# -e: fail on error -# -v: show commands -# -x: show expanded commands -set -vex - - -env | sort - -which python3 -which meson -which ninja -echo ${CXX} -${CXX} --version -python3 --version -meson --version -ninja --version -_COMPILER_NAME=`basename ${CXX}` -_BUILD_DIR_NAME="build-${BUILD_TYPE}_${LIB_TYPE}_${_COMPILER_NAME}" - -./.travis_scripts/run-clang-format.sh -meson --fatal-meson-warnings --werror --buildtype ${BUILD_TYPE} --default-library ${LIB_TYPE} . "${_BUILD_DIR_NAME}" -ninja -v -j 2 -C "${_BUILD_DIR_NAME}" - -cd "${_BUILD_DIR_NAME}" - meson test --no-rebuild --print-errorlogs - - if [ "${DESTDIR}" != "/usr/local" ]; then - ninja install - fi -cd - - -if ${CLEANUP+false}; then - echo "Skipping cleanup: build directory will persist." -else - rm -r "${_BUILD_DIR_NAME}" -fi diff --git a/.travis_scripts/run-clang-format.py b/.travis_scripts/run-clang-format.py deleted file mode 100755 index 68179aafd..000000000 --- a/.travis_scripts/run-clang-format.py +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env python -"""A wrapper script around clang-format, suitable for linting multiple files -and to use for continuous integration. -This is an alternative API for the clang-format command line. -It runs over multiple files and directories in parallel. -A diff output is produced and a sensible exit code is returned. - -NOTE: pulled from https://github.com/Sarcasm/run-clang-format, which is -licensed under the MIT license. -""" - -from __future__ import print_function, unicode_literals - -import argparse -import codecs -import difflib -import fnmatch -import io -import multiprocessing -import os -import signal -import subprocess -import sys -import traceback - -from functools import partial - -try: - from subprocess import DEVNULL # py3k -except ImportError: - DEVNULL = open(os.devnull, "wb") - - -DEFAULT_EXTENSIONS = 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx' - - -class ExitStatus: - SUCCESS = 0 - DIFF = 1 - TROUBLE = 2 - - -def list_files(files, recursive=False, extensions=None, exclude=None): - if extensions is None: - extensions = [] - if exclude is None: - exclude = [] - - out = [] - for file in files: - if recursive and os.path.isdir(file): - for dirpath, dnames, fnames in os.walk(file): - fpaths = [os.path.join(dirpath, fname) for fname in fnames] - for pattern in exclude: - # os.walk() supports trimming down the dnames list - # by modifying it in-place, - # to avoid unnecessary directory listings. - dnames[:] = [ - x for x in dnames - if - not fnmatch.fnmatch(os.path.join(dirpath, x), pattern) - ] - fpaths = [ - x for x in fpaths if not fnmatch.fnmatch(x, pattern) - ] - for f in fpaths: - ext = os.path.splitext(f)[1][1:] - if ext in extensions: - out.append(f) - else: - out.append(file) - return out - - -def make_diff(file, original, reformatted): - return list( - difflib.unified_diff( - original, - reformatted, - fromfile='{}\t(original)'.format(file), - tofile='{}\t(reformatted)'.format(file), - n=3)) - - -class DiffError(Exception): - def __init__(self, message, errs=None): - super(DiffError, self).__init__(message) - self.errs = errs or [] - - -class UnexpectedError(Exception): - def __init__(self, message, exc=None): - super(UnexpectedError, self).__init__(message) - self.formatted_traceback = traceback.format_exc() - self.exc = exc - - -def run_clang_format_diff_wrapper(args, file): - try: - ret = run_clang_format_diff(args, file) - return ret - except DiffError: - raise - except Exception as e: - raise UnexpectedError('{}: {}: {}'.format(file, e.__class__.__name__, - e), e) - - -def run_clang_format_diff(args, file): - try: - with io.open(file, 'r', encoding='utf-8') as f: - original = f.readlines() - except IOError as exc: - raise DiffError(str(exc)) - invocation = [args.clang_format_executable, file] - - # Use of utf-8 to decode the process output. - # - # Hopefully, this is the correct thing to do. - # - # It's done due to the following assumptions (which may be incorrect): - # - clang-format will returns the bytes read from the files as-is, - # without conversion, and it is already assumed that the files use utf-8. - # - if the diagnostics were internationalized, they would use utf-8: - # > Adding Translations to Clang - # > - # > Not possible yet! - # > Diagnostic strings should be written in UTF-8, - # > the client can translate to the relevant code page if needed. - # > Each translation completely replaces the format string - # > for the diagnostic. - # > -- http://clang.llvm.org/docs/InternalsManual.html#internals-diag-translation - # - # It's not pretty, due to Python 2 & 3 compatibility. - encoding_py3 = {} - if sys.version_info[0] >= 3: - encoding_py3['encoding'] = 'utf-8' - - try: - proc = subprocess.Popen( - invocation, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - **encoding_py3) - except OSError as exc: - raise DiffError( - "Command '{}' failed to start: {}".format( - subprocess.list2cmdline(invocation), exc - ) - ) - proc_stdout = proc.stdout - proc_stderr = proc.stderr - if sys.version_info[0] < 3: - # make the pipes compatible with Python 3, - # reading lines should output unicode - encoding = 'utf-8' - proc_stdout = codecs.getreader(encoding)(proc_stdout) - proc_stderr = codecs.getreader(encoding)(proc_stderr) - # hopefully the stderr pipe won't get full and block the process - outs = list(proc_stdout.readlines()) - errs = list(proc_stderr.readlines()) - proc.wait() - if proc.returncode: - raise DiffError( - "Command '{}' returned non-zero exit status {}".format( - subprocess.list2cmdline(invocation), proc.returncode - ), - errs, - ) - return make_diff(file, original, outs), errs - - -def bold_red(s): - return '\x1b[1m\x1b[31m' + s + '\x1b[0m' - - -def colorize(diff_lines): - def bold(s): - return '\x1b[1m' + s + '\x1b[0m' - - def cyan(s): - return '\x1b[36m' + s + '\x1b[0m' - - def green(s): - return '\x1b[32m' + s + '\x1b[0m' - - def red(s): - return '\x1b[31m' + s + '\x1b[0m' - - for line in diff_lines: - if line[:4] in ['--- ', '+++ ']: - yield bold(line) - elif line.startswith('@@ '): - yield cyan(line) - elif line.startswith('+'): - yield green(line) - elif line.startswith('-'): - yield red(line) - else: - yield line - - -def print_diff(diff_lines, use_color): - if use_color: - diff_lines = colorize(diff_lines) - if sys.version_info[0] < 3: - sys.stdout.writelines((l.encode('utf-8') for l in diff_lines)) - else: - sys.stdout.writelines(diff_lines) - - -def print_trouble(prog, message, use_colors): - error_text = 'error:' - if use_colors: - error_text = bold_red(error_text) - print("{}: {} {}".format(prog, error_text, message), file=sys.stderr) - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - '--clang-format-executable', - metavar='EXECUTABLE', - help='path to the clang-format executable', - default='clang-format') - parser.add_argument( - '--extensions', - help='comma separated list of file extensions (default: {})'.format( - DEFAULT_EXTENSIONS), - default=DEFAULT_EXTENSIONS) - parser.add_argument( - '-r', - '--recursive', - action='store_true', - help='run recursively over directories') - parser.add_argument('files', metavar='file', nargs='+') - parser.add_argument( - '-q', - '--quiet', - action='store_true') - parser.add_argument( - '-j', - metavar='N', - type=int, - default=0, - help='run N clang-format jobs in parallel' - ' (default number of cpus + 1)') - parser.add_argument( - '--color', - default='auto', - choices=['auto', 'always', 'never'], - help='show colored diff (default: auto)') - parser.add_argument( - '-e', - '--exclude', - metavar='PATTERN', - action='append', - default=[], - help='exclude paths matching the given glob-like pattern(s)' - ' from recursive search') - - args = parser.parse_args() - - # use default signal handling, like diff return SIGINT value on ^C - # https://bugs.python.org/issue14229#msg156446 - signal.signal(signal.SIGINT, signal.SIG_DFL) - try: - signal.SIGPIPE - except AttributeError: - # compatibility, SIGPIPE does not exist on Windows - pass - else: - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - - colored_stdout = False - colored_stderr = False - if args.color == 'always': - colored_stdout = True - colored_stderr = True - elif args.color == 'auto': - colored_stdout = sys.stdout.isatty() - colored_stderr = sys.stderr.isatty() - - version_invocation = [args.clang_format_executable, str("--version")] - try: - subprocess.check_call(version_invocation, stdout=DEVNULL) - except subprocess.CalledProcessError as e: - print_trouble(parser.prog, str(e), use_colors=colored_stderr) - return ExitStatus.TROUBLE - except OSError as e: - print_trouble( - parser.prog, - "Command '{}' failed to start: {}".format( - subprocess.list2cmdline(version_invocation), e - ), - use_colors=colored_stderr, - ) - return ExitStatus.TROUBLE - - retcode = ExitStatus.SUCCESS - files = list_files( - args.files, - recursive=args.recursive, - exclude=args.exclude, - extensions=args.extensions.split(',')) - - if not files: - return - - njobs = args.j - if njobs == 0: - njobs = multiprocessing.cpu_count() + 1 - njobs = min(len(files), njobs) - - if njobs == 1: - # execute directly instead of in a pool, - # less overhead, simpler stacktraces - it = (run_clang_format_diff_wrapper(args, file) for file in files) - pool = None - else: - pool = multiprocessing.Pool(njobs) - it = pool.imap_unordered( - partial(run_clang_format_diff_wrapper, args), files) - while True: - try: - outs, errs = next(it) - except StopIteration: - break - except DiffError as e: - print_trouble(parser.prog, str(e), use_colors=colored_stderr) - retcode = ExitStatus.TROUBLE - sys.stderr.writelines(e.errs) - except UnexpectedError as e: - print_trouble(parser.prog, str(e), use_colors=colored_stderr) - sys.stderr.write(e.formatted_traceback) - retcode = ExitStatus.TROUBLE - # stop at the first unexpected error, - # something could be very wrong, - # don't process all files unnecessarily - if pool: - pool.terminate() - break - else: - sys.stderr.writelines(errs) - if outs == []: - continue - if not args.quiet: - print_diff(outs, use_color=colored_stdout) - if retcode == ExitStatus.SUCCESS: - retcode = ExitStatus.DIFF - return retcode - - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file diff --git a/.travis_scripts/run-clang-format.sh b/.travis_scripts/run-clang-format.sh deleted file mode 100755 index 91972840d..000000000 --- a/.travis_scripts/run-clang-format.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -python $DIR/run-clang-format.py -r $DIR/../src/**/ $DIR/../include/**/ \ No newline at end of file diff --git a/.travis_scripts/travis.before_install.linux.sh b/.travis_scripts/travis.before_install.linux.sh deleted file mode 100644 index 9b556de15..000000000 --- a/.travis_scripts/travis.before_install.linux.sh +++ /dev/null @@ -1,8 +0,0 @@ -set -vex - -# Preinstalled versions of python are dependent on which Ubuntu distribution -# you are running. The below version needs to be updated whenever we roll -# the Ubuntu version used in Travis. -# https://docs.travis-ci.com/user/languages/python/ - -pyenv global 3.7.1 diff --git a/.travis_scripts/travis.before_install.osx.sh b/.travis_scripts/travis.before_install.osx.sh deleted file mode 100644 index 5d83c0c71..000000000 --- a/.travis_scripts/travis.before_install.osx.sh +++ /dev/null @@ -1 +0,0 @@ -# NOTHING TO DO HERE diff --git a/.travis_scripts/travis.install.linux.sh b/.travis_scripts/travis.install.linux.sh deleted file mode 100644 index 7c5846f1a..000000000 --- a/.travis_scripts/travis.install.linux.sh +++ /dev/null @@ -1,10 +0,0 @@ -set -vex - -wget https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-linux.zip -unzip -q ninja-linux.zip -d build - -pip3 install meson -echo ${PATH} -ls /usr/local -ls /usr/local/bin -export PATH="${PWD}"/build:/usr/local/bin:/usr/bin:${PATH} diff --git a/.travis_scripts/travis.install.osx.sh b/.travis_scripts/travis.install.osx.sh deleted file mode 100644 index 5d83c0c71..000000000 --- a/.travis_scripts/travis.install.osx.sh +++ /dev/null @@ -1 +0,0 @@ -# NOTHING TO DO HERE diff --git a/AUTHORS b/AUTHORS index 3723d546a..7a3def276 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,11 +16,12 @@ Baruch Siach Ben Boeckel Benjamin Knecht Bernd Kuhls -Billy Donahue +Billy Donahue Braden McDorman Brandon Myers Brendan Drew chason +chenguoping Chris Gilling Christopher Dawes Christopher Dunn @@ -97,6 +98,7 @@ selaselah Sergiy80 sergzub Stefan Schweter +Stefano Fiorentino Steffen Kieß Steven Hahn Stuart Eichert diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 000000000..45f7a21b3 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,63 @@ +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +licenses(["unencumbered"]) # Public Domain or MIT + +exports_files(["LICENSE"]) + +bool_flag( + name = "use_exception", + build_setting_default = False, +) + +config_setting( + name = "use_exception_cfg", + flag_values = {":use_exception": "true"}, +) + +bool_flag( + name = "has_int64", + build_setting_default = True, +) + +config_setting( + name = "has_int64_cfg", + flag_values = {":has_int64": "true"}, +) + +cc_library( + name = "jsoncpp", + srcs = [ + "src/lib_json/json_reader.cpp", + "src/lib_json/json_tool.h", + "src/lib_json/json_value.cpp", + "src/lib_json/json_writer.cpp", + ], + hdrs = [ + "include/json/allocator.h", + "include/json/assertions.h", + "include/json/config.h", + "include/json/forwards.h", + "include/json/json.h", + "include/json/json_features.h", + "include/json/reader.h", + "include/json/value.h", + "include/json/version.h", + "include/json/writer.h", + ], + defines = select({ + ":use_exception_cfg": ["JSON_USE_EXCEPTION=1"], + "//conditions:default": ["JSON_USE_EXCEPTION=0"], + }) + select({ + ":has_int64_cfg": ["JSON_HAS_INT64"], + "//conditions:default": [], + }), + includes = ["include"], + visibility = ["//visibility:public"], + deps = [":private"], +) + +cc_library( + name = "private", + textual_hdrs = ["src/lib_json/json_valueiterator.inl"], +) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e0633173..c1de7aefc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,13 +6,13 @@ # policies that provide successful builds. By setting JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION # to a value greater than the oldest policies, all policies between # JSONCPP_OLDEST_VALIDATED_POLICIES_VERSION and CMAKE_VERSION (used for this build) -# are set to their NEW behaivor, thereby suppressing policy warnings related to policies +# are set to their NEW behavior, thereby suppressing policy warnings related to policies # between the JSONCPP_OLDEST_VALIDATED_POLICIES_VERSION and CMAKE_VERSION. # # CMake versions greater than the JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION policies will # continue to generate policy warnings "CMake Warning (dev)...Policy CMP0XXX is not set:" # -set(JSONCPP_OLDEST_VALIDATED_POLICIES_VERSION "3.8.0") +set(JSONCPP_OLDEST_VALIDATED_POLICIES_VERSION "3.10.0") set(JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION "3.13.2") cmake_minimum_required(VERSION ${JSONCPP_OLDEST_VALIDATED_POLICIES_VERSION}) if("${CMAKE_VERSION}" VERSION_LESS "${JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION}") @@ -22,6 +22,9 @@ else() set(JSONCPP_CMAKE_POLICY_VERSION "${JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION}") endif() cmake_policy(VERSION ${JSONCPP_CMAKE_POLICY_VERSION}) +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) +endif() # # Now enumerate specific policies newer than JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION # that may need to be individually set to NEW/OLD @@ -37,44 +40,31 @@ foreach(pold "") # Currently Empty endif() endforeach() -# ==== Define language standard configurations requiring at least c++11 standard -if(CMAKE_CXX_STANDARD EQUAL "98" ) - message(FATAL_ERROR "CMAKE_CXX_STANDARD:STRING=98 is not supported.") -endif() - -##### -## Set the default target properties -if(NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 11) # Supported values are ``11``, ``14``, and ``17``. -endif() -if(NOT CMAKE_CXX_STANDARD_REQUIRED) - set(CMAKE_CXX_STANDARD_REQUIRED ON) -endif() -if(NOT CMAKE_CXX_EXTENSIONS) - set(CMAKE_CXX_EXTENSIONS OFF) -endif() - -# ==== - -# Ensures that CMAKE_BUILD_TYPE has a default value -if(NOT DEFINED CMAKE_BUILD_TYPE) +# Ensure that CMAKE_BUILD_TYPE has a value specified for single configuration generators. +if(NOT DEFINED CMAKE_BUILD_TYPE AND NOT DEFINED CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel Coverage.") endif() -project(JSONCPP - # Note: version must be updated in three places when doing a release. This +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +project(jsoncpp + # Note: version must be updated in four places when doing a release. This # annoying process ensures that amalgamate, CMake, and meson all report the # correct version. - # 1. /meson.build - # 2. /include/json/version.h - # 3. /CMakeLists.txt - # IMPORTANT: also update the SOVERSION!! - VERSION 1.9.2 # [.[.[.]]] + # 1. ./meson.build + # 2. ./include/json/version.h + # 3. ./CMakeLists.txt + # 4. ./MODULE.bazel + # IMPORTANT: also update the PROJECT_SOVERSION!! + VERSION 1.9.8 # [.[.[.]]] LANGUAGES CXX) -message(STATUS "JsonCpp Version: ${JSONCPP_VERSION_MAJOR}.${JSONCPP_VERSION_MINOR}.${JSONCPP_VERSION_PATCH}") -set( JSONCPP_SOVERSION 22 ) +message(STATUS "JsonCpp Version: ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") +set(PROJECT_SOVERSION 27) + +include(${CMAKE_CURRENT_SOURCE_DIR}/include/PreventInSourceBuilds.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/include/PreventInBuildInstalls.cmake) option(JSONCPP_WITH_TESTS "Compile and (for jsoncpp_check) run JsonCpp test executables" ON) option(JSONCPP_WITH_POST_BUILD_UNITTEST "Automatically run unit-tests as a post build step" ON) @@ -83,120 +73,100 @@ option(JSONCPP_WITH_STRICT_ISO "Issue all the warnings demanded by strict ISO C option(JSONCPP_WITH_PKGCONFIG_SUPPORT "Generate and install .pc files" ON) option(JSONCPP_WITH_CMAKE_PACKAGE "Generate and install cmake package files" ON) option(JSONCPP_WITH_EXAMPLE "Compile JsonCpp example" OFF) -option(BUILD_SHARED_LIBS "Build jsoncpp_lib as a shared library." OFF) - -# Enable runtime search path support for dynamic libraries on OSX -if(APPLE) - set(CMAKE_MACOSX_RPATH 1) -endif() +option(JSONCPP_WITH_INSTALL "Include JsonCpp header and binaries in the install target" ON) +option(JSONCPP_STATIC_WINDOWS_RUNTIME "Use static (MT/MTd) Windows runtime" OFF) +option(BUILD_SHARED_LIBS "Build jsoncpp_lib as a shared library." ON) +option(BUILD_STATIC_LIBS "Build jsoncpp_lib as a static library." ON) +option(BUILD_OBJECT_LIBS "Build jsoncpp_lib as a object library." ON) # Adhere to GNU filesystem layout conventions include(GNUInstallDirs) -set(DEBUG_LIBNAME_SUFFIX "" CACHE STRING "Optional suffix to append to the library name for a debug build") +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" CACHE PATH "Archive output dir.") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" CACHE PATH "Library output dir.") + set(CMAKE_PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" CACHE PATH "PDB (MSVC debug symbol)output dir.") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" CACHE PATH "Executable/dll output dir.") +endif() -set(JSONCPP_USE_SECURE_MEMORY "0" CACHE STRING "-D...=1 to use memory-wiping allocator for STL" ) +include(CheckFunctionExists) +check_function_exists(memset_s HAVE_MEMSET_S) +if(HAVE_MEMSET_S) + add_definitions("-DHAVE_MEMSET_S=1") +endif() -configure_file( "${PROJECT_SOURCE_DIR}/version.in" - "${PROJECT_BINARY_DIR}/version" - NEWLINE_STYLE UNIX ) +if(JSONCPP_USE_SECURE_MEMORY) + add_definitions("-DJSONCPP_USE_SECURE_MEMORY=1") +endif() + +configure_file("${PROJECT_SOURCE_DIR}/version.in" + "${PROJECT_BINARY_DIR}/version" + NEWLINE_STYLE UNIX) -macro(UseCompilationWarningAsError) +macro(use_compilation_warning_as_error) if(MSVC) # Only enabled in debug because some old versions of VS STL generate # warnings when compiled in release configuration. - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_options($<$:/WX>) - else() - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /WX ") - endif() + add_compile_options($<$:/WX>) elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_options(-Werror) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") - endif() + add_compile_options(-Werror) if(JSONCPP_WITH_STRICT_ISO) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) add_compile_options(-pedantic-errors) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic-errors") - endif() endif() endif() endmacro() # Include our configuration header -include_directories( ${jsoncpp_SOURCE_DIR}/include ) +include_directories(${jsoncpp_SOURCE_DIR}/include) if(MSVC) # Only enabled in debug because some old versions of VS STL generate # unreachable code warning when compiled in release configuration. - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_options($<$:/W4>) - else() - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /W4 ") + add_compile_options($<$:/W4>) + if (JSONCPP_STATIC_WINDOWS_RUNTIME) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endif() endif() if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # using regular Clang or AppleClang - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_options(-Wall -Wconversion -Wshadow -Werror=conversion -Werror=sign-compare) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wconversion -Wshadow -Werror=conversion -Werror=sign-compare") + add_compile_options(-Wall -Wconversion -Wshadow) + + if(JSONCPP_WITH_WARNING_AS_ERROR) + add_compile_options(-Werror=conversion -Werror=sign-compare) endif() elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # using GCC - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_options(-Wall -Wconversion -Wshadow -Wextra) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wconversion -Wshadow -Wextra") - endif() + add_compile_options(-Wall -Wconversion -Wshadow -Wextra) # not yet ready for -Wsign-conversion if(JSONCPP_WITH_STRICT_ISO) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_options(-pedantic) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic") - endif() + add_compile_options(-Wpedantic) endif() if(JSONCPP_WITH_WARNING_AS_ERROR) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) add_compile_options(-Werror=conversion) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=conversion") - endif() endif() elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Intel") # using Intel compiler - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_options(-Wall -Wconversion -Wshadow -Wextra -Werror=conversion) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wconversion -Wshadow -Wextra -Werror=conversion") - endif() + add_compile_options(-Wall -Wconversion -Wshadow -Wextra) - if(JSONCPP_WITH_STRICT_ISO AND NOT JSONCPP_WITH_WARNING_AS_ERROR) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_options(-pedantic) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic") - endif() + if(JSONCPP_WITH_WARNING_AS_ERROR) + add_compile_options(-Werror=conversion) + elseif(JSONCPP_WITH_STRICT_ISO) + add_compile_options(-Wpedantic) endif() endif() -find_program(CCACHE_FOUND ccache) -if(CCACHE_FOUND) - set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) - set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) -endif(CCACHE_FOUND) - if(JSONCPP_WITH_WARNING_AS_ERROR) - UseCompilationWarningAsError() + use_compilation_warning_as_error() endif() if(JSONCPP_WITH_PKGCONFIG_SUPPORT) + include(JoinPaths) + + join_paths(libdir_for_pc_file "\${exec_prefix}" "${CMAKE_INSTALL_LIBDIR}") + join_paths(includedir_for_pc_file "\${prefix}" "${CMAKE_INSTALL_INCLUDEDIR}") + configure_file( "pkg-config/jsoncpp.pc.in" "pkg-config/jsoncpp.pc" @@ -206,29 +176,34 @@ if(JSONCPP_WITH_PKGCONFIG_SUPPORT) endif() if(JSONCPP_WITH_CMAKE_PACKAGE) - include (CMakePackageConfigHelpers) - install(EXPORT jsoncpp - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jsoncpp - FILE jsoncppConfig.cmake) - write_basic_package_version_file ("${CMAKE_CURRENT_BINARY_DIR}/jsoncppConfigVersion.cmake" - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/jsoncppConfigVersion.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jsoncpp) + include(CMakePackageConfigHelpers) + install(EXPORT jsoncpp + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jsoncpp + FILE jsoncpp-targets.cmake) + configure_package_config_file(jsoncppConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/jsoncppConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jsoncpp) + + write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/jsoncppConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/jsoncppConfigVersion.cmake ${CMAKE_CURRENT_BINARY_DIR}/jsoncppConfig.cmake + ${CMAKE_CURRENT_SOURCE_DIR}/jsoncpp-namespaced-targets.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jsoncpp) endif() if(JSONCPP_WITH_TESTS) - enable_testing() - include(CTest) + enable_testing() + include(CTest) endif() # Build the different applications -add_subdirectory( src ) +add_subdirectory(src) #install the includes -add_subdirectory( include ) +add_subdirectory(include) #install the example if(JSONCPP_WITH_EXAMPLE) - add_subdirectory( example ) + add_subdirectory(example) endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08428b6f8..5f5c032a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ If you wish to install to a directory other than /usr/local, set an environment DESTDIR=/path/to/install/dir Then, - +```sh cd jsoncpp/ BUILD_TYPE=debug #BUILD_TYPE=release @@ -27,9 +27,15 @@ Then, #LIB_TYPE=static meson --buildtype ${BUILD_TYPE} --default-library ${LIB_TYPE} . build-${LIB_TYPE} ninja -v -C build-${LIB_TYPE} - cd build-${LIB_TYPE} - meson test --no-rebuild --print-errorlogs + + ninja -C build-static/ test + + # Or + #cd build-${LIB_TYPE} + #meson test --no-rebuild --print-errorlogs + sudo ninja install +``` ## Building and testing with other build systems See https://github.com/open-source-parsers/jsoncpp/wiki/Building @@ -71,7 +77,7 @@ See `doxybuild.py --help` for options. To add a test, you need to create two files in test/data: * a `TESTNAME.json` file, that contains the input document in JSON format. -* a `TESTNAME.expected` file, that contains a flatened representation of the +* a `TESTNAME.expected` file, that contains a flattened representation of the input document. The `TESTNAME.expected` file format is as follows: @@ -138,7 +144,9 @@ bool Reader::decodeNumber(Token& token) { ``` Before submitting your code, ensure that you meet the versioning requirements above, follow the style guide of the file you are modifying (or the above rules for new files), and run clang format. Meson exposes clang format with the following command: - ``` ninja -v -C build-${LIB_TYPE}/ clang-format ``` + +For convenience, you can also run the `reformat.sh` script located in the root directory. + diff --git a/LICENSE b/LICENSE index 89280a6c4..c41a1d1c7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,25 +1,25 @@ -The JsonCpp library's source code, including accompanying documentation, +The JsonCpp library's source code, including accompanying documentation, tests and demonstration applications, are licensed under the following conditions... -Baptiste Lepilleur and The JsonCpp Authors explicitly disclaim copyright in all -jurisdictions which recognize such a disclaimer. In such jurisdictions, +Baptiste Lepilleur and The JsonCpp Authors explicitly disclaim copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, this software is released into the Public Domain. In jurisdictions which do not recognize Public Domain property (e.g. Germany as of 2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur and The JsonCpp Authors, and is released under the terms of the MIT License (see below). -In jurisdictions which recognize Public Domain property, the user of this -software may choose to accept it either as 1) Public Domain, 2) under the -conditions of the MIT License (see below), or 3) under the terms of dual +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual Public Domain/MIT License conditions described here, as they choose. The MIT License is about as close to Public Domain as a license can get, and is described in clear, concise terms at: http://en.wikipedia.org/wiki/MIT_License - + The full text of the MIT License follows: ======================================================================== diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 000000000..25b8bfc8c --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,27 @@ +module( + name = "jsoncpp", + + # Note: version must be updated in four places when doing a release. This + # annoying process ensures that amalgamate, CMake, and meson all report the + # correct version. + # 1. /meson.build + # 2. /include/json/version.h + # 3. /CMakeLists.txt + # 4. /MODULE.bazel + # IMPORTANT: also update the SOVERSION!! + version = "1.9.8", + compatibility_level = 1, +) + +bazel_dep( + name = "bazel_skylib", + version = "1.7.1", +) +bazel_dep( + name = "rules_cc", + version = "0.0.17", +) +bazel_dep( + name = "rules_python", + version = "1.0.0", +) diff --git a/README.md b/README.md index b6fc76913..fe2b4f956 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # JsonCpp -[![badge](https://img.shields.io/badge/conan.io-jsoncpp%2F1.8.0-green.svg?logo=data:image/png;base64%2CiVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAA1VBMVEUAAABhlctjlstkl8tlmMtlmMxlmcxmmcxnmsxpnMxpnM1qnc1sn85voM91oM11oc1xotB2oc56pNF6pNJ2ptJ8ptJ8ptN9ptN8p9N5qNJ9p9N9p9R8qtOBqdSAqtOAqtR%2BrNSCrNJ/rdWDrNWCsNWCsNaJs9eLs9iRvNuVvdyVv9yXwd2Zwt6axN6dxt%2Bfx%2BChyeGiyuGjyuCjyuGly%2BGlzOKmzOGozuKoz%2BKqz%2BOq0OOv1OWw1OWw1eWx1eWy1uay1%2Baz1%2Baz1%2Bez2Oe02Oe12ee22ujUGwH3AAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfgBQkREyOxFIh/AAAAiklEQVQI12NgAAMbOwY4sLZ2NtQ1coVKWNvoc/Eq8XDr2wB5Ig62ekza9vaOqpK2TpoMzOxaFtwqZua2Bm4makIM7OzMAjoaCqYuxooSUqJALjs7o4yVpbowvzSUy87KqSwmxQfnsrPISyFzWeWAXCkpMaBVIC4bmCsOdgiUKwh3JojLgAQ4ZCE0AMm2D29tZwe6AAAAAElFTkSuQmCC)](https://bintray.com/theirix/conan-repo/jsoncpp%3Atheirix) +[![Conan Center](https://img.shields.io/conan/v/jsoncpp)](https://conan.io/center/recipes/jsoncpp) [![badge](https://img.shields.io/badge/license-MIT-blue)](https://github.com/open-source-parsers/jsoncpp/blob/master/LICENSE) [![badge](https://img.shields.io/badge/document-doxygen-brightgreen)](http://open-source-parsers.github.io/jsoncpp-docs/doxygen/index.html) [![Coverage Status](https://coveralls.io/repos/github/open-source-parsers/jsoncpp/badge.svg?branch=master)](https://coveralls.io/github/open-source-parsers/jsoncpp?branch=master) - [JSON][json-org] is a lightweight data-interchange format. It can represent numbers, strings, ordered sequences of values, and collections of name/value pairs. @@ -14,48 +13,97 @@ pairs. JsonCpp is a C++ library that allows manipulating JSON values, including serialization and deserialization to and from strings. It can also preserve -existing comment in unserialization/serialization steps, making it a convenient +existing comment in deserialization/serialization steps, making it a convenient format to store user input files. +## Project Status -## Documentation +JsonCpp is a mature project in maintenance mode. Our priority is providing a stable, +reliable JSON library for the long tail of C++ development. + +### Current Focus -[JsonCpp documentation][JsonCpp-documentation] is generated using [Doxygen][]. +* **Security:** Addressing vulnerabilities and fuzzing results. +* **Compatibility:** Ensuring the library builds without warnings on the latest versions of GCC, +Clang, and MSVC. +* **Reliability:** Fixing regressions and critical logical bugs. -[JsonCpp-documentation]: http://open-source-parsers.github.io/jsoncpp-docs/doxygen/index.html -[Doxygen]: http://www.doxygen.org +### Out of Scope +* **Performance:** We are not competing with SIMD-accelerated or reflection-based parsers. +* **Features:** We are generally not accepting requests for new data formats or major API changes. + +JsonCpp remains a primary choice for developers who require comment preservation and support for +legacy toolchains where modern C++ standards are unavailable. The library is intended to be a +reliable dependency that does not require frequent updates or major migration efforts. ## A note on backward-compatibility -* `1.y.z` is built with C++11. -* `0.y.z` can be used with older compilers. -* Major versions maintain binary-compatibility. +* **`1.y.z` (master):** Actively maintained. Requires C++11. + +* **`0.y.z`:** Legacy support for pre-C++11 compilers. Maintenance is limited to critical security fixes. + +* **`00.11.z`:** Discontinued. + +Major versions maintain binary compatibility. Critical security fixes are accepted for both the `master` and `0.y.z` branches. + +## Integration + +> [!NOTE] +> Package manager ports (vcpkg, Conan, etc.) are community-maintained. Please report outdated versions or missing generators to their respective repositories. + +### vcpkg +Add `jsoncpp` to your `vcpkg.json` manifest: +```json +{ + "dependencies": ["jsoncpp"] +} +``` -## Using JsonCpp in your project +Or install via classic mode: `vcpkg install jsoncpp`. -### The vcpkg dependency manager -You can download and install JsonCpp using the [vcpkg](https://github.com/Microsoft/vcpkg/) dependency manager: +### Conan - git clone https://github.com/Microsoft/vcpkg.git - cd vcpkg - ./bootstrap-vcpkg.sh - ./vcpkg integrate install - vcpkg install jsoncpp +```sh +conan install --requires="jsoncpp/[*]" --build=missing +``` -The JsonCpp port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository. +If you are using a `conanfile.txt` in a Conan 2 project, ensure you use the appropriate generators: + +```ini +[requires] +jsoncpp/[*] + +[generators] +CMakeToolchain +CMakeDeps +``` + +### Meson + +```sh +meson wrap install jsoncpp +``` ### Amalgamated source -https://github.com/open-source-parsers/jsoncpp/wiki/Amalgamated-(Possibly-outdated) -### The Meson Build System -If you are using the [Meson Build System](http://mesonbuild.com), then you can get a wrap file by downloading it from [Meson WrapDB](https://wrapdb.mesonbuild.com/jsoncpp), or simply use `meson wrap install jsoncpp`. +For projects requiring a single-header approach, JsonCpp provides a script to generate an amalgamated source and header file. + +You can generate the amalgamated files by running the following Python script from the top-level directory: + +```sh +python3 amalgamate.py +``` + +This will generate a `dist` directory containing `jsoncpp.cpp`, `json/json.h`, and `json/json-forwards.h`. You can then drop these files directly into your project's source tree and compile `jsoncpp.cpp` alongside your other source files. + +## Documentation -### Other ways -If you have trouble, see the [Wiki](https://github.com/open-source-parsers/jsoncpp/wiki), or post a question as an Issue. +Documentation is generated via [Doxygen](http://open-source-parsers.github.io/jsoncpp-docs/doxygen/index.html). +Additional information is available on the [Project Wiki](https://github.com/open-source-parsers/jsoncpp/wiki). ## License -See the `LICENSE` file for details. In summary, JsonCpp is licensed under the -MIT license, or public domain if desired and recognized in your jurisdiction. +JsonCpp is licensed under the MIT license, or public domain where recognized. +See [LICENSE](./LICENSE) for details. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..67af8830b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +If you have discovered a security vulnerability in this project, please report it +privately. **Do not disclose it as a public issue.** This gives us time to work with you +to fix the issue before public exposure, reducing the chance that the exploit will be +used before a patch is released. + +Please submit the report by filling out +[this form](https://github.com/open-source-parsers/jsoncpp/security/advisories/new). + +Please provide the following information in your report: + +- A description of the vulnerability and its impact +- How to reproduce the issue + +This project is maintained by volunteers on a reasonable-effort basis. As such, +we ask that you give us 90 days to work on a fix before public exposure. diff --git a/amalgamate.py b/amalgamate.py index fedee1eae..1d1e48810 100755 --- a/amalgamate.py +++ b/amalgamate.py @@ -63,7 +63,7 @@ def amalgamate_source(source_top_dir=None, """ print("Amalgamating header...") header = AmalgamationFile(source_top_dir) - header.add_text("/// Json-cpp amalgamated header (http://jsoncpp.sourceforge.net/).") + header.add_text("/// Json-cpp amalgamated header (https://github.com/open-source-parsers/jsoncpp/).") header.add_text('/// It is intended to be used with #include "%s"' % header_include_path) header.add_file("LICENSE", wrap_in_comment=True) header.add_text("#ifndef JSON_AMALGAMATED_H_INCLUDED") @@ -90,7 +90,7 @@ def amalgamate_source(source_top_dir=None, forward_header_include_path = base + "-forwards" + ext print("Amalgamating forward header...") header = AmalgamationFile(source_top_dir) - header.add_text("/// Json-cpp amalgamated forward header (http://jsoncpp.sourceforge.net/).") + header.add_text("/// Json-cpp amalgamated forward header (https://github.com/open-source-parsers/jsoncpp/).") header.add_text('/// It is intended to be used with #include "%s"' % forward_header_include_path) header.add_text("/// This header provides forward declaration for all JsonCpp types.") header.add_file("LICENSE", wrap_in_comment=True) @@ -99,6 +99,8 @@ def amalgamate_source(source_top_dir=None, header.add_text("/// If defined, indicates that the source file is amalgamated") header.add_text("/// to prevent private header inclusion.") header.add_text("#define JSON_IS_AMALGAMATION") + header.add_file(os.path.join(INCLUDE_PATH, "version.h")) + header.add_file(os.path.join(INCLUDE_PATH, "allocator.h")) header.add_file(os.path.join(INCLUDE_PATH, "config.h")) header.add_file(os.path.join(INCLUDE_PATH, "forwards.h")) header.add_text("#endif //ifndef JSON_FORWARD_AMALGAMATED_H_INCLUDED") @@ -110,7 +112,7 @@ def amalgamate_source(source_top_dir=None, print("Amalgamating source...") source = AmalgamationFile(source_top_dir) - source.add_text("/// Json-cpp amalgamated source (http://jsoncpp.sourceforge.net/).") + source.add_text("/// Json-cpp amalgamated source (https://github.com/open-source-parsers/jsoncpp/).") source.add_text('/// It is intended to be used with #include "%s"' % header_include_path) source.add_file("LICENSE", wrap_in_comment=True) source.add_text("") diff --git a/appveyor.yml b/appveyor.yml index 0b9c8fe3f..cccce4298 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,7 @@ clone_folder: c:\projects\jsoncpp environment: + matrix: - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 CMAKE_GENERATOR: Visual Studio 14 2015 @@ -13,11 +14,15 @@ environment: build_script: - cmake --version - - cd c:\projects\jsoncpp - - cmake -G "%CMAKE_GENERATOR%" -DCMAKE_INSTALL_PREFIX:PATH=%CD:\=/%/install -DBUILD_SHARED_LIBS:BOOL=ON . + # The build script starts in root. + - set JSONCPP_FOLDER=%cd% + - set JSONCPP_BUILD_FOLDER=%JSONCPP_FOLDER%\build\release + - mkdir -p %JSONCPP_BUILD_FOLDER% + - cd %JSONCPP_BUILD_FOLDER% + - cmake -G "%CMAKE_GENERATOR%" -DCMAKE_INSTALL_PREFIX:PATH=%CD:\=/%/install -DBUILD_SHARED_LIBS:BOOL=ON %JSONCPP_FOLDER% # Use ctest to make a dashboard build: # - ctest -D Experimental(Start|Update|Configure|Build|Test|Coverage|MemCheck|Submit) - # NOTE: Testing on window is not yet finished: + # NOTE: Testing on windows is not yet finished: # - ctest -C Release -D ExperimentalStart -D ExperimentalConfigure -D ExperimentalBuild -D ExperimentalTest -D ExperimentalSubmit - ctest -C Release -D ExperimentalStart -D ExperimentalConfigure -D ExperimentalBuild -D ExperimentalSubmit # Final step is to verify that installation succeeds diff --git a/cmake/JoinPaths.cmake b/cmake/JoinPaths.cmake new file mode 100644 index 000000000..2b376b733 --- /dev/null +++ b/cmake/JoinPaths.cmake @@ -0,0 +1,23 @@ +# This module provides a function for joining paths +# known from most languages +# +# SPDX-License-Identifier: (MIT OR CC0-1.0) +# Copyright 2020 Jan Tojnar +# https://github.com/jtojnar/cmake-snips +# +# Modelled after Python’s os.path.join +# https://docs.python.org/3.7/library/os.path.html#os.path.join +# Windows not supported +function(join_paths joined_path first_path_segment) + set(temp_path "${first_path_segment}") + foreach(current_segment IN LISTS ARGN) + if(NOT ("${current_segment}" STREQUAL "")) + if(IS_ABSOLUTE "${current_segment}") + set(temp_path "${current_segment}") + else() + set(temp_path "${temp_path}/${current_segment}") + endif() + endif() + endforeach() + set(${joined_path} "${temp_path}" PARENT_SCOPE) +endfunction() diff --git a/dev.makefile b/dev.makefile index 1a4be6a91..545ff2730 100644 --- a/dev.makefile +++ b/dev.makefile @@ -1,9 +1,11 @@ # This is only for jsoncpp developers/contributors. # We use this to sign releases, generate documentation, etc. -VER?=$(shell cat version.txt) +VER?=$(shell cat version) default: @echo "VER=${VER}" +update-version: + perl get_version.pl meson.build >| version sign: jsoncpp-${VER}.tar.gz gpg --armor --detach-sign $< gpg --verify $<.asc diff --git a/devtools/antglob.py b/devtools/antglob.py index 98437658c..bd2d7aee9 100644 --- a/devtools/antglob.py +++ b/devtools/antglob.py @@ -146,7 +146,7 @@ def glob_impl(root_dir_path): entry_type = is_file and FILE_LINK or DIR_LINK else: entry_type = is_file and FILE or DIR -## print '=> type: %d' % entry_type, +## print '=> type: %d' % entry_type, if (entry_type & entry_type_filter) != 0: ## print ' => KEEP' yield os.path.join(dir_path, entry) diff --git a/devtools/batchbuild.py b/devtools/batchbuild.py index 0eb0690e8..bf8be48df 100644 --- a/devtools/batchbuild.py +++ b/devtools/batchbuild.py @@ -9,7 +9,7 @@ import string import subprocess import sys -import cgi +import html class BuildDesc: def __init__(self, prepend_envs=None, variables=None, build_type=None, generator=None): @@ -195,12 +195,12 @@ def generate_html_report(html_report_path, builds): for variable in variables: build_types = sorted(build_types_by_variable[variable]) nb_build_type = len(build_types_by_variable[variable]) - th_vars.append('%s' % (nb_build_type, cgi.escape(' '.join(variable)))) + th_vars.append('%s' % (nb_build_type, html.escape(' '.join(variable)))) for build_type in build_types: - th_build_types.append('%s' % cgi.escape(build_type)) + th_build_types.append('%s' % html.escape(build_type)) tr_builds = [] for generator in sorted(builds_by_generator): - tds = [ '%s\n' % cgi.escape(generator) ] + tds = [ '%s\n' % html.escape(generator) ] for variable in variables: build_types = sorted(build_types_by_variable[variable]) for build_type in build_types: diff --git a/devtools/fixeol.py b/devtools/fixeol.py index 45252a07d..11e1ce2a1 100644 --- a/devtools/fixeol.py +++ b/devtools/fixeol.py @@ -32,8 +32,8 @@ def fix_source_eol(path, is_dry_run = True, verbose = True, eol = '\n'): if verbose: print(is_dry_run and ' NEED FIX' or ' FIXED') return True -## -## +## +## ## ##def _do_fix(is_dry_run = True): ## from waftools import antglob diff --git a/devtools/licenseupdater.py b/devtools/licenseupdater.py index 36bdb5c09..d9b662e01 100644 --- a/devtools/licenseupdater.py +++ b/devtools/licenseupdater.py @@ -20,7 +20,7 @@ def update_license(path, dry_run, show_diff): dry_run: if True, just print the path of the file that would be updated, but don't change it. show_diff: if True, print the path of the file that would be modified, - as well as the change made to the file. + as well as the change made to the file. """ with open(path, 'rt') as fin: original_text = fin.read().replace('\r\n','\n') @@ -51,7 +51,7 @@ def update_license_in_source_directories(source_dirs, dry_run, show_diff): dry_run: if True, just print the path of the file that would be updated, but don't change it. show_diff: if True, print the path of the file that would be modified, - as well as the change made to the file. + as well as the change made to the file. """ from devtools import antglob prune_dirs = antglob.prune_dirs + 'scons-local* ./build* ./libs ./dist' diff --git a/doxybuild.py b/doxybuild.py index 862c1f43f..254ab714b 100644 --- a/doxybuild.py +++ b/doxybuild.py @@ -46,7 +46,7 @@ def do_subst_in_file(targetfile, sourcefile, dict): with open(sourcefile, 'r') as f: contents = f.read() for (k,v) in list(dict.items()): - v = v.replace('\\','\\\\') + v = v.replace('\\','\\\\') contents = re.sub(k, v, contents) with open(targetfile, 'w') as f: f.write(contents) @@ -158,7 +158,7 @@ def main(): Generates doxygen documentation in build/doxygen. Optionally makes a tarball of the documentation to dist/. - Must be started in the project top directory. + Must be started in the project top directory. """ from optparse import OptionParser parser = OptionParser(usage=usage) diff --git a/example/BUILD.bazel b/example/BUILD.bazel new file mode 100644 index 000000000..35813085b --- /dev/null +++ b/example/BUILD.bazel @@ -0,0 +1,41 @@ +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") + +cc_binary( + name = "readFromStream_ok", + srcs = ["readFromStream/readFromStream.cpp"], + args = ["$(location :readFromStream/withComment.json)"], + data = ["readFromStream/withComment.json"], + deps = ["//:jsoncpp"], +) + +cc_binary( + name = "readFromStream_err", + srcs = ["readFromStream/readFromStream.cpp"], + args = ["$(location :readFromStream/errorFormat.json)"], + data = ["readFromStream/errorFormat.json"], + deps = ["//:jsoncpp"], +) + +cc_binary( + name = "readFromString", + srcs = ["readFromString/readFromString.cpp"], + deps = ["//:jsoncpp"], +) + +cc_binary( + name = "streamWrite", + srcs = ["streamWrite/streamWrite.cpp"], + deps = ["//:jsoncpp"], +) + +cc_binary( + name = "stringWrite", + srcs = ["stringWrite/stringWrite.cpp"], + deps = ["//:jsoncpp"], +) + +cc_binary( + name = "stringView", + srcs = ["stringView/stringView.cpp"], + deps = ["//:jsoncpp"], +) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index c8eba5335..0666db763 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,29 +1,28 @@ #vim: et ts =4 sts = 4 sw = 4 tw = 0 -cmake_minimum_required(VERSION 3.1) - set(EXAMPLES - readFromString - readFromStream - stringWrite - streamWrite - ) + readFromString + readFromStream + stringWrite + streamWrite + stringView +) add_definitions(-D_GLIBCXX_USE_CXX11_ABI) -set_property(DIRECTORY PROPERTY COMPILE_OPTIONS ${EXTRA_CXX_FLAGS}) -if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra ") -else() - add_definitions( - -D_SCL_SECURE_NO_WARNINGS - -D_CRT_SECURE_NO_WARNINGS - -D_WIN32_WINNT=0x601 - -D_WINSOCK_DEPRECATED_NO_WARNINGS) +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options(-Wall -Wextra) +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_definitions( + -D_SCL_SECURE_NO_WARNINGS + -D_CRT_SECURE_NO_WARNINGS + -D_WIN32_WINNT=0x601 + -D_WINSOCK_DEPRECATED_NO_WARNINGS + ) endif() -foreach (example ${EXAMPLES}) - add_executable(${example} ${example}/${example}.cpp) - target_include_directories(${example} PUBLIC ${CMAKE_SOURCE_DIR}/include) - target_link_libraries(${example} jsoncpp_lib) +foreach(example ${EXAMPLES}) + add_executable(${example} ${example}/${example}.cpp) + target_include_directories(${example} PUBLIC ${CMAKE_SOURCE_DIR}/include) + target_link_libraries(${example} jsoncpp_lib) endforeach() add_custom_target(examples ALL DEPENDS ${EXAMPLES}) diff --git a/example/README.md b/example/README.md index b1ae4c875..92b925c96 100644 --- a/example/README.md +++ b/example/README.md @@ -1,8 +1,8 @@ -***NOTE*** +***NOTE*** If you get linker errors about undefined references to symbols that involve types in the `std::__cxx11` namespace or the tag `[abi:cxx11]` then it probably indicates that you are trying to link together object files that were compiled with different -values for the _GLIBCXX_USE_CXX11_ABI marco. This commonly happens when linking to a third-party library that was compiled with +values for the _GLIBCXX_USE_CXX11_ABI marco. This commonly happens when linking to a third-party library that was compiled with an older version of GCC. If the third-party library cannot be rebuilt with the new ABI, then you need to recompile your code with the old ABI,just like: **g++ stringWrite.cpp -ljsoncpp -std=c++11 -D_GLIBCXX_USE_CXX11_ABI=0 -o stringWrite** diff --git a/example/readFromString/readFromString.cpp b/example/readFromString/readFromString.cpp index c27bbd5ae..878f9eb92 100644 --- a/example/readFromString/readFromString.cpp +++ b/example/readFromString/readFromString.cpp @@ -1,5 +1,6 @@ #include "json/json.h" #include +#include /** * \brief Parse a raw string into Value object using the CharReaderBuilder * class, or the legacy Reader class. @@ -24,7 +25,7 @@ int main() { const std::unique_ptr reader(builder.newCharReader()); if (!reader->parse(rawJson.c_str(), rawJson.c_str() + rawJsonLength, &root, &err)) { - std::cout << "error" << std::endl; + std::cout << "error: " << err << std::endl; return EXIT_FAILURE; } } diff --git a/example/streamWrite/streamWrite.cpp b/example/streamWrite/streamWrite.cpp index 6f7f7972a..a72f5a52e 100644 --- a/example/streamWrite/streamWrite.cpp +++ b/example/streamWrite/streamWrite.cpp @@ -1,5 +1,6 @@ #include "json/json.h" #include +#include /** \brief Write the Value object to a stream. * Example Usage: * $g++ streamWrite.cpp -ljsoncpp -std=c++11 -o streamWrite diff --git a/example/stringView/stringView.cpp b/example/stringView/stringView.cpp new file mode 100644 index 000000000..b5e33e9fd --- /dev/null +++ b/example/stringView/stringView.cpp @@ -0,0 +1,29 @@ +#include "json/json.h" +#include +#include + +#if defined(JSONCPP_HAS_STRING_VIEW) +#include +#endif + +/** + * \brief Example using std::string_view with JsonCpp. + */ +int main() { + Json::Value root; + root["key"] = "value"; + +#if defined(JSONCPP_HAS_STRING_VIEW) + std::cout << "Has string_view support" << std::endl; + std::string_view sv("key"); + if (root.isMember(sv)) { + std::cout << root[sv].asString() << std::endl; + } +#else + std::cout << "No string_view support" << std::endl; + if (root.isMember("key")) { + std::cout << root["key"].asString() << std::endl; + } +#endif + return EXIT_SUCCESS; +} diff --git a/gcovr.cfg b/gcovr.cfg new file mode 100644 index 000000000..621e71053 --- /dev/null +++ b/gcovr.cfg @@ -0,0 +1,19 @@ +# Newer versions of gcovr have strict function merging by default, which +# can cause issues with header-only functions or macros (like in jsontest.h). +# 'separate' mode keeps them distinct, fixing the GcovrMergeAssertionError. +merge-mode-functions = separate + +# --- Filtering --- +# Only include the library sources in the coverage report. +# This ensures coverage stats reflect the library quality and ignores test code. +filter = src/lib_json/ +filter = include/json/ + +# --- Noise Reduction --- +# Ignore branches that are generated by the compiler (e.g., exception handling) +# This drastically reduces "false positives" for missing branch coverage. +exclude-throw-branches = yes + +# --- CI Visibility --- +# Print a small summary table to the console logs. +print-summary = yes diff --git a/get_version.pl b/get_version.pl new file mode 100644 index 000000000..19b6a543b --- /dev/null +++ b/get_version.pl @@ -0,0 +1,5 @@ +while (<>) { + if (/version : '(.+)',/) { + print "$1"; + } +} diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index dc40d95e8..bbd640559 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -1,5 +1,8 @@ +if (JSONCPP_WITH_INSTALL) + file(GLOB INCLUDE_FILES "json/*.h") install(FILES ${INCLUDE_FILES} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/json) +endif() \ No newline at end of file diff --git a/include/PreventInBuildInstalls.cmake b/include/PreventInBuildInstalls.cmake new file mode 100644 index 000000000..accfea64c --- /dev/null +++ b/include/PreventInBuildInstalls.cmake @@ -0,0 +1,9 @@ +string(TOLOWER "${CMAKE_INSTALL_PREFIX}" _PREFIX) +string(TOLOWER "${ITK_BINARY_DIR}" _BUILD) +if("${_PREFIX}" STREQUAL "${_BUILD}") + message(FATAL_ERROR + "The current CMAKE_INSTALL_PREFIX points at the build tree:\n" + " ${CMAKE_INSTALL_PREFIX}\n" + "This is not supported." + ) +endif() diff --git a/include/PreventInSourceBuilds.cmake b/include/PreventInSourceBuilds.cmake new file mode 100644 index 000000000..be5d0dd41 --- /dev/null +++ b/include/PreventInSourceBuilds.cmake @@ -0,0 +1,45 @@ +# +# This function will prevent in-source builds +function(AssureOutOfSourceBuilds) + # make sure the user doesn't play dirty with symlinks + get_filename_component(srcdir "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH) + get_filename_component(bindir "${CMAKE_CURRENT_BINARY_DIR}" REALPATH) + + # disallow in-source builds + if("${srcdir}" STREQUAL "${bindir}") + message("######################################################") + message("# jsoncpp should not be configured & built in the jsoncpp source directory") + message("# You must run cmake in a build directory.") + message("# For example:") + message("# mkdir jsoncpp-Sandbox ; cd jsoncpp-sandbox") + message("# git clone https://github.com/open-source-parsers/jsoncpp.git # or download & unpack the source tarball") + message("# mkdir jsoncpp-build") + message("# this will create the following directory structure") + message("#") + message("# jsoncpp-Sandbox") + message("# +--jsoncpp") + message("# +--jsoncpp-build") + message("#") + message("# Then you can proceed to configure and build") + message("# by using the following commands") + message("#") + message("# cd jsoncpp-build") + message("# cmake ../jsoncpp # or ccmake, or cmake-gui ") + message("# make") + message("#") + message("# NOTE: Given that you already tried to make an in-source build") + message("# CMake have already created several files & directories") + message("# in your source tree. run 'git status' to find them and") + message("# remove them by doing:") + message("#") + message("# cd jsoncpp-Sandbox/jsoncpp") + message("# git clean -n -d") + message("# git clean -f -d") + message("# git checkout --") + message("#") + message("######################################################") + message(FATAL_ERROR "Quitting configuration") + endif() +endfunction() + +AssureOutOfSourceBuilds() diff --git a/include/json/allocator.h b/include/json/allocator.h index 0f5c224b9..459c34c61 100644 --- a/include/json/allocator.h +++ b/include/json/allocator.h @@ -6,10 +6,12 @@ #ifndef JSON_ALLOCATOR_H_INCLUDED #define JSON_ALLOCATOR_H_INCLUDED +#include #include #include -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { template class SecureAllocator { @@ -35,11 +37,18 @@ template class SecureAllocator { * Release memory which was allocated for N items at pointer P. * * The memory block is filled with zeroes before being released. - * The pointer argument is tagged as "volatile" to prevent the - * compiler optimizing out this critical step. */ - void deallocate(volatile pointer p, size_type n) { - std::memset(p, 0, n * sizeof(T)); + void deallocate(pointer p, size_type n) { + // These constructs will not be removed by the compiler during optimization, + // unlike memset. +#if defined(HAVE_MEMSET_S) + memset_s(p, n * sizeof(T), 0, n * sizeof(T)); +#elif defined(_WIN32) + RtlSecureZeroMemory(p, n * sizeof(T)); +#else + std::fill_n(reinterpret_cast(p), n, 0); +#endif + // free using "global operator delete" ::operator delete(p); } @@ -69,7 +78,9 @@ template class SecureAllocator { // Boilerplate SecureAllocator() {} template SecureAllocator(const SecureAllocator&) {} - template struct rebind { using other = SecureAllocator; }; + template struct rebind { + using other = SecureAllocator; + }; }; template diff --git a/include/json/assertions.h b/include/json/assertions.h index 9d93238dc..666fa7f54 100644 --- a/include/json/assertions.h +++ b/include/json/assertions.h @@ -21,19 +21,19 @@ // @todo <= add detail about condition in exception #define JSON_ASSERT(condition) \ - { \ + do { \ if (!(condition)) { \ Json::throwLogicError("assert json failed"); \ } \ - } + } while (0) #define JSON_FAIL_MESSAGE(message) \ - { \ + do { \ OStringStream oss; \ oss << message; \ Json::throwLogicError(oss.str()); \ abort(); \ - } + } while (0) #else // JSON_USE_EXCEPTION @@ -52,8 +52,10 @@ #endif #define JSON_ASSERT_MESSAGE(condition, message) \ - if (!(condition)) { \ - JSON_FAIL_MESSAGE(message); \ - } + do { \ + if (!(condition)) { \ + JSON_FAIL_MESSAGE(message); \ + } \ + } while (0) #endif // JSON_ASSERTIONS_H_INCLUDED diff --git a/include/json/config.h b/include/json/config.h index 3d148a69b..4619e93bd 100644 --- a/include/json/config.h +++ b/include/json/config.h @@ -74,20 +74,6 @@ extern JSON_API int msvc_pre1900_c99_snprintf(char* outBuf, size_t size, // C++11 should be used directly in JSONCPP. #define JSONCPP_OVERRIDE override -#if __cplusplus >= 201103L -#define JSONCPP_NOEXCEPT noexcept -#define JSONCPP_OP_EXPLICIT explicit -#elif defined(_MSC_VER) && _MSC_VER < 1900 -#define JSONCPP_NOEXCEPT throw() -#define JSONCPP_OP_EXPLICIT explicit -#elif defined(_MSC_VER) && _MSC_VER >= 1900 -#define JSONCPP_NOEXCEPT noexcept -#define JSONCPP_OP_EXPLICIT explicit -#else -#define JSONCPP_NOEXCEPT throw() -#define JSONCPP_OP_EXPLICIT -#endif - #ifdef __clang__ #if __has_extension(attribute_deprecated_with_message) #define JSONCPP_DEPRECATED(message) __attribute__((deprecated(message))) @@ -119,6 +105,7 @@ extern JSON_API int msvc_pre1900_c99_snprintf(char* outBuf, size_t size, #endif // if !defined(JSON_IS_AMALGAMATION) namespace Json { +JSON_API const char* version(); using Int = int; using UInt = unsigned int; #if defined(JSON_NO_INT64) @@ -136,12 +123,14 @@ using UInt64 = uint64_t; #endif // if defined(_MSC_VER) using LargestInt = Int64; using LargestUInt = UInt64; +#ifndef JSON_HAS_INT64 #define JSON_HAS_INT64 +#endif // ifndef JSON_HAS_INT64 #endif // if defined(JSON_NO_INT64) template using Allocator = - typename std::conditional, + typename std::conditional, std::allocator>::type; using String = std::basic_string, Allocator>; using IStringStream = diff --git a/include/json/forwards.h b/include/json/forwards.h index affe33a7f..2887bdd78 100644 --- a/include/json/forwards.h +++ b/include/json/forwards.h @@ -37,6 +37,8 @@ class Value; class ValueIteratorBase; class ValueIterator; class ValueConstIterator; +class ValueMembersView; +class ValueConstMembersView; } // namespace Json diff --git a/include/json/json_features.h b/include/json/json_features.h index a5b73052a..e4a61d6f1 100644 --- a/include/json/json_features.h +++ b/include/json/json_features.h @@ -10,7 +10,8 @@ #include "forwards.h" #endif // if !defined(JSON_IS_AMALGAMATION) -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { @@ -23,7 +24,6 @@ class JSON_API Features { /** \brief A configuration that allows all features and assumes all strings * are UTF-8. * - C & C++ comments are allowed - * - Trailing commas in objects and arrays are allowed. * - Root object can be any JSON value * - Assumes Value strings are encoded in UTF-8 */ @@ -32,7 +32,6 @@ class JSON_API Features { /** \brief A configuration that is strictly compatible with the JSON * specification. * - Comments are forbidden. - * - Trailing commas in objects and arrays are forbidden. * - Root object must be either an array or an object value. * - Assumes Value strings are encoded in UTF-8 */ @@ -45,10 +44,6 @@ class JSON_API Features { /// \c true if comments are allowed. Default: \c true. bool allowComments_{true}; - /// \c true if trailing commas in objects and arrays are allowed. Default \c - /// true. - bool allowTrailingCommas_{true}; - /// \c true if root must be either an array or an object value. Default: \c /// false. bool strictRoot_{false}; diff --git a/include/json/reader.h b/include/json/reader.h index 917546608..7aa227188 100644 --- a/include/json/reader.h +++ b/include/json/reader.h @@ -23,7 +23,8 @@ #pragma warning(disable : 4251) #endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { @@ -33,8 +34,7 @@ namespace Json { * \deprecated Use CharReader and CharReaderBuilder. */ -class JSONCPP_DEPRECATED( - "Use CharReader and CharReaderBuilder instead.") JSON_API Reader { +class JSON_API Reader { public: using Char = char; using Location = const Char*; @@ -51,13 +51,13 @@ class JSONCPP_DEPRECATED( }; /** \brief Constructs a Reader allowing all features for parsing. + * \deprecated Use CharReader and CharReaderBuilder. */ - JSONCPP_DEPRECATED("Use CharReader and CharReaderBuilder instead") Reader(); /** \brief Constructs a Reader allowing the specified feature set for parsing. + * \deprecated Use CharReader and CharReaderBuilder. */ - JSONCPP_DEPRECATED("Use CharReader and CharReaderBuilder instead") Reader(const Features& features); /** \brief Read a Value from a JSON @@ -81,7 +81,10 @@ class JSONCPP_DEPRECATED( * document. * * \param beginDoc Pointer on the beginning of the UTF-8 encoded - * string of the document to read. + * string of the document to read. The pointed-to + * buffer must outlive this Reader if error + * methods (e.g. getFormattedErrorMessages()) are + * called after parse() returns. * \param endDoc Pointer on the end of the UTF-8 encoded string * of the document to read. Must be >= beginDoc. * \param[out] root Contains the root value of the document if it @@ -190,6 +193,7 @@ class JSONCPP_DEPRECATED( using Errors = std::deque; bool readToken(Token& token); + bool readTokenSkippingComments(Token& token); void skipSpaces(); bool match(const Char* pattern, int patternLength); bool readComment(); @@ -221,7 +225,6 @@ class JSONCPP_DEPRECATED( int& column) const; String getLocationLineAndColumn(Location location) const; void addComment(Location begin, Location end, CommentPlacement placement); - void skipCommentTokens(Token& token); static bool containsNewLine(Location begin, Location end); static String normalizeEOL(Location begin, Location end); @@ -244,6 +247,12 @@ class JSONCPP_DEPRECATED( */ class JSON_API CharReader { public: + struct JSON_API StructuredError { + ptrdiff_t offset_start; + ptrdiff_t offset_limit; + String message; + }; + virtual ~CharReader() = default; /** \brief Read a Value from a JSON * document. The document must be a UTF-8 encoded string containing the @@ -262,7 +271,12 @@ class JSON_API CharReader { * error occurred. */ virtual bool parse(char const* beginDoc, char const* endDoc, Value* root, - String* errs) = 0; + String* errs); + + /** \brief Returns a vector of structured errors encountered while parsing. + * Each parse call resets the stored list of errors. + */ + std::vector getStructuredErrors() const; class JSON_API Factory { public: @@ -272,7 +286,21 @@ class JSON_API CharReader { */ virtual CharReader* newCharReader() const = 0; }; // Factory -}; // CharReader + +protected: + class Impl { + public: + virtual ~Impl() = default; + virtual bool parse(char const* beginDoc, char const* endDoc, Value* root, + String* errs) = 0; + virtual std::vector getStructuredErrors() const = 0; + }; + + explicit CharReader(std::unique_ptr impl) : _impl(std::move(impl)) {} + +private: + std::unique_ptr _impl; +}; // CharReader /** \brief Build a CharReader implementation. * @@ -324,6 +352,9 @@ class JSON_API CharReaderBuilder : public CharReader::Factory { * - `"allowSpecialFloats": false or true` * - If true, special float values (NaNs and infinities) are allowed and * their values are lossfree restorable. + * - `"skipBom": false or true` + * - If true, if the input starts with the Unicode byte order mark (BOM), + * it is skipped. * * You can examine 'settings_` yourself to see the defaults. You can also * write and read them just like any JSON Value. @@ -357,6 +388,12 @@ class JSON_API CharReaderBuilder : public CharReader::Factory { * \snippet src/lib_json/json_reader.cpp CharReaderBuilderStrictMode */ static void strictMode(Json::Value* settings); + /** ECMA-404 mode. + * \pre 'settings' != NULL (but Json::null is fine) + * \remark Defaults: + * \snippet src/lib_json/json_reader.cpp CharReaderBuilderECMA404Mode + */ + static void ecma404Mode(Json::Value* settings); }; /** Consume entire stream and use its begin/end. diff --git a/include/json/value.h b/include/json/value.h index e169c3c69..f14a71ce5 100644 --- a/include/json/value.h +++ b/include/json/value.h @@ -3,8 +3,8 @@ // recognized in your jurisdiction. // See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE -#ifndef JSON_H_INCLUDED -#define JSON_H_INCLUDED +#ifndef JSON_VALUE_H_INCLUDED +#define JSON_VALUE_H_INCLUDED #if !defined(JSON_IS_AMALGAMATION) #include "forwards.h" @@ -21,6 +21,30 @@ #endif #endif +// Support for '= delete' with template declarations was a late addition +// to the c++11 standard and is rejected by clang 3.8 and Apple clang 8.2 +// even though these declare themselves to be c++11 compilers. +#if !defined(JSONCPP_TEMPLATE_DELETE) +#if defined(__clang__) && defined(__apple_build_version__) +#if __apple_build_version__ <= 8000042 +#define JSONCPP_TEMPLATE_DELETE +#endif +#elif defined(__clang__) +#if __clang_major__ == 3 && __clang_minor__ <= 8 +#define JSONCPP_TEMPLATE_DELETE +#endif +#endif +#if !defined(JSONCPP_TEMPLATE_DELETE) +#define JSONCPP_TEMPLATE_DELETE = delete +#endif +#endif + +#ifndef JSONCPP_HAS_STRING_VIEW +#if __cplusplus >= 201703L +#define JSONCPP_HAS_STRING_VIEW 1 +#endif +#endif + #include #include #include @@ -28,14 +52,22 @@ #include #include +// Forward declaration for testing. +struct ValueTest; + +#ifdef JSONCPP_HAS_STRING_VIEW +#include +#endif + // Disable warning C4251: : needs to have dll-interface to // be used by... #if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) #pragma warning(push) -#pragma warning(disable : 4251) +#pragma warning(disable : 4251 4275) #endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() /** \brief JSON (JavaScript Object Notation). */ @@ -49,8 +81,8 @@ namespace Json { class JSON_API Exception : public std::exception { public: Exception(String msg); - ~Exception() JSONCPP_NOEXCEPT override; - char const* what() const JSONCPP_NOEXCEPT override; + ~Exception() noexcept override; + char const* what() const noexcept override; protected: String msg_; @@ -174,6 +206,7 @@ class JSON_API StaticString { */ class JSON_API Value { friend class ValueIteratorBase; + friend struct ::ValueTest; public: using Members = std::vector; @@ -239,16 +272,16 @@ class JSON_API Value { private: #endif #ifndef JSONCPP_DOC_EXCLUDE_IMPLEMENTATION - class CZString { + class JSON_API CZString { public: enum DuplicationPolicy { noDuplication = 0, duplicate, duplicateOnCopy }; CZString(ArrayIndex index); CZString(char const* str, unsigned length, DuplicationPolicy allocate); CZString(CZString const& other); - CZString(CZString&& other); + CZString(CZString&& other) noexcept; ~CZString(); CZString& operator=(const CZString& other); - CZString& operator=(CZString&& other); + CZString& operator=(CZString&& other) noexcept; bool operator<(CZString const& other) const; bool operator==(CZString const& other) const; @@ -323,15 +356,20 @@ class JSON_API Value { */ Value(const StaticString& value); Value(const String& value); +#ifdef JSONCPP_HAS_STRING_VIEW + inline Value(std::string_view value) + : Value(value.data(), value.data() + value.length()) {} +#endif Value(bool value); + Value(std::nullptr_t ptr) = delete; Value(const Value& other); - Value(Value&& other); + Value(Value&& other) noexcept; ~Value(); /// \note Overwrite existing comments. To preserve comments, use /// #swapPayload(). Value& operator=(const Value& other); - Value& operator=(Value&& other); + Value& operator=(Value&& other) noexcept; /// Swap everything. void swap(Value& other); @@ -355,7 +393,7 @@ class JSON_API Value { int compare(const Value& other) const; const char* asCString() const; ///< Embedded zeroes could cause you trouble! -#if JSONCPP_USING_SECURE_MEMORY +#if JSONCPP_USE_SECURE_MEMORY unsigned getCStringLength() const; // Allows you to understand the length of // the CString #endif @@ -364,6 +402,19 @@ class JSON_API Value { * \return false if !string. (Seg-fault if str or end are NULL.) */ bool getString(char const** begin, char const** end) const; +#ifdef JSONCPP_HAS_STRING_VIEW + /** Get string_view of string-value. + * \return false if !string. (Seg-fault if str is NULL.) + */ + inline bool getString(std::string_view* str) const { + char const* begin; + char const* end; + if (!getString(&begin, &end)) + return false; + *str = std::string_view(begin, static_cast(end - begin)); + return true; + } +#endif Int asInt() const; UInt asUInt() const; #if defined(JSON_HAS_INT64) @@ -390,8 +441,8 @@ class JSON_API Value { bool isObject() const; /// The `as` and `is` member function templates and specializations. - template T as() const = delete; - template bool is() const = delete; + template T as() const JSONCPP_TEMPLATE_DELETE; + template bool is() const JSONCPP_TEMPLATE_DELETE; bool isConvertibleTo(ValueType other) const; @@ -403,7 +454,7 @@ class JSON_API Value { bool empty() const; /// Return !isNull() - JSONCPP_OP_EXPLICIT operator bool() const; + explicit operator bool() const; /// Remove all object members and array elements. /// \pre type() is arrayValue, objectValue, or nullValue @@ -417,7 +468,7 @@ class JSON_API Value { /// \post type() is arrayValue void resize(ArrayIndex newSize); - //@{ + ///@{ /// Access an array element (zero based index). If the array contains less /// than index element, then null value are inserted in the array so that /// its size is index+1. @@ -425,15 +476,15 @@ class JSON_API Value { /// this from the operator[] which takes a string.) Value& operator[](ArrayIndex index); Value& operator[](int index); - //@} + ///@} - //@{ + ///@{ /// Access an array element (zero based index). /// (You may need to say 'value[0u]' to get your compiler to distinguish /// this from the operator[] which takes a string.) const Value& operator[](ArrayIndex index) const; const Value& operator[](int index) const; - //@} + ///@} /// If the array contains at least index+1 elements, returns the element /// value, otherwise returns defaultValue. @@ -450,6 +501,22 @@ class JSON_API Value { bool insert(ArrayIndex index, const Value& newValue); bool insert(ArrayIndex index, Value&& newValue); +#ifdef JSONCPP_HAS_STRING_VIEW + /// Access an object value by name, create a null member if it does not exist. + /// \param key may contain embedded nulls. + inline Value& operator[](std::string_view key) { + return resolveReference(key.data(), key.data() + key.length()); + } + /// Access an object value by name, returns null if there is no member with + /// that name. + /// \param key may contain embedded nulls. + inline const Value& operator[](std::string_view key) const { + Value const* found = find(key.data(), key.data() + key.length()); + if (!found) + return nullSingleton(); + return *found; + } +#endif /// Access an object value by name, create a null member if it does not exist. /// \note Because of our implementation, keys are limited to 2^30 -1 chars. /// Exceeding that will cause an exception. @@ -477,22 +544,55 @@ class JSON_API Value { * \endcode */ Value& operator[](const StaticString& key); +#ifdef JSONCPP_HAS_STRING_VIEW /// Return the member named key if it exist, defaultValue otherwise. /// \note deep copy - Value get(const char* key, const Value& defaultValue) const; + inline Value get(std::string_view key, const Value& defaultValue) const { + return get(key.data(), key.data() + key.length(), defaultValue); + } +#endif /// Return the member named key if it exist, defaultValue otherwise. /// \note deep copy - /// \note key may contain embedded nulls. - Value get(const char* begin, const char* end, - const Value& defaultValue) const; + Value get(const char* key, const Value& defaultValue) const; /// Return the member named key if it exist, defaultValue otherwise. /// \note deep copy /// \param key may contain embedded nulls. Value get(const String& key, const Value& defaultValue) const; + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + /// \note key may contain embedded nulls. + Value get(const char* begin, const char* end, + const Value& defaultValue) const; /// Most general and efficient version of isMember()const, get()const, /// and operator[]const /// \note As stated elsewhere, behavior is undefined if (end-begin) >= 2^30 Value const* find(char const* begin, char const* end) const; + /// Most general and efficient version of isMember()const, get()const, + /// and operator[]const + Value const* find(const String& key) const; + + /// Calls find and only returns a valid pointer if the type is found + template + Value const* findValue(const String& key) const { + Value const* found = find(key); + if (!found || !(found->*TMemFn)()) + return nullptr; + return found; + } + + Value const* findNull(const String& key) const; + Value const* findBool(const String& key) const; + Value const* findInt(const String& key) const; + Value const* findInt64(const String& key) const; + Value const* findUInt(const String& key) const; + Value const* findUInt64(const String& key) const; + Value const* findIntegral(const String& key) const; + Value const* findDouble(const String& key) const; + Value const* findNumeric(const String& key) const; + Value const* findString(const String& key) const; + Value const* findArray(const String& key) const; + Value const* findObject(const String& key) const; + /// Most general and efficient version of object-mutators. /// \note As stated elsewhere, behavior is undefined if (end-begin) >= 2^30 /// \return non-zero, but JSON_ASSERT if this is neither object nor nullValue. @@ -502,20 +602,30 @@ class JSON_API Value { /// Do nothing if it did not exist. /// \pre type() is objectValue or nullValue /// \post type() is unchanged +#if JSONCPP_HAS_STRING_VIEW + inline void removeMember(std::string_view key) { + removeMember(key.data(), key.data() + key.length(), nullptr); + } +#endif void removeMember(const char* key); /// Same as removeMember(const char*) /// \param key may contain embedded nulls. void removeMember(const String& key); - /// Same as removeMember(const char* begin, const char* end, Value* removed), - /// but 'key' is null-terminated. - bool removeMember(const char* key, Value* removed); /** \brief Remove the named map member. * * Update 'removed' iff removed. * \param key may contain embedded nulls. * \return true iff removed (no exceptions) */ +#if JSONCPP_HAS_STRING_VIEW + inline bool removeMember(std::string_view key, Value* removed) { + return removeMember(key.data(), key.data() + key.length(), removed); + } +#endif bool removeMember(String const& key, Value* removed); + /// Same as removeMember(const char* begin, const char* end, Value* removed), + /// but 'key' is null-terminated. + bool removeMember(const char* key, Value* removed); /// Same as removeMember(String const& key, Value* removed) bool removeMember(const char* begin, const char* end, Value* removed); /** \brief Remove the indexed array element. @@ -526,6 +636,13 @@ class JSON_API Value { */ bool removeIndex(ArrayIndex index, Value* removed); +#ifdef JSONCPP_HAS_STRING_VIEW + /// Return true if the object has a member named key. + /// \param key may contain embedded nulls. + inline bool isMember(std::string_view key) const { + return isMember(key.data(), key.data() + key.length()); + } +#endif /// Return true if the object has a member named key. /// \note 'key' must be null-terminated. bool isMember(const char* key) const; @@ -565,6 +682,31 @@ class JSON_API Value { iterator begin(); iterator end(); + // \brief Returns a view of member pairs for range-based for loops. + ValueMembersView members(); + // \brief Returns a view of member pairs for range-based for loops. + ValueConstMembersView members() const; + + /// \brief Returns a reference to the first element in the `Value`. + /// Requires that this value holds an array or json object, with at least one + /// element. + const Value& front() const; + + /// \brief Returns a reference to the first element in the `Value`. + /// Requires that this value holds an array or json object, with at least one + /// element. + Value& front(); + + /// \brief Returns a reference to the last element in the `Value`. + /// Requires that value holds an array or json object, with at least one + /// element. + const Value& back() const; + + /// \brief Returns a reference to the last element in the `Value`. + /// Requires that this value holds an array or json object, with at least one + /// element. + Value& back(); + // Accessors for the [start, limit) range of bytes within the JSON text from // which this value was parsed, if any. void setOffsetStart(ptrdiff_t start); @@ -616,9 +758,9 @@ class JSON_API Value { public: Comments() = default; Comments(const Comments& that); - Comments(Comments&& that); + Comments(Comments&& that) noexcept; Comments& operator=(const Comments& that); - Comments& operator=(Comments&& that); + Comments& operator=(Comments&& that) noexcept; bool has(CommentPlacement slot) const; String get(CommentPlacement slot) const; void set(CommentPlacement slot, String comment); @@ -899,12 +1041,145 @@ class JSON_API ValueIterator : public ValueIteratorBase { * because the returned references/pointers can be used * to change state of the base class. */ - reference operator*() { return deref(); } - pointer operator->() { return &deref(); } + reference operator*() const { return const_cast(deref()); } + pointer operator->() const { return const_cast(&deref()); } }; +/** \brief Proxy struct to enable range-based for loops over object members. + */ +struct MemberProxy { + const String name; + Value& value; +}; + +/** \brief Proxy struct to enable range-based for loops over const object + * members. + */ +struct ConstMemberProxy { + const String name; + const Value& value; +}; + +/** \brief Iterator adapter for range-based for loops. + */ +class ValueMembersIterator { +public: + using iterator_category = std::forward_iterator_tag; + using value_type = MemberProxy; + using difference_type = int; + using pointer = MemberProxy*; + using reference = MemberProxy; + + ValueMembersIterator() = default; + explicit ValueMembersIterator(ValueIterator const& iter) : it_(iter) {} + + ValueMembersIterator& operator++() { + ++it_; + return *this; + } + ValueMembersIterator operator++(int) { + ValueMembersIterator temp(*this); + ++*this; + return temp; + } + bool operator==(ValueMembersIterator const& other) const { + return it_ == other.it_; + } + bool operator!=(ValueMembersIterator const& other) const { + return it_ != other.it_; + } + MemberProxy operator*() const { return MemberProxy{it_.name(), *it_}; } + +private: + ValueIterator it_; +}; + +/** \brief Iterator adapter for range-based for loops. + */ +class ValueConstMembersIterator { +public: + using iterator_category = std::forward_iterator_tag; + using value_type = ConstMemberProxy; + using difference_type = int; + using pointer = ConstMemberProxy*; + using reference = ConstMemberProxy; + + ValueConstMembersIterator() = default; + explicit ValueConstMembersIterator(ValueConstIterator const& iter) + : it_(iter) {} + + ValueConstMembersIterator& operator++() { + ++it_; + return *this; + } + ValueConstMembersIterator operator++(int) { + ValueConstMembersIterator temp(*this); + ++*this; + return temp; + } + bool operator==(ValueConstMembersIterator const& other) const { + return it_ == other.it_; + } + bool operator!=(ValueConstMembersIterator const& other) const { + return it_ != other.it_; + } + ConstMemberProxy operator*() const { + return ConstMemberProxy{it_.name(), *it_}; + } + +private: + ValueConstIterator it_; +}; + +/** \brief Range-based for loop adapter for object members. + */ +class ValueMembersView { +public: + ValueMembersView(ValueIterator begin, ValueIterator end) + : begin_(begin), end_(end) {} + ValueMembersIterator begin() const { return ValueMembersIterator(begin_); } + ValueMembersIterator end() const { return ValueMembersIterator(end_); } + +private: + ValueIterator begin_; + ValueIterator end_; +}; + +/** \brief Range-based for loop adapter for object members. + */ +class ValueConstMembersView { +public: + ValueConstMembersView(ValueConstIterator begin, ValueConstIterator end) + : begin_(begin), end_(end) {} + ValueConstMembersIterator begin() const { + return ValueConstMembersIterator(begin_); + } + ValueConstMembersIterator end() const { + return ValueConstMembersIterator(end_); + } + +private: + ValueConstIterator begin_; + ValueConstIterator end_; +}; + +inline ValueMembersView Value::members() { + return ValueMembersView(begin(), end()); +} +inline ValueConstMembersView Value::members() const { + return ValueConstMembersView(begin(), end()); +} + inline void swap(Value& a, Value& b) { a.swap(b); } +inline const Value& Value::front() const { return *begin(); } + +inline Value& Value::front() { return *begin(); } + +inline const Value& Value::back() const { return *(--end()); } + +inline Value& Value::back() { return *(--end()); } + } // namespace Json #pragma pack(pop) diff --git a/include/json/version.h b/include/json/version.h index ff94372b0..1579c7807 100644 --- a/include/json/version.h +++ b/include/json/version.h @@ -1,27 +1,26 @@ #ifndef JSON_VERSION_H_INCLUDED #define JSON_VERSION_H_INCLUDED -// Note: version must be updated in three places when doing a release. This +// Note: version must be updated in four places when doing a release. This // annoying process ensures that amalgamate, CMake, and meson all report the // correct version. // 1. /meson.build // 2. /include/json/version.h // 3. /CMakeLists.txt +// 4. /MODULE.bazel // IMPORTANT: also update the SOVERSION!! -#define JSONCPP_VERSION_STRING "1.9.2" +#define JSONCPP_VERSION_STRING "1.9.8" #define JSONCPP_VERSION_MAJOR 1 #define JSONCPP_VERSION_MINOR 9 -#define JSONCPP_VERSION_PATCH 2 -#define JSONCPP_VERSION_QUALIFIER +#define JSONCPP_VERSION_PATCH 8 #define JSONCPP_VERSION_HEXA \ ((JSONCPP_VERSION_MAJOR << 24) | (JSONCPP_VERSION_MINOR << 16) | \ (JSONCPP_VERSION_PATCH << 8)) -#ifdef JSONCPP_USING_SECURE_MEMORY -#undef JSONCPP_USING_SECURE_MEMORY +#if !defined(JSONCPP_USE_SECURE_MEMORY) +#define JSONCPP_USE_SECURE_MEMORY 0 #endif -#define JSONCPP_USING_SECURE_MEMORY 0 // If non-zero, the library zeroes any memory that it has allocated before // it frees its memory. diff --git a/include/json/writer.h b/include/json/writer.h index fb0852a0c..7c56a2107 100644 --- a/include/json/writer.h +++ b/include/json/writer.h @@ -20,7 +20,8 @@ #pragma warning(disable : 4251) #endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { @@ -63,7 +64,7 @@ class JSON_API StreamWriter { */ virtual StreamWriter* newStreamWriter() const = 0; }; // Factory -}; // StreamWriter +}; // StreamWriter /** \brief Write into stringstream, then return string, for convenience. * A StreamWriter will be created from the factory, used, and then deleted. @@ -110,6 +111,8 @@ class JSON_API StreamWriterBuilder : public StreamWriter::Factory { * - Number of precision digits for formatting of real values. * - "precisionType": "significant"(default) or "decimal" * - Type of precision for formatting of real values. + * - "emitUTF8": false or true + * - If true, outputs raw UTF8 strings instead of escaping them. * You can examine 'settings_` yourself * to see the defaults. You can also write and read them just like any @@ -145,7 +148,7 @@ class JSON_API StreamWriterBuilder : public StreamWriter::Factory { /** \brief Abstract class for writers. * \deprecated Use StreamWriter. (And really, this is an implementation detail.) */ -class JSONCPP_DEPRECATED("Use StreamWriter instead") JSON_API Writer { +class JSON_API Writer { public: virtual ~Writer(); @@ -165,8 +168,7 @@ class JSONCPP_DEPRECATED("Use StreamWriter instead") JSON_API Writer { #pragma warning(push) #pragma warning(disable : 4996) // Deriving from deprecated class #endif -class JSONCPP_DEPRECATED("Use StreamWriterBuilder instead") JSON_API FastWriter - : public Writer { +class JSON_API FastWriter : public Writer { public: FastWriter(); ~FastWriter() override = default; @@ -215,7 +217,7 @@ class JSONCPP_DEPRECATED("Use StreamWriterBuilder instead") JSON_API FastWriter * - otherwise, it the values do not fit on one line, or the array contains * object or non empty array, then print one value per line. * - * If the Value have comments then they are outputed according to their + * If the Value have comments then they are outputted according to their *#CommentPlacement. * * \sa Reader, Value, Value::setComment() @@ -225,8 +227,7 @@ class JSONCPP_DEPRECATED("Use StreamWriterBuilder instead") JSON_API FastWriter #pragma warning(push) #pragma warning(disable : 4996) // Deriving from deprecated class #endif -class JSONCPP_DEPRECATED("Use StreamWriterBuilder instead") JSON_API - StyledWriter : public Writer { +class JSON_API StyledWriter : public Writer { public: StyledWriter(); ~StyledWriter() override = default; @@ -284,7 +285,7 @@ class JSONCPP_DEPRECATED("Use StreamWriterBuilder instead") JSON_API * - otherwise, it the values do not fit on one line, or the array contains * object or non empty array, then print one value per line. * - * If the Value have comments then they are outputed according to their + * If the Value have comments then they are outputted according to their #CommentPlacement. * * \sa Reader, Value, Value::setComment() @@ -294,8 +295,7 @@ class JSONCPP_DEPRECATED("Use StreamWriterBuilder instead") JSON_API #pragma warning(push) #pragma warning(disable : 4996) // Deriving from deprecated class #endif -class JSONCPP_DEPRECATED("Use StreamWriterBuilder instead") JSON_API - StyledStreamWriter { +class JSON_API StyledStreamWriter { public: /** * \param indentation Each level will be indented by this amount extra. @@ -351,6 +351,7 @@ String JSON_API valueToString( PrecisionType precisionType = PrecisionType::significantDigits); String JSON_API valueToString(bool value); String JSON_API valueToQuotedString(const char* value); +String JSON_API valueToQuotedString(const char* value, size_t length); /// \brief Output using the StyledStreamWriter. /// \see Json::operator>>() diff --git a/jsoncpp-namespaced-targets.cmake b/jsoncpp-namespaced-targets.cmake new file mode 100644 index 000000000..70a79ee7f --- /dev/null +++ b/jsoncpp-namespaced-targets.cmake @@ -0,0 +1,9 @@ +if (NOT TARGET JsonCpp::JsonCpp) + if (TARGET jsoncpp_static) + add_library(JsonCpp::JsonCpp INTERFACE IMPORTED) + set_target_properties(JsonCpp::JsonCpp PROPERTIES INTERFACE_LINK_LIBRARIES "jsoncpp_static") + elseif (TARGET jsoncpp_lib) + add_library(JsonCpp::JsonCpp INTERFACE IMPORTED) + set_target_properties(JsonCpp::JsonCpp PROPERTIES INTERFACE_LINK_LIBRARIES "jsoncpp_lib") + endif () +endif () diff --git a/jsoncppConfig.cmake.in b/jsoncppConfig.cmake.in new file mode 100644 index 000000000..35fed3687 --- /dev/null +++ b/jsoncppConfig.cmake.in @@ -0,0 +1,11 @@ +cmake_policy(PUSH) +cmake_policy(VERSION 3.5...3.26) + +@PACKAGE_INIT@ + +include ( "${CMAKE_CURRENT_LIST_DIR}/jsoncpp-targets.cmake" ) +include ( "${CMAKE_CURRENT_LIST_DIR}/jsoncpp-namespaced-targets.cmake" ) + +check_required_components(JsonCpp) + +cmake_policy(POP) diff --git a/jsoncppConfig.cmake.meson.in b/jsoncppConfig.cmake.meson.in new file mode 100644 index 000000000..be8852d0c --- /dev/null +++ b/jsoncppConfig.cmake.meson.in @@ -0,0 +1,6 @@ +@PACKAGE_INIT@ + +@MESON_SHARED_TARGET@ +@MESON_STATIC_TARGET@ + +include ( "${CMAKE_CURRENT_LIST_DIR}/jsoncpp-namespaced-targets.cmake" ) diff --git a/meson.build b/meson.build index 1bc94a8af..e08314bcd 100644 --- a/meson.build +++ b/meson.build @@ -2,23 +2,24 @@ project( 'jsoncpp', 'cpp', - # Note: version must be updated in three places when doing a release. This + # Note: version must be updated in four places when doing a release. This # annoying process ensures that amalgamate, CMake, and meson all report the # correct version. # 1. /meson.build # 2. /include/json/version.h # 3. /CMakeLists.txt + # 4. /MODULE.bazel # IMPORTANT: also update the SOVERSION!! - version : '1.9.2', + version : '1.9.8', default_options : [ 'buildtype=release', 'cpp_std=c++11', 'warning_level=1'], license : 'Public Domain', - meson_version : '>= 0.49.0') + meson_version : '>= 0.54.0') -jsoncpp_headers = [ +jsoncpp_headers = files([ 'include/json/allocator.h', 'include/json/assertions.h', 'include/json/config.h', @@ -28,7 +29,8 @@ jsoncpp_headers = [ 'include/json/reader.h', 'include/json/value.h', 'include/json/version.h', - 'include/json/writer.h'] + 'include/json/writer.h', +]) jsoncpp_include_directories = include_directories('include') install_headers( @@ -44,13 +46,12 @@ else endif jsoncpp_lib = library( - 'jsoncpp', - [ jsoncpp_headers, - 'src/lib_json/json_tool.h', + 'jsoncpp', files([ 'src/lib_json/json_reader.cpp', 'src/lib_json/json_value.cpp', - 'src/lib_json/json_writer.cpp'], - soversion : 22, + 'src/lib_json/json_writer.cpp', + ]), + soversion : 27, install : true, include_directories : jsoncpp_include_directories, cpp_args: dll_export_flag) @@ -62,22 +63,62 @@ import('pkgconfig').generate( filebase : 'jsoncpp', description : 'A C++ library for interacting with JSON') +cmakeconf = configuration_data() +cmakeconf.set('MESON_LIB_DIR', get_option('libdir')) +cmakeconf.set('MESON_INCLUDE_DIR', get_option('includedir')) + +fs = import('fs') +if get_option('default_library') == 'shared' + shared_name = fs.name(jsoncpp_lib.full_path()) +endif +if get_option('default_library') == 'static' + static_name = fs.name(jsoncpp_lib.full_path()) +endif +if get_option('default_library') == 'both' + shared_name = fs.name(jsoncpp_lib.get_shared_lib().full_path()) + static_name = fs.name(jsoncpp_lib.get_static_lib().full_path()) +endif + +if get_option('default_library') == 'shared' or get_option('default_library') == 'both' + cmakeconf.set('MESON_SHARED_TARGET', ''' +add_library(jsoncpp_lib IMPORTED SHARED) +set_target_properties(jsoncpp_lib PROPERTIES + IMPORTED_LOCATION "''' + join_paths('${PACKAGE_PREFIX_DIR}', get_option('libdir'), shared_name) + '''" + INTERFACE_INCLUDE_DIRECTORIES "''' + join_paths('${PACKAGE_PREFIX_DIR}', get_option('includedir')) + '")') +endif +if get_option('default_library') == 'static' or get_option('default_library') == 'both' + cmakeconf.set('MESON_STATIC_TARGET', ''' +add_library(jsoncpp_static IMPORTED STATIC) +set_target_properties(jsoncpp_static PROPERTIES + IMPORTED_LOCATION "''' + join_paths('${PACKAGE_PREFIX_DIR}', get_option('libdir'), static_name) + '''" + INTERFACE_INCLUDE_DIRECTORIES "''' + join_paths('${PACKAGE_PREFIX_DIR}', get_option('includedir')) + '")') +endif + +import('cmake').configure_package_config_file( + name: 'jsoncpp', + input: 'jsoncppConfig.cmake.meson.in', + configuration: cmakeconf) +install_data('jsoncpp-namespaced-targets.cmake', install_dir : join_paths(get_option('libdir'), 'cmake', jsoncpp_lib.name())) + # for libraries bundling jsoncpp jsoncpp_dep = declare_dependency( include_directories : jsoncpp_include_directories, link_with : jsoncpp_lib, - version : meson.project_version(), - ) + version : meson.project_version()) # tests -python = import('python').find_installation() +if meson.is_subproject() or not get_option('tests') + subdir_done() +endif + +python = find_program('python3') jsoncpp_test = executable( - 'jsoncpp_test', - [ 'src/test_lib_json/jsontest.cpp', - 'src/test_lib_json/jsontest.h', + 'jsoncpp_test', files([ + 'src/test_lib_json/jsontest.cpp', 'src/test_lib_json/main.cpp', - 'src/test_lib_json/fuzz.cpp'], + 'src/test_lib_json/fuzz.cpp', + ]), include_directories : jsoncpp_include_directories, link_with : jsoncpp_lib, install : false, @@ -100,5 +141,17 @@ test( '-B', join_paths(meson.current_source_dir(), 'test/runjsontests.py'), jsontestrunner, - join_paths(meson.current_source_dir(), 'test/data')] + join_paths(meson.current_source_dir(), 'test/data')], + ) +test( + 'jsonchecker_jsontestrunner', + python, + is_parallel : false, + args : [ + '-B', + join_paths(meson.current_source_dir(), 'test/runjsontests.py'), + '--with-json-checker', + jsontestrunner, + join_paths(meson.current_source_dir(), 'test/data')], + workdir : join_paths(meson.current_source_dir(), 'test/data'), ) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 000000000..9c215ae6f --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,5 @@ +option( + 'tests', + type : 'boolean', + value : true, + description : 'Enable building tests') diff --git a/pkg-config/jsoncpp.pc.in b/pkg-config/jsoncpp.pc.in index d4fa9ef26..2a2221069 100644 --- a/pkg-config/jsoncpp.pc.in +++ b/pkg-config/jsoncpp.pc.in @@ -1,11 +1,11 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=@CMAKE_INSTALL_PREFIX@ -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ -includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=@libdir_for_pc_file@ +includedir=@includedir_for_pc_file@ Name: jsoncpp Description: A C++ library for interacting with JSON -Version: @JSONCPP_VERSION@ +Version: @PROJECT_VERSION@ URL: https://github.com/open-source-parsers/jsoncpp Libs: -L${libdir} -ljsoncpp Cflags: -I${includedir} diff --git a/reformat.sh b/reformat.sh new file mode 100755 index 000000000..86bc066f1 --- /dev/null +++ b/reformat.sh @@ -0,0 +1 @@ +find src include example -name '*.cpp' -or -name '*.h' -or -name '*.inl' | xargs clang-format -i diff --git a/src/jsontestrunner/BUILD.bazel b/src/jsontestrunner/BUILD.bazel new file mode 100644 index 000000000..fa1f39510 --- /dev/null +++ b/src/jsontestrunner/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") + +cc_binary( + name = "jsontestrunner", + srcs = ["main.cpp"], + visibility = ["//test:__pkg__"], + deps = ["//:jsoncpp"], +) diff --git a/src/jsontestrunner/CMakeLists.txt b/src/jsontestrunner/CMakeLists.txt index 210e0900b..7f536d318 100644 --- a/src/jsontestrunner/CMakeLists.txt +++ b/src/jsontestrunner/CMakeLists.txt @@ -1,26 +1,28 @@ if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - # The new Python3 module is much more robust than the previous PythonInterp - find_package (Python3 COMPONENTS Interpreter) - # Set variables for backwards compatibility with cmake < 3.12.0 - set(PYTHONINTERP_FOUND ${Python3_Interpreter_FOUND}) - set(PYTHON_EXECUTABLE ${Python3_EXECUTABLE}) + # The new Python3 module is much more robust than the previous PythonInterp + find_package(Python3 COMPONENTS Interpreter) + # Set variables for backwards compatibility with cmake < 3.12.0 + set(PYTHONINTERP_FOUND ${Python3_Interpreter_FOUND}) + set(PYTHON_EXECUTABLE ${Python3_EXECUTABLE}) else() - set(Python_ADDITIONAL_VERSIONS 3.8) - find_package(PythonInterp 3) + set(Python_ADDITIONAL_VERSIONS 3.8) + find_package(PythonInterp 3) endif() add_executable(jsontestrunner_exe - main.cpp - ) + main.cpp +) if(BUILD_SHARED_LIBS) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_definitions( JSON_DLL ) - else() - add_definitions( -DJSON_DLL ) - endif() + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) + add_compile_definitions( JSON_DLL ) + else() + add_definitions(-DJSON_DLL) + endif() + target_link_libraries(jsontestrunner_exe jsoncpp_lib) +else() + target_link_libraries(jsontestrunner_exe jsoncpp_static) endif() -target_link_libraries(jsontestrunner_exe jsoncpp_lib) set_target_properties(jsontestrunner_exe PROPERTIES OUTPUT_NAME jsontestrunner_exe) @@ -32,18 +34,23 @@ if(PYTHONINTERP_FOUND) # Run unit tests in post-build # (default cmake workflow hides away the test result into a file, resulting in poor dev workflow?!?) add_custom_target(jsoncpp_readerwriter_tests - "${PYTHON_EXECUTABLE}" -B "${RUNJSONTESTS_PATH}" $ "${TEST_DIR}/data" - DEPENDS jsontestrunner_exe jsoncpp_test - ) + "${PYTHON_EXECUTABLE}" -B "${RUNJSONTESTS_PATH}" $ "${TEST_DIR}/data" + DEPENDS jsontestrunner_exe jsoncpp_test + ) add_custom_target(jsoncpp_check DEPENDS jsoncpp_readerwriter_tests) ## Create tests for dashboard submission, allows easy review of CI results https://my.cdash.org/index.php?project=jsoncpp add_test(NAME jsoncpp_readerwriter - COMMAND "${PYTHON_EXECUTABLE}" -B "${RUNJSONTESTS_PATH}" $ "${TEST_DIR}/data" - WORKING_DIRECTORY "${TEST_DIR}/data" + COMMAND "${PYTHON_EXECUTABLE}" -B "${RUNJSONTESTS_PATH}" $ "${TEST_DIR}/data" + WORKING_DIRECTORY "${TEST_DIR}/data" ) add_test(NAME jsoncpp_readerwriter_json_checker - COMMAND "${PYTHON_EXECUTABLE}" -B "${RUNJSONTESTS_PATH}" --with-json-checker $ "${TEST_DIR}/data" - WORKING_DIRECTORY "${TEST_DIR}/data" + COMMAND "${PYTHON_EXECUTABLE}" -B "${RUNJSONTESTS_PATH}" --with-json-checker $ "${TEST_DIR}/data" + WORKING_DIRECTORY "${TEST_DIR}/data" + ) + + # Both tests write .actual/.actual-rewrite along with test data, need to prevent collision when running tests via ctest -j + set_tests_properties(jsoncpp_readerwriter jsoncpp_readerwriter_json_checker + PROPERTIES RESOURCE_LOCK "test_data_files" ) endif() diff --git a/src/jsontestrunner/main.cpp b/src/jsontestrunner/main.cpp index f6b1a4dc3..ab6a80039 100644 --- a/src/jsontestrunner/main.cpp +++ b/src/jsontestrunner/main.cpp @@ -57,10 +57,10 @@ static Json::String readInputTestFile(const char* path) { if (!file) return ""; fseek(file, 0, SEEK_END); - long const size = ftell(file); - const auto usize = static_cast(size); + auto const size = ftell(file); + auto const usize = static_cast(size); fseek(file, 0, SEEK_SET); - char* buffer = new char[size + 1]; + auto buffer = new char[size + 1]; buffer[size] = 0; Json::String text; if (fread(buffer, 1, usize, file) == usize) @@ -111,7 +111,7 @@ static void printValueTree(FILE* fout, Json::Value& value, Json::Value::Members members(value.getMemberNames()); std::sort(members.begin(), members.end()); Json::String suffix = *(path.end() - 1) == '.' ? "" : "."; - for (auto name : members) { + for (const auto& name : members) { printValueTree(fout, value[name], path + suffix + name); } } break; @@ -240,11 +240,14 @@ static int parseCommandLine(int argc, const char* argv[], Options* opts) { return printUsage(argv); } int index = 1; - if (Json::String(argv[index]) == "--json-checker") { - opts->features = Json::Features::strictMode(); + if (Json::String(argv[index]) == "--parse-only") { opts->parseOnly = true; ++index; } + if (Json::String(argv[index]) == "--strict") { + opts->features = Json::Features::strictMode(); + ++index; + } if (Json::String(argv[index]) == "--json-config") { printConfig(); return 3; @@ -335,6 +338,7 @@ int main(int argc, const char* argv[]) { std::cerr << "Unhandled exception:" << std::endl << e.what() << std::endl; return 1; } + return 0; } #if defined(__GNUC__) diff --git a/src/lib_json/CMakeLists.txt b/src/lib_json/CMakeLists.txt index b56788e27..7197ba793 100644 --- a/src/lib_json/CMakeLists.txt +++ b/src/lib_json/CMakeLists.txt @@ -1,13 +1,7 @@ -if( CMAKE_COMPILER_IS_GNUCXX ) - #Get compiler version. - execute_process( COMMAND ${CMAKE_CXX_COMPILER} -dumpversion - OUTPUT_VARIABLE GNUCXX_VERSION ) - +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.1.2) #-Werror=* was introduced -after- GCC 4.1.2 - if( GNUCXX_VERSION VERSION_GREATER 4.1.2 ) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=strict-aliasing") - endif() -endif( CMAKE_COMPILER_IS_GNUCXX ) + add_compile_options("-Werror=strict-aliasing") +endif() include(CheckIncludeFileCXX) include(CheckTypeSize) @@ -17,33 +11,23 @@ include(CheckCXXSymbolExists) check_include_file_cxx(clocale HAVE_CLOCALE) check_cxx_symbol_exists(localeconv clocale HAVE_LOCALECONV) -if(CMAKE_VERSION VERSION_LESS 3.0.0) - # The "LANGUAGE CXX" parameter is not supported in CMake versions below 3, - # so the C compiler and header has to be used. - check_include_file(locale.h HAVE_LOCALE_H) - set(CMAKE_EXTRA_INCLUDE_FILES locale.h) - check_type_size("struct lconv" LCONV_SIZE) - unset(CMAKE_EXTRA_INCLUDE_FILES) - check_struct_has_member("struct lconv" decimal_point locale.h HAVE_DECIMAL_POINT) -else() - set(CMAKE_EXTRA_INCLUDE_FILES clocale) - check_type_size(lconv LCONV_SIZE LANGUAGE CXX) - unset(CMAKE_EXTRA_INCLUDE_FILES) - check_struct_has_member(lconv decimal_point clocale HAVE_DECIMAL_POINT LANGUAGE CXX) -endif() +set(CMAKE_EXTRA_INCLUDE_FILES clocale) +check_type_size(lconv LCONV_SIZE LANGUAGE CXX) +unset(CMAKE_EXTRA_INCLUDE_FILES) +check_struct_has_member(lconv decimal_point clocale HAVE_DECIMAL_POINT LANGUAGE CXX) if(NOT (HAVE_CLOCALE AND HAVE_LCONV_SIZE AND HAVE_DECIMAL_POINT AND HAVE_LOCALECONV)) message(WARNING "Locale functionality is not supported") if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_definitions(JSONCPP_NO_LOCALE_SUPPORT) + add_compile_definitions(JSONCPP_NO_LOCALE_SUPPORT) else() - add_definitions(-DJSONCPP_NO_LOCALE_SUPPORT) + add_definitions(-DJSONCPP_NO_LOCALE_SUPPORT) endif() endif() -set( JSONCPP_INCLUDE_DIR ../../include ) +set(JSONCPP_INCLUDE_DIR ../../include) -set( PUBLIC_HEADERS +set(PUBLIC_HEADERS ${JSONCPP_INCLUDE_DIR}/json/config.h ${JSONCPP_INCLUDE_DIR}/json/forwards.h ${JSONCPP_INCLUDE_DIR}/json/json_features.h @@ -52,49 +36,29 @@ set( PUBLIC_HEADERS ${JSONCPP_INCLUDE_DIR}/json/version.h ${JSONCPP_INCLUDE_DIR}/json/writer.h ${JSONCPP_INCLUDE_DIR}/json/assertions.h - ) +) -source_group( "Public API" FILES ${PUBLIC_HEADERS} ) +source_group("Public API" FILES ${PUBLIC_HEADERS}) -set(jsoncpp_sources - json_tool.h - json_reader.cpp - json_valueiterator.inl - json_value.cpp - json_writer.cpp) +set(JSONCPP_SOURCES + json_tool.h + json_reader.cpp + json_valueiterator.inl + json_value.cpp + json_writer.cpp +) # Install instructions for this target if(JSONCPP_WITH_CMAKE_PACKAGE) set(INSTALL_EXPORT EXPORT jsoncpp) -else(JSONCPP_WITH_CMAKE_PACKAGE) +else() set(INSTALL_EXPORT) endif() - -if(BUILD_SHARED_LIBS) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_definitions( JSON_DLL_BUILD ) - else() - add_definitions( -DJSON_DLL_BUILD ) - endif() -endif() - - -add_library(jsoncpp_lib ${PUBLIC_HEADERS} ${jsoncpp_sources}) -set_target_properties( jsoncpp_lib PROPERTIES VERSION ${JSONCPP_VERSION} SOVERSION ${JSONCPP_SOVERSION}) -set_target_properties( jsoncpp_lib PROPERTIES OUTPUT_NAME jsoncpp - DEBUG_OUTPUT_NAME jsoncpp${DEBUG_LIBNAME_SUFFIX} ) -set_target_properties( jsoncpp_lib PROPERTIES POSITION_INDEPENDENT_CODE ON) - -# Set library's runtime search path on OSX -if(APPLE) - set_target_properties( jsoncpp_lib PROPERTIES INSTALL_RPATH "@loader_path/." ) -endif() - # Specify compiler features required when compiling a given target. # See https://cmake.org/cmake/help/v3.1/prop_gbl/CMAKE_CXX_KNOWN_FEATURES.html#prop_gbl:CMAKE_CXX_KNOWN_FEATURES # for complete list of features available -target_compile_features(jsoncpp_lib PUBLIC +list(APPEND REQUIRED_FEATURES cxx_std_11 # Compiler mode is aware of C++ 11. #MSVC 1900 cxx_alignas # Alignment control alignas, as defined in N2341. #MSVC 1900 cxx_alignof # Alignment control alignof, as defined in N2341. @@ -141,14 +105,103 @@ target_compile_features(jsoncpp_lib PUBLIC cxx_variadic_templates # Variadic templates, as defined in N2242. ) -install( TARGETS jsoncpp_lib ${INSTALL_EXPORT} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) -if(NOT CMAKE_VERSION VERSION_LESS 2.8.11) - target_include_directories( jsoncpp_lib PUBLIC - $ - $ - $) +if(BUILD_SHARED_LIBS) + set(SHARED_LIB ${PROJECT_NAME}_lib) + add_library(${SHARED_LIB} SHARED ${PUBLIC_HEADERS} ${JSONCPP_SOURCES}) + set_target_properties(${SHARED_LIB} PROPERTIES + OUTPUT_NAME jsoncpp + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_SOVERSION} + POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS} + ) + + target_compile_definitions(${SHARED_LIB} PRIVATE JSON_DLL_BUILD) + + # Set library's runtime search path on OSX + if(APPLE) + set_target_properties(${SHARED_LIB} PROPERTIES INSTALL_RPATH "@loader_path/.") + endif() + + target_compile_features(${SHARED_LIB} PUBLIC ${REQUIRED_FEATURES}) + + + target_include_directories(${SHARED_LIB} PUBLIC + $ + $ + ) + + list(APPEND CMAKE_TARGETS ${SHARED_LIB}) +endif() + +if(BUILD_STATIC_LIBS) + set(STATIC_LIB ${PROJECT_NAME}_static) + add_library(${STATIC_LIB} STATIC ${PUBLIC_HEADERS} ${JSONCPP_SOURCES}) + + # avoid name clashes on windows as the shared import lib is also named jsoncpp.lib + if(NOT DEFINED STATIC_SUFFIX AND BUILD_SHARED_LIBS) + if (MSVC OR ("${CMAKE_C_SIMULATE_ID}" STREQUAL "MSVC")) + set(STATIC_SUFFIX "_static") + else() + set(STATIC_SUFFIX "") + endif() + endif() + + set_target_properties(${STATIC_LIB} PROPERTIES + OUTPUT_NAME jsoncpp${STATIC_SUFFIX} + VERSION ${PROJECT_VERSION} + ) + + # Set library's runtime search path on OSX + if(APPLE) + set_target_properties(${STATIC_LIB} PROPERTIES INSTALL_RPATH "@loader_path/.") + endif() + + target_compile_features(${STATIC_LIB} PUBLIC ${REQUIRED_FEATURES}) + + + target_include_directories(${STATIC_LIB} PUBLIC + $ + $ + ) + + list(APPEND CMAKE_TARGETS ${STATIC_LIB}) +endif() + +if(BUILD_OBJECT_LIBS) + set(OBJECT_LIB ${PROJECT_NAME}_object) + add_library(${OBJECT_LIB} OBJECT ${PUBLIC_HEADERS} ${JSONCPP_SOURCES}) + + set_target_properties(${OBJECT_LIB} PROPERTIES + OUTPUT_NAME jsoncpp + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_SOVERSION} + POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS} + ) + + # Set library's runtime search path on OSX + if(APPLE) + set_target_properties(${OBJECT_LIB} PROPERTIES INSTALL_RPATH "@loader_path/.") + endif() + + target_compile_features(${OBJECT_LIB} PUBLIC ${REQUIRED_FEATURES}) + + + target_include_directories(${OBJECT_LIB} PUBLIC + $ + $ + ) + + list(APPEND CMAKE_TARGETS ${OBJECT_LIB}) +endif() + +if (JSONCPP_WITH_INSTALL) + +install(TARGETS ${CMAKE_TARGETS} ${INSTALL_EXPORT} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + OBJECTS DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + endif() diff --git a/src/lib_json/json_reader.cpp b/src/lib_json/json_reader.cpp index 9818b5bf2..83743f73b 100644 --- a/src/lib_json/json_reader.cpp +++ b/src/lib_json/json_reader.cpp @@ -10,10 +10,13 @@ #include #include #endif // if !defined(JSON_IS_AMALGAMATION) +#include #include +#include #include #include #include +#include #include #include #include @@ -21,13 +24,6 @@ #include #include -#if __cplusplus >= 201103L - -#if !defined(sscanf) -#define sscanf std::sscanf -#endif - -#endif //__cplusplus #if defined(_MSC_VER) #if !defined(_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES) @@ -43,7 +39,7 @@ // Define JSONCPP_DEPRECATED_STACK_LIMIT as an appropriate integer at compile // time to change the stack limit #if !defined(JSONCPP_DEPRECATED_STACK_LIMIT) -#define JSONCPP_DEPRECATED_STACK_LIMIT 1000 +#define JSONCPP_DEPRECATED_STACK_LIMIT 256 #endif static size_t const stackLimit_g = @@ -51,11 +47,7 @@ static size_t const stackLimit_g = namespace Json { -#if __cplusplus >= 201103L || (defined(_CPPLIB_VER) && _CPPLIB_VER >= 520) using CharReaderPtr = std::unique_ptr; -#else -using CharReaderPtr = std::auto_ptr; -#endif // Implementation of class Features // //////////////////////////////// @@ -67,7 +59,6 @@ Features Features::all() { return {}; } Features Features::strictMode() { Features features; features.allowComments_ = false; - features.allowTrailingCommas_ = false; features.strictRoot_ = true; features.allowDroppedNullPlaceholders_ = false; features.allowNumericKeys_ = false; @@ -78,10 +69,7 @@ Features Features::strictMode() { // //////////////////////////////// bool Reader::containsNewLine(Reader::Location begin, Reader::Location end) { - for (; begin < end; ++begin) - if (*begin == '\n' || *begin == '\r') - return true; - return false; + return std::any_of(begin, end, [](char b) { return b == '\n' || b == '\r'; }); } // Class Reader @@ -100,16 +88,10 @@ bool Reader::parse(const std::string& document, Value& root, } bool Reader::parse(std::istream& is, Value& root, bool collectComments) { - // std::istream_iterator begin(is); - // std::istream_iterator end; - // Those would allow streamed input from a file, if parse() were a - // template function. - - // Since String is reference-counted, this at least does not - // create an extra copy. - String doc; - std::getline(is, doc, static_cast EOF); - return parse(doc.data(), doc.data() + doc.size(), root, collectComments); + document_.assign(std::istreambuf_iterator(is), + std::istreambuf_iterator()); + return parse(document_.data(), document_.data() + document_.size(), root, + collectComments); } bool Reader::parse(const char* beginDoc, const char* endDoc, Value& root, @@ -132,7 +114,7 @@ bool Reader::parse(const char* beginDoc, const char* endDoc, Value& root, bool successful = readValue(); Token token; - skipCommentTokens(token); + readTokenSkippingComments(token); if (collectComments_ && !commentsBefore_.empty()) root.setComment(commentsBefore_, commentAfter); if (features_.strictRoot_) { @@ -157,10 +139,15 @@ bool Reader::readValue() { // after calling readValue(). parse() executes one nodes_.push(), so > instead // of >=. if (nodes_.size() > stackLimit_g) +#if JSON_USE_EXCEPTION throwRuntimeError("Exceeded stackLimit in readValue()."); +#else + // throwRuntimeError aborts. Don't abort here. + return false; +#endif Token token; - skipCommentTokens(token); + readTokenSkippingComments(token); bool successful = true; if (collectComments_ && !commentsBefore_.empty()) { @@ -228,14 +215,14 @@ bool Reader::readValue() { return successful; } -void Reader::skipCommentTokens(Token& token) { +bool Reader::readTokenSkippingComments(Token& token) { + bool success = readToken(token); if (features_.allowComments_) { - do { - readToken(token); - } while (token.type_ == tokenComment); - } else { - readToken(token); + while (success && token.type_ == tokenComment) { + success = readToken(token); + } } + return success; } bool Reader::readToken(Token& token) { @@ -449,15 +436,8 @@ bool Reader::readObject(Token& token) { Value init(objectValue); currentValue().swapPayload(init); currentValue().setOffsetStart(token.start_ - begin_); - while (readToken(tokenName)) { - bool initialTokenOk = true; - while (tokenName.type_ == tokenComment && initialTokenOk) - initialTokenOk = readToken(tokenName); - if (!initialTokenOk) - break; - if (tokenName.type_ == tokenObjectEnd && - (name.empty() || - features_.allowTrailingCommas_)) // empty object or trailing comma + while (readTokenSkippingComments(tokenName)) { + if (tokenName.type_ == tokenObjectEnd && name.empty()) // empty object return true; name.clear(); if (tokenName.type_ == tokenString) { @@ -485,15 +465,11 @@ bool Reader::readObject(Token& token) { return recoverFromError(tokenObjectEnd); Token comma; - if (!readToken(comma) || - (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator && - comma.type_ != tokenComment)) { + if (!readTokenSkippingComments(comma) || + (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator)) { return addErrorAndRecover("Missing ',' or '}' in object declaration", comma, tokenObjectEnd); } - bool finalizeTokenOk = true; - while (comma.type_ == tokenComment && finalizeTokenOk) - finalizeTokenOk = readToken(comma); if (comma.type_ == tokenObjectEnd) return true; } @@ -505,20 +481,15 @@ bool Reader::readArray(Token& token) { Value init(arrayValue); currentValue().swapPayload(init); currentValue().setOffsetStart(token.start_ - begin_); + skipSpaces(); + if (current_ != end_ && *current_ == ']') // empty array + { + Token endArray; + readToken(endArray); + return true; + } int index = 0; for (;;) { - skipSpaces(); - if (current_ != end_ && *current_ == ']' && - (index == 0 || - (features_.allowTrailingCommas_ && - !features_.allowDroppedNullPlaceholders_))) // empty array or trailing - // comma - { - Token endArray; - readToken(endArray); - return true; - } - Value& value = currentValue()[index++]; nodes_.push(&value); bool ok = readValue(); @@ -528,10 +499,7 @@ bool Reader::readArray(Token& token) { Token currentToken; // Accept Comment after last item in the array. - ok = readToken(currentToken); - while (currentToken.type_ == tokenComment && ok) { - ok = readToken(currentToken); - } + ok = readTokenSkippingComments(currentToken); bool badTokenType = (currentToken.type_ != tokenArraySeparator && currentToken.type_ != tokenArrayEnd); if (!ok || badTokenType) { @@ -609,11 +577,17 @@ bool Reader::decodeDouble(Token& token) { bool Reader::decodeDouble(Token& token, Value& decoded) { double value = 0; - String buffer(token.start_, token.end_); - IStringStream is(buffer); - if (!(is >> value)) - return addError( - "'" + String(token.start_, token.end_) + "' is not a number.", token); + IStringStream is(String(token.start_, token.end_)); + is.imbue(std::locale::classic()); + if (!(is >> value)) { + if (value == std::numeric_limits::max()) + value = std::numeric_limits::infinity(); + else if (value == std::numeric_limits::lowest()) + value = -std::numeric_limits::infinity(); + else if (!std::isinf(value)) + return addError( + "'" + String(token.start_, token.end_) + "' is not a number.", token); + } decoded = value; return true; } @@ -637,7 +611,7 @@ bool Reader::decodeString(Token& token, String& decoded) { Char c = *current++; if (c == '"') break; - else if (c == '\\') { + if (c == '\\') { if (current == end) return addError("Empty escape sequence in string", token, current); Char escape = *current++; @@ -676,6 +650,8 @@ bool Reader::decodeString(Token& token, String& decoded) { return addError("Bad escape sequence in string", token, current); } } else { + if (static_cast(c) < 0x20) + return addError("Control character in string", token, current - 1); decoded += c; } } @@ -777,7 +753,7 @@ void Reader::getLocationLineAndColumn(Location location, int& line, while (current < location && current != end_) { Char c = *current++; if (c == '\r') { - if (*current == '\n') + if (current != end_ && *current == '\n') ++current; lastLineStart = current; ++line; @@ -879,6 +855,7 @@ class OurFeatures { bool failIfExtra_; bool rejectDupKeys_; bool allowSpecialFloats_; + bool skipBom_; size_t stackLimit_; }; // OurFeatures @@ -893,17 +870,12 @@ class OurReader { public: using Char = char; using Location = const Char*; - struct StructuredError { - ptrdiff_t offset_start; - ptrdiff_t offset_limit; - String message; - }; explicit OurReader(OurFeatures const& features); bool parse(const char* beginDoc, const char* endDoc, Value& root, bool collectComments = true); String getFormattedErrorMessages() const; - std::vector getStructuredErrors() const; + std::vector getStructuredErrors() const; private: OurReader(OurReader const&); // no impl @@ -946,7 +918,9 @@ class OurReader { using Errors = std::deque; bool readToken(Token& token); + bool readTokenSkippingComments(Token& token); void skipSpaces(); + void skipBom(bool skipBom); bool match(const Char* pattern, int patternLength); bool readComment(); bool readCStyleComment(bool* containsNewLineResult); @@ -978,7 +952,6 @@ class OurReader { int& column) const; String getLocationLineAndColumn(Location location) const; void addComment(Location begin, Location end, CommentPlacement placement); - void skipCommentTokens(Token& token); static String normalizeEOL(Location begin, Location end); static bool containsNewLine(Location begin, Location end); @@ -1004,10 +977,7 @@ class OurReader { bool OurReader::containsNewLine(OurReader::Location begin, OurReader::Location end) { - for (; begin < end; ++begin) - if (*begin == '\n' || *begin == '\r') - return true; - return false; + return std::any_of(begin, end, [](char b) { return b == '\n' || b == '\r'; }); } OurReader::OurReader(OurFeatures const& features) : features_(features) {} @@ -1030,10 +1000,12 @@ bool OurReader::parse(const char* beginDoc, const char* endDoc, Value& root, nodes_.pop(); nodes_.push(&root); + // skip byte order mark if it exists at the beginning of the UTF-8 text. + skipBom(features_.skipBom_); bool successful = readValue(); nodes_.pop(); Token token; - skipCommentTokens(token); + readTokenSkippingComments(token); if (features_.failIfExtra_ && (token.type_ != tokenEndOfStream)) { addError("Extra non-whitespace after JSON value.", token); return false; @@ -1061,7 +1033,7 @@ bool OurReader::readValue() { if (nodes_.size() > features_.stackLimit_) throwRuntimeError("Exceeded stackLimit in readValue()."); Token token; - skipCommentTokens(token); + readTokenSkippingComments(token); bool successful = true; if (collectComments_ && !commentsBefore_.empty()) { @@ -1148,14 +1120,14 @@ bool OurReader::readValue() { return successful; } -void OurReader::skipCommentTokens(Token& token) { +bool OurReader::readTokenSkippingComments(Token& token) { + bool success = readToken(token); if (features_.allowComments_) { - do { - readToken(token); - } while (token.type_ == tokenComment); - } else { - readToken(token); + while (success && token.type_ == tokenComment) { + success = readToken(token); + } } + return success; } bool OurReader::readToken(Token& token) { @@ -1184,8 +1156,11 @@ bool OurReader::readToken(Token& token) { if (features_.allowSingleQuotes_) { token.type_ = tokenString; ok = readStringSingleQuote(); - break; - } // else fall through + } else { + // If we don't allow single quotes, this is a failure case. + ok = false; + } + break; case '/': token.type_ = tokenComment; ok = readComment(); @@ -1276,6 +1251,16 @@ void OurReader::skipSpaces() { } } +void OurReader::skipBom(bool skipBom) { + // The default behavior is to skip BOM. + if (skipBom) { + if ((end_ - begin_) >= 3 && strncmp(begin_, "\xEF\xBB\xBF", 3) == 0) { + begin_ += 3; + current_ = begin_; + } + } +} + bool OurReader::match(const Char* pattern, int patternLength) { if (end_ - current_ < patternLength) return false; @@ -1358,11 +1343,10 @@ bool OurReader::readCStyleComment(bool* containsNewLineResult) { while ((current_ + 1) < end_) { Char c = getNextChar(); - if (c == '*' && *current_ == '/') { + if (c == '*' && *current_ == '/') break; - } else if (c == '\n') { + if (c == '\n') *containsNewLineResult = true; - } } return getNextChar() == '/'; @@ -1440,12 +1424,7 @@ bool OurReader::readObject(Token& token) { Value init(objectValue); currentValue().swapPayload(init); currentValue().setOffsetStart(token.start_ - begin_); - while (readToken(tokenName)) { - bool initialTokenOk = true; - while (tokenName.type_ == tokenComment && initialTokenOk) - initialTokenOk = readToken(tokenName); - if (!initialTokenOk) - break; + while (readTokenSkippingComments(tokenName)) { if (tokenName.type_ == tokenObjectEnd && (name.empty() || features_.allowTrailingCommas_)) // empty object or trailing comma @@ -1482,15 +1461,11 @@ bool OurReader::readObject(Token& token) { return recoverFromError(tokenObjectEnd); Token comma; - if (!readToken(comma) || - (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator && - comma.type_ != tokenComment)) { + if (!readTokenSkippingComments(comma) || + (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator)) { return addErrorAndRecover("Missing ',' or '}' in object declaration", comma, tokenObjectEnd); } - bool finalizeTokenOk = true; - while (comma.type_ == tokenComment && finalizeTokenOk) - finalizeTokenOk = readToken(comma); if (comma.type_ == tokenObjectEnd) return true; } @@ -1524,10 +1499,7 @@ bool OurReader::readArray(Token& token) { Token currentToken; // Accept Comment after last item in the array. - ok = readToken(currentToken); - while (currentToken.type_ == tokenComment && ok) { - ok = readToken(currentToken); - } + ok = readTokenSkippingComments(currentToken); bool badTokenType = (currentToken.type_ != tokenArraySeparator && currentToken.type_ != tokenArrayEnd); if (!ok || badTokenType) { @@ -1586,9 +1558,9 @@ bool OurReader::decodeNumber(Token& token, Value& decoded) { // then take the inverse. This assumes that minLargestInt is only a single // power of 10 different in magnitude, which we check above. For the last // digit, we take the modulus before negating for the same reason. - static constexpr Value::LargestUInt negative_threshold = + static constexpr auto negative_threshold = Value::LargestUInt(-(Value::minLargestInt / 10)); - static constexpr Value::UInt negative_last_digit = + static constexpr auto negative_last_digit = Value::UInt(-(Value::minLargestInt % 10)); const Value::LargestUInt threshold = @@ -1602,10 +1574,10 @@ bool OurReader::decodeNumber(Token& token, Value& decoded) { if (c < '0' || c > '9') return decodeDouble(token, decoded); - const Value::UInt digit(static_cast(c - '0')); + const auto digit(static_cast(c - '0')); if (value >= threshold) { // We've hit or exceeded the max value divided by 10 (rounded down). If - // a) we've only just touched the limit, meaing value == threshold, + // a) we've only just touched the limit, meaning value == threshold, // b) this is the last digit, or // c) it's small enough to fit in that rounding delta, we're okay. // Otherwise treat this number as a double to avoid overflow. @@ -1619,7 +1591,7 @@ bool OurReader::decodeNumber(Token& token, Value& decoded) { if (isNegative) { // We use the same magnitude assumption here, just in case. - const Value::UInt last_digit = static_cast(value % 10); + const auto last_digit = static_cast(value % 10); decoded = -Value::LargestInt(value / 10) * 10 - last_digit; } else if (value <= Value::LargestUInt(Value::maxLargestInt)) { decoded = Value::LargestInt(value); @@ -1642,11 +1614,16 @@ bool OurReader::decodeDouble(Token& token) { bool OurReader::decodeDouble(Token& token, Value& decoded) { double value = 0; - const String buffer(token.start_, token.end_); - IStringStream is(buffer); + IStringStream is(String(token.start_, token.end_)); + is.imbue(std::locale::classic()); if (!(is >> value)) { - return addError( - "'" + String(token.start_, token.end_) + "' is not a number.", token); + if (value == std::numeric_limits::max()) + value = std::numeric_limits::infinity(); + else if (value == std::numeric_limits::lowest()) + value = -std::numeric_limits::infinity(); + else if (!std::isinf(value)) + return addError( + "'" + String(token.start_, token.end_) + "' is not a number.", token); } decoded = value; return true; @@ -1669,9 +1646,9 @@ bool OurReader::decodeString(Token& token, String& decoded) { Location end = token.end_ - 1; // do not include '"' while (current != end) { Char c = *current++; - if (c == '"') { + if (c == '"') break; - } else if (c == '\\') { + if (c == '\\') { if (current == end) return addError("Empty escape sequence in string", token, current); Char escape = *current++; @@ -1710,6 +1687,8 @@ bool OurReader::decodeString(Token& token, String& decoded) { return addError("Bad escape sequence in string", token, current); } } else { + if (static_cast(c) < 0x20) + return addError("Control character in string", token, current - 1); decoded += c; } } @@ -1811,7 +1790,7 @@ void OurReader::getLocationLineAndColumn(Location location, int& line, while (current < location && current != end_) { Char c = *current++; if (c == '\r') { - if (*current == '\n') + if (current != end_ && *current == '\n') ++current; lastLineStart = current; ++line; @@ -1846,10 +1825,11 @@ String OurReader::getFormattedErrorMessages() const { return formattedMessage; } -std::vector OurReader::getStructuredErrors() const { - std::vector allErrors; +std::vector +OurReader::getStructuredErrors() const { + std::vector allErrors; for (const auto& error : errors_) { - OurReader::StructuredError structured; + CharReader::StructuredError structured; structured.offset_start = error.token_.start_ - begin_; structured.offset_limit = error.token_.end_ - begin_; structured.message = error.message_; @@ -1859,20 +1839,36 @@ std::vector OurReader::getStructuredErrors() const { } class OurCharReader : public CharReader { - bool const collectComments_; - OurReader reader_; public: OurCharReader(bool collectComments, OurFeatures const& features) - : collectComments_(collectComments), reader_(features) {} - bool parse(char const* beginDoc, char const* endDoc, Value* root, - String* errs) override { - bool ok = reader_.parse(beginDoc, endDoc, *root, collectComments_); - if (errs) { - *errs = reader_.getFormattedErrorMessages(); + : CharReader( + std::unique_ptr(new OurImpl(collectComments, features))) {} + +protected: + class OurImpl : public Impl { + public: + OurImpl(bool collectComments, OurFeatures const& features) + : collectComments_(collectComments), reader_(features) {} + + bool parse(char const* beginDoc, char const* endDoc, Value* root, + String* errs) override { + bool ok = reader_.parse(beginDoc, endDoc, *root, collectComments_); + if (errs) { + *errs = reader_.getFormattedErrorMessages(); + } + return ok; } - return ok; - } + + std::vector + getStructuredErrors() const override { + return reader_.getStructuredErrors(); + } + + private: + bool const collectComments_; + OurReader reader_; + }; }; CharReaderBuilder::CharReaderBuilder() { setDefaults(&settings_); } @@ -1894,39 +1890,37 @@ CharReader* CharReaderBuilder::newCharReader() const { features.failIfExtra_ = settings_["failIfExtra"].asBool(); features.rejectDupKeys_ = settings_["rejectDupKeys"].asBool(); features.allowSpecialFloats_ = settings_["allowSpecialFloats"].asBool(); + features.skipBom_ = settings_["skipBom"].asBool(); return new OurCharReader(collectComments, features); } -static void getValidReaderKeys(std::set* valid_keys) { - valid_keys->clear(); - valid_keys->insert("collectComments"); - valid_keys->insert("allowComments"); - valid_keys->insert("allowTrailingCommas"); - valid_keys->insert("strictRoot"); - valid_keys->insert("allowDroppedNullPlaceholders"); - valid_keys->insert("allowNumericKeys"); - valid_keys->insert("allowSingleQuotes"); - valid_keys->insert("stackLimit"); - valid_keys->insert("failIfExtra"); - valid_keys->insert("rejectDupKeys"); - valid_keys->insert("allowSpecialFloats"); -} + bool CharReaderBuilder::validate(Json::Value* invalid) const { - Json::Value my_invalid; - if (!invalid) - invalid = &my_invalid; // so we do not need to test for NULL - Json::Value& inv = *invalid; - std::set valid_keys; - getValidReaderKeys(&valid_keys); - Value::Members keys = settings_.getMemberNames(); - size_t n = keys.size(); - for (size_t i = 0; i < n; ++i) { - String const& key = keys[i]; - if (valid_keys.find(key) == valid_keys.end()) { - inv[key] = settings_[key]; - } + static const auto& valid_keys = *new std::set{ + "collectComments", + "allowComments", + "allowTrailingCommas", + "strictRoot", + "allowDroppedNullPlaceholders", + "allowNumericKeys", + "allowSingleQuotes", + "stackLimit", + "failIfExtra", + "rejectDupKeys", + "allowSpecialFloats", + "skipBom", + }; + for (auto si = settings_.begin(); si != settings_.end(); ++si) { + auto key = si.name(); + if (valid_keys.count(key)) + continue; + if (invalid) + (*invalid)[key] = *si; + else + return false; } - return inv.empty(); + return invalid ? invalid->empty() : true; } + Value& CharReaderBuilder::operator[](const String& key) { return settings_[key]; } @@ -1939,10 +1933,11 @@ void CharReaderBuilder::strictMode(Json::Value* settings) { (*settings)["allowDroppedNullPlaceholders"] = false; (*settings)["allowNumericKeys"] = false; (*settings)["allowSingleQuotes"] = false; - (*settings)["stackLimit"] = 1000; + (*settings)["stackLimit"] = 256; (*settings)["failIfExtra"] = true; (*settings)["rejectDupKeys"] = true; (*settings)["allowSpecialFloats"] = false; + (*settings)["skipBom"] = true; //! [CharReaderBuilderStrictMode] } // static @@ -1955,12 +1950,39 @@ void CharReaderBuilder::setDefaults(Json::Value* settings) { (*settings)["allowDroppedNullPlaceholders"] = false; (*settings)["allowNumericKeys"] = false; (*settings)["allowSingleQuotes"] = false; - (*settings)["stackLimit"] = 1000; + (*settings)["stackLimit"] = 256; (*settings)["failIfExtra"] = false; (*settings)["rejectDupKeys"] = false; (*settings)["allowSpecialFloats"] = false; + (*settings)["skipBom"] = true; //! [CharReaderBuilderDefaults] } +// static +void CharReaderBuilder::ecma404Mode(Json::Value* settings) { + //! [CharReaderBuilderECMA404Mode] + (*settings)["allowComments"] = false; + (*settings)["allowTrailingCommas"] = false; + (*settings)["strictRoot"] = false; + (*settings)["allowDroppedNullPlaceholders"] = false; + (*settings)["allowNumericKeys"] = false; + (*settings)["allowSingleQuotes"] = false; + (*settings)["stackLimit"] = 256; + (*settings)["failIfExtra"] = true; + (*settings)["rejectDupKeys"] = false; + (*settings)["allowSpecialFloats"] = false; + (*settings)["skipBom"] = false; + //! [CharReaderBuilderECMA404Mode] +} + +std::vector +CharReader::getStructuredErrors() const { + return _impl->getStructuredErrors(); +} + +bool CharReader::parse(char const* beginDoc, char const* endDoc, Value* root, + String* errs) { + return _impl->parse(beginDoc, endDoc, root, errs); +} ////////////////////////////////// // global functions @@ -1969,7 +1991,7 @@ bool parseFromStream(CharReader::Factory const& fact, IStream& sin, Value* root, String* errs) { OStringStream ssin; ssin << sin.rdbuf(); - String doc = ssin.str(); + String doc = std::move(ssin).str(); char const* begin = doc.data(); char const* end = begin + doc.size(); // Note that we do not actually need a null-terminator. diff --git a/src/lib_json/json_tool.h b/src/lib_json/json_tool.h index 2d7b7d9a0..b952c1916 100644 --- a/src/lib_json/json_tool.h +++ b/src/lib_json/json_tool.h @@ -116,14 +116,18 @@ template void fixNumericLocaleInput(Iter begin, Iter end) { * Return iterator that would be the new end of the range [begin,end), if we * were to delete zeros in the end of string, but not the last zero before '.'. */ -template Iter fixZerosInTheEnd(Iter begin, Iter end) { +template +Iter fixZerosInTheEnd(Iter begin, Iter end, unsigned int precision) { for (; begin != end; --end) { if (*(end - 1) != '0') { return end; } // Don't delete the last zero before the decimal point. - if (begin != (end - 1) && *(end - 2) == '.') { - return end; + if (begin != (end - 1) && begin != (end - 2) && *(end - 2) == '.') { + if (precision) { + return end; + } + return end - 2; } } return end; diff --git a/src/lib_json/json_value.cpp b/src/lib_json/json_value.cpp index 1e3bbdebd..168251ee1 100644 --- a/src/lib_json/json_value.cpp +++ b/src/lib_json/json_value.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -86,7 +87,8 @@ template static inline bool InRange(double d, T min, U max) { // The casts can lose precision, but we are looking only for // an approximate range. Might fail on edge cases though. ~cdunn - return d >= static_cast(min) && d <= static_cast(max); + return d >= static_cast(min) && d <= static_cast(max) && + !(static_cast(d) == min && d != static_cast(min)); } #else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) static inline double integerToDouble(Json::UInt64 value) { @@ -100,7 +102,8 @@ template static inline double integerToDouble(T value) { template static inline bool InRange(double d, T min, U max) { - return d >= integerToDouble(min) && d <= integerToDouble(max); + return d >= integerToDouble(min) && d <= integerToDouble(max) && + !(static_cast(d) == min && d != integerToDouble(min)); } #endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) @@ -162,7 +165,7 @@ inline static void decodePrefixedString(bool isPrefixed, char const* prefixed, /** Free the string duplicated by * duplicateStringValue()/duplicateAndPrefixStringValue(). */ -#if JSONCPP_USING_SECURE_MEMORY +#if JSONCPP_USE_SECURE_MEMORY static inline void releasePrefixedStringValue(char* value) { unsigned length = 0; char const* valueDecoded; @@ -177,10 +180,10 @@ static inline void releaseStringValue(char* value, unsigned length) { memset(value, 0, size); free(value); } -#else // !JSONCPP_USING_SECURE_MEMORY +#else // !JSONCPP_USE_SECURE_MEMORY static inline void releasePrefixedStringValue(char* value) { free(value); } static inline void releaseStringValue(char* value, unsigned) { free(value); } -#endif // JSONCPP_USING_SECURE_MEMORY +#endif // JSONCPP_USE_SECURE_MEMORY } // namespace Json @@ -200,8 +203,8 @@ namespace Json { #if JSON_USE_EXCEPTION Exception::Exception(String msg) : msg_(std::move(msg)) {} -Exception::~Exception() JSONCPP_NOEXCEPT = default; -char const* Exception::what() const JSONCPP_NOEXCEPT { return msg_.c_str(); } +Exception::~Exception() noexcept = default; +char const* Exception::what() const noexcept { return msg_.c_str(); } RuntimeError::RuntimeError(String const& msg) : Exception(msg) {} LogicError::LogicError(String const& msg) : Exception(msg) {} JSONCPP_NORETURN void throwRuntimeError(String const& msg) { @@ -211,8 +214,14 @@ JSONCPP_NORETURN void throwLogicError(String const& msg) { throw LogicError(msg); } #else // !JSON_USE_EXCEPTION -JSONCPP_NORETURN void throwRuntimeError(String const& msg) { abort(); } -JSONCPP_NORETURN void throwLogicError(String const& msg) { abort(); } +JSONCPP_NORETURN void throwRuntimeError(String const& msg) { + std::cerr << msg << std::endl; + abort(); +} +JSONCPP_NORETURN void throwLogicError(String const& msg) { + std::cerr << msg << std::endl; + abort(); +} #endif // ////////////////////////////////////////////////////////////////// @@ -240,20 +249,29 @@ Value::CZString::CZString(const CZString& other) { cstr_ = (other.storage_.policy_ != noDuplication && other.cstr_ != nullptr ? duplicateStringValue(other.cstr_, other.storage_.length_) : other.cstr_); - storage_.policy_ = - static_cast( - other.cstr_ - ? (static_cast(other.storage_.policy_) == - noDuplication - ? noDuplication - : duplicate) - : static_cast(other.storage_.policy_)) & - 3U; - storage_.length_ = other.storage_.length_; -} - -Value::CZString::CZString(CZString&& other) - : cstr_(other.cstr_), index_(other.index_) { + if (other.cstr_) { + storage_.policy_ = + static_cast( + other.cstr_ + ? (static_cast(other.storage_.policy_) == + noDuplication + ? noDuplication + : duplicate) + : static_cast(other.storage_.policy_)) & + 3U; + storage_.length_ = other.storage_.length_; + } else { + index_ = other.index_; + } +} + +Value::CZString::CZString(CZString&& other) noexcept : cstr_(other.cstr_) { + if (other.cstr_) { + storage_.policy_ = other.storage_.policy_; + storage_.length_ = other.storage_.length_; + } else { + index_ = other.index_; + } other.cstr_ = nullptr; } @@ -278,9 +296,17 @@ Value::CZString& Value::CZString::operator=(const CZString& other) { return *this; } -Value::CZString& Value::CZString::operator=(CZString&& other) { +Value::CZString& Value::CZString::operator=(CZString&& other) noexcept { + if (cstr_ && storage_.policy_ == duplicate) { + releasePrefixedStringValue(const_cast(cstr_)); + } cstr_ = other.cstr_; - index_ = other.index_; + if (other.cstr_) { + storage_.policy_ = other.storage_.policy_; + storage_.length_ = other.storage_.length_; + } else { + index_ = other.index_; + } other.cstr_ = nullptr; return *this; } @@ -426,7 +452,7 @@ Value::Value(const Value& other) { dupMeta(other); } -Value::Value(Value&& other) { +Value::Value(Value&& other) noexcept { initBasic(nullValue); swap(other); } @@ -441,7 +467,7 @@ Value& Value::operator=(const Value& other) { return *this; } -Value& Value::operator=(Value&& other) { +Value& Value::operator=(Value&& other) noexcept { other.swap(*this); return *this; } @@ -592,7 +618,7 @@ const char* Value::asCString() const { return this_str; } -#if JSONCPP_USING_SECURE_MEMORY +#if JSONCPP_USE_SECURE_MEMORY unsigned Value::getCStringLength() const { JSON_ASSERT_MESSAGE(type() == stringValue, "in Json::Value::asCString(): requires stringValue"); @@ -675,7 +701,7 @@ Value::UInt Value::asUInt() const { JSON_ASSERT_MESSAGE(isUInt(), "LargestUInt out of UInt range"); return UInt(value_.uint_); case realValue: - JSON_ASSERT_MESSAGE(InRange(value_.real_, 0, maxUInt), + JSON_ASSERT_MESSAGE(InRange(value_.real_, 0u, maxUInt), "double out of UInt range"); return UInt(value_.real_); case nullValue: @@ -698,6 +724,11 @@ Value::Int64 Value::asInt64() const { JSON_ASSERT_MESSAGE(isInt64(), "LargestUInt out of Int64 range"); return Int64(value_.uint_); case realValue: + // If the double value is in proximity to minInt64, it will be rounded to + // minInt64. The correct value in this scenario is indeterminable + JSON_ASSERT_MESSAGE( + value_.real_ != minInt64, + "Double value is minInt64, precise value cannot be determined"); JSON_ASSERT_MESSAGE(InRange(value_.real_, minInt64, maxInt64), "double out of Int64 range"); return Int64(value_.real_); @@ -719,7 +750,7 @@ Value::UInt64 Value::asUInt64() const { case uintValue: return UInt64(value_.uint_); case realValue: - JSON_ASSERT_MESSAGE(InRange(value_.real_, 0, maxUInt64), + JSON_ASSERT_MESSAGE(InRange(value_.real_, 0u, maxUInt64), "double out of UInt64 range"); return UInt64(value_.real_); case nullValue: @@ -830,7 +861,7 @@ bool Value::isConvertibleTo(ValueType other) const { type() == booleanValue || type() == nullValue; case uintValue: return isUInt() || - (type() == realValue && InRange(value_.real_, 0, maxUInt)) || + (type() == realValue && InRange(value_.real_, 0u, maxUInt)) || type() == booleanValue || type() == nullValue; case realValue: return isNumeric() || type() == booleanValue || type() == nullValue; @@ -875,8 +906,7 @@ ArrayIndex Value::size() const { bool Value::empty() const { if (isNull() || isArray() || isObject()) return size() == 0U; - else - return false; + return false; } Value::operator bool() const { return !isNull(); } @@ -906,7 +936,8 @@ void Value::resize(ArrayIndex newSize) { if (newSize == 0) clear(); else if (newSize > oldSize) - this->operator[](newSize - 1); + for (ArrayIndex i = oldSize; i < newSize; ++i) + (*this)[i]; else { for (ArrayIndex index = newSize; index < oldSize; ++index) { value_.map_->erase(index); @@ -1085,6 +1116,47 @@ Value const* Value::find(char const* begin, char const* end) const { return nullptr; return &(*it).second; } +Value const* Value::find(const String& key) const { + return find(key.data(), key.data() + key.length()); +} + +Value const* Value::findNull(const String& key) const { + return findValue(key); +} +Value const* Value::findBool(const String& key) const { + return findValue(key); +} +Value const* Value::findInt(const String& key) const { + return findValue(key); +} +Value const* Value::findInt64(const String& key) const { + return findValue(key); +} +Value const* Value::findUInt(const String& key) const { + return findValue(key); +} +Value const* Value::findUInt64(const String& key) const { + return findValue(key); +} +Value const* Value::findIntegral(const String& key) const { + return findValue(key); +} +Value const* Value::findDouble(const String& key) const { + return findValue(key); +} +Value const* Value::findNumeric(const String& key) const { + return findValue(key); +} +Value const* Value::findString(const String& key) const { + return findValue(key); +} +Value const* Value::findArray(const String& key) const { + return findValue(key); +} +Value const* Value::findObject(const String& key) const { + return findValue(key); +} + Value* Value::demand(char const* begin, char const* end) { JSON_ASSERT_MESSAGE(type() == nullValue || type() == objectValue, "in Json::Value::demand(begin, end): requires " @@ -1098,7 +1170,7 @@ const Value& Value::operator[](const char* key) const { return *found; } Value const& Value::operator[](const String& key) const { - Value const* found = find(key.data(), key.data() + key.length()); + Value const* found = find(key); if (!found) return nullSingleton(); return *found; @@ -1137,13 +1209,12 @@ bool Value::insert(ArrayIndex index, Value&& newValue) { ArrayIndex length = size(); if (index > length) { return false; - } else { - for (ArrayIndex i = length; i > index; i--) { - (*this)[i] = std::move((*this)[i - 1]); - } - (*this)[index] = std::move(newValue); - return true; } + for (ArrayIndex i = length; i > index; i--) { + (*this)[i] = std::move((*this)[i - 1]); + } + (*this)[index] = std::move(newValue); + return true; } Value Value::get(char const* begin, char const* end, @@ -1178,6 +1249,7 @@ bool Value::removeMember(const char* key, Value* removed) { bool Value::removeMember(String const& key, Value* removed) { return removeMember(key.data(), key.data() + key.length(), removed); } + void Value::removeMember(const char* key) { JSON_ASSERT_MESSAGE(type() == nullValue || type() == objectValue, "in Json::Value::removeMember(): requires objectValue"); @@ -1199,7 +1271,7 @@ bool Value::removeIndex(ArrayIndex index, Value* removed) { return false; } if (removed) - *removed = it->second; + *removed = std::move(it->second); ArrayIndex oldSize = size(); // shift left all items left, into the place of the "removed" for (ArrayIndex i = index; i < (oldSize - 1); ++i) { @@ -1302,8 +1374,12 @@ bool Value::isInt64() const { // Note that maxInt64 (= 2^63 - 1) is not exactly representable as a // double, so double(maxInt64) will be rounded up to 2^63. Therefore we // require the value to be strictly less than the limit. - return value_.real_ >= double(minInt64) && - value_.real_ < double(maxInt64) && IsIntegral(value_.real_); + // minInt64 is -2^63 which can be represented as a double, but since double + // values in its proximity are also rounded to -2^63, we require the value + // to be strictly greater than the limit to avoid returning 'true' for + // values that are not in the range + return value_.real_ > double(minInt64) && value_.real_ < double(maxInt64) && + IsIntegral(value_.real_); default: break; } @@ -1341,7 +1417,11 @@ bool Value::isIntegral() const { // Note that maxUInt64 (= 2^64 - 1) is not exactly representable as a // double, so double(maxUInt64) will be rounded up to 2^64. Therefore we // require the value to be strictly less than the limit. - return value_.real_ >= double(minInt64) && + // minInt64 is -2^63 which can be represented as a double, but since double + // values in its proximity are also rounded to -2^63, we require the value + // to be strictly greater than the limit to avoid returning 'true' for + // values that are not in the range + return value_.real_ > double(minInt64) && value_.real_ < maxUInt64AsDouble && IsIntegral(value_.real_); #else return value_.real_ >= minInt && value_.real_ <= maxUInt && @@ -1368,14 +1448,15 @@ bool Value::isObject() const { return type() == objectValue; } Value::Comments::Comments(const Comments& that) : ptr_{cloneUnique(that.ptr_)} {} -Value::Comments::Comments(Comments&& that) : ptr_{std::move(that.ptr_)} {} +Value::Comments::Comments(Comments&& that) noexcept + : ptr_{std::move(that.ptr_)} {} Value::Comments& Value::Comments::operator=(const Comments& that) { ptr_ = cloneUnique(that.ptr_); return *this; } -Value::Comments& Value::Comments::operator=(Comments&& that) { +Value::Comments& Value::Comments::operator=(Comments&& that) noexcept { ptr_ = std::move(that.ptr_); return *this; } @@ -1391,13 +1472,11 @@ String Value::Comments::get(CommentPlacement slot) const { } void Value::Comments::set(CommentPlacement slot, String comment) { - if (!ptr_) { + if (slot >= CommentPlacement::numberOfCommentPlacement) + return; + if (!ptr_) ptr_ = std::unique_ptr(new Array()); - } - // check comments array boundry. - if (slot < CommentPlacement::numberOfCommentPlacement) { - (*ptr_)[slot] = std::move(comment); - } + (*ptr_)[slot] = std::move(comment); } void Value::setComment(String comment, CommentPlacement placement) { @@ -1405,9 +1484,8 @@ void Value::setComment(String comment, CommentPlacement placement) { // Always discard trailing newline, to aid indentation. comment.pop_back(); } - JSON_ASSERT(!comment.empty()); JSON_ASSERT_MESSAGE( - comment[0] == '\0' || comment[0] == '/', + comment.empty() || comment[0] == '/', "in Json::Value::setComment(): Comments must start with /"); comments_.set(placement, std::move(comment)); } @@ -1626,4 +1704,6 @@ Value& Path::make(Value& root) const { return *node; } +const char* version() { return JSONCPP_VERSION_STRING; } + } // namespace Json diff --git a/src/lib_json/json_valueiterator.inl b/src/lib_json/json_valueiterator.inl index d6128b8ed..4e77f368b 100644 --- a/src/lib_json/json_valueiterator.inl +++ b/src/lib_json/json_valueiterator.inl @@ -122,8 +122,8 @@ ValueConstIterator::ValueConstIterator( ValueConstIterator::ValueConstIterator(ValueIterator const& other) : ValueIteratorBase(other) {} -ValueConstIterator& ValueConstIterator:: -operator=(const ValueIteratorBase& other) { +ValueConstIterator& +ValueConstIterator::operator=(const ValueIteratorBase& other) { copy(other); return *this; } diff --git a/src/lib_json/json_writer.cpp b/src/lib_json/json_writer.cpp index 56195dc15..ac14eb11f 100644 --- a/src/lib_json/json_writer.cpp +++ b/src/lib_json/json_writer.cpp @@ -7,7 +7,11 @@ #include "json_tool.h" #include #endif // if !defined(JSON_IS_AMALGAMATION) +#include #include +#include +#include +#include #include #include #include @@ -15,67 +19,6 @@ #include #include -#if __cplusplus >= 201103L -#include -#include - -#if !defined(isnan) -#define isnan std::isnan -#endif - -#if !defined(isfinite) -#define isfinite std::isfinite -#endif - -#else -#include -#include - -#if defined(_MSC_VER) -#if !defined(isnan) -#include -#define isnan _isnan -#endif - -#if !defined(isfinite) -#include -#define isfinite _finite -#endif - -#if !defined(_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES) -#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1 -#endif //_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES - -#endif //_MSC_VER - -#if defined(__sun) && defined(__SVR4) // Solaris -#if !defined(isfinite) -#include -#define isfinite finite -#endif -#endif - -#if defined(__hpux) -#if !defined(isfinite) -#if defined(__ia64) && !defined(finite) -#define isfinite(x) \ - ((sizeof(x) == sizeof(float) ? _Isfinitef(x) : _IsFinite(x))) -#endif -#endif -#endif - -#if !defined(isnan) -// IEEE standard states that NaN values will not compare to themselves -#define isnan(x) (x != x) -#endif - -#if !defined(__APPLE__) -#if !defined(isfinite) -#define isfinite finite -#endif -#endif -#endif - #if defined(_MSC_VER) // Disable warning about strdup being deprecated. #pragma warning(disable : 4996) @@ -83,11 +26,7 @@ namespace Json { -#if __cplusplus >= 201103L || (defined(_CPPLIB_VER) && _CPPLIB_VER >= 520) using StreamWriterPtr = std::unique_ptr; -#else -using StreamWriterPtr = std::auto_ptr; -#endif String valueToString(LargestInt value) { UIntToStringBuffer buffer; @@ -127,11 +66,12 @@ String valueToString(double value, bool useSpecialFloats, // Print into the buffer. We need not request the alternative representation // that always has a decimal point because JSON doesn't distinguish the // concepts of reals and integers. - if (!isfinite(value)) { - static const char* const reps[2][3] = {{"NaN", "-Infinity", "Infinity"}, - {"null", "-1e+9999", "1e+9999"}}; - return reps[useSpecialFloats ? 0 : 1] - [isnan(value) ? 0 : (value < 0) ? 1 : 2]; + if (!std::isfinite(value)) { + if (std::isnan(value)) + return useSpecialFloats ? "NaN" : "null"; + if (value < 0) + return useSpecialFloats ? "-Infinity" : "-1e+9999"; + return useSpecialFloats ? "Infinity" : "1e+9999"; } String buffer(size_t(36), '\0'); @@ -152,16 +92,18 @@ String valueToString(double value, bool useSpecialFloats, buffer.erase(fixNumericLocale(buffer.begin(), buffer.end()), buffer.end()); - // strip the zero padding from the right - if (precisionType == PrecisionType::decimalPlaces) { - buffer.erase(fixZerosInTheEnd(buffer.begin(), buffer.end()), buffer.end()); - } - // try to ensure we preserve the fact that this was given to us as a double on // input if (buffer.find('.') == buffer.npos && buffer.find('e') == buffer.npos) { buffer += ".0"; } + + // strip the zero padding from the right + if (precisionType == PrecisionType::decimalPlaces) { + buffer.erase(fixZerosInTheEnd(buffer.begin(), buffer.end(), precision), + buffer.end()); + } + return buffer; } } // namespace @@ -173,17 +115,12 @@ String valueToString(double value, unsigned int precision, String valueToString(bool value) { return value ? "true" : "false"; } -static bool isAnyCharRequiredQuoting(char const* s, size_t n) { +static bool doesAnyCharRequireEscaping(char const* s, size_t n) { assert(s || !n); - char const* const end = s + n; - for (char const* cur = s; cur < end; ++cur) { - if (*cur == '\\' || *cur == '\"' || - static_cast(*cur) < ' ' || - static_cast(*cur) >= 0x80) - return true; - } - return false; + return std::any_of(s, s + n, [](unsigned char c) { + return c == '\\' || c == '"' || c < 0x20 || c > 0x7F; + }); } static unsigned int utf8ToCodepoint(const char*& s, const char* e) { @@ -265,12 +202,20 @@ static String toHex16Bit(unsigned int x) { return result; } -static String valueToQuotedStringN(const char* value, unsigned length, +static void appendRaw(String& result, unsigned ch) { + result += static_cast(ch); +} + +static void appendHex(String& result, unsigned ch) { + result.append("\\u").append(toHex16Bit(ch)); +} + +static String valueToQuotedStringN(const char* value, size_t length, bool emitUTF8 = false) { if (value == nullptr) return ""; - if (!isAnyCharRequiredQuoting(value, length)) + if (!doesAnyCharRequireEscaping(value, length)) return String("\"") + value + "\""; // We have to walk value and escape any special characters. // Appending to String is not efficient, but this should be rare. @@ -313,29 +258,26 @@ static String valueToQuotedStringN(const char* value, unsigned length, // sequence from occurring. default: { if (emitUTF8) { - result += *c; + unsigned codepoint = static_cast(*c); + if (codepoint < 0x20) { + appendHex(result, codepoint); + } else { + appendRaw(result, codepoint); + } } else { - unsigned int codepoint = utf8ToCodepoint(c, end); - const unsigned int FIRST_NON_CONTROL_CODEPOINT = 0x20; - const unsigned int LAST_NON_CONTROL_CODEPOINT = 0x7F; - const unsigned int FIRST_SURROGATE_PAIR_CODEPOINT = 0x10000; - // don't escape non-control characters - // (short escape sequence are applied above) - if (FIRST_NON_CONTROL_CODEPOINT <= codepoint && - codepoint <= LAST_NON_CONTROL_CODEPOINT) { - result += static_cast(codepoint); - } else if (codepoint < - FIRST_SURROGATE_PAIR_CODEPOINT) { // codepoint is in Basic - // Multilingual Plane - result += "\\u"; - result += toHex16Bit(codepoint); - } else { // codepoint is not in Basic Multilingual Plane - // convert to surrogate pair first - codepoint -= FIRST_SURROGATE_PAIR_CODEPOINT; - result += "\\u"; - result += toHex16Bit((codepoint >> 10) + 0xD800); - result += "\\u"; - result += toHex16Bit((codepoint & 0x3FF) + 0xDC00); + unsigned codepoint = utf8ToCodepoint(c, end); // modifies `c` + if (codepoint < 0x20) { + appendHex(result, codepoint); + } else if (codepoint < 0x80) { + appendRaw(result, codepoint); + } else if (codepoint < 0x10000) { + // Basic Multilingual Plane + appendHex(result, codepoint); + } else { + // Extended Unicode. Encode 20 bits as a surrogate pair. + codepoint -= 0x10000; + appendHex(result, 0xd800 + ((codepoint >> 10) & 0x3ff)); + appendHex(result, 0xdc00 + (codepoint & 0x3ff)); } } } break; @@ -346,7 +288,11 @@ static String valueToQuotedStringN(const char* value, unsigned length, } String valueToQuotedString(const char* value) { - return valueToQuotedStringN(value, static_cast(strlen(value))); + return valueToQuotedStringN(value, strlen(value)); +} + +String valueToQuotedString(const char* value, size_t length) { + return valueToQuotedStringN(value, length); } // Class Writer @@ -395,7 +341,7 @@ void FastWriter::writeValue(const Value& value) { char const* end; bool ok = value.getString(&str, &end); if (ok) - document_ += valueToQuotedStringN(str, static_cast(end - str)); + document_ += valueToQuotedStringN(str, static_cast(end - str)); break; } case booleanValue: @@ -418,8 +364,7 @@ void FastWriter::writeValue(const Value& value) { const String& name = *it; if (it != members.begin()) document_ += ','; - document_ += valueToQuotedStringN(name.data(), - static_cast(name.length())); + document_ += valueToQuotedStringN(name.data(), name.length()); document_ += yamlCompatibilityEnabled_ ? ": " : ":"; writeValue(value[name]); } @@ -464,7 +409,7 @@ void StyledWriter::writeValue(const Value& value) { char const* end; bool ok = value.getString(&str, &end); if (ok) - pushValue(valueToQuotedStringN(str, static_cast(end - str))); + pushValue(valueToQuotedStringN(str, static_cast(end - str))); else pushValue(""); break; @@ -487,7 +432,7 @@ void StyledWriter::writeValue(const Value& value) { const String& name = *it; const Value& childValue = value[name]; writeCommentBeforeValue(childValue); - writeWithIndent(valueToQuotedString(name.c_str())); + writeWithIndent(valueToQuotedString(name.c_str(), name.size())); document_ += " : "; writeValue(childValue); if (++it == members.end()) { @@ -505,7 +450,7 @@ void StyledWriter::writeValue(const Value& value) { } void StyledWriter::writeArrayValue(const Value& value) { - unsigned size = value.size(); + size_t size = value.size(); if (size == 0) pushValue("[]"); else { @@ -514,7 +459,7 @@ void StyledWriter::writeArrayValue(const Value& value) { writeWithIndent("["); indent(); bool hasChildValue = !childValues_.empty(); - unsigned index = 0; + ArrayIndex index = 0; for (;;) { const Value& childValue = value[index]; writeCommentBeforeValue(childValue); @@ -537,7 +482,7 @@ void StyledWriter::writeArrayValue(const Value& value) { { assert(childValues_.size() == size); document_ += "[ "; - for (unsigned index = 0; index < size; ++index) { + for (size_t index = 0; index < size; ++index) { if (index > 0) document_ += ", "; document_ += childValues_[index]; @@ -682,7 +627,7 @@ void StyledStreamWriter::writeValue(const Value& value) { char const* end; bool ok = value.getString(&str, &end); if (ok) - pushValue(valueToQuotedStringN(str, static_cast(end - str))); + pushValue(valueToQuotedStringN(str, static_cast(end - str))); else pushValue(""); break; @@ -705,7 +650,7 @@ void StyledStreamWriter::writeValue(const Value& value) { const String& name = *it; const Value& childValue = value[name]; writeCommentBeforeValue(childValue); - writeWithIndent(valueToQuotedString(name.c_str())); + writeWithIndent(valueToQuotedString(name.c_str(), name.size())); *document_ << " : "; writeValue(childValue); if (++it == members.end()) { @@ -956,8 +901,8 @@ void BuiltStyledStreamWriter::writeValue(Value const& value) { char const* end; bool ok = value.getString(&str, &end); if (ok) - pushValue(valueToQuotedStringN(str, static_cast(end - str), - emitUTF8_)); + pushValue( + valueToQuotedStringN(str, static_cast(end - str), emitUTF8_)); else pushValue(""); break; @@ -980,8 +925,8 @@ void BuiltStyledStreamWriter::writeValue(Value const& value) { String const& name = *it; Value const& childValue = value[name]; writeCommentBeforeValue(childValue); - writeWithIndent(valueToQuotedStringN( - name.data(), static_cast(name.length()), emitUTF8_)); + writeWithIndent( + valueToQuotedStringN(name.data(), name.length(), emitUTF8_)); *sout_ << colonSymbol_; writeValue(childValue); if (++it == members.end()) { @@ -1198,34 +1143,30 @@ StreamWriter* StreamWriterBuilder::newStreamWriter() const { endingLineFeedSymbol, usf, emitUTF8, pre, precisionType); } -static void getValidWriterKeys(std::set* valid_keys) { - valid_keys->clear(); - valid_keys->insert("indentation"); - valid_keys->insert("commentStyle"); - valid_keys->insert("enableYAMLCompatibility"); - valid_keys->insert("dropNullPlaceholders"); - valid_keys->insert("useSpecialFloats"); - valid_keys->insert("emitUTF8"); - valid_keys->insert("precision"); - valid_keys->insert("precisionType"); -} + bool StreamWriterBuilder::validate(Json::Value* invalid) const { - Json::Value my_invalid; - if (!invalid) - invalid = &my_invalid; // so we do not need to test for NULL - Json::Value& inv = *invalid; - std::set valid_keys; - getValidWriterKeys(&valid_keys); - Value::Members keys = settings_.getMemberNames(); - size_t n = keys.size(); - for (size_t i = 0; i < n; ++i) { - String const& key = keys[i]; - if (valid_keys.find(key) == valid_keys.end()) { - inv[key] = settings_[key]; - } + static const auto& valid_keys = *new std::set{ + "indentation", + "commentStyle", + "enableYAMLCompatibility", + "dropNullPlaceholders", + "useSpecialFloats", + "emitUTF8", + "precision", + "precisionType", + }; + for (auto si = settings_.begin(); si != settings_.end(); ++si) { + auto key = si.name(); + if (valid_keys.count(key)) + continue; + if (invalid) + (*invalid)[key] = *si; + else + return false; } - return inv.empty(); + return invalid ? invalid->empty() : true; } + Value& StreamWriterBuilder::operator[](const String& key) { return settings_[key]; } @@ -1247,7 +1188,7 @@ String writeString(StreamWriter::Factory const& factory, Value const& root) { OStringStream sout; StreamWriterPtr const writer(factory.newStreamWriter()); writer->write(root, &sout); - return sout.str(); + return std::move(sout).str(); } OStream& operator<<(OStream& sout, Value const& root) { diff --git a/src/test_lib_json/BUILD.bazel b/src/test_lib_json/BUILD.bazel new file mode 100644 index 000000000..18b2a1801 --- /dev/null +++ b/src/test_lib_json/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +cc_test( + name = "jsoncpp_test", + srcs = [ + "fuzz.cpp", + "fuzz.h", + "jsontest.cpp", + "jsontest.h", + "main.cpp", + ], + deps = ["//:jsoncpp"], +) diff --git a/src/test_lib_json/CMakeLists.txt b/src/test_lib_json/CMakeLists.txt index 6e301ec69..1c3fce913 100644 --- a/src/test_lib_json/CMakeLists.txt +++ b/src/test_lib_json/CMakeLists.txt @@ -1,46 +1,39 @@ # vim: et ts=4 sts=4 sw=4 tw=0 -add_executable( jsoncpp_test - jsontest.cpp - jsontest.h - fuzz.cpp - fuzz.h - main.cpp - ) +add_executable(jsoncpp_test + jsontest.cpp + jsontest.h + fuzz.cpp + fuzz.h + main.cpp +) if(BUILD_SHARED_LIBS) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) - add_compile_definitions( JSON_DLL ) - else() - add_definitions( -DJSON_DLL ) - endif() + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12.0) + add_compile_definitions( JSON_DLL ) + else() + add_definitions( -DJSON_DLL ) + endif() + target_link_libraries(jsoncpp_test jsoncpp_lib) +else() + target_link_libraries(jsoncpp_test jsoncpp_static) endif() -target_link_libraries(jsoncpp_test jsoncpp_lib) # another way to solve issue #90 #set_target_properties(jsoncpp_test PROPERTIES COMPILE_FLAGS -ffloat-store) +## Create tests for dashboard submission, allows easy review of CI results https://my.cdash.org/index.php?project=jsoncpp +add_test(NAME jsoncpp_test + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ +) +set_target_properties(jsoncpp_test PROPERTIES OUTPUT_NAME jsoncpp_test) + # Run unit tests in post-build # (default cmake workflow hides away the test result into a file, resulting in poor dev workflow?!?) if(JSONCPP_WITH_POST_BUILD_UNITTEST) - if(BUILD_SHARED_LIBS) - # First, copy the shared lib, for Microsoft. - # Then, run the test executable. - add_custom_command( TARGET jsoncpp_test - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ - COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $) - else(BUILD_SHARED_LIBS) - # Just run the test executable. - add_custom_command( TARGET jsoncpp_test - POST_BUILD - COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $) - endif() - ## Create tests for dashboard submission, allows easy review of CI results https://my.cdash.org/index.php?project=jsoncpp - add_test(NAME jsoncpp_test - COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ + add_custom_command(TARGET jsoncpp_test + POST_BUILD + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ ) endif() - -set_target_properties(jsoncpp_test PROPERTIES OUTPUT_NAME jsoncpp_test) diff --git a/src/test_lib_json/fuzz.cpp b/src/test_lib_json/fuzz.cpp index 68f839d53..3679a95ec 100644 --- a/src/test_lib_json/fuzz.cpp +++ b/src/test_lib_json/fuzz.cpp @@ -9,13 +9,8 @@ #include #include #include -#include #include -namespace Json { -class Exception; -} - extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { Json::CharReaderBuilder builder; @@ -46,10 +41,14 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { Json::Value root; const auto data_str = reinterpret_cast(data); +#if JSON_USE_EXCEPTION try { +#endif // JSON_USE_EXCEPTION reader->parse(data_str, data_str + size, &root, nullptr); +#if JSON_USE_EXCEPTION } catch (Json::Exception const&) { } +#endif // JSON_USE_EXCEPTION // Whether it succeeded or not doesn't matter. return 0; } diff --git a/src/test_lib_json/jsontest.cpp b/src/test_lib_json/jsontest.cpp index 0cc500a88..508067a77 100644 --- a/src/test_lib_json/jsontest.cpp +++ b/src/test_lib_json/jsontest.cpp @@ -267,19 +267,18 @@ bool Runner::runAllTest(bool printSummary) const { printf("All %zu tests passed\n", count); } return true; - } else { - for (auto& result : failures) { - result.printFailure(count > 1); - } + } + for (auto& result : failures) { + result.printFailure(count > 1); + } - if (printSummary) { - size_t const failedCount = failures.size(); - size_t const passedCount = count - failedCount; - printf("%zu/%zu tests passed (%zu failure(s))\n", passedCount, count, - failedCount); - } - return false; + if (printSummary) { + size_t const failedCount = failures.size(); + size_t const passedCount = count - failedCount; + printf("%zu/%zu tests passed (%zu failure(s))\n", passedCount, count, + failedCount); } + return false; } bool Runner::testIndex(const Json::String& testName, size_t& indexOut) const { @@ -308,7 +307,8 @@ int Runner::runCommandLine(int argc, const char* argv[]) const { if (opt == "--list-tests") { listTests(); return 0; - } else if (opt == "--test-auto") { + } + if (opt == "--test-auto") { preventDialogOnCrash(); } else if (opt == "--test") { ++index; @@ -410,7 +410,7 @@ Json::String ToJsonString(const char* toConvert) { Json::String ToJsonString(Json::String in) { return in; } -#if JSONCPP_USING_SECURE_MEMORY +#if JSONCPP_USE_SECURE_MEMORY Json::String ToJsonString(std::string in) { return Json::String(in.data(), in.data() + in.length()); } diff --git a/src/test_lib_json/jsontest.h b/src/test_lib_json/jsontest.h index 8c3aa5e32..3652c4029 100644 --- a/src/test_lib_json/jsontest.h +++ b/src/test_lib_json/jsontest.h @@ -74,7 +74,7 @@ class TestResult { /// Removes the last PredicateContext added to the predicate stack /// chained list. - /// Next messages will be targed at the PredicateContext that was removed. + /// Next messages will be targeted at the PredicateContext that was removed. TestResult& popPredicateContext(); bool failed() const; @@ -185,7 +185,7 @@ TestResult& checkEqual(TestResult& result, T expected, U actual, Json::String ToJsonString(const char* toConvert); Json::String ToJsonString(Json::String in); -#if JSONCPP_USING_SECURE_MEMORY +#if JSONCPP_USE_SECURE_MEMORY Json::String ToJsonString(std::string in); #endif @@ -207,7 +207,7 @@ TestResult& checkStringEqual(TestResult& result, const Json::String& expected, /// The predicate may do other assertions and be a member function of the /// fixture. #define JSONTEST_ASSERT_PRED(expr) \ - { \ + do { \ JsonTest::PredicateContext _minitest_Context = { \ result_->predicateId_, __FILE__, __LINE__, #expr, NULL, NULL}; \ result_->predicateStackTail_->next_ = &_minitest_Context; \ @@ -215,7 +215,7 @@ TestResult& checkStringEqual(TestResult& result, const Json::String& expected, result_->predicateStackTail_ = &_minitest_Context; \ (expr); \ result_->popPredicateContext(); \ - } + } while (0) /// \brief Asserts that two values are equals. #define JSONTEST_ASSERT_EQUAL(expected, actual) \ @@ -228,9 +228,11 @@ TestResult& checkStringEqual(TestResult& result, const Json::String& expected, JsonTest::ToJsonString(actual), __FILE__, \ __LINE__, #expected " == " #actual) +#if JSON_USE_EXCEPTION + /// \brief Asserts that a given expression throws an exception #define JSONTEST_ASSERT_THROWS(expr) \ - { \ + do { \ bool _threw = false; \ try { \ expr; \ @@ -240,7 +242,9 @@ TestResult& checkStringEqual(TestResult& result, const Json::String& expected, if (!_threw) \ result_->addFailure(__FILE__, __LINE__, \ "expected exception thrown: " #expr); \ - } + } while (0) + +#endif // JSON_USE_EXCEPTION /// \brief Begin a fixture test case. #define JSONTEST_FIXTURE(FixtureType, name) \ diff --git a/src/test_lib_json/main.cpp b/src/test_lib_json/main.cpp index 40d8ff273..501aba10e 100644 --- a/src/test_lib_json/main.cpp +++ b/src/test_lib_json/main.cpp @@ -12,6 +12,7 @@ #include "fuzz.h" #include "jsontest.h" +#include #include #include #include @@ -24,6 +25,7 @@ #include #include #include +#include using CharReaderPtr = std::unique_ptr; @@ -74,6 +76,8 @@ struct ValueTest : JsonTest::TestCase { Json::Value float_{0.00390625f}; Json::Value array1_; Json::Value object1_; + Json::Value object2_; + Json::Value object3_; Json::Value emptyString_{""}; Json::Value string1_{"a"}; Json::Value string_{"sometext with space"}; @@ -83,6 +87,34 @@ struct ValueTest : JsonTest::TestCase { ValueTest() { array1_.append(1234); object1_["id"] = 1234; + + // object2 with matching values + object2_["null"] = Json::nullValue; + object2_["bool"] = true; + object2_["int"] = Json::Int{Json::Value::maxInt}; + object2_["int64"] = Json::Int64{Json::Value::maxInt64}; + object2_["uint"] = Json::UInt{Json::Value::maxUInt}; + object2_["uint64"] = Json::UInt64{Json::Value::maxUInt64}; + object2_["integral"] = 1234; + object2_["double"] = 1234.56789; + object2_["numeric"] = 0.12345f; + object2_["string"] = "string"; + object2_["array"] = Json::arrayValue; + object2_["object"] = Json::objectValue; + + // object3 with not matching values + object3_["object"] = Json::nullValue; + object3_["null"] = true; + object3_["bool"] = Json::Int{Json::Value::maxInt}; + object3_["int"] = "not_an_int"; + object3_["int64"] = "not_an_int64"; + object3_["uint"] = "not_an_uint"; + object3_["uin64"] = "not_an_uint64"; + object3_["integral"] = 1234.56789; + object3_["double"] = false; + object3_["numeric"] = "string"; + object3_["string"] = Json::arrayValue; + object3_["array"] = Json::objectValue; } struct IsCheck { @@ -118,6 +150,8 @@ struct ValueTest : JsonTest::TestCase { /// Normalize the representation of floating-point number by stripped leading /// 0 in exponent. static Json::String normalizeFloatingPointStr(const Json::String& s); + + void runCZStringTests(); }; Json::String ValueTest::normalizeFloatingPointStr(const Json::String& s) { @@ -135,6 +169,44 @@ Json::String ValueTest::normalizeFloatingPointStr(const Json::String& s) { return normalized + exponent; } +void ValueTest::runCZStringTests() { + // 1. Copy Constructor (Index) + Json::Value::CZString idx1(123); + Json::Value::CZString idx2(idx1); + JSONTEST_ASSERT_EQUAL(idx2.index(), 123); + + // 2. Move Constructor (Index) + Json::Value::CZString idx3(std::move(idx1)); + JSONTEST_ASSERT_EQUAL(idx3.index(), 123); + + // 3. Move Assignment (Index) + Json::Value::CZString idx4(456); + idx4 = std::move(idx3); + JSONTEST_ASSERT_EQUAL(idx4.index(), 123); + + // 4. Copy Constructor (String) + Json::Value::CZString str1("param", 5, + Json::Value::CZString::duplicateOnCopy); + Json::Value::CZString str2((str1)); // copy makes it duplicate (owning) + JSONTEST_ASSERT_STRING_EQUAL(str2.data(), "param"); + + // 5. Move Constructor (String) + // Move from Owning string (str2) + Json::Value::CZString str3(std::move(str2)); + JSONTEST_ASSERT_STRING_EQUAL(str3.data(), "param"); + + // 6. Move Assignment (String) + Json::Value::CZString str4("other", 5, + Json::Value::CZString::duplicateOnCopy); + Json::Value::CZString str5((str4)); // owning "other" + // Move-assign owning "param" (str3) into owning "other" (str5) + // This verifies we don't leak "other" (if fixed) and correctly take "param" + str5 = std::move(str3); + JSONTEST_ASSERT_STRING_EQUAL(str5.data(), "param"); +} + +JSONTEST_FIXTURE_LOCAL(ValueTest, CZStringCoverage) { runCZStringTests(); } + JSONTEST_FIXTURE_LOCAL(ValueTest, checkNormalizeFloatingPointStr) { struct TestData { std::string in; @@ -218,11 +290,76 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, objects) { JSONTEST_ASSERT(foundId != nullptr); JSONTEST_ASSERT_EQUAL(Json::Value(1234), *foundId); + const std::string stringIdKey = "id"; + const Json::Value* stringFoundId = object1_.find(stringIdKey); + JSONTEST_ASSERT(stringFoundId != nullptr); + JSONTEST_ASSERT_EQUAL(Json::Value(1234), *stringFoundId); + const char unknownIdKey[] = "unknown id"; const Json::Value* foundUnknownId = object1_.find(unknownIdKey, unknownIdKey + strlen(unknownIdKey)); JSONTEST_ASSERT_EQUAL(nullptr, foundUnknownId); + const std::string stringUnknownIdKey = "unknown id"; + const Json::Value* stringFoundUnknownId = object1_.find(stringUnknownIdKey); + JSONTEST_ASSERT_EQUAL(nullptr, stringFoundUnknownId); + + // Access through find() + const Json::Value* nullFound = object2_.findNull("null"); + JSONTEST_ASSERT(nullFound != nullptr); + JSONTEST_ASSERT_EQUAL(Json::nullValue, *nullFound); + JSONTEST_ASSERT(object3_.findNull("null") == nullptr); + + const Json::Value* boolFound = object2_.findBool("bool"); + JSONTEST_ASSERT(boolFound != nullptr); + JSONTEST_ASSERT_EQUAL(true, *boolFound); + JSONTEST_ASSERT(object3_.findBool("bool") == nullptr); + + const Json::Value* intFound = object2_.findInt("int"); + JSONTEST_ASSERT(intFound != nullptr); + JSONTEST_ASSERT_EQUAL(Json::Int{Json::Value::maxInt}, *intFound); + JSONTEST_ASSERT(object3_.findInt("int") == nullptr); + + const Json::Value* int64Found = object2_.findInt64("int64"); + JSONTEST_ASSERT(int64Found != nullptr); + JSONTEST_ASSERT_EQUAL(Json::Int64{Json::Value::maxInt64}, *int64Found); + JSONTEST_ASSERT(object3_.findInt64("int64") == nullptr); + + const Json::Value* uintFound = object2_.findUInt("uint"); + JSONTEST_ASSERT(uintFound != nullptr); + JSONTEST_ASSERT_EQUAL(Json::UInt{Json::Value::maxUInt}, *uintFound); + JSONTEST_ASSERT(object3_.findUInt("uint") == nullptr); + + const Json::Value* uint64Found = object2_.findUInt64("uint64"); + JSONTEST_ASSERT(uint64Found != nullptr); + JSONTEST_ASSERT_EQUAL(Json::UInt64{Json::Value::maxUInt64}, *uint64Found); + JSONTEST_ASSERT(object3_.findUInt64("uint64") == nullptr); + + const Json::Value* integralFound = object2_.findIntegral("integral"); + JSONTEST_ASSERT(integralFound != nullptr); + JSONTEST_ASSERT_EQUAL(1234, *integralFound); + JSONTEST_ASSERT(object3_.findIntegral("integral") == nullptr); + + const Json::Value* doubleFound = object2_.findDouble("double"); + JSONTEST_ASSERT(doubleFound != nullptr); + JSONTEST_ASSERT_EQUAL(1234.56789, *doubleFound); + JSONTEST_ASSERT(object3_.findDouble("double") == nullptr); + + const Json::Value* numericFound = object2_.findNumeric("numeric"); + JSONTEST_ASSERT(numericFound != nullptr); + JSONTEST_ASSERT_EQUAL(0.12345f, *numericFound); + JSONTEST_ASSERT(object3_.findNumeric("numeric") == nullptr); + + const Json::Value* stringFound = object2_.findString("string"); + JSONTEST_ASSERT(stringFound != nullptr); + JSONTEST_ASSERT_EQUAL(std::string{"string"}, *stringFound); + JSONTEST_ASSERT(object3_.findString("string") == nullptr); + + const Json::Value* arrayFound = object2_.findArray("array"); + JSONTEST_ASSERT(arrayFound != nullptr); + JSONTEST_ASSERT_EQUAL(Json::arrayValue, *arrayFound); + JSONTEST_ASSERT(object3_.findArray("array") == nullptr); + // Access through demand() const char yetAnotherIdKey[] = "yet another id"; const Json::Value* foundYetAnotherId = @@ -308,10 +445,14 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, arrays) { const Json::Value& constArray = array1_; JSONTEST_ASSERT_EQUAL(Json::Value(1234), constArray[index0]); JSONTEST_ASSERT_EQUAL(Json::Value(1234), constArray[0]); + JSONTEST_ASSERT_EQUAL(Json::Value(1234), constArray.front()); + JSONTEST_ASSERT_EQUAL(Json::Value(1234), constArray.back()); // Access through non-const reference JSONTEST_ASSERT_EQUAL(Json::Value(1234), array1_[index0]); JSONTEST_ASSERT_EQUAL(Json::Value(1234), array1_[0]); + JSONTEST_ASSERT_EQUAL(Json::Value(1234), array1_.front()); + JSONTEST_ASSERT_EQUAL(Json::Value(1234), array1_.back()); array1_[2] = Json::Value(17); JSONTEST_ASSERT_EQUAL(Json::Value(), array1_[1]); @@ -347,6 +488,37 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, resizeArray) { JSONTEST_ASSERT_EQUAL(array.size(), 0); } } + +JSONTEST_FIXTURE_LOCAL(ValueTest, copyMoveArray) { + Json::Value array; + array.append("item1"); + array.append("item2"); + + // Test Copy Constructor (covers CZString(const CZString&) with index) + Json::Value copy(array); + JSONTEST_ASSERT_EQUAL(copy.size(), 2); + JSONTEST_ASSERT_EQUAL(Json::Value("item1"), copy[0]); + JSONTEST_ASSERT_EQUAL(Json::Value("item2"), copy[1]); + + // Test Move Constructor (covers CZString(CZString&&) with index) + Json::Value moved(std::move(copy)); + JSONTEST_ASSERT_EQUAL(moved.size(), 2); + JSONTEST_ASSERT_EQUAL(Json::Value("item1"), moved[0]); + JSONTEST_ASSERT_EQUAL(Json::Value("item2"), moved[1]); +} + +JSONTEST_FIXTURE_LOCAL(ValueTest, resizePopulatesAllMissingElements) { + Json::ArrayIndex n = 10; + Json::Value v; + v.resize(n); + JSONTEST_ASSERT_EQUAL(n, v.size()); + JSONTEST_ASSERT_EQUAL(n, std::distance(v.begin(), v.end())); + JSONTEST_ASSERT_EQUAL(v.front(), Json::Value{}); + JSONTEST_ASSERT_EQUAL(v.back(), Json::Value{}); + for (const Json::Value& e : v) + JSONTEST_ASSERT_EQUAL(e, Json::Value{}); +} + JSONTEST_FIXTURE_LOCAL(ValueTest, getArrayValue) { Json::Value array; for (Json::ArrayIndex i = 0; i < 5; i++) @@ -393,6 +565,8 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, arrayInsertAtRandomIndex) { JSONTEST_ASSERT_EQUAL(Json::Value("index0"), array[0]); // check append JSONTEST_ASSERT_EQUAL(Json::Value("index1"), array[1]); JSONTEST_ASSERT_EQUAL(Json::Value("index2"), array[2]); + JSONTEST_ASSERT_EQUAL(Json::Value("index0"), array.front()); + JSONTEST_ASSERT_EQUAL(Json::Value("index2"), array.back()); // insert lvalue at the head JSONTEST_ASSERT(array.insert(0, str1)); @@ -400,6 +574,8 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, arrayInsertAtRandomIndex) { JSONTEST_ASSERT_EQUAL(Json::Value("index0"), array[1]); JSONTEST_ASSERT_EQUAL(Json::Value("index1"), array[2]); JSONTEST_ASSERT_EQUAL(Json::Value("index2"), array[3]); + JSONTEST_ASSERT_EQUAL(Json::Value("index3"), array.front()); + JSONTEST_ASSERT_EQUAL(Json::Value("index2"), array.back()); // checking address for (Json::ArrayIndex i = 0; i < 3; i++) { JSONTEST_ASSERT_EQUAL(vec[i], &array[i]); @@ -412,6 +588,8 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, arrayInsertAtRandomIndex) { JSONTEST_ASSERT_EQUAL(Json::Value("index4"), array[2]); JSONTEST_ASSERT_EQUAL(Json::Value("index1"), array[3]); JSONTEST_ASSERT_EQUAL(Json::Value("index2"), array[4]); + JSONTEST_ASSERT_EQUAL(Json::Value("index3"), array.front()); + JSONTEST_ASSERT_EQUAL(Json::Value("index2"), array.back()); // checking address for (Json::ArrayIndex i = 0; i < 4; i++) { JSONTEST_ASSERT_EQUAL(vec[i], &array[i]); @@ -425,6 +603,8 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, arrayInsertAtRandomIndex) { JSONTEST_ASSERT_EQUAL(Json::Value("index1"), array[3]); JSONTEST_ASSERT_EQUAL(Json::Value("index2"), array[4]); JSONTEST_ASSERT_EQUAL(Json::Value("index5"), array[5]); + JSONTEST_ASSERT_EQUAL(Json::Value("index3"), array.front()); + JSONTEST_ASSERT_EQUAL(Json::Value("index5"), array.back()); // checking address for (Json::ArrayIndex i = 0; i < 5; i++) { JSONTEST_ASSERT_EQUAL(vec[i], &array[i]); @@ -1155,15 +1335,13 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, integers) { JSONTEST_ASSERT_EQUAL(true, val.asBool()); JSONTEST_ASSERT_STRING_EQUAL("-9223372036854775808", val.asString()); - // int64 min (floating point constructor). Note that kint64min *is* exactly - // representable as a double. + // int64 min (floating point constructor). Since double values in proximity of + // kint64min are rounded to kint64min, we don't check for conversion to int64. val = Json::Value(double(kint64min)); JSONTEST_ASSERT_EQUAL(Json::realValue, val.type()); checks = IsCheck(); - checks.isInt64_ = true; - checks.isIntegral_ = true; checks.isDouble_ = true; checks.isNumeric_ = true; JSONTEST_ASSERT_PRED(checkIs(val, checks)); @@ -1172,8 +1350,6 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, integers) { JSONTEST_ASSERT(!val.isConvertibleTo(Json::intValue)); JSONTEST_ASSERT(!val.isConvertibleTo(Json::uintValue)); - JSONTEST_ASSERT_EQUAL(kint64min, val.asInt64()); - JSONTEST_ASSERT_EQUAL(kint64min, val.asLargestInt()); JSONTEST_ASSERT_EQUAL(-9223372036854775808.0, val.asDouble()); JSONTEST_ASSERT_EQUAL(-9223372036854775808.0, val.asFloat()); JSONTEST_ASSERT_EQUAL(true, val.asBool()); @@ -1476,9 +1652,7 @@ void ValueTest::checkMemberCount(Json::Value& value, JSONTEST_ASSERT_PRED(checkConstMemberCount(value, expectedCount)); } -ValueTest::IsCheck::IsCheck() - - = default; +ValueTest::IsCheck::IsCheck() = default; void ValueTest::checkIs(const Json::Value& value, const IsCheck& check) { JSONTEST_ASSERT_EQUAL(check.isObject_, value.isObject()); @@ -1772,7 +1946,7 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, typeChecksThrowExceptions) { JSONTEST_ASSERT_THROWS(objVal.asBool()); JSONTEST_ASSERT_THROWS(arrVal.asBool()); -#endif +#endif // JSON_USE_EXCEPTION } JSONTEST_FIXTURE_LOCAL(ValueTest, offsetAccessors) { @@ -1876,7 +2050,7 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, CommentBefore) { Json::String result = Json::writeString(wbuilder, val); JSONTEST_ASSERT_STRING_EQUAL(expected, result); Json::String res2 = val.toStyledString(); - Json::String exp2 = ""; + Json::String exp2; exp2 += expected; exp2 += "\n"; JSONTEST_ASSERT_STRING_EQUAL(exp2, res2); @@ -2007,6 +2181,34 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, precision) { result = Json::writeString(b, v); JSONTEST_ASSERT_STRING_EQUAL(expected, result); + b.settings_["precision"] = 0; + b.settings_["precisionType"] = "decimal"; + v = 123.56345694873740545068; + expected = "124"; + result = Json::writeString(b, v); + JSONTEST_ASSERT_STRING_EQUAL(expected, result); + + b.settings_["precision"] = 1; + b.settings_["precisionType"] = "decimal"; + v = 1230.001; + expected = "1230.0"; + result = Json::writeString(b, v); + JSONTEST_ASSERT_STRING_EQUAL(expected, result); + + b.settings_["precision"] = 0; + b.settings_["precisionType"] = "decimal"; + v = 1230.001; + expected = "1230"; + result = Json::writeString(b, v); + JSONTEST_ASSERT_STRING_EQUAL(expected, result); + + b.settings_["precision"] = 0; + b.settings_["precisionType"] = "decimal"; + v = 1231.5; + expected = "1232"; + result = Json::writeString(b, v); + JSONTEST_ASSERT_STRING_EQUAL(expected, result); + b.settings_["precision"] = 10; b.settings_["precisionType"] = "decimal"; v = 0.23300000; @@ -2594,7 +2796,7 @@ JSONTEST_FIXTURE_LOCAL(StreamWriterTest, indentation) { JSONTEST_FIXTURE_LOCAL(StreamWriterTest, writeZeroes) { Json::String binary("hi", 3); // include trailing 0 JSONTEST_ASSERT_EQUAL(3, binary.length()); - Json::String expected("\"hi\\u0000\""); // unicoded zero + Json::String expected(R"("hi\u0000")"); // unicoded zero Json::StreamWriterBuilder b; { Json::Value root; @@ -2642,6 +2844,96 @@ JSONTEST_FIXTURE_LOCAL(StreamWriterTest, unicode) { "\"\\t\\n\\ud806\\udca1=\\u0133\\ud82c\\udd1b\\uff67\"\n}"); } +// Control chars should be escaped regardless of UTF-8 input encoding. +JSONTEST_FIXTURE_LOCAL(StreamWriterTest, escapeControlCharacters) { + auto uEscape = [](unsigned ch) { + static const char h[] = "0123456789abcdef"; + std::string r = "\\u"; + r += h[(ch >> (3 * 4)) & 0xf]; + r += h[(ch >> (2 * 4)) & 0xf]; + r += h[(ch >> (1 * 4)) & 0xf]; + r += h[(ch >> (0 * 4)) & 0xf]; + return r; + }; + auto shortEscape = [](unsigned ch) -> const char* { + switch (ch) { + case '\"': + return "\\\""; + case '\\': + return "\\\\"; + case '\b': + return "\\b"; + case '\f': + return "\\f"; + case '\n': + return "\\n"; + case '\r': + return "\\r"; + case '\t': + return "\\t"; + default: + return nullptr; + } + }; + + Json::StreamWriterBuilder b; + + for (bool emitUTF8 : {true, false}) { + b.settings_["emitUTF8"] = emitUTF8; + + for (unsigned i = 0; i != 0x100; ++i) { + if (!emitUTF8 && i >= 0x80) + break; // The algorithm would try to parse UTF-8, so stop here. + + std::string raw({static_cast(i)}); + std::string esc = raw; + if (i < 0x20) + esc = uEscape(i); + if (const char* shEsc = shortEscape(i)) + esc = shEsc; + + // std::cout << "emit=" << emitUTF8 << ", i=" << std::hex << i << std::dec + // << std::endl; + + Json::Value root; + root["test"] = raw; + JSONTEST_ASSERT_STRING_EQUAL( + std::string("{\n\t\"test\" : \"").append(esc).append("\"\n}"), + Json::writeString(b, root)) + << ", emit=" << emitUTF8 << ", i=" << i << ", raw=\"" << raw << "\"" + << ", esc=\"" << esc << "\""; + } + } +} + +#ifdef _WIN32 +JSONTEST_FIXTURE_LOCAL(StreamWriterTest, escapeTabCharacterWindows) { + // Get the current locale before changing it + std::string currentLocale = setlocale(LC_ALL, NULL); + setlocale(LC_ALL, "English_United States.1252"); + + Json::Value root; + root["test"] = "\tTabTesting\t"; + + Json::StreamWriterBuilder b; + + JSONTEST_ASSERT(Json::writeString(b, root) == "{\n\t\"test\" : " + "\"\\tTabTesting\\t\"\n}"); + + b.settings_["emitUTF8"] = true; + JSONTEST_ASSERT(Json::writeString(b, root) == "{\n\t\"test\" : " + "\"\\tTabTesting\\t\"\n}"); + + b.settings_["emitUTF8"] = false; + JSONTEST_ASSERT(Json::writeString(b, root) == "{\n\t\"test\" : " + "\"\\tTabTesting\\t\"\n}"); + + // Restore the locale + if (!currentLocale.empty()) + setlocale(LC_ALL, currentLocale.c_str()); +} +#endif + struct ReaderTest : JsonTest::TestCase { void setStrictMode() { reader = std::unique_ptr( @@ -2861,6 +3153,17 @@ JSONTEST_FIXTURE_LOCAL(ReaderTest, allowNumericKeysTest) { checkParse(R"({ 123 : "abc" })"); } +JSONTEST_FIXTURE_LOCAL(ReaderTest, allowDroppedNullPlaceholders) { + Json::Features features; + features.allowDroppedNullPlaceholders_ = true; + setFeatures(features); + checkParse(R"([1,,2])"); + JSONTEST_ASSERT_EQUAL(3, root.size()); + JSONTEST_ASSERT_EQUAL(1, root[0].asInt()); + JSONTEST_ASSERT(root[1].isNull()); + JSONTEST_ASSERT_EQUAL(2, root[2].asInt()); +} + struct CharReaderTest : JsonTest::TestCase {}; JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseWithNoErrors) { @@ -2868,7 +3171,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseWithNoErrors) { CharReaderPtr reader(b.newCharReader()); Json::String errs; Json::Value root; - char const doc[] = "{ \"property\" : \"value\" }"; + char const doc[] = R"({ "property" : "value" })"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(ok); JSONTEST_ASSERT(errs.empty()); @@ -2916,14 +3219,14 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseString) { JSONTEST_ASSERT_EQUAL("", root[0]); } { - char const doc[] = "[\"\\u8A2a\"]"; + char const doc[] = R"(["\u8A2a"])"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(ok); JSONTEST_ASSERT(errs.empty()); JSONTEST_ASSERT_EQUAL(u8"\u8A2a", root[0].asString()); // "訪" } { - char const doc[] = "[ \"\\uD801\" ]"; + char const doc[] = R"([ "\uD801" ])"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT(errs == "* Line 1, Column 3\n" @@ -2932,7 +3235,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseString) { "See Line 1, Column 10 for detail.\n"); } { - char const doc[] = "[ \"\\uD801\\d1234\" ]"; + char const doc[] = R"([ "\uD801\d1234" ])"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT(errs == "* Line 1, Column 3\n" @@ -2941,7 +3244,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseString) { "See Line 1, Column 12 for detail.\n"); } { - char const doc[] = "[ \"\\ua3t@\" ]"; + char const doc[] = R"([ "\ua3t@" ])"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT(errs == "* Line 1, Column 3\n" @@ -2950,7 +3253,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseString) { "See Line 1, Column 9 for detail.\n"); } { - char const doc[] = "[ \"\\ua3t\" ]"; + char const doc[] = R"([ "\ua3t" ])"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT( @@ -2962,7 +3265,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseString) { { b.settings_["allowSingleQuotes"] = true; CharReaderPtr charreader(b.newCharReader()); - char const doc[] = "{'a': 'x\\ty', \"b\":'x\\\\y'}"; + char const doc[] = R"({'a': 'x\ty', "b":'x\\y'})"; bool ok = charreader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(ok); JSONTEST_ASSERT_STRING_EQUAL("", errs); @@ -3009,7 +3312,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseObjectWithErrors) { Json::Value root; Json::String errs; { - char const doc[] = "{ \"property\" : \"value\" "; + char const doc[] = R"({ "property" : "value" )"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT(errs == "* Line 1, Column 24\n" @@ -3017,7 +3320,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseObjectWithErrors) { JSONTEST_ASSERT_EQUAL("value", root["property"]); } { - char const doc[] = "{ \"property\" : \"value\" ,"; + char const doc[] = R"({ "property" : "value" ,)"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT(errs == "* Line 1, Column 25\n" @@ -3040,7 +3343,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseArrayWithErrors) { JSONTEST_ASSERT_EQUAL("value", root[0]); } { - char const doc[] = "[ \"value1\" \"value2\" ]"; + char const doc[] = R"([ "value1" "value2" ])"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT(errs == "* Line 1, Column 12\n" @@ -3054,7 +3357,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseWithOneError) { CharReaderPtr reader(b.newCharReader()); Json::String errs; Json::Value root; - char const doc[] = "{ \"property\" :: \"value\" }"; + char const doc[] = R"({ "property" :: "value" })"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT(errs == @@ -3080,7 +3383,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseWithDetailError) { CharReaderPtr reader(b.newCharReader()); Json::String errs; Json::Value root; - char const doc[] = "{ \"property\" : \"v\\alue\" }"; + char const doc[] = R"({ "property" : "v\alue" })"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(!ok); JSONTEST_ASSERT(errs == @@ -3089,9 +3392,11 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseWithDetailError) { } JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseWithStackLimit) { +#if JSON_USE_EXCEPTION + Json::CharReaderBuilder b; Json::Value root; - char const doc[] = "{ \"property\" : \"value\" }"; + char const doc[] = R"({ "property" : "value" })"; { b.settings_["stackLimit"] = 2; CharReaderPtr reader(b.newCharReader()); @@ -3108,10 +3413,22 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseWithStackLimit) { JSONTEST_ASSERT_THROWS( reader->parse(doc, doc + std::strlen(doc), &root, &errs)); } + // Default stack limit should reject deeply nested input (regression test for + // stack exhaustion from fuzz input like [[[[...]]]]) + { + Json::CharReaderBuilder defaultBuilder; + Json::String nested(300, '['); + CharReaderPtr reader(defaultBuilder.newCharReader()); + Json::String errs; + JSONTEST_ASSERT_THROWS(reader->parse( + nested.data(), nested.data() + nested.size(), &root, &errs)); + } + +#endif // JSON_USE_EXCEPTION } JSONTEST_FIXTURE_LOCAL(CharReaderTest, testOperator) { - const std::string styled = "{ \"property\" : \"value\" }"; + const std::string styled = R"({ "property" : "value" })"; std::istringstream iss(styled); Json::Value root; iss >> root; @@ -3124,7 +3441,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderStrictModeTest, dupKeys) { Json::CharReaderBuilder b; Json::Value root; char const doc[] = - "{ \"property\" : \"value\", \"key\" : \"val1\", \"key\" : \"val2\" }"; + R"({ "property" : "value", "key" : "val1", "key" : "val2" })"; { b.strictMode(&b.settings_); CharReaderPtr reader(b.newCharReader()); @@ -3143,7 +3460,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderFailIfExtraTest, issue164) { // This is interpreted as a string value followed by a colon. Json::CharReaderBuilder b; Json::Value root; - char const doc[] = " \"property\" : \"value\" }"; + char const doc[] = R"( "property" : "value" })"; { b.settings_["failIfExtra"] = false; CharReaderPtr reader(b.newCharReader()); @@ -3288,11 +3605,11 @@ struct CharReaderAllowDropNullTest : JsonTest::TestCase { return [=](const Value& root) { JSONTEST_ASSERT_EQUAL(root, v); }; } - ValueCheck objGetAnd(std::string idx, ValueCheck f) { + static ValueCheck objGetAnd(std::string idx, ValueCheck f) { return [=](const Value& root) { f(root.get(idx, true)); }; } - ValueCheck arrGetAnd(int idx, ValueCheck f) { + static ValueCheck arrGetAnd(int idx, ValueCheck f) { return [=](const Value& root) { f(root[idx]); }; } }; @@ -3309,15 +3626,15 @@ JSONTEST_FIXTURE_LOCAL(CharReaderAllowDropNullTest, issue178) { {__LINE__, R"({"a":,"b":true})", 2, objGetAnd("a", checkEq(nullValue))}, {__LINE__, R"({"a":})", 1, objGetAnd("a", checkEq(nullValue))}, {__LINE__, "[]", 0, checkEq(emptyArray)}, - {__LINE__, "[null]", 1}, - {__LINE__, "[,]", 2}, - {__LINE__, "[,,,]", 4}, - {__LINE__, "[null,]", 2}, - {__LINE__, "[,null]", 2}, - {__LINE__, "[,,]", 3}, - {__LINE__, "[null,,]", 3}, - {__LINE__, "[,null,]", 3}, - {__LINE__, "[,,null]", 3}, + {__LINE__, "[null]", 1, nullptr}, + {__LINE__, "[,]", 2, nullptr}, + {__LINE__, "[,,,]", 4, nullptr}, + {__LINE__, "[null,]", 2, nullptr}, + {__LINE__, "[,null]", 2, nullptr}, + {__LINE__, "[,,]", 3, nullptr}, + {__LINE__, "[null,,]", 3, nullptr}, + {__LINE__, "[,null,]", 3, nullptr}, + {__LINE__, "[,,null]", 3, nullptr}, {__LINE__, "[[],,,]", 4, arrGetAnd(0, checkEq(emptyArray))}, {__LINE__, "[,[],,]", 4, arrGetAnd(1, checkEq(emptyArray))}, {__LINE__, "[,,,[]]", 4, arrGetAnd(3, checkEq(emptyArray))}, @@ -3447,8 +3764,7 @@ JSONTEST_FIXTURE_LOCAL(CharReaderAllowSpecialFloatsTest, issue209) { Json::String errs; CharReaderPtr reader(b.newCharReader()); { - char const doc[] = - "{\"a\":NaN,\"b\":Infinity,\"c\":-Infinity,\"d\":+Infinity}"; + char const doc[] = R"({"a":NaN,"b":Infinity,"c":-Infinity,"d":+Infinity})"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(ok); JSONTEST_ASSERT_STRING_EQUAL("", errs); @@ -3490,16 +3806,16 @@ JSONTEST_FIXTURE_LOCAL(CharReaderAllowSpecialFloatsTest, issue209) { for (const auto& td : test_data) { bool ok = reader->parse(&*td.in.begin(), &*td.in.begin() + td.in.size(), &root, &errs); - JSONTEST_ASSERT(td.ok == ok) << "line:" << td.line << "\n" - << " expected: {" - << "ok:" << td.ok << ", in:\'" << td.in << "\'" - << "}\n" - << " actual: {" - << "ok:" << ok << "}\n"; + // clang-format off + JSONTEST_ASSERT(td.ok == ok) << + "line:" << td.line << "\n " << + "expected: {ok:" << td.ok << ", in:\'" << td.in << "\'}\n " << + "actual: {ok:" << ok << "}\n"; + // clang-format on } { - char const doc[] = "{\"posInf\": +Infinity, \"NegInf\": -Infinity}"; + char const doc[] = R"({"posInf": +Infinity, "NegInf": -Infinity})"; bool ok = reader->parse(doc, doc + std::strlen(doc), &root, &errs); JSONTEST_ASSERT(ok); JSONTEST_ASSERT_STRING_EQUAL("", errs); @@ -3580,8 +3896,82 @@ JSONTEST_FIXTURE_LOCAL(BuilderTest, settings) { } } +struct BomTest : JsonTest::TestCase {}; + +JSONTEST_FIXTURE_LOCAL(BomTest, skipBom) { + const std::string with_bom = "\xEF\xBB\xBF{\"key\" : \"value\"}"; + Json::Value root; + JSONCPP_STRING errs; + std::istringstream iss(with_bom); + bool ok = parseFromStream(Json::CharReaderBuilder(), iss, &root, &errs); + // The default behavior is to skip the BOM, so we can parse it normally. + JSONTEST_ASSERT(ok); + JSONTEST_ASSERT(errs.empty()); + JSONTEST_ASSERT_STRING_EQUAL(root["key"].asString(), "value"); +} +JSONTEST_FIXTURE_LOCAL(BomTest, notSkipBom) { + const std::string with_bom = "\xEF\xBB\xBF{\"key\" : \"value\"}"; + Json::Value root; + JSONCPP_STRING errs; + std::istringstream iss(with_bom); + Json::CharReaderBuilder b; + b.settings_["skipBom"] = false; + bool ok = parseFromStream(b, iss, &root, &errs); + // Detect the BOM, and failed on it. + JSONTEST_ASSERT(!ok); + JSONTEST_ASSERT(!errs.empty()); +} + struct IteratorTest : JsonTest::TestCase {}; +JSONTEST_FIXTURE_LOCAL(IteratorTest, members) { + Json::Value j; + j["k1"] = "a"; + j["k2"] = "b"; + + std::vector keys; + std::vector values; + + for (const auto& member : j.members()) { + keys.push_back(member.name); + values.push_back(member.value.asString()); + } + + JSONTEST_ASSERT((keys == std::vector{"k1", "k2"})); + JSONTEST_ASSERT((values == std::vector{"a", "b"})); + + // Test modification through value reference + for (const auto& member : j.members()) { + member.value = "c"; + } + + JSONTEST_ASSERT(j["k1"].asString() == "c"); + + // Test const members + const Json::Value& cj = j; + keys.clear(); + values.clear(); + + for (const auto& member : cj.members()) { + keys.push_back(member.name); + values.push_back(member.value.asString()); + } + + JSONTEST_ASSERT((keys == std::vector{"k1", "k2"})); + JSONTEST_ASSERT((values == std::vector{"c", "c"})); + +#if __cplusplus >= 201703L + keys.clear(); + values.clear(); + for (auto const& [k, v] : cj.members()) { + keys.push_back(k); + values.push_back(v.asString()); + } + JSONTEST_ASSERT((keys == std::vector{"k1", "k2"})); + JSONTEST_ASSERT((values == std::vector{"c", "c"})); +#endif +} + JSONTEST_FIXTURE_LOCAL(IteratorTest, convert) { Json::Value j; const Json::Value& cj = j; @@ -3702,6 +4092,8 @@ JSONTEST_FIXTURE_LOCAL(IteratorTest, indexes) { } JSONTEST_FIXTURE_LOCAL(IteratorTest, constness) { +#if JSON_USE_EXCEPTION + Json::Value const v; JSONTEST_ASSERT_THROWS( Json::Value::iterator it(v.begin())); // Compile, but throw. @@ -3721,8 +4113,10 @@ JSONTEST_FIXTURE_LOCAL(IteratorTest, constness) { for (; iter != value.end(); ++iter) { out << *iter << ','; } - Json::String expected = "\" 9\",\"10\",\"11\","; + Json::String expected = R"(" 9","10","11",)"; JSONTEST_ASSERT_STRING_EQUAL(expected, out.str()); + +#endif // JSON_USE_EXCEPTION } struct RValueTest : JsonTest::TestCase {}; @@ -3749,11 +4143,41 @@ JSONTEST_FIXTURE_LOCAL(FuzzTest, fuzzDoesntCrash) { example.size())); } +struct ParseWithStructuredErrorsTest : JsonTest::TestCase { + void testErrors( + const std::string& doc, bool success, + const std::vector& expectedErrors) { + Json::CharReaderBuilder b; + CharReaderPtr reader(b.newCharReader()); + Json::Value root; + JSONTEST_ASSERT_EQUAL( + reader->parse(doc.data(), doc.data() + doc.length(), &root, nullptr), + success); + auto actualErrors = reader->getStructuredErrors(); + JSONTEST_ASSERT_EQUAL(expectedErrors.size(), actualErrors.size()); + for (std::size_t i = 0; i < actualErrors.size(); i++) { + const auto& a = actualErrors[i]; + const auto& e = expectedErrors[i]; + JSONTEST_ASSERT_EQUAL(a.offset_start, e.offset_start); + JSONTEST_ASSERT_EQUAL(a.offset_limit, e.offset_limit); + JSONTEST_ASSERT_STRING_EQUAL(a.message, e.message); + } + } +}; + +JSONTEST_FIXTURE_LOCAL(ParseWithStructuredErrorsTest, success) { + testErrors("{}", true, {}); +} + +JSONTEST_FIXTURE_LOCAL(ParseWithStructuredErrorsTest, singleError) { + testErrors("{ 1 : 2 }", false, {{2, 3, "Missing '}' or object member name"}}); +} + int main(int argc, const char* argv[]) { JsonTest::Runner runner; - for (auto it = local_.begin(); it != local_.end(); it++) { - runner.add(*it); + for (auto& local : local_) { + runner.add(local); } return runner.runCommandLine(argc, argv); @@ -3803,6 +4227,20 @@ JSONTEST_FIXTURE_LOCAL(MemberTemplateIs, BehavesSameAsNamedIs) { } } +class VersionTest : public JsonTest::TestCase {}; + +JSONTEST_FIXTURE_LOCAL(VersionTest, VersionNumbersMatch) { + std::ostringstream vstr; + vstr << JSONCPP_VERSION_MAJOR << '.' << JSONCPP_VERSION_MINOR << '.' + << JSONCPP_VERSION_PATCH; + JSONTEST_ASSERT_EQUAL(vstr.str(), std::string(JSONCPP_VERSION_STRING)); +} + +JSONTEST_FIXTURE_LOCAL(VersionTest, RuntimeVersionString) { + JSONTEST_ASSERT_EQUAL(std::string(JSONCPP_VERSION_STRING), + std::string(Json::version())); +} + #if defined(__GNUC__) #pragma GCC diagnostic pop #endif diff --git a/test/BUILD.bazel b/test/BUILD.bazel new file mode 100644 index 000000000..c1a3623a1 --- /dev/null +++ b/test/BUILD.bazel @@ -0,0 +1,29 @@ +load("@rules_python//python:defs.bzl", "py_test") + +filegroup( + name = "expected", + srcs = glob( + [ + "data/**", + "jsonchecker/**", + ], + exclude = ["**/*.json"], + ), +) + +[py_test( + name = "runjson_%s_test" % "_".join(f.split("/")), + srcs = ["runjsontests.py"], + args = [ + "--with-json-checker", + "$(location //src/jsontestrunner:jsontestrunner)", + "$(location :%s)" % f, + ], + data = [ + ":expected", + "//src/jsontestrunner", + ":%s" % f, + ], + main = "runjsontests.py", + tags = ["no-sandbox"], +) for f in glob(["**/*.json"])] diff --git a/test/data/fail_invalid_quote.json b/test/data/fail_invalid_quote.json new file mode 100644 index 000000000..dae27f53f --- /dev/null +++ b/test/data/fail_invalid_quote.json @@ -0,0 +1 @@ +{'//this is bad JSON.'} \ No newline at end of file diff --git a/test/data/fail_strict_comment_01.json b/test/data/fail_strict_comment_01.json new file mode 100644 index 000000000..b7e0a5e75 --- /dev/null +++ b/test/data/fail_strict_comment_01.json @@ -0,0 +1,4 @@ +{ + "a": "aaa", + "b": "bbb" // comments not allowed in strict mode +} diff --git a/test/data/fail_strict_comment_02.json b/test/data/fail_strict_comment_02.json new file mode 100644 index 000000000..699a7f731 --- /dev/null +++ b/test/data/fail_strict_comment_02.json @@ -0,0 +1,4 @@ +{ + "a": "aaa", // comments not allowed in strict mode + "b": "bbb" +} diff --git a/test/data/fail_strict_comment_03.json b/test/data/fail_strict_comment_03.json new file mode 100644 index 000000000..5f0fabf1f --- /dev/null +++ b/test/data/fail_strict_comment_03.json @@ -0,0 +1,3 @@ +{ + "array" : [1, 2, 3 /* comments not allowed in strict mode */] +} diff --git a/test/data/fail_test_control_char_01.json b/test/data/fail_test_control_char_01.json new file mode 100644 index 000000000..1f6835573 --- /dev/null +++ b/test/data/fail_test_control_char_01.json @@ -0,0 +1 @@ +"" \ No newline at end of file diff --git a/test/data/fail_test_object_02.json b/test/data/fail_test_object_02.json new file mode 100644 index 000000000..afe62c5b5 --- /dev/null +++ b/test/data/fail_test_object_02.json @@ -0,0 +1 @@ +{"one": 1 /* } */ { "two" : 2 } diff --git a/test/data/legacy_test_array_06.json b/test/data/legacy_test_array_06.json index 7f6c516af..1fda03bb1 100644 --- a/test/data/legacy_test_array_06.json +++ b/test/data/legacy_test_array_06.json @@ -1,4 +1,4 @@ -[ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", +[ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "ccccccccccccccccccccccc", "dddddddddddddddddddddddddddddddddddddddddddddddddddd" ] \ No newline at end of file diff --git a/test/data/legacy_test_complex_01.json b/test/data/legacy_test_complex_01.json index cc0f30f5c..2c4a869ab 100644 --- a/test/data/legacy_test_complex_01.json +++ b/test/data/legacy_test_complex_01.json @@ -1,17 +1,17 @@ -{ +{ "count" : 1234, "name" : { "aka" : "T.E.S.T.", "id" : 123987 }, - "attribute" : [ - "random", - "short", - "bold", - 12, - { "height" : 7, "width" : 64 } + "attribute" : [ + "random", + "short", + "bold", + 12, + { "height" : 7, "width" : 64 } ], - "test": { "1" : - { "2" : - { "3" : { "coord" : [ 1,2] } - } + "test": { "1" : + { "2" : + { "3" : { "coord" : [ 1,2] } + } } } } diff --git a/test/data/legacy_test_object_03.json b/test/data/legacy_test_object_03.json index 4fcd4d821..90dba2af8 100644 --- a/test/data/legacy_test_object_03.json +++ b/test/data/legacy_test_object_03.json @@ -1,4 +1,4 @@ -{ +{ "count" : 1234, "name" : "test", "attribute" : "random" diff --git a/test/data/legacy_test_object_04.json b/test/data/legacy_test_object_04.json index 450762d71..9e43ff89b 100644 --- a/test/data/legacy_test_object_04.json +++ b/test/data/legacy_test_object_04.json @@ -1,3 +1,3 @@ -{ +{ "" : 1234 } diff --git a/test/data/legacy_test_preserve_comment_01.expected b/test/data/legacy_test_preserve_comment_01.expected index 2797aa7d6..d6c11b4c9 100644 --- a/test/data/legacy_test_preserve_comment_01.expected +++ b/test/data/legacy_test_preserve_comment_01.expected @@ -6,6 +6,6 @@ /* Comment before 'second' */ .second=2 -/* A comment at +/* A comment at the end of the file. */ diff --git a/test/data/legacy_test_preserve_comment_01.json b/test/data/legacy_test_preserve_comment_01.json index fabd55dd9..21b5ea7fa 100644 --- a/test/data/legacy_test_preserve_comment_01.json +++ b/test/data/legacy_test_preserve_comment_01.json @@ -9,6 +9,6 @@ "second" : 2 } -/* A comment at +/* A comment at the end of the file. */ diff --git a/test/data/legacy_test_real_13.expected b/test/data/legacy_test_real_13.expected new file mode 100644 index 000000000..8d3f03faa --- /dev/null +++ b/test/data/legacy_test_real_13.expected @@ -0,0 +1,3 @@ +.=[] +.[0]=-inf +.[1]=inf diff --git a/test/data/legacy_test_real_13.json b/test/data/legacy_test_real_13.json new file mode 100644 index 000000000..287258a81 --- /dev/null +++ b/test/data/legacy_test_real_13.json @@ -0,0 +1 @@ +[-1e+9999, 1e+9999] diff --git a/test/pyjsontestrunner.py b/test/pyjsontestrunner.py index bd749b530..8acdbd2de 100644 --- a/test/pyjsontestrunner.py +++ b/test/pyjsontestrunner.py @@ -15,7 +15,7 @@ if len(sys.argv) != 2: print("Usage: %s input-json-file", sys.argv[0]) sys.exit(3) - + input_path = sys.argv[1] base_path = os.path.splitext(input_path)[0] actual_path = base_path + '.actual' @@ -23,7 +23,7 @@ rewrite_actual_path = base_path + '.actual-rewrite' def valueTreeToString(fout, value, path = '.'): - ty = type(value) + ty = type(value) if ty is types.DictType: fout.write('%s={}\n' % path) suffix = path[-1] != '.' and '.' or '' @@ -49,7 +49,7 @@ def valueTreeToString(fout, value, path = '.'): fout.write('%s=null\n' % path) else: assert False and "Unexpected value type" - + def parseAndSaveValueTree(input, actual_path): root = json.loads(input) fout = file(actual_path, 'wt') @@ -62,7 +62,7 @@ def rewriteValueTree(value, rewrite_path): #rewrite = rewrite[1:-1] # Somehow the string is quoted ! jsonpy bug ? file(rewrite_path, 'wt').write(rewrite + '\n') return rewrite - + input = file(input_path, 'rt').read() root = parseAndSaveValueTree(input, actual_path) rewrite = rewriteValueTree(json.write(root), rewrite_path) diff --git a/test/runjsontests.py b/test/runjsontests.py index dfdeca3ea..14275ec22 100644 --- a/test/runjsontests.py +++ b/test/runjsontests.py @@ -62,64 +62,65 @@ def safeReadFile(path): except IOError as e: return '' % (path,e) -def runAllTests(jsontest_executable_path, input_dir = None, +class FailError(Exception): + def __init__(self, msg): + super(Exception, self).__init__(msg) + +def runAllTests(jsontest_executable_path, input_path = None, use_valgrind=False, with_json_checker=False, writerClass='StyledWriter'): - if not input_dir: - input_dir = os.path.join(os.getcwd(), 'data') - tests = glob(os.path.join(input_dir, '*.json')) - if with_json_checker: - all_test_jsonchecker = glob(os.path.join(input_dir, '../jsonchecker', '*.json')) - # These tests fail with strict json support, but pass with jsoncpp extra lieniency - """ - Failure details: - * Test ../jsonchecker/fail25.json - Parsing should have failed: - [" tab character in string "] - - * Test ../jsonchecker/fail13.json - Parsing should have failed: - {"Numbers cannot have leading zeroes": 013} - - * Test ../jsonchecker/fail18.json - Parsing should have failed: - [[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]] - - * Test ../jsonchecker/fail8.json - Parsing should have failed: - ["Extra close"]] - - * Test ../jsonchecker/fail7.json - Parsing should have failed: - ["Comma after the close"], - - * Test ../jsonchecker/fail10.json - Parsing should have failed: - {"Extra value after close": true} "misplaced quoted value" - - * Test ../jsonchecker/fail27.json - Parsing should have failed: - ["line - break"] - """ - known_differences_withjsonchecker = [ "fail25.json", "fail13.json", "fail18.json", "fail8.json", - "fail7.json", "fail10.json", "fail27.json" ] - test_jsonchecker = [ test for test in all_test_jsonchecker if os.path.basename(test) not in known_differences_withjsonchecker ] - + if not input_path: + input_path = os.path.join(os.getcwd(), 'data') + + if os.path.isdir(input_path): + tests = [ + os.path.normpath(os.path.abspath(test)) + for test in glob(os.path.join(input_path, '*.json')) + ] + + if with_json_checker: + tests += [ + os.path.normpath(os.path.abspath(test)) + for test in glob(os.path.join(input_path, '../jsonchecker', '*.json')) + ] else: - test_jsonchecker = [] + tests = [input_path] + + # These tests fail with strict json support, but pass with JsonCPP's + # extra leniency features. When adding a new exclusion to this list, + # remember to add the test's number and reasoning here: + known = ["fail{}.json".format(n) for n in [ + 4, 9, # fail because we allow trailing commas + 7, # fails because we allow commas after close + 8, # fails because we allow extra close + 10, # fails because we allow extra values after close + 13, # fails because we allow leading zeroes in numbers + 18, # fails because we allow deeply nested values + 25, # fails because we allow tab characters in strings + 27, # fails because we allow string line breaks + ]] + + tests = [ + test for test in tests + if os.path.basename(test) not in known or + os.path.basename(os.path.dirname(test)) != "jsonchecker" + ] + failed_tests = [] valgrind_path = use_valgrind and VALGRIND_CMD or '' - for input_path in tests + test_jsonchecker: + for input_path in tests: expect_failure = os.path.basename(input_path).startswith('fail') - is_json_checker_test = (input_path in test_jsonchecker) or expect_failure + is_json_checker_test = os.path.basename(os.path.dirname(input_path)) == "jsonchecker" + is_parse_only = is_json_checker_test or expect_failure + is_strict_test = ('_strict_' in os.path.basename(input_path)) or is_json_checker_test print('TESTING:', input_path, end=' ') - options = is_json_checker_test and '--json-checker' or '' + options = is_parse_only and '--parse-only' or '' + options += is_strict_test and ' --strict' or '' options += ' --json-writer %s'%writerClass cmd = '%s%s %s "%s"' % ( valgrind_path, jsontest_executable_path, options, input_path) status, process_output = getStatusOutput(cmd) - if is_json_checker_test: + if is_parse_only: if expect_failure: if not status: print('FAILED') @@ -161,10 +162,9 @@ def runAllTests(jsontest_executable_path, input_dir = None, print() print('Test results: %d passed, %d failed.' % (len(tests)-len(failed_tests), len(failed_tests))) - return 1 + raise FailError(repr(failed_tests)) else: print('All %d tests passed.' % len(tests)) - return 0 def main(): from optparse import OptionParser @@ -187,24 +187,21 @@ def main(): input_path = os.path.normpath(os.path.abspath(args[1])) else: input_path = None - status = runAllTests(jsontest_executable_path, input_path, + runAllTests(jsontest_executable_path, input_path, use_valgrind=options.valgrind, with_json_checker=options.with_json_checker, writerClass='StyledWriter') - if status: - sys.exit(status) - status = runAllTests(jsontest_executable_path, input_path, + runAllTests(jsontest_executable_path, input_path, use_valgrind=options.valgrind, with_json_checker=options.with_json_checker, writerClass='StyledStreamWriter') - if status: - sys.exit(status) - status = runAllTests(jsontest_executable_path, input_path, + runAllTests(jsontest_executable_path, input_path, use_valgrind=options.valgrind, with_json_checker=options.with_json_checker, writerClass='BuiltStyledStreamWriter') - if status: - sys.exit(status) if __name__ == '__main__': - main() + try: + main() + except FailError: + sys.exit(1) diff --git a/version.in b/version.in index bfc03f7dd..f1e333570 100644 --- a/version.in +++ b/version.in @@ -1 +1 @@ -@JSONCPP_VERSION@ +@jsoncpp_VERSION@