mirror of
https://github.com/nlohmann/json.git
synced 2026-09-27 14:20:19 +01:00
Merge branch 'develop' into bon8
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
@@ -42,6 +42,9 @@ dump() serializes any non-finite double the same deterministic way (as JSON
|
||||
`null`, since JSON itself cannot represent NaN/Infinity), so comparing
|
||||
dumps is stable under exactly the same values that break operator==.
|
||||
|
||||
The unit tests run the same checks on a fixed corpus (see the "BJData round-trip
|
||||
invariants" test case), so keep both in sync.
|
||||
|
||||
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
|
||||
drivers.
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,9 @@ array data, it performs the following steps:
|
||||
- j4 = from_ubjson(vec3)
|
||||
- assert(j1 == j4)
|
||||
|
||||
The unit tests run the same checks on a fixed corpus (see the "UBJSON round-trip
|
||||
invariants" test case), so keep both in sync.
|
||||
|
||||
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
|
||||
drivers.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | 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 <cmath> // nan
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // int32_t, int64_t, uint32_t, uint64_t
|
||||
#include <limits> // numeric_limits
|
||||
#include <random> // mt19937
|
||||
#include <string> // string, to_string
|
||||
#include <utility> // move
|
||||
#include <vector> // vector
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// Values for the round-trip property tests of the UBJSON and BJData writers.
|
||||
//
|
||||
// The fuzzer drivers (tests/src/fuzzer-parse_ubjson.cpp and
|
||||
// fuzzer-parse_bjdata.cpp) check that anything the library parses can be
|
||||
// serialized, parsed back, and serialized again without loss. Those checks
|
||||
// only run at OSS-Fuzz, so a regression used to surface days later as an
|
||||
// external report. The unit tests run the same checks on this corpus in CI.
|
||||
//
|
||||
// The corpus is deterministic: std::mt19937's output sequence is fixed by
|
||||
// the standard, and it is used directly rather than through a distribution
|
||||
// (whose results are implementation-defined).
|
||||
namespace utils
|
||||
{
|
||||
|
||||
class round_trip_corpus
|
||||
{
|
||||
public:
|
||||
using json = nlohmann::json;
|
||||
|
||||
static std::vector<json> values()
|
||||
{
|
||||
round_trip_corpus corpus;
|
||||
return corpus.build();
|
||||
}
|
||||
|
||||
// whether a value contains a binary value, which a BJData or UBJSON round
|
||||
// trip may turn into an array of integers
|
||||
static bool contains_binary(const json& j)
|
||||
{
|
||||
if (j.is_binary())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (j.is_structured())
|
||||
{
|
||||
for (const auto& element : j)
|
||||
{
|
||||
if (contains_binary(element))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<json> atoms;
|
||||
// a fixed seed is the point: the corpus must be the same in every run
|
||||
std::mt19937 generator{42}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
|
||||
round_trip_corpus()
|
||||
: atoms
|
||||
{
|
||||
nullptr, true, false,
|
||||
// integers at the boundaries of every UBJSON/BJData integer type
|
||||
0, 1, -1, 127, 128, 255, 256, -128, -129,
|
||||
32767, 32768, 65535, 65536, -32768, -32769,
|
||||
(std::numeric_limits<std::int32_t>::min)(), (std::numeric_limits<std::int32_t>::max)(),
|
||||
(std::numeric_limits<std::uint32_t>::max)(),
|
||||
(std::numeric_limits<std::int64_t>::min)(), (std::numeric_limits<std::int64_t>::max)(),
|
||||
static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1u,
|
||||
(std::numeric_limits<std::uint64_t>::max)(),
|
||||
// floating-point numbers, including non-finite ones
|
||||
0.0, -0.0, 1.5, -2.25, 3.4e38, (std::numeric_limits<double>::max)(),
|
||||
std::nan(""), std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity(),
|
||||
// strings, including a non-ASCII one and one longer than 255 bytes
|
||||
"", "a", "\xC3\xA4", std::string(300, 'x'),
|
||||
// binary values with and without subtype
|
||||
json::binary({}), json::binary({1, 2, 255}), json::binary({0x80, 0x7F}, 42), json::binary({1}, 0)
|
||||
}
|
||||
{}
|
||||
|
||||
std::vector<json> build()
|
||||
{
|
||||
std::vector<json> result = atoms;
|
||||
|
||||
// each atom inside containers, including homogeneous ones that the
|
||||
// writers encode as optimized (typed) containers
|
||||
result.emplace_back(json::array());
|
||||
result.emplace_back(json::object());
|
||||
for (const auto& atom : atoms)
|
||||
{
|
||||
result.push_back(json::array({atom}));
|
||||
result.push_back(json::array({atom, atom, atom}));
|
||||
result.push_back(json::array({json::array({atom})}));
|
||||
result.push_back(json::object({{"key", atom}}));
|
||||
}
|
||||
result.push_back(json::array({1, 1.5}));
|
||||
result.push_back(json::array({-1, 255}));
|
||||
result.push_back(json::array({"a", "b"}));
|
||||
|
||||
// deep, but well below any recursion or depth limit
|
||||
json nested_array = 1;
|
||||
json nested_object = 1;
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
nested_array = json::array({nested_array});
|
||||
nested_object = json::object({{"key", nested_object}});
|
||||
}
|
||||
result.push_back(nested_array);
|
||||
result.push_back(nested_object);
|
||||
|
||||
add_annotated_arrays(result);
|
||||
add_random_values(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// objects in the JData annotated array format, which the BJData writer
|
||||
// encodes as ND-arrays when the annotation describes a packed array, and
|
||||
// as plain objects otherwise (see #5398, #5399, #5403, #5404, and #5542)
|
||||
static void add_annotated_arrays(std::vector<json>& result)
|
||||
{
|
||||
const std::vector<json> types =
|
||||
{
|
||||
"uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64",
|
||||
"single", "double", "char", "byte", "bool", "unknown", 5, nullptr
|
||||
};
|
||||
const std::vector<json> sizes =
|
||||
{
|
||||
json::array(), {3}, {1, 3}, {3, 1}, {2, 3}, {2, 0}, {0, 2}, {2, 2, 2}, {-1, 2}, {2, 1.5},
|
||||
"3", 3, nullptr, json::binary({})
|
||||
};
|
||||
const std::vector<json> data =
|
||||
{
|
||||
nullptr, 5, "s", json::object({{"a", 1}}), json::array(),
|
||||
{1, 2, 3}, {1, 2, 3, 4, 5, 6}, {1, 2, 3, 4, 5, 6, 7, 8},
|
||||
{1.5, 2.5, 3.5, 4.5, 5.5, 6.5}, {300, -300, 70000, -70000, 1, 2},
|
||||
{"a", "b", "c", "d", "e", "f"}, {json::array({1, 2, 3}), json::array({4, 5, 6})}
|
||||
};
|
||||
|
||||
for (const auto& type : types)
|
||||
{
|
||||
for (const auto& size : sizes)
|
||||
{
|
||||
for (const auto& d : data)
|
||||
{
|
||||
result.push_back({{"_ArrayType_", type}, {"_ArraySize_", size}, {"_ArrayData_", d}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// incomplete annotations and annotations with an extra key
|
||||
result.push_back({{"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
||||
result.push_back({{"_ArrayType_", "uint8"}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
||||
result.push_back({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}});
|
||||
result.push_back({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}, {"extra", 1}});
|
||||
}
|
||||
|
||||
// random containers of atoms, both homogeneous and mixed
|
||||
void add_random_values(std::vector<json>& result)
|
||||
{
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
result.push_back(random_value(0));
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t random_below(std::size_t bound)
|
||||
{
|
||||
return generator() % bound;
|
||||
}
|
||||
|
||||
json random_value(int depth)
|
||||
{
|
||||
const auto kind = random_below(10);
|
||||
if (depth > 3 || kind < 5)
|
||||
{
|
||||
return atoms[random_below(atoms.size())];
|
||||
}
|
||||
|
||||
json result = kind < 8 ? json::array() : json::object();
|
||||
const auto count = random_below(5);
|
||||
const bool homogeneous = random_below(2) == 0;
|
||||
const json fixed = atoms[random_below(atoms.size())];
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
json element = homogeneous ? fixed : random_value(depth + 1);
|
||||
if (result.is_array())
|
||||
{
|
||||
result.push_back(std::move(element));
|
||||
}
|
||||
else
|
||||
{
|
||||
result[std::to_string(i)] = std::move(element);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace utils
|
||||
@@ -19,6 +19,7 @@ using nlohmann::json;
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "round_trip_corpus.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
namespace
|
||||
@@ -2867,6 +2868,21 @@ TEST_CASE("BJData")
|
||||
const auto out_num = json::to_bjdata(j_num);
|
||||
CHECK(out_num.at(0) == '{');
|
||||
CHECK(json::from_bjdata(out_num) == j_num);
|
||||
|
||||
// OSS-Fuzz issue 474400817: an empty object _ArraySize_ was
|
||||
// written as the ND-array header length, which from_bjdata()
|
||||
// could not read back
|
||||
const std::vector<uint8_t> input =
|
||||
{
|
||||
'[', '{', 'U', 11, '_', 'A', 'r', 'r', 'a', 'y', 'D', 'a', 't', 'a', '_', 'Z',
|
||||
'U', 11, '_', 'A', 'r', 'r', 'a', 'y', 'T', 'y', 'p', 'e', '_', 'S', 'i', 5, 'i', 'n', 't', '1', '6',
|
||||
'U', 11, '_', 'A', 'r', 'r', 'a', 'y', 'S', 'i', 'z', 'e', '_', '{', '}', '}', ']'
|
||||
};
|
||||
const json j1 = json::from_bjdata(input);
|
||||
CHECK(j1 == json::parse(R"([{"_ArrayType_":"int16","_ArraySize_":{},"_ArrayData_":null}])"));
|
||||
json j2;
|
||||
CHECK_NOTHROW(j2 = json::from_bjdata(json::to_bjdata(j1, false, false)));
|
||||
CHECK(j2 == j1);
|
||||
}
|
||||
|
||||
SECTION("ndarray with out-of-range _ArrayData_ elements stays as object")
|
||||
@@ -4273,6 +4289,93 @@ 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")
|
||||
{
|
||||
// OSS-Fuzz issue 474480402: a Draft 3 optimized binary array is read as a
|
||||
// binary value, which to_bjdata() writes in the default Draft 2 mode as a
|
||||
// plain array of uint8 numbers. That is read back as an array of numbers,
|
||||
// for which the writer then picks the smallest type marker, int8 ('i'),
|
||||
// so re-serializing changes the bytes, but not the value. This is the
|
||||
// exception described in the "Round trips" note of the BJData
|
||||
// documentation, and why the fuzzer checks value stability (see #5494).
|
||||
const std::vector<uint8_t> input = {'[', '$', 'B', '#', 'U', 1, 0x20};
|
||||
const json j1 = json::from_bjdata(input);
|
||||
CHECK(j1 == json::binary({0x20}));
|
||||
|
||||
const std::vector<uint8_t> vec = json::to_bjdata(j1, false, false);
|
||||
CHECK(vec == std::vector<uint8_t>({'[', 'U', 0x20, ']'}));
|
||||
const json j2 = json::from_bjdata(vec);
|
||||
CHECK(j2 == json::array({0x20}));
|
||||
|
||||
const std::vector<uint8_t> vec2 = json::to_bjdata(j2, false, false);
|
||||
CHECK(vec2 == std::vector<uint8_t>({'[', 'i', 0x20, ']'}));
|
||||
CHECK(json::from_bjdata(vec2) == j2);
|
||||
}
|
||||
|
||||
TEST_CASE("BJData roundtrips" * doctest::skip())
|
||||
{
|
||||
SECTION("input from self-generated BJData files")
|
||||
|
||||
@@ -25,6 +25,7 @@ using nlohmann::json;
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <valarray>
|
||||
|
||||
#if defined(_WIN32)
|
||||
@@ -1233,6 +1234,57 @@ TEST_CASE("deserialization")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("stream position after extraction without JSON_PRECISE_STREAM_POSITION (#5340)")
|
||||
{
|
||||
// By default, the character that terminates a number is consumed, so
|
||||
// the stream is left one byte too far after a number (and only after a
|
||||
// number). JSON_PRECISE_STREAM_POSITION changes this; see
|
||||
// unit-precise-stream-position.cpp. These checks pin the default.
|
||||
const auto remaining = [](std::istream & is)
|
||||
{
|
||||
return std::string(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>());
|
||||
};
|
||||
|
||||
SECTION("the character after a number is consumed")
|
||||
{
|
||||
std::istringstream ss("1true");
|
||||
json j;
|
||||
ss >> j;
|
||||
CHECK(j == 1);
|
||||
CHECK(remaining(ss) == "rue");
|
||||
}
|
||||
|
||||
SECTION("the character after other values is not consumed")
|
||||
{
|
||||
std::istringstream ss("[1]true");
|
||||
json j;
|
||||
ss >> j;
|
||||
CHECK(j == json::parse("[1]"));
|
||||
CHECK(remaining(ss) == "true");
|
||||
}
|
||||
|
||||
SECTION("comma-separated numbers can be read one by one")
|
||||
{
|
||||
std::istringstream ss("1,2,3");
|
||||
json j1, j2, j3;
|
||||
ss >> j1 >> j2 >> j3;
|
||||
CHECK(j1 == 1);
|
||||
CHECK(j2 == 2);
|
||||
CHECK(j3 == 3);
|
||||
}
|
||||
|
||||
SECTION("std::getline after a number skips the line break")
|
||||
{
|
||||
std::istringstream ss("42\nfoo");
|
||||
json j;
|
||||
std::string line;
|
||||
ss >> j;
|
||||
std::getline(ss, line);
|
||||
CHECK(j == 42);
|
||||
CHECK(line == "foo");
|
||||
}
|
||||
}
|
||||
|
||||
// build with C++20
|
||||
// JSON_HAS_CPP_20
|
||||
#if defined(__cpp_char8_t)
|
||||
|
||||
@@ -361,6 +361,106 @@ TEST_CASE("Regression tests for extended diagnostics")
|
||||
CHECK(p == o);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Regression test - erase() and update() must keep JSON_DIAGNOSTICS parent pointers of ordered_json members")
|
||||
{
|
||||
// ordered_json keeps its members in a vector: erasing a member
|
||||
// re-constructs all members after it in place, and adding a key may
|
||||
// reallocate the vector; both reset the parent pointers of the members
|
||||
// that were moved
|
||||
using nlohmann::ordered_json;
|
||||
|
||||
const auto check_parents = [](const ordered_json & j)
|
||||
{
|
||||
// const access, so operator[] cannot repair the parent pointers
|
||||
CHECK_THROWS_WITH_AS(j["z"]["x"].at(0), "[json.exception.type_error.304] (/z/x) cannot use at() with number", ordered_json::type_error);
|
||||
|
||||
// must not trigger assert_invariant() in a debug/assert-enabled build
|
||||
ordered_json const copy = j; // NOLINT(performance-unnecessary-copy-initialization)
|
||||
CHECK(copy == j);
|
||||
};
|
||||
|
||||
// erase(key)
|
||||
{
|
||||
ordered_json j = {{"a", 1}, {"z", {{"x", 1}}}};
|
||||
CHECK(j.erase("a") == 1);
|
||||
check_parents(j);
|
||||
}
|
||||
|
||||
// erase(iterator)
|
||||
{
|
||||
ordered_json j = {{"a", 1}, {"z", {{"x", 1}}}};
|
||||
j.erase(j.begin());
|
||||
check_parents(j);
|
||||
}
|
||||
|
||||
// erase(iterator, iterator)
|
||||
{
|
||||
ordered_json j = {{"a", 1}, {"b", 2}, {"z", {{"x", 1}}}};
|
||||
j.erase(j.begin(), j.find("z"));
|
||||
check_parents(j);
|
||||
}
|
||||
|
||||
// patch() removes via erase(iterator)
|
||||
{
|
||||
ordered_json j = {{"a", 1}, {"z", {{"x", 1}}}};
|
||||
j.patch_inplace(ordered_json::parse(R"([{"op": "remove", "path": "/a"}])"));
|
||||
check_parents(j);
|
||||
}
|
||||
|
||||
// update(j)
|
||||
{
|
||||
ordered_json j = {{"z", {{"x", 1}}}};
|
||||
j.update({{"a", 1}, {"b", 2}});
|
||||
check_parents(j);
|
||||
}
|
||||
|
||||
// update(j, true), the outer and the nested vector both grow
|
||||
{
|
||||
ordered_json j = {{"z", {{"x", 1}}}};
|
||||
j.update({{"z", {{"y", 2}}}, {"a", 1}}, true);
|
||||
check_parents(j);
|
||||
}
|
||||
|
||||
// update(j, true) around its descent bound, where the nested vectors
|
||||
// grow while the objects are merged without recursing
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(), nlohmann::detail::recursion_depth_limit() + 2
|
||||
})
|
||||
{
|
||||
ordered_json j = {{"z", {{"x", 1}}}};
|
||||
ordered_json patch = {{"a", 1}, {"b", 2}, {"c", {{"d", 3}}}};
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
j = ordered_json{{"k", 0}, {"n", std::move(j)}};
|
||||
patch = ordered_json{{"n", std::move(patch)}, {"l", 1}, {"m", 2}};
|
||||
}
|
||||
j.update(patch, true);
|
||||
|
||||
// must not trigger assert_invariant() on any level in a
|
||||
// debug/assert-enabled build
|
||||
ordered_json const copy = j; // NOLINT(performance-unnecessary-copy-initialization)
|
||||
CHECK(copy == j);
|
||||
}
|
||||
|
||||
// merge_patch() inserts "c" and removes "d" at /a/c, then inserts "e"
|
||||
// at /a, which copies /a/c
|
||||
{
|
||||
auto j = ordered_json::parse(R"({"a": {"c": {"d": {}}}})");
|
||||
j.merge_patch(ordered_json::parse(R"({"a": {"c": {"c": "s", "d": null}, "e": "s"}})"));
|
||||
CHECK(j.dump() == R"({"a":{"c":{"c":"s"},"e":"s"}})");
|
||||
|
||||
auto const& constJ = j;
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
CHECK_THROWS_WITH_AS(constJ["a"]["c"]["c"].at(0), "[json.exception.type_error.304] (/a/c/c) (bytes 18-21) cannot use at() with string", ordered_json::type_error);
|
||||
#else
|
||||
CHECK_THROWS_WITH_AS(constJ["a"]["c"]["c"].at(0), "[json.exception.type_error.304] (/a/c/c) cannot use at() with string", ordered_json::type_error);
|
||||
#endif
|
||||
ordered_json const copy = j;
|
||||
CHECK(copy == j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()")
|
||||
|
||||
@@ -157,6 +157,54 @@ TEST_CASE("tests on deeply nested JSONs")
|
||||
CHECK(deep_depth == depth);
|
||||
}
|
||||
|
||||
SECTION("comparing")
|
||||
{
|
||||
// Comparing used to descend once per level, and an ordered
|
||||
// comparison used to compare every pair of elements twice, once in
|
||||
// each direction, which took exponentially long in the nesting
|
||||
// depth. Both are gone: these finish in milliseconds, where the
|
||||
// second used to take longer than anyone would wait even for a
|
||||
// value nested only a few dozen levels deep.
|
||||
const std::string text = std::string(depth, '[') + '0' + std::string(depth, ']');
|
||||
const json j = json::parse(text);
|
||||
const json same = json::parse(text);
|
||||
const json larger = json::parse(std::string(depth, '[') + '1' + std::string(depth, ']'));
|
||||
|
||||
CHECK(j == same);
|
||||
CHECK_FALSE(j == larger);
|
||||
CHECK(j != larger);
|
||||
|
||||
CHECK(j < larger);
|
||||
CHECK_FALSE(larger < j);
|
||||
CHECK(larger > j);
|
||||
CHECK(j <= same);
|
||||
CHECK(j >= same);
|
||||
|
||||
// a value that ends earlier is the smaller one
|
||||
const json shorter = json::parse(std::string(depth - 1, '[') + '0' + std::string(depth - 1, ']'));
|
||||
CHECK_FALSE(j == shorter);
|
||||
}
|
||||
|
||||
SECTION("comparing objects")
|
||||
{
|
||||
std::string text;
|
||||
text.reserve((6 * depth) + 1);
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
text += "{\"a\":";
|
||||
}
|
||||
text += '1';
|
||||
text.append(depth, '}');
|
||||
|
||||
const json j = json::parse(text);
|
||||
const json same = json::parse(text);
|
||||
|
||||
CHECK(j == same);
|
||||
CHECK_FALSE(j != same);
|
||||
CHECK(j <= same);
|
||||
CHECK(j >= same);
|
||||
}
|
||||
|
||||
SECTION("the copy is independent of the original")
|
||||
{
|
||||
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | 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"
|
||||
|
||||
// This file tests the opt-in JSON_PRECISE_STREAM_POSITION, so it defines the
|
||||
// macro itself rather than relying on a -D flag, and runs in every build. The
|
||||
// default behavior is pinned in unit-deserialization.cpp.
|
||||
#ifdef JSON_PRECISE_STREAM_POSITION
|
||||
#undef JSON_PRECISE_STREAM_POSITION
|
||||
#endif
|
||||
|
||||
#define JSON_PRECISE_STREAM_POSITION 1
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <cstddef>
|
||||
#include <sstream>
|
||||
#include <streambuf>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#define STRINGIZE_EX(x) #x
|
||||
#define STRINGIZE(x) STRINGIZE_EX(x)
|
||||
|
||||
namespace
|
||||
{
|
||||
// A streambuf that keeps no get area at all and refuses every putback: with an
|
||||
// empty get area, sungetc() always ends up in pbackfail(). Used to check that
|
||||
// the character terminating a number is left in the input without relying on
|
||||
// the streambuf being able to put a consumed character back.
|
||||
class no_putback_streambuf : public std::streambuf
|
||||
{
|
||||
public:
|
||||
explicit no_putback_streambuf(std::string s) : m_data(std::move(s)) {}
|
||||
|
||||
protected:
|
||||
// peek at the next character without consuming it
|
||||
int_type underflow() override
|
||||
{
|
||||
if (m_pos >= m_data.size())
|
||||
{
|
||||
return traits_type::eof();
|
||||
}
|
||||
return traits_type::to_int_type(m_data[m_pos]);
|
||||
}
|
||||
|
||||
// consume the next character
|
||||
int_type uflow() override
|
||||
{
|
||||
if (m_pos >= m_data.size())
|
||||
{
|
||||
return traits_type::eof();
|
||||
}
|
||||
return traits_type::to_int_type(m_data[m_pos++]);
|
||||
}
|
||||
|
||||
int_type pbackfail(int_type /*c*/) override
|
||||
{
|
||||
return traits_type::eof();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_data;
|
||||
std::size_t m_pos = 0;
|
||||
};
|
||||
|
||||
// read the characters that are left in a stream
|
||||
std::string remaining(std::istream& is)
|
||||
{
|
||||
std::string result;
|
||||
char c = 0;
|
||||
while (is.get(c))
|
||||
{
|
||||
result += c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("JSON_PRECISE_STREAM_POSITION")
|
||||
{
|
||||
SECTION("the macro is part of the ABI tag")
|
||||
{
|
||||
const std::string ns = STRINGIZE(NLOHMANN_JSON_NAMESPACE);
|
||||
// other tags may come before it, e.g. json_abi_diag_psp
|
||||
CHECK(ns.find("_psp") != std::string::npos);
|
||||
}
|
||||
|
||||
SECTION("a number does not consume the character that terminates it")
|
||||
{
|
||||
// a number is only terminated by the character following it; that
|
||||
// character must be given back so the stream is positioned right
|
||||
// after the value
|
||||
const std::vector<std::pair<std::string, std::string>> tests =
|
||||
{
|
||||
{"1true", "true"},
|
||||
{"1[2]", "[2]"},
|
||||
{"1{}", "{}"},
|
||||
{R"(1"a")", R"("a")"},
|
||||
{"1 true", " true"},
|
||||
{"12,", ","},
|
||||
{"-0.5e3x", "x"},
|
||||
{"1null", "null"}
|
||||
};
|
||||
|
||||
for (const auto& test : tests)
|
||||
{
|
||||
CAPTURE(test.first);
|
||||
std::istringstream ss(test.first);
|
||||
json j;
|
||||
ss >> j;
|
||||
CHECK(j == json::parse(test.first.substr(0, test.first.size() - test.second.size())));
|
||||
CHECK(remaining(ss) == test.second);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("values that are self-delimiting are unaffected")
|
||||
{
|
||||
const std::vector<std::pair<std::string, std::string>> tests =
|
||||
{
|
||||
{"truefalse", "false"},
|
||||
{"[1][2]", "[2]"},
|
||||
{R"({"a":1}{"b":2})", R"({"b":2})"},
|
||||
{R"("a""b")", R"("b")"},
|
||||
{"null null", " null"}
|
||||
};
|
||||
|
||||
for (const auto& test : tests)
|
||||
{
|
||||
CAPTURE(test.first);
|
||||
std::istringstream ss(test.first);
|
||||
json j;
|
||||
ss >> j;
|
||||
CHECK(remaining(ss) == test.second);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a number at the end of the input leaves nothing behind")
|
||||
{
|
||||
for (const std::string s :
|
||||
{"1", "12", "-3.5e2", " 7 "
|
||||
})
|
||||
{
|
||||
CAPTURE(s);
|
||||
std::istringstream ss(s);
|
||||
json j;
|
||||
ss >> j;
|
||||
CHECK(remaining(ss).find_first_not_of(" \t\n\r") == std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("repeated extraction of concatenated values")
|
||||
{
|
||||
std::istringstream ss(R"(1true[2]3"x"{"a":4}5)");
|
||||
const std::vector<json> expected =
|
||||
{
|
||||
json(1), json(true), json::parse("[2]"), json(3),
|
||||
json("x"), json::parse(R"({"a":4})"), json(5)
|
||||
};
|
||||
|
||||
for (const auto& e : expected)
|
||||
{
|
||||
json j;
|
||||
ss >> j;
|
||||
CHECK(j == e);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("differences to the default behavior")
|
||||
{
|
||||
// both of these work by accident without the macro, because the
|
||||
// character after a number is swallowed; see unit-deserialization.cpp
|
||||
|
||||
SECTION("a separator after a number is not skipped")
|
||||
{
|
||||
std::istringstream ss("1,2");
|
||||
json j;
|
||||
ss >> j;
|
||||
CHECK(j == 1);
|
||||
CHECK_THROWS_AS(ss >> j, json::parse_error&);
|
||||
}
|
||||
|
||||
SECTION("std::getline after a number sees the line break")
|
||||
{
|
||||
std::istringstream ss("42\nfoo");
|
||||
json j;
|
||||
std::string line;
|
||||
ss >> j;
|
||||
std::getline(ss, line);
|
||||
CHECK(j == 42);
|
||||
CHECK(line.empty());
|
||||
std::getline(ss, line);
|
||||
CHECK(line == "foo");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("sax_parse with strict == false")
|
||||
{
|
||||
std::istringstream ss("1true");
|
||||
json j;
|
||||
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::input_stream_adapter> sdp(j, true);
|
||||
CHECK(json::sax_parse(ss, &sdp, nlohmann::detail::input_format_t::json, false));
|
||||
CHECK(j == 1);
|
||||
CHECK(remaining(ss) == "true");
|
||||
}
|
||||
|
||||
SECTION("strict parsing still rejects trailing data")
|
||||
{
|
||||
std::istringstream ss("1true");
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::parse(ss),
|
||||
"[json.exception.parse_error.101] parse error at line 1, column 5: syntax error while parsing value - unexpected true literal; expected end of input", json::parse_error&);
|
||||
|
||||
std::istringstream ss2("1true");
|
||||
CHECK_FALSE(json::accept(ss2));
|
||||
}
|
||||
|
||||
SECTION("a streambuf that cannot put back is not needed")
|
||||
{
|
||||
// the terminating character is never consumed, so no putback
|
||||
// position is required
|
||||
no_putback_streambuf buf("1true");
|
||||
std::istream is(&buf);
|
||||
json j;
|
||||
is >> j;
|
||||
CHECK(j == json(1));
|
||||
CHECK(remaining(is) == "true");
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using nlohmann::json;
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "round_trip_corpus.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
namespace
|
||||
@@ -2265,7 +2266,9 @@ TEST_CASE("UBJSON optimized arrays of a valueless type are bounded")
|
||||
|
||||
SECTION("an excessive count is rejected")
|
||||
{
|
||||
// 'l' is a big-endian int32: 0x7FFFFFFF elements, about 34 GB of value
|
||||
// 'l' is a big-endian int32: 0x7FFFFFFF elements, about 34 GB of value;
|
||||
// OSS-Fuzz reported this shape as a parse_ubjson_fuzzer timeout
|
||||
// (testcase 6347769435193344, no issue filed)
|
||||
for (const auto marker :
|
||||
{'Z', 'T', 'F'
|
||||
})
|
||||
@@ -2817,6 +2820,51 @@ TEST_CASE("UBJSON use_type requires use_size")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("UBJSON round-trip invariants")
|
||||
{
|
||||
// This checks what the parse_ubjson_fuzzer driver checks (see
|
||||
// tests/src/fuzzer-parse_ubjson.cpp), so that a regression shows up in CI
|
||||
// rather than as an OSS-Fuzz report: every value from_ubjson() 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
|
||||
// reproduces the exact bytes. Beyond the driver, this also checks that j2
|
||||
// equals j1. Values are compared with dump() rather than operator==,
|
||||
// because a NaN never compares equal to itself.
|
||||
struct options
|
||||
{
|
||||
bool use_size;
|
||||
bool use_type;
|
||||
};
|
||||
const std::vector<options> all_options =
|
||||
{
|
||||
{false, false},
|
||||
{true, false},
|
||||
{true, true},
|
||||
};
|
||||
|
||||
for (const auto& j0 : utils::round_trip_corpus::values())
|
||||
{
|
||||
// turn the corpus value into a value as from_ubjson() returns it; this
|
||||
// has no binary values, as UBJSON writes them as arrays of integers
|
||||
for (const auto& initial : all_options)
|
||||
{
|
||||
const json j1 = json::from_ubjson(json::to_ubjson(j0, initial.use_size, initial.use_type));
|
||||
|
||||
for (const auto& o : all_options)
|
||||
{
|
||||
INFO("j1 = " << j1.dump() << ", use_size = " << o.use_size << ", use_type = " << o.use_type);
|
||||
|
||||
const std::vector<std::uint8_t> vec = json::to_ubjson(j1, o.use_size, o.use_type);
|
||||
json j2;
|
||||
// anything the library writes must be parsable by the library
|
||||
REQUIRE_NOTHROW(j2 = json::from_ubjson(vec));
|
||||
CHECK(j2.dump() == j1.dump());
|
||||
CHECK(json::to_ubjson(j2, o.use_size, o.use_type) == vec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("UBJSON roundtrips" * doctest::skip())
|
||||
{
|
||||
SECTION("input from self-generated UBJSON files")
|
||||
|
||||
Reference in New Issue
Block a user