Merge branch 'develop' into claude/fix-issue-3989-db7e45
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
342 files changed
+605561
-780
No files matched your search
@@ -285,6 +285,10 @@ if (JSON_TestSimdutf)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# test-file_input checks from a second thread that parsing from a FILE* releases the file's lock
|
||||
find_package(Threads REQUIRED)
|
||||
json_test_set_test_options("test-file_input;test-file_input_unlocked_io_disabled" LINK_LIBRARIES Threads::Threads)
|
||||
|
||||
# *DO* use json_test_set_test_options() above this line
|
||||
|
||||
json_test_should_build_32bit_test(json_32bit_test json_32bit_test_only "${JSON_32bitTest}")
|
||||
@@ -369,6 +373,52 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# test reading from a std::istream again without scanning the get area in bulk
|
||||
json_test_set_test_options(test-stream-bulk-scan_disabled
|
||||
COMPILE_DEFINITIONS JSON_DISABLE_STREAM_BULK_SCAN=1
|
||||
)
|
||||
json_test_add_test_for(src/unit-stream-bulk-scan.cpp
|
||||
NAME test-stream-bulk-scan_disabled
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# test reading from a std::istream again with JSON_PRECISE_STREAM_POSITION
|
||||
json_test_set_test_options(test-stream-bulk-scan_precise
|
||||
COMPILE_DEFINITIONS JSON_PRECISE_STREAM_POSITION=1
|
||||
)
|
||||
json_test_add_test_for(src/unit-stream-bulk-scan.cpp
|
||||
NAME test-stream-bulk-scan_precise
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# test reading from a FILE* again with the locking stdio functions
|
||||
json_test_set_test_options(test-file_input_unlocked_io_disabled
|
||||
COMPILE_DEFINITIONS JSON_DISABLE_UNLOCKED_FILE_IO=1
|
||||
)
|
||||
json_test_add_test_for(src/unit-file_input.cpp
|
||||
NAME test-file_input_unlocked_io_disabled
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# the json_view parser again with the portable string scanning instead of
|
||||
# NEON/SSE2, and on x86-64 with the SSSE3 UTF-8 check (JSON_VIEW_USE_SSSE3)
|
||||
json_test_set_test_options(test-json_view_builder_portable
|
||||
COMPILE_DEFINITIONS JSON_VIEW_NO_SIMD
|
||||
)
|
||||
json_test_add_test_for(src/unit-json_view_builder.cpp
|
||||
NAME test-json_view_builder_portable
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC)
|
||||
json_test_set_test_options(test-json_view_builder_ssse3
|
||||
COMPILE_DEFINITIONS JSON_VIEW_USE_SSSE3
|
||||
COMPILE_OPTIONS -mssse3
|
||||
)
|
||||
json_test_add_test_for(src/unit-json_view_builder.cpp
|
||||
NAME test-json_view_builder_ssse3
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# *DO NOT* use json_test_set_test_options() below this line
|
||||
|
||||
+7
-1
@@ -10,12 +10,18 @@ CXXFLAGS += -std=c++11
|
||||
CPPFLAGS += -I ../single_include
|
||||
|
||||
FUZZER_ENGINE = src/fuzzer-driver_afl.cpp
|
||||
FUZZERS = parse_afl_fuzzer parse_bson_fuzzer parse_cbor_fuzzer parse_msgpack_fuzzer parse_ubjson_fuzzer parse_bjdata_fuzzer parse_bon8_fuzzer
|
||||
FUZZERS = parse_afl_fuzzer parse_bson_fuzzer parse_cbor_fuzzer parse_msgpack_fuzzer parse_ubjson_fuzzer parse_bjdata_fuzzer parse_bon8_fuzzer parse_json_view_fuzzer json_view_image_fuzzer
|
||||
fuzzers: $(FUZZERS)
|
||||
|
||||
parse_afl_fuzzer:
|
||||
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_json.cpp -o $@
|
||||
|
||||
parse_json_view_fuzzer:
|
||||
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_json_view.cpp -o $@
|
||||
|
||||
json_view_image_fuzzer:
|
||||
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-json_view_image.cpp -o $@
|
||||
|
||||
parse_bson_fuzzer:
|
||||
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_bson.cpp -o $@
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@ foreach(test default noversion)
|
||||
target_compile_definitions(abi_config_${test}_all_tags PRIVATE
|
||||
JSON_DIAGNOSTICS=1
|
||||
JSON_DIAGNOSTIC_POSITIONS=1
|
||||
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1)
|
||||
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1
|
||||
JSON_DISABLE_STREAM_BULK_SCAN=1
|
||||
JSON_DISABLE_UNLOCKED_FILE_IO=1
|
||||
JSON_VIEW_NO_SIMD
|
||||
JSON_VIEW_USE_SSSE3)
|
||||
target_link_libraries(abi_config_${test}_all_tags PRIVATE abi_compat_main)
|
||||
add_test(
|
||||
NAME test-abi_config_${test}_all_tags
|
||||
|
||||
@@ -52,6 +52,22 @@ TEST_CASE("default namespace")
|
||||
expected += "_ekmo";
|
||||
#endif
|
||||
|
||||
#if JSON_DISABLE_STREAM_BULK_SCAN
|
||||
expected += "_dsbs";
|
||||
#endif
|
||||
|
||||
#if JSON_DISABLE_UNLOCKED_FILE_IO
|
||||
expected += "_dufio";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
expected += "_vns";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_USE_SSSE3
|
||||
expected += "_vssse3";
|
||||
#endif
|
||||
|
||||
expected += "_v" STRINGIZE(NLOHMANN_JSON_VERSION_MAJOR);
|
||||
expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_MINOR);
|
||||
expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_PATCH) "::basic_json";
|
||||
|
||||
@@ -53,6 +53,22 @@ TEST_CASE("default namespace without version component")
|
||||
expected += "_ekmo";
|
||||
#endif
|
||||
|
||||
#if JSON_DISABLE_STREAM_BULK_SCAN
|
||||
expected += "_dsbs";
|
||||
#endif
|
||||
|
||||
#if JSON_DISABLE_UNLOCKED_FILE_IO
|
||||
expected += "_dufio";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
expected += "_vns";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_USE_SSSE3
|
||||
expected += "_vssse3";
|
||||
#endif
|
||||
|
||||
expected += "::basic_json";
|
||||
|
||||
// fallback for Clang
|
||||
|
||||
@@ -39,3 +39,10 @@ target_compile_features(json_benchmarks PRIVATE cxx_std_11)
|
||||
target_link_libraries(json_benchmarks benchmark ${CMAKE_THREAD_LIBS_INIT})
|
||||
add_dependencies(json_benchmarks download_test_data)
|
||||
target_include_directories(json_benchmarks PRIVATE ${JSON_BENCHMARK_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/include)
|
||||
|
||||
# the json_document benchmarks, unless the header to benchmark predates json_view.hpp
|
||||
if(EXISTS "${JSON_BENCHMARK_INCLUDE_DIR}/nlohmann/json_view.hpp")
|
||||
target_sources(json_benchmarks PRIVATE src/benchmarks_view.cpp)
|
||||
else()
|
||||
message(STATUS "${JSON_BENCHMARK_INCLUDE_DIR} has no nlohmann/json_view.hpp: skipping the json_document benchmarks")
|
||||
endif()
|
||||
@@ -9,6 +9,7 @@ Micro-benchmarks for parsing, serialization and the binary formats, written with
|
||||
| benchmark | what it does |
|
||||
|---|---|
|
||||
| `ParseFile`, `ParseString` | parse JSON from a file stream or a string |
|
||||
| `Accept` | validate JSON from a string (`json::accept`) |
|
||||
| `ParseIndented` | parse the large files re-indented by 4 spaces, for the lexer's whitespace handling |
|
||||
| `Dump` | serialize, compact (`-`) and indented (`4`) |
|
||||
| `ToCbor`, `BinaryToCbor` | write CBOR; `BinaryToCbor` writes binary values of growing size |
|
||||
@@ -16,6 +17,11 @@ Micro-benchmarks for parsing, serialization and the binary formats, written with
|
||||
| `FromBinaryBuffer`, `FromBinaryFile` | read CBOR, MessagePack, UBJSON, BJData and BSON from a buffer or a `FILE*` |
|
||||
| `FromBinaryShape` | read deeply nested, container-heavy and scalar-heavy documents in every binary format |
|
||||
| `FromCborChunkedString` | read CBOR strings split into indefinite-length chunks |
|
||||
| `ViewParse`, `ViewRead` | parse with [`json_document`](https://json.nlohmann.me/features/json_view/) into a new document, or into one that is reused; compare with `ParseString` |
|
||||
| `ViewParseIndented` | as `ParseIndented`, with a reused `json_document` |
|
||||
| `ViewAccept` | validate with `json_document::accept`; compare with `Accept` |
|
||||
| `ViewMaterialize` | convert a parsed `json_document` into a `json` value |
|
||||
| `ViewDump` | serialize a parsed `json_document`; compare with `Dump` |
|
||||
|
||||
The input files are those of [nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark) (`canada`,
|
||||
`citm_catalog`, `twitter`), a large `jeopardy` file, and number-heavy files (`floats`, `signed_ints`, ...).
|
||||
@@ -117,7 +123,9 @@ Both builds use the same Google Benchmark, the one `CMakeLists.txt` names, so on
|
||||
reason, do not compare against result files from an earlier checkout: a different Google Benchmark, compiler or
|
||||
machine state can shift the numbers as much as the change under test. Rerun the baseline instead.
|
||||
|
||||
The header you compare with must support everything the benchmarks use. The current benchmarks build against 3.12.0.
|
||||
The header you compare with must support everything the benchmarks use. The current benchmarks build against 3.12.0;
|
||||
the `View*` benchmarks (in `src/benchmarks_view.cpp`) are only built if the directory also holds
|
||||
`nlohmann/json_view.hpp`.
|
||||
Only benchmarks present in both result files are compared, so for older releases, either filter the benchmarks or
|
||||
build that release's own `tests/benchmarks` against its own header.
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
#include <fstream>
|
||||
#include <numeric>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <test_data.hpp>
|
||||
|
||||
@@ -51,6 +53,65 @@ BENCHMARK_CAPTURE(ParseFile, signed_ints, TEST_DATA_DIRECTORY "/regression
|
||||
BENCHMARK_CAPTURE(ParseFile, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json");
|
||||
BENCHMARK_CAPTURE(ParseFile, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// parse JSON from a FILE*
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void ParseFILE(benchmark::State& state, const char* filename)
|
||||
{
|
||||
while (state.KeepRunning())
|
||||
{
|
||||
state.PauseTiming();
|
||||
std::FILE* f = std::fopen(filename, "rb");
|
||||
auto* j = new json();
|
||||
state.ResumeTiming();
|
||||
|
||||
*j = json::parse(f);
|
||||
|
||||
state.PauseTiming();
|
||||
std::fclose(f);
|
||||
delete j;
|
||||
state.ResumeTiming();
|
||||
}
|
||||
|
||||
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
||||
state.SetBytesProcessed(state.iterations() * file.tellg());
|
||||
}
|
||||
BENCHMARK_CAPTURE(ParseFILE, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(ParseFILE, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(ParseFILE, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// read many small values from one stream (NDJSON)
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void ParseStreamValues(benchmark::State& state, bool numbers)
|
||||
{
|
||||
std::string input;
|
||||
for (int i = 0; i < 10000; ++i)
|
||||
{
|
||||
input += numbers ? std::to_string(i * 7919) + " "
|
||||
: R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","active":true,"score":)" + std::to_string(i * 0.25) + "}\n";
|
||||
}
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
std::istringstream is(input);
|
||||
json j;
|
||||
std::size_t count = 0;
|
||||
while (!(is >> std::ws).eof())
|
||||
{
|
||||
is >> j;
|
||||
++count;
|
||||
}
|
||||
benchmark::DoNotOptimize(count);
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(static_cast<int64_t>(state.iterations() * input.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(ParseStreamValues, ndjson, false);
|
||||
BENCHMARK_CAPTURE(ParseStreamValues, numbers, true);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// parse JSON from string
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
@@ -84,6 +145,31 @@ BENCHMARK_CAPTURE(ParseString, signed_ints, TEST_DATA_DIRECTORY "/regressi
|
||||
BENCHMARK_CAPTURE(ParseString, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json");
|
||||
BENCHMARK_CAPTURE(ParseString, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// validate JSON from string
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void Accept(benchmark::State& state, const char* filename)
|
||||
{
|
||||
std::ifstream f(filename);
|
||||
std::string str((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
|
||||
while (state.KeepRunning())
|
||||
{
|
||||
benchmark::DoNotOptimize(json::accept(str));
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * str.size());
|
||||
}
|
||||
BENCHMARK_CAPTURE(Accept, jeopardy, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json");
|
||||
BENCHMARK_CAPTURE(Accept, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Accept, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Accept, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
BENCHMARK_CAPTURE(Accept, floats, TEST_DATA_DIRECTORY "/regression/floats.json");
|
||||
BENCHMARK_CAPTURE(Accept, signed_ints, TEST_DATA_DIRECTORY "/regression/signed_ints.json");
|
||||
BENCHMARK_CAPTURE(Accept, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json");
|
||||
BENCHMARK_CAPTURE(Accept, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// parse pretty-printed JSON from string
|
||||
//
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Benchmarks of json_document (nlohmann/json_view.hpp). They use the inputs of
|
||||
// ParseString and ParseIndented in benchmarks.cpp, so that each ViewParse row
|
||||
// can be read against the json::parse row of the same file.
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <nlohmann/json_view.hpp>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <test_data.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using json_document = nlohmann::json_document;
|
||||
|
||||
static std::string read_file(const char* filename)
|
||||
{
|
||||
std::ifstream f(filename, std::ios::binary);
|
||||
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
#define JSON_VIEW_BENCHMARK_FILES(fn) \
|
||||
BENCHMARK_CAPTURE(fn, jeopardy, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json"); \
|
||||
BENCHMARK_CAPTURE(fn, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); \
|
||||
BENCHMARK_CAPTURE(fn, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); \
|
||||
BENCHMARK_CAPTURE(fn, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); \
|
||||
BENCHMARK_CAPTURE(fn, floats, TEST_DATA_DIRECTORY "/regression/floats.json"); \
|
||||
BENCHMARK_CAPTURE(fn, signed_ints, TEST_DATA_DIRECTORY "/regression/signed_ints.json"); \
|
||||
BENCHMARK_CAPTURE(fn, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json"); \
|
||||
BENCHMARK_CAPTURE(fn, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json")
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// parse into a new document (compare with ParseString)
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void ViewParse(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::string str = read_file(filename);
|
||||
|
||||
while (state.KeepRunning())
|
||||
{
|
||||
state.PauseTiming();
|
||||
auto* d = new json_document();
|
||||
state.ResumeTiming();
|
||||
|
||||
*d = json_document::parse(str);
|
||||
|
||||
state.PauseTiming();
|
||||
delete d;
|
||||
state.ResumeTiming();
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * str.size());
|
||||
}
|
||||
JSON_VIEW_BENCHMARK_FILES(ViewParse);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// parse into a document that is reused (its memory stays allocated)
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void ViewRead(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::string str = read_file(filename);
|
||||
json_document d;
|
||||
|
||||
while (state.KeepRunning())
|
||||
{
|
||||
d.read(str);
|
||||
benchmark::DoNotOptimize(d);
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * str.size());
|
||||
}
|
||||
JSON_VIEW_BENCHMARK_FILES(ViewRead);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// parse pretty-printed JSON (compare with ParseIndented)
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void ViewParseIndented(benchmark::State& state, const char* filename, int indent)
|
||||
{
|
||||
const std::string indented = json::parse(read_file(filename)).dump(indent);
|
||||
json_document d;
|
||||
|
||||
while (state.KeepRunning())
|
||||
{
|
||||
d.read(indented);
|
||||
benchmark::DoNotOptimize(d);
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * indented.size());
|
||||
}
|
||||
BENCHMARK_CAPTURE(ViewParseIndented, jeopardy / 4, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewParseIndented, canada / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewParseIndented, citm_catalog / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewParseIndented, twitter / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", 4);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// validate only (compare with Accept)
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void ViewAccept(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::string str = read_file(filename);
|
||||
|
||||
while (state.KeepRunning())
|
||||
{
|
||||
benchmark::DoNotOptimize(json_document::accept(str));
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * str.size());
|
||||
}
|
||||
JSON_VIEW_BENCHMARK_FILES(ViewAccept);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// convert a parsed document into a json value
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void ViewMaterialize(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::string str = read_file(filename);
|
||||
const json_document d = json_document::parse(str);
|
||||
|
||||
while (state.KeepRunning())
|
||||
{
|
||||
state.PauseTiming();
|
||||
auto* j = new json();
|
||||
state.ResumeTiming();
|
||||
|
||||
*j = d.root().materialize();
|
||||
|
||||
state.PauseTiming();
|
||||
delete j;
|
||||
state.ResumeTiming();
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * str.size());
|
||||
}
|
||||
JSON_VIEW_BENCHMARK_FILES(ViewMaterialize);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// serialize a parsed document (compare with Dump)
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void ViewDump(benchmark::State& state, const char* filename, int indent)
|
||||
{
|
||||
const std::string str = read_file(filename);
|
||||
const json_document d = json_document::parse(str);
|
||||
|
||||
while (state.KeepRunning())
|
||||
{
|
||||
std::string output = d.root().dump(indent);
|
||||
benchmark::DoNotOptimize(output);
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * d.root().dump(indent).size());
|
||||
}
|
||||
BENCHMARK_CAPTURE(ViewDump, jeopardy / -, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", -1);
|
||||
BENCHMARK_CAPTURE(ViewDump, jeopardy / 4, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewDump, canada / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", -1);
|
||||
BENCHMARK_CAPTURE(ViewDump, canada / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewDump, citm_catalog / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", -1);
|
||||
BENCHMARK_CAPTURE(ViewDump, citm_catalog / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewDump, twitter / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", -1);
|
||||
BENCHMARK_CAPTURE(ViewDump, twitter / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewDump, floats / -, TEST_DATA_DIRECTORY "/regression/floats.json", -1);
|
||||
BENCHMARK_CAPTURE(ViewDump, floats / 4, TEST_DATA_DIRECTORY "/regression/floats.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewDump, signed_ints / -, TEST_DATA_DIRECTORY "/regression/signed_ints.json", -1);
|
||||
BENCHMARK_CAPTURE(ViewDump, signed_ints / 4, TEST_DATA_DIRECTORY "/regression/signed_ints.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewDump, unsigned_ints / -, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json", -1);
|
||||
BENCHMARK_CAPTURE(ViewDump, unsigned_ints / 4, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json", 4);
|
||||
BENCHMARK_CAPTURE(ViewDump, small_signed_ints / -, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json", -1);
|
||||
BENCHMARK_CAPTURE(ViewDump, small_signed_ints / 4, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json", 4);
|
||||
@@ -3,6 +3,21 @@
|
||||
Each parser of the library (JSON, BJData, BON8, BSON, CBOR, MessagePack, and UBJSON) can be fuzz tested. Currently,
|
||||
[libFuzzer](https://llvm.org/docs/LibFuzzer.html) and [afl++](https://github.com/AFLplusplus/AFLplusplus) are supported.
|
||||
|
||||
Additionally, `parse_json_view_fuzzer` (`tests/src/fuzzer-parse_json_view.cpp`) cross-checks `json_document`/`json_view`
|
||||
(the zero-copy, read-only view declared in `json_view.hpp`) against `basic_json` on the same JSON text: it asserts that
|
||||
`json_document::accept` agrees with `json::accept`, that an accepted input materializes to the same value `json::parse`
|
||||
produces, and that a rejected input makes both parsers throw with an identical `what()`. It checks this for a
|
||||
`std::string` input (borrowed, with a NUL after the last byte) and for an exact-size `std::vector<std::uint8_t>`
|
||||
(borrowed, with nothing after the last byte), and for the `ignore_comments` and `ignore_trailing_commas` options, which
|
||||
are taken from the low bits of the first input byte (the byte stays part of the text). It takes plain JSON text, so it
|
||||
reuses the `corpus_json` corpus rather than a format of its own.
|
||||
|
||||
`json_view_image_fuzzer` (`tests/src/fuzzer-json_view_image.cpp`) tests the images of `json_document` (`save()` and
|
||||
`load()`). It uses each input twice: as an image, which `load()` must either reject with `parse_error.116` or read
|
||||
safely (with `image_check::full`, the document must also serialize to the JSON it reads as), and as a JSON text, whose
|
||||
image must load and serialize to the same text. A corpus of images can be made from JSON files with a small program
|
||||
that calls `json_document::parse(text).save()`; plain JSON files work as well.
|
||||
|
||||
## What the fuzzers check
|
||||
|
||||
Each fuzzer driver (`tests/src/fuzzer-parse_*.cpp`) parses its input twice: once with `allow_exceptions = false` and
|
||||
|
||||
@@ -52,8 +52,13 @@ int main()
|
||||
std::ostringstream os;
|
||||
os << j << oj << lit;
|
||||
|
||||
// json_document / json_view (json_view.hpp): zero-copy parse of a literal
|
||||
const nlohmann::json_document doc = nlohmann::json_document::parse(R"({"a": 1, "list": [1, 2, 3]})");
|
||||
const std::size_t doc_size = doc.root().size();
|
||||
|
||||
// use every result so the references cannot be optimized away
|
||||
return (a == 1 && last == 3 && b == 2 && lit.size() == 3
|
||||
&& m.size() == 1 && !dumped.empty() && !os.str().empty())
|
||||
&& m.size() == 1 && !dumped.empty() && !os.str().empty()
|
||||
&& doc_size == 2)
|
||||
? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/*
|
||||
This file implements a test of json_document images suitable for fuzz
|
||||
testing. The input is used twice:
|
||||
|
||||
- as an image: json_document::load() with image_check::full must either throw
|
||||
a parse_error or yield a document that serializes to the JSON text it reads
|
||||
as; with image_check::bounds, reading and serializing must be safe (checked
|
||||
by the sanitizers), and serializing may only throw type_error.316
|
||||
- as a JSON text: if json_document::parse() accepts it, the image of the
|
||||
document must load (with every check) and serialize to the same text
|
||||
|
||||
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
|
||||
drivers.
|
||||
*/
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
// the checks below are assertions; NDEBUG would compile them away
|
||||
#ifdef NDEBUG
|
||||
#error "the fuzzer drivers must be built without NDEBUG"
|
||||
#endif
|
||||
|
||||
using json = nlohmann::json;
|
||||
using json_document = nlohmann::json_document;
|
||||
using image_check = json_document::image_check;
|
||||
|
||||
// see http://llvm.org/docs/LibFuzzer.html
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
|
||||
{
|
||||
// the input as an image
|
||||
for (const image_check check :
|
||||
{
|
||||
image_check::full, image_check::bounds
|
||||
})
|
||||
{
|
||||
json_document d;
|
||||
try
|
||||
{
|
||||
d = json_document::load(data, size, check);
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
assert(e.id == 116);
|
||||
continue;
|
||||
}
|
||||
std::string dumped;
|
||||
try
|
||||
{
|
||||
dumped = d.root().dump();
|
||||
}
|
||||
catch (const json::type_error& e)
|
||||
{
|
||||
// invalid UTF-8 can only pass the bounds check
|
||||
assert(check == image_check::bounds && e.id == 316);
|
||||
continue;
|
||||
}
|
||||
const json j = d.root().materialize();
|
||||
if (check == image_check::full)
|
||||
{
|
||||
assert(json::parse(dumped) == j);
|
||||
// an image of the loaded document is the input
|
||||
assert(d.save() == std::vector<std::uint8_t>(data, data + size));
|
||||
}
|
||||
}
|
||||
|
||||
// the input as a JSON text
|
||||
const std::string text(reinterpret_cast<const char*>(data), size); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const json_document parsed = json_document::parse(text, false);
|
||||
if (!parsed.is_discarded())
|
||||
{
|
||||
const std::vector<std::uint8_t> image = parsed.save();
|
||||
for (const image_check check :
|
||||
{
|
||||
image_check::full, image_check::bounds, image_check::none
|
||||
})
|
||||
{
|
||||
const json_document loaded = json_document::load(image, check);
|
||||
assert(loaded.root().dump() == parsed.root().dump());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/*
|
||||
This file implements a parser test suitable for fuzz testing. It checks that
|
||||
json_document (the zero-copy, read-only view of a parsed JSON text declared in
|
||||
json_view.hpp) agrees with basic_json on every input, for the parse options
|
||||
selected by the low bits of the first input byte (bit 0: ignore_comments, bit 1:
|
||||
ignore_trailing_commas; the byte stays part of the text):
|
||||
|
||||
- json_document::accept(data) must equal json::accept(data)
|
||||
- if the input is accepted, json_document::parse(data).root().materialize()
|
||||
must equal json::parse(data)
|
||||
- if the input is rejected, json_document::parse(data) (with exceptions
|
||||
enabled) must throw a json::parse_error or json::out_of_range whose what()
|
||||
is identical to the one json::parse(data) throws
|
||||
|
||||
This is checked for two kinds of input: a std::string, which the document
|
||||
borrows and which ends in the NUL the parser uses as sentinel, and an
|
||||
exact-size byte vector, which has no NUL after its last byte and takes the
|
||||
parser's bounds-checked path (AddressSanitizer reports any read past the end).
|
||||
|
||||
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
|
||||
drivers.
|
||||
*/
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
// the checks below are assertions; NDEBUG would compile them away
|
||||
#ifdef NDEBUG
|
||||
#error "the fuzzer drivers must be built without NDEBUG"
|
||||
#endif
|
||||
|
||||
using json = nlohmann::json;
|
||||
using json_document = nlohmann::json_document;
|
||||
|
||||
namespace
|
||||
{
|
||||
// what json::parse does with a text: the value, or the message of the exception
|
||||
struct reference_result
|
||||
{
|
||||
bool accepted = false;
|
||||
json value{};
|
||||
std::string what{}; // NOLINT(readability-redundant-member-init)
|
||||
};
|
||||
|
||||
reference_result parse_reference(const std::uint8_t* data, std::size_t size, bool comments, bool trailing_commas)
|
||||
{
|
||||
reference_result r;
|
||||
r.accepted = json::accept(data, data + size, comments, trailing_commas);
|
||||
bool json_threw = false;
|
||||
try
|
||||
{
|
||||
r.value = json::parse(data, data + size, nullptr, true, comments, trailing_commas);
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
r.what = e.what();
|
||||
json_threw = true;
|
||||
}
|
||||
catch (const json::out_of_range& e)
|
||||
{
|
||||
r.what = e.what();
|
||||
json_threw = true;
|
||||
}
|
||||
// json::accept and json::parse must agree
|
||||
assert(json_threw == !r.accepted);
|
||||
static_cast<void>(json_threw);
|
||||
return r;
|
||||
}
|
||||
|
||||
// json_document must agree with the reference for this input (a container
|
||||
// that json_document::parse borrows)
|
||||
template<typename Input>
|
||||
void check_input(const Input& input, const reference_result& expected, bool comments, bool trailing_commas)
|
||||
{
|
||||
// json_document::accept must agree with json::accept on every input
|
||||
const bool accepted_by_view = json_document::accept(input, comments, trailing_commas);
|
||||
assert(expected.accepted == accepted_by_view);
|
||||
static_cast<void>(accepted_by_view);
|
||||
|
||||
if (expected.accepted)
|
||||
{
|
||||
// both parsers must agree on the resulting value
|
||||
json_document const doc = json_document::parse(input, true, comments, trailing_commas);
|
||||
assert(!doc.is_discarded());
|
||||
json const j2 = doc.root().materialize();
|
||||
assert(expected.value == j2);
|
||||
static_cast<void>(j2);
|
||||
|
||||
// (without exceptions, the same document)
|
||||
json_document const quiet = json_document::parse(input, false, comments, trailing_commas);
|
||||
assert(!quiet.is_discarded());
|
||||
assert(quiet.node_count() == doc.node_count());
|
||||
}
|
||||
else
|
||||
{
|
||||
// both parsers must reject the input the same way when exceptions are used
|
||||
bool view_threw = false;
|
||||
try
|
||||
{
|
||||
static_cast<void>(json_document::parse(input, true, comments, trailing_commas));
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
assert(e.what() == expected.what);
|
||||
view_threw = true;
|
||||
}
|
||||
catch (const json::out_of_range& e)
|
||||
{
|
||||
assert(e.what() == expected.what);
|
||||
view_threw = true;
|
||||
}
|
||||
assert(view_threw);
|
||||
static_cast<void>(view_threw);
|
||||
|
||||
// and without exceptions, the document is discarded
|
||||
json_document const quiet = json_document::parse(input, false, comments, trailing_commas);
|
||||
assert(quiet.is_discarded());
|
||||
static_cast<void>(quiet);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// see http://llvm.org/docs/LibFuzzer.html
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
|
||||
{
|
||||
// the parse options are taken from the low bits of the first byte
|
||||
const bool comments = size > 0 && (data[0] & 1U) != 0;
|
||||
const bool trailing_commas = size > 0 && (data[0] & 2U) != 0;
|
||||
|
||||
const reference_result expected = parse_reference(data, size, comments, trailing_commas);
|
||||
|
||||
// a std::string: borrowed, with the NUL of std::string as sentinel
|
||||
const std::string input(reinterpret_cast<const char*>(data), size); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
check_input(input, expected, comments, trailing_commas);
|
||||
|
||||
// an exact-size byte vector: borrowed, with nothing after its last byte
|
||||
const std::vector<std::uint8_t> exact(data, data + size);
|
||||
check_input(exact, expected, comments, trailing_commas);
|
||||
|
||||
// return 0 - non-zero return values are reserved for future use
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
// helpers shared by the unit-json_view*.cpp files, which are split to keep every
|
||||
// object file small enough for the MinGW linker (it fails to link objects with
|
||||
// more than 32767 sections)
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#include <algorithm> // any_of
|
||||
#include <functional> // function
|
||||
#include <random> // mt19937
|
||||
#include <string> // string
|
||||
|
||||
namespace json_view_test
|
||||
{
|
||||
// a small deterministic generator of documents
|
||||
struct generator
|
||||
{
|
||||
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
|
||||
int r(int n)
|
||||
{
|
||||
return static_cast<int>(rng() % static_cast<unsigned>(n));
|
||||
}
|
||||
|
||||
void str(std::string& o)
|
||||
{
|
||||
static const char* const pieces[] = {"a", "Z", " ", "\\n", "\\\"", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "long text beyond the first sixteen bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
o += '"';
|
||||
for (int n = r(5); n > 0; --n)
|
||||
{
|
||||
o += pieces[r(10)];
|
||||
}
|
||||
o += '"';
|
||||
}
|
||||
|
||||
void value(std::string& o, int depth)
|
||||
{
|
||||
static const char* const scalars[] = {"0", "-1", "123456789012", "18446744073709551615", "18446744073709551616", "-9223372036854775809", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
"1.5", "-2.25e-3", "1E2", "0.1", "true", "false", "null"
|
||||
};
|
||||
const int k = depth > 5 ? 2 + r(4) : r(6);
|
||||
if (k < 2)
|
||||
{
|
||||
const bool object = k == 0;
|
||||
o += object ? '{' : '[';
|
||||
for (int i = r(5); i > 0; --i)
|
||||
{
|
||||
if (object)
|
||||
{
|
||||
str(o);
|
||||
o += r(2) == 0 ? ":" : " : ";
|
||||
}
|
||||
value(o, depth + 1);
|
||||
o += i > 1 ? ", " : "";
|
||||
}
|
||||
o += object ? '}' : ']';
|
||||
}
|
||||
else if (k < 4)
|
||||
{
|
||||
str(o);
|
||||
}
|
||||
else
|
||||
{
|
||||
o += scalars[r(13)];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// whether an object of the view repeats a key
|
||||
inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v)
|
||||
{
|
||||
if (v.is_object() && v.size() != v.materialize().size())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return std::any_of(v.begin(), v.end(), [](const nlohmann::ordered_json_view e)
|
||||
{
|
||||
return e.is_structured() && has_duplicate_keys(e);
|
||||
});
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// the exception a call throws, or "" if it throws none
|
||||
inline std::string exception_of_call(const std::function<void()>& f)
|
||||
{
|
||||
try
|
||||
{
|
||||
f();
|
||||
}
|
||||
catch (const nlohmann::json::exception& e)
|
||||
{
|
||||
return e.what();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
} // namespace json_view_test
|
||||
@@ -11,8 +11,10 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -35,6 +37,17 @@ std::vector<json> test_values()
|
||||
big_object[std::to_string(i)] = i;
|
||||
}
|
||||
|
||||
// large strings and binary values between small ones: a write too large
|
||||
// for the space the vector sink has ahead takes a different path than a
|
||||
// small one, and the small writes after it must still land behind it
|
||||
json large_payloads = json::array();
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
large_payloads.push_back(std::string(300, static_cast<char>('a' + (i % 26))));
|
||||
large_payloads.push_back(i);
|
||||
large_payloads.push_back(json::binary(std::vector<std::uint8_t>(static_cast<std::size_t>(100 * i), static_cast<std::uint8_t>(i))));
|
||||
}
|
||||
|
||||
return
|
||||
{
|
||||
json(nullptr), json(true), json(false),
|
||||
@@ -46,10 +59,23 @@ std::vector<json> test_values()
|
||||
json::array(), json::object(),
|
||||
json::array({1, 2, 3}), json({{"a", 1}, {"b", nullptr}}),
|
||||
json({{"nested", {{"deep", json::array({1, "two", 3.0, nullptr})}}}}),
|
||||
big_array, big_object
|
||||
big_array, big_object, large_payloads,
|
||||
json::binary(std::vector<std::uint8_t>(5000, 0x42))
|
||||
};
|
||||
}
|
||||
|
||||
// explicit char -> uint8_t conversion (the iterator-range constructor converts implicitly, which -fsanitize=integer flags)
|
||||
std::vector<std::uint8_t> to_bytes(const std::string& str)
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve(str.size());
|
||||
for (const char c : str)
|
||||
{
|
||||
result.push_back(static_cast<std::uint8_t>(c));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// BON8 has no integers above the int64 range, so to_bon8() rejects them
|
||||
bool bon8_representable(const json& j)
|
||||
{
|
||||
@@ -170,6 +196,258 @@ TEST_CASE("binary writer output sinks")
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// the CBOR head of an integer, encoded independently of binary_writer
|
||||
void reference_cbor_head(std::vector<std::uint8_t>& out, const std::uint8_t major_type, const std::uint64_t argument)
|
||||
{
|
||||
if (argument <= 23)
|
||||
{
|
||||
out.push_back(static_cast<std::uint8_t>(major_type + argument));
|
||||
return;
|
||||
}
|
||||
|
||||
int length = 8;
|
||||
std::uint8_t additional = 27;
|
||||
if (argument <= 0xFF)
|
||||
{
|
||||
length = 1;
|
||||
additional = 24;
|
||||
}
|
||||
else if (argument <= 0xFFFF)
|
||||
{
|
||||
length = 2;
|
||||
additional = 25;
|
||||
}
|
||||
else if (argument <= 0xFFFFFFFF)
|
||||
{
|
||||
length = 4;
|
||||
additional = 26;
|
||||
}
|
||||
|
||||
out.push_back(static_cast<std::uint8_t>(major_type + additional));
|
||||
for (int i = length - 1; i >= 0; --i)
|
||||
{
|
||||
out.push_back(static_cast<std::uint8_t>(argument >> (8 * i)));
|
||||
}
|
||||
}
|
||||
|
||||
// the CBOR encoding of an integer value
|
||||
void reference_cbor_integer(std::vector<std::uint8_t>& out, const json& j)
|
||||
{
|
||||
if (j.is_number_unsigned())
|
||||
{
|
||||
reference_cbor_head(out, 0x00, j.get<std::uint64_t>());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto n = j.get<std::int64_t>();
|
||||
if (n >= 0)
|
||||
{
|
||||
reference_cbor_head(out, 0x00, static_cast<std::uint64_t>(n));
|
||||
}
|
||||
else
|
||||
{
|
||||
reference_cbor_head(out, 0x20, static_cast<std::uint64_t>(-1 - n));
|
||||
}
|
||||
}
|
||||
|
||||
// the MessagePack encoding of an integer value, encoded independently of
|
||||
// binary_writer
|
||||
void reference_msgpack_integer(std::vector<std::uint8_t>& out, const json& j)
|
||||
{
|
||||
const auto put = [&out](const std::uint8_t marker, const std::uint64_t bits, const int length)
|
||||
{
|
||||
out.push_back(marker);
|
||||
for (int i = length - 1; i >= 0; --i)
|
||||
{
|
||||
out.push_back(static_cast<std::uint8_t>(bits >> (8 * i)));
|
||||
}
|
||||
};
|
||||
|
||||
if (j.is_number_unsigned() || j.get<std::int64_t>() >= 0)
|
||||
{
|
||||
const auto u = j.get<std::uint64_t>();
|
||||
if (u < 128)
|
||||
{
|
||||
out.push_back(static_cast<std::uint8_t>(u));
|
||||
}
|
||||
else if (u <= 0xFF)
|
||||
{
|
||||
put(0xCC, u, 1);
|
||||
}
|
||||
else if (u <= 0xFFFF)
|
||||
{
|
||||
put(0xCD, u, 2);
|
||||
}
|
||||
else if (u <= 0xFFFFFFFF)
|
||||
{
|
||||
put(0xCE, u, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
put(0xCF, u, 8);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto n = j.get<std::int64_t>();
|
||||
const auto bits = static_cast<std::uint64_t>(n);
|
||||
if (n >= -32)
|
||||
{
|
||||
out.push_back(static_cast<std::uint8_t>(bits));
|
||||
}
|
||||
else if (n >= -128)
|
||||
{
|
||||
put(0xD0, bits, 1);
|
||||
}
|
||||
else if (n >= -32768)
|
||||
{
|
||||
put(0xD1, bits, 2);
|
||||
}
|
||||
else if (n >= -2147483648LL)
|
||||
{
|
||||
put(0xD2, bits, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
put(0xD3, bits, 8);
|
||||
}
|
||||
}
|
||||
|
||||
// integers at every boundary of the CBOR and MessagePack sizes, signed and
|
||||
// unsigned
|
||||
std::vector<json> boundary_integers()
|
||||
{
|
||||
std::vector<json> result;
|
||||
const std::array<std::uint64_t, 38> boundaries =
|
||||
{
|
||||
{
|
||||
0, 1, 22, 23, 24, 25, 31, 32, 33, 126, 127, 128, 129, 254, 255, 256, 257,
|
||||
32766, 32767, 32768, 32769, 65534, 65535, 65536, 65537,
|
||||
2147483646ULL, 2147483647ULL, 2147483648ULL, 2147483649ULL,
|
||||
4294967294ULL, 4294967295ULL, 4294967296ULL, 4294967297ULL,
|
||||
9223372036854775806ULL, 9223372036854775807ULL, 9223372036854775808ULL,
|
||||
18446744073709551614ULL, 18446744073709551615ULL
|
||||
}
|
||||
};
|
||||
for (const std::uint64_t u : boundaries)
|
||||
{
|
||||
result.emplace_back(u);
|
||||
if (u <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
|
||||
{
|
||||
const auto n = static_cast<std::int64_t>(u);
|
||||
result.emplace_back(n);
|
||||
result.emplace_back(-n);
|
||||
result.emplace_back(-n - 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Arrays of integers are written in runs straight into space reserved from the
|
||||
// sink, with branch-free encodings; check them against independent encoders for
|
||||
// runs shorter and longer than one reservation, broken up by other values, and
|
||||
// through each sink.
|
||||
TEST_CASE("CBOR and MessagePack integer arrays")
|
||||
{
|
||||
const std::vector<json> integers = boundary_integers();
|
||||
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{27}, std::size_t{28}, std::size_t{29}, std::size_t{57}, std::size_t{1000}
|
||||
})
|
||||
{
|
||||
for (const std::size_t break_every :
|
||||
{
|
||||
std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{28}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
CAPTURE(break_every)
|
||||
|
||||
json array = json::array();
|
||||
std::vector<std::uint8_t> expected_cbor;
|
||||
std::vector<std::uint8_t> expected_msgpack;
|
||||
for (std::size_t i = 0; i < length; ++i)
|
||||
{
|
||||
if (break_every != 0 && i % (break_every + 1) == break_every)
|
||||
{
|
||||
array.push_back("x");
|
||||
expected_cbor.push_back(0x61);
|
||||
expected_cbor.push_back('x');
|
||||
expected_msgpack.push_back(0xA1);
|
||||
expected_msgpack.push_back('x');
|
||||
}
|
||||
else
|
||||
{
|
||||
const json& value = integers[(i * 7) % integers.size()];
|
||||
array.push_back(value);
|
||||
reference_cbor_integer(expected_cbor, value);
|
||||
reference_msgpack_integer(expected_msgpack, value);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> head;
|
||||
reference_cbor_head(head, 0x80, length);
|
||||
expected_cbor.insert(expected_cbor.begin(), head.begin(), head.end());
|
||||
|
||||
head.clear();
|
||||
if (length <= 15)
|
||||
{
|
||||
head.push_back(static_cast<std::uint8_t>(0x90 + length));
|
||||
}
|
||||
else
|
||||
{
|
||||
head = {0xDC, static_cast<std::uint8_t>(length >> 8), static_cast<std::uint8_t>(length)};
|
||||
}
|
||||
expected_msgpack.insert(expected_msgpack.begin(), head.begin(), head.end());
|
||||
|
||||
CHECK(json::to_cbor(array) == expected_cbor);
|
||||
CHECK(json::to_msgpack(array) == expected_msgpack);
|
||||
|
||||
std::vector<std::uint8_t> through_adapter = {0xAA};
|
||||
json::to_cbor(array, through_adapter);
|
||||
CHECK(std::vector<std::uint8_t>(through_adapter.begin() + 1, through_adapter.end()) == expected_cbor);
|
||||
through_adapter = {0xAA};
|
||||
json::to_msgpack(array, through_adapter);
|
||||
CHECK(std::vector<std::uint8_t>(through_adapter.begin() + 1, through_adapter.end()) == expected_msgpack);
|
||||
|
||||
std::ostringstream stream;
|
||||
json::to_cbor(array, stream);
|
||||
std::string streamed = stream.str();
|
||||
CHECK(to_bytes(streamed) == expected_cbor);
|
||||
stream.str("");
|
||||
json::to_msgpack(array, stream);
|
||||
streamed = stream.str();
|
||||
CHECK(to_bytes(streamed) == expected_msgpack);
|
||||
|
||||
CHECK(json::from_cbor(expected_cbor) == array);
|
||||
CHECK(json::from_msgpack(expected_msgpack) == array);
|
||||
|
||||
// the same array as object members
|
||||
const json object = {{"a", array}, {"b", array}};
|
||||
CHECK(json::from_cbor(json::to_cbor(object)) == object);
|
||||
CHECK(json::from_msgpack(json::to_msgpack(object)) == object);
|
||||
}
|
||||
}
|
||||
|
||||
// single values, outside an array
|
||||
for (const auto& value : integers)
|
||||
{
|
||||
CAPTURE(value)
|
||||
std::vector<std::uint8_t> expected;
|
||||
reference_cbor_integer(expected, value);
|
||||
CHECK(json::to_cbor(value) == expected);
|
||||
expected.clear();
|
||||
reference_msgpack_integer(expected, value);
|
||||
CHECK(json::to_msgpack(value) == expected);
|
||||
}
|
||||
}
|
||||
|
||||
// binary_reserve_hint() is documented as a *lower* bound on the serialized size,
|
||||
// so that reserving it up front can never leave the returned vector holding
|
||||
// capacity beyond what the value actually needs.
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
@@ -164,3 +166,47 @@ TEST_CASE("array type without at()")
|
||||
CHECK(j.at(no_at_json::json_pointer("/1")) == 2);
|
||||
CHECK_THROWS_AS(j.at(no_at_json::json_pointer("/3")), no_at_json::out_of_range);
|
||||
}
|
||||
|
||||
TEST_CASE("regression test #5476 - array type without reserve()")
|
||||
{
|
||||
// the capacity reserved for definite-length arrays must not require the
|
||||
// array type to have a reserve() member function
|
||||
SECTION("std::deque")
|
||||
{
|
||||
const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})");
|
||||
CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})");
|
||||
|
||||
// the binary formats pass a definite length to start_array()
|
||||
CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j);
|
||||
CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j);
|
||||
CHECK(deque_json::from_bon8(deque_json::to_bon8(j)) == j);
|
||||
|
||||
// parse() instantiates the callback parser as well, which reserves too
|
||||
const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) noexcept
|
||||
{
|
||||
return true;
|
||||
});
|
||||
CHECK(with_callback == deque_json({1, 2, 3}));
|
||||
}
|
||||
|
||||
SECTION("std::vector still reserves")
|
||||
{
|
||||
json array = json::array();
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
array.push_back(i);
|
||||
}
|
||||
|
||||
const auto j = json::from_cbor(json::to_cbor(array));
|
||||
CHECK(j == array);
|
||||
CHECK(j.get_ref<const json::array_t&>().capacity() >= 100);
|
||||
}
|
||||
|
||||
SECTION("the reservation stays capped")
|
||||
{
|
||||
// CBOR array announcing 2^32-1 elements, but truncated right after the
|
||||
// header: the input must be rejected without reserving that capacity
|
||||
const std::vector<std::uint8_t> truncated = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
CHECK(json::from_cbor(truncated, true, false).is_discarded());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
#include <compare>
|
||||
#endif
|
||||
|
||||
namespace custom_number_types
|
||||
{
|
||||
|
||||
// a signed integer of class type: std::is_signed is only true for arithmetic
|
||||
// types, so the library has to take the signedness from std::numeric_limits,
|
||||
// as it does for 128-bit and multiprecision integer classes such as
|
||||
// absl::int128 or boost::multiprecision::cpp_int
|
||||
class class_int
|
||||
{
|
||||
public:
|
||||
// trivial, like the 128-bit integer classes: the value is stored in a union
|
||||
class_int() = default;
|
||||
|
||||
// implicit from built-in integers and explicit from floats, like absl::int128;
|
||||
// non-template overloads (as absl::int128 has) because clang 3.4 and 3.5
|
||||
// reject basic_json's defaulted data() for a number type with template
|
||||
// constructors
|
||||
class_int(int v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
|
||||
class_int(unsigned v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
|
||||
class_int(long v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
|
||||
class_int(unsigned long v) : value(static_cast<std::int64_t>(v)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
|
||||
class_int(long long v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
|
||||
class_int(unsigned long long v) : value(static_cast<std::int64_t>(v)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
|
||||
explicit class_int(double v) : value(static_cast<std::int64_t>(v)) {}
|
||||
|
||||
template<typename T, typename std::enable_if<std::is_arithmetic<T>::value, int>::type = 0>
|
||||
explicit operator T() const
|
||||
{
|
||||
return static_cast<T>(value);
|
||||
}
|
||||
|
||||
friend bool operator==(class_int lhs, class_int rhs)
|
||||
{
|
||||
return lhs.value == rhs.value;
|
||||
}
|
||||
friend bool operator!=(class_int lhs, class_int rhs)
|
||||
{
|
||||
return lhs.value != rhs.value;
|
||||
}
|
||||
friend bool operator<(class_int lhs, class_int rhs)
|
||||
{
|
||||
return lhs.value < rhs.value;
|
||||
}
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
friend std::strong_ordering operator<=>(class_int lhs, class_int rhs) // *NOPAD*
|
||||
{
|
||||
return lhs.value <=> rhs.value; // *NOPAD*
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
std::int64_t value;
|
||||
};
|
||||
|
||||
} // namespace custom_number_types
|
||||
|
||||
using custom_number_types::class_int;
|
||||
|
||||
namespace std
|
||||
{
|
||||
// only the members the library uses
|
||||
template<>
|
||||
class numeric_limits<class_int>
|
||||
{
|
||||
public:
|
||||
static constexpr bool is_signed = true;
|
||||
static constexpr int digits = std::numeric_limits<std::int64_t>::digits;
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using class_int_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, class_int, std::uint64_t, double>;
|
||||
|
||||
class_int_json make_class_int(std::int64_t v)
|
||||
{
|
||||
class_int_json j(class_int_json::value_t::number_integer);
|
||||
j.get_ref<class_int&>() = class_int(v);
|
||||
return j;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// the serializer cannot print an integer of class type, so doctest must not
|
||||
// try when an assertion fails
|
||||
namespace doctest
|
||||
{
|
||||
template<>
|
||||
struct StringMaker<class_int_json>
|
||||
{
|
||||
static String convert(const class_int_json& j)
|
||||
{
|
||||
return j.type_name();
|
||||
}
|
||||
};
|
||||
} // namespace doctest
|
||||
|
||||
// __int128 as number type needs the std::numeric_limits (for the range checks)
|
||||
// and std::is_integral (for the serializer) specializations, which libc++
|
||||
// always provides and libstdc++ only outside strict ISO modes; the MSVC
|
||||
// standard library has none
|
||||
#if defined(__SIZEOF_INT128__) && (defined(_LIBCPP_VERSION) || defined(__GLIBCXX_TYPE_INT_N_0))
|
||||
#define JSON_TEST_INT128_NUMBER_TYPES 1
|
||||
#else
|
||||
#define JSON_TEST_INT128_NUMBER_TYPES 0
|
||||
#endif
|
||||
|
||||
#if JSON_TEST_INT128_NUMBER_TYPES
|
||||
namespace
|
||||
{
|
||||
|
||||
// __extension__ keeps -Wpedantic from flagging the non-standard type
|
||||
__extension__ typedef __int128 int128; // NOLINT(modernize-use-using)
|
||||
__extension__ typedef unsigned __int128 uint128; // NOLINT(modernize-use-using)
|
||||
|
||||
static_assert(std::numeric_limits<int128>::digits == 127 && std::numeric_limits<uint128>::digits == 128 &&
|
||||
std::is_integral<int128>::value && std::is_integral<uint128>::value, // NOLINT(misc-redundant-expression)
|
||||
"__int128 is not fully supported by the standard library");
|
||||
|
||||
using wide_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, int128, uint128, double>;
|
||||
|
||||
wide_json make_int(int128 v)
|
||||
{
|
||||
wide_json j(wide_json::value_t::number_integer);
|
||||
j.get_ref<int128&>() = v;
|
||||
return j;
|
||||
}
|
||||
|
||||
wide_json make_uint(uint128 v)
|
||||
{
|
||||
wide_json j(wide_json::value_t::number_unsigned);
|
||||
j.get_ref<uint128&>() = v;
|
||||
return j;
|
||||
}
|
||||
|
||||
wide_json wrap(const wide_json& value)
|
||||
{
|
||||
wide_json o(wide_json::value_t::object);
|
||||
o["v"] = value;
|
||||
return o;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("custom number types: integers beyond 64 bits")
|
||||
{
|
||||
using out_of_range = wide_json::out_of_range;
|
||||
|
||||
const int128 two_64 = static_cast<int128>(1) << 64;
|
||||
const int128 two_100 = static_cast<int128>(1) << 100;
|
||||
const int128 max_int64 = (std::numeric_limits<std::int64_t>::max)();
|
||||
const int128 min_int64 = (std::numeric_limits<std::int64_t>::min)();
|
||||
const uint128 max_uint64 = (std::numeric_limits<std::uint64_t>::max)();
|
||||
|
||||
// before the range checks, these values were silently truncated, e.g.,
|
||||
// 2^100 was written as 0
|
||||
const wide_json int_big = make_int(two_100);
|
||||
const wide_json int_big_negative = make_int(-two_100);
|
||||
const wide_json uint_big = make_uint(static_cast<uint128>(two_100));
|
||||
std::vector<std::uint8_t> _;
|
||||
|
||||
SECTION("CBOR")
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit uint64", out_of_range&);
|
||||
// the extremes of the 128-bit types, whose digits are produced without
|
||||
// negating the minimum as a whole
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_int((std::numeric_limits<int128>::min)())), "[json.exception.out_of_range.407] integer number -170141183460469231731687303715884105728 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_int((std::numeric_limits<int128>::max)())), "[json.exception.out_of_range.407] integer number 170141183460469231731687303715884105727 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_uint((std::numeric_limits<uint128>::max)())), "[json.exception.out_of_range.407] integer number 340282366920938463463374607431768211455 cannot be represented by CBOR as it does not fit uint64", out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_cbor(make_int(two_64)), out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_cbor(make_int(-two_64 - 1)), out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_cbor(make_uint(static_cast<uint128>(max_uint64) + 1)), out_of_range&);
|
||||
|
||||
// CBOR's integers cover [-2^64, 2^64-1]
|
||||
CHECK(wide_json::to_cbor(make_int(two_64 - 1)) == std::vector<std::uint8_t>({0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
|
||||
CHECK(wide_json::to_cbor(make_int(-two_64)) == std::vector<std::uint8_t>({0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
|
||||
CHECK(wide_json::to_cbor(make_uint(max_uint64)) == std::vector<std::uint8_t>({0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
|
||||
}
|
||||
|
||||
SECTION("MessagePack")
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit [-2^63, 2^64-1]", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit [-2^63, 2^64-1]", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit uint64", out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_msgpack(make_int(two_64)), out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_msgpack(make_int(min_int64 - 1)), out_of_range&);
|
||||
|
||||
// MessagePack's integers cover [-2^63, 2^64-1]
|
||||
CHECK(wide_json::to_msgpack(make_int(two_64 - 1)) == std::vector<std::uint8_t>({0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
|
||||
CHECK(wide_json::to_msgpack(make_int(min_int64)) == std::vector<std::uint8_t>({0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}));
|
||||
CHECK(wide_json::to_msgpack(make_uint(max_uint64)) == std::vector<std::uint8_t>({0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
|
||||
}
|
||||
|
||||
SECTION("BSON")
|
||||
{
|
||||
// wrap() stores the value at /v, which JSON_DIAGNOSTICS adds to the message
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big)), "[json.exception.out_of_range.407] (/v) integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big_negative)), "[json.exception.out_of_range.407] (/v) integer number -1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(uint_big)), "[json.exception.out_of_range.407] (/v) integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit uint64", out_of_range&);
|
||||
#else
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big)), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big_negative)), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(uint_big)), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit uint64", out_of_range&);
|
||||
#endif
|
||||
CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(make_int(max_int64 + 1))), out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(make_int(min_int64 - 1))), out_of_range&);
|
||||
|
||||
// nested values are checked as well, before anything is written
|
||||
wide_json nested(wide_json::value_t::object);
|
||||
nested["a"] = wide_json::array({wrap(int_big)});
|
||||
std::vector<std::uint8_t> out;
|
||||
CHECK_THROWS_AS(wide_json::to_bson(nested, out), out_of_range&);
|
||||
CHECK(out.empty());
|
||||
|
||||
CHECK(wide_json::from_bson(wide_json::to_bson(wrap(make_int(max_int64)))) == wrap(make_int(max_int64)));
|
||||
CHECK(wide_json::from_bson(wide_json::to_bson(wrap(make_int(min_int64)))) == wrap(make_int(min_int64)));
|
||||
CHECK_NOTHROW(wide_json::to_bson(wrap(make_uint(max_uint64))));
|
||||
}
|
||||
|
||||
SECTION("BON8")
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_bon8(make_int(max_int64 + 1)), out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_bon8(make_int(min_int64 - 1)), out_of_range&);
|
||||
|
||||
CHECK(wide_json::from_bon8(wide_json::to_bon8(make_int(max_int64))) == make_int(max_int64));
|
||||
CHECK(wide_json::from_bon8(wide_json::to_bon8(make_int(min_int64))) == make_int(min_int64));
|
||||
}
|
||||
|
||||
SECTION("UBJSON and BJData")
|
||||
{
|
||||
// integers beyond 64 bits are written exactly as high-precision numbers
|
||||
const std::string digits = "1267650600228229401496703205376";
|
||||
std::vector<std::uint8_t> expected = {'H', 'i', static_cast<std::uint8_t>(digits.size())};
|
||||
expected.insert(expected.end(), digits.begin(), digits.end());
|
||||
CHECK(wide_json::to_ubjson(int_big) == expected);
|
||||
CHECK(wide_json::to_ubjson(uint_big) == expected);
|
||||
CHECK(wide_json::to_bjdata(int_big) == expected);
|
||||
// BJData's uint64 marker 'M' was used for any unsigned value beyond
|
||||
// int64, which truncated the ones beyond 64 bits
|
||||
CHECK(wide_json::to_bjdata(uint_big) == expected);
|
||||
|
||||
// the uint64 marker is still used where the value fits
|
||||
CHECK(wide_json::to_bjdata(make_uint(max_uint64)) == std::vector<std::uint8_t>({'M', 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
|
||||
|
||||
// the elements of an optimized container are written the same way;
|
||||
// BJData does not allow 'H' as the type of an optimized container
|
||||
std::vector<std::uint8_t> expected_ubjson_array = {'[', '$', 'H', '#', 'i', 2};
|
||||
std::vector<std::uint8_t> expected_bjdata_array = {'[', '#', 'i', 2};
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
expected_ubjson_array.insert(expected_ubjson_array.end(), expected.begin() + 1, expected.end());
|
||||
expected_bjdata_array.insert(expected_bjdata_array.end(), expected.begin(), expected.end());
|
||||
}
|
||||
CHECK(wide_json::to_ubjson(wide_json::array({uint_big, uint_big}), true, true) == expected_ubjson_array);
|
||||
CHECK(wide_json::to_bjdata(wide_json::array({uint_big, uint_big}), true, true) == expected_bjdata_array);
|
||||
}
|
||||
|
||||
SECTION("BJData ND-array")
|
||||
{
|
||||
// an ND-array element beyond 64 bits does not fit any dtype, so the
|
||||
// annotated object is written as a plain object instead of truncating
|
||||
// the element into range
|
||||
wide_json element(wide_json::value_t::object);
|
||||
element["_ArrayType_"] = "uint8";
|
||||
element["_ArraySize_"] = wide_json::array({make_uint(2), make_uint(1)});
|
||||
element["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(static_cast<uint128>(two_64) + 1)});
|
||||
const auto element_bytes = wide_json::to_bjdata(element, true, true);
|
||||
REQUIRE(!element_bytes.empty());
|
||||
CHECK(element_bytes[0] == '{');
|
||||
|
||||
wide_json signed_element = element;
|
||||
signed_element["_ArrayType_"] = "int8";
|
||||
signed_element["_ArrayData_"] = wide_json::array({make_int(1), make_int(-two_64 + 1)});
|
||||
const auto signed_element_bytes = wide_json::to_bjdata(signed_element, true, true);
|
||||
REQUIRE(!signed_element_bytes.empty());
|
||||
CHECK(signed_element_bytes[0] == '{');
|
||||
|
||||
// the same for a dimension beyond 64 bits, which wrapped into a
|
||||
// dimension matching the size of _ArrayData_ (here: 1)
|
||||
wide_json dimension = element;
|
||||
dimension["_ArraySize_"] = wide_json::array({make_uint(2), make_uint(static_cast<uint128>(two_64) + 1)});
|
||||
dimension["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(2)});
|
||||
const auto dimension_bytes = wide_json::to_bjdata(dimension, true, true);
|
||||
REQUIRE(!dimension_bytes.empty());
|
||||
CHECK(dimension_bytes[0] == '{');
|
||||
|
||||
// in range, the ND-array is still written
|
||||
wide_json fits = element;
|
||||
fits["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(2)});
|
||||
const auto fits_bytes = wide_json::to_bjdata(fits, true, true);
|
||||
REQUIRE(!fits_bytes.empty());
|
||||
CHECK(fits_bytes[0] == '[');
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST_CASE("custom number types")
|
||||
{
|
||||
SECTION("signed integer of class type compared with a float")
|
||||
{
|
||||
// std::is_signed is false for a class type, which made the comparison
|
||||
// treat any float below zero as less than every integer
|
||||
const class_int_json minus_five = make_class_int(-5);
|
||||
const class_int_json minus_two = make_class_int(-2);
|
||||
const class_int_json five = make_class_int(5);
|
||||
const class_int_json minus_two_and_a_half = -2.5;
|
||||
const class_int_json two_and_a_half = 2.5;
|
||||
const class_int_json huge_negative = -1e30;
|
||||
const class_int_json huge_positive = 1e30;
|
||||
|
||||
CHECK(minus_five < minus_two_and_a_half);
|
||||
CHECK_FALSE(minus_two_and_a_half < minus_five);
|
||||
CHECK(minus_two_and_a_half > minus_five);
|
||||
CHECK(minus_five <= minus_two_and_a_half);
|
||||
CHECK(minus_two_and_a_half >= minus_five);
|
||||
CHECK(minus_five != minus_two_and_a_half);
|
||||
|
||||
CHECK(minus_two_and_a_half < minus_two);
|
||||
CHECK_FALSE(minus_two < minus_two_and_a_half);
|
||||
|
||||
CHECK(two_and_a_half < five);
|
||||
CHECK(minus_five < two_and_a_half);
|
||||
|
||||
// floats beyond the integer's range
|
||||
CHECK(huge_negative < minus_five);
|
||||
CHECK_FALSE(minus_five < huge_negative);
|
||||
CHECK(five < huge_positive);
|
||||
CHECK_FALSE(huge_positive < five);
|
||||
|
||||
// equality
|
||||
const class_int_json minus_two_float = -2.0;
|
||||
CHECK(minus_two == minus_two_float);
|
||||
CHECK(minus_two_float == minus_two);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
// Parsing from a FILE* locks the file once and reads it with the unlocked
|
||||
// stdio functions (see JSON_DISABLE_UNLOCKED_FILE_IO). These tests pin that
|
||||
// this is not observable: the results are the same, the file is unlocked
|
||||
// again when parsing ends - also with an exception - and the same thread can
|
||||
// keep using the file while it is locked, e.g., from a SAX callback.
|
||||
|
||||
namespace
|
||||
{
|
||||
// a file name of its own for every run: the test runs for several C++
|
||||
// standards and configurations at the same time, in the same directory
|
||||
std::string temp_file_name()
|
||||
{
|
||||
std::random_device rd;
|
||||
return "unit-file_input-" + std::to_string(rd()) + ".tmp";
|
||||
}
|
||||
|
||||
using file_ptr = std::unique_ptr<std::FILE, int (*)(std::FILE*)>;
|
||||
|
||||
// write @a content to the file @a name and open it for reading
|
||||
file_ptr open_with(const char* name, const std::string& content)
|
||||
{
|
||||
{
|
||||
std::ofstream file(name, std::ios::binary);
|
||||
file << content;
|
||||
}
|
||||
return {std::fopen(name, "rb"), &std::fclose};
|
||||
}
|
||||
|
||||
// The stdio lock is recursive, so whether parsing released it can only be
|
||||
// seen from another thread: ftrylockfile() fails there while this thread
|
||||
// holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped.
|
||||
#if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
|
||||
void check_unlocked(std::FILE* f)
|
||||
{
|
||||
int result = -1;
|
||||
std::thread other([f, &result]
|
||||
{
|
||||
result = ftrylockfile(f);
|
||||
if (result == 0)
|
||||
{
|
||||
funlockfile(f);
|
||||
}
|
||||
});
|
||||
other.join();
|
||||
CHECK(result == 0);
|
||||
}
|
||||
#else
|
||||
void check_unlocked(std::FILE* /*f*/) {}
|
||||
#endif
|
||||
|
||||
// SAX consumer that also reads the position of the file it is parsing, which
|
||||
// takes the file's lock again on the same thread
|
||||
class position_reading_sax : public nlohmann::json_sax<json>
|
||||
{
|
||||
public:
|
||||
explicit position_reading_sax(std::FILE* f) : m_file(f) {}
|
||||
position_reading_sax(const position_reading_sax&) = delete;
|
||||
position_reading_sax(position_reading_sax&&) = delete;
|
||||
position_reading_sax& operator=(const position_reading_sax&) = delete;
|
||||
position_reading_sax& operator=(position_reading_sax&&) = delete;
|
||||
~position_reading_sax() override = default;
|
||||
|
||||
bool null() override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool boolean(bool /*val*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool number_integer(number_integer_t /*val*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool number_unsigned(number_unsigned_t /*val*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool string(string_t& /*val*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool binary(binary_t& /*val*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool start_object(std::size_t /*elements*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool key(string_t& /*val*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool end_object() override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool start_array(std::size_t /*elements*/) override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool end_array() override
|
||||
{
|
||||
return record();
|
||||
}
|
||||
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<long>& positions() const
|
||||
{
|
||||
return m_positions;
|
||||
}
|
||||
|
||||
private:
|
||||
bool record()
|
||||
{
|
||||
m_positions.push_back(std::ftell(m_file));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::FILE* m_file;
|
||||
std::vector<long> m_positions{}; // NOLINT(readability-redundant-member-init)
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("parsing from a FILE*")
|
||||
{
|
||||
const std::string temp_name = temp_file_name();
|
||||
const char* const name = temp_name.c_str();
|
||||
const std::string input = "{\"a\": [1, -2.5, \"three\", true, null], \"b\": {\"c\": \"caf\xc3\xa9\"}}";
|
||||
|
||||
SECTION("values are the same as when parsing from a string")
|
||||
{
|
||||
const file_ptr f = open_with(name, input);
|
||||
REQUIRE(f != nullptr);
|
||||
CHECK(json::parse(f.get()) == json::parse(input));
|
||||
// the whole input was consumed
|
||||
std::array<char, 8> rest{};
|
||||
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) == nullptr);
|
||||
check_unlocked(f.get());
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("the file is unlocked after a parse error")
|
||||
{
|
||||
const file_ptr f = open_with(name, "[1, 2, x]");
|
||||
REQUIRE(f != nullptr);
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::parse(f.get()),
|
||||
"[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - invalid literal; last read: '2, x'",
|
||||
json::parse_error&);
|
||||
check_unlocked(f.get());
|
||||
|
||||
// the file is still usable: the rest of the input is there
|
||||
std::array<char, 8> rest{};
|
||||
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
|
||||
CHECK(std::string(rest.data()) == "]");
|
||||
}
|
||||
|
||||
#endif // !defined(JSON_NOEXCEPTION)
|
||||
|
||||
SECTION("the file can be parsed again and read with stdio afterwards")
|
||||
{
|
||||
const file_ptr f = open_with(name, "[1] [2] tail");
|
||||
REQUIRE(f != nullptr);
|
||||
|
||||
// sax_parse with strict == false stops after the first value; an array
|
||||
// ends with its closing bracket, so the space after it is not read
|
||||
json j;
|
||||
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax1(j);
|
||||
CHECK(json::sax_parse(f.get(), &sax1, json::input_format_t::json, false));
|
||||
CHECK(j == json({1}));
|
||||
check_unlocked(f.get());
|
||||
|
||||
json k;
|
||||
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax2(k);
|
||||
CHECK(json::sax_parse(f.get(), &sax2, json::input_format_t::json, false));
|
||||
CHECK(k == json({2}));
|
||||
check_unlocked(f.get());
|
||||
|
||||
std::array<char, 8> rest{};
|
||||
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
|
||||
CHECK(std::string(rest.data()) == " tail");
|
||||
}
|
||||
|
||||
SECTION("the same thread can use the file while it is parsed")
|
||||
{
|
||||
const file_ptr f = open_with(name, "[10, 20, 30]");
|
||||
REQUIRE(f != nullptr);
|
||||
position_reading_sax sax(f.get());
|
||||
CHECK(json::sax_parse(f.get(), &sax));
|
||||
// start_array, three numbers, end_array
|
||||
CHECK(sax.positions().size() == 5);
|
||||
CHECK(sax.positions().front() == 1);
|
||||
check_unlocked(f.get());
|
||||
}
|
||||
|
||||
SECTION("binary formats read from a FILE*")
|
||||
{
|
||||
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
|
||||
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
|
||||
const file_ptr f = open_with(name, std::string(reinterpret_cast<const char*>(cbor.data()), cbor.size()));
|
||||
REQUIRE(f != nullptr);
|
||||
CHECK(json::from_cbor(f.get()) == j);
|
||||
check_unlocked(f.get());
|
||||
}
|
||||
|
||||
static_cast<void>(std::remove(name));
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// The fast paths for contiguous input (bulk string scanning, SWAR searches,
|
||||
// UTF-8 prefix validation, the 8-digit number loop, the 8-byte indentation
|
||||
// skip, the BSON/BON8 bulk string readers) load several bytes at a time. This
|
||||
// test places every input directly in front of (or directly behind) an
|
||||
// inaccessible page, so reading even one byte outside the input crashes, with
|
||||
// or without sanitizers. ASan alone misses over-reads into the slack of the
|
||||
// same heap allocation.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define JSON_TEST_GUARD_PAGE_WINDOWS
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#elif defined(__unix__) || defined(__APPLE__)
|
||||
#define JSON_TEST_GUARD_PAGE_POSIX
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if defined(JSON_TEST_GUARD_PAGE_WINDOWS) || defined(JSON_TEST_GUARD_PAGE_POSIX)
|
||||
|
||||
namespace
|
||||
{
|
||||
// Three pages: an inaccessible one, a readable one, an inaccessible one. An
|
||||
// input is copied either to the end of the middle page (any read past its last
|
||||
// byte faults) or to its start (any read before its first byte faults).
|
||||
class guarded_buffer
|
||||
{
|
||||
public:
|
||||
guarded_buffer()
|
||||
: page(page_size())
|
||||
{
|
||||
#if defined(JSON_TEST_GUARD_PAGE_WINDOWS)
|
||||
void* const p = VirtualAlloc(nullptr, 3 * page, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS);
|
||||
REQUIRE(p != nullptr);
|
||||
base = static_cast<unsigned char*>(p);
|
||||
DWORD old_protect = 0;
|
||||
REQUIRE(VirtualProtect(base + page, page, PAGE_READWRITE, &old_protect) != 0);
|
||||
#else
|
||||
void* const p = mmap(nullptr, 3 * page, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
REQUIRE(p != MAP_FAILED);
|
||||
base = static_cast<unsigned char*>(p);
|
||||
REQUIRE(mprotect(base + page, page, PROT_READ | PROT_WRITE) == 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
~guarded_buffer()
|
||||
{
|
||||
#if defined(JSON_TEST_GUARD_PAGE_WINDOWS)
|
||||
VirtualFree(base, 0, MEM_RELEASE);
|
||||
#else
|
||||
munmap(base, 3 * page);
|
||||
#endif
|
||||
}
|
||||
|
||||
guarded_buffer(const guarded_buffer&) = delete;
|
||||
guarded_buffer& operator=(const guarded_buffer&) = delete;
|
||||
guarded_buffer(guarded_buffer&&) = delete;
|
||||
guarded_buffer& operator=(guarded_buffer&&) = delete;
|
||||
|
||||
// copies @a input so its last byte is the last readable byte
|
||||
const std::uint8_t* at_end(const std::string& input)
|
||||
{
|
||||
REQUIRE(input.size() <= page);
|
||||
unsigned char* const first = base + (2 * page) - input.size();
|
||||
std::transform(input.begin(), input.end(), first, [](const char c)
|
||||
{
|
||||
return static_cast<unsigned char>(c);
|
||||
});
|
||||
return first;
|
||||
}
|
||||
|
||||
// copies @a input so its first byte is the first readable byte
|
||||
const std::uint8_t* at_start(const std::string& input)
|
||||
{
|
||||
REQUIRE(input.size() <= page);
|
||||
unsigned char* const first = base + page;
|
||||
std::transform(input.begin(), input.end(), first, [](const char c)
|
||||
{
|
||||
return static_cast<unsigned char>(c);
|
||||
});
|
||||
return first;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::size_t page_size()
|
||||
{
|
||||
#if defined(JSON_TEST_GUARD_PAGE_WINDOWS)
|
||||
SYSTEM_INFO info;
|
||||
GetSystemInfo(&info);
|
||||
return static_cast<std::size_t>(info.dwPageSize);
|
||||
#else
|
||||
return static_cast<std::size_t>(sysconf(_SC_PAGESIZE));
|
||||
#endif
|
||||
}
|
||||
|
||||
std::size_t page = 0;
|
||||
unsigned char* base = nullptr;
|
||||
};
|
||||
|
||||
// the serialization of the result of @a f, or the exception message; @a f
|
||||
// takes the allow_exceptions flag, which is false without exceptions, where
|
||||
// an error yields a discarded value instead
|
||||
template<typename F>
|
||||
std::string outcome(F f)
|
||||
{
|
||||
#if defined(JSON_NOEXCEPTION)
|
||||
return "value: " + f(false).dump(-1, ' ', false, json::error_handler_t::keep);
|
||||
#else
|
||||
try
|
||||
{
|
||||
return "value: " + f(true).dump(-1, ' ', false, json::error_handler_t::keep);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
return std::string("error: ") + e.what();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// a SAX handler that only counts events, so sax_parse runs without a DOM
|
||||
struct counting_sax : nlohmann::json_sax<json>
|
||||
{
|
||||
std::size_t events = 0;
|
||||
std::string error{}; // NOLINT(readability-redundant-member-init)
|
||||
|
||||
bool null() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool boolean(bool /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_integer(number_integer_t /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_unsigned(number_unsigned_t /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool string(string_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool binary(binary_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool start_object(std::size_t /*elements*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool key(string_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool end_object() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool start_array(std::size_t /*elements*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool end_array() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool parse_error(std::size_t position, const std::string& /*last_token*/, const nlohmann::detail::exception& ex) override
|
||||
{
|
||||
error = std::to_string(position) + " " + ex.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool count()
|
||||
{
|
||||
++events;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// all prefixes of each seed, so the input ends at every byte of it
|
||||
std::vector<std::string> all_prefixes(const std::vector<std::string>& seeds)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
for (const auto& seed : seeds)
|
||||
{
|
||||
for (std::size_t n = 0; n <= seed.size(); ++n)
|
||||
{
|
||||
result.push_back(seed.substr(0, n));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> json_seeds()
|
||||
{
|
||||
std::vector<std::string> seeds =
|
||||
{
|
||||
// string runs: ASCII, escapes, \u escapes, surrogate pairs
|
||||
"\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"",
|
||||
R"("ab\ncd\"ef\\gh\u00e4ij\uD83D\uDE00kl\u12")",
|
||||
// 2-, 3- and 4-byte UTF-8 sequences, then invalid UTF-8
|
||||
std::string("\"\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80") + "abcdefgh\"",
|
||||
"\"abcdefgh\xC3\x28 \xED\xA0\x80 \xF4\x90\x80\x80 \xF8 \xC0\xAF\"",
|
||||
std::string("\"abcdefg\x01") + "abcdefgh\"",
|
||||
// numbers, so the 8-digit loop runs up to the end
|
||||
"12345678",
|
||||
"1234567890123456",
|
||||
"12345678901234567890123",
|
||||
"-0.000000001234567890123456789",
|
||||
"3.14159265358979323846264338327950288e+100",
|
||||
"-1.2345678901234567e-12",
|
||||
"1E-5",
|
||||
"[1.5,2e10,-3E-5,0.000000001,123456789012345678]",
|
||||
// literals
|
||||
"[true,false,null]",
|
||||
// comments (only parsed with ignore_comments)
|
||||
"/* comment */ [1, // line comment\n 2] /**/",
|
||||
// a pretty-printed document
|
||||
json::parse(R"({"a": [1, 2.5, "three", {"b": null, "c": [true, false]}], "d": {"e": {"f": {"g": "h"}}}})").dump(4),
|
||||
json::parse(R"({"a": [1, 2.5, "three", {"b": null, "c": [true, false]}], "d": {"e": {"f": {"g": "h"}}}})").dump(8, '\t'),
|
||||
};
|
||||
|
||||
// indentation runs of every length, with and without a closing bracket
|
||||
for (std::size_t n = 0; n <= 20; ++n)
|
||||
{
|
||||
seeds.push_back("[\n" + std::string(n, ' ') + "1,\n" + std::string(n, ' ') + "2\n]");
|
||||
}
|
||||
|
||||
return all_prefixes(seeds);
|
||||
}
|
||||
|
||||
// a value whose binary serializations contain long strings and keys, numbers
|
||||
// of every width, and nested containers
|
||||
json binary_document()
|
||||
{
|
||||
json j;
|
||||
j["a key that is longer than eight bytes"] = "a string value that is longer than eight bytes";
|
||||
j["\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80"] = "\xF0\x9F\x98\x80\xE2\x82\xAC\xC3\xA4 utf-8 0123456789";
|
||||
j["numbers"] = {0, 1, -1, 255, -256, 65536, -2147483649LL, 4294967296ULL, 1.5, -2.25e100};
|
||||
j["nested"] = {{"x", {{"y", {true, false, nullptr}}}}};
|
||||
j["s"] = "";
|
||||
return j;
|
||||
}
|
||||
|
||||
// readers for check_binary(), callable with a container or an iterator range
|
||||
// followed by the allow_exceptions flag
|
||||
#define JSON_TEST_GUARD_PAGE_READER(name, from) \
|
||||
struct name \
|
||||
{ \
|
||||
template<typename... Args> \
|
||||
json operator()(bool allow_exceptions, Args&& ... args) const \
|
||||
{ \
|
||||
return json::from(std::forward<Args>(args)..., true, allow_exceptions); \
|
||||
} \
|
||||
};
|
||||
JSON_TEST_GUARD_PAGE_READER(read_bson, from_bson)
|
||||
JSON_TEST_GUARD_PAGE_READER(read_bon8, from_bon8)
|
||||
JSON_TEST_GUARD_PAGE_READER(read_cbor, from_cbor)
|
||||
JSON_TEST_GUARD_PAGE_READER(read_msgpack, from_msgpack)
|
||||
JSON_TEST_GUARD_PAGE_READER(read_ubjson, from_ubjson)
|
||||
JSON_TEST_GUARD_PAGE_READER(read_bjdata, from_bjdata)
|
||||
#undef JSON_TEST_GUARD_PAGE_READER
|
||||
|
||||
// @a bytes as characters (explicitly converted: the iterator-range constructor
|
||||
// would convert implicitly, which -fsanitize=integer reports)
|
||||
std::string as_chars(const std::vector<std::uint8_t>& bytes)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(bytes.size());
|
||||
for (const std::uint8_t b : bytes)
|
||||
{
|
||||
result.push_back(static_cast<char>(b));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// reads every prefix of @a bytes at both ends of the guarded page and from a
|
||||
// std::string, which must give the same outcome
|
||||
template<typename Reader>
|
||||
void check_binary(guarded_buffer& guard, const std::vector<std::uint8_t>& bytes, Reader parse)
|
||||
{
|
||||
for (const auto& input : all_prefixes({as_chars(bytes)}))
|
||||
{
|
||||
CAPTURE(input)
|
||||
const std::string expected = outcome([&](bool allow)
|
||||
{
|
||||
return parse(allow, input);
|
||||
});
|
||||
for (int placement = 0; placement < 2; ++placement)
|
||||
{
|
||||
const std::uint8_t* const first = placement == 0 ? guard.at_end(input) : guard.at_start(input);
|
||||
CHECK(outcome([&](bool allow)
|
||||
{
|
||||
return parse(allow, first, first + input.size());
|
||||
}) == expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse_float_native() at both ends of the guarded page, compared bit for bit
|
||||
// with converting the same token from a std::string
|
||||
template<typename FloatType>
|
||||
std::uint64_t float_bits(FloatType value)
|
||||
{
|
||||
static_assert(sizeof(FloatType) <= sizeof(std::uint64_t), "unsupported floating-point width");
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(value));
|
||||
return bits;
|
||||
}
|
||||
|
||||
template<typename FloatType>
|
||||
void check_float_native(guarded_buffer& guard, const std::string& token)
|
||||
{
|
||||
const auto decimal_point = token.find('.');
|
||||
const auto exponent = token.find_first_of("eE");
|
||||
const auto mantissa_end = exponent == std::string::npos ? token.size() : exponent;
|
||||
const auto expected = nlohmann::detail::parse_float_native<FloatType>(token.data(), token.data() + token.size(), decimal_point, mantissa_end);
|
||||
for (int placement = 0; placement < 2; ++placement)
|
||||
{
|
||||
const char* const first = reinterpret_cast<const char*>(placement == 0 ? guard.at_end(token) : guard.at_start(token));
|
||||
const auto value = nlohmann::detail::parse_float_native<FloatType>(first, first + token.size(), decimal_point, mantissa_end);
|
||||
CHECK(float_bits(value) == float_bits(expected));
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("guard page")
|
||||
{
|
||||
guarded_buffer guard;
|
||||
|
||||
SECTION("string scanning")
|
||||
{
|
||||
// fills that drive each function to a different stop
|
||||
const std::vector<std::string> fills =
|
||||
{
|
||||
std::string(64, 'a'),
|
||||
std::string(64, '"'),
|
||||
std::string(64, '\\'),
|
||||
std::string(64, '\x01'),
|
||||
std::string(64, '\x7F'),
|
||||
std::string(64, '\xFF'),
|
||||
};
|
||||
std::vector<std::string> patterns = fills;
|
||||
{
|
||||
std::string utf8;
|
||||
while (utf8.size() < 64)
|
||||
{
|
||||
utf8 += "\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80" "ab";
|
||||
}
|
||||
patterns.push_back(utf8);
|
||||
}
|
||||
{
|
||||
std::string bytes;
|
||||
for (unsigned int i = 0; i < 64; ++i)
|
||||
{
|
||||
bytes.push_back(static_cast<char>((i * 37u) & 0xFFu));
|
||||
}
|
||||
patterns.push_back(bytes);
|
||||
}
|
||||
|
||||
for (const auto& pattern : patterns)
|
||||
{
|
||||
for (std::size_t n = 0; n <= 64; ++n)
|
||||
{
|
||||
const std::string input = pattern.substr(0, n);
|
||||
for (int placement = 0; placement < 2; ++placement)
|
||||
{
|
||||
const unsigned char* const p = placement == 0 ? guard.at_end(input) : guard.at_start(input);
|
||||
|
||||
CHECK(nlohmann::detail::find_string_special(p, n) <= n);
|
||||
CHECK(nlohmann::detail::find_ascii_copyable_run(p, n) <= n);
|
||||
CHECK(nlohmann::detail::valid_utf8_prefix(p, n) <= n);
|
||||
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
|
||||
CHECK(nlohmann::detail::find_string_delimiter(p, n) <= n);
|
||||
#endif
|
||||
CHECK(nlohmann::detail::string_bulk_run(p, n) <= n);
|
||||
CHECK(nlohmann::detail::scalar_string_bulk_run(p, n) <= n);
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
CHECK(nlohmann::detail::validate_one_utf8(p + i, n - i) <= n - i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("float conversion")
|
||||
{
|
||||
// the lexer converts floats from its token buffer, so the JSON section
|
||||
// below never places a float token at the page boundary; this calls the
|
||||
// conversion directly: the 8-digit loops of parse_float_significand()
|
||||
// and digit_comparison() read up to the end of the token
|
||||
const std::vector<std::string> tokens = all_prefixes(
|
||||
{
|
||||
"1234567812345678123456781234567812345678",
|
||||
"-1.2345678901234567890123456789e-123",
|
||||
"0.00000000000000000000000000000000000001",
|
||||
"98765432.10987654",
|
||||
"123456789012345678901234567890e-30",
|
||||
// halfway between 1 and the next double, then just above it
|
||||
"1.00000000000000011102230246251565404236316680908203125",
|
||||
"1.000000000000000111022302462515654042363166809082031250000000001",
|
||||
});
|
||||
for (const auto& token : tokens)
|
||||
{
|
||||
// only complete number tokens: the conversion expects validated input
|
||||
if (!token.empty() && json::accept(token))
|
||||
{
|
||||
CAPTURE(token)
|
||||
check_float_native<double>(guard, token);
|
||||
check_float_native<float>(guard, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("JSON")
|
||||
{
|
||||
for (const auto& input : json_seeds())
|
||||
{
|
||||
CAPTURE(input)
|
||||
const std::string expected = outcome([&](bool allow)
|
||||
{
|
||||
return json::parse(input, nullptr, allow);
|
||||
});
|
||||
const std::string expected_comments = outcome([&](bool allow)
|
||||
{
|
||||
return json::parse(input, nullptr, allow, true);
|
||||
});
|
||||
const bool expected_accept = json::accept(input);
|
||||
counting_sax expected_sax;
|
||||
const bool expected_sax_result = json::sax_parse(input, &expected_sax);
|
||||
|
||||
for (int placement = 0; placement < 2; ++placement)
|
||||
{
|
||||
const std::uint8_t* const first = placement == 0 ? guard.at_end(input) : guard.at_start(input);
|
||||
const std::uint8_t* const last = first + input.size();
|
||||
|
||||
CHECK(outcome([&](bool allow)
|
||||
{
|
||||
return json::parse(first, last, nullptr, allow);
|
||||
}) == expected);
|
||||
CHECK(outcome([&](bool allow)
|
||||
{
|
||||
return json::parse(first, last, nullptr, allow, true);
|
||||
}) == expected_comments);
|
||||
CHECK(json::accept(first, last) == expected_accept);
|
||||
|
||||
counting_sax sax;
|
||||
CHECK(json::sax_parse(first, last, &sax) == expected_sax_result);
|
||||
CHECK(sax.events == expected_sax.events);
|
||||
CHECK(sax.error == expected_sax.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("binary formats")
|
||||
{
|
||||
// every prefix ends inside a value, e.g., inside a BSON key or BON8
|
||||
// string without its terminator
|
||||
const json j = binary_document();
|
||||
check_binary(guard, json::to_bson(j), read_bson());
|
||||
check_binary(guard, json::to_bon8(j), read_bon8());
|
||||
check_binary(guard, json::to_cbor(j), read_cbor());
|
||||
check_binary(guard, json::to_msgpack(j), read_msgpack());
|
||||
check_binary(guard, json::to_ubjson(j), read_ubjson());
|
||||
check_binary(guard, json::to_bjdata(j), read_bjdata());
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,610 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#if JSON_TEST_USING_MULTIPLE_HEADERS
|
||||
#include <nlohmann/detail/view/builder.hpp>
|
||||
#include <nlohmann/detail/view/materialize.hpp>
|
||||
#include <nlohmann/detail/view/string_ref.hpp>
|
||||
#else
|
||||
#include <nlohmann/json_view.hpp> // the single header contains the internal headers
|
||||
#endif
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <test_data.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
using nlohmann::detail::view::document_data;
|
||||
using nlohmann::detail::view::node;
|
||||
|
||||
// the node index of a text; a vector input has no terminating NUL, so that
|
||||
// AddressSanitizer catches any read past the last byte
|
||||
struct built
|
||||
{
|
||||
std::unique_ptr<document_data, document_data::deleter> data{}; // NOLINT(readability-redundant-member-init)
|
||||
std::vector<char> copy{}; // NOLINT(readability-redundant-member-init)
|
||||
bool ok = false;
|
||||
nlohmann::detail::view::parse_failure failure{};
|
||||
};
|
||||
|
||||
template<typename FloatType = double>
|
||||
built build(const std::string& text, bool comments, bool trailing_commas, bool sentinel)
|
||||
{
|
||||
built r;
|
||||
r.data.reset(document_data::create(nlohmann::detail::view::estimate_nodes(text.data(), text.size())));
|
||||
const char* src = text.c_str();
|
||||
if (!sentinel)
|
||||
{
|
||||
r.copy.assign(text.begin(), text.end());
|
||||
src = r.copy.data();
|
||||
}
|
||||
r.ok = nlohmann::detail::view::build < FloatType, !nlohmann::detail::abi_config::strict_nul_handling > (*r.data, src, text.size(), comments, trailing_commas, sentinel, r.failure);
|
||||
r.data->src = src;
|
||||
r.data->base[0] = src;
|
||||
r.data->base[1] = r.data->arena.data();
|
||||
return r;
|
||||
}
|
||||
|
||||
// the value of a subtree, as json::parse would build it
|
||||
json value_of(const document_data& d, const node*& n)
|
||||
{
|
||||
const node& x = *n;
|
||||
++n;
|
||||
switch (static_cast<json::value_t>(x.kind))
|
||||
{
|
||||
case json::value_t::object:
|
||||
{
|
||||
json o = json::object();
|
||||
const node* const end = &x + x.next;
|
||||
while (n != end)
|
||||
{
|
||||
const std::string key(d.str(*n), n->len);
|
||||
++n;
|
||||
o[key] = value_of(d, n);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
case json::value_t::array:
|
||||
{
|
||||
json a = json::array();
|
||||
const node* const end = &x + x.next;
|
||||
while (n != end)
|
||||
{
|
||||
a.push_back(value_of(d, n));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
case json::value_t::string:
|
||||
return std::string(d.str(x), x.len);
|
||||
case json::value_t::boolean:
|
||||
return (x.flags & nlohmann::detail::view::node_flags::is_true) != 0;
|
||||
case json::value_t::number_integer:
|
||||
return static_cast<std::int64_t>(nlohmann::detail::view::integer_bits(x));
|
||||
case json::value_t::number_unsigned:
|
||||
return nlohmann::detail::view::integer_bits(x);
|
||||
case json::value_t::number_float:
|
||||
return json::parse(std::string(d.src + x.off, x.len)).get<double>();
|
||||
case json::value_t::null:
|
||||
case json::value_t::binary:
|
||||
case json::value_t::discarded:
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
json value_of(const built& b)
|
||||
{
|
||||
const node* n = b.data->nodes;
|
||||
json v = value_of(*b.data, n);
|
||||
CHECK(n == b.data->nodes + b.data->node_count);
|
||||
return v;
|
||||
}
|
||||
|
||||
// accept/reject and the value must match json::parse, for all options and
|
||||
// with and without a NUL after the text
|
||||
void check_same(const std::string& text)
|
||||
{
|
||||
CAPTURE(text)
|
||||
for (int options = 0; options < 4; ++options)
|
||||
{
|
||||
const bool comments = (options & 1) != 0;
|
||||
const bool trailing_commas = (options & 2) != 0;
|
||||
const bool accepted = json::accept(text, comments, trailing_commas);
|
||||
for (const bool sentinel :
|
||||
{
|
||||
true, false
|
||||
})
|
||||
{
|
||||
const built b = build(text, comments, trailing_commas, sentinel);
|
||||
CHECK(b.ok == accepted);
|
||||
if (b.ok && accepted)
|
||||
{
|
||||
CHECK(value_of(b) == json::parse(text, nullptr, true, comments, trailing_commas));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a string value and a key must be accepted or rejected as json::parse does,
|
||||
// and give its value (cheaper than check_same: the options do not matter)
|
||||
void check_string(const std::string& content)
|
||||
{
|
||||
for (const std::string& text :
|
||||
{
|
||||
"[\"" + content + "\"]", "{\"" + content + "\":1}"
|
||||
})
|
||||
{
|
||||
const bool accepted = json::accept(text);
|
||||
for (const bool sentinel :
|
||||
{
|
||||
true, false
|
||||
})
|
||||
{
|
||||
const built b = build(text, false, false, sentinel);
|
||||
if (b.ok != accepted || (accepted && value_of(b) != json::parse(text)))
|
||||
{
|
||||
CAPTURE(text)
|
||||
CHECK(b.ok == accepted);
|
||||
CHECK((b.ok && accepted ? value_of(b) == json::parse(text) : true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a small deterministic generator of documents
|
||||
struct generator
|
||||
{
|
||||
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
|
||||
int r(int n)
|
||||
{
|
||||
return static_cast<int>(rng() % static_cast<unsigned>(n));
|
||||
}
|
||||
|
||||
void ws(std::string& o)
|
||||
{
|
||||
for (int n = r(4) == 0 ? r(12) : r(2); n > 0; --n)
|
||||
{
|
||||
o += " \n\t\r "[r(6)];
|
||||
}
|
||||
}
|
||||
|
||||
void str(std::string& o)
|
||||
{
|
||||
static const char* const pieces[] = {"a", "Z", " ", "~", "\\n", "\\\"", "\\\\", "\\/", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\\u001f", "long enough text to leave the first 16 bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
o += '"';
|
||||
for (int n = r(3) == 0 ? r(20) : r(6); n > 0; --n)
|
||||
{
|
||||
o += pieces[r(16)];
|
||||
}
|
||||
o += '"';
|
||||
}
|
||||
|
||||
void num(std::string& o)
|
||||
{
|
||||
static const char* const numbers[] = {"0", "-0", "1", "-1", "12", "123456789", "1234567890123456789", "9223372036854775807", "-9223372036854775808", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
"9223372036854775808", "18446744073709551615", "18446744073709551616", "-9223372036854775809",
|
||||
"1.5", "-2.25e-3", "1e10", "1E+2", "0.000001", "3.141592653589793238462643", "1e308", "-1e-400", "123.456e7"
|
||||
};
|
||||
o += numbers[r(22)];
|
||||
}
|
||||
|
||||
void value(std::string& o, int depth)
|
||||
{
|
||||
ws(o);
|
||||
const int k = depth > 5 ? 2 + r(6) : r(8);
|
||||
if (k == 0 || k == 1)
|
||||
{
|
||||
const bool object = k == 0;
|
||||
o += object ? '{' : '[';
|
||||
for (int i = r(5); i > 0; --i)
|
||||
{
|
||||
ws(o);
|
||||
if (object)
|
||||
{
|
||||
str(o);
|
||||
ws(o);
|
||||
o += ':';
|
||||
}
|
||||
value(o, depth + 1);
|
||||
o += i > 1 ? "," : "";
|
||||
}
|
||||
ws(o);
|
||||
o += object ? '}' : ']';
|
||||
}
|
||||
else if (k < 4)
|
||||
{
|
||||
str(o);
|
||||
}
|
||||
else if (k < 6)
|
||||
{
|
||||
num(o);
|
||||
}
|
||||
else
|
||||
{
|
||||
static const char* const literals[] = {"true", "false", "null"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
o += literals[r(3)];
|
||||
}
|
||||
ws(o);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view string_ref")
|
||||
{
|
||||
// std::string_view in C++17, a stand-in with the same members before
|
||||
using nlohmann::detail::view::string_ref;
|
||||
const std::string text = "abc";
|
||||
const string_ref r(text);
|
||||
CHECK(r.length() == 3);
|
||||
CHECK(std::string(r.begin(), r.end()) == "abc");
|
||||
CHECK(r[1] == 'b');
|
||||
CHECK(r != string_ref("abd"));
|
||||
CHECK_FALSE(r != string_ref("abcd", 3));
|
||||
std::ostringstream o;
|
||||
o << r;
|
||||
CHECK(o.str() == "abc");
|
||||
}
|
||||
|
||||
TEST_CASE("json_view builder")
|
||||
{
|
||||
SECTION("scalars and containers")
|
||||
{
|
||||
for (const char* text :
|
||||
{
|
||||
"null", "true", "false", "0", "-0", "42", "-42", "1.5", "\"\"", "\"abc\"", "[]", "{}", "[1,2,3]", "{\"a\":1,\"b\":[true,null]}", // NOLINT(modernize-raw-string-literal)
|
||||
" [ 1 , 2 ] ", "{\"a\" : {\"b\" : {}}}", "[[[]]]", "\"\\u00e4\\n\\ud83d\\ude00\"", "{\"a\":1,\"a\":2}", "18446744073709551616", // NOLINT(modernize-raw-string-literal)
|
||||
"-9223372036854775809", "123456789012345678901234567890", "1e400", "-1e400", "1.7976931348623157e308"
|
||||
})
|
||||
{
|
||||
check_same(text);
|
||||
}
|
||||
|
||||
// the midpoint between the largest double and 2^1024 rounds to
|
||||
// infinity (an overflow), one less to the largest double: with more
|
||||
// than 19 digits, Eisel-Lemire cannot decide these, and the overflow
|
||||
// check needs the exact comparison with the midpoint
|
||||
const std::string midpoint = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792";
|
||||
const std::string below = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497791";
|
||||
check_same(midpoint);
|
||||
check_same("-" + midpoint);
|
||||
check_same(below);
|
||||
check_same("[" + below + "," + midpoint + "]");
|
||||
|
||||
// the check uses the floating-point type of the document: with float,
|
||||
// the view rejects what parse() rejects (out_of_range.406), and a
|
||||
// double document is not affected
|
||||
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
||||
CHECK_FALSE(float_json::accept("1e39"));
|
||||
CHECK(float_json::accept("3.4028235e38"));
|
||||
CHECK_FALSE(float_json::accept("3.4028236e38"));
|
||||
for (const char* text :
|
||||
{
|
||||
"1e39", "-1e39", "3.4028235e38", "-3.4028235e38", "3.4028236e38", "-3.4028236e38", "3.4028234663852886e38", "1e38",
|
||||
"340282356779733661637539395458142568448", "340282356779733661637539395458142568447.99", "0.00034028236e42",
|
||||
"[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})"
|
||||
})
|
||||
{
|
||||
CAPTURE(text)
|
||||
const bool float_accepted = float_json::accept(text);
|
||||
for (const bool sentinel :
|
||||
{
|
||||
true, false
|
||||
})
|
||||
{
|
||||
const built f = build<float>(text, false, false, sentinel);
|
||||
CHECK(f.ok == float_accepted);
|
||||
if (!f.ok)
|
||||
{
|
||||
CHECK(f.failure.code == nlohmann::detail::view::error_code::number_overflow);
|
||||
}
|
||||
CHECK(build<double>(text, false, false, sentinel).ok == json::accept(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("malformed input")
|
||||
{
|
||||
for (const char* text :
|
||||
{
|
||||
"", " ", "[", "]", "{", "}", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e", "1e+",
|
||||
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\u12G4\"", "\"\\ud800\"", "\"\\udc00\"", "\"\\ud800\\u0041\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", // NOLINT(modernize-raw-string-literal)
|
||||
"\"\xe0\x80\x80\"", "\"\xed\xa0\x80\"", "[1]x", "[1] [2]", "/", "/*", "/* */ 1", "// c\n1", "1 // c", "[1,/*c*/2]", "[1,2,]"
|
||||
})
|
||||
{
|
||||
check_same(text);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("NUL, BOM, and whitespace")
|
||||
{
|
||||
// a NUL inside a string is a control character, as for json::parse
|
||||
// (where a NUL ends the input, it does so only between values)
|
||||
for (const bool sentinel :
|
||||
{
|
||||
true, false
|
||||
})
|
||||
{
|
||||
const built b = build(std::string("[\"ab\0cd\"]", 9), false, false, sentinel);
|
||||
CHECK(!b.ok);
|
||||
CHECK(b.failure.code == nlohmann::detail::view::error_code::string_control_character);
|
||||
CHECK(b.failure.offset == 4);
|
||||
}
|
||||
check_same(std::string("[1]\0garbage", 11));
|
||||
check_same(std::string("[1\0]", 4));
|
||||
check_same(std::string("[1, // c\0\n2]", 12));
|
||||
check_same(std::string("[1, /* c\0 */ 2]", 15));
|
||||
check_same("\xEF\xBB\xBF[1]");
|
||||
check_same("\xEF\xBB[1]");
|
||||
check_same(" \t\r\n 7 \n");
|
||||
for (const char* text :
|
||||
{"[1]\r", "[1]\n", "[1]\r\n", "[1,\r2]", "[1,\r\n2]", "7\r", "\"x\"\r", "{\"a\":\r\n1}\r", "[\n 1,\n 2\n]", "{\n \"a\": [\n 1\n ]\n}"
|
||||
})
|
||||
{
|
||||
check_same(text);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
// the deepest nesting is recorded (materialize() sizes its stack from it)
|
||||
CHECK(build("1", false, false, false).data->max_depth == 0);
|
||||
CHECK(build("[]", false, false, false).data->max_depth == 1);
|
||||
CHECK(build(R"([[1],{"a":[[]]},[]])", false, false, false).data->max_depth == 4);
|
||||
// a stack that turns out too small still grows
|
||||
const std::string nested = R"([[1],{"a":[[{"b":[]}]]},[]])";
|
||||
const built shallow = build(nested, false, false, false);
|
||||
REQUIRE(shallow.ok);
|
||||
shallow.data->max_depth = 1;
|
||||
CHECK(nlohmann::detail::view::materialize<json, false>(*shallow.data, shallow.data->nodes) == json::parse(nested));
|
||||
|
||||
// the open containers beyond 64 levels live on the heap
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
63u, 64u, 65u, 1000u, 100000u
|
||||
})
|
||||
{
|
||||
const std::string arrays = std::string(depth, '[') + std::string(depth, ']');
|
||||
const built b = build(arrays, false, false, false);
|
||||
REQUIRE(b.ok);
|
||||
CHECK(b.data->node_count == depth);
|
||||
CHECK(b.data->nodes[0].next == depth);
|
||||
CHECK(b.data->max_depth == depth);
|
||||
std::string objects;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
objects += "{\"a\":";
|
||||
}
|
||||
objects += '1' + std::string(depth, '}');
|
||||
const built o = build(objects, false, false, false);
|
||||
REQUIRE(o.ok);
|
||||
CHECK(o.data->node_count == (2 * depth) + 1);
|
||||
CHECK(o.data->max_depth == depth);
|
||||
CHECK(!build(std::string(depth, '[') + std::string(depth - 1, ']'), false, false, false).ok);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("generated documents and damaged copies")
|
||||
{
|
||||
generator g;
|
||||
for (int i = 0; i < 3000; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
check_same(text);
|
||||
// damage: flip one byte, or cut the text
|
||||
std::string damaged = text;
|
||||
const auto at = static_cast<std::size_t>(g.r(static_cast<int>(damaged.size())));
|
||||
static const char replacements[] = {'x', '"', '\\', ',', ':', ']', '}', '[', '{', '1', '-', '.', 'e', '\0', '\n', '/'}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
damaged[at] = replacements[g.r(16)];
|
||||
check_same(damaged);
|
||||
check_same(text.substr(0, at));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("test files")
|
||||
{
|
||||
for (const char* name :
|
||||
{
|
||||
"/json.org/1.json", "/json.org/2.json", "/json.org/3.json", "/json.org/4.json", "/json.org/5.json",
|
||||
"/json_testsuite/sample.json", "/nativejson-benchmark/canada.json", "/nativejson-benchmark/citm_catalog.json",
|
||||
"/nativejson-benchmark/twitter.json", "/json_tests/pass1.json", "/json_tests/pass2.json", "/json_tests/pass3.json"
|
||||
})
|
||||
{
|
||||
CAPTURE(name)
|
||||
const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary);
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
const std::string text = ss.str();
|
||||
REQUIRE(!text.empty());
|
||||
const built b = build(text, false, false, true);
|
||||
REQUIRE(b.ok);
|
||||
CHECK(value_of(b) == json::parse(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view builder: strings across vector blocks")
|
||||
{
|
||||
// Strings are scanned 8 or 16 bytes at a time (NEON, SSE2, or SWAR) and
|
||||
// non-ASCII text with the vector UTF-8 check (NEON, SSSE3) or one sequence
|
||||
// at a time. Sequences are placed so that they start at every offset
|
||||
// around the block boundaries of keys (16, 32) and values (8, 24), with
|
||||
// text of several lengths after them.
|
||||
const std::array<std::size_t, 16> prefixes = {{0, 6, 7, 8, 13, 14, 15, 16, 21, 22, 23, 24, 29, 30, 31, 32}};
|
||||
const std::array<std::size_t, 3> suffixes = {{0, 3, 17}};
|
||||
const auto around = [&](const std::string & seq, std::size_t prefix, std::size_t suffix)
|
||||
{
|
||||
return std::string(prefix, 'a') + seq + std::string(suffix, 'b');
|
||||
};
|
||||
|
||||
SECTION("every two-byte sequence")
|
||||
{
|
||||
for (unsigned lead = 0x80; lead <= 0xFF; ++lead)
|
||||
{
|
||||
for (unsigned second = 0; second <= 0xFF; ++second)
|
||||
{
|
||||
if (second == '"' || second == '\\')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::string seq = {static_cast<char>(lead), static_cast<char>(second)};
|
||||
check_string(around(seq, prefixes[(lead + second) % 16], suffixes[second % 3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("three- and four-byte sequences")
|
||||
{
|
||||
const std::array<unsigned, 8> conts = {{0x7F, 0x80, 0x8F, 0x90, 0x9F, 0xA0, 0xBF, 0xC0}};
|
||||
for (unsigned lead = 0xE0; lead <= 0xF7; ++lead)
|
||||
{
|
||||
for (const unsigned b2 : conts)
|
||||
{
|
||||
for (const unsigned b3 : conts)
|
||||
{
|
||||
for (const std::size_t prefix : prefixes)
|
||||
{
|
||||
std::string seq = {static_cast<char>(lead), static_cast<char>(b2), static_cast<char>(b3)};
|
||||
if (lead >= 0xF0)
|
||||
{
|
||||
seq += static_cast<char>(prefix % 2 == 0 ? 0x80 : 0xBF);
|
||||
}
|
||||
check_string(around(seq, prefix, suffixes[prefix % 3]));
|
||||
// cut short before the end of the string
|
||||
check_string(around(seq.substr(0, seq.size() - 1), prefix, suffixes[prefix % 3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("long runs of text with one damaged byte")
|
||||
{
|
||||
const std::array<const char*, 7> chars = {{"a", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\xed\x9f\xbf", "\xef\xbf\xbf", "\xf4\x8f\xbf\xbf"}};
|
||||
const std::array<char, 11> damage = {{'\x80', '\xbf', '\xc0', '\xc1', '\xe0', '\xed', '\xf5', '\xff', '\x1f', '"', '\\'}};
|
||||
std::mt19937 rng(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
for (int i = 0; i < 4000; ++i)
|
||||
{
|
||||
std::string text;
|
||||
const auto n = rng() % 60;
|
||||
for (unsigned k = 0; k < n; ++k)
|
||||
{
|
||||
text += chars[rng() % chars.size()];
|
||||
}
|
||||
check_string(text);
|
||||
if (!text.empty())
|
||||
{
|
||||
text[rng() % text.size()] = damage[rng() % damage.size()];
|
||||
check_string(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view node integer bits")
|
||||
{
|
||||
using nlohmann::detail::view::integer_bits;
|
||||
using nlohmann::detail::view::set_integer_bits;
|
||||
|
||||
// an integer lives in len (low half) and next (high half), on any byte
|
||||
// order; a big-endian target must not store the native word over both
|
||||
SECTION("set_integer_bits and integer_bits")
|
||||
{
|
||||
node n = {};
|
||||
for (const std::uint64_t v :
|
||||
{
|
||||
std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{0xFFFFFFFFu}, std::uint64_t{0x100000000u},
|
||||
std::uint64_t{0x0000000200000003u}, std::uint64_t{0x0123456789ABCDEFu}, std::uint64_t{0xFFFFFFFFFFFFFFFEu}
|
||||
})
|
||||
{
|
||||
CAPTURE(v)
|
||||
n.kind = 0x5A;
|
||||
n.flags = 0xA5;
|
||||
n.extra = 0x1234;
|
||||
n.off = 0x89ABCDEFu;
|
||||
set_integer_bits(n, v);
|
||||
CHECK(integer_bits(n) == v);
|
||||
CHECK(n.len == static_cast<std::uint32_t>(v));
|
||||
CHECK(n.next == static_cast<std::uint32_t>(v >> 32));
|
||||
// the other fields are untouched
|
||||
CHECK(n.kind == 0x5A);
|
||||
CHECK(n.flags == 0xA5);
|
||||
CHECK(n.extra == 0x1234);
|
||||
CHECK(n.off == 0x89ABCDEFu);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("parsed integers")
|
||||
{
|
||||
struct integer_case
|
||||
{
|
||||
const char* text;
|
||||
std::uint64_t bits;
|
||||
};
|
||||
for (const integer_case c :
|
||||
{
|
||||
integer_case{"[8589934595]", 0x0000000200000003u}, integer_case{"[4294967296]", 0x100000000u}, integer_case{"[4294967295]", 0xFFFFFFFFu},
|
||||
integer_case{"[-2]", 0xFFFFFFFFFFFFFFFEu}, integer_case{"[-4294967297]", 0xFFFFFFFEFFFFFFFFu}, integer_case{"[18446744073709551615]", 0xFFFFFFFFFFFFFFFFu},
|
||||
integer_case{"[7]", 7u}
|
||||
})
|
||||
{
|
||||
CAPTURE(c.text)
|
||||
const built b = build(c.text, false, false, true);
|
||||
REQUIRE(b.ok);
|
||||
const node& n = b.data->nodes[1];
|
||||
CHECK(integer_bits(n) == c.bits);
|
||||
CHECK(n.len == static_cast<std::uint32_t>(c.bits));
|
||||
CHECK(n.next == static_cast<std::uint32_t>(c.bits >> 32));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view node array size limit")
|
||||
{
|
||||
// a node array larger than the address space is refused, not wrapped to a
|
||||
// small allocation (the size computation overflows on 32-bit targets, and
|
||||
// for absurd counts everywhere)
|
||||
std::unique_ptr<document_data, document_data::deleter> d(document_data::create(0));
|
||||
d->reserve(8);
|
||||
REQUIRE(d->node_capacity >= 8);
|
||||
d->node_count = 2;
|
||||
const std::size_t cap = d->node_capacity;
|
||||
node* const nodes = d->nodes;
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
const std::size_t too_many = document_data::max_nodes() + 1;
|
||||
CHECK_THROWS_AS(d->reserve(too_many), std::bad_alloc&);
|
||||
CHECK_THROWS_AS(d->reserve((std::numeric_limits<std::size_t>::max)()), std::bad_alloc&);
|
||||
// the array is unchanged
|
||||
CHECK(d->nodes == nodes);
|
||||
CHECK(d->node_capacity == cap);
|
||||
CHECK(d->node_count == 2);
|
||||
#endif
|
||||
|
||||
// the largest count that fits is not refused by the check (nothing is
|
||||
// allocated for a count that is already there)
|
||||
d->reserve(cap);
|
||||
CHECK(d->nodes == nodes);
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
using json_view_test::generator;
|
||||
using json_view_test::has_duplicate_keys;
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view.cpp, whose object file got too large
|
||||
// for the MinGW linker (see json_view_test_helpers.hpp). Like that file, this one
|
||||
// is also built with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view dump")
|
||||
{
|
||||
SECTION("the output of ordered_json::dump()")
|
||||
{
|
||||
generator g;
|
||||
for (int i = 0; i < 2000; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
if (has_duplicate_keys(d.root()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
CAPTURE(text)
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
-1, 0, 2
|
||||
})
|
||||
{
|
||||
for (const bool ensure_ascii :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii) == j.dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii));
|
||||
}
|
||||
}
|
||||
// also of each element
|
||||
for (const ordered_json_view e : d.root())
|
||||
{
|
||||
CHECK(e.dump() == e.materialize().dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("strings")
|
||||
{
|
||||
const std::string text = R"(["plain", "\u0000\u0001\u001f\u007f\u0080é€😀", "\"\\\/\b\f\n\r\t", "aéあ😀b", "long text beyond the eight bytes of a word \n with an escape in the middle"])";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
const std::string key_text = R"({"é\n": {"\"": [], "": {}}})";
|
||||
const ordered_json_document keys = ordered_json_document::parse(key_text);
|
||||
const ordered_json key_json = ordered_json::parse(key_text);
|
||||
CHECK(keys.root().dump(2, ' ', true) == key_json.dump(2, ' ', true));
|
||||
}
|
||||
|
||||
SECTION("numbers")
|
||||
{
|
||||
const std::string text = "[1.50, 1E2, -0, -0.0, 123456789012345678901234567890, 18446744073709551615, -9223372036854775808, 0.1, 1e-7, 5e-324]";
|
||||
const json_document d = json_document::parse(text);
|
||||
CHECK(d.root().dump() == json::parse(text).dump());
|
||||
CHECK(d.root().dump() == "[1.5,100.0,0,-0.0,1.2345678901234568e+29,18446744073709551615,-9223372036854775808,0.1,1e-07,5e-324]");
|
||||
CHECK(d.root().dump(-1, ' ', false, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
|
||||
// also indented, and with ensure_ascii
|
||||
CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]");
|
||||
CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
|
||||
|
||||
// float tokens of up to 17 significant digits in every spelling: those
|
||||
// of at most 15 digits are written from their digits, the others
|
||||
// through the conversion; both as dump() writes them
|
||||
{
|
||||
std::mt19937_64 tokens(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
// a number below n; the remainder is a std::uint64_t, which is
|
||||
// std::size_t on some platforms and wider on others
|
||||
const auto draw = [&tokens](std::size_t n)
|
||||
{
|
||||
const std::uint64_t r = tokens() % n;
|
||||
return static_cast<std::size_t>(r);
|
||||
};
|
||||
std::string many_tokens = "[";
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
const std::size_t length = 1 + draw(17);
|
||||
std::string digits(1, static_cast<char>('1' + draw(9)));
|
||||
for (std::size_t k = 1; k < length; ++k)
|
||||
{
|
||||
digits += static_cast<char>('0' + draw(10));
|
||||
}
|
||||
digits += std::string(draw(4), '0'); // trailing zeros
|
||||
std::string token = draw(3) == 0 ? "-" : "";
|
||||
const std::size_t point = draw(digits.size() + 1);
|
||||
if (point == 0)
|
||||
{
|
||||
token += "0." + std::string(draw(5), '0') + digits;
|
||||
}
|
||||
else
|
||||
{
|
||||
token += digits.substr(0, point) + (point < digits.size() ? "." + digits.substr(point) : "");
|
||||
}
|
||||
// an exponent that keeps the value between about 1e-320 and 1e300
|
||||
const int exponent = static_cast<int>(draw(600)) - 300 - static_cast<int>(point);
|
||||
if (draw(4) != 0)
|
||||
{
|
||||
token += (draw(2) == 0 ? "e" : "E") + std::string(exponent >= 0 && draw(2) == 0 ? "+" : "") + std::to_string(exponent);
|
||||
}
|
||||
else if (point == digits.size())
|
||||
{
|
||||
token += ".0"; // (a float, not an integer)
|
||||
}
|
||||
many_tokens += (i != 0 ? "," : "") + token;
|
||||
}
|
||||
many_tokens += ']';
|
||||
const json_document many_doc = json_document::parse(many_tokens);
|
||||
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
|
||||
}
|
||||
|
||||
// random doubles, written as parse() and dump() would
|
||||
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::string many = "[";
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
const std::uint64_t bits = rng();
|
||||
double x = 0;
|
||||
std::memcpy(&x, &bits, sizeof(x));
|
||||
if (std::isfinite(x))
|
||||
{
|
||||
many += (many.size() > 1 ? "," : "") + json(x).dump();
|
||||
}
|
||||
}
|
||||
many += ']';
|
||||
const json_document many_document = json_document::parse(many);
|
||||
CHECK(many_document.root().dump() == json::parse(many).dump());
|
||||
|
||||
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
||||
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
|
||||
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
|
||||
}
|
||||
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
std::ostringstream compact;
|
||||
compact << d.root();
|
||||
CHECK(compact.str() == R"({"a":[1,2]})");
|
||||
std::ostringstream pretty;
|
||||
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
||||
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
||||
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,834 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_editable_document;
|
||||
using nlohmann::json_editable_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::ordered_json_editable_view;
|
||||
using ptr_t = ordered_json::json_pointer;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
using json_view_test::exception_of_call;
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::uint32_t rng()
|
||||
{
|
||||
static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
// result_type is std::uint_fast32_t, which may be wider than 32 bits
|
||||
const std::mt19937::result_type value = generator();
|
||||
return static_cast<std::uint32_t>(value);
|
||||
}
|
||||
|
||||
int r(int n)
|
||||
{
|
||||
return static_cast<int>(rng() % static_cast<unsigned>(n));
|
||||
}
|
||||
|
||||
int counter = 0;
|
||||
|
||||
std::uint64_t bits(double x)
|
||||
{
|
||||
std::uint64_t b = 0;
|
||||
std::memcpy(&b, &x, sizeof(b));
|
||||
return b;
|
||||
}
|
||||
|
||||
std::string random_string()
|
||||
{
|
||||
static const std::array<const char*, 15> pieces = {{"a", "Z", " ", "~", "\n", "\"", "\\", "/", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\x1f", "0", "key"}};
|
||||
std::string s;
|
||||
for (int i = r(3) == 0 ? r(30) : r(6); i > 0; --i)
|
||||
{
|
||||
s += pieces[static_cast<std::size_t>(r(15))];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
ordered_json random_scalar()
|
||||
{
|
||||
switch (r(9))
|
||||
{
|
||||
case 0:
|
||||
return nullptr;
|
||||
case 1:
|
||||
return r(2) == 0;
|
||||
case 2:
|
||||
return static_cast<std::int64_t>(rng()) - 2147483648LL;
|
||||
case 3:
|
||||
return (static_cast<std::uint64_t>(rng()) * 4294967296ULL) + rng();
|
||||
case 4:
|
||||
return static_cast<double>(static_cast<std::int32_t>(rng())) / (1 + r(1000));
|
||||
case 5:
|
||||
return r(2) == 0 ? 1e300 * (r(2) == 0 ? 1 : -1) : 5e-324;
|
||||
case 6:
|
||||
return -0.0;
|
||||
default:
|
||||
return random_string();
|
||||
}
|
||||
}
|
||||
|
||||
ordered_json random_value(int depth)
|
||||
{
|
||||
const int k = depth > 3 ? 4 : r(7);
|
||||
if (k == 0)
|
||||
{
|
||||
ordered_json o = ordered_json::object();
|
||||
for (int i = r(5); i > 0; --i)
|
||||
{
|
||||
o[random_string() + "#" + std::to_string(counter++)] = random_value(depth + 1);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
if (k == 1)
|
||||
{
|
||||
ordered_json a = ordered_json::array();
|
||||
for (int i = r(5); i > 0; --i)
|
||||
{
|
||||
a.push_back(random_value(depth + 1));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return random_scalar();
|
||||
}
|
||||
|
||||
void collect(const ordered_json& j, const ptr_t& p, std::vector<ptr_t>& out)
|
||||
{
|
||||
out.push_back(p);
|
||||
if (j.is_object())
|
||||
{
|
||||
for (const auto& kv : j.items())
|
||||
{
|
||||
collect(kv.value(), p / kv.key(), out);
|
||||
}
|
||||
}
|
||||
else if (j.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < j.size(); ++i)
|
||||
{
|
||||
collect(j[i], p / i, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// the edited view and the ordered_json value read the same through the whole
|
||||
// read API
|
||||
void compare(const ordered_json_editable_view& v, const ordered_json& j)
|
||||
{
|
||||
REQUIRE(v.type() == j.type());
|
||||
REQUIRE(v.size() == j.size());
|
||||
if (j.is_object())
|
||||
{
|
||||
auto jt = j.begin();
|
||||
for (auto it = v.begin(); it != v.end(); ++it, ++jt)
|
||||
{
|
||||
CHECK(std::string(it.key().data(), it.key().size()) == jt.key());
|
||||
CHECK(v[jt.key()].dump() == jt.value().dump());
|
||||
CHECK(v.contains(jt.key()));
|
||||
compare(*it, jt.value());
|
||||
}
|
||||
}
|
||||
else if (j.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < j.size(); ++i)
|
||||
{
|
||||
CHECK(v[i].dump() == j[i].dump());
|
||||
}
|
||||
std::size_t i = 0;
|
||||
for (const auto e : v)
|
||||
{
|
||||
compare(e, j[i++]);
|
||||
}
|
||||
if (!j.empty())
|
||||
{
|
||||
CHECK(v.back().dump() == j.back().dump());
|
||||
}
|
||||
}
|
||||
else if (j.is_string())
|
||||
{
|
||||
CHECK(v.get<std::string>() == j.get<std::string>());
|
||||
}
|
||||
else if (j.is_number_float())
|
||||
{
|
||||
CHECK(bits(v.get<double>()) == bits(j.get<double>()));
|
||||
}
|
||||
else if (j.is_number_integer())
|
||||
{
|
||||
CHECK(v.get<std::int64_t>() == j.get<std::int64_t>());
|
||||
CHECK(v.get<std::uint64_t>() == j.get<std::uint64_t>());
|
||||
}
|
||||
}
|
||||
|
||||
// the text of j, in which some objects repeat a key of theirs after their
|
||||
// members: lookups of the edited document find the first member of a key, so
|
||||
// j (the first members) is what they must agree with, while parse() would
|
||||
// keep the last value
|
||||
std::string text_with_duplicates(const ordered_json& j)
|
||||
{
|
||||
if (j.is_object())
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (const auto& kv : j.items())
|
||||
{
|
||||
keys.push_back(kv.key());
|
||||
}
|
||||
const auto repeated = [&keys]()
|
||||
{
|
||||
return ordered_json(keys[static_cast<std::size_t>(r(static_cast<int>(keys.size())))]).dump() + ":" + random_value(2).dump();
|
||||
};
|
||||
std::string text = "{";
|
||||
bool first = true;
|
||||
for (const auto& kv : j.items())
|
||||
{
|
||||
text += (first ? "" : ",") + ordered_json(kv.key()).dump() + ":" + text_with_duplicates(kv.value());
|
||||
first = false;
|
||||
}
|
||||
for (int i = keys.empty() ? 0 : r(3); i > 0; --i)
|
||||
{
|
||||
text += "," + repeated();
|
||||
}
|
||||
return text + "}";
|
||||
}
|
||||
if (j.is_array())
|
||||
{
|
||||
std::string text = "[";
|
||||
for (std::size_t i = 0; i < j.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? "," : "") + text_with_duplicates(j[i]);
|
||||
}
|
||||
return text + "]";
|
||||
}
|
||||
return j.dump();
|
||||
}
|
||||
|
||||
// the number of members of an object view with this key, by iteration
|
||||
std::size_t occurrences(const ordered_json_editable_view& v, const std::string& key)
|
||||
{
|
||||
std::size_t result = 0;
|
||||
for (const auto& item : v.items())
|
||||
{
|
||||
result += std::string(item.key().data(), item.key().size()) == key ? 1u : 0u;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// every lookup of the edited view finds the first member of its key, which
|
||||
// the iteration of the view shows
|
||||
void check_lookups(const ordered_json_editable_view& v)
|
||||
{
|
||||
if (v.is_object())
|
||||
{
|
||||
std::set<std::string> seen;
|
||||
for (const auto item : v.items())
|
||||
{
|
||||
const std::string key(item.key().data(), item.key().size());
|
||||
if (!seen.insert(key).second)
|
||||
{
|
||||
continue; // (not the first member of the key)
|
||||
}
|
||||
const ptr_t ptr = ptr_t() / key;
|
||||
CAPTURE(key)
|
||||
const ordered_json expected = item.value().materialize();
|
||||
const ordered_json_editable_view m = v[key];
|
||||
REQUIRE(!m.is_discarded());
|
||||
CHECK(m.materialize() == expected);
|
||||
CHECK(v.at(key).materialize() == expected);
|
||||
CHECK(v[ptr].materialize() == expected);
|
||||
CHECK(v.at(ptr).materialize() == expected);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.contains(ptr));
|
||||
CHECK(v.count(key) == occurrences(v, key));
|
||||
const auto it = v.find(key);
|
||||
REQUIRE(it != v.end());
|
||||
CHECK((*it).materialize() == expected);
|
||||
CHECK(v.value(ptr, ordered_json(nullptr)) == expected);
|
||||
if (expected.is_string())
|
||||
{
|
||||
CHECK(v.value(key, std::string("-")) == expected.get<std::string>());
|
||||
}
|
||||
check_lookups(m);
|
||||
}
|
||||
CHECK(v["missing#key"].is_discarded());
|
||||
CHECK(!v.contains("missing#key"));
|
||||
CHECK(v.find("missing#key") == v.end());
|
||||
}
|
||||
else if (v.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < v.size(); ++i) // NOLINT(modernize-loop-convert): both v[i] and v.at(i) are tested
|
||||
{
|
||||
check_lookups(v[i]);
|
||||
check_lookups(v.at(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// every lookup of the edited view finds what the first members of j hold
|
||||
void check_first_members(const ordered_json_editable_view& v, const ordered_json& j)
|
||||
{
|
||||
REQUIRE(v.type() == j.type());
|
||||
if (j.is_object())
|
||||
{
|
||||
for (const auto& kv : j.items())
|
||||
{
|
||||
const std::string& key = kv.key();
|
||||
const ptr_t ptr = ptr_t() / key;
|
||||
CAPTURE(key)
|
||||
REQUIRE(v.contains(key));
|
||||
CHECK(v.contains(ptr));
|
||||
CHECK(v.count(key) == occurrences(v, key));
|
||||
CHECK(v.find(key) != v.end());
|
||||
CHECK(v.at(ptr).type() == kv.value().type());
|
||||
check_first_members(v[key], kv.value());
|
||||
check_first_members(v.at(key), kv.value());
|
||||
check_first_members(v[ptr], kv.value());
|
||||
}
|
||||
CHECK(!v.contains("missing#key"));
|
||||
}
|
||||
else if (j.is_array())
|
||||
{
|
||||
REQUIRE(v.size() == j.size());
|
||||
for (std::size_t i = 0; i < j.size(); ++i)
|
||||
{
|
||||
check_first_members(v[i], j[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
}
|
||||
|
||||
// (for documents whose objects repeat keys: j holds the first members, the
|
||||
// lookups must find them; dump(), size(), and iteration list all members, but
|
||||
// materialize() keeps the last value of a key, as parse() does of the dump)
|
||||
void check_duplicates(const ordered_json_editable_document& d, const ordered_json& j)
|
||||
{
|
||||
check_first_members(d.root(), j);
|
||||
check_lookups(d.root());
|
||||
CHECK(d.root().materialize() == ordered_json::parse(d.root().dump()));
|
||||
}
|
||||
|
||||
void check_all(const ordered_json_editable_document& d, const ordered_json& j, bool deep)
|
||||
{
|
||||
const std::string text = d.root().dump();
|
||||
REQUIRE(text == j.dump());
|
||||
CHECK(d.root().materialize() == j);
|
||||
if (deep)
|
||||
{
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root() == j);
|
||||
compare(d.root(), j);
|
||||
// a fresh document of the text reads the same
|
||||
const ordered_json_document fresh = ordered_json_document::parse(text);
|
||||
CHECK(fresh.root() == d.root());
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view edits: differential")
|
||||
{
|
||||
// random edits are applied to an ordered_json_editable_document and to the
|
||||
// ordered_json parse() produces; after every edit both must serialize,
|
||||
// materialize, and read back the same
|
||||
for (int n = 0; n < 250; ++n)
|
||||
{
|
||||
// the last 100 documents repeat keys in some of their objects; j then
|
||||
// holds the first member of each key, which the edits address
|
||||
const bool duplicates = n >= 150;
|
||||
ordered_json j = random_value(0);
|
||||
if (r(4) == 0)
|
||||
{
|
||||
j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}});
|
||||
}
|
||||
if (duplicates)
|
||||
{
|
||||
j = ordered_json::parse(j.dump()); // (normalized as the document reads it)
|
||||
}
|
||||
const std::string text = duplicates ? text_with_duplicates(j) : j.dump(r(2) == 0 ? -1 : 2);
|
||||
CAPTURE(text)
|
||||
ordered_json_editable_document d = ordered_json_editable_document::parse(text);
|
||||
if (!duplicates)
|
||||
{
|
||||
j = ordered_json::parse(text);
|
||||
}
|
||||
const ordered_json_document other = ordered_json_document::parse(random_value(0).dump());
|
||||
const int edits = 30;
|
||||
for (int e = 0; e < edits; ++e)
|
||||
{
|
||||
std::vector<ptr_t> paths;
|
||||
collect(j, ptr_t(), paths);
|
||||
const ptr_t p = paths[static_cast<std::size_t>(r(static_cast<int>(paths.size())))];
|
||||
const ordered_json& target = j[p];
|
||||
const ordered_json_editable_view tv = d.root().at(p);
|
||||
const int op = r(12);
|
||||
{
|
||||
if (op == 0) // assign a scalar
|
||||
{
|
||||
const ordered_json v = random_scalar();
|
||||
if (v.is_string() && r(2) == 0)
|
||||
{
|
||||
d.set(tv, v.get<std::string>());
|
||||
}
|
||||
else if (v.is_number_unsigned() && r(2) == 0)
|
||||
{
|
||||
d.set(tv, v.get<std::uint64_t>());
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(tv, v);
|
||||
}
|
||||
j[p] = v;
|
||||
}
|
||||
else if (op == 1) // assign a new array/object (or anything), sometimes via the pointer API
|
||||
{
|
||||
const ordered_json v = random_value(2);
|
||||
if (r(2) == 0)
|
||||
{
|
||||
d.set(p, v);
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(tv, v);
|
||||
}
|
||||
j[p] = v;
|
||||
}
|
||||
else if (op == 2) // copy a value of the same document
|
||||
{
|
||||
const ptr_t& q = paths[static_cast<std::size_t>(r(static_cast<int>(paths.size())))];
|
||||
const ordered_json v = j[q];
|
||||
d.set(tv, d.root().at(q));
|
||||
j[p] = v;
|
||||
}
|
||||
else if (op == 3) // copy a value of another document
|
||||
{
|
||||
d.set(tv, other.root());
|
||||
j[p] = other.root().materialize();
|
||||
}
|
||||
else if ((op == 4 || op == 5) && target.is_object()) // set a member (new or existing)
|
||||
{
|
||||
std::string key = random_string() + "#" + std::to_string(counter++);
|
||||
if (op == 5 && !target.empty())
|
||||
{
|
||||
key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
|
||||
}
|
||||
const ordered_json v = random_value(2);
|
||||
d.set(tv, key, v);
|
||||
j[p][key] = v;
|
||||
}
|
||||
else if (op == 6 && target.is_object() && !target.empty()) // erase a member
|
||||
{
|
||||
const std::string key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
|
||||
if (r(2) == 0)
|
||||
{
|
||||
d.erase(tv, key);
|
||||
}
|
||||
else
|
||||
{
|
||||
d.erase(p / key);
|
||||
}
|
||||
j[p].erase(key);
|
||||
}
|
||||
else if (op == 7 && (target.is_array() || target.is_null())) // push_back
|
||||
{
|
||||
const ordered_json v = random_value(2);
|
||||
d.push_back(tv, v);
|
||||
j[p].push_back(v);
|
||||
}
|
||||
else if (op == 8 && target.is_array()) // insert
|
||||
{
|
||||
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size()) + 1));
|
||||
const ordered_json v = random_value(2);
|
||||
d.insert(tv, i, v);
|
||||
j[p].insert(j[p].begin() + static_cast<std::ptrdiff_t>(i), v);
|
||||
}
|
||||
else if (op == 9 && target.is_array() && !target.empty()) // erase an element
|
||||
{
|
||||
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
|
||||
if (r(2) == 0)
|
||||
{
|
||||
d.erase(tv, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
d.erase(p / i);
|
||||
}
|
||||
j[p].erase(i);
|
||||
}
|
||||
else if (op == 10 && target.is_array() && !target.empty()) // assign an element
|
||||
{
|
||||
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
|
||||
const ordered_json v = random_value(2);
|
||||
d.set(tv, i, v);
|
||||
j[p][i] = v;
|
||||
}
|
||||
else if (op == 11) // a held view sees the assignment
|
||||
{
|
||||
const ordered_json v = random_value(2);
|
||||
const ordered_json_editable_view held = d.root().at(p);
|
||||
d.set(p, v);
|
||||
j[p] = v;
|
||||
CHECK(held.dump() == v.dump());
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
CAPTURE(p.to_string())
|
||||
CAPTURE(op)
|
||||
if (duplicates)
|
||||
{
|
||||
check_duplicates(d, j);
|
||||
}
|
||||
else
|
||||
{
|
||||
check_all(d, j, e % 8 == 7 || e == edits - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view edits: errors")
|
||||
{
|
||||
SECTION("an empty document")
|
||||
{
|
||||
json_editable_document d;
|
||||
CHECK_THROWS_WITH_AS(d.set(d.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
||||
}
|
||||
|
||||
json_editable_document d = json_editable_document::parse(R"({"o": {"a": 1}, "a": [1, 2], "n": 1, "z": null})");
|
||||
const json_editable_view root = d.root();
|
||||
const json_document other = json_document::parse("[1]");
|
||||
json_editable_document other_editable = json_editable_document::parse("[1]");
|
||||
|
||||
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// invalid UTF-8 is rejected when it enters the document, with the error
|
||||
// basic_json::dump() reports for the same string
|
||||
for (const std::string bad :
|
||||
{"\xC3\x28", "a\xE2\x28\xA1", "\xF0\x28\x8C\xBC", "\xE2\x82", "x\xF0\x9F\x98", "\xFF", "\xED\xA0\x80", "\xC0\xAF"
|
||||
})
|
||||
{
|
||||
CAPTURE(bad)
|
||||
const std::string expected = exception_of_call([&]
|
||||
{
|
||||
const std::string text = json(bad).dump();
|
||||
static_cast<void>(text);
|
||||
});
|
||||
CHECK(!expected.empty());
|
||||
CHECK(exception_of_call([&] { d.set(root["z"], bad); }) == expected);
|
||||
CHECK(exception_of_call([&] { d.set(root["o"], bad, 1); }) == expected);
|
||||
CHECK(exception_of_call([&] { d.set(root["z"], json{{"k", bad}}); }) == expected);
|
||||
}
|
||||
#endif
|
||||
// nothing of the failed edits is visible
|
||||
CHECK(root.dump() == R"({"o":{"a":1},"a":[1,2],"n":1,"z":null})");
|
||||
static_cast<void>(other);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view edits: the size of a text arena")
|
||||
{
|
||||
using nlohmann::detail::view::text_capacity;
|
||||
constexpr std::size_t limit = 0xFFFFFFFFu;
|
||||
// grows by doubling, or to what is needed (plus some room)
|
||||
CHECK(text_capacity(0, 0, 10) == 266);
|
||||
CHECK(text_capacity(1000, 990, 20) == 2000);
|
||||
CHECK(text_capacity(100, 100, 5000) == 5356);
|
||||
// an arena beyond 2 GiB: doubling is clamped to 4 GiB - 1
|
||||
CHECK(text_capacity(0x90000000u, 0x8FFFFFFFu, 2) == limit);
|
||||
CHECK(text_capacity(limit, limit - 10, 10) == limit);
|
||||
// exactly what fits is accepted, without room to spare
|
||||
CHECK(text_capacity(100, 90, limit - 90) == limit);
|
||||
CHECK(text_capacity(limit - 100, limit - 100, 100) == limit);
|
||||
// what does not fit is an error
|
||||
CHECK_THROWS_WITH_AS(text_capacity(100, 90, limit - 89), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(text_capacity(limit, limit, 1), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&);
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("json_view edits: the chunks of new nodes")
|
||||
{
|
||||
using nlohmann::detail::view::alloc_nodes;
|
||||
using nlohmann::detail::view::document_data;
|
||||
const std::unique_ptr<document_data, document_data::deleter> d(document_data::create(0));
|
||||
// the first chunk has room for 64 nodes, the next one for 128
|
||||
const nlohmann::detail::view::node* const first = alloc_nodes(*d, 10);
|
||||
CHECK(d->edits->chunks.size() == 1);
|
||||
// a request of half of the next chunk (128 nodes) or more gets a chunk of
|
||||
// its own
|
||||
const nlohmann::detail::view::node* const large = alloc_nodes(*d, 100);
|
||||
CHECK(d->edits->chunks.size() == 2);
|
||||
CHECK(large == d->edits->chunks.back().get());
|
||||
// and the rest of the first chunk is still used
|
||||
CHECK(alloc_nodes(*d, 50) == first + 10);
|
||||
CHECK(d->edits->chunks.size() == 2);
|
||||
// a smaller request that does not fit starts a new (doubled) chunk; the
|
||||
// call is a statement of its own, as the operands of == are unsequenced
|
||||
// and MSVC reads chunks.back() before the call adds the chunk
|
||||
const nlohmann::detail::view::node* const doubled = alloc_nodes(*d, 10);
|
||||
CHECK(doubled == d->edits->chunks.back().get());
|
||||
CHECK(d->edits->chunks.size() == 3);
|
||||
CHECK(d->edits->chunk_end - d->edits->chunk_cur == 128 - 10);
|
||||
CHECK(d->edits->bytes == (64 + 100 + 128) * sizeof(nlohmann::detail::view::node));
|
||||
}
|
||||
|
||||
TEST_CASE("json_view edits: replacing arrays and objects by scalars")
|
||||
{
|
||||
// the first assignment switches the parent to links; the later ones do
|
||||
// not need to look for the parent again
|
||||
std::string text = "[";
|
||||
json expected = json::array();
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",[" : "[") + std::to_string(i) + ",{\"k\":" + std::to_string(i) + "}]";
|
||||
expected.push_back(json::array({i, json{{"k", i}}}));
|
||||
}
|
||||
text += ']';
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
CHECK(d.root().dump() == expected.dump());
|
||||
for (int i = 0; i < 300; i += 2)
|
||||
{
|
||||
d.set(d.root()[static_cast<std::size_t>(i)], i);
|
||||
expected[static_cast<std::size_t>(i)] = i;
|
||||
}
|
||||
CHECK(d.root().dump() == expected.dump());
|
||||
for (int i = 1; i < 300; i += 2) // (elements that are still in the parsed layout of their parent)
|
||||
{
|
||||
d.set(d.root()[static_cast<std::size_t>(i)][1], "x"); // replaces an object
|
||||
expected[static_cast<std::size_t>(i)][1] = "x";
|
||||
}
|
||||
CHECK(d.root().dump() == expected.dump());
|
||||
|
||||
// new values, and values in new values
|
||||
d.push_back(d.root(), json::parse(R"([[1,2],{"a":[3]}])"));
|
||||
expected.push_back(json::parse(R"([[1,2],{"a":[3]}])"));
|
||||
d.set(d.root()[300][0], 7);
|
||||
expected[300][0] = 7;
|
||||
d.insert(d.root(), 0, json::array({1, 2}));
|
||||
expected.insert(expected.begin(), json::array({1, 2}));
|
||||
d.set(d.root()[0], nullptr);
|
||||
expected[0] = nullptr;
|
||||
d.set(d.root()[301], 5);
|
||||
expected[301] = 5;
|
||||
CHECK(d.root().dump() == expected.dump());
|
||||
CHECK(d.root().materialize() == expected);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// reads of "a" in an object that repeats it: the first member wins, however the
|
||||
// object is stored
|
||||
template<typename View>
|
||||
void check_first_wins(const View& o, int expected)
|
||||
{
|
||||
CAPTURE(expected)
|
||||
CHECK(o["a"].template get<int>() == expected);
|
||||
CHECK(o.at("a").template get<int>() == expected);
|
||||
CHECK(o.find("a")->template get<int>() == expected);
|
||||
CHECK(o.value("a", -1) == expected);
|
||||
CHECK(o[json::json_pointer("/a")].template get<int>() == expected);
|
||||
CHECK(o.at(json::json_pointer("/a")).template get<int>() == expected);
|
||||
CHECK(o.value(json::json_pointer("/a"), -1) == expected);
|
||||
CHECK(o.contains("a"));
|
||||
CHECK(o.contains(json::json_pointer("/a")));
|
||||
std::size_t members_a = 0;
|
||||
for (const auto& item : o.items())
|
||||
{
|
||||
members_a += std::string(item.key().data(), item.key().size()) == "a" ? 1u : 0u;
|
||||
}
|
||||
CHECK(o.count("a") == members_a); // every member, unlike the lookups
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view edits: duplicate keys")
|
||||
{
|
||||
SECTION("an object in its parsed layout, then moved by edits")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
check_first_wins(d.root(), 1);
|
||||
CHECK(d.root().size() == 3);
|
||||
CHECK(d.root().dump() == R"({"a":1,"b":2,"a":3})");
|
||||
CHECK(d.root().materialize()["a"] == 3); // (parse() keeps the last value)
|
||||
|
||||
// an edit of a value does not move the object
|
||||
d.set(d.root()["b"], 5);
|
||||
check_first_wins(d.root(), 1);
|
||||
d.set(d.root()["a"], 4); // the member that lookups find
|
||||
check_first_wins(d.root(), 4);
|
||||
CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3})");
|
||||
|
||||
// an appended member moves it
|
||||
d.set(d.root(), "c", true);
|
||||
check_first_wins(d.root(), 4);
|
||||
CHECK(d.root().size() == 4);
|
||||
CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3,"c":true})");
|
||||
d.set(d.root()["a"], 6);
|
||||
check_first_wins(d.root(), 6);
|
||||
CHECK(d.root().dump() == R"({"a":6,"b":5,"a":3,"c":true})");
|
||||
d.set(json::json_pointer("/a"), 7);
|
||||
check_first_wins(d.root(), 7);
|
||||
}
|
||||
|
||||
SECTION("set assigns the member that lookups find")
|
||||
{
|
||||
// the key stays where its first occurrence is, the later members are dropped
|
||||
ordered_json_editable_document d = ordered_json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})");
|
||||
const ordered_json_editable_view held = d.root()["a"];
|
||||
CHECK(held.get<int>() == 1);
|
||||
const ordered_json_editable_view assigned = d.set(d.root(), "a", "x");
|
||||
CHECK(held.get<std::string>() == "x");
|
||||
CHECK(assigned.get<std::string>() == "x");
|
||||
CHECK(d.root().dump() == R"({"a":"x","b":2,"c":4})");
|
||||
CHECK(d.root().materialize() == ordered_json::parse(R"({"a": "x", "b": 2, "c": 4})"));
|
||||
CHECK(d.root().size() == 3);
|
||||
|
||||
// as for the object that parse() makes of the text
|
||||
ordered_json j = ordered_json::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})");
|
||||
j["a"] = "x";
|
||||
CHECK(d.root().materialize().dump() == j.dump());
|
||||
|
||||
// through a pointer, below a duplicate
|
||||
d = ordered_json_editable_document::parse(R"({"a": {"x": 1}, "b": 2, "a": {"x": 3}})");
|
||||
d.set(ptr_t("/a/x"), 4);
|
||||
CHECK(d.root().dump() == R"({"a":{"x":4},"b":2,"a":{"x":3}})");
|
||||
d.set(ptr_t("/a"), 0);
|
||||
CHECK(d.root().dump() == R"({"a":0,"b":2})");
|
||||
}
|
||||
|
||||
SECTION("erase removes every member")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
d.set(d.root(), "c", 4);
|
||||
check_first_wins(d.root(), 1);
|
||||
CHECK(d.erase(d.root(), "a") == 2);
|
||||
CHECK(d.root().dump() == R"({"b":2,"c":4})");
|
||||
CHECK(!d.root().contains("a"));
|
||||
CHECK(d.root()["a"].is_discarded());
|
||||
CHECK(d.erase(d.root(), "a") == 0);
|
||||
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
CHECK(d.erase(json::json_pointer("/a")) == 2);
|
||||
CHECK(d.root().dump() == R"({"b":2})");
|
||||
}
|
||||
|
||||
SECTION("a large object with an index")
|
||||
{
|
||||
std::string text = R"({"a":0,"k7":"first")";
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
text += ",\"k" + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"a":1,"k7":"last","a":2})";
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
check_first_wins(d.root(), 0);
|
||||
CHECK(d.root()["k7"].get_string() == "first");
|
||||
CHECK(d.root()["k199"].get<int>() == 199);
|
||||
|
||||
// a value assigned in place: the index stays in use
|
||||
d.set(d.root()["a"], 3);
|
||||
check_first_wins(d.root(), 3);
|
||||
d.set(d.root()["k7"], "changed");
|
||||
CHECK(d.root()["k7"].get_string() == "changed");
|
||||
CHECK(d.root()["k199"].get<int>() == 199);
|
||||
|
||||
// an appended member moves the members: the lookup scans them
|
||||
d.set(d.root(), "new", 1);
|
||||
check_first_wins(d.root(), 3);
|
||||
CHECK(d.root()["k7"].get_string() == "changed");
|
||||
CHECK(d.root()["k7"].get_string() == d.root().at("k7").get_string());
|
||||
CHECK(d.root()["k199"].get<int>() == 199);
|
||||
CHECK(d.root()["new"].get<int>() == 1);
|
||||
|
||||
// a value replaced by a container, in an object that was not moved
|
||||
d = json_editable_document::parse(text);
|
||||
d.set(d.root()["a"], json{{"x", 1}});
|
||||
CHECK(d.root()["a"]["x"].get<int>() == 1);
|
||||
CHECK(d.root()["k7"].get_string() == "first");
|
||||
CHECK(d.erase(d.root(), "k7") == 3); // (the key occurs three times)
|
||||
CHECK(d.root()["k7"].is_discarded());
|
||||
CHECK(d.root()["a"]["x"].get<int>() == 1);
|
||||
}
|
||||
|
||||
SECTION("objects in moved arrays and in new values")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"([{"a": 1, "a": 2}, {"a": 3, "b": 0, "a": 4}])");
|
||||
check_first_wins(d.root()[0], 1);
|
||||
check_first_wins(d.root()[1], 3);
|
||||
d.insert(d.root(), 0, json::parse(R"({"a": 0})"));
|
||||
d.push_back(d.root(), json::parse(R"({"a": 5, "a": 6})")); // (a basic_json value has one member)
|
||||
check_first_wins(d.root()[1], 1);
|
||||
check_first_wins(d.root()[2], 3);
|
||||
CHECK(d.root()[3]["a"].get<int>() == 6);
|
||||
d.set(d.root()[1], "a", 8);
|
||||
check_first_wins(d.root()[1], 8);
|
||||
CHECK(d.root()[1].dump() == R"({"a":8})");
|
||||
d.erase(d.root(), 0);
|
||||
check_first_wins(d.root()[1], 3);
|
||||
CHECK(d.root().dump() == R"([{"a":8},{"a":3,"b":0,"a":4},{"a":6}])");
|
||||
}
|
||||
|
||||
SECTION("values of other documents")
|
||||
{
|
||||
const json_document source = json_document::parse(R"({"list": [{"a": 1, "a": 2}], "o": {"b": {"a": 3, "a": 4}, "a": 5, "a": 6}})");
|
||||
json_editable_document d = json_editable_document::parse("{}");
|
||||
d.set(d.root(), "copy", source.root()["list"]);
|
||||
d.set(d.root(), "o", source.root()["o"]);
|
||||
// (the copies keep the repeated members)
|
||||
CHECK(d.root().dump() == R"({"copy":[{"a":1,"a":2}],"o":{"b":{"a":3,"a":4},"a":5,"a":6}})");
|
||||
check_first_wins(d.root()["copy"][0], 1);
|
||||
check_first_wins(d.root()["o"]["b"], 3);
|
||||
check_first_wins(d.root()["o"], 5);
|
||||
d.set(d.root()["o"]["b"], "c", 0);
|
||||
check_first_wins(d.root()["o"]["b"], 3);
|
||||
d.set(d.root()["o"], "d", 0);
|
||||
check_first_wins(d.root()["o"], 5);
|
||||
CHECK(d.root()["o"]["b"]["a"].get<int>() == 3);
|
||||
CHECK(d.root()["o"]["d"].get<int>() == 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_editable_document;
|
||||
using nlohmann::json_editable_view;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
using json_view_test::exception_of_call;
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_edit.cpp, whose object file got too
|
||||
// large for the MinGW linker (see json_view_test_helpers.hpp).
|
||||
|
||||
TEST_CASE("json_view edits: views and values")
|
||||
{
|
||||
SECTION("a value that is no longer part of the document")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse("[[[1,2]]]");
|
||||
const json_editable_view inner = d.root()[0][0];
|
||||
d.set(d.root()[0], json::array({7}));
|
||||
d.set(inner, 5);
|
||||
CHECK(d.root().dump() == "[[7]]");
|
||||
CHECK(inner.get<int>() == 5);
|
||||
}
|
||||
|
||||
SECTION("views keep referring to their value")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})");
|
||||
const json_editable_view a = d.root()["a"];
|
||||
const json_editable_view twenty = a[1];
|
||||
const json_editable_view c = d.root()["b"]["c"];
|
||||
d.insert(a, 0, 5);
|
||||
d.push_back(a, 40);
|
||||
CHECK(twenty.get<int>() == 20);
|
||||
CHECK(a[2].get<int>() == 20);
|
||||
d.erase(a, 2);
|
||||
CHECK(twenty.get<int>() == 20); // an erased value keeps its last value
|
||||
d.set(c, 7);
|
||||
CHECK(c.get<int>() == 7); // a held view sees an assignment
|
||||
d.set(d.root()["b"], json::array({1, 2}));
|
||||
CHECK(d.root()["b"].dump() == "[1,2]");
|
||||
CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})");
|
||||
CHECK(d.root()["a"][0].source_offset() == static_cast<std::size_t>(-1)); // a new value
|
||||
CHECK(d.root()["a"][1].source_offset() != static_cast<std::size_t>(-1));
|
||||
}
|
||||
|
||||
SECTION("strings stay valid while more edits come")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse("[]");
|
||||
const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string();
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
d.push_back(d.root(), std::string(static_cast<std::size_t>(i % 50), 'y'));
|
||||
}
|
||||
CHECK(std::string(first.data(), first.size()) == std::string(100, 'x'));
|
||||
CHECK(d.root().size() == 1001);
|
||||
}
|
||||
|
||||
SECTION("numbers")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])");
|
||||
d.set(d.root(), 2, 0.1);
|
||||
d.push_back(d.root(), std::numeric_limits<double>::quiet_NaN());
|
||||
d.push_back(d.root(), -std::numeric_limits<double>::infinity());
|
||||
d.push_back(d.root(), (std::numeric_limits<std::uint64_t>::max)());
|
||||
d.push_back(d.root(), (std::numeric_limits<std::int64_t>::min)());
|
||||
CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]");
|
||||
CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]");
|
||||
CHECK(std::isnan(d.root()[3].get<double>()));
|
||||
CHECK(std::isinf(d.root()[4].get<double>()));
|
||||
CHECK(d.root()[2].number_token() == "0.1");
|
||||
CHECK(d.root()[5].get<std::uint64_t>() == 18446744073709551615u);
|
||||
CHECK(d.root()[6].number_token() == "-9223372036854775808");
|
||||
CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump());
|
||||
}
|
||||
|
||||
SECTION("numbers of other float types")
|
||||
{
|
||||
// doubles have their own path to the output; other float types are
|
||||
// written as basic_json writes them, non-finite values as null
|
||||
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
||||
using document_float = nlohmann::basic_json_document<json_float, true>;
|
||||
document_float d = document_float::parse("[1.5]");
|
||||
d.push_back(d.root(), std::numeric_limits<float>::quiet_NaN());
|
||||
d.push_back(d.root(), -std::numeric_limits<float>::infinity());
|
||||
CHECK(d.root().dump() == "[1.5,null,null]");
|
||||
CHECK(d.root().dump(2) == json_float::parse("[1.5, null, null]").dump(2));
|
||||
}
|
||||
|
||||
SECTION("nulls become containers, and the root can be replaced")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse("[null, null]");
|
||||
d.set(d.root()[0], "k", 1);
|
||||
d.push_back(d.root()[1], true);
|
||||
CHECK(d.root().dump() == R"([{"k":1},[true]])");
|
||||
d.set(d.root(), "scalar");
|
||||
CHECK(d.root().dump() == R"("scalar")");
|
||||
d.set(d.root(), json{{"x", {1, 2}}});
|
||||
d.set(json::json_pointer("/x/-"), 3);
|
||||
d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too
|
||||
d.set(json::json_pointer("/y"), false);
|
||||
CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})");
|
||||
CHECK(d.erase(json::json_pointer("/x/0")) == 1);
|
||||
CHECK(d.erase(json::json_pointer("/y")) == 1);
|
||||
CHECK(d.erase(json::json_pointer("/nothing")) == 0);
|
||||
CHECK(d.root().dump() == R"({"x":[2,3,4]})");
|
||||
}
|
||||
|
||||
SECTION("duplicate keys")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
d.set(d.root(), "a", 4); // the first member is assigned, the others dropped
|
||||
CHECK(d.root().dump() == R"({"a":4,"b":2})");
|
||||
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
CHECK(d.erase(d.root(), "a") == 2);
|
||||
CHECK(d.root().dump() == R"({"b":2})");
|
||||
}
|
||||
|
||||
SECTION("values from other documents")
|
||||
{
|
||||
const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})");
|
||||
json_editable_document edited = json_editable_document::parse("[0]");
|
||||
edited.set(edited.root(), 0, json{{"inner", {1, 2}}});
|
||||
json_editable_document d = json_editable_document::parse("{}");
|
||||
d.set(d.root(), "copy", source.root()["list"]);
|
||||
d.set(d.root(), "text", source.root()["text"]);
|
||||
d.set(d.root(), "edited", edited.root()[0]);
|
||||
d.set(d.root(), "self", d.root()["copy"]);
|
||||
// (a raw string with a backslash must not be a macro argument: MSVC C2017)
|
||||
const std::string expected = R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})";
|
||||
CHECK(d.root().dump() == expected);
|
||||
CHECK(d.root()["copy"] == source.root()["list"]);
|
||||
CHECK(source.root()["list"] == d.root()["self"]);
|
||||
CHECK(d.root() != source.root());
|
||||
}
|
||||
|
||||
SECTION("large objects")
|
||||
{
|
||||
std::string text = "{";
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use
|
||||
CHECK(d.root()["k7"].get_string() == "seven");
|
||||
d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear
|
||||
CHECK(d.root()["new"].get<int>() == 1);
|
||||
CHECK(d.root()["k199"].get<int>() == 199);
|
||||
d.erase(d.root(), "k0");
|
||||
CHECK(!d.root().contains("k0"));
|
||||
CHECK(d.root().size() == 200);
|
||||
}
|
||||
|
||||
SECTION("reuse and memory")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse("[1, 2, 3]");
|
||||
const std::size_t before = d.memory_usage();
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
d.push_back(d.root(), "some text");
|
||||
}
|
||||
CHECK(d.memory_usage() > before);
|
||||
const json_editable_view first = d.root()[0];
|
||||
d.shrink_to_fit(); // (with edits, the index stays in place)
|
||||
CHECK(first.get<int>() == 1);
|
||||
d.read(std::string("[true]"));
|
||||
CHECK(d.root().dump() == "[true]");
|
||||
d.push_back(d.root(), false);
|
||||
CHECK(d.root().dump() == "[true,false]");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view edits: deeply nested values")
|
||||
{
|
||||
// copying a value into a document must not recurse per nesting level
|
||||
const std::size_t depth = 100000;
|
||||
const std::string brackets = std::string(depth, '[') + std::string(depth, ']');
|
||||
std::string braces;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
braces += "{\"a\":";
|
||||
}
|
||||
braces += '1';
|
||||
braces += std::string(depth, '}');
|
||||
|
||||
SECTION("a view of a read-only document")
|
||||
{
|
||||
const json_document source = json_document::parse(brackets);
|
||||
json_editable_document d = json_editable_document::parse("[]");
|
||||
d.push_back(d.root(), source.root());
|
||||
CHECK(d.root().dump() == "[" + brackets + "]");
|
||||
}
|
||||
|
||||
SECTION("a view of an editable document")
|
||||
{
|
||||
const json_editable_document source = json_editable_document::parse(braces);
|
||||
json_editable_document d = json_editable_document::parse("{}");
|
||||
d.set(d.root(), "deep", source.root());
|
||||
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
|
||||
}
|
||||
|
||||
SECTION("a view of an edited document (values behind links)")
|
||||
{
|
||||
const json_document source = json_document::parse(brackets);
|
||||
json_editable_document edited = json_editable_document::parse("[[]]");
|
||||
edited.push_back(edited.root()[0], source.root());
|
||||
edited.push_back(edited.root(), source.root());
|
||||
json_editable_document d = json_editable_document::parse("null");
|
||||
d.set(d.root(), edited.root());
|
||||
CHECK(d.root().dump() == "[[" + brackets + "]," + brackets + "]");
|
||||
}
|
||||
|
||||
SECTION("a basic_json value")
|
||||
{
|
||||
json deep = json::array();
|
||||
json* inner = &deep;
|
||||
for (std::size_t i = 1; i < depth; ++i)
|
||||
{
|
||||
inner->push_back(json::array());
|
||||
inner = &inner->back();
|
||||
}
|
||||
json_editable_document d = json_editable_document::parse("[]");
|
||||
d.push_back(d.root(), deep);
|
||||
CHECK(d.root().dump() == "[" + brackets + "]");
|
||||
}
|
||||
|
||||
SECTION("a basic_json value with objects")
|
||||
{
|
||||
json deep = 1;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
json outer = json::object();
|
||||
outer["a"] = std::move(deep);
|
||||
deep = std::move(outer);
|
||||
}
|
||||
json_editable_document d = json_editable_document::parse("{}");
|
||||
d.set(d.root(), "deep", deep);
|
||||
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view edits: pointers below a null value")
|
||||
{
|
||||
// a null value on the way becomes what basic_json makes of it: an array
|
||||
// for "-" and for digits, an object otherwise
|
||||
struct test_case
|
||||
{
|
||||
const char* document;
|
||||
const char* pointer;
|
||||
};
|
||||
const std::array<test_case, 16> cases =
|
||||
{
|
||||
{
|
||||
{R"({"a":null})", "/a/0"},
|
||||
{R"({"a":null})", "/a/-"},
|
||||
{R"({"a":null})", "/a/3"},
|
||||
{R"({"a":null})", "/a/x"},
|
||||
{R"({"a":null})", "/a/+1"},
|
||||
{R"({"a":null})", "/a/01"},
|
||||
{R"({"a":null})", "/a/"},
|
||||
{R"({"a":{"b":null}})", "/a/b/1"},
|
||||
{R"({"a":{"b":null}})", "/a/b/-"},
|
||||
{R"({"a":[null]})", "/a/0/0"},
|
||||
{R"({"a":[null,null]})", "/a/1/k"},
|
||||
{R"([null])", "/0"},
|
||||
{"null", "/0"},
|
||||
{"null", "/-"},
|
||||
{"null", "/k"},
|
||||
{"null", ""},
|
||||
}
|
||||
};
|
||||
for (const test_case& c : cases)
|
||||
{
|
||||
CAPTURE(c.document)
|
||||
CAPTURE(c.pointer)
|
||||
json expected = json::parse(c.document);
|
||||
const std::string error = exception_of_call([&]
|
||||
{
|
||||
expected[json::json_pointer(c.pointer)] = 1;
|
||||
});
|
||||
json_editable_document d = json_editable_document::parse(c.document);
|
||||
if (error.empty())
|
||||
{
|
||||
d.set(json::json_pointer(c.pointer), 1);
|
||||
CHECK(d.root().dump() == expected.dump());
|
||||
CHECK(d.root().materialize() == expected);
|
||||
}
|
||||
else
|
||||
{
|
||||
// the same error, and the document is not changed
|
||||
CHECK(exception_of_call([&] { d.set(json::json_pointer(c.pointer), 1); }) == error);
|
||||
CHECK(d.root().dump() == json::parse(c.document).dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view edits: strings of other documents are checked")
|
||||
{
|
||||
// A document borrows the text it was parsed from, and sees later changes
|
||||
// of the text: a way to get ill-formed UTF-8 into a view. Copying it into
|
||||
// an editable document is an error, as for any other string.
|
||||
std::string text = R"({"key":"abc","list":["abc"]})";
|
||||
const json_document source = json_document::parse(text);
|
||||
const auto message_of = [](const std::string & bad)
|
||||
{
|
||||
return exception_of_call([&]
|
||||
{
|
||||
const std::string dumped = json(bad).dump();
|
||||
static_cast<void>(dumped);
|
||||
});
|
||||
};
|
||||
json_editable_document d = json_editable_document::parse("[1]");
|
||||
d.push_back(d.root(), source.root());
|
||||
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
|
||||
|
||||
text[text.find("abc") + 1] = '\xC3'; // "a\xC3c"
|
||||
text[text.rfind("abc") + 1] = '\xC3';
|
||||
const std::string bad_value = message_of(std::string("a\xC3" "c"));
|
||||
CHECK(!bad_value.empty());
|
||||
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()["key"]); }) == bad_value);
|
||||
CHECK(exception_of_call([&] { d.set(d.root()[0], source.root()["list"][0]); }) == bad_value);
|
||||
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()["list"]); }) == bad_value);
|
||||
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_value);
|
||||
|
||||
text[text.find("key") + 1] = '\xC3'; // a key is checked as well
|
||||
const std::string bad_key = message_of(std::string("k\xC3" "y"));
|
||||
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_key);
|
||||
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()); }) == bad_key);
|
||||
|
||||
// nothing of the failed edits is visible
|
||||
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,47 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
// All other tests keep the library's macros (JSON_TEST_KEEP_MACROS). This one
|
||||
// includes json_view.hpp as users do, so that json.hpp undefines its macros
|
||||
// (JSON_HAS_CPP_17, JSON_STRICT_NUL_HANDLING, ...) before the view is compiled.
|
||||
#undef JSON_TEST_KEEP_MACROS
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
|
||||
#include <string_view>
|
||||
#define JSON_VIEW_TEST_HAS_STRING_VIEW 1
|
||||
#else
|
||||
#define JSON_VIEW_TEST_HAS_STRING_VIEW 0
|
||||
#endif
|
||||
|
||||
// the view's own macros do not leak
|
||||
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) \
|
||||
|| defined(NLOHMANN_VIEW_NODISCARD) || defined(NLOHMANN_VIEW_THROW) || defined(NLOHMANN_VIEW_HAS_CPP_17) || defined(NLOHMANN_VIEW_LITTLE_ENDIAN) \
|
||||
|| defined(NLOHMANN_VIEW_REPEAT16)
|
||||
#error "json_view.hpp leaks a macro"
|
||||
#endif
|
||||
|
||||
TEST_CASE("json_view without the library's macros")
|
||||
{
|
||||
// (this file also gets C++17 builds: it mentions JSON_HAS_CPP_17)
|
||||
#if JSON_VIEW_TEST_HAS_STRING_VIEW
|
||||
CHECK(std::is_same<nlohmann::json_view::string_view_t, std::string_view>::value);
|
||||
#endif
|
||||
const std::string text = "[1, 2.5, \"x\"]";
|
||||
const nlohmann::json_document d = nlohmann::json_document::parse(text);
|
||||
CHECK(d.root().size() == 3);
|
||||
CHECK(d.root().materialize() == nlohmann::json::parse(text));
|
||||
// the NUL handling of the library's configuration
|
||||
const std::string with_nul("[1]\0x", 5);
|
||||
CHECK(nlohmann::json_document::accept(with_nul) == nlohmann::json::accept(with_nul));
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// MessagePack tests with basic_json specializations other than nlohmann::json;
|
||||
// they are kept apart from unit-msgpack.cpp because every specialization adds
|
||||
// many sections to the object file, and MinGW cannot link objects with more
|
||||
// than 65535 sections (see the clang job in .github/workflows/windows.yml)
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <cstdint> // SIZE_MAX, UINT32_MAX
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// the fake sizes below do not fit into a 32-bit std::size_t
|
||||
// with clang and libstdc++ 10, the std::filesystem::path conversion that
|
||||
// C++17 builds consider for every string type is ambiguous for a class
|
||||
// derived from std::string, so the string case is not tested there
|
||||
#if !(defined(__clang__) && defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 11)
|
||||
#define JSON_TEST_BEYOND_UINT32_STRING 1
|
||||
#endif
|
||||
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
template<typename T, typename A = std::allocator<T>>
|
||||
struct huge_array : std::vector<T, A>
|
||||
{
|
||||
using base = std::vector<T, A>;
|
||||
using base::base;
|
||||
|
||||
bool fake_size = false;
|
||||
|
||||
std::size_t size() const noexcept
|
||||
{
|
||||
if (fake_size)
|
||||
{
|
||||
return (std::numeric_limits<std::uint32_t>::max)() + 1ULL;
|
||||
}
|
||||
|
||||
return base::size();
|
||||
}
|
||||
};
|
||||
|
||||
using huge_array_json = nlohmann::json::with_array_t<huge_array>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for array")
|
||||
{
|
||||
huge_array_json j = huge_array_json::array();
|
||||
|
||||
j.push_back(1);
|
||||
j.push_back(2);
|
||||
j.push_back(3);
|
||||
|
||||
auto& array = j.get_ref<huge_array_json::array_t&>();
|
||||
array.fake_size = true;
|
||||
|
||||
// write into a caller-owned vector: to_msgpack(j) reserves space based on
|
||||
// the (faked) element count, which fails with bad_alloc on Windows
|
||||
std::vector<std::uint8_t> result;
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_array_json::to_msgpack(j, result),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
|
||||
array.fake_size = false;
|
||||
}
|
||||
|
||||
template<typename K, typename V,
|
||||
typename C = std::less<K>,
|
||||
typename A = std::allocator<std::pair<const K, V>>>
|
||||
struct huge_map : std::map<K, V, C, A>
|
||||
{
|
||||
using base = std::map<K, V, C, A>;
|
||||
using base::base;
|
||||
|
||||
bool fake_size = false;
|
||||
|
||||
std::size_t size() const noexcept
|
||||
{
|
||||
if (fake_size)
|
||||
{
|
||||
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
|
||||
}
|
||||
|
||||
return base::size();
|
||||
}
|
||||
};
|
||||
|
||||
using huge_object_json = nlohmann::json::with_object_t<huge_map>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for object")
|
||||
{
|
||||
|
||||
huge_object_json j = huge_object_json::object();
|
||||
|
||||
j["one"] = 1;
|
||||
j["two"] = 2;
|
||||
|
||||
auto& object = j.get_ref<huge_object_json::object_t&>();
|
||||
object.fake_size = true;
|
||||
|
||||
// write into a caller-owned vector: to_msgpack(j) reserves space based on
|
||||
// the (faked) element count, which fails with bad_alloc on Windows
|
||||
std::vector<std::uint8_t> result;
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_object_json::to_msgpack(j, result),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
|
||||
object.fake_size = false;
|
||||
}
|
||||
|
||||
#ifdef JSON_TEST_BEYOND_UINT32_STRING
|
||||
struct huge_string : std::string
|
||||
{
|
||||
using std::string::string;
|
||||
|
||||
std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
|
||||
}
|
||||
};
|
||||
|
||||
using huge_string_json = nlohmann::json::with_string_t<huge_string>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for string")
|
||||
{
|
||||
const huge_string_json j = "hello";
|
||||
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_string_json::to_msgpack(j),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
}
|
||||
#endif
|
||||
|
||||
struct huge_binary : std::vector<std::uint8_t>
|
||||
{
|
||||
using std::vector<std::uint8_t>::vector;
|
||||
|
||||
std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
|
||||
}
|
||||
};
|
||||
|
||||
using huge_binary_json = nlohmann::json::with_binary_t<huge_binary>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for binary")
|
||||
{
|
||||
|
||||
huge_binary_json j = huge_binary_json::binary(huge_binary{});
|
||||
|
||||
j.get_binary().push_back(0x01);
|
||||
j.get_binary().push_back(0x02);
|
||||
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_binary_json::to_msgpack(j),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
// types that report a size beyond UINT32_MAX without allocating that much
|
||||
// memory, so the MessagePack length limit can be tested cheaply; see the
|
||||
// similar types in unit-bson.cpp
|
||||
std::size_t beyond_uint32_size()
|
||||
{
|
||||
return static_cast<std::size_t>((std::numeric_limits<std::uint32_t>::max)()) + 1;
|
||||
}
|
||||
|
||||
class beyond_uint32_binary_t : public std::vector<std::uint8_t>
|
||||
{
|
||||
public:
|
||||
using std::vector<std::uint8_t>::vector;
|
||||
|
||||
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return beyond_uint32_size();
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef JSON_TEST_BEYOND_UINT32_STRING
|
||||
class beyond_uint32_string_t : public std::string
|
||||
{
|
||||
public:
|
||||
using std::string::string;
|
||||
|
||||
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return beyond_uint32_size();
|
||||
}
|
||||
};
|
||||
|
||||
using beyond_uint32_string_json = nlohmann::json::with_string_t<beyond_uint32_string_t>;
|
||||
#endif
|
||||
|
||||
using beyond_uint32_binary_json = nlohmann::json::with_binary_t<beyond_uint32_binary_t>;
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized")
|
||||
{
|
||||
// MessagePack stores the length of a string, binary value, array, or
|
||||
// object in at most 32 bits; a larger one used to be written without any
|
||||
// length at all
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
{
|
||||
const char* const expected = "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295";
|
||||
|
||||
const beyond_uint32_binary_json binary = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{});
|
||||
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(binary), expected, beyond_uint32_binary_json::out_of_range&);
|
||||
|
||||
const beyond_uint32_binary_json ext = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}, 42);
|
||||
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(ext), expected, beyond_uint32_binary_json::out_of_range&);
|
||||
|
||||
#ifdef JSON_TEST_BEYOND_UINT32_STRING
|
||||
// created from its type rather than from a beyond_uint32_string_t:
|
||||
// that would consider the std::filesystem::path conversion, which
|
||||
// libstdc++ 10 cannot decide for a class derived from std::string
|
||||
const beyond_uint32_string_json string(beyond_uint32_string_json::value_t::string);
|
||||
CHECK_THROWS_WITH_AS(beyond_uint32_string_json::to_msgpack(string), expected, beyond_uint32_string_json::out_of_range&);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("MessagePack numbers use the active union member (see #5644)")
|
||||
{
|
||||
// when number_integer_t is narrower than number_unsigned_t, to_msgpack()
|
||||
// used to read the union member that was not the active one, writing
|
||||
// wrong bytes for some values; std::int64_t/std::uint64_t (the default
|
||||
// types, where both members have the same width) were not affected
|
||||
using int32_json = nlohmann::json::with_integers_t<std::int32_t, std::uint64_t>;
|
||||
using int16_json = nlohmann::json::with_integers_t<std::int16_t, std::uint64_t>;
|
||||
|
||||
SECTION("number_integer_t = std::int32_t")
|
||||
{
|
||||
SECTION("6442450944 (uint 64; the low 32 bits used to be sign-extended)")
|
||||
{
|
||||
const int32_json j = 6442450944ULL;
|
||||
CHECK(j.is_number_unsigned());
|
||||
|
||||
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00};
|
||||
const auto result = int32_json::to_msgpack(j);
|
||||
CHECK(result == expected);
|
||||
CHECK(int32_json::from_msgpack(result) == j);
|
||||
}
|
||||
|
||||
SECTION("4294967496 (uint 64; the low 32 bits used to be the whole value)")
|
||||
{
|
||||
const int32_json j = 4294967496ULL;
|
||||
CHECK(j.is_number_unsigned());
|
||||
|
||||
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8};
|
||||
const auto result = int32_json::to_msgpack(j);
|
||||
CHECK(result == expected);
|
||||
CHECK(int32_json::from_msgpack(result) == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("number_integer_t = std::int16_t, 98304 (uint 32)")
|
||||
{
|
||||
const int16_json j = 98304ULL;
|
||||
CHECK(j.is_number_unsigned());
|
||||
|
||||
std::vector<uint8_t> const expected{0xce, 0x00, 0x01, 0x80, 0x00};
|
||||
const auto result = int16_json::to_msgpack(j);
|
||||
CHECK(result == expected);
|
||||
CHECK(int16_json::from_msgpack(result) == j);
|
||||
}
|
||||
|
||||
SECTION("default types (std::int64_t/std::uint64_t) are unaffected")
|
||||
{
|
||||
const json j = 4294967496ULL;
|
||||
CHECK(j.is_number_unsigned());
|
||||
|
||||
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8};
|
||||
const auto result = json::to_msgpack(j);
|
||||
CHECK(result == expected);
|
||||
CHECK(json::from_msgpack(result) == j);
|
||||
}
|
||||
}
|
||||
@@ -2098,276 +2098,6 @@ TEST_CASE("MessagePack roundtrips" * doctest::skip())
|
||||
}
|
||||
}
|
||||
|
||||
// the fake sizes below do not fit into a 32-bit std::size_t
|
||||
// with clang and libstdc++ 10, the std::filesystem::path conversion that
|
||||
// C++17 builds consider for every string type is ambiguous for a class
|
||||
// derived from std::string, so the string case is not tested there
|
||||
#if !(defined(__clang__) && defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 11)
|
||||
#define JSON_TEST_BEYOND_UINT32_STRING 1
|
||||
#endif
|
||||
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
template<typename T, typename A = std::allocator<T>>
|
||||
struct huge_array : std::vector<T, A>
|
||||
{
|
||||
using base = std::vector<T, A>;
|
||||
using base::base;
|
||||
|
||||
bool fake_size = false;
|
||||
|
||||
std::size_t size() const noexcept
|
||||
{
|
||||
if (fake_size)
|
||||
{
|
||||
return (std::numeric_limits<std::uint32_t>::max)() + 1ULL;
|
||||
}
|
||||
|
||||
return base::size();
|
||||
}
|
||||
};
|
||||
|
||||
using huge_array_json = nlohmann::json::with_array_t<huge_array>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for array")
|
||||
{
|
||||
huge_array_json j = huge_array_json::array();
|
||||
|
||||
j.push_back(1);
|
||||
j.push_back(2);
|
||||
j.push_back(3);
|
||||
|
||||
auto& array = j.get_ref<huge_array_json::array_t&>();
|
||||
array.fake_size = true;
|
||||
|
||||
// write into a caller-owned vector: to_msgpack(j) reserves space based on
|
||||
// the (faked) element count, which fails with bad_alloc on Windows
|
||||
std::vector<std::uint8_t> result;
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_array_json::to_msgpack(j, result),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
|
||||
array.fake_size = false;
|
||||
}
|
||||
|
||||
template<typename K, typename V,
|
||||
typename C = std::less<K>,
|
||||
typename A = std::allocator<std::pair<const K, V>>>
|
||||
struct huge_map : std::map<K, V, C, A>
|
||||
{
|
||||
using base = std::map<K, V, C, A>;
|
||||
using base::base;
|
||||
|
||||
bool fake_size = false;
|
||||
|
||||
std::size_t size() const noexcept
|
||||
{
|
||||
if (fake_size)
|
||||
{
|
||||
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
|
||||
}
|
||||
|
||||
return base::size();
|
||||
}
|
||||
};
|
||||
|
||||
using huge_object_json = nlohmann::json::with_object_t<huge_map>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for object")
|
||||
{
|
||||
|
||||
huge_object_json j = huge_object_json::object();
|
||||
|
||||
j["one"] = 1;
|
||||
j["two"] = 2;
|
||||
|
||||
auto& object = j.get_ref<huge_object_json::object_t&>();
|
||||
object.fake_size = true;
|
||||
|
||||
// write into a caller-owned vector: to_msgpack(j) reserves space based on
|
||||
// the (faked) element count, which fails with bad_alloc on Windows
|
||||
std::vector<std::uint8_t> result;
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_object_json::to_msgpack(j, result),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
|
||||
object.fake_size = false;
|
||||
}
|
||||
|
||||
#ifdef JSON_TEST_BEYOND_UINT32_STRING
|
||||
struct huge_string : std::string
|
||||
{
|
||||
using std::string::string;
|
||||
|
||||
std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
|
||||
}
|
||||
};
|
||||
|
||||
using huge_string_json = nlohmann::json::with_string_t<huge_string>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for string")
|
||||
{
|
||||
const huge_string_json j = "hello";
|
||||
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_string_json::to_msgpack(j),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
}
|
||||
#endif
|
||||
|
||||
struct huge_binary : std::vector<std::uint8_t>
|
||||
{
|
||||
using std::vector<std::uint8_t>::vector;
|
||||
|
||||
std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
|
||||
}
|
||||
};
|
||||
|
||||
using huge_binary_json = nlohmann::json::with_binary_t<huge_binary>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for binary")
|
||||
{
|
||||
|
||||
huge_binary_json j = huge_binary_json::binary(huge_binary{});
|
||||
|
||||
j.get_binary().push_back(0x01);
|
||||
j.get_binary().push_back(0x02);
|
||||
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_binary_json::to_msgpack(j),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
// types that report a size beyond UINT32_MAX without allocating that much
|
||||
// memory, so the MessagePack length limit can be tested cheaply; see the
|
||||
// similar types in unit-bson.cpp
|
||||
std::size_t beyond_uint32_size()
|
||||
{
|
||||
return static_cast<std::size_t>((std::numeric_limits<std::uint32_t>::max)()) + 1;
|
||||
}
|
||||
|
||||
class beyond_uint32_binary_t : public std::vector<std::uint8_t>
|
||||
{
|
||||
public:
|
||||
using std::vector<std::uint8_t>::vector;
|
||||
|
||||
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return beyond_uint32_size();
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef JSON_TEST_BEYOND_UINT32_STRING
|
||||
class beyond_uint32_string_t : public std::string
|
||||
{
|
||||
public:
|
||||
using std::string::string;
|
||||
|
||||
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return beyond_uint32_size();
|
||||
}
|
||||
};
|
||||
|
||||
using beyond_uint32_string_json = nlohmann::json::with_string_t<beyond_uint32_string_t>;
|
||||
#endif
|
||||
|
||||
using beyond_uint32_binary_json = nlohmann::json::with_binary_t<beyond_uint32_binary_t>;
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized")
|
||||
{
|
||||
// MessagePack stores the length of a string, binary value, array, or
|
||||
// object in at most 32 bits; a larger one used to be written without any
|
||||
// length at all
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
{
|
||||
const char* const expected = "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295";
|
||||
|
||||
const beyond_uint32_binary_json binary = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{});
|
||||
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(binary), expected, beyond_uint32_binary_json::out_of_range&);
|
||||
|
||||
const beyond_uint32_binary_json ext = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}, 42);
|
||||
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(ext), expected, beyond_uint32_binary_json::out_of_range&);
|
||||
|
||||
#ifdef JSON_TEST_BEYOND_UINT32_STRING
|
||||
// created from its type rather than from a beyond_uint32_string_t:
|
||||
// that would consider the std::filesystem::path conversion, which
|
||||
// libstdc++ 10 cannot decide for a class derived from std::string
|
||||
const beyond_uint32_string_json string(beyond_uint32_string_json::value_t::string);
|
||||
CHECK_THROWS_WITH_AS(beyond_uint32_string_json::to_msgpack(string), expected, beyond_uint32_string_json::out_of_range&);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("MessagePack numbers use the active union member (see #5644)")
|
||||
{
|
||||
// when number_integer_t is narrower than number_unsigned_t, to_msgpack()
|
||||
// used to read the union member that was not the active one, writing
|
||||
// wrong bytes for some values; std::int64_t/std::uint64_t (the default
|
||||
// types, where both members have the same width) were not affected
|
||||
using int32_json = nlohmann::json::with_integers_t<std::int32_t, std::uint64_t>;
|
||||
using int16_json = nlohmann::json::with_integers_t<std::int16_t, std::uint64_t>;
|
||||
|
||||
SECTION("number_integer_t = std::int32_t")
|
||||
{
|
||||
SECTION("6442450944 (uint 64; the low 32 bits used to be sign-extended)")
|
||||
{
|
||||
const int32_json j = 6442450944ULL;
|
||||
CHECK(j.is_number_unsigned());
|
||||
|
||||
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00};
|
||||
const auto result = int32_json::to_msgpack(j);
|
||||
CHECK(result == expected);
|
||||
CHECK(int32_json::from_msgpack(result) == j);
|
||||
}
|
||||
|
||||
SECTION("4294967496 (uint 64; the low 32 bits used to be the whole value)")
|
||||
{
|
||||
const int32_json j = 4294967496ULL;
|
||||
CHECK(j.is_number_unsigned());
|
||||
|
||||
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8};
|
||||
const auto result = int32_json::to_msgpack(j);
|
||||
CHECK(result == expected);
|
||||
CHECK(int32_json::from_msgpack(result) == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("number_integer_t = std::int16_t, 98304 (uint 32)")
|
||||
{
|
||||
const int16_json j = 98304ULL;
|
||||
CHECK(j.is_number_unsigned());
|
||||
|
||||
std::vector<uint8_t> const expected{0xce, 0x00, 0x01, 0x80, 0x00};
|
||||
const auto result = int16_json::to_msgpack(j);
|
||||
CHECK(result == expected);
|
||||
CHECK(int16_json::from_msgpack(result) == j);
|
||||
}
|
||||
|
||||
SECTION("default types (std::int64_t/std::uint64_t) are unaffected")
|
||||
{
|
||||
const json j = 4294967496ULL;
|
||||
CHECK(j.is_number_unsigned());
|
||||
|
||||
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8};
|
||||
const auto result = json::to_msgpack(j);
|
||||
CHECK(result == expected);
|
||||
CHECK(json::from_msgpack(result) == j);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("MessagePack large strings and binaries (chunked reader)")
|
||||
{
|
||||
// get_msgpack_string()/get_msgpack_binary() both read through get_binary(),
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <sstream>
|
||||
#include <streambuf>
|
||||
@@ -72,6 +73,40 @@ class no_putback_streambuf : public std::streambuf
|
||||
std::size_t m_pos = 0;
|
||||
};
|
||||
|
||||
// A streambuf that serves its input in get areas of a fixed size, so that the
|
||||
// end of a number falls on every possible offset of a get area. The lexer scans
|
||||
// the get area directly; the stream position must not depend on its size.
|
||||
class chunked_streambuf : public std::streambuf
|
||||
{
|
||||
public:
|
||||
chunked_streambuf(std::string s, std::size_t chunk_size)
|
||||
: m_data(std::move(s)), m_chunk_size(chunk_size)
|
||||
{}
|
||||
|
||||
protected:
|
||||
int_type underflow() override
|
||||
{
|
||||
if (gptr() < egptr())
|
||||
{
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
m_pos += static_cast<std::size_t>(egptr() - eback());
|
||||
if (m_pos >= m_data.size())
|
||||
{
|
||||
setg(nullptr, nullptr, nullptr);
|
||||
return traits_type::eof();
|
||||
}
|
||||
char* const begin = &m_data[m_pos];
|
||||
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_data;
|
||||
std::size_t m_pos = 0;
|
||||
std::size_t m_chunk_size;
|
||||
};
|
||||
|
||||
// read the characters that are left in a stream
|
||||
std::string remaining(std::istream& is)
|
||||
{
|
||||
@@ -263,4 +298,26 @@ TEST_CASE("JSON_PRECISE_STREAM_POSITION")
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
|
||||
}
|
||||
|
||||
SECTION("the position does not depend on the get area of the streambuf")
|
||||
{
|
||||
const std::vector<std::size_t> chunk_sizes = {1, 2, 3, 4, 5, 7, 8, 13, 4096};
|
||||
for (const std::size_t chunk_size : chunk_sizes)
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf("12345678901234567890true\"a long string value\"-1.25e+3,[1,2]42", chunk_size);
|
||||
std::istream input(&buf);
|
||||
json j;
|
||||
|
||||
input >> j;
|
||||
CHECK(j == 12345678901234567890u);
|
||||
input >> j;
|
||||
CHECK(j == true);
|
||||
input >> j;
|
||||
CHECK(j == "a long string value");
|
||||
input >> j;
|
||||
CHECK(j == -1250.0);
|
||||
CHECK(remaining(input) == ",[1,2]42");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
// Qt defines its keywords as macros (see qtmetamacros.h and qforeach.h) unless
|
||||
// QT_NO_KEYWORDS is defined, and it is common to include Qt headers before this
|
||||
// library; it must therefore not use these words as identifiers (see #5865)
|
||||
#define slots
|
||||
#define signals public
|
||||
#define emit
|
||||
#define Q_FOREACH(variable, container) for (variable : container) // NOLINT(bugprone-macro-parentheses): variable is a declaration, as in Qt
|
||||
#define foreach Q_FOREACH
|
||||
#define forever for (;;)
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
TEST_CASE("Qt keyword macros")
|
||||
{
|
||||
SECTION("parse and dump")
|
||||
{
|
||||
const json j = json::parse(R"({"a": [1, 2.5, "x", null, true]})");
|
||||
CHECK(j.dump() == R"({"a":[1,2.5,"x",null,true]})");
|
||||
}
|
||||
|
||||
SECTION("flatten and unflatten")
|
||||
{
|
||||
const json j = {{"a", {{"b", {1, 2}}, {"c", "d"}}}, {"e", nullptr}};
|
||||
CHECK(j.flatten().unflatten() == j);
|
||||
}
|
||||
|
||||
SECTION("JSON pointer")
|
||||
{
|
||||
json j = {{"a", {{"b", {1, 2}}}}};
|
||||
CHECK(j.at(json::json_pointer("/a/b/1")) == 2);
|
||||
j[json::json_pointer("/a/c")] = 3;
|
||||
CHECK(j.contains(json::json_pointer("/a/c")));
|
||||
}
|
||||
|
||||
SECTION("binary formats")
|
||||
{
|
||||
const json j = {{"a", {1, 2, 3}}, {"b", "c"}};
|
||||
CHECK(json::from_cbor(json::to_cbor(j)) == j);
|
||||
CHECK(json::from_msgpack(json::to_msgpack(j)) == j);
|
||||
CHECK(json::from_ubjson(json::to_ubjson(j)) == j);
|
||||
CHECK(json::from_bjdata(json::to_bjdata(j)) == j);
|
||||
CHECK(json::from_bson(json::to_bson(j)) == j);
|
||||
}
|
||||
|
||||
SECTION("patch and diff")
|
||||
{
|
||||
const json source = {{"a", 1}, {"b", {1, 2}}};
|
||||
const json target = {{"a", 2}, {"c", "d"}};
|
||||
CHECK(source.patch(json::diff(source, target)) == target);
|
||||
}
|
||||
}
|
||||
|
||||
// testing for the macros also keeps -Wunused-macros from rejecting them
|
||||
#if !defined(slots) || !defined(signals) || !defined(emit) || !defined(Q_FOREACH) || !defined(foreach) || !defined(forever)
|
||||
#error "the Qt keyword macros must stay defined for the tests above"
|
||||
#endif
|
||||
@@ -35,7 +35,6 @@ using ordered_json = nlohmann::ordered_json;
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -815,52 +814,6 @@ TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("regression test #5476 - array type without reserve()")
|
||||
{
|
||||
// the capacity reserved for definite-length arrays must not require the
|
||||
// array type to have a reserve() member function
|
||||
using deque_json = nlohmann::json::with_array_t<std::deque>;
|
||||
|
||||
SECTION("std::deque")
|
||||
{
|
||||
const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})");
|
||||
CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})");
|
||||
|
||||
// the binary formats pass a definite length to start_array()
|
||||
CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j);
|
||||
CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j);
|
||||
CHECK(deque_json::from_bon8(deque_json::to_bon8(j)) == j);
|
||||
|
||||
// parse() instantiates the callback parser as well, which reserves too
|
||||
const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) noexcept
|
||||
{
|
||||
return true;
|
||||
});
|
||||
CHECK(with_callback == deque_json({1, 2, 3}));
|
||||
}
|
||||
|
||||
SECTION("std::vector still reserves")
|
||||
{
|
||||
json array = json::array();
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
array.push_back(i);
|
||||
}
|
||||
|
||||
const auto j = json::from_cbor(json::to_cbor(array));
|
||||
CHECK(j == array);
|
||||
CHECK(j.get_ref<const json::array_t&>().capacity() >= 100);
|
||||
}
|
||||
|
||||
SECTION("the reservation stays capped")
|
||||
{
|
||||
// CBOR array announcing 2^32-1 elements, but truncated right after the
|
||||
// header: the input must be rejected without reserving that capacity
|
||||
const std::vector<std::uint8_t> truncated = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
CHECK(json::from_cbor(truncated, true, false).is_discarded());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("issue #5317 - nested indefinite-length CBOR string chunks are rejected")
|
||||
{
|
||||
json _;
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
// the library undefines its configuration macros at the end of the header
|
||||
#if defined(JSON_DISABLE_STREAM_BULK_SCAN) && JSON_DISABLE_STREAM_BULK_SCAN
|
||||
#define TEST_STREAM_BULK_SCAN 0
|
||||
#else
|
||||
#define TEST_STREAM_BULK_SCAN 1
|
||||
#endif
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <streambuf>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// The lexer scans the characters in the get area of a std::istream's
|
||||
// streambuf directly instead of reading them one at a time. These tests pin
|
||||
// that this is not observable: for any get area size - including a streambuf
|
||||
// without a get area, which still takes the character-at-a-time path - the
|
||||
// parsed values, the exception messages (including the "last read" token),
|
||||
// the stream state, and the characters left in the stream are the same. See
|
||||
// unit-stream-bulk-scan-disabled.cpp for JSON_DISABLE_STREAM_BULK_SCAN.
|
||||
|
||||
namespace
|
||||
{
|
||||
// A streambuf that serves its input in get areas of a fixed size, so that
|
||||
// tokens, numbers, UTF-8 sequences, and indentation cross the end of the get
|
||||
// area at every possible offset. With a size of 0, it keeps no get area at all
|
||||
// and serves every character from underflow()/uflow().
|
||||
class chunked_streambuf : public std::streambuf
|
||||
{
|
||||
public:
|
||||
chunked_streambuf(std::string s, std::size_t chunk_size)
|
||||
: m_data(std::move(s)), m_chunk_size(chunk_size)
|
||||
{}
|
||||
|
||||
protected:
|
||||
int_type underflow() override
|
||||
{
|
||||
if (m_chunk_size == 0)
|
||||
{
|
||||
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos]) : traits_type::eof();
|
||||
}
|
||||
if (gptr() < egptr())
|
||||
{
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
// the previous get area has been consumed
|
||||
m_pos += static_cast<std::size_t>(egptr() - eback());
|
||||
if (m_pos >= m_data.size())
|
||||
{
|
||||
setg(nullptr, nullptr, nullptr);
|
||||
return traits_type::eof();
|
||||
}
|
||||
char* const begin = &m_data[m_pos];
|
||||
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
|
||||
int_type uflow() override
|
||||
{
|
||||
if (m_chunk_size == 0)
|
||||
{
|
||||
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos++]) : traits_type::eof();
|
||||
}
|
||||
return std::streambuf::uflow();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_data;
|
||||
std::size_t m_pos = 0;
|
||||
std::size_t m_chunk_size;
|
||||
};
|
||||
|
||||
// how a test reads from the stream
|
||||
enum class read_mode
|
||||
{
|
||||
extract, // up to four times operator>>, as for concatenated values
|
||||
parse, // json::parse(stream)
|
||||
parse_lenient, // json::parse(stream) with comments
|
||||
};
|
||||
|
||||
// Read @a input from a chunked_streambuf and describe everything observable:
|
||||
// the values or the exception, the stream state after each value, and the
|
||||
// characters left in the stream.
|
||||
std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode)
|
||||
{
|
||||
chunked_streambuf buf(input, chunk_size);
|
||||
std::istream is(&buf);
|
||||
std::string result;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
json j;
|
||||
try
|
||||
{
|
||||
if (mode == read_mode::extract)
|
||||
{
|
||||
is >> j;
|
||||
}
|
||||
else
|
||||
{
|
||||
j = json::parse(is, nullptr, true, mode == read_mode::parse_lenient);
|
||||
}
|
||||
result += "value " + j.dump() + "; state " + std::to_string(static_cast<unsigned int>(is.rdstate())) + "\n";
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
result += std::string("exception ") + e.what() + "\n";
|
||||
break;
|
||||
}
|
||||
if (mode != read_mode::extract)
|
||||
{
|
||||
break;
|
||||
}
|
||||
is.clear();
|
||||
}
|
||||
|
||||
is.clear();
|
||||
result += "remaining ";
|
||||
char c = 0;
|
||||
while (is.get(c))
|
||||
{
|
||||
result += c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// get area sizes that put every boundary everywhere in short inputs, plus one
|
||||
// that holds most inputs completely
|
||||
std::vector<std::size_t> chunk_sizes()
|
||||
{
|
||||
return {1, 2, 3, 4, 5, 7, 8, 9, 13, 16, 64, 4096};
|
||||
}
|
||||
|
||||
// valid and invalid inputs covering every token type and every bulk path:
|
||||
// strings (plain runs, escapes, UTF-8), numbers (all grammar parts, too large
|
||||
// for an integer), and indentation
|
||||
std::vector<std::string> corpus()
|
||||
{
|
||||
return
|
||||
{
|
||||
// numbers, terminated in every way
|
||||
"0", "-0", "123", "-123", "1.5", "-1.5e+10", "2E-3", "123456789012345678901234567890",
|
||||
"18446744073709551615", "18446744073709551616", "-9223372036854775809",
|
||||
"123 456", "123,456", "123]", "1true", "42\nfoo", "1.25e3}", "7\t8\r9",
|
||||
// malformed numbers
|
||||
"1.", "-", "1e", "1e+", "01", "-a", "1.x", "1ee5",
|
||||
// strings
|
||||
R"("")", R"("abc")", R"("a long string with a run of ordinary characters to scan in bulk")",
|
||||
R"("escapes \" \\ \/ \b \f \n \r \t é 😀")",
|
||||
"\"UTF-8: \xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80 done\"",
|
||||
// malformed strings
|
||||
R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")",
|
||||
R"("\ud800 lone surrogate")",
|
||||
// literals
|
||||
"true", "false", "null", "tru", "nul", "falsey",
|
||||
// structures
|
||||
"[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]",
|
||||
// pretty-printed
|
||||
"{\n \"key\": [\n 1,\n -2.5,\n \"three\"\n ]\n}",
|
||||
"[\n \"deep\"\n ]",
|
||||
"{\n \"a\": 1,\n \"b\": 2x\n}",
|
||||
// concatenated values and whitespace
|
||||
"1 2 3", R"("a""b")", "[1][2]", "{}{}", " \n\t 42 \n\t ", "",
|
||||
// comments (only accepted by parse_lenient)
|
||||
"/* comment */ 1", "// comment\n[1, /* c */ 2]",
|
||||
// byte order mark
|
||||
"\xef\xbb\xbf[1]",
|
||||
};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("parsing from a std::istream scans the get area in bulk")
|
||||
{
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("results do not depend on the get area")
|
||||
{
|
||||
const std::vector<read_mode> read_modes = {read_mode::extract, read_mode::parse, read_mode::parse_lenient};
|
||||
for (const auto& input : corpus())
|
||||
{
|
||||
for (const read_mode mode : read_modes)
|
||||
{
|
||||
// no get area: the character-at-a-time path
|
||||
const std::string expected = observe(input, 0, mode);
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(input)
|
||||
CAPTURE(chunk_size)
|
||||
CAPTURE(static_cast<int>(mode))
|
||||
CHECK(observe(input, chunk_size, mode) == expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !defined(JSON_NOEXCEPTION)
|
||||
|
||||
SECTION("values are the same as when parsing from a string")
|
||||
{
|
||||
const std::string input = R"({"name": "nlohmann::json", "numbers": [1, -2, 3.5, 4e10, 18446744073709551615],)"
|
||||
"\n \"text\": \"caf\xc3\xa9 \\u00e9 \\\"quoted\\\"\", \"nested\": [[{}], [[]]], \"flags\": [true, false, null]}";
|
||||
const json expected = json::parse(input);
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf(input, chunk_size);
|
||||
std::istream is(&buf);
|
||||
CHECK(json::parse(is) == expected);
|
||||
}
|
||||
|
||||
std::istringstream ss(input);
|
||||
CHECK(json::parse(ss) == expected);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("a token that spans several get areas is reported completely")
|
||||
{
|
||||
// the error is found after the string has crossed many get areas; the
|
||||
// token string holds everything since the start of the last token
|
||||
const std::string input = R"(["this string is longer than every get area", tru])";
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf(input, chunk_size);
|
||||
std::istream is(&buf);
|
||||
json j;
|
||||
CHECK_THROWS_WITH_AS(is >> j,
|
||||
"[json.exception.parse_error.101] parse error at line 1, column 50: syntax error while parsing value - invalid literal; last read: '\"this string is longer than every get area\", tru]'",
|
||||
json::parse_error&);
|
||||
}
|
||||
|
||||
const std::string unterminated = R"("this string is longer than every get area)";
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf(unterminated, chunk_size);
|
||||
std::istream is(&buf);
|
||||
json j;
|
||||
CHECK_THROWS_WITH_AS(is >> j,
|
||||
"[json.exception.parse_error.101] parse error at line 1, column 43: syntax error while parsing value - invalid string: missing closing quote; last read: '\"this string is longer than every get area'",
|
||||
json::parse_error&);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("an error before the first token reports everything read")
|
||||
{
|
||||
chunked_streambuf buf(" \n x", 2);
|
||||
std::istream is(&buf);
|
||||
json j;
|
||||
CHECK_THROWS_WITH_AS(is >> j,
|
||||
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - invalid literal; last read: ' <U+000A> x'",
|
||||
json::parse_error&);
|
||||
}
|
||||
|
||||
SECTION("lines and columns are counted across get areas")
|
||||
{
|
||||
const std::string input = "[\n 1,\n \"two\",\n 3.0,\n x\n]";
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf(input, chunk_size);
|
||||
std::istream is(&buf);
|
||||
json j;
|
||||
CHECK_THROWS_WITH_AS(is >> j,
|
||||
"[json.exception.parse_error.101] parse error at line 5, column 5: syntax error while parsing value - invalid literal; last read: '3.0,<U+000A> x'",
|
||||
json::parse_error&);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !defined(JSON_NOEXCEPTION)
|
||||
|
||||
SECTION("many values from one stream")
|
||||
{
|
||||
std::string input;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
input += R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","score":)" + std::to_string(i) + ".5}\n";
|
||||
input += std::to_string(i * 7919) + "\n";
|
||||
}
|
||||
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf(input, chunk_size);
|
||||
std::istream is(&buf);
|
||||
json j;
|
||||
int count = 0;
|
||||
while (!(is >> std::ws).eof())
|
||||
{
|
||||
is >> j;
|
||||
if (count % 2 == 0)
|
||||
{
|
||||
CHECK(j["id"] == count / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(j == (count / 2) * 7919);
|
||||
}
|
||||
++count;
|
||||
}
|
||||
CHECK(count == 400);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the stream can be used after parsing")
|
||||
{
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf(R"({"a": "some text"} trailing words 42)", chunk_size);
|
||||
std::istream is(&buf);
|
||||
json j;
|
||||
is >> j;
|
||||
CHECK(j == json({{"a", "some text"}}));
|
||||
|
||||
std::string word;
|
||||
int number = 0;
|
||||
is >> word;
|
||||
CHECK(word == "trailing");
|
||||
is >> word >> number;
|
||||
CHECK(word == "words");
|
||||
CHECK(number == 42);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("std::stringstream and std::ifstream")
|
||||
{
|
||||
const std::string input = R"([1, 2.5, "three", {"four": [true, null]}] 5)";
|
||||
|
||||
std::stringstream ss(input);
|
||||
json j1;
|
||||
json j2;
|
||||
ss >> j1 >> j2;
|
||||
CHECK(j1 == json::parse(R"([1, 2.5, "three", {"four": [true, null]}])"));
|
||||
CHECK(j2 == 5);
|
||||
CHECK(ss.eof());
|
||||
|
||||
// a file name of its own for every run: the test runs for several C++
|
||||
// standards and configurations at the same time, in the same directory
|
||||
std::random_device rd;
|
||||
const std::string temp_name = "unit-stream-bulk-scan-" + std::to_string(rd()) + ".tmp";
|
||||
const char* const filename = temp_name.c_str();
|
||||
{
|
||||
std::ofstream file(filename);
|
||||
file << input;
|
||||
}
|
||||
{
|
||||
std::ifstream file(filename);
|
||||
json k1;
|
||||
json k2;
|
||||
file >> k1 >> k2;
|
||||
CHECK(k1 == j1);
|
||||
CHECK(k2 == j2);
|
||||
}
|
||||
static_cast<void>(std::remove(filename));
|
||||
}
|
||||
|
||||
SECTION("binary formats read from a stream")
|
||||
{
|
||||
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
|
||||
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
|
||||
const std::string bytes(reinterpret_cast<const char*>(cbor.data()), cbor.size());
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf(bytes, chunk_size);
|
||||
std::istream is(&buf);
|
||||
CHECK(json::from_cbor(is) == j);
|
||||
}
|
||||
}
|
||||
|
||||
#if TEST_STREAM_BULK_SCAN
|
||||
SECTION("the adapter keeps no token unless the lexer starts one")
|
||||
{
|
||||
// the binary readers read through the same adapter, but never start a
|
||||
// token; refilling the get area must then not collect the input
|
||||
chunked_streambuf buf(std::string(1000, 'x'), 7);
|
||||
std::istream is(&buf);
|
||||
nlohmann::detail::input_stream_adapter ia(is);
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
CHECK(ia.get_character() == 'x');
|
||||
}
|
||||
|
||||
std::vector<char> token;
|
||||
ia.copy_token(token, false);
|
||||
CHECK(token.empty());
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("SAX parsing from a stream")
|
||||
{
|
||||
const std::string input = R"({"a": [1, "two", 3.5]} rest)";
|
||||
for (const std::size_t chunk_size : chunk_sizes())
|
||||
{
|
||||
CAPTURE(chunk_size)
|
||||
chunked_streambuf buf(input, chunk_size);
|
||||
std::istream is(&buf);
|
||||
json j;
|
||||
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(j);
|
||||
CHECK(json::sax_parse(is, &sax, json::input_format_t::json, false));
|
||||
CHECK(j == json::parse(R"({"a": [1, "two", 3.5]})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -846,3 +846,85 @@ TEST_CASE("choice of the conversion")
|
||||
DOCTEST_MSVC_SUPPRESS_WARNING_POP
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("short decimals")
|
||||
{
|
||||
// write_short_decimal() writes digits * 10^exp for the digits of a double
|
||||
// that need no conversion (at most 15, the first not 0): as to_chars()
|
||||
// writes the (positive) double that has these digits
|
||||
const auto written = [](std::uint64_t digits, int exp) -> std::string
|
||||
{
|
||||
std::array<char, 64> buf{}; // (up to 41 bytes are written)
|
||||
char* const end = nlohmann::detail::dtoa_impl::write_short_decimal(buf.data(), digits, exp);
|
||||
return {buf.data(), end};
|
||||
};
|
||||
const auto written_counted = [](std::uint64_t digits, int count, int exp) -> std::string
|
||||
{
|
||||
std::array<char, 64> buf{};
|
||||
char* const end = nlohmann::detail::dtoa_impl::write_short_decimal(buf.data(), digits, count, exp);
|
||||
return {buf.data(), end};
|
||||
};
|
||||
const auto expected = [](std::uint64_t digits, int exp) -> std::string
|
||||
{
|
||||
const double value = std::strtod((std::to_string(digits) + "e" + std::to_string(exp)).c_str(), nullptr);
|
||||
std::array<char, 64> buf{};
|
||||
char* const end = nlohmann::detail::to_chars(buf.data(), buf.data() + 32, value);
|
||||
return {buf.data(), end};
|
||||
};
|
||||
|
||||
SECTION("powers of ten")
|
||||
{
|
||||
const auto& powers = nlohmann::detail::dtoa_impl::powers_of_ten();
|
||||
CHECK(powers.front() == 1u);
|
||||
for (std::size_t i = 1; i < powers.size(); ++i)
|
||||
{
|
||||
CHECK(powers[i] == powers[i - 1] * 10u);
|
||||
}
|
||||
CHECK(powers.back() == 10000000000000000000u);
|
||||
}
|
||||
|
||||
SECTION("examples")
|
||||
{
|
||||
CHECK(written(1, 0) == "1.0");
|
||||
CHECK(written(15, -1) == "1.5");
|
||||
CHECK(written(125, -2) == "1.25");
|
||||
CHECK(written(1, 22) == "1e+22");
|
||||
CHECK(written(123456789012345, -2) == "1234567890123.45");
|
||||
CHECK(written(999999999999999, -15) == "0.999999999999999");
|
||||
CHECK(written(5, -324) == "5e-324");
|
||||
CHECK(written_counted(1, 1, 0) == "1.0");
|
||||
CHECK(written_counted(125, 3, -2) == "1.25");
|
||||
CHECK(written_counted(100, 3, -2) == "1.0");
|
||||
CHECK(written_counted(999999999999999, 15, -15) == "0.999999999999999");
|
||||
}
|
||||
|
||||
SECTION("random digits, exponents and trailing zeros")
|
||||
{
|
||||
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (int i = 0; i < 100000; ++i)
|
||||
{
|
||||
// 1 to 15 digits, the first not 0, and up to 14 of them trailing zeros
|
||||
std::uint64_t count = 1 + (rng() % 15u);
|
||||
std::uint64_t power = 1;
|
||||
for (std::uint64_t k = 1; k < count; ++k)
|
||||
{
|
||||
power *= 10;
|
||||
}
|
||||
std::uint64_t digits = power + (rng() % (9 * power));
|
||||
const std::uint64_t zeros = (rng() % 3u == 0) ? (rng() % count) : 0;
|
||||
for (std::uint64_t k = 0; k < zeros; ++k)
|
||||
{
|
||||
digits = (digits / 10) * 10;
|
||||
}
|
||||
// (a value between 1e-300 and 1e300)
|
||||
const int exp = static_cast<int>(rng() % 560u) - 300 - static_cast<int>(count);
|
||||
|
||||
CAPTURE(digits)
|
||||
CAPTURE(count)
|
||||
CAPTURE(exp)
|
||||
const std::string want = expected(digits, exp);
|
||||
CHECK(written(digits, exp) == want);
|
||||
CHECK(written_counted(digits, static_cast<int>(count), exp) == want);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user