From a4f5a669212fbbc089e549f326d7c7bb495f15db Mon Sep 17 00:00:00 2001 From: Morax Date: Fri, 11 Sep 2026 02:48:11 +0200 Subject: [PATCH 1/2] Fix negative integer parsing with JSON_NO_INT64 --- .github/workflows/cmake.yml | 19 ++- src/lib_json/json_reader.cpp | 3 +- src/lib_json/json_value.cpp | 4 +- src/test_lib_json/jsontest.cpp | 2 + src/test_lib_json/jsontest.h | 2 + src/test_lib_json/main.cpp | 108 +++++++++++++++++- ...y_test_integer_06_64bits.expected-no-int64 | 1 + ...y_test_integer_07_64bits.expected-no-int64 | 1 + ...y_test_integer_08_64bits.expected-no-int64 | 1 + test/runjsontests.py | 16 +++ 10 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 test/data/legacy_test_integer_06_64bits.expected-no-int64 create mode 100644 test/data/legacy_test_integer_07_64bits.expected-no-int64 create mode 100644 test/data/legacy_test_integer_08_64bits.expected-no-int64 diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index cccf8ed6c..17de32fed 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -22,4 +22,21 @@ jobs: uses: threeal/cmake-action@v2.0.0 with: options: CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }} - + + cmake-no-int64: + runs-on: ubuntu-latest + steps: + - name: checkout project + uses: actions/checkout@v5 + + - name: build without 64-bit integer storage + run: | + cmake -S . -B build-no-int64 \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_STANDARD=11 \ + -DCMAKE_CXX_FLAGS=-DJSON_NO_INT64 \ + -DJSONCPP_WITH_POST_BUILD_UNITTEST=OFF + cmake --build build-no-int64 --parallel 2 + + - name: test without 64-bit integer storage + run: ctest --test-dir build-no-int64 --output-on-failure diff --git a/src/lib_json/json_reader.cpp b/src/lib_json/json_reader.cpp index 2f901f510..ed7371205 100644 --- a/src/lib_json/json_reader.cpp +++ b/src/lib_json/json_reader.cpp @@ -1644,7 +1644,8 @@ bool OurReader::decodeNumber(Token& token, Value& decoded) { if (isNegative) { // We use the same magnitude assumption here, just in case. - const auto last_digit = static_cast(value % 10); + // Keep the subtraction signed when JSON_NO_INT64 makes LargestInt an int. + 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); diff --git a/src/lib_json/json_value.cpp b/src/lib_json/json_value.cpp index 1725f771b..69521fd93 100644 --- a/src/lib_json/json_value.cpp +++ b/src/lib_json/json_value.cpp @@ -90,11 +90,13 @@ static inline bool InRange(double d, T min, U 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) +#else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) +#if defined(JSON_HAS_INT64) static inline double integerToDouble(Json::UInt64 value) { return static_cast(Int64(value / 2)) * 2.0 + static_cast(Int64(value & 1)); } +#endif template static inline double integerToDouble(T value) { return static_cast(value); diff --git a/src/test_lib_json/jsontest.cpp b/src/test_lib_json/jsontest.cpp index 508067a77..de6189f9a 100644 --- a/src/test_lib_json/jsontest.cpp +++ b/src/test_lib_json/jsontest.cpp @@ -187,6 +187,7 @@ TestResult& TestResult::addToLastFailure(const Json::String& message) { return *this; } +#if defined(JSON_HAS_INT64) TestResult& TestResult::operator<<(Json::Int64 value) { return addToLastFailure(Json::valueToString(value)); } @@ -194,6 +195,7 @@ TestResult& TestResult::operator<<(Json::Int64 value) { TestResult& TestResult::operator<<(Json::UInt64 value) { return addToLastFailure(Json::valueToString(value)); } +#endif TestResult& TestResult::operator<<(bool value) { return addToLastFailure(value ? "true" : "false"); diff --git a/src/test_lib_json/jsontest.h b/src/test_lib_json/jsontest.h index 3652c4029..ef154f771 100644 --- a/src/test_lib_json/jsontest.h +++ b/src/test_lib_json/jsontest.h @@ -91,8 +91,10 @@ class TestResult { // Specialized versions. TestResult& operator<<(bool value); // std:ostream does not support 64bits integers on all STL implementation +#if defined(JSON_HAS_INT64) TestResult& operator<<(Json::Int64 value); TestResult& operator<<(Json::UInt64 value); +#endif private: TestResult& addToLastFailure(const Json::String& message); diff --git a/src/test_lib_json/main.cpp b/src/test_lib_json/main.cpp index e09b87b84..6d28372b1 100644 --- a/src/test_lib_json/main.cpp +++ b/src/test_lib_json/main.cpp @@ -14,6 +14,7 @@ #include "jsontest.h" #include #include +#include #include #include #include @@ -39,9 +40,15 @@ JSON_API size_t& newlineScanByteCountForTesting(); #define kint32max Json::Value::maxInt #define kint32min Json::Value::minInt #define kuint32max Json::Value::maxUInt +#if defined(JSON_HAS_INT64) #define kint64max Json::Value::maxInt64 #define kint64min Json::Value::minInt64 #define kuint64max Json::Value::maxUInt64 +#else +#define kint64max INT64_MAX +#define kint64min INT64_MIN +#define kuint64max UINT64_MAX +#endif // static const double kdint64max = double(kint64max); // static const float kfint64max = float(kint64max); @@ -54,6 +61,7 @@ static const float kfuint32max = float(kuint32max); // ////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////// +#if defined(JSON_HAS_INT64) #if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) static inline double uint64ToDouble(Json::UInt64 value) { return static_cast(value); @@ -64,6 +72,7 @@ static inline double uint64ToDouble(Json::UInt64 value) { static_cast(Json::Int64(value & 1)); } #endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) +#endif // if defined(JSON_HAS_INT64) // local_ is the collection for the testcases in this code file. static std::deque local_; @@ -97,9 +106,9 @@ struct ValueTest : JsonTest::TestCase { object2_["null"] = Json::nullValue; object2_["bool"] = true; object2_["int"] = Json::Int{Json::Value::maxInt}; - object2_["int64"] = Json::Int64{Json::Value::maxInt64}; + object2_["int64"] = Json::LargestInt{Json::Value::maxLargestInt}; object2_["uint"] = Json::UInt{Json::Value::maxUInt}; - object2_["uint64"] = Json::UInt64{Json::Value::maxUInt64}; + object2_["uint64"] = Json::LargestUInt{Json::Value::maxLargestUInt}; object2_["integral"] = 1234; object2_["double"] = 1234.56789; object2_["numeric"] = 0.12345f; @@ -326,8 +335,12 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, objects) { JSONTEST_ASSERT(object3_.findInt("int") == nullptr); const Json::Value* int64Found = object2_.findInt64("int64"); +#if defined(JSON_HAS_INT64) JSONTEST_ASSERT(int64Found != nullptr); JSONTEST_ASSERT_EQUAL(Json::Int64{Json::Value::maxInt64}, *int64Found); +#else + JSONTEST_ASSERT(int64Found == nullptr); +#endif JSONTEST_ASSERT(object3_.findInt64("int64") == nullptr); const Json::Value* uintFound = object2_.findUInt("uint"); @@ -336,8 +349,12 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, objects) { JSONTEST_ASSERT(object3_.findUInt("uint") == nullptr); const Json::Value* uint64Found = object2_.findUInt64("uint64"); +#if defined(JSON_HAS_INT64) JSONTEST_ASSERT(uint64Found != nullptr); JSONTEST_ASSERT_EQUAL(Json::UInt64{Json::Value::maxUInt64}, *uint64Found); +#else + JSONTEST_ASSERT(uint64Found == nullptr); +#endif JSONTEST_ASSERT(object3_.findUInt64("uint64") == nullptr); const Json::Value* integralFound = object2_.findIntegral("integral"); @@ -1151,7 +1168,7 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, integers) { JSONTEST_ASSERT_EQUAL(double(kint64max), val.asDouble()); JSONTEST_ASSERT_EQUAL(float(kint64max), val.asFloat()); JSONTEST_ASSERT_EQUAL(true, val.asBool()); - JSONTEST_ASSERT_STRING_EQUAL("9.22337e+18", val.asString()); + JSONTEST_ASSERT_STRING_EQUAL("9.2233720368547758e+18", val.asString()); // int64 min val = Json::Value(double(kint64min)); @@ -1170,7 +1187,7 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, integers) { JSONTEST_ASSERT_EQUAL(double(kint64min), val.asDouble()); JSONTEST_ASSERT_EQUAL(float(kint64min), val.asFloat()); JSONTEST_ASSERT_EQUAL(true, val.asBool()); - JSONTEST_ASSERT_STRING_EQUAL("-9.22337e+18", val.asString()); + JSONTEST_ASSERT_STRING_EQUAL("-9.2233720368547758e+18", val.asString()); // uint64 max val = Json::Value(double(kuint64max)); @@ -1189,7 +1206,7 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, integers) { JSONTEST_ASSERT_EQUAL(double(kuint64max), val.asDouble()); JSONTEST_ASSERT_EQUAL(float(kuint64max), val.asFloat()); JSONTEST_ASSERT_EQUAL(true, val.asBool()); - JSONTEST_ASSERT_STRING_EQUAL("1.84467e+19", val.asString()); + JSONTEST_ASSERT_STRING_EQUAL("1.8446744073709552e+19", val.asString()); #else // ifdef JSON_NO_INT64 // 2^40 (signed constructor arg) val = Json::Value(Json::Int64(1) << 40); @@ -1961,6 +1978,7 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, typeChecksThrowExceptions) { JSONTEST_ASSERT_THROWS(objVal.asUInt()); JSONTEST_ASSERT_THROWS(arrVal.asUInt()); +#if defined(JSON_HAS_INT64) JSONTEST_ASSERT_THROWS(strVal.asInt64()); JSONTEST_ASSERT_THROWS(objVal.asInt64()); JSONTEST_ASSERT_THROWS(arrVal.asInt64()); @@ -1968,6 +1986,7 @@ JSONTEST_FIXTURE_LOCAL(ValueTest, typeChecksThrowExceptions) { JSONTEST_ASSERT_THROWS(strVal.asUInt64()); JSONTEST_ASSERT_THROWS(objVal.asUInt64()); JSONTEST_ASSERT_THROWS(arrVal.asUInt64()); +#endif JSONTEST_ASSERT_THROWS(strVal.asDouble()); JSONTEST_ASSERT_THROWS(objVal.asDouble()); @@ -3252,6 +3271,81 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseNumber) { } } +JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseNegativeIntegers) { + Json::CharReaderBuilder builder; + CharReaderPtr reader(builder.newCharReader()); + const Json::LargestInt values[] = {0, + -1, + -9, + -10, + -11, + -19, + -20, + -99, + -100, + Json::Value::minInt, + Json::Value::minLargestInt + 1, + Json::Value::minLargestInt}; + for (const auto expected : values) { + const Json::String number = + expected == 0 ? "-0" : Json::valueToString(expected); + const Json::String documents[] = {number, "[" + number + "]", + "{\"test\":" + number + "}"}; + for (const auto& document : documents) { + Json::Value root; + Json::String errors; + const bool ok = reader->parse( + document.data(), document.data() + document.size(), &root, &errors); + JSONTEST_ASSERT(ok); + JSONTEST_ASSERT(errors.empty()); + if (!ok) + continue; + const Json::Value& actual = root.isArray() ? root[0] + : root.isObject() ? root["test"] + : root; + JSONTEST_ASSERT_EQUAL(Json::intValue, actual.type()); + JSONTEST_ASSERT_EQUAL(Json::Value(expected), actual); + if (actual.type() == Json::intValue) + JSONTEST_ASSERT_EQUAL(expected, actual.asLargestInt()); + } + } +} + +JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseIntegerLimits) { + Json::CharReaderBuilder builder; + CharReaderPtr reader(builder.newCharReader()); + const struct { + Json::String document; + Json::Value expected; + } cases[] = { + {"0", Json::Value(0)}, + {"1", Json::Value(1)}, + {Json::valueToString(Json::Value::maxLargestInt), + Json::Value(Json::Value::maxLargestInt)}, + {Json::valueToString(Json::Value::maxLargestUInt), + Json::Value(Json::Value::maxLargestUInt)}, +#if defined(JSON_HAS_INT64) + {"9223372036854775808", Json::Value(Json::UInt64(1) << 63)}, + {"-9223372036854775809", Json::Value(-9223372036854775809.0)}, + {"18446744073709551616", Json::Value(18446744073709551616.0)}, +#else + {"2147483648", Json::Value(Json::UInt(1) << 31)}, + {"-2147483649", Json::Value(-2147483649.0)}, + {"4294967296", Json::Value(4294967296.0)}, +#endif + }; + for (const auto& c : cases) { + Json::Value root; + Json::String errors; + JSONTEST_ASSERT(reader->parse(c.document.data(), + c.document.data() + c.document.size(), &root, + &errors)); + JSONTEST_ASSERT(errors.empty()); + JSONTEST_ASSERT_EQUAL(c.expected.type(), root.type()); + JSONTEST_ASSERT_EQUAL(c.expected, root); + } +} + JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseSubnormal) { // Regression test for #1427: subnormal doubles make operator>> set failbit // even though it produced the correctly-rounded value, so they used to fail @@ -4407,9 +4501,11 @@ JSONTEST_FIXTURE_LOCAL(MemberTemplateIs, BehavesSameAsNamedIs) { for (const Json::Value& j : values) { JSONTEST_ASSERT_EQUAL(j.is(), j.isBool()); JSONTEST_ASSERT_EQUAL(j.is(), j.isInt()); - JSONTEST_ASSERT_EQUAL(j.is(), j.isInt64()); JSONTEST_ASSERT_EQUAL(j.is(), j.isUInt()); +#if defined(JSON_HAS_INT64) + JSONTEST_ASSERT_EQUAL(j.is(), j.isInt64()); JSONTEST_ASSERT_EQUAL(j.is(), j.isUInt64()); +#endif JSONTEST_ASSERT_EQUAL(j.is(), j.isDouble()); JSONTEST_ASSERT_EQUAL(j.is(), j.isString()); } diff --git a/test/data/legacy_test_integer_06_64bits.expected-no-int64 b/test/data/legacy_test_integer_06_64bits.expected-no-int64 new file mode 100644 index 000000000..03dbadec3 --- /dev/null +++ b/test/data/legacy_test_integer_06_64bits.expected-no-int64 @@ -0,0 +1 @@ +.=9.223372036854776e+18 diff --git a/test/data/legacy_test_integer_07_64bits.expected-no-int64 b/test/data/legacy_test_integer_07_64bits.expected-no-int64 new file mode 100644 index 000000000..c967af433 --- /dev/null +++ b/test/data/legacy_test_integer_07_64bits.expected-no-int64 @@ -0,0 +1 @@ +.=-9.223372036854776e+18 diff --git a/test/data/legacy_test_integer_08_64bits.expected-no-int64 b/test/data/legacy_test_integer_08_64bits.expected-no-int64 new file mode 100644 index 000000000..a0003192d --- /dev/null +++ b/test/data/legacy_test_integer_08_64bits.expected-no-int64 @@ -0,0 +1 @@ +.=1.844674407370955e+19 diff --git a/test/runjsontests.py b/test/runjsontests.py index 14275ec22..769b26912 100644 --- a/test/runjsontests.py +++ b/test/runjsontests.py @@ -11,6 +11,7 @@ import os import os.path import optparse +import subprocess VALGRIND_CMD = 'valgrind --tool=memcheck --leak-check=yes --undef-value-errors=yes ' @@ -66,9 +67,22 @@ class FailError(Exception): def __init__(self, msg): super(Exception, self).__init__(msg) +def isInt64Enabled(jsontest_executable_path): + process = subprocess.Popen([jsontest_executable_path, '--json-config'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, errors = process.communicate() + configuration = output.decode('utf-8').strip() + # The existing --json-config command exits with the usage status (3). + if process.returncode not in (0, 3) or configuration not in ( + 'JSON_NO_INT64=0', 'JSON_NO_INT64=1'): + raise FailError('Could not read JsonCpp integer configuration: %s' % + errors.decode('utf-8', 'replace')) + return configuration == 'JSON_NO_INT64=0' + def runAllTests(jsontest_executable_path, input_path = None, use_valgrind=False, with_json_checker=False, writerClass='StyledWriter'): + int64_enabled = isInt64Enabled(jsontest_executable_path) if not input_path: input_path = os.path.join(os.getcwd(), 'data') @@ -144,6 +158,8 @@ def runAllTests(jsontest_executable_path, input_path = None, failed_tests.append((input_path, 'Parsing failed:\n' + process_output)) else: expected_output_path = os.path.splitext(input_path)[0] + '.expected' + if not int64_enabled and os.path.isfile(expected_output_path + '-no-int64'): + expected_output_path += '-no-int64' expected_output = open(expected_output_path, 'rt', encoding = 'utf-8').read() detail = (compareOutputs(expected_output, actual_output, 'input') or compareOutputs(expected_output, actual_rewrite_output, 'rewrite')) From 94dcd051f51b031b517690d85326cfc4e212ba49 Mon Sep 17 00:00:00 2001 From: Morax Date: Fri, 11 Sep 2026 03:01:22 +0200 Subject: [PATCH 2/2] Pin checkout and limit permissions in the no-int64 CI job --- .github/workflows/cmake.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 17de32fed..e130fc171 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -25,9 +25,13 @@ jobs: cmake-no-int64: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: checkout project - uses: actions/checkout@v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false - name: build without 64-bit integer storage run: |