Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1824a43cd | ||
|
|
0e95fe8200 |
No files matched your search
@@ -118,7 +118,7 @@ endif()
|
|||||||
|
|
||||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
||||||
# avoid stack overflow, see https://github.com/nlohmann/json/issues/2955
|
# avoid stack overflow, see https://github.com/nlohmann/json/issues/2955
|
||||||
json_test_set_test_options("test-bon8;test-cbor;test-msgpack;test-ubjson;test-bjdata;test-binary_formats" LINK_OPTIONS /STACK:4000000)
|
json_test_set_test_options("test-bon8;test-cbor;test-msgpack;test-ubjson;test-bjdata;test-bjdata_round_trip_draft2;test-bjdata_round_trip_draft3;test-binary_formats" LINK_OPTIONS /STACK:4000000)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# disable exceptions for test-disabled_exceptions
|
# disable exceptions for test-disabled_exceptions
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ 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*` |
|
| `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 |
|
| `FromBinaryShape` | read deeply nested, container-heavy and scalar-heavy documents in every binary format |
|
||||||
| `FromCborChunkedString` | read CBOR strings split into indefinite-length chunks |
|
| `FromCborChunkedString` | read CBOR strings split into indefinite-length chunks |
|
||||||
|
| `SaxParse` | parse with a SAX handler that only counts events, so no values are built |
|
||||||
|
| `Copy`, `CopyAssign`, `CopyRing` | copy a parsed document and destroy a copy, both timed: a temporary copy, an assignment over an earlier copy, and replacing the oldest of eight live copies |
|
||||||
|
| `CopyOnly`, `DestroyOnly` | only the copy or only the destruction, the other half untimed; a fast destructor can leave work to the next allocations, so these tell the halves apart but do not compare versions |
|
||||||
|
| `Equal`, `Iterate` | compare a parsed document with a copy, or visit every value with range-based `for` loops |
|
||||||
|
| `Flatten`, `FlattenOrdered`, `Unflatten` | `flatten()` for `json` and `ordered_json`, and `unflatten()` |
|
||||||
|
| `Diff`, `Patch` | `diff()` against the document with every eighth scalar changed, and applying that patch |
|
||||||
|
| `JsonPointerParse`, `JsonPointerToString`, `JsonPointerAt`, `JsonPointerContains` | the JSON pointers of every scalar in a document: parse them from strings, turn them back into strings, look them up with `at()` and `contains()` |
|
||||||
|
| `GetVectorInt`, `GetVectorDouble`, `GetMap` | `get<std::vector<std::int64_t>>()`, `get<std::vector<double>>()` and `get<std::map<std::string, json>>()` on large arrays and a large flat object |
|
||||||
|
| `ToJsonStrings` | create a `json` array from a `std::vector` of 100,000 strings, copied or moved, and destroy it again |
|
||||||
|
|
||||||
The input files are those of [nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark) (`canada`,
|
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`, ...).
|
`citm_catalog`, `twitter`), a large `jeopardy` file, and number-heavy files (`floats`, `signed_ints`, ...).
|
||||||
|
|||||||
@@ -8,11 +8,14 @@
|
|||||||
|
|
||||||
#include <benchmark/benchmark.h>
|
#include <benchmark/benchmark.h>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <fstream>
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <fstream>
|
||||||
|
#include <map>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <test_data.hpp>
|
#include <test_data.hpp>
|
||||||
|
|
||||||
@@ -624,6 +627,584 @@ static void FromCborChunkedString(benchmark::State& state, const std::size_t chu
|
|||||||
|
|
||||||
BENCHMARK_CAPTURE(FromCborChunkedString, 10000 chunks, 10000);
|
BENCHMARK_CAPTURE(FromCborChunkedString, 10000 chunks, 10000);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// SAX-parse JSON from string
|
||||||
|
//
|
||||||
|
// sax_parse() with a handler that does next to nothing runs the lexer and
|
||||||
|
// parser without building values, so the gap to ParseString above is the cost
|
||||||
|
// of building the DOM.
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static std::string read_file(const char* filename)
|
||||||
|
{
|
||||||
|
std::ifstream f(filename);
|
||||||
|
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
static json load_json(const char* filename)
|
||||||
|
{
|
||||||
|
return json::parse(read_file(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SAX handler that only counts the events, so that the handler costs next to nothing
|
||||||
|
class counting_sax : public nlohmann::json_sax<json>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::size_t events = 0;
|
||||||
|
|
||||||
|
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 json::exception& /*ex*/) override
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool count()
|
||||||
|
{
|
||||||
|
++events;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void SaxParse(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const std::string str = read_file(filename);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
counting_sax sax;
|
||||||
|
bool result = json::sax_parse(str, &sax);
|
||||||
|
benchmark::DoNotOptimize(result);
|
||||||
|
benchmark::DoNotOptimize(sax.events);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetBytesProcessed(state.iterations() * str.size());
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(SaxParse, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(SaxParse, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(SaxParse, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// work with values: copy, destroy, compare, iterate
|
||||||
|
//
|
||||||
|
// The parsed test files stand in for a document an application keeps around.
|
||||||
|
// Each benchmark times one whole-document operation; setup and cleanup run
|
||||||
|
// outside the timed section.
|
||||||
|
//
|
||||||
|
// A copy is always destroyed again, and how a document is freed shapes the
|
||||||
|
// heap the next copy allocates from: a destructor that frees quickly can leave
|
||||||
|
// work to the allocator's next calls. Copy, CopyAssign and CopyRing therefore
|
||||||
|
// time the copy together with the destruction of a copy; CopyOnly and
|
||||||
|
// DestroyOnly time one half each and are meant for telling the halves apart,
|
||||||
|
// not for comparing versions.
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/// copy a document and destroy the copy again, as a temporary copy is used
|
||||||
|
static void Copy(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
json copy = j;
|
||||||
|
benchmark::DoNotOptimize(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(Copy, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(Copy, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(Copy, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
/// copy-assign a document over an earlier copy of it, which is freed in the process
|
||||||
|
static void CopyAssign(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
json target = j;
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
target = j;
|
||||||
|
benchmark::DoNotOptimize(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(CopyAssign, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(CopyAssign, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(CopyAssign, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
/// keep eight copies alive and replace the oldest one, as a cache or an undo
|
||||||
|
/// history does, so that the copy is not made from memory just freed by an
|
||||||
|
/// identical document
|
||||||
|
static void CopyRing(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
std::vector<json> ring(8, j);
|
||||||
|
std::size_t oldest = 0;
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
ring[oldest] = j;
|
||||||
|
benchmark::DoNotOptimize(ring[oldest]);
|
||||||
|
oldest = (oldest + 1) % ring.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(CopyRing, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(CopyRing, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(CopyRing, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
/// only the copy; the copy is destroyed with the timer paused (diagnostic)
|
||||||
|
static void CopyOnly(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
auto* copy = new json(j);
|
||||||
|
|
||||||
|
state.PauseTiming();
|
||||||
|
delete copy;
|
||||||
|
state.ResumeTiming();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(CopyOnly, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(CopyOnly, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(CopyOnly, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
/// only the destruction; the copy is made with the timer paused (diagnostic)
|
||||||
|
static void DestroyOnly(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
state.PauseTiming();
|
||||||
|
auto* copy = new json(j);
|
||||||
|
state.ResumeTiming();
|
||||||
|
|
||||||
|
delete copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(DestroyOnly, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(DestroyOnly, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(DestroyOnly, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
static void Equal(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
// two separate values, so that the comparison cannot stop at identical addresses
|
||||||
|
const json j = load_json(filename);
|
||||||
|
const json copy = j;
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
bool result = (j == copy);
|
||||||
|
benchmark::DoNotOptimize(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(Equal, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(Equal, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(Equal, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
/// visit every value with range-based for loops, as client code walks a document
|
||||||
|
static std::size_t count_values(const json& j)
|
||||||
|
{
|
||||||
|
std::size_t result = 1;
|
||||||
|
if (j.is_structured())
|
||||||
|
{
|
||||||
|
for (const auto& element : j)
|
||||||
|
{
|
||||||
|
result += count_values(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Iterate(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
std::size_t result = count_values(j);
|
||||||
|
benchmark::DoNotOptimize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(count_values(j)));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(Iterate, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(Iterate, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(Iterate, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// flatten and unflatten
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static void Flatten(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
json flat = j.flatten();
|
||||||
|
benchmark::DoNotOptimize(flat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(Flatten, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(Flatten, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(Flatten, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
// ordered_json objects look keys up linearly, which flatten() used to do for
|
||||||
|
// every value; canada (330,000 values in one flat object) is left out, as it
|
||||||
|
// would take minutes with such versions
|
||||||
|
static void FlattenOrdered(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const nlohmann::ordered_json j = nlohmann::ordered_json::parse(read_file(filename));
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
nlohmann::ordered_json flat = j.flatten();
|
||||||
|
benchmark::DoNotOptimize(flat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(FlattenOrdered, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(FlattenOrdered, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
static void Unflatten(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json flat = load_json(filename).flatten();
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
json j = flat.unflatten();
|
||||||
|
benchmark::DoNotOptimize(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(Unflatten, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(Unflatten, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(Unflatten, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// diff and patch
|
||||||
|
//
|
||||||
|
// The target is the document with every eighth scalar changed, so the patch
|
||||||
|
// has one "replace" operation per changed value and the structure stays the
|
||||||
|
// same.
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static void change_every_eighth_scalar(json& j, std::size_t& counter)
|
||||||
|
{
|
||||||
|
if (j.is_structured())
|
||||||
|
{
|
||||||
|
for (auto& element : j)
|
||||||
|
{
|
||||||
|
change_every_eighth_scalar(element, counter);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++counter % 8 != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (j.type())
|
||||||
|
{
|
||||||
|
case json::value_t::number_integer:
|
||||||
|
j = j.get<json::number_integer_t>() + 1;
|
||||||
|
break;
|
||||||
|
case json::value_t::number_unsigned:
|
||||||
|
j = j.get<json::number_unsigned_t>() + 1;
|
||||||
|
break;
|
||||||
|
case json::value_t::number_float:
|
||||||
|
j = j.get<json::number_float_t>() + 0.5;
|
||||||
|
break;
|
||||||
|
case json::value_t::string:
|
||||||
|
j = j.get<std::string>() + "!";
|
||||||
|
break;
|
||||||
|
case json::value_t::boolean:
|
||||||
|
j = !j.get<bool>();
|
||||||
|
break;
|
||||||
|
case json::value_t::null:
|
||||||
|
case json::value_t::object:
|
||||||
|
case json::value_t::array:
|
||||||
|
case json::value_t::binary:
|
||||||
|
case json::value_t::discarded:
|
||||||
|
default:
|
||||||
|
j = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static json changed_copy(const json& j)
|
||||||
|
{
|
||||||
|
json result = j;
|
||||||
|
std::size_t counter = 0;
|
||||||
|
change_every_eighth_scalar(result, counter);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Diff(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json source = load_json(filename);
|
||||||
|
const json target = changed_copy(source);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
json patch = json::diff(source, target);
|
||||||
|
benchmark::DoNotOptimize(patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(json::diff(source, target).size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(Diff, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(Diff, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(Diff, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
static void Patch(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json source = load_json(filename);
|
||||||
|
const json patch = json::diff(source, changed_copy(source));
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
json result = source.patch(patch);
|
||||||
|
benchmark::DoNotOptimize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(patch.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(Patch, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(Patch, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(Patch, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// JSON Pointer
|
||||||
|
//
|
||||||
|
// The pointers are those of every scalar in the document, i.e., the keys of
|
||||||
|
// its flattened form.
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static std::vector<std::string> pointer_strings(const json& j)
|
||||||
|
{
|
||||||
|
std::vector<std::string> result;
|
||||||
|
const json flat = j.flatten();
|
||||||
|
result.reserve(flat.size());
|
||||||
|
for (auto it = flat.begin(); it != flat.end(); ++it)
|
||||||
|
{
|
||||||
|
result.push_back(it.key());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<json::json_pointer> pointers(const json& j)
|
||||||
|
{
|
||||||
|
const std::vector<std::string> strings = pointer_strings(j);
|
||||||
|
std::vector<json::json_pointer> result;
|
||||||
|
result.reserve(strings.size());
|
||||||
|
for (const auto& s : strings)
|
||||||
|
{
|
||||||
|
result.emplace_back(s);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void JsonPointerParse(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const std::vector<std::string> strings = pointer_strings(load_json(filename));
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
for (const auto& s : strings)
|
||||||
|
{
|
||||||
|
json::json_pointer ptr(s);
|
||||||
|
benchmark::DoNotOptimize(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(strings.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(JsonPointerParse, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(JsonPointerParse, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
static void JsonPointerToString(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const std::vector<json::json_pointer> ptrs = pointers(load_json(filename));
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
for (const auto& ptr : ptrs)
|
||||||
|
{
|
||||||
|
std::string s = ptr.to_string();
|
||||||
|
benchmark::DoNotOptimize(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(JsonPointerToString, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(JsonPointerToString, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
static void JsonPointerAt(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
const std::vector<json::json_pointer> ptrs = pointers(j);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
for (const auto& ptr : ptrs)
|
||||||
|
{
|
||||||
|
const json& value = j.at(ptr);
|
||||||
|
benchmark::DoNotOptimize(&value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(JsonPointerAt, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(JsonPointerAt, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
static void JsonPointerContains(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
const std::vector<json::json_pointer> ptrs = pointers(j);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
for (const auto& ptr : ptrs)
|
||||||
|
{
|
||||||
|
bool result = j.contains(ptr);
|
||||||
|
benchmark::DoNotOptimize(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(JsonPointerContains, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(JsonPointerContains, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// conversions from and to C++ containers
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static void GetVectorInt(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
auto v = j.get<std::vector<std::int64_t>>();
|
||||||
|
benchmark::DoNotOptimize(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(GetVectorInt, signed_ints, TEST_DATA_DIRECTORY "/regression/signed_ints.json");
|
||||||
|
|
||||||
|
static void GetVectorDouble(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
auto v = j.get<std::vector<double>>();
|
||||||
|
benchmark::DoNotOptimize(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(GetVectorDouble, floats, TEST_DATA_DIRECTORY "/regression/floats.json");
|
||||||
|
|
||||||
|
// a large flat object: the flattened document, one member per scalar
|
||||||
|
static void GetMap(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
const json j = load_json(filename).flatten();
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
auto m = j.get<std::map<std::string, json>>();
|
||||||
|
benchmark::DoNotOptimize(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(GetMap, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
|
||||||
|
/// 100,000 strings, each too long for the small string optimization
|
||||||
|
static std::vector<std::string> make_strings()
|
||||||
|
{
|
||||||
|
std::vector<std::string> result;
|
||||||
|
result.reserve(100000);
|
||||||
|
for (std::size_t i = 0; i < 100000; ++i)
|
||||||
|
{
|
||||||
|
result.push_back("a string that does not fit in place: " + std::to_string(i));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ToJsonStrings(benchmark::State& state, const bool move)
|
||||||
|
{
|
||||||
|
const std::vector<std::string> strings = make_strings();
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
state.PauseTiming();
|
||||||
|
std::vector<std::string> input = strings;
|
||||||
|
state.ResumeTiming();
|
||||||
|
|
||||||
|
// destroyed inside the timed section, like the copies above
|
||||||
|
auto* j = move ? new json(std::move(input)) : new json(input);
|
||||||
|
delete j;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(strings.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(ToJsonStrings, copy, false);
|
||||||
|
BENCHMARK_CAPTURE(ToJsonStrings, move, true);
|
||||||
|
|
||||||
// conversion of a JSON array into standard containers
|
// conversion of a JSON array into standard containers
|
||||||
template<typename Container>
|
template<typename Container>
|
||||||
static void GetArray(benchmark::State& state)
|
static void GetArray(benchmark::State& state)
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | 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
|
||||||
|
|
||||||
|
#include <cstdint> // uint8_t
|
||||||
|
#include <vector> // vector
|
||||||
|
|
||||||
|
#include "doctest_compatibility.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include "round_trip_corpus.hpp"
|
||||||
|
|
||||||
|
// The BJData round-trip invariants, split by the BJData version that turns a
|
||||||
|
// corpus value into j1 (unit-bjdata_round_trip_draft2.cpp and
|
||||||
|
// unit-bjdata_round_trip_draft3.cpp). They were part of unit-bjdata.cpp, but
|
||||||
|
// took 80 % of its time; as separate tests, both halves run in parallel and
|
||||||
|
// stay below the ctest timeout under Valgrind.
|
||||||
|
namespace utils
|
||||||
|
{
|
||||||
|
|
||||||
|
// This checks what the parse_bjdata_fuzzer driver checks (see
|
||||||
|
// tests/src/fuzzer-parse_bjdata.cpp), so that a regression shows up in CI
|
||||||
|
// rather than as an OSS-Fuzz report: every value from_bjdata() returns
|
||||||
|
// (j1) can be serialized with any combination of options, the result can
|
||||||
|
// be parsed back (j2), and serializing j2 again with the same options
|
||||||
|
// yields a value-equal result.
|
||||||
|
//
|
||||||
|
// Beyond the driver, this also checks that j2 equals j1 and that
|
||||||
|
// serializing j2 reproduces the exact bytes, both except for values that
|
||||||
|
// contain a binary value: a binary value is only written as a binary
|
||||||
|
// value with Draft 3's optimized binary array, and otherwise read back as
|
||||||
|
// an array of integers, for which the writer may choose different (but
|
||||||
|
// equally valid) type markers when it is serialized again (see #5494).
|
||||||
|
//
|
||||||
|
// Values are compared with dump() rather than operator==, because a NaN
|
||||||
|
// never compares equal to itself.
|
||||||
|
//
|
||||||
|
// Only the corpus values written with @a initial_version become j1; each j1
|
||||||
|
// is then serialized with every combination of options, both versions
|
||||||
|
// included.
|
||||||
|
inline void check_bjdata_round_trip(const nlohmann::json::bjdata_version_t initial_version)
|
||||||
|
{
|
||||||
|
using nlohmann::json;
|
||||||
|
|
||||||
|
struct options
|
||||||
|
{
|
||||||
|
bool use_size;
|
||||||
|
bool use_type;
|
||||||
|
json::bjdata_version_t version;
|
||||||
|
};
|
||||||
|
const std::vector<options> all_options =
|
||||||
|
{
|
||||||
|
{false, false, json::bjdata_version_t::draft2},
|
||||||
|
{true, false, json::bjdata_version_t::draft2},
|
||||||
|
{true, true, json::bjdata_version_t::draft2},
|
||||||
|
{false, false, json::bjdata_version_t::draft3},
|
||||||
|
{true, false, json::bjdata_version_t::draft3},
|
||||||
|
{true, true, json::bjdata_version_t::draft3},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& j0 : utils::round_trip_corpus::values())
|
||||||
|
{
|
||||||
|
// turn the corpus value into a value as from_bjdata() returns it
|
||||||
|
for (const auto& initial : all_options)
|
||||||
|
{
|
||||||
|
if (initial.version != initial_version)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json j1 = json::from_bjdata(json::to_bjdata(j0, initial.use_size, initial.use_type, initial.version));
|
||||||
|
const bool has_binary = utils::round_trip_corpus::contains_binary(j1);
|
||||||
|
|
||||||
|
for (const auto& o : all_options)
|
||||||
|
{
|
||||||
|
INFO("j1 = " << j1.dump() << ", use_size = " << o.use_size << ", use_type = " << o.use_type
|
||||||
|
<< ", draft3 = " << (o.version == json::bjdata_version_t::draft3));
|
||||||
|
|
||||||
|
const std::vector<std::uint8_t> vec = json::to_bjdata(j1, o.use_size, o.use_type, o.version);
|
||||||
|
json j2;
|
||||||
|
// anything the library writes must be parsable by the library
|
||||||
|
REQUIRE_NOTHROW(j2 = json::from_bjdata(vec));
|
||||||
|
const std::vector<std::uint8_t> vec2 = json::to_bjdata(j2, o.use_size, o.use_type, o.version);
|
||||||
|
CHECK(json::from_bjdata(vec2).dump() == j2.dump());
|
||||||
|
|
||||||
|
if (!has_binary)
|
||||||
|
{
|
||||||
|
CHECK(j2.dump() == j1.dump());
|
||||||
|
CHECK(vec2 == vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace utils
|
||||||
@@ -28,7 +28,6 @@ using ordered_json = nlohmann::ordered_json;
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include "make_test_data_available.hpp"
|
#include "make_test_data_available.hpp"
|
||||||
#include "round_trip_corpus.hpp"
|
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
#include "sax_countdown.hpp"
|
#include "sax_countdown.hpp"
|
||||||
using utils::SaxCountdown;
|
using utils::SaxCountdown;
|
||||||
@@ -4441,70 +4440,6 @@ TEST_CASE("BJData use_type requires use_size")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BJData round-trip invariants")
|
|
||||||
{
|
|
||||||
// This checks what the parse_bjdata_fuzzer driver checks (see
|
|
||||||
// tests/src/fuzzer-parse_bjdata.cpp), so that a regression shows up in CI
|
|
||||||
// rather than as an OSS-Fuzz report: every value from_bjdata() returns
|
|
||||||
// (j1) can be serialized with any combination of options, the result can
|
|
||||||
// be parsed back (j2), and serializing j2 again with the same options
|
|
||||||
// yields a value-equal result.
|
|
||||||
//
|
|
||||||
// Beyond the driver, this also checks that j2 equals j1 and that
|
|
||||||
// serializing j2 reproduces the exact bytes, both except for values that
|
|
||||||
// contain a binary value: a binary value is only written as a binary
|
|
||||||
// value with Draft 3's optimized binary array, and otherwise read back as
|
|
||||||
// an array of integers, for which the writer may choose different (but
|
|
||||||
// equally valid) type markers when it is serialized again (see #5494).
|
|
||||||
//
|
|
||||||
// Values are compared with dump() rather than operator==, because a NaN
|
|
||||||
// never compares equal to itself.
|
|
||||||
struct options
|
|
||||||
{
|
|
||||||
bool use_size;
|
|
||||||
bool use_type;
|
|
||||||
json::bjdata_version_t version;
|
|
||||||
};
|
|
||||||
const std::vector<options> all_options =
|
|
||||||
{
|
|
||||||
{false, false, json::bjdata_version_t::draft2},
|
|
||||||
{true, false, json::bjdata_version_t::draft2},
|
|
||||||
{true, true, json::bjdata_version_t::draft2},
|
|
||||||
{false, false, json::bjdata_version_t::draft3},
|
|
||||||
{true, false, json::bjdata_version_t::draft3},
|
|
||||||
{true, true, json::bjdata_version_t::draft3},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& j0 : utils::round_trip_corpus::values())
|
|
||||||
{
|
|
||||||
// turn the corpus value into a value as from_bjdata() returns it
|
|
||||||
for (const auto& initial : all_options)
|
|
||||||
{
|
|
||||||
const json j1 = json::from_bjdata(json::to_bjdata(j0, initial.use_size, initial.use_type, initial.version));
|
|
||||||
const bool has_binary = utils::round_trip_corpus::contains_binary(j1);
|
|
||||||
|
|
||||||
for (const auto& o : all_options)
|
|
||||||
{
|
|
||||||
INFO("j1 = " << j1.dump() << ", use_size = " << o.use_size << ", use_type = " << o.use_type
|
|
||||||
<< ", draft3 = " << (o.version == json::bjdata_version_t::draft3));
|
|
||||||
|
|
||||||
const std::vector<std::uint8_t> vec = json::to_bjdata(j1, o.use_size, o.use_type, o.version);
|
|
||||||
json j2;
|
|
||||||
// anything the library writes must be parsable by the library
|
|
||||||
REQUIRE_NOTHROW(j2 = json::from_bjdata(vec));
|
|
||||||
const std::vector<std::uint8_t> vec2 = json::to_bjdata(j2, o.use_size, o.use_type, o.version);
|
|
||||||
CHECK(json::from_bjdata(vec2).dump() == j2.dump());
|
|
||||||
|
|
||||||
if (!has_binary)
|
|
||||||
{
|
|
||||||
CHECK(j2.dump() == j1.dump());
|
|
||||||
CHECK(vec2 == vec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("BJData round trip of a binary value is value-stable, not byte-stable")
|
TEST_CASE("BJData round trip of a binary value is value-stable, not byte-stable")
|
||||||
{
|
{
|
||||||
// OSS-Fuzz issue 474480402: a Draft 3 optimized binary array is read as a
|
// OSS-Fuzz issue 474480402: a Draft 3 optimized binary array is read as a
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | 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 "bjdata_round_trip.hpp"
|
||||||
|
|
||||||
|
// split from unit-bjdata.cpp, see bjdata_round_trip.hpp; the other half is in
|
||||||
|
// unit-bjdata_round_trip_draft3.cpp
|
||||||
|
|
||||||
|
TEST_CASE("BJData round-trip invariants (j1 written with Draft 2)")
|
||||||
|
{
|
||||||
|
utils::check_bjdata_round_trip(json::bjdata_version_t::draft2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | 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 "bjdata_round_trip.hpp"
|
||||||
|
|
||||||
|
// split from unit-bjdata.cpp, see bjdata_round_trip.hpp; the other half is in
|
||||||
|
// unit-bjdata_round_trip_draft2.cpp
|
||||||
|
|
||||||
|
TEST_CASE("BJData round-trip invariants (j1 written with Draft 3)")
|
||||||
|
{
|
||||||
|
utils::check_bjdata_round_trip(json::bjdata_version_t::draft3);
|
||||||
|
}
|
||||||
@@ -13,8 +13,6 @@ using nlohmann::json;
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
TEST_CASE("tests on very large JSONs")
|
TEST_CASE("tests on very large JSONs")
|
||||||
{
|
{
|
||||||
@@ -55,24 +53,6 @@ const json* innermost_value(const json& j, std::size_t& depth)
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The text of a value nested depth levels deep around the number 0. Level i is
|
|
||||||
// an array if pattern[i % pattern.size()] is '[', and otherwise an object with
|
|
||||||
// the single member "a", which every object type enumerates in the same order.
|
|
||||||
std::string nested_text(std::size_t depth, const std::string& pattern)
|
|
||||||
{
|
|
||||||
std::string text;
|
|
||||||
std::string closing;
|
|
||||||
for (std::size_t i = 0; i < depth; ++i)
|
|
||||||
{
|
|
||||||
const bool array = pattern[i % pattern.size()] == '[';
|
|
||||||
text += array ? "[" : "{\"a\":";
|
|
||||||
closing += array ? ']' : '}';
|
|
||||||
}
|
|
||||||
text += '0';
|
|
||||||
text.append(closing.rbegin(), closing.rend());
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST_CASE("tests on deeply nested JSONs")
|
TEST_CASE("tests on deeply nested JSONs")
|
||||||
@@ -244,385 +224,4 @@ TEST_CASE("tests on deeply nested JSONs")
|
|||||||
CHECK(*innermost_value(j, unused) == 0);
|
CHECK(*innermost_value(j, unused) == 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("issue #5650 - stack overflow converting between specializations")
|
|
||||||
{
|
|
||||||
const std::vector<std::string> patterns = {"[", "{", "[{"};
|
|
||||||
|
|
||||||
SECTION("json to ordered_json")
|
|
||||||
{
|
|
||||||
for (const auto& pattern : patterns)
|
|
||||||
{
|
|
||||||
CAPTURE(pattern)
|
|
||||||
const std::string text = nested_text(depth, pattern);
|
|
||||||
const json j = json::parse(text);
|
|
||||||
|
|
||||||
const nlohmann::ordered_json converted = j;
|
|
||||||
CHECK(converted.dump() == text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("ordered_json to json")
|
|
||||||
{
|
|
||||||
for (const auto& pattern : patterns)
|
|
||||||
{
|
|
||||||
CAPTURE(pattern)
|
|
||||||
const std::string text = nested_text(depth, pattern);
|
|
||||||
const nlohmann::ordered_json o = nlohmann::ordered_json::parse(text);
|
|
||||||
|
|
||||||
const json converted = o;
|
|
||||||
CHECK(converted.dump() == text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("get<ordered_json>()")
|
|
||||||
{
|
|
||||||
for (const auto& pattern : patterns)
|
|
||||||
{
|
|
||||||
CAPTURE(pattern)
|
|
||||||
const std::string text = nested_text(depth, pattern);
|
|
||||||
const json j = json::parse(text);
|
|
||||||
|
|
||||||
CHECK(j.get<nlohmann::ordered_json>().dump() == text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("depths around the bound of the recursive descent")
|
|
||||||
{
|
|
||||||
for (std::size_t d = 1; d <= 300; ++d)
|
|
||||||
{
|
|
||||||
CAPTURE(d)
|
|
||||||
for (const auto& pattern : patterns)
|
|
||||||
{
|
|
||||||
CAPTURE(pattern)
|
|
||||||
const std::string text = nested_text(d, pattern);
|
|
||||||
const json j = json::parse(text);
|
|
||||||
|
|
||||||
const nlohmann::ordered_json converted = j;
|
|
||||||
CHECK(converted.dump() == text);
|
|
||||||
const json back = converted;
|
|
||||||
CHECK(back.dump() == text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("values below the bound are converted as values above it")
|
|
||||||
{
|
|
||||||
// Bury a value below the bound, where it is converted without the
|
|
||||||
// call stack, and compare it with the same value converted on its
|
|
||||||
// own by the containers' range constructors. Its objects have
|
|
||||||
// members that the two object types enumerate in different orders.
|
|
||||||
const auto bury = [](nlohmann::ordered_json value)
|
|
||||||
{
|
|
||||||
for (std::size_t i = 0; i < 200; ++i)
|
|
||||||
{
|
|
||||||
value = nlohmann::ordered_json::array({std::move(value)});
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
const auto dig = [](const json & value)
|
|
||||||
{
|
|
||||||
const json* current = &value;
|
|
||||||
for (std::size_t i = 0; i < 200; ++i)
|
|
||||||
{
|
|
||||||
current = ¤t->at(0);
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
};
|
|
||||||
|
|
||||||
nlohmann::ordered_json value = nlohmann::ordered_json::object();
|
|
||||||
value["z"] = {1, -2, 3U, 4.5, true, nullptr, "six", nlohmann::ordered_json::binary({7, 8}, 9),
|
|
||||||
nlohmann::ordered_json::binary({10}), nlohmann::ordered_json::array(), nlohmann::ordered_json::object()
|
|
||||||
};
|
|
||||||
value["y"] = {{"x", {{"w", 1}, {"v", 2}}}, {"u", {3, {{"t", 4}, {"s", 5}}}}};
|
|
||||||
value["r"] = nlohmann::ordered_json::array({nlohmann::ordered_json(nlohmann::ordered_json::value_t::discarded)});
|
|
||||||
|
|
||||||
const json converted_above = value;
|
|
||||||
const json buried = bury(value);
|
|
||||||
const json& converted_below = *dig(buried);
|
|
||||||
|
|
||||||
CHECK(converted_below.dump() == converted_above.dump());
|
|
||||||
CHECK(converted_below.at("z").at(7).get_binary().subtype() == 9);
|
|
||||||
CHECK_FALSE(converted_below.at("z").at(8).get_binary().has_subtype());
|
|
||||||
CHECK(converted_below.at("r").at(0).is_discarded());
|
|
||||||
|
|
||||||
// a discarded value is never equal to anything, so compare the rest
|
|
||||||
value.erase("r");
|
|
||||||
const json without_discarded_above = value;
|
|
||||||
const json without_discarded_buried = bury(value);
|
|
||||||
CHECK(*dig(without_discarded_buried) == without_discarded_above);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
json nested_array(const std::size_t depth, json leaf)
|
|
||||||
{
|
|
||||||
json j = std::move(leaf);
|
|
||||||
for (std::size_t i = 0; i < depth; ++i)
|
|
||||||
{
|
|
||||||
json a = json::array();
|
|
||||||
a.push_back(std::move(j));
|
|
||||||
j = std::move(a);
|
|
||||||
}
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
|
|
||||||
json nested_object(const std::size_t depth, json leaf)
|
|
||||||
{
|
|
||||||
json j = std::move(leaf);
|
|
||||||
for (std::size_t i = 0; i < depth; ++i)
|
|
||||||
{
|
|
||||||
json o = json::object();
|
|
||||||
o["k"] = std::move(j);
|
|
||||||
j = std::move(o);
|
|
||||||
}
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_CASE("issue #5392 - binary writers on deeply nested values")
|
|
||||||
{
|
|
||||||
// 200 is past the point where the writers stop recursing, and still
|
|
||||||
// shallow enough that from_* and operator== (which still recurse) are fine.
|
|
||||||
const json deep_array = nested_array(200, json(0));
|
|
||||||
const json deep_object = nested_object(200, json("x"));
|
|
||||||
const json empty_array = nested_array(200, json::array());
|
|
||||||
const json empty_object = nested_object(200, json::object());
|
|
||||||
const json mixed = nested_object(80, nested_array(80, json(true)));
|
|
||||||
|
|
||||||
SECTION("roundtrip past the recursion bound")
|
|
||||||
{
|
|
||||||
CHECK(json::from_cbor(json::to_cbor(deep_array)) == deep_array);
|
|
||||||
CHECK(json::from_msgpack(json::to_msgpack(deep_array)) == deep_array);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(deep_array)) == deep_array);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, false)) == deep_array);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, true)) == deep_array);
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(deep_array)) == deep_array);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(deep_array)) == deep_array);
|
|
||||||
|
|
||||||
CHECK(json::from_cbor(json::to_cbor(deep_object)) == deep_object);
|
|
||||||
CHECK(json::from_msgpack(json::to_msgpack(deep_object)) == deep_object);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(deep_object)) == deep_object);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(deep_object, true, true)) == deep_object);
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(deep_object)) == deep_object);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(deep_object)) == deep_object);
|
|
||||||
|
|
||||||
CHECK(json::from_cbor(json::to_cbor(empty_array)) == empty_array);
|
|
||||||
CHECK(json::from_msgpack(json::to_msgpack(empty_array)) == empty_array);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(empty_array)) == empty_array);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(empty_array, true, true)) == empty_array);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(empty_array)) == empty_array);
|
|
||||||
|
|
||||||
CHECK(json::from_cbor(json::to_cbor(empty_object)) == empty_object);
|
|
||||||
CHECK(json::from_msgpack(json::to_msgpack(empty_object)) == empty_object);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(empty_object)) == empty_object);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(empty_object)) == empty_object);
|
|
||||||
|
|
||||||
CHECK(json::from_cbor(json::to_cbor(mixed)) == mixed);
|
|
||||||
CHECK(json::from_msgpack(json::to_msgpack(mixed)) == mixed);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(mixed)) == mixed);
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(mixed)) == mixed);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(mixed)) == mixed);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("the two ways of writing a value meet at the bound")
|
|
||||||
{
|
|
||||||
for (std::size_t depth = 120; depth <= 140; ++depth)
|
|
||||||
{
|
|
||||||
CAPTURE(depth)
|
|
||||||
|
|
||||||
const json array = nested_array(depth, json(7));
|
|
||||||
CHECK(json::from_cbor(json::to_cbor(array)) == array);
|
|
||||||
CHECK(json::from_msgpack(json::to_msgpack(array)) == array);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(array, true, true)) == array);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(array)) == array);
|
|
||||||
|
|
||||||
const json object = nested_object(depth, json(7));
|
|
||||||
CHECK(json::from_cbor(json::to_cbor(object)) == object);
|
|
||||||
CHECK(json::from_msgpack(json::to_msgpack(object)) == object);
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(object)) == object);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(object)) == object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a BJData ndarray below the bound is still an ndarray")
|
|
||||||
{
|
|
||||||
const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
|
||||||
const json invalid = json({{"_ArrayType_", "nope"}, {"_ArraySize_", {1}}, {"_ArrayData_", {1}}});
|
|
||||||
|
|
||||||
const json deep_ndarray = nested_array(140, ndarray);
|
|
||||||
const json deep_invalid = nested_array(140, invalid);
|
|
||||||
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray);
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(deep_invalid)) == deep_invalid);
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(ndarray)) == ndarray);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("byte-exact across the switch-over")
|
|
||||||
{
|
|
||||||
// nested one-element arrays around the recursion bound: the exact
|
|
||||||
// bytes a writer produces do not depend on whether it stayed on the
|
|
||||||
// call stack or moved to the heap one partway through
|
|
||||||
for (const std::size_t depth :
|
|
||||||
{
|
|
||||||
nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(),
|
|
||||||
nlohmann::detail::recursion_depth_limit() + 1, nlohmann::detail::recursion_depth_limit() + 2
|
|
||||||
})
|
|
||||||
{
|
|
||||||
CAPTURE(depth)
|
|
||||||
const json array = nested_array(depth, json(0));
|
|
||||||
|
|
||||||
std::vector<std::uint8_t> expected_cbor(depth, 0x81);
|
|
||||||
expected_cbor.push_back(0x00);
|
|
||||||
CHECK(json::to_cbor(array) == expected_cbor);
|
|
||||||
|
|
||||||
std::vector<std::uint8_t> expected_msgpack(depth, 0x91);
|
|
||||||
expected_msgpack.push_back(0x00);
|
|
||||||
CHECK(json::to_msgpack(array) == expected_msgpack);
|
|
||||||
|
|
||||||
std::string expected_ubjson(depth, '[');
|
|
||||||
expected_ubjson += "i";
|
|
||||||
expected_ubjson += '\0';
|
|
||||||
expected_ubjson.append(depth, ']');
|
|
||||||
const auto packed_ubjson = json::to_ubjson(array);
|
|
||||||
CHECK(std::string(packed_ubjson.begin(), packed_ubjson.end()) == expected_ubjson);
|
|
||||||
|
|
||||||
std::vector<std::uint8_t> expected_bon8(depth, 0x81);
|
|
||||||
expected_bon8.push_back(0x90);
|
|
||||||
CHECK(json::to_bon8(array) == expected_bon8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a deep object, and a BJData ndarray, past the recursion bound")
|
|
||||||
{
|
|
||||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50;
|
|
||||||
|
|
||||||
const json object = nested_object(depth, json(42));
|
|
||||||
CHECK(json::from_cbor(json::to_cbor(object)) == object);
|
|
||||||
CHECK(json::from_msgpack(json::to_msgpack(object)) == object);
|
|
||||||
CHECK(json::from_ubjson(json::to_ubjson(object, true, true)) == object);
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(object)) == object);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(object)) == object);
|
|
||||||
|
|
||||||
const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
|
||||||
const json deep_ndarray = nested_array(depth, ndarray);
|
|
||||||
CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a discarded value past the recursion bound still throws type_error.321")
|
|
||||||
{
|
|
||||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50;
|
|
||||||
const json discarded_leaf(json::value_t::discarded);
|
|
||||||
const json deep_discarded = nested_array(depth, discarded_leaf);
|
|
||||||
|
|
||||||
// with diagnostics, the message names the path to the discarded leaf
|
|
||||||
#if JSON_DIAGNOSTICS
|
|
||||||
std::string path;
|
|
||||||
for (std::size_t i = 0; i < depth; ++i)
|
|
||||||
{
|
|
||||||
path += "/0";
|
|
||||||
}
|
|
||||||
const std::string prefix = "[json.exception.type_error.321] (" + path + ") ";
|
|
||||||
#else
|
|
||||||
const std::string prefix = "[json.exception.type_error.321] ";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
CHECK_THROWS_WITH_AS(json::to_cbor(deep_discarded), (prefix + "cannot serialize discarded value to CBOR").c_str(), json::type_error);
|
|
||||||
CHECK_THROWS_WITH_AS(json::to_msgpack(deep_discarded), (prefix + "cannot serialize discarded value to MessagePack").c_str(), json::type_error);
|
|
||||||
CHECK_THROWS_WITH_AS(json::to_ubjson(deep_discarded), (prefix + "cannot serialize discarded value to UBJSON").c_str(), json::type_error);
|
|
||||||
CHECK_THROWS_WITH_AS(json::to_bjdata(deep_discarded), (prefix + "cannot serialize discarded value to BJData").c_str(), json::type_error);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("BON8 past the recursion bound: discarded values and errors")
|
|
||||||
{
|
|
||||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50;
|
|
||||||
|
|
||||||
// BON8 rejects a discarded value (type_error.321), deep or not
|
|
||||||
const json deep_discarded = nested_array(depth, json(json::value_t::discarded));
|
|
||||||
CHECK_THROWS_AS(json::to_bon8(deep_discarded), json::type_error);
|
|
||||||
|
|
||||||
// errors are thrown from below the bound as from above it
|
|
||||||
const json deep_invalid_string = nested_array(depth, json(std::string("\x80")));
|
|
||||||
CHECK_THROWS_AS(json::to_bon8(deep_invalid_string), json::type_error);
|
|
||||||
|
|
||||||
json deep_invalid_key = json::object();
|
|
||||||
deep_invalid_key[std::string("\x80")] = 1;
|
|
||||||
deep_invalid_key = nested_object(depth, deep_invalid_key);
|
|
||||||
CHECK_THROWS_AS(json::to_bon8(deep_invalid_key), json::type_error);
|
|
||||||
|
|
||||||
const json deep_too_large = nested_array(depth, json(9223372036854775808u));
|
|
||||||
CHECK_THROWS_AS(json::to_bon8(deep_too_large), json::out_of_range);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("does not overflow the C++ stack")
|
|
||||||
{
|
|
||||||
const std::size_t depth = 100000;
|
|
||||||
const json j = json::parse(std::string(depth, '[') + "0" + std::string(depth, ']'));
|
|
||||||
|
|
||||||
std::vector<std::uint8_t> packed;
|
|
||||||
CHECK_NOTHROW(packed = json::to_cbor(j));
|
|
||||||
CHECK(json::from_cbor(packed) == j);
|
|
||||||
|
|
||||||
CHECK_NOTHROW(packed = json::to_msgpack(j));
|
|
||||||
CHECK(json::from_msgpack(packed) == j);
|
|
||||||
|
|
||||||
CHECK_NOTHROW(packed = json::to_ubjson(j));
|
|
||||||
CHECK(json::from_ubjson(packed) == j);
|
|
||||||
|
|
||||||
CHECK_NOTHROW(packed = json::to_ubjson(j, true, false));
|
|
||||||
CHECK(json::from_ubjson(packed) == j);
|
|
||||||
|
|
||||||
CHECK_NOTHROW(packed = json::to_bjdata(j));
|
|
||||||
CHECK(json::from_bjdata(packed) == j);
|
|
||||||
|
|
||||||
CHECK_NOTHROW(packed = json::to_bon8(j));
|
|
||||||
CHECK(json::from_bon8(packed) == j);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("BON8 containers with more than four elements past the recursion bound")
|
|
||||||
{
|
|
||||||
// such containers are closed with 0xFE, which the iterative writer
|
|
||||||
// must emit when it leaves them; strings next to each other must
|
|
||||||
// still be separated with 0xFF
|
|
||||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50;
|
|
||||||
json wide_array = json::array({1, "a", "b", json::array(), 5});
|
|
||||||
json wide_object = json({{"k1", "v"}, {"k2", 2}, {"k3", ""}, {"k4", json::object()}, {"k5", nullptr}});
|
|
||||||
for (std::size_t i = 0; i < depth; ++i)
|
|
||||||
{
|
|
||||||
wide_array = json::array({"s", wide_array, "t", wide_object, true});
|
|
||||||
wide_object = json({{"a", wide_object}, {"b", "x"}, {"c", "y"}, {"d", wide_array.size()}, {"e", "z"}});
|
|
||||||
}
|
|
||||||
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(wide_array)) == wide_array);
|
|
||||||
CHECK(json::from_bon8(json::to_bon8(wide_object)) == wide_object);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("regression test for https://issues.oss-fuzz.com/issues/566583014")
|
|
||||||
{
|
|
||||||
// 200000 nested one-element CBOR arrays, the innermost holding null;
|
|
||||||
// round-tripping this used to recurse once per level on the way back
|
|
||||||
// out through to_cbor(), deep enough to overflow the stack
|
|
||||||
std::vector<std::uint8_t> v(200000, 0x81);
|
|
||||||
v.push_back(0xf6);
|
|
||||||
const json j = json::from_cbor(v);
|
|
||||||
CHECK(json::to_cbor(j) == v);
|
|
||||||
|
|
||||||
// the MessagePack analogue: fixarray of 1 nesting down to nil
|
|
||||||
std::vector<std::uint8_t> v_msgpack(200000, 0x91);
|
|
||||||
v_msgpack.push_back(0xc0);
|
|
||||||
const json j_msgpack = json::from_msgpack(v_msgpack);
|
|
||||||
CHECK(json::to_msgpack(j_msgpack) == v_msgpack);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("regression test for https://issues.oss-fuzz.com/issues/572238015")
|
|
||||||
{
|
|
||||||
// the BON8 analogue: 200000 nested one-element arrays down to null
|
|
||||||
std::vector<std::uint8_t> v(200000, 0x81);
|
|
||||||
v.push_back(0xFA);
|
|
||||||
const json j = json::from_bon8(v);
|
|
||||||
CHECK(json::to_bon8(j) == v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | 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 <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// split from unit-large_json.cpp so that both run in parallel and stay below
|
||||||
|
// the ctest timeout under Valgrind
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
json nested_array(const std::size_t depth, json leaf)
|
||||||
|
{
|
||||||
|
json j = std::move(leaf);
|
||||||
|
for (std::size_t i = 0; i < depth; ++i)
|
||||||
|
{
|
||||||
|
json a = json::array();
|
||||||
|
a.push_back(std::move(j));
|
||||||
|
j = std::move(a);
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
json nested_object(const std::size_t depth, json leaf)
|
||||||
|
{
|
||||||
|
json j = std::move(leaf);
|
||||||
|
for (std::size_t i = 0; i < depth; ++i)
|
||||||
|
{
|
||||||
|
json o = json::object();
|
||||||
|
o["k"] = std::move(j);
|
||||||
|
j = std::move(o);
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("issue #5392 - binary writers on deeply nested values")
|
||||||
|
{
|
||||||
|
// 200 is past the point where the writers stop recursing, and still
|
||||||
|
// shallow enough that from_* and operator== (which still recurse) are fine.
|
||||||
|
const json deep_array = nested_array(200, json(0));
|
||||||
|
const json deep_object = nested_object(200, json("x"));
|
||||||
|
const json empty_array = nested_array(200, json::array());
|
||||||
|
const json empty_object = nested_object(200, json::object());
|
||||||
|
const json mixed = nested_object(80, nested_array(80, json(true)));
|
||||||
|
|
||||||
|
SECTION("roundtrip past the recursion bound")
|
||||||
|
{
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(deep_array)) == deep_array);
|
||||||
|
CHECK(json::from_msgpack(json::to_msgpack(deep_array)) == deep_array);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(deep_array)) == deep_array);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, false)) == deep_array);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, true)) == deep_array);
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(deep_array)) == deep_array);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(deep_array)) == deep_array);
|
||||||
|
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(deep_object)) == deep_object);
|
||||||
|
CHECK(json::from_msgpack(json::to_msgpack(deep_object)) == deep_object);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(deep_object)) == deep_object);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(deep_object, true, true)) == deep_object);
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(deep_object)) == deep_object);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(deep_object)) == deep_object);
|
||||||
|
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(empty_array)) == empty_array);
|
||||||
|
CHECK(json::from_msgpack(json::to_msgpack(empty_array)) == empty_array);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(empty_array)) == empty_array);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(empty_array, true, true)) == empty_array);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(empty_array)) == empty_array);
|
||||||
|
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(empty_object)) == empty_object);
|
||||||
|
CHECK(json::from_msgpack(json::to_msgpack(empty_object)) == empty_object);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(empty_object)) == empty_object);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(empty_object)) == empty_object);
|
||||||
|
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(mixed)) == mixed);
|
||||||
|
CHECK(json::from_msgpack(json::to_msgpack(mixed)) == mixed);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(mixed)) == mixed);
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(mixed)) == mixed);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(mixed)) == mixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("the two ways of writing a value meet at the bound")
|
||||||
|
{
|
||||||
|
for (std::size_t depth = 120; depth <= 140; ++depth)
|
||||||
|
{
|
||||||
|
CAPTURE(depth)
|
||||||
|
|
||||||
|
const json array = nested_array(depth, json(7));
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(array)) == array);
|
||||||
|
CHECK(json::from_msgpack(json::to_msgpack(array)) == array);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(array, true, true)) == array);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(array)) == array);
|
||||||
|
|
||||||
|
const json object = nested_object(depth, json(7));
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(object)) == object);
|
||||||
|
CHECK(json::from_msgpack(json::to_msgpack(object)) == object);
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(object)) == object);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(object)) == object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a BJData ndarray below the bound is still an ndarray")
|
||||||
|
{
|
||||||
|
const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
||||||
|
const json invalid = json({{"_ArrayType_", "nope"}, {"_ArraySize_", {1}}, {"_ArrayData_", {1}}});
|
||||||
|
|
||||||
|
const json deep_ndarray = nested_array(140, ndarray);
|
||||||
|
const json deep_invalid = nested_array(140, invalid);
|
||||||
|
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray);
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(deep_invalid)) == deep_invalid);
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(ndarray)) == ndarray);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("byte-exact across the switch-over")
|
||||||
|
{
|
||||||
|
// nested one-element arrays around the recursion bound: the exact
|
||||||
|
// bytes a writer produces do not depend on whether it stayed on the
|
||||||
|
// call stack or moved to the heap one partway through
|
||||||
|
for (const std::size_t depth :
|
||||||
|
{
|
||||||
|
nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(),
|
||||||
|
nlohmann::detail::recursion_depth_limit() + 1, nlohmann::detail::recursion_depth_limit() + 2
|
||||||
|
})
|
||||||
|
{
|
||||||
|
CAPTURE(depth)
|
||||||
|
const json array = nested_array(depth, json(0));
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> expected_cbor(depth, 0x81);
|
||||||
|
expected_cbor.push_back(0x00);
|
||||||
|
CHECK(json::to_cbor(array) == expected_cbor);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> expected_msgpack(depth, 0x91);
|
||||||
|
expected_msgpack.push_back(0x00);
|
||||||
|
CHECK(json::to_msgpack(array) == expected_msgpack);
|
||||||
|
|
||||||
|
std::string expected_ubjson(depth, '[');
|
||||||
|
expected_ubjson += "i";
|
||||||
|
expected_ubjson += '\0';
|
||||||
|
expected_ubjson.append(depth, ']');
|
||||||
|
const auto packed_ubjson = json::to_ubjson(array);
|
||||||
|
CHECK(std::string(packed_ubjson.begin(), packed_ubjson.end()) == expected_ubjson);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> expected_bon8(depth, 0x81);
|
||||||
|
expected_bon8.push_back(0x90);
|
||||||
|
CHECK(json::to_bon8(array) == expected_bon8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a deep object, and a BJData ndarray, past the recursion bound")
|
||||||
|
{
|
||||||
|
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50;
|
||||||
|
|
||||||
|
const json object = nested_object(depth, json(42));
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(object)) == object);
|
||||||
|
CHECK(json::from_msgpack(json::to_msgpack(object)) == object);
|
||||||
|
CHECK(json::from_ubjson(json::to_ubjson(object, true, true)) == object);
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(object)) == object);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(object)) == object);
|
||||||
|
|
||||||
|
const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
||||||
|
const json deep_ndarray = nested_array(depth, ndarray);
|
||||||
|
CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a discarded value past the recursion bound still throws type_error.321")
|
||||||
|
{
|
||||||
|
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50;
|
||||||
|
const json discarded_leaf(json::value_t::discarded);
|
||||||
|
const json deep_discarded = nested_array(depth, discarded_leaf);
|
||||||
|
|
||||||
|
// with diagnostics, the message names the path to the discarded leaf
|
||||||
|
#if JSON_DIAGNOSTICS
|
||||||
|
std::string path;
|
||||||
|
for (std::size_t i = 0; i < depth; ++i)
|
||||||
|
{
|
||||||
|
path += "/0";
|
||||||
|
}
|
||||||
|
const std::string prefix = "[json.exception.type_error.321] (" + path + ") ";
|
||||||
|
#else
|
||||||
|
const std::string prefix = "[json.exception.type_error.321] ";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
CHECK_THROWS_WITH_AS(json::to_cbor(deep_discarded), (prefix + "cannot serialize discarded value to CBOR").c_str(), json::type_error);
|
||||||
|
CHECK_THROWS_WITH_AS(json::to_msgpack(deep_discarded), (prefix + "cannot serialize discarded value to MessagePack").c_str(), json::type_error);
|
||||||
|
CHECK_THROWS_WITH_AS(json::to_ubjson(deep_discarded), (prefix + "cannot serialize discarded value to UBJSON").c_str(), json::type_error);
|
||||||
|
CHECK_THROWS_WITH_AS(json::to_bjdata(deep_discarded), (prefix + "cannot serialize discarded value to BJData").c_str(), json::type_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("BON8 past the recursion bound: discarded values and errors")
|
||||||
|
{
|
||||||
|
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50;
|
||||||
|
|
||||||
|
// BON8 rejects a discarded value (type_error.321), deep or not
|
||||||
|
const json deep_discarded = nested_array(depth, json(json::value_t::discarded));
|
||||||
|
CHECK_THROWS_AS(json::to_bon8(deep_discarded), json::type_error);
|
||||||
|
|
||||||
|
// errors are thrown from below the bound as from above it
|
||||||
|
const json deep_invalid_string = nested_array(depth, json(std::string("\x80")));
|
||||||
|
CHECK_THROWS_AS(json::to_bon8(deep_invalid_string), json::type_error);
|
||||||
|
|
||||||
|
json deep_invalid_key = json::object();
|
||||||
|
deep_invalid_key[std::string("\x80")] = 1;
|
||||||
|
deep_invalid_key = nested_object(depth, deep_invalid_key);
|
||||||
|
CHECK_THROWS_AS(json::to_bon8(deep_invalid_key), json::type_error);
|
||||||
|
|
||||||
|
const json deep_too_large = nested_array(depth, json(9223372036854775808u));
|
||||||
|
CHECK_THROWS_AS(json::to_bon8(deep_too_large), json::out_of_range);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("does not overflow the C++ stack")
|
||||||
|
{
|
||||||
|
const std::size_t depth = 100000;
|
||||||
|
const json j = json::parse(std::string(depth, '[') + "0" + std::string(depth, ']'));
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> packed;
|
||||||
|
CHECK_NOTHROW(packed = json::to_cbor(j));
|
||||||
|
CHECK(json::from_cbor(packed) == j);
|
||||||
|
|
||||||
|
CHECK_NOTHROW(packed = json::to_msgpack(j));
|
||||||
|
CHECK(json::from_msgpack(packed) == j);
|
||||||
|
|
||||||
|
CHECK_NOTHROW(packed = json::to_ubjson(j));
|
||||||
|
CHECK(json::from_ubjson(packed) == j);
|
||||||
|
|
||||||
|
CHECK_NOTHROW(packed = json::to_ubjson(j, true, false));
|
||||||
|
CHECK(json::from_ubjson(packed) == j);
|
||||||
|
|
||||||
|
CHECK_NOTHROW(packed = json::to_bjdata(j));
|
||||||
|
CHECK(json::from_bjdata(packed) == j);
|
||||||
|
|
||||||
|
CHECK_NOTHROW(packed = json::to_bon8(j));
|
||||||
|
CHECK(json::from_bon8(packed) == j);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("BON8 containers with more than four elements past the recursion bound")
|
||||||
|
{
|
||||||
|
// such containers are closed with 0xFE, which the iterative writer
|
||||||
|
// must emit when it leaves them; strings next to each other must
|
||||||
|
// still be separated with 0xFF
|
||||||
|
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50;
|
||||||
|
json wide_array = json::array({1, "a", "b", json::array(), 5});
|
||||||
|
json wide_object = json({{"k1", "v"}, {"k2", 2}, {"k3", ""}, {"k4", json::object()}, {"k5", nullptr}});
|
||||||
|
for (std::size_t i = 0; i < depth; ++i)
|
||||||
|
{
|
||||||
|
wide_array = json::array({"s", wide_array, "t", wide_object, true});
|
||||||
|
wide_object = json({{"a", wide_object}, {"b", "x"}, {"c", "y"}, {"d", wide_array.size()}, {"e", "z"}});
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(wide_array)) == wide_array);
|
||||||
|
CHECK(json::from_bon8(json::to_bon8(wide_object)) == wide_object);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("regression test for https://issues.oss-fuzz.com/issues/566583014")
|
||||||
|
{
|
||||||
|
// 200000 nested one-element CBOR arrays, the innermost holding null;
|
||||||
|
// round-tripping this used to recurse once per level on the way back
|
||||||
|
// out through to_cbor(), deep enough to overflow the stack
|
||||||
|
std::vector<std::uint8_t> v(200000, 0x81);
|
||||||
|
v.push_back(0xf6);
|
||||||
|
const json j = json::from_cbor(v);
|
||||||
|
CHECK(json::to_cbor(j) == v);
|
||||||
|
|
||||||
|
// the MessagePack analogue: fixarray of 1 nesting down to nil
|
||||||
|
std::vector<std::uint8_t> v_msgpack(200000, 0x91);
|
||||||
|
v_msgpack.push_back(0xc0);
|
||||||
|
const json j_msgpack = json::from_msgpack(v_msgpack);
|
||||||
|
CHECK(json::to_msgpack(j_msgpack) == v_msgpack);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("regression test for https://issues.oss-fuzz.com/issues/572238015")
|
||||||
|
{
|
||||||
|
// the BON8 analogue: 200000 nested one-element arrays down to null
|
||||||
|
std::vector<std::uint8_t> v(200000, 0x81);
|
||||||
|
v.push_back(0xFA);
|
||||||
|
const json j = json::from_bon8(v);
|
||||||
|
CHECK(json::to_bon8(j) == v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | 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 <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// split from unit-large_json.cpp so that both run in parallel and stay below
|
||||||
|
// the ctest timeout under Valgrind
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// The text of a value nested depth levels deep around the number 0. Level i is
|
||||||
|
// an array if pattern[i % pattern.size()] is '[', and otherwise an object with
|
||||||
|
// the single member "a", which every object type enumerates in the same order.
|
||||||
|
std::string nested_text(std::size_t depth, const std::string& pattern)
|
||||||
|
{
|
||||||
|
std::string text;
|
||||||
|
std::string closing;
|
||||||
|
for (std::size_t i = 0; i < depth; ++i)
|
||||||
|
{
|
||||||
|
const bool array = pattern[i % pattern.size()] == '[';
|
||||||
|
text += array ? "[" : "{\"a\":";
|
||||||
|
closing += array ? ']' : '}';
|
||||||
|
}
|
||||||
|
text += '0';
|
||||||
|
text.append(closing.rbegin(), closing.rend());
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("issue #5650 - stack overflow converting between specializations")
|
||||||
|
{
|
||||||
|
// deep enough to exhaust the call stack, but small enough to stay cheap:
|
||||||
|
// parsing is iterative, so building the values below costs little
|
||||||
|
const std::size_t depth = 100000;
|
||||||
|
|
||||||
|
const std::vector<std::string> patterns = {"[", "{", "[{"};
|
||||||
|
|
||||||
|
SECTION("json to ordered_json")
|
||||||
|
{
|
||||||
|
for (const auto& pattern : patterns)
|
||||||
|
{
|
||||||
|
CAPTURE(pattern)
|
||||||
|
const std::string text = nested_text(depth, pattern);
|
||||||
|
const json j = json::parse(text);
|
||||||
|
|
||||||
|
const nlohmann::ordered_json converted = j;
|
||||||
|
CHECK(converted.dump() == text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("ordered_json to json")
|
||||||
|
{
|
||||||
|
for (const auto& pattern : patterns)
|
||||||
|
{
|
||||||
|
CAPTURE(pattern)
|
||||||
|
const std::string text = nested_text(depth, pattern);
|
||||||
|
const nlohmann::ordered_json o = nlohmann::ordered_json::parse(text);
|
||||||
|
|
||||||
|
const json converted = o;
|
||||||
|
CHECK(converted.dump() == text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("get<ordered_json>()")
|
||||||
|
{
|
||||||
|
for (const auto& pattern : patterns)
|
||||||
|
{
|
||||||
|
CAPTURE(pattern)
|
||||||
|
const std::string text = nested_text(depth, pattern);
|
||||||
|
const json j = json::parse(text);
|
||||||
|
|
||||||
|
CHECK(j.get<nlohmann::ordered_json>().dump() == text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("depths around the bound of the recursive descent")
|
||||||
|
{
|
||||||
|
for (std::size_t d = 1; d <= 300; ++d)
|
||||||
|
{
|
||||||
|
CAPTURE(d)
|
||||||
|
for (const auto& pattern : patterns)
|
||||||
|
{
|
||||||
|
CAPTURE(pattern)
|
||||||
|
const std::string text = nested_text(d, pattern);
|
||||||
|
const json j = json::parse(text);
|
||||||
|
|
||||||
|
const nlohmann::ordered_json converted = j;
|
||||||
|
CHECK(converted.dump() == text);
|
||||||
|
const json back = converted;
|
||||||
|
CHECK(back.dump() == text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("values below the bound are converted as values above it")
|
||||||
|
{
|
||||||
|
// Bury a value below the bound, where it is converted without the
|
||||||
|
// call stack, and compare it with the same value converted on its
|
||||||
|
// own by the containers' range constructors. Its objects have
|
||||||
|
// members that the two object types enumerate in different orders.
|
||||||
|
const auto bury = [](nlohmann::ordered_json value)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < 200; ++i)
|
||||||
|
{
|
||||||
|
value = nlohmann::ordered_json::array({std::move(value)});
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
const auto dig = [](const json & value)
|
||||||
|
{
|
||||||
|
const json* current = &value;
|
||||||
|
for (std::size_t i = 0; i < 200; ++i)
|
||||||
|
{
|
||||||
|
current = ¤t->at(0);
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
nlohmann::ordered_json value = nlohmann::ordered_json::object();
|
||||||
|
value["z"] = {1, -2, 3U, 4.5, true, nullptr, "six", nlohmann::ordered_json::binary({7, 8}, 9),
|
||||||
|
nlohmann::ordered_json::binary({10}), nlohmann::ordered_json::array(), nlohmann::ordered_json::object()
|
||||||
|
};
|
||||||
|
value["y"] = {{"x", {{"w", 1}, {"v", 2}}}, {"u", {3, {{"t", 4}, {"s", 5}}}}};
|
||||||
|
value["r"] = nlohmann::ordered_json::array({nlohmann::ordered_json(nlohmann::ordered_json::value_t::discarded)});
|
||||||
|
|
||||||
|
const json converted_above = value;
|
||||||
|
const json buried = bury(value);
|
||||||
|
const json& converted_below = *dig(buried);
|
||||||
|
|
||||||
|
CHECK(converted_below.dump() == converted_above.dump());
|
||||||
|
CHECK(converted_below.at("z").at(7).get_binary().subtype() == 9);
|
||||||
|
CHECK_FALSE(converted_below.at("z").at(8).get_binary().has_subtype());
|
||||||
|
CHECK(converted_below.at("r").at(0).is_discarded());
|
||||||
|
|
||||||
|
// a discarded value is never equal to anything, so compare the rest
|
||||||
|
value.erase("r");
|
||||||
|
const json without_discarded_above = value;
|
||||||
|
const json without_discarded_buried = bury(value);
|
||||||
|
CHECK(*dig(without_discarded_buried) == without_discarded_above);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user