Merge branch 'develop' into claude/3-13-0-perf-blog-a3ba5b
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
139 files changed
+24103
-2968
No files matched your search
@@ -629,6 +629,22 @@ static json make_containers()
|
||||
return j;
|
||||
}
|
||||
|
||||
/// many arrays of one number each: one-element arrays are left to the general
|
||||
/// path (CBOR and MessagePack), which reads a lone number slower than the loop
|
||||
/// for arrays of scalars would
|
||||
static json make_singles()
|
||||
{
|
||||
json data = json::array();
|
||||
for (std::size_t i = 0; i < 100000; ++i)
|
||||
{
|
||||
data.push_back(json::array({i}));
|
||||
}
|
||||
|
||||
json j = json::object();
|
||||
j["data"] = std::move(data);
|
||||
return j;
|
||||
}
|
||||
|
||||
/// one flat array of numbers: the scalar decoding path, which must not move
|
||||
static json make_scalars()
|
||||
{
|
||||
@@ -676,6 +692,8 @@ BENCHMARK_CAPTURE(FromBinaryShape, containers / ubjson_optimized, make_container
|
||||
BENCHMARK_CAPTURE(FromBinaryShape, containers / bjdata, make_containers, binary_format::bjdata);
|
||||
BENCHMARK_CAPTURE(FromBinaryShape, containers / bson, make_containers, binary_format::bson);
|
||||
BENCHMARK_CAPTURE(FromBinaryShape, containers / bon8, make_containers, binary_format::bon8);
|
||||
BENCHMARK_CAPTURE(FromBinaryShape, singles / cbor, make_singles, binary_format::cbor);
|
||||
BENCHMARK_CAPTURE(FromBinaryShape, singles / msgpack, make_singles, binary_format::msgpack);
|
||||
// BSON names every array element, so a large array measures key generation
|
||||
// rather than scalar decoding and is left out here
|
||||
BENCHMARK_CAPTURE(FromBinaryShape, scalars / cbor, make_scalars, binary_format::cbor);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#include <algorithm> // any_of
|
||||
#include <cstddef> // size_t
|
||||
#include <functional> // function
|
||||
#include <random> // mt19937
|
||||
#include <string> // string
|
||||
@@ -88,6 +89,27 @@ inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v)
|
||||
});
|
||||
}
|
||||
|
||||
// the message of an exception about the value at path (a JSON pointer, "" for
|
||||
// the root) whose source bytes are [start, end): the context basic_json adds
|
||||
// with JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS, if enabled, then what
|
||||
inline std::string message(const std::string& id, const std::string& path, std::size_t start, std::size_t end, const std::string& what)
|
||||
{
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
const std::string positions = "(bytes " + std::to_string(start) + "-" + std::to_string(end) + ") ";
|
||||
#else
|
||||
const std::string positions;
|
||||
static_cast<void>(start);
|
||||
static_cast<void>(end);
|
||||
#endif
|
||||
#if JSON_DIAGNOSTICS
|
||||
const std::string context = path.empty() ? "" : "(" + path + ") " + positions;
|
||||
#else
|
||||
const std::string& context = positions;
|
||||
static_cast<void>(path);
|
||||
#endif
|
||||
return "[json.exception." + id + "] " + context + what;
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// the exception a call throws, or "" if it throws none
|
||||
inline std::string exception_of_call(const std::function<void()>& f)
|
||||
|
||||
@@ -97,3 +97,23 @@ TEST_CASE("CBOR")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("MessagePack")
|
||||
{
|
||||
SECTION("parse errors")
|
||||
{
|
||||
SECTION("array/map size equal to the unknown-size sentinel")
|
||||
{
|
||||
// on 32-bit platforms an array 32/map 32 length of 0xFFFFFFFF aliases unknown_size()
|
||||
std::vector<uint8_t> const varr = {0xDD, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
std::vector<uint8_t> const vmap = {0xDF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_msgpack(varr), "[json.exception.out_of_range.408] syntax error while parsing MessagePack size: excessive array size", json::out_of_range&);
|
||||
CHECK(json::from_msgpack(varr, true, false).is_discarded());
|
||||
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_msgpack(vmap), "[json.exception.out_of_range.408] syntax error while parsing MessagePack size: excessive map size", json::out_of_range&);
|
||||
CHECK(json::from_msgpack(vmap, true, false).is_discarded());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <valarray>
|
||||
#if JSON_HAS_RANGES
|
||||
#include <ranges>
|
||||
@@ -790,7 +792,7 @@ TEST_CASE("a failed allocation leaves the value unchanged")
|
||||
#if JSON_HAS_RANGES && !defined(__MINGW32__)
|
||||
const std::vector<int> numbers = {1, 2};
|
||||
next_construct_fails = true;
|
||||
CHECK_THROWS_AS(nlohmann::to_json(j, numbers | std::views::filter([](int /*unused*/)
|
||||
CHECK_THROWS_AS(nlohmann::to_json(j, numbers | std::views::filter([](int /*unused*/) noexcept
|
||||
{
|
||||
return true;
|
||||
})), std::bad_alloc&);
|
||||
@@ -804,3 +806,410 @@ TEST_CASE("a failed allocation leaves the value unchanged")
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// the no-exceptions CI job has no std::bad_alloc to catch; with iterator
|
||||
// debugging (MSVC STL debug builds) containers allocate inside noexcept
|
||||
// functions, so failing an allocation there terminates the program
|
||||
#if !defined(JSON_NOEXCEPTION) && !(defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
|
||||
namespace
|
||||
{
|
||||
// whether every child of j names j as its parent (always true without
|
||||
// JSON_DIAGNOSTICS, where no parents are stored)
|
||||
template<class BasicJsonType>
|
||||
bool children_name_their_parent(const BasicJsonType& j)
|
||||
{
|
||||
#if JSON_DIAGNOSTICS
|
||||
if (j.is_array())
|
||||
{
|
||||
for (const auto& element : *j.m_data.m_value.array)
|
||||
{
|
||||
if (element.m_parent != &j)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (j.is_object())
|
||||
{
|
||||
for (const auto& element : *j.m_data.m_value.object)
|
||||
{
|
||||
if (element.second.m_parent != &j)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
static_cast<void>(j);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class guarantee
|
||||
{
|
||||
basic, // the value stays valid and keeps its type
|
||||
strong // the value is unchanged
|
||||
};
|
||||
|
||||
// Runs op on a fresh value make() with the 0th, 1st, 2nd, ... allocation made
|
||||
// by the library failing in turn, until op completes. Every failed attempt
|
||||
// must have thrown std::bad_alloc and left the value valid, with all parent
|
||||
// pointers intact (copying a value asserts the invariant), and for the strong
|
||||
// guarantee exactly as it was.
|
||||
template<class BasicJsonType>
|
||||
void check_exception_guarantee(guarantee g,
|
||||
const std::function<BasicJsonType()>& make,
|
||||
const std::function<void(BasicJsonType&)>& op)
|
||||
{
|
||||
fail_at_alloc_call = -1;
|
||||
|
||||
bool completed = false;
|
||||
std::size_t failures = 0;
|
||||
for (std::size_t n = 0; !completed; ++n)
|
||||
{
|
||||
CAPTURE(n)
|
||||
REQUIRE(n < 1000);
|
||||
|
||||
BasicJsonType j = make();
|
||||
const std::string before = j.dump();
|
||||
const auto type_before = j.type();
|
||||
|
||||
alloc_call_count = 0;
|
||||
fail_at_alloc_call = static_cast<long>(n);
|
||||
try
|
||||
{
|
||||
op(j);
|
||||
completed = true;
|
||||
}
|
||||
catch (const std::bad_alloc&)
|
||||
{
|
||||
++failures;
|
||||
}
|
||||
fail_at_alloc_call = -1;
|
||||
|
||||
if (!completed)
|
||||
{
|
||||
CHECK((j.type() == type_before));
|
||||
CHECK(children_name_their_parent(j));
|
||||
const BasicJsonType copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is the point, it allocates
|
||||
CHECK(copy.dump() == j.dump());
|
||||
if (g == guarantee::strong)
|
||||
{
|
||||
CHECK(j.dump() == before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the operation allocates, so at least one attempt must have failed
|
||||
CHECK(failures > 0);
|
||||
}
|
||||
|
||||
template<class BasicJsonType>
|
||||
void check_insert_exception_guarantees()
|
||||
{
|
||||
using json_t = BasicJsonType;
|
||||
using make_t = std::function<json_t()>;
|
||||
|
||||
// strings of this length do not fit into the small string buffer, so
|
||||
// copying them allocates
|
||||
const json_t val = std::string(64, 'x');
|
||||
const json_t source = json_t::array({std::string(64, 'p'), std::string(64, 'q'), std::string(64, 'r')});
|
||||
|
||||
for (const bool spare_capacity :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
CAPTURE(spare_capacity)
|
||||
|
||||
// [ "aaa...", "bbb...", "ccc..." ], with a spare slot if requested
|
||||
const make_t make = [spare_capacity]()
|
||||
{
|
||||
json_t j = json_t::array({std::string(64, 'a'), std::string(64, 'b'), std::string(64, 'c')});
|
||||
if (spare_capacity)
|
||||
{
|
||||
j.push_back(std::string(64, 'd'));
|
||||
j.erase(3);
|
||||
}
|
||||
return json_t(std::move(j));
|
||||
};
|
||||
|
||||
for (const bool at_end :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
CAPTURE(at_end)
|
||||
|
||||
SECTION("insert(pos, const basic_json&)")
|
||||
{
|
||||
check_exception_guarantee<json_t>(guarantee::strong, make, [&](json_t& j)
|
||||
{
|
||||
j.insert(at_end ? j.end() : j.begin() + 1, val);
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("insert(pos, basic_json&&)")
|
||||
{
|
||||
check_exception_guarantee<json_t>(guarantee::strong, make, [&](json_t& j)
|
||||
{
|
||||
j.insert(at_end ? j.end() : j.begin() + 1, json_t(val));
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("insert(pos, size_type, const basic_json&)")
|
||||
{
|
||||
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
|
||||
{
|
||||
j.insert(at_end ? j.end() : j.begin() + 1, 3, val);
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("insert(pos, const_iterator, const_iterator)")
|
||||
{
|
||||
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
|
||||
{
|
||||
j.insert(at_end ? j.end() : j.begin() + 1, source.begin(), source.end());
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("insert(pos, initializer_list_t)")
|
||||
{
|
||||
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
|
||||
{
|
||||
j.insert(at_end ? j.end() : j.begin() + 1, {val, val});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("insert(const_iterator, const_iterator) for objects")
|
||||
{
|
||||
const json_t members = {{"m", std::string(64, 'm')}, {"n", std::string(64, 'n')}, {"o", std::string(64, 'o')}};
|
||||
|
||||
SECTION("no member exists yet")
|
||||
{
|
||||
check_exception_guarantee<json_t>(guarantee::basic, []()
|
||||
{
|
||||
return json_t({{"a", 1}});
|
||||
}, [&](json_t& j)
|
||||
{
|
||||
j.insert(members.begin(), members.end());
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("some members exist already")
|
||||
{
|
||||
check_exception_guarantee<json_t>(guarantee::basic, []()
|
||||
{
|
||||
return json_t({{"a", 1}, {"n", std::string(64, 'x')}});
|
||||
}, [&](json_t& j)
|
||||
{
|
||||
j.insert(members.begin(), members.end());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("insert() provides its documented exception guarantees (#5986)")
|
||||
{
|
||||
SECTION("std::map-backed object_t")
|
||||
{
|
||||
check_insert_exception_guarantees<nlohmann::json::with_allocator_t<nth_alloc_fails_allocator>>();
|
||||
}
|
||||
|
||||
SECTION("ordered_map-backed object_t")
|
||||
{
|
||||
check_insert_exception_guarantees<nlohmann::ordered_json::with_allocator_t<nth_alloc_fails_allocator>>();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
namespace
|
||||
{
|
||||
// the allocation numbered sweep_fail_at (counted from 1) throws; 0 never fails
|
||||
std::size_t sweep_allocations = 0;
|
||||
std::size_t sweep_fail_at = 0;
|
||||
std::ptrdiff_t sweep_live = 0;
|
||||
|
||||
template<class T>
|
||||
struct sweep_allocator
|
||||
{
|
||||
using value_type = T;
|
||||
// GCC 4.8's std::map takes these from the allocator instead of from
|
||||
// std::allocator_traits
|
||||
using pointer = T*;
|
||||
using const_pointer = const T*;
|
||||
using reference = T&;
|
||||
using const_reference = const T&;
|
||||
using size_type = std::size_t;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
sweep_allocator() = default;
|
||||
template<class U> sweep_allocator(const sweep_allocator<U>& /*unused*/) noexcept {}
|
||||
|
||||
T* allocate(std::size_t n)
|
||||
{
|
||||
if (sweep_fail_at != 0 && ++sweep_allocations == sweep_fail_at)
|
||||
{
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
++sweep_live;
|
||||
return std::allocator<T>().allocate(n);
|
||||
}
|
||||
|
||||
void deallocate(T* p, std::size_t n) noexcept
|
||||
{
|
||||
--sweep_live;
|
||||
std::allocator<T>().deallocate(p, n);
|
||||
}
|
||||
|
||||
// GCC 4.8's std::map calls these on the allocator directly instead of
|
||||
// going through std::allocator_traits
|
||||
std::size_t max_size() const noexcept
|
||||
{
|
||||
return (std::numeric_limits<std::size_t>::max)() / sizeof(T);
|
||||
}
|
||||
|
||||
template<class U, class... Args>
|
||||
void construct(U* p, Args&& ... args) noexcept(noexcept(::new (static_cast<void*>(p)) U(std::forward<Args>(args)...)))
|
||||
{
|
||||
::new (static_cast<void*>(p)) U(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
// libc++ probes whether construct() is usable for const objects (when
|
||||
// checking for trivial copies); this must not be viable then
|
||||
template<class U, class... Args>
|
||||
void construct(const U* /*unused*/, const Args& ... /*unused*/) = delete;
|
||||
|
||||
template<class U>
|
||||
void destroy(U* p) noexcept
|
||||
{
|
||||
static_cast<void>(p); // unused for trivially destructible types
|
||||
p->~U();
|
||||
}
|
||||
|
||||
template <class U>
|
||||
struct rebind
|
||||
{
|
||||
using other = sweep_allocator<U>;
|
||||
};
|
||||
|
||||
bool operator==(const sweep_allocator& /*unused*/) const noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool operator!=(const sweep_allocator& /*unused*/) const noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// run make() with the allocation numbered k failing, for k = 1, 2, ...: every
|
||||
// failure must be a std::bad_alloc and must not leave anything behind, and
|
||||
// the first k without a failure must yield the value made without failures
|
||||
template<class BasicJsonType, class Make>
|
||||
void sweep_allocations_of(const Make& make)
|
||||
{
|
||||
sweep_fail_at = 0;
|
||||
const BasicJsonType expected = make();
|
||||
const std::ptrdiff_t live_before = sweep_live;
|
||||
|
||||
std::size_t failures = 0;
|
||||
for (std::size_t k = 1; k < 10000; ++k)
|
||||
{
|
||||
sweep_allocations = 0;
|
||||
sweep_fail_at = k;
|
||||
try
|
||||
{
|
||||
const BasicJsonType result = make();
|
||||
sweep_fail_at = 0;
|
||||
CHECK(result == expected);
|
||||
break;
|
||||
}
|
||||
catch (const std::bad_alloc&)
|
||||
{
|
||||
sweep_fail_at = 0;
|
||||
++failures;
|
||||
}
|
||||
CHECK(sweep_live == live_before);
|
||||
}
|
||||
sweep_fail_at = 0;
|
||||
CHECK(failures > 10);
|
||||
CHECK(sweep_live == live_before);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("parse() survives a failing allocation at every point (#5985)")
|
||||
{
|
||||
// strings beyond the small string buffer, nested objects and arrays
|
||||
const std::string text = R"({"a":"a long string that does not fit into the small string buffer1","b":[1,2,[3,{"k":"another string that is longer than any small string buffer"}]],"c":{"x":1,"y":{"z":[]},"w":"yet another string that does not fit into a small buffer"},"d":1,"b":[[],{}]})";
|
||||
|
||||
using sweep_json = nlohmann::json::with_allocator_t<sweep_allocator>;
|
||||
using sweep_ordered_json = nlohmann::ordered_json::with_allocator_t<sweep_allocator>;
|
||||
|
||||
SECTION("json")
|
||||
{
|
||||
sweep_allocations_of<sweep_json>([&]
|
||||
{
|
||||
return sweep_json::parse(text);
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("ordered_json")
|
||||
{
|
||||
sweep_allocations_of<sweep_ordered_json>([&]
|
||||
{
|
||||
return sweep_ordered_json::parse(text);
|
||||
});
|
||||
}
|
||||
|
||||
// With iterator debugging (MSVC STL debug builds, also used by clang-cl),
|
||||
// containers allocate a debug proxy through the allocator inside their
|
||||
// noexcept default and move constructors, so failing that allocation
|
||||
// terminates the program instead of throwing std::bad_alloc. Nothing to
|
||||
// check there.
|
||||
#if !(defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
|
||||
// Clang 3.4 and 3.5 leak memory when an element constructor throws
|
||||
// inside libstdc++'s containers (a std::map copy, a std::vector
|
||||
// reallocation), without any json value involved.
|
||||
#if !(defined(__clang__) && !defined(__apple_build_version__) && __clang_major__ == 3 && __clang_minor__ < 6)
|
||||
SECTION("parser callback")
|
||||
{
|
||||
const auto keep_all = [](int /*depth*/, sweep_json::parse_event_t /*event*/, sweep_json& /*parsed*/) noexcept
|
||||
{
|
||||
return true;
|
||||
};
|
||||
sweep_allocations_of<sweep_json>([&]
|
||||
{
|
||||
return sweep_json::parse(text, keep_all);
|
||||
});
|
||||
|
||||
const auto drop_d = [](int /*depth*/, sweep_json::parse_event_t event, sweep_json & parsed)
|
||||
{
|
||||
return event != sweep_json::parse_event_t::key || parsed != "d";
|
||||
};
|
||||
sweep_allocations_of<sweep_json>([&]
|
||||
{
|
||||
return sweep_json::parse(text, drop_d);
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("sax_parse into an existing value")
|
||||
{
|
||||
// a failed parse leaves the target in a state that is safe to destroy
|
||||
using sax_t = nlohmann::detail::json_sax_dom_parser<sweep_json, nlohmann::detail::iterator_input_adapter<const char*>>;
|
||||
sweep_allocations_of<sweep_json>([&]() -> sweep_json
|
||||
{
|
||||
sweep_json value;
|
||||
sax_t sax(value, false);
|
||||
static_cast<void>(sweep_json::sax_parse(text, &sax));
|
||||
return value;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -483,4 +484,56 @@ TEST_CASE("alternative string type")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("alternative string type with json_view")
|
||||
{
|
||||
// regression test for #5994: basic_json_view::dump() called
|
||||
// StringType::capacity(), which alt_string (and the documented StringType
|
||||
// requirements) do not provide
|
||||
using alt_document = nlohmann::basic_json_document<alt_json, nlohmann::view_mode::read_only>;
|
||||
using alt_editable_document = nlohmann::basic_json_document<alt_json, nlohmann::view_mode::editable>;
|
||||
|
||||
const char* text = R"({"a":[1,"x\u00e4"],"b":{"c":true,"d":null},"e":2.5})";
|
||||
const alt_json j = alt_json::parse(text);
|
||||
|
||||
SECTION("read-only document")
|
||||
{
|
||||
const alt_document d = alt_document::parse(text);
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().materialize() == j);
|
||||
CHECK(d.root().contains(alt_string("b")));
|
||||
CHECK(d.root()[alt_string("b")][alt_string("c")].dump() == "true");
|
||||
}
|
||||
|
||||
SECTION("long output")
|
||||
{
|
||||
// more than one growth step of the output buffer
|
||||
alt_json big = alt_json::array();
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
big.push_back(alt_json::parse(text));
|
||||
}
|
||||
const alt_string big_text = big.dump();
|
||||
const alt_document d = alt_document::parse(big_text);
|
||||
CHECK(d.root().dump() == big_text);
|
||||
CHECK(d.root().dump(4) == big.dump(4));
|
||||
}
|
||||
|
||||
SECTION("editable document")
|
||||
{
|
||||
alt_editable_document d = alt_editable_document::parse(text);
|
||||
d.set(d.root(), "f", "new");
|
||||
d.push_back(d.root()[alt_string("a")], 3);
|
||||
alt_json expected = j;
|
||||
expected["f"] = "new";
|
||||
expected["a"].push_back(3);
|
||||
CHECK(d.root().materialize() == expected);
|
||||
CHECK(alt_json::parse(d.root().dump()) == expected);
|
||||
|
||||
const alt_editable_document loaded = alt_editable_document::load(d.save());
|
||||
CHECK(loaded.root().materialize() == expected);
|
||||
}
|
||||
}
|
||||
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
@@ -14,7 +14,9 @@ using nlohmann::json;
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -48,6 +50,14 @@ std::vector<json> test_values()
|
||||
large_payloads.push_back(json::binary(std::vector<std::uint8_t>(static_cast<std::size_t>(100 * i), static_cast<std::uint8_t>(i))));
|
||||
}
|
||||
|
||||
// several times vector_sink_window characters in every format: the vector
|
||||
// sink makes the capacity it grows to writable a window at a time
|
||||
json many_windows = json::array();
|
||||
for (std::uint64_t i = 0; i < 30000; ++i)
|
||||
{
|
||||
many_windows.push_back((i * 0x9E3779B97F4A7C15u) >> 1u);
|
||||
}
|
||||
|
||||
return
|
||||
{
|
||||
json(nullptr), json(true), json(false),
|
||||
@@ -59,7 +69,7 @@ std::vector<json> test_values()
|
||||
json::array(), json::object(),
|
||||
json::array({1, 2, 3}), json({{"a", 1}, {"b", nullptr}}),
|
||||
json({{"nested", {{"deep", json::array({1, "two", 3.0, nullptr})}}}}),
|
||||
big_array, big_object, large_payloads,
|
||||
big_array, big_object, large_payloads, many_windows,
|
||||
json::binary(std::vector<std::uint8_t>(5000, 0x42))
|
||||
};
|
||||
}
|
||||
@@ -767,3 +777,69 @@ TEST_CASE("a write that throws leaves the same bytes in every kind of output")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
namespace
|
||||
{
|
||||
|
||||
// an adapter whose every write fails
|
||||
class failing_adapter : public nlohmann::detail::output_adapter_protocol<char>
|
||||
{
|
||||
public:
|
||||
[[noreturn]] void write_character(char /*c*/) override
|
||||
{
|
||||
throw std::runtime_error("write failed");
|
||||
}
|
||||
|
||||
[[noreturn]] void write_characters(const char* /*s*/, std::size_t /*length*/) override
|
||||
{
|
||||
throw std::runtime_error("write failed");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
TEST_CASE("output adapters")
|
||||
{
|
||||
SECTION("writing a single character")
|
||||
{
|
||||
// the writers pass whole buffers on, but an adapter takes single
|
||||
// characters as well
|
||||
std::vector<char> as_vector;
|
||||
nlohmann::detail::output_vector_adapter<char> vector_adapter(as_vector);
|
||||
vector_adapter.write_character('a');
|
||||
vector_adapter.write_character('b');
|
||||
CHECK(as_vector == std::vector<char>({'a', 'b'}));
|
||||
|
||||
std::ostringstream as_stream;
|
||||
nlohmann::detail::output_stream_adapter<char> stream_adapter(as_stream);
|
||||
stream_adapter.write_character('c');
|
||||
stream_adapter.write_character('d');
|
||||
CHECK(as_stream.str() == "cd");
|
||||
|
||||
std::string as_string;
|
||||
nlohmann::detail::output_string_adapter<char> string_adapter(as_string);
|
||||
string_adapter.write_character('e');
|
||||
string_adapter.write_character('f');
|
||||
CHECK(as_string == "ef");
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("a sink that is destroyed with characters left over")
|
||||
{
|
||||
// the destructor passes the pending characters on, as a write that
|
||||
// threw leaves them there; the adapter failing then must not escape
|
||||
{
|
||||
nlohmann::detail::output_adapter_sink<char> sink(std::make_shared<failing_adapter>());
|
||||
sink.write_character('x');
|
||||
}
|
||||
|
||||
// while flush() reports the failure
|
||||
nlohmann::detail::output_adapter_sink<char> sink(std::make_shared<failing_adapter>());
|
||||
sink.write_character('x');
|
||||
CHECK_THROWS_AS(sink.flush(), std::runtime_error&);
|
||||
// (the character counts as written, so there is nothing left for the destructor)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+136
-11
@@ -26,11 +26,14 @@ using nlohmann::ordered_json;
|
||||
#include <limits>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "sax_countdown.hpp"
|
||||
#include "sax_event_loggers.hpp"
|
||||
using utils::SaxCountdown;
|
||||
using utils::SaxEventLogger;
|
||||
|
||||
|
||||
// trait_test_arg and the "value_in_range_of trait" TEST_CASE_TEMPLATE_DEFINE
|
||||
@@ -75,23 +78,25 @@ TEST_CASE("BJData")
|
||||
{
|
||||
SECTION("binary_reader BJData lookup tables")
|
||||
{
|
||||
// both lookups are static member functions
|
||||
// bjd_type_name() is static; is_valid_optimized_type() needs a BJData reader
|
||||
std::vector<std::uint8_t> const data;
|
||||
using reader_t = nlohmann::detail::binary_reader<json, decltype(nlohmann::detail::input_adapter(data))>;
|
||||
|
||||
// the excluded optimized-type markers must match binary_writer's
|
||||
// is_bjdata_excluded_type_marker(), which encodes the same 8 markers
|
||||
// the markers BJData excludes as optimized types must match
|
||||
// binary_writer's is_bjdata_excluded_type_marker(), which encodes the
|
||||
// same 8 markers
|
||||
const reader_t reader(nlohmann::detail::input_adapter(data), json::input_format_t::bjdata);
|
||||
for (const char marker :
|
||||
{'[', '{', 'S', 'H', 'T', 'F', 'N', 'Z'
|
||||
{'[', '{', 'S', 'H', 'T', 'F', 'N', 'Z', 'x'
|
||||
})
|
||||
{
|
||||
CHECK(reader_t::is_bjd_excluded_optimized_type(static_cast<unsigned char>(marker)));
|
||||
CHECK(!reader.is_valid_optimized_type(static_cast<unsigned char>(marker)));
|
||||
}
|
||||
for (const char marker :
|
||||
{'U', 'i', 'u', 'I', 'm', 'l', 'M', 'L', 'd', 'D', 'C', 'B', 'x'
|
||||
{'U', 'i', 'u', 'I', 'm', 'l', 'M', 'L', 'd', 'D', 'C', 'B', 'h'
|
||||
})
|
||||
{
|
||||
CHECK(!reader_t::is_bjd_excluded_optimized_type(static_cast<unsigned char>(marker)));
|
||||
CHECK(reader.is_valid_optimized_type(static_cast<unsigned char>(marker)));
|
||||
}
|
||||
|
||||
// every dtype marker must round-trip to its ND-array type name
|
||||
@@ -2309,6 +2314,23 @@ TEST_CASE("BJData")
|
||||
CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata));
|
||||
}
|
||||
|
||||
SECTION("every event of an ndarray of chars")
|
||||
{
|
||||
// the elements of an ndarray of 'C' are read one by one
|
||||
std::vector<uint8_t> const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c', 'd'};
|
||||
// start_object, key, string, key, start_array, 2 numbers, end_array,
|
||||
// key, start_array, 4 chars, end_array, end_object
|
||||
constexpr int events = 16;
|
||||
for (int allowed = 0; allowed < events; ++allowed)
|
||||
{
|
||||
CAPTURE(allowed)
|
||||
SaxCountdown scp(allowed);
|
||||
CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata));
|
||||
}
|
||||
SaxCountdown scp(events);
|
||||
CHECK(json::sax_parse(v, &scp, json::input_format_t::bjdata));
|
||||
}
|
||||
|
||||
SECTION("start_array() in ndarray _ArrayData_")
|
||||
{
|
||||
std::vector<uint8_t> const v = {'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 6, 5, 4, 3, 2, 1};
|
||||
@@ -3092,6 +3114,15 @@ TEST_CASE("BJData")
|
||||
json _;
|
||||
CHECK_THROWS_WITH(_ = json::from_bjdata(v), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing BJData char: byte after 'C' must be in range 0x00..0x7F; last byte: 0x82");
|
||||
}
|
||||
|
||||
SECTION("ndarray of chars cut off by the end of the input")
|
||||
{
|
||||
// 2x2 chars announced, three present
|
||||
std::vector<uint8_t> const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c'};
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(v), "[json.exception.parse_error.110] parse error at byte 16: syntax error while parsing BJData char: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_bjdata(v, true, false).is_discarded());
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("byte")
|
||||
@@ -3208,14 +3239,34 @@ TEST_CASE("BJData")
|
||||
#endif
|
||||
CHECK(json::from_bjdata(vM, true, false).is_discarded());
|
||||
|
||||
#if SIZE_MAX != 0xffffffff
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vMX), "[json.exception.out_of_range.408] syntax error while parsing BJData size: excessive ndarray size caused overflow", json::out_of_range&);
|
||||
#else
|
||||
// the first dimension is 2^64-1, which is rejected as a count (see below)
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vMX), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&);
|
||||
#endif
|
||||
CHECK(json::from_bjdata(vMX, true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("optimized array: count equal to SIZE_MAX")
|
||||
{
|
||||
// the reader marks a container without a count by SIZE_MAX, so
|
||||
// a count of that value must not be read as "no count"
|
||||
// (it used to parse as [1])
|
||||
json _;
|
||||
#if SIZE_MAX != 0xffffffff
|
||||
std::vector<uint8_t> const vM = {'[', '#', 'M', 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'};
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vM), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&);
|
||||
CHECK(json::from_bjdata(vM, true, false).is_discarded());
|
||||
// one less is an ordinary excessive count
|
||||
std::vector<uint8_t> const vM1 = {'[', '#', 'M', 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'};
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vM1), "[json.exception.out_of_range.408] excessive array size: 18446744073709551614", json::out_of_range&);
|
||||
#else
|
||||
std::vector<uint8_t> const vm = {'[', '#', 'm', 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'};
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vm), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&);
|
||||
CHECK(json::from_bjdata(vm, true, false).is_discarded());
|
||||
std::vector<uint8_t> const vL = {'[', '#', 'L', 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 'i', 0x01, ']'};
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vL), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&);
|
||||
CHECK(json::from_bjdata(vL, true, false).is_discarded());
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("optimized array: integer value overflow")
|
||||
{
|
||||
#if SIZE_MAX == 0xffffffff
|
||||
@@ -4746,3 +4797,77 @@ TEST_CASE("BJData optimized container type marker is validated for count 0")
|
||||
CHECK(json::from_bjdata(vb) == json::binary({}));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("BJData optimized arrays from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input reads the numbers of an array with a type marker in
|
||||
// one loop, other input number by number; both must give the same events,
|
||||
// value, or error for the complete document and for every truncation of
|
||||
// it, and stop at every event a SAX handler rejects
|
||||
json document =
|
||||
{
|
||||
{"u8", {0, 200, 255}},
|
||||
{"i8", {-5, 100, -128}},
|
||||
{"i16", {-30000, 1000, 30000}},
|
||||
{"i32", {-2000000000, 5, 70000}},
|
||||
{"i64", {-9000000000000000000LL, 1, 5000000000LL}},
|
||||
{"f64", {1.5, -0.1, 2.5}},
|
||||
{"pairs", {{1.25, -3.75}, {1.25, -3.75}, json::array()}},
|
||||
{"mixed", {1, "a", 2.5, nullptr}},
|
||||
{"u16", {40000, 1, 65535}},
|
||||
{"u32", {4000000000U, 1, 70000}},
|
||||
{"u64", {18446744073709551615ULL, 1, 5000000000ULL}},
|
||||
{"nd", {{"_ArrayType_", "double"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {1.5, -2.5, 3.5, -4.5}}}},
|
||||
{"nd8", {{"_ArrayType_", "uint8"}, {"_ArraySize_", {3}}, {"_ArrayData_", {1, 2, 3}}}},
|
||||
};
|
||||
const std::vector<std::uint8_t> encoded = json::to_bjdata(document, true, true);
|
||||
CHECK(json::from_bjdata(encoded) == document);
|
||||
|
||||
for (std::size_t length = 0; length <= encoded.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(encoded.begin(), encoded.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_bjdata(contiguous, true, false).dump() == json::from_bjdata(bytewise.begin(), bytewise.end(), true, false).dump());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string contiguous_error;
|
||||
std::string bytewise_error;
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_bjdata(contiguous);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
contiguous_error = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_bjdata(bytewise.begin(), bytewise.end());
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
bytewise_error = e.what();
|
||||
}
|
||||
CHECK(contiguous_error == bytewise_error);
|
||||
#endif
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::bjdata)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::bjdata));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
|
||||
for (int events = 0; events <= 80; ++events)
|
||||
{
|
||||
CAPTURE(events)
|
||||
const std::list<std::uint8_t> bytewise(encoded.begin(), encoded.end());
|
||||
SaxCountdown from_contiguous(events);
|
||||
SaxCountdown from_bytewise(events);
|
||||
CHECK(json::sax_parse(encoded, &from_contiguous, json::input_format_t::bjdata)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::bjdata));
|
||||
}
|
||||
}
|
||||
@@ -329,6 +329,9 @@ TEST_CASE("BON8")
|
||||
check_bon8({false, nullptr, true}, {0x83, 0xF8, 0xFA, 0xF9});
|
||||
check_bon8({false, nullptr, true, 1.0}, {0x84, 0xF8, 0xFA, 0xF9, 0xFD});
|
||||
check_bon8({false, nullptr, true, 1.0, json::array(), 0.0}, {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE});
|
||||
// an array that starts with a number: -1, 0, and 1 have bytes of their own
|
||||
check_bon8({-1.0, 0.0, 1.0}, {0x83, 0xFB, 0xFC, 0xFD});
|
||||
check_bon8({1.0, -1.0, -0.0}, {0x83, 0xFD, 0xFB, 0x8E, 0x80, 0x00, 0x00, 0x00});
|
||||
check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91});
|
||||
check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF});
|
||||
check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF});
|
||||
|
||||
+299
-2
@@ -19,9 +19,11 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include "make_test_data_available.hpp"
|
||||
@@ -80,8 +82,20 @@ class huge_string_t : public std::string
|
||||
return result;
|
||||
}
|
||||
|
||||
// returns a copy of @a s whose size() pretends to be @a n
|
||||
static huge_string_t as_size(const std::string& s, const size_type n)
|
||||
{
|
||||
huge_string_t result(s);
|
||||
result.fake_size = n;
|
||||
return result;
|
||||
}
|
||||
|
||||
size_type size() const noexcept
|
||||
{
|
||||
if (fake_size != 0)
|
||||
{
|
||||
return fake_size;
|
||||
}
|
||||
if (pretend_huge)
|
||||
{
|
||||
// one byte more than the BSON length field can represent
|
||||
@@ -92,6 +106,7 @@ class huge_string_t : public std::string
|
||||
|
||||
private:
|
||||
bool pretend_huge = false;
|
||||
size_type fake_size = 0;
|
||||
};
|
||||
|
||||
using huge_string_json = nlohmann::json::with_string_t<huge_string_t>;
|
||||
@@ -266,6 +281,17 @@ TEST_CASE("BSON")
|
||||
CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&);
|
||||
}
|
||||
|
||||
SECTION("an array element that no longer fits the document")
|
||||
{
|
||||
// the string fits the length field, but not the document that
|
||||
// holds it; nothing of it is written
|
||||
const auto limit = static_cast<std::string::size_type>((std::numeric_limits<std::int32_t>::max)());
|
||||
huge_string_json j;
|
||||
j["a"] = huge_string_json::array({huge_string_t::as_size("value", limit - 8)});
|
||||
|
||||
CHECK_THROWS_AS(huge_string_json::to_bson(j), huge_string_json::out_of_range&);
|
||||
}
|
||||
|
||||
SECTION("document")
|
||||
{
|
||||
// an oversized string nested one level deep makes the
|
||||
@@ -897,6 +923,38 @@ TEST_CASE("BSON")
|
||||
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
|
||||
}
|
||||
|
||||
SECTION("binary member with subtype 0x02 (old binary) with a truncated inner length")
|
||||
{
|
||||
// the inner length field is cut off by the end of the input
|
||||
std::vector<std::uint8_t> const input =
|
||||
{
|
||||
0x0F, 0x00, 0x00, 0x00, // size (little endian)
|
||||
0x05, 'a', 0x00, // entry: binary
|
||||
0x08, 0x00, 0x00, 0x00, // size of binary (little endian)
|
||||
0x02, // "old binary" subtype
|
||||
0x01 // first byte of the inner length
|
||||
};
|
||||
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON number: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_bson(input, true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("string member that is cut off by the end of the input")
|
||||
{
|
||||
std::vector<std::uint8_t> const input =
|
||||
{
|
||||
0x0F, 0x00, 0x00, 0x00, // size (little endian)
|
||||
0x02, 'a', 0x00, // entry: string
|
||||
0x0A, 0x00, 0x00, 0x00, // size of the string (little endian)
|
||||
'b', 'c' // the string ends early
|
||||
};
|
||||
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON string: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_bson(input, true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("Some more complex document")
|
||||
{
|
||||
json const j =
|
||||
@@ -1164,7 +1222,13 @@ TEST_CASE("BSON keys from contiguous and stream input")
|
||||
// contiguous input reads a key up to its \x00-byte in one step, a stream
|
||||
// reads it byte by byte; both must give the same value or error for the
|
||||
// complete document and for every truncation of it
|
||||
const json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}};
|
||||
json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}};
|
||||
// keys and array element names ("0" to "11") that end at every position of
|
||||
// an 8-byte word, which contiguous input scans at a time
|
||||
for (std::size_t length = 1; length <= 17; ++length)
|
||||
{
|
||||
j[std::string(length, static_cast<char>('a' + length))] = json::array({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, {11}});
|
||||
}
|
||||
const std::vector<std::uint8_t> bson = json::to_bson(j);
|
||||
CHECK(json::from_bson(bson) == j);
|
||||
|
||||
@@ -1184,7 +1248,7 @@ TEST_CASE("BSON keys from contiguous and stream input")
|
||||
}
|
||||
try
|
||||
{
|
||||
std::istringstream stream(std::string(input.begin(), input.end()));
|
||||
std::istringstream stream(std::string(reinterpret_cast<const char*>(input.data()), input.size()));
|
||||
from_stream = json::from_bson(stream).dump();
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
@@ -1192,8 +1256,72 @@ TEST_CASE("BSON keys from contiguous and stream input")
|
||||
from_stream = e.what();
|
||||
}
|
||||
CHECK(from_vector == from_stream);
|
||||
|
||||
// error_handler_t::strict checks the array element names in place on
|
||||
// contiguous input and byte by byte otherwise; the names are
|
||||
// well-formed, so both give the same value or error as above
|
||||
std::string strict_from_vector;
|
||||
std::string strict_from_list;
|
||||
try
|
||||
{
|
||||
strict_from_vector = json::from_bson(input, true, true, json::error_handler_t::strict).dump();
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
strict_from_vector = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
const std::list<std::uint8_t> list(input.begin(), input.end());
|
||||
strict_from_list = json::from_bson(list.begin(), list.end(), true, true, json::error_handler_t::strict).dump();
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
strict_from_list = e.what();
|
||||
}
|
||||
CHECK(strict_from_vector == from_vector);
|
||||
CHECK(strict_from_list == from_vector);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("BSON array element names")
|
||||
{
|
||||
// an array's element names are not part of the value, so they are not
|
||||
// passed on; error_handler_t::strict still rejects ill-formed UTF-8 in them
|
||||
const std::vector<std::uint8_t> bson =
|
||||
{
|
||||
0x1D, 0x00, 0x00, 0x00, // size (little endian)
|
||||
0x04, 'a', 0x00, // entry: array "a"
|
||||
0x15, 0x00, 0x00, 0x00, // size (little endian)
|
||||
0x08, 0xFF, 0x00, 0x01, // entry: boolean named "\xFF", true
|
||||
0x08, '1', '2', '3', '4', '5', '6', '7', '8', '9', 0x00, 0x00, // entry: boolean named "123456789", false
|
||||
0x00, // end of the array
|
||||
0x00 // end of the document
|
||||
};
|
||||
const json expected = {{"a", {true, false}}};
|
||||
|
||||
CHECK(json::from_bson(bson) == expected);
|
||||
CHECK(json::from_bson(bson, true, true, json::error_handler_t::replace) == expected);
|
||||
CHECK(json::from_bson(bson, true, true, json::error_handler_t::ignore) == expected);
|
||||
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bson(bson, true, true, json::error_handler_t::strict), "[json.exception.parse_error.113] parse error at byte 14: syntax error while parsing BSON key: invalid string: ill-formed UTF-8 byte", json::parse_error&);
|
||||
|
||||
std::istringstream stream(std::string(reinterpret_cast<const char*>(bson.data()), bson.size()));
|
||||
CHECK(json::from_bson(stream) == expected);
|
||||
|
||||
// input that is not contiguous reads the names byte by byte and must reject
|
||||
// the same name
|
||||
const std::list<std::uint8_t> list(bson.begin(), bson.end());
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bson(list.begin(), list.end(), true, true, json::error_handler_t::strict), "[json.exception.parse_error.113] parse error at byte 14: syntax error while parsing BSON key: invalid string: ill-formed UTF-8 byte", json::parse_error&);
|
||||
|
||||
// without exceptions, the rejected name ends the parse
|
||||
CHECK(json::from_bson(bson, true, false, json::error_handler_t::strict).is_discarded());
|
||||
|
||||
// well-formed names pass the check without being stored
|
||||
const json valid = {{"a", {true, false, {{"k", json::array({1, 2})}}}}};
|
||||
CHECK(json::from_bson(json::to_bson(valid), true, true, json::error_handler_t::strict) == valid);
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("Negative size of binary value")
|
||||
@@ -2102,6 +2230,25 @@ value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} :
|
||||
CHECK(output.empty());
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("U+0000 in a long key is found at any position")
|
||||
{
|
||||
// the key is searched eight characters at a time
|
||||
for (const std::string& key :
|
||||
{
|
||||
std::string("abc\0defghijkl", 12), std::string("abcdefgh\0ijkl", 13), std::string("abcdefghijklmnop\0", 17), std::string("\0abcdefghijkl", 13),
|
||||
std::string("abcdefghijklmno\0", 16)
|
||||
})
|
||||
{
|
||||
CAPTURE(key)
|
||||
const json value = json::object({{key, 1}});
|
||||
std::vector<std::uint8_t> output;
|
||||
CHECK_THROWS_AS(json::to_bson(value, output), json::out_of_range&);
|
||||
CHECK(output.empty());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)")
|
||||
{
|
||||
// the offending value is nested, so this also covers that the check
|
||||
@@ -2267,3 +2414,153 @@ TEST_CASE("BSON large strings and binaries (chunked reader)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("BSON written in one pass and in two passes")
|
||||
{
|
||||
// to_bson() into a returned vector writes each size once the object or
|
||||
// array is complete; through an output adapter, all sizes are computed
|
||||
// first. Both must produce the same bytes and throw the same exceptions.
|
||||
const auto two_pass = [](const json & j, const json::error_handler_t handler)
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
json::to_bson(j, result, handler);
|
||||
return result;
|
||||
};
|
||||
|
||||
const auto check_same = [&](const json & j, const json::error_handler_t handler)
|
||||
{
|
||||
CAPTURE(j.dump(-1, ' ', true, json::error_handler_t::replace))
|
||||
CAPTURE(static_cast<int>(handler))
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string one_pass_error;
|
||||
std::string two_pass_error;
|
||||
std::vector<std::uint8_t> one;
|
||||
std::vector<std::uint8_t> two;
|
||||
try
|
||||
{
|
||||
one = json::to_bson(j, handler);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
one_pass_error = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
two = two_pass(j, handler);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
two_pass_error = e.what();
|
||||
}
|
||||
CHECK(one_pass_error == two_pass_error);
|
||||
CHECK(one == two);
|
||||
#else
|
||||
CHECK(json::to_bson(j, handler) == two_pass(j, handler));
|
||||
#endif
|
||||
};
|
||||
|
||||
const std::array<json::error_handler_t, 4> handlers = {{
|
||||
json::error_handler_t::keep, json::error_handler_t::strict,
|
||||
json::error_handler_t::replace, json::error_handler_t::ignore
|
||||
}
|
||||
};
|
||||
|
||||
SECTION("corpus")
|
||||
{
|
||||
for (const auto& value : utils::round_trip_corpus::values())
|
||||
{
|
||||
const json document = value.is_object() ? value : json{{"value", value}};
|
||||
for (const auto handler : handlers)
|
||||
{
|
||||
check_same(document, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nesting and arrays of every length class")
|
||||
{
|
||||
json document = json::object();
|
||||
document["empty object"] = json::object();
|
||||
document["empty array"] = json::array();
|
||||
for (const std::size_t n :
|
||||
{
|
||||
1u, 9u, 10u, 11u, 99u, 100u, 101u, 1000u
|
||||
})
|
||||
{
|
||||
json numbers = json::array();
|
||||
json mixed = json::array();
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
numbers.push_back(i % 3 == 0 ? json(static_cast<double>(i) / 3) : json(static_cast<std::int64_t>(i) * 1000003));
|
||||
switch (i % 4)
|
||||
{
|
||||
case 0:
|
||||
mixed.push_back(json("s" + std::to_string(i)));
|
||||
break;
|
||||
case 1:
|
||||
mixed.push_back(json::array({i, json::object({{"k", i}})}));
|
||||
break;
|
||||
case 2:
|
||||
mixed.push_back(json(nullptr));
|
||||
break;
|
||||
default:
|
||||
mixed.push_back(json(i % 2 == 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
document["numbers " + std::to_string(n)] = numbers;
|
||||
document["mixed " + std::to_string(n)] = mixed;
|
||||
document["nested " + std::to_string(n)] = json::object({{"a", numbers}, {"b", json::array({mixed, json::object()})}});
|
||||
}
|
||||
document["binary"] = json::binary({1, 2, 3}, 0x80);
|
||||
document["binary old"] = json::binary({1, 2, 3}, 0x02);
|
||||
for (const auto handler : handlers)
|
||||
{
|
||||
check_same(document, handler);
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// every entry here throws, which aborts without exceptions
|
||||
SECTION("errors at every position")
|
||||
{
|
||||
// an entry that cannot be written, at each place among valid ones
|
||||
const std::string bad_utf8 = "\xC3\x28";
|
||||
const std::array<json, 7> bad_entries =
|
||||
{
|
||||
{
|
||||
json::object({{std::string("a\0b", 3), 1}}),
|
||||
json::object({{"s", bad_utf8}}),
|
||||
json::object({{bad_utf8, 1}}),
|
||||
json::array({1, 2, bad_utf8}),
|
||||
json::object({{"b", json::binary({1}, 0x100)}}),
|
||||
json::object({{"d", json(json::value_t::discarded)}}),
|
||||
json::array({json(json::value_t::discarded)}),
|
||||
}
|
||||
};
|
||||
for (const auto& bad : bad_entries)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < 4; ++pos)
|
||||
{
|
||||
json document = json::object();
|
||||
json list = json::array();
|
||||
for (std::size_t i = 0; i < 4; ++i)
|
||||
{
|
||||
if (i == pos)
|
||||
{
|
||||
document["x" + std::to_string(i)] = bad;
|
||||
list.push_back(bad);
|
||||
}
|
||||
document["n" + std::to_string(i)] = json::array({i, "str", json::object({{"k", i}})});
|
||||
list.push_back(i);
|
||||
}
|
||||
document["list"] = list;
|
||||
for (const auto handler : handlers)
|
||||
{
|
||||
check_same(document, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -29,7 +29,9 @@ using nlohmann::json;
|
||||
#include "round_trip_corpus.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "sax_countdown.hpp"
|
||||
#include "sax_event_loggers.hpp"
|
||||
using utils::SaxCountdown;
|
||||
using utils::SaxEventLogger;
|
||||
|
||||
|
||||
TEST_CASE("CBOR")
|
||||
@@ -1807,6 +1809,30 @@ TEST_CASE("CBOR")
|
||||
CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("tag in front of a key that is cut off by the end of the input")
|
||||
{
|
||||
// 0xD8: the tag number is in the next byte, which is missing
|
||||
const std::vector<std::uint8_t> input = {0xA1, 0xD8};
|
||||
json _;
|
||||
for (const auto handler :
|
||||
{
|
||||
json::cbor_tag_handler_t::ignore, json::cbor_tag_handler_t::store
|
||||
})
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input, true, true, handler), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_cbor(input, true, false, handler).is_discarded());
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("chunk of an indefinite-length string that is cut off by the end of the input")
|
||||
{
|
||||
// the second byte announces a chunk of three bytes, but two follow
|
||||
const std::vector<std::uint8_t> input = {0x7F, 0x63, 'a', 'b'};
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_cbor(input, true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("tag in front of a key")
|
||||
{
|
||||
const auto ignore = json::cbor_tag_handler_t::ignore;
|
||||
@@ -2446,6 +2472,106 @@ TEST_CASE("CBOR SAX parsing stops at every event")
|
||||
CHECK(count_events(std::vector<std::uint8_t>({0xBF, 0x61, 'a', 0x9F, 0x01, 0xFF, 0xFF})) == 6);
|
||||
}
|
||||
|
||||
TEST_CASE("CBOR arrays of scalars from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input reads the integers, floats, booleans, and nulls an
|
||||
// array begins with in one loop, other input value by value; both must
|
||||
// give the same events, value, or error for the complete document and for
|
||||
// every truncation of it, and stop at every event a SAX handler rejects
|
||||
const std::vector<std::uint8_t> cbor =
|
||||
{
|
||||
0xA3, // map(3)
|
||||
0x61, 'a', // "a"
|
||||
0x98, 0x16, // array(22)
|
||||
0x00, 0x17, 0x18, 0xFF, 0x19, 0x01, 0x00, // 0, 23, 255, 256
|
||||
0x1A, 0x00, 0x01, 0x00, 0x00, // 65536
|
||||
0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 18446744073709551615
|
||||
0x20, 0x37, 0x38, 0xFF, 0x39, 0x01, 0x00, // -1, -24, -256, -257
|
||||
0x3A, 0x00, 0x01, 0x00, 0x00, // -65537
|
||||
0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -18446744073709551616 (a float)
|
||||
0x3B, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -9223372036854775808
|
||||
0xF4, 0xF5, 0xF6, // false, true, null
|
||||
0xFA, 0x3F, 0xC0, 0x00, 0x00, // 1.5 (float32)
|
||||
0xFB, 0xBF, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, // -0.1
|
||||
0xF9, 0x3C, 0x00, // 1.0 (float16, ends the loop)
|
||||
0x01, 0x61, 'a', // 1, "a"
|
||||
0xFB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2.5
|
||||
0x61, 'b', // "b"
|
||||
0x83, // array(3)
|
||||
0x82, 0xFB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
|
||||
0xFB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
|
||||
0x82, 0xFB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
|
||||
0xFB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
|
||||
0x80, // []
|
||||
0x61, 'c', // "c"
|
||||
0x84, 0xA0, 0x01, 0xC1, 0x02, 0x81, 0x03 // [{}, 1, tag(1) 2, [3]]
|
||||
};
|
||||
const auto tags = json::cbor_tag_handler_t::ignore;
|
||||
|
||||
const json j = json::from_cbor(cbor, true, true, tags);
|
||||
CHECK(j["a"].size() == 22);
|
||||
CHECK(j["a"][5] == 18446744073709551615u);
|
||||
CHECK(j["a"][11].is_number_float());
|
||||
CHECK(j["a"][11] == -18446744073709551616.0);
|
||||
CHECK(j["a"][12] == (std::numeric_limits<std::int64_t>::min)());
|
||||
CHECK(j["a"][17] == -0.1);
|
||||
CHECK(j["b"] == json::parse("[[1.25, -3.75], [1.25, -3.75], []]"));
|
||||
CHECK(j["c"] == json::parse("[{}, 1, 2, [3]]"));
|
||||
|
||||
for (std::size_t length = 0; length <= cbor.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(cbor.begin(), cbor.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_cbor(contiguous, true, false, tags).dump() == json::from_cbor(bytewise.begin(), bytewise.end(), true, false, tags).dump());
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::cbor, true, false, false, tags)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false, false, tags));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
|
||||
for (int events = 0; events <= 60; ++events)
|
||||
{
|
||||
CAPTURE(events)
|
||||
const std::list<std::uint8_t> bytewise(cbor.begin(), cbor.end());
|
||||
SaxCountdown from_contiguous(events);
|
||||
SaxCountdown from_bytewise(events);
|
||||
CHECK(json::sax_parse(cbor, &from_contiguous, json::input_format_t::cbor, true, false, false, tags)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false, false, tags));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("CBOR arrays of one element from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input leaves an array of one element to the general path
|
||||
// rather than to the loop that reads arrays of scalars; both inputs must
|
||||
// still give the same events, value, or error for the complete document
|
||||
// and for every truncation of it, with enough bytes after each array that
|
||||
// the loop could have been entered
|
||||
const json j = json::parse(R"({"a": [[[[1.5]]]], "b": [7], "c": [{"x": 2.5}], "d": ["s"], "e": [[1, 2]], "f": [null], "g": 1.25})");
|
||||
const std::vector<std::uint8_t> bytes = json::to_cbor(j);
|
||||
|
||||
CHECK(json::from_cbor(bytes) == j);
|
||||
|
||||
for (std::size_t length = 0; length <= bytes.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_cbor(contiguous, true, false).dump() == json::from_cbor(bytewise.begin(), bytewise.end(), true, false).dump());
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::cbor, true, false)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("CBOR indefinite-length strings do not recurse per chunk")
|
||||
{
|
||||
// Reading an indefinite-length string or byte array used to call itself
|
||||
|
||||
@@ -2092,6 +2092,12 @@ TEST_CASE("UTF-8 validators accept the same sequences")
|
||||
}
|
||||
};
|
||||
|
||||
// an ASCII byte is a sequence of its own
|
||||
for (unsigned lead = 0; lead < 0x80; ++lead)
|
||||
{
|
||||
CHECK(nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead)).length == 1);
|
||||
}
|
||||
|
||||
for (unsigned lead = 0x80; lead <= 0xFF; ++lead)
|
||||
{
|
||||
const auto shape = nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead));
|
||||
@@ -2157,3 +2163,73 @@ TEST_CASE("UTF-8 validators accept the same sequences")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("float_bigint")
|
||||
{
|
||||
// the big integer of the exact comparison of a float token with the
|
||||
// midpoint between two floats (digit_comparison())
|
||||
using nlohmann::detail::float_bigint;
|
||||
|
||||
SECTION("zero")
|
||||
{
|
||||
// shifting zero keeps it zero, whatever the distance
|
||||
float_bigint zero(0);
|
||||
zero.shift_left(0);
|
||||
zero.shift_left(5);
|
||||
zero.shift_left(64);
|
||||
CHECK(zero.compare(float_bigint(0)) == 0);
|
||||
CHECK(zero.compare(float_bigint(1)) == -1);
|
||||
CHECK(float_bigint(1).compare(zero) == 1);
|
||||
}
|
||||
|
||||
SECTION("compare numbers of different lengths")
|
||||
{
|
||||
// the number with more 32-bit limbs is the greater one
|
||||
const float_bigint small(0xFFFFFFFFu);
|
||||
const float_bigint large(std::uint64_t{1} << 32u);
|
||||
CHECK(small.compare(large) == -1);
|
||||
CHECK(large.compare(small) == 1);
|
||||
CHECK(large.compare(large) == 0);
|
||||
|
||||
// a shift can add a limb
|
||||
float_bigint shifted(1);
|
||||
shifted.shift_left(32);
|
||||
CHECK(shifted.compare(large) == 0);
|
||||
shifted.shift_left(1);
|
||||
CHECK(shifted.compare(large) == 1);
|
||||
CHECK(large.compare(shifted) == -1);
|
||||
}
|
||||
|
||||
SECTION("compare numbers of the same length")
|
||||
{
|
||||
CHECK(float_bigint(2).compare(float_bigint(3)) == -1);
|
||||
CHECK(float_bigint(3).compare(float_bigint(2)) == 1);
|
||||
CHECK(float_bigint(3).compare(float_bigint(3)) == 0);
|
||||
CHECK(float_bigint((std::uint64_t{3} << 32u) | 1u).compare(float_bigint((std::uint64_t{3} << 32u) | 2u)) == -1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("serialization of short strings")
|
||||
{
|
||||
// the serializer takes strings of up to 32 characters that need no
|
||||
// escaping by a table lookup of its own where the scan is not vectorized
|
||||
// (this file is built that way, too, with JSON_NO_SIMD)
|
||||
for (const std::string& s :
|
||||
{
|
||||
std::string(), std::string("a"), std::string("hello"), std::string("0123456789abcdef"), std::string("0123456789abcdef0123456789abcdef"),
|
||||
std::string("0123456789abcdef0123456789abcdef0")
|
||||
})
|
||||
{
|
||||
CAPTURE(s)
|
||||
const json j = s;
|
||||
const std::string quoted = "\"" + s + "\""; // NOLINT(performance-inefficient-string-concatenation)
|
||||
CHECK(j.dump() == quoted);
|
||||
CHECK(j.dump(-1, ' ', true) == quoted);
|
||||
CHECK(json({{s, s}}).dump() == "{" + quoted + ":" + quoted + "}"); // NOLINT(performance-inefficient-string-concatenation)
|
||||
}
|
||||
|
||||
// a character that needs an escape
|
||||
CHECK(json("a\"b").dump() == "\"a\\\"b\"");
|
||||
CHECK(json("caf\xC3\xA9").dump() == "\"caf\xC3\xA9\"");
|
||||
CHECK(json("caf\xC3\xA9").dump(-1, ' ', true) == "\"caf\\u00e9\"");
|
||||
}
|
||||
@@ -2982,6 +2982,67 @@ TEST_CASE("accept() agrees with parse() on floats around the overflow limit")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("accept() agrees with parse() on strings, which it checks without copying them")
|
||||
{
|
||||
// accept() scans strings and object keys with a separate instantiation of
|
||||
// the string scanner that does not copy them into the token buffer; it
|
||||
// must accept and reject exactly what parse() does
|
||||
const std::vector<std::string> pieces =
|
||||
{
|
||||
"", "abc", R"(\"\\\/\b\f\n\r\t)", R"(\u0041\u00e4\u20AC)", "\\ud83d\\ude00", "\\uD83D\\uDE00",
|
||||
"\\ud800", "\\udc00", "\\ud800\\u0041", "\\ud800x", "\\udbff\\udfff", "\\u12", "\\u12g4", "\\x", "\\",
|
||||
"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf", "\xc0\xaf", "\xed\xa0\x80", "\xf4\x90\x80\x80",
|
||||
"\xff", "\xe2\x82", "\xc3", "\x01", "\x1f", "\x7f", "\n", std::string(1, '\0')
|
||||
};
|
||||
|
||||
for (std::size_t run = 0; run <= 70; ++run)
|
||||
{
|
||||
for (const std::string& piece : pieces)
|
||||
{
|
||||
// a run of ordinary characters puts the piece at every offset of the bulk scanners
|
||||
const std::string str = std::string(run, 'a') + piece;
|
||||
std::string opened = "\"";
|
||||
opened += str;
|
||||
std::string closed = opened;
|
||||
closed += '"';
|
||||
std::vector<std::string> texts;
|
||||
texts.push_back(closed);
|
||||
texts.emplace_back("[");
|
||||
texts.back() += closed;
|
||||
texts.back() += ']';
|
||||
texts.emplace_back("{");
|
||||
texts.back() += closed;
|
||||
texts.back() += ":1}";
|
||||
texts.emplace_back("{\"k\":");
|
||||
texts.back() += closed;
|
||||
texts.back() += ',';
|
||||
texts.back() += closed;
|
||||
texts.back() += ":[";
|
||||
texts.back() += closed;
|
||||
texts.back() += "]}";
|
||||
texts.emplace_back("[");
|
||||
texts.back() += opened;
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
for (int options = 0; options < 4; ++options)
|
||||
{
|
||||
const bool ignore_comments = (options & 1) != 0;
|
||||
const bool ignore_trailing_commas = (options & 2) != 0;
|
||||
const bool parses = !json::parse(text, nullptr, false, ignore_comments, ignore_trailing_commas).is_discarded();
|
||||
CHECK(json::accept(text, ignore_comments, ignore_trailing_commas) == parses);
|
||||
|
||||
std::istringstream stream(text);
|
||||
CHECK(json::accept(stream, ignore_comments, ignore_trailing_commas) == parses);
|
||||
|
||||
const std::list<char> chars(text.begin(), text.end());
|
||||
CHECK(json::accept(chars.begin(), chars.end(), ignore_comments, ignore_trailing_commas) == parses);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// a DOM parser that reports parse errors without throwing, as in the sax_no_exception example
|
||||
|
||||
@@ -1260,6 +1260,44 @@ TEST_CASE("value conversion")
|
||||
#undef JSON_HAS_CPP_17
|
||||
#endif
|
||||
|
||||
TEST_CASE("get<T>() of a boolean (#5988)")
|
||||
{
|
||||
// a boolean converts to 0/1 for every arithmetic type except number_integer_t, number_unsigned_t and
|
||||
// number_float_t, whose dedicated from_json() overloads throw
|
||||
const json t = true;
|
||||
const json f = false;
|
||||
|
||||
SECTION("0 and 1")
|
||||
{
|
||||
// long and unsigned long are left out: they are std::int64_t and std::uint64_t on some platforms
|
||||
CHECK(t.get<bool>() == true);
|
||||
CHECK(f.get<bool>() == false);
|
||||
CHECK(t.get<char>() == 1);
|
||||
CHECK(t.get<signed char>() == 1);
|
||||
CHECK(t.get<unsigned char>() == 1);
|
||||
CHECK(t.get<short>() == 1); // NOLINT(google-runtime-int)
|
||||
CHECK(t.get<unsigned short>() == 1); // NOLINT(google-runtime-int)
|
||||
CHECK(t.get<int>() == 1);
|
||||
CHECK(t.get<unsigned>() == 1);
|
||||
CHECK(t.get<float>() == 1.0F);
|
||||
CHECK(t.get<long double>() == 1.0L);
|
||||
CHECK(f.get<int>() == 0);
|
||||
CHECK(f.get<float>() == 0.0F);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("number_integer_t, number_unsigned_t and number_float_t throw")
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(t.get<json::number_integer_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(t.get<json::number_unsigned_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(t.get<json::number_float_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(f.get<json::number_integer_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(f.get<json::number_unsigned_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(f.get<json::number_float_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_14
|
||||
#undef JSON_HAS_CPP_14
|
||||
#endif
|
||||
|
||||
@@ -210,3 +210,23 @@ TEST_CASE("regression test #5476 - array type without reserve()")
|
||||
CHECK(json::from_cbor(truncated, true, false).is_discarded());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("array type with std::deque nested beyond the recursion limit")
|
||||
{
|
||||
// the binary writers finish a value nested deeper than
|
||||
// recursion_depth_limit() with an explicit stack of frames; with the
|
||||
// iterators of std::deque, these frames are copied and moved through
|
||||
// their own special members
|
||||
deque_json j = 1;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
deque_json outer = deque_json::array();
|
||||
outer.push_back(std::move(j));
|
||||
j = std::move(outer);
|
||||
}
|
||||
|
||||
// 200 arrays of one element each (0x81) around the integer 1
|
||||
std::vector<std::uint8_t> expected(200, 0x81);
|
||||
expected.push_back(0x01);
|
||||
CHECK(deque_json::to_cbor(j) == expected);
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -495,3 +496,92 @@ TEST_CASE("Regression test for issue #5674 - copy construction must not require
|
||||
CHECK(*c == 1);
|
||||
}
|
||||
}
|
||||
|
||||
// counts the constructions and destructions of the base class (#5995)
|
||||
struct counting_base
|
||||
{
|
||||
static std::size_t constructions;
|
||||
static std::size_t destructions;
|
||||
|
||||
counting_base() noexcept
|
||||
{
|
||||
++constructions;
|
||||
}
|
||||
counting_base(const counting_base& /*unused*/) noexcept
|
||||
{
|
||||
++constructions;
|
||||
}
|
||||
counting_base(counting_base&& /*unused*/) noexcept
|
||||
{
|
||||
++constructions;
|
||||
}
|
||||
counting_base& operator=(const counting_base& /*unused*/) noexcept = default;
|
||||
counting_base& operator=(counting_base&& /*unused*/) noexcept = default;
|
||||
~counting_base()
|
||||
{
|
||||
++destructions;
|
||||
}
|
||||
|
||||
static void reset() noexcept
|
||||
{
|
||||
constructions = 0;
|
||||
destructions = 0;
|
||||
}
|
||||
};
|
||||
|
||||
std::size_t counting_base::constructions = 0;
|
||||
std::size_t counting_base::destructions = 0;
|
||||
|
||||
using json_with_counting_base = nlohmann::json::with_base_class_t<counting_base>;
|
||||
|
||||
TEST_CASE("destroying a value does not construct the custom base class")
|
||||
{
|
||||
// regression test for #5995: destroying an array or object used a
|
||||
// default-constructed basic_json as a stand-in, which constructed a base
|
||||
// class that belonged to no value (and could allocate in the noexcept
|
||||
// destructor); now every destroyed value destroys exactly its own base
|
||||
// object, and none is constructed
|
||||
const auto check_destroy = [](std::unique_ptr<json_with_counting_base> j, std::size_t values)
|
||||
{
|
||||
counting_base::reset();
|
||||
j = nullptr;
|
||||
CHECK(counting_base::constructions == 0);
|
||||
CHECK(counting_base::destructions == values);
|
||||
};
|
||||
|
||||
SECTION("scalar")
|
||||
{
|
||||
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(42)), 1);
|
||||
}
|
||||
|
||||
SECTION("array and object")
|
||||
{
|
||||
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(json_with_counting_base::parse(R"({"a":[1,2,3],"b":{"c":true}})"))), 7);
|
||||
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(json_with_counting_base::array())), 1);
|
||||
}
|
||||
|
||||
SECTION("nested deeper than the recursion depth limit")
|
||||
{
|
||||
// alternating arrays and objects, so both kinds of container are
|
||||
// walked by destroy_walk()
|
||||
constexpr std::size_t depth = 1000;
|
||||
std::unique_ptr<json_with_counting_base> j(new json_with_counting_base(json_with_counting_base::array({1, 2})));
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
json_with_counting_base outer = (i % 2 == 0) ? json_with_counting_base::object() : json_with_counting_base::array();
|
||||
if (outer.is_object())
|
||||
{
|
||||
outer["x"] = std::move(*j);
|
||||
outer["y"] = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
outer.push_back(std::move(*j));
|
||||
outer.push_back(true);
|
||||
}
|
||||
*j = std::move(outer);
|
||||
}
|
||||
// the innermost array with its two elements, and two values per level
|
||||
check_destroy(std::move(j), 3 + (2 * depth));
|
||||
}
|
||||
}
|
||||
@@ -49,3 +49,24 @@ TEST_CASE("custom object key types: MessagePack (json_full)")
|
||||
{
|
||||
custom_key_test::test_msgpack<json_full>();
|
||||
}
|
||||
|
||||
TEST_CASE("custom object key types: patch (json_full)")
|
||||
{
|
||||
// the members of a patch are found by lookup, as their keys are not std::string
|
||||
const json_full j = json_full::parse(R"({"a":1,"b":[1,2],"c":{"d":2}})");
|
||||
const json_full patch = json_full::parse(R"([
|
||||
{"op": "add", "path": "/e", "value": 5},
|
||||
{"op": "remove", "path": "/a"},
|
||||
{"op": "replace", "path": "/c/d", "value": 3},
|
||||
{"op": "test", "path": "/c/d", "value": 3},
|
||||
{"op": "move", "from": "/e", "path": "/f"},
|
||||
{"op": "copy", "from": "/f", "path": "/g"},
|
||||
{"op": "add", "path": "/b/-", "value": 7}
|
||||
])");
|
||||
|
||||
CHECK(j.patch(patch).dump() == R"({"b":[1,2,7],"c":{"d":3},"f":5,"g":5})");
|
||||
|
||||
// a patch with a missing member is rejected
|
||||
CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "add", "path": "/e"}])")), json_full::parse_error&);
|
||||
CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "test", "path": "/a", "value": 2}])")), json_full::other_error&);
|
||||
}
|
||||
@@ -337,6 +337,52 @@ TEST_CASE("custom number types: integers beyond 64 bits")
|
||||
CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64));
|
||||
}
|
||||
|
||||
SECTION("MessagePack integers of every width")
|
||||
{
|
||||
// an integer of a number type wider than 64 bits is written with the
|
||||
// smallest MessagePack integer that holds it, as for 64-bit types
|
||||
const wide_json widths = wide_json::array(
|
||||
{
|
||||
make_int(-33), make_int(-100), make_int(-128), make_int(-129), make_int(-32768), make_int(-32769), make_int(-2147483648LL), make_int(-2147483649LL),
|
||||
make_int(128), make_int(255), make_int(256), make_int(300), make_int(65535), make_int(65536), make_int(70000), make_int(4294967295LL), make_int(4294967296LL),
|
||||
make_uint(128), make_uint(255), make_uint(256), make_uint(300), make_uint(65535), make_uint(65536), make_uint(70000), make_uint(4294967295ULL), make_uint(4294967296ULL)
|
||||
});
|
||||
const nlohmann::json widths64 = nlohmann::json::array(
|
||||
{
|
||||
-33, -100, -128, -129, -32768, -32769, -2147483648LL, -2147483649LL,
|
||||
128, 255, 256, 300, 65535, 65536, 70000, 4294967295LL, 4294967296LL,
|
||||
128u, 255u, 256u, 300u, 65535u, 65536u, 70000u, 4294967295ULL, 4294967296ULL
|
||||
});
|
||||
CHECK(wide_json::to_msgpack(widths) == nlohmann::json::to_msgpack(widths64));
|
||||
|
||||
// as single values, too
|
||||
CHECK(wide_json::to_msgpack(make_int(-100)) == std::vector<std::uint8_t>({0xD0, 0x9C}));
|
||||
CHECK(wide_json::to_msgpack(make_int(300)) == std::vector<std::uint8_t>({0xCD, 0x01, 0x2C}));
|
||||
CHECK(wide_json::to_msgpack(make_int(70000)) == std::vector<std::uint8_t>({0xCE, 0x00, 0x01, 0x11, 0x70}));
|
||||
CHECK(wide_json::to_msgpack(make_uint(300)) == std::vector<std::uint8_t>({0xCD, 0x01, 0x2C}));
|
||||
CHECK(wide_json::to_msgpack(make_uint(70000)) == std::vector<std::uint8_t>({0xCE, 0x00, 0x01, 0x11, 0x70}));
|
||||
}
|
||||
|
||||
SECTION("dump")
|
||||
{
|
||||
// beyond 64 bits, the digits are counted before they are written
|
||||
CHECK(make_int(0).dump() == "0");
|
||||
CHECK(make_int(two_64).dump() == "18446744073709551616");
|
||||
CHECK(make_int(-two_64).dump() == "-18446744073709551616");
|
||||
CHECK(make_int(two_100).dump() == "1267650600228229401496703205376");
|
||||
CHECK(make_int(-two_100).dump() == "-1267650600228229401496703205376");
|
||||
// powers of ten, where the count of digits ends on a single digit or two
|
||||
const int128 ten_20 = static_cast<int128>(100000000000ULL) * static_cast<int128>(1000000000ULL);
|
||||
CHECK(make_int(ten_20).dump() == "100000000000000000000");
|
||||
CHECK(make_int(-ten_20).dump() == "-100000000000000000000");
|
||||
CHECK(make_int(ten_20 * 10).dump() == "1000000000000000000000");
|
||||
CHECK(make_int(ten_20 * 100).dump() == "10000000000000000000000");
|
||||
CHECK(make_int(ten_20 * 1000).dump() == "100000000000000000000000");
|
||||
CHECK(make_uint(static_cast<uint128>(ten_20) * 10u).dump() == "1000000000000000000000");
|
||||
CHECK(make_uint((std::numeric_limits<uint128>::max)()).dump() == "340282366920938463463374607431768211455");
|
||||
CHECK(make_int((std::numeric_limits<int128>::min)()).dump() == "-170141183460469231731687303715884105728");
|
||||
}
|
||||
|
||||
SECTION("BJData ND-array")
|
||||
{
|
||||
// an ND-array element beyond 64 bits does not fit any dtype, so the
|
||||
@@ -417,3 +463,23 @@ TEST_CASE("custom number types")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("custom number types: 32-bit integers in the binary formats")
|
||||
{
|
||||
// integers narrower than 64 bits are widened (a signed one with its sign)
|
||||
// before the CBOR, MessagePack, and UBJSON writers pick their encoding
|
||||
using json32 = nlohmann::json::with_integers_t<std::int32_t, std::uint32_t>;
|
||||
json32 values = json32::array();
|
||||
values.push_back(std::uint32_t{0});
|
||||
values.push_back(std::uint32_t{200});
|
||||
values.push_back(std::uint32_t{4294967295u});
|
||||
values.push_back(std::int32_t{-1});
|
||||
values.push_back(std::int32_t{-2147483647 - 1});
|
||||
REQUIRE(values[2].is_number_unsigned());
|
||||
REQUIRE(values[4].is_number_integer());
|
||||
|
||||
const nlohmann::json values64 = {0u, 200u, 4294967295u, -1, -2147483648LL};
|
||||
CHECK(json32::to_cbor(values) == nlohmann::json::to_cbor(values64));
|
||||
CHECK(json32::to_msgpack(values) == nlohmann::json::to_msgpack(values64));
|
||||
CHECK(json32::to_ubjson(values) == nlohmann::json::to_ubjson(values64));
|
||||
}
|
||||
@@ -11,8 +11,13 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "custom_object_key_type.hpp"
|
||||
|
||||
@@ -125,3 +130,163 @@ TEST_CASE("MessagePack supports custom object key types nested deeper than the r
|
||||
CHECK(encoded == nlohmann::json::to_msgpack(expected));
|
||||
CHECK(nlohmann::json::from_msgpack(encoded) == expected);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// A key type that cannot be constructed from a std::string, so that a value
|
||||
// with such keys cannot be converted from a nlohmann::json by the range
|
||||
// constructor of its object type: the serializer converts the object instead.
|
||||
class strict_key
|
||||
{
|
||||
public:
|
||||
strict_key(const char* value)
|
||||
: m_value(value)
|
||||
{}
|
||||
|
||||
strict_key(const std::string& /*value*/) = delete;
|
||||
|
||||
#if JSON_DIAGNOSTICS
|
||||
// Required by JSON_DIAGNOSTICS, which reads object keys through data()
|
||||
// when building the path of an exception.
|
||||
const char* data() const noexcept
|
||||
{
|
||||
return m_value.data();
|
||||
}
|
||||
#endif
|
||||
|
||||
friend bool operator<(const strict_key& lhs, const strict_key& rhs)
|
||||
{
|
||||
return lhs.m_value < rhs.m_value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_value;
|
||||
};
|
||||
|
||||
strict_key make_strict_key(const std::string& key)
|
||||
{
|
||||
return {key.c_str()};
|
||||
}
|
||||
|
||||
template<class Key, class Value, class Compare, class Allocator>
|
||||
class strict_object
|
||||
: public std::map <
|
||||
strict_key,
|
||||
Value,
|
||||
std::less<strict_key>, // NOLINT(modernize-use-transparent-functors)
|
||||
typename std::allocator_traits<Allocator>::template rebind_alloc <
|
||||
std::pair<const strict_key, Value >>>
|
||||
{
|
||||
public:
|
||||
strict_object() = default;
|
||||
};
|
||||
|
||||
template<typename T, typename = void>
|
||||
struct strict_serializer : nlohmann::adl_serializer<T> {};
|
||||
|
||||
using strict_json = nlohmann::basic_json < strict_object, std::vector, std::string, bool, std::int64_t,
|
||||
std::uint64_t, double, std::allocator, strict_serializer, std::vector<std::uint8_t >>;
|
||||
|
||||
// the members of an object are converted one by one, their keys by make_strict_key()
|
||||
template<>
|
||||
struct strict_serializer<nlohmann::json::object_t, void>
|
||||
{
|
||||
static void to_json(strict_json& j, const nlohmann::json::object_t& object)
|
||||
{
|
||||
strict_json::object_t converted;
|
||||
for (const auto& member : object)
|
||||
{
|
||||
converted.emplace(make_strict_key(member.first), strict_json(member.second));
|
||||
}
|
||||
j = strict_json(std::move(converted));
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("converting a value with keys that cannot be made from a std::string nested deeper than the recursion depth limit")
|
||||
{
|
||||
static_assert(!std::is_constructible<strict_key, const std::string&>::value, "the keys cannot be made from a std::string");
|
||||
|
||||
// below detail::recursion_depth_limit(), objects and arrays are converted
|
||||
// by the serializers instead of the range constructors
|
||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10;
|
||||
|
||||
nlohmann::json value = 1;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
if (i % 2 == 0)
|
||||
{
|
||||
value = nlohmann::json::object({{"a", std::move(value)}, {"b", true}});
|
||||
}
|
||||
else
|
||||
{
|
||||
value = nlohmann::json::array({std::move(value), "sibling"});
|
||||
}
|
||||
}
|
||||
|
||||
// direct-initialization: a copy-initialization makes GCC 4.8 instantiate the
|
||||
// copy constructor, which needs a range constructor strict_object lacks
|
||||
const strict_json converted(value);
|
||||
|
||||
// at() needs to make a std::string of a key for its error message
|
||||
const auto member = [](const strict_json & object, const char* name) -> const strict_json &
|
||||
{
|
||||
const auto it = object.find(strict_key(name));
|
||||
REQUIRE(it != object.end());
|
||||
return *it;
|
||||
};
|
||||
|
||||
// walk down the nesting levels without recursion
|
||||
const strict_json* current = &converted;
|
||||
for (std::size_t i = depth; i > 0; --i)
|
||||
{
|
||||
if (i % 2 == 1)
|
||||
{
|
||||
REQUIRE(current->is_object());
|
||||
CHECK(current->size() == 2);
|
||||
CHECK(member(*current, "b").get<bool>());
|
||||
current = &member(*current, "a");
|
||||
}
|
||||
else
|
||||
{
|
||||
REQUIRE(current->is_array());
|
||||
CHECK(current->size() == 2);
|
||||
CHECK(current->at(1).get<std::string>() == "sibling");
|
||||
current = ¤t->at(0);
|
||||
}
|
||||
}
|
||||
CHECK(current->get<int>() == 1);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// shorter keys come first
|
||||
struct shortlex_less
|
||||
{
|
||||
bool operator()(const std::string& lhs, const std::string& rhs) const
|
||||
{
|
||||
return lhs.size() != rhs.size() ? lhs.size() < rhs.size() : lhs < rhs;
|
||||
}
|
||||
};
|
||||
|
||||
template<class Key, class Value, class Compare, class Allocator>
|
||||
using shortlex_map = std::map<Key, Value, shortlex_less, Allocator>;
|
||||
|
||||
using shortlex_json = nlohmann::json::with_object_t<shortlex_map>;
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("unflatten with an object type that does not sort the keys alphabetically")
|
||||
{
|
||||
// the keys of /a come before and after one of /b: unlike in an object
|
||||
// sorted alphabetically, the values below a prefix are not found one
|
||||
// after the other, so the value of /a must be looked up again
|
||||
shortlex_json flat = shortlex_json::object();
|
||||
flat["/a/p"] = 1;
|
||||
flat["/b/qq"] = 2;
|
||||
flat["/a/pqr"] = 3;
|
||||
REQUIRE(flat.begin().key() == "/a/p");
|
||||
REQUIRE((++flat.begin()).key() == "/b/qq");
|
||||
|
||||
CHECK(flat.unflatten().dump() == R"({"a":{"p":1,"pqr":3},"b":{"qq":2}})");
|
||||
CHECK(flat.unflatten().flatten() == flat);
|
||||
}
|
||||
@@ -1195,6 +1195,28 @@ TEST_CASE("deserialization")
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
|
||||
TEST_CASE("deserialization from a pointer and a length (deprecated)")
|
||||
{
|
||||
// the unit test build disables -Wdeprecated-declarations (see
|
||||
// cmake/clang_flags.cmake)
|
||||
const std::string text = R"({"foo": [1, 2, 3]})";
|
||||
const std::string invalid = R"({"foo": )";
|
||||
|
||||
SECTION("parse")
|
||||
{
|
||||
const json j = json::parse({text.data(), text.size()});
|
||||
CHECK(j == json({{"foo", {1, 2, 3}}}));
|
||||
}
|
||||
|
||||
SECTION("accept")
|
||||
{
|
||||
CHECK(json::accept({text.data(), text.size()}));
|
||||
CHECK_FALSE(json::accept({invalid.data(), invalid.size()}));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// select the types to test - char8_t is only available since C++20 if and only
|
||||
// if __cpp_char8_t is defined.
|
||||
#define TYPE_LIST(...) __VA_ARGS__
|
||||
|
||||
@@ -193,6 +193,28 @@ TEST_CASE("Better diagnostics with positions")
|
||||
check_conversion(300);
|
||||
}
|
||||
|
||||
SECTION("exception about the root value (#5987)")
|
||||
{
|
||||
// the root has no path, but still has a byte range
|
||||
const json root = json::parse("true");
|
||||
const json nested = json::parse("[1, true]");
|
||||
const json constructed = true;
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK_THROWS_WITH_AS(root.get<std::string>(),
|
||||
"[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean", json::type_error);
|
||||
CHECK_THROWS_WITH_AS(nested[1].get<std::string>(),
|
||||
"[json.exception.type_error.302] (/1) (bytes 4-8) type must be string, but is boolean", json::type_error);
|
||||
#else
|
||||
CHECK_THROWS_WITH_AS(root.get<std::string>(),
|
||||
"[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean", json::type_error);
|
||||
CHECK_THROWS_WITH_AS(nested[1].get<std::string>(),
|
||||
"[json.exception.type_error.302] (bytes 4-8) type must be string, but is boolean", json::type_error);
|
||||
#endif
|
||||
// a value without positions gets no range and no stray whitespace
|
||||
CHECK_THROWS_WITH_AS(constructed.get<std::string>(),
|
||||
"[json.exception.type_error.302] type must be string, but is boolean", json::type_error);
|
||||
}
|
||||
|
||||
SECTION("JSON patch add to primitive parent (#4292)")
|
||||
{
|
||||
// the JSON Patch "add" target /foo/bar/baz has a string parent
|
||||
@@ -243,3 +265,14 @@ TEST_CASE("values read from a binary format have no positions")
|
||||
check_no_positions(j);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("comparing a value-initialized iterator throws invalid_iterator.212")
|
||||
{
|
||||
// such an iterator has no value to take the byte positions from; the
|
||||
// comparison used to dereference the null pointer instead of throwing
|
||||
json j = {1, 2};
|
||||
const json::iterator it{};
|
||||
CHECK_THROWS_WITH_AS(it == j.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
|
||||
CHECK_THROWS_WITH_AS(it < j.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
|
||||
CHECK_THROWS_WITH_AS(j.begin() == it, "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
|
||||
}
|
||||
@@ -868,3 +868,23 @@ TEST_CASE("Parent pointers stay consistent for object types not known to keep th
|
||||
obj["1"].push_back(42);
|
||||
CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&);
|
||||
}
|
||||
|
||||
TEST_CASE("patch_inplace() with a patch that is stored inside the patched value")
|
||||
{
|
||||
// the patch is modified-while-iterated if it is not copied first: it is a
|
||||
// descendant of the value it is applied to, which only the parent pointers
|
||||
// (JSON_DIAGNOSTICS) reveal
|
||||
json j =
|
||||
{
|
||||
{"patch", {{{"op", "add"}, {"path", "/x"}, {"value", 1}}, {{"op", "remove"}, {"path", "/patch/0"}}}},
|
||||
{"y", 2}
|
||||
};
|
||||
|
||||
j.patch_inplace(j["patch"]);
|
||||
|
||||
CHECK(j.size() == 3);
|
||||
CHECK(j["x"] == 1);
|
||||
CHECK(j["y"] == 2);
|
||||
CHECK(j["patch"].size() == 1);
|
||||
CHECK(j["patch"][0]["op"] == "remove");
|
||||
}
|
||||
@@ -49,6 +49,14 @@ file_ptr open_with(const char* name, const std::string& content)
|
||||
return {std::fopen(name, "rb"), &std::fclose};
|
||||
}
|
||||
|
||||
// the smallest size setvbuf() accepts for _IOFBF: the Microsoft C runtime
|
||||
// rejects sizes below 2 as an invalid parameter, which ends the process
|
||||
#if defined(_WIN32)
|
||||
constexpr int min_full_buffer = 2;
|
||||
#else
|
||||
constexpr int min_full_buffer = 1;
|
||||
#endif
|
||||
|
||||
// The stdio lock is recursive, so whether parsing released it can only be
|
||||
// seen from another thread: ftrylockfile() fails there while this thread
|
||||
// holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped.
|
||||
@@ -225,6 +233,131 @@ TEST_CASE("parsing from a FILE*")
|
||||
check_unlocked(f.get());
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("tokens across the edges of the file's buffer")
|
||||
{
|
||||
// the adapter reads the buffer of the FILE directly where it can (see
|
||||
// JSON_FILE_WINDOW): with buffers of 1 to 70 bytes and none, every
|
||||
// token crosses an edge somewhere; the results, the error messages
|
||||
// and their positions must be those of the string
|
||||
const std::vector<std::string> documents =
|
||||
{
|
||||
input,
|
||||
R"({"long": "a string that is longer than the buffer of the file, with \"escapes\" and \u00e4", "n": [123456789012345678901, -0.000123e-45, 1.5E+300, 0, -0]})",
|
||||
"[\n \"indented\",\n \"caf\xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80\",\n {\n \"key\": 1\n }\n]",
|
||||
R"({"a": [1, 2, 3], "b": "unterminated)",
|
||||
R"([1, 2, 3, 1e999])",
|
||||
R"({"a": "invalid escape \x"})",
|
||||
"[\"invalid UTF-8 \xc3\x28\"]",
|
||||
"[1, 2, 3] trailing",
|
||||
R"([truex])",
|
||||
};
|
||||
for (const auto& document : documents)
|
||||
{
|
||||
std::string expected;
|
||||
try
|
||||
{
|
||||
expected = json::parse(document).dump();
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
expected = e.what();
|
||||
}
|
||||
const bool expected_accept = json::accept(document);
|
||||
for (int size = 0; size <= 70; ++size)
|
||||
{
|
||||
if (size != 0 && size < min_full_buffer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
CAPTURE(document)
|
||||
CAPTURE(size)
|
||||
std::vector<char> buffer(static_cast<std::size_t>(size) + 1);
|
||||
for (int pass = 0; pass < 2; ++pass)
|
||||
{
|
||||
const file_ptr f = open_with(name, document);
|
||||
REQUIRE(f != nullptr);
|
||||
// size 0: unbuffered
|
||||
REQUIRE(std::setvbuf(f.get(), size == 0 ? nullptr : buffer.data(), size == 0 ? _IONBF : _IOFBF, static_cast<std::size_t>(size)) == 0);
|
||||
if (pass == 0)
|
||||
{
|
||||
std::string actual;
|
||||
try
|
||||
{
|
||||
actual = json::parse(f.get()).dump();
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
actual = e.what();
|
||||
}
|
||||
CHECK(actual == expected);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(json::accept(f.get()) == expected_accept);
|
||||
}
|
||||
check_unlocked(f.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// the C standard guarantees only one pushed-back character (and the
|
||||
// Windows C runtime fails further calls to ungetc()), so this is limited
|
||||
// to the C libraries that keep them in a separate buffer
|
||||
#if !defined(JSON_NOEXCEPTION) && ((defined(__GLIBC__) && !defined(__UCLIBC__)) || defined(__APPLE__))
|
||||
SECTION("characters pushed back with ungetc() before parsing")
|
||||
{
|
||||
const file_ptr f = open_with(name, "2, 3]");
|
||||
REQUIRE(f != nullptr);
|
||||
REQUIRE(std::ungetc(',', f.get()) == ',');
|
||||
REQUIRE(std::ungetc('1', f.get()) == '1');
|
||||
REQUIRE(std::ungetc('[', f.get()) == '[');
|
||||
CHECK(json::parse(f.get()) == json({1, 2, 3}));
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("SAX events across the edges of the file's buffer")
|
||||
{
|
||||
const std::string document = R"({"key": "a value that is longer than the buffer", "n": [1, -2.5e3, true, null]})";
|
||||
std::vector<long> expected;
|
||||
{
|
||||
const file_ptr f = open_with(name, document);
|
||||
REQUIRE(f != nullptr);
|
||||
position_reading_sax sax(f.get());
|
||||
CHECK(json::sax_parse(f.get(), &sax));
|
||||
expected = sax.positions();
|
||||
}
|
||||
for (int size = min_full_buffer; size <= 20; ++size)
|
||||
{
|
||||
CAPTURE(size)
|
||||
std::vector<char> buffer(static_cast<std::size_t>(size));
|
||||
const file_ptr f = open_with(name, document);
|
||||
REQUIRE(f != nullptr);
|
||||
REQUIRE(std::setvbuf(f.get(), buffer.data(), _IOFBF, buffer.size()) == 0);
|
||||
position_reading_sax sax(f.get());
|
||||
CHECK(json::sax_parse(f.get(), &sax));
|
||||
// the events happen at the same positions of the file
|
||||
CHECK(sax.positions() == expected);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("binary strings across the edges of the file's buffer")
|
||||
{
|
||||
const json value = {{"a string that is longer than the buffer of the file", "and a value that is longer, too"}, {"n", {1, 2}}};
|
||||
const std::vector<std::uint8_t> cbor = json::to_cbor(value);
|
||||
for (int size = min_full_buffer; size <= 20; ++size)
|
||||
{
|
||||
CAPTURE(size)
|
||||
std::vector<char> buffer(static_cast<std::size_t>(size));
|
||||
const file_ptr f = open_with(name, std::string(reinterpret_cast<const char*>(cbor.data()), cbor.size()));
|
||||
REQUIRE(f != nullptr);
|
||||
REQUIRE(std::setvbuf(f.get(), buffer.data(), _IOFBF, buffer.size()) == 0);
|
||||
CHECK(json::from_cbor(f.get()) == value);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("binary formats read from a FILE*")
|
||||
{
|
||||
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
|
||||
|
||||
@@ -403,6 +403,7 @@ TEST_CASE("guard page")
|
||||
CHECK(nlohmann::detail::find_string_special(p, n) <= n);
|
||||
CHECK(nlohmann::detail::find_ascii_copyable_run(p, n) <= n);
|
||||
CHECK(nlohmann::detail::valid_utf8_prefix(p, n) <= n);
|
||||
CHECK(nlohmann::detail::valid_utf8_string(p, n) == (nlohmann::detail::valid_utf8_prefix(p, n) == n));
|
||||
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
|
||||
CHECK(nlohmann::detail::find_string_delimiter(p, n) <= n);
|
||||
#endif
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <iterator>
|
||||
|
||||
#if JSON_HAS_RANGES
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
@@ -665,6 +667,23 @@ TEST_CASE("iterators 2")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("reverse iterators of a const value")
|
||||
{
|
||||
const json j_array = {1, 2, 3};
|
||||
const json j_object = {{"one", 1}, {"two", 2}};
|
||||
|
||||
// rbegin() and rend() of a const value are crbegin() and crend()
|
||||
CHECK(j_array.rbegin() == j_array.crbegin());
|
||||
CHECK(j_array.rend() == j_array.crend());
|
||||
CHECK(*j_array.rbegin() == 3);
|
||||
CHECK(std::distance(j_array.rbegin(), j_array.rend()) == 3);
|
||||
|
||||
CHECK(j_object.rbegin() == j_object.crbegin());
|
||||
CHECK(j_object.rend() == j_object.crend());
|
||||
CHECK(j_object.rbegin().key() == "two");
|
||||
CHECK(std::distance(j_object.rbegin(), j_object.rend()) == 2);
|
||||
}
|
||||
|
||||
SECTION("reverse iterator arithmetic")
|
||||
{
|
||||
json j_object = {{"one", 1}, {"two", 2}, {"three", 3}};
|
||||
|
||||
@@ -14,9 +14,11 @@ using nlohmann::json;
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "make_test_data_available.hpp"
|
||||
|
||||
@@ -2453,3 +2455,99 @@ TEST_CASE("JSON patch - diff behavior documented in the API reference")
|
||||
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - operations that are left to json_pointer")
|
||||
{
|
||||
// the usual operations on a plain pointer are applied without building a
|
||||
// json_pointer; the following are not plain, and some are errors
|
||||
SECTION("a path without a slash")
|
||||
{
|
||||
const json j = {{"a", 1}};
|
||||
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "add", "path": "abc", "value": 1}])"_json), json::parse_error&);
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "abc"}])"_json), json::parse_error&);
|
||||
}
|
||||
|
||||
SECTION("remove with an escaped reference token")
|
||||
{
|
||||
const json j = {{"a/b", 1}, {"c~d", 2}, {"e", 3}};
|
||||
|
||||
CHECK(j.patch(R"([{"op": "remove", "path": "/a~1b"}])"_json) == json({{"c~d", 2}, {"e", 3}}));
|
||||
CHECK(j.patch(R"([{"op": "remove", "path": "/c~0d"}])"_json) == json({{"a/b", 1}, {"e", 3}}));
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "/missing~1key"}])"_json), json::out_of_range&);
|
||||
}
|
||||
|
||||
SECTION("a path through a primitive value")
|
||||
{
|
||||
const json j = {{"a", 1}};
|
||||
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "test", "path": "/a/b", "value": 1}])"_json), json::other_error&);
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "replace", "path": "/a/b", "value": 1}])"_json), json::out_of_range&);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - diff of objects with many members")
|
||||
{
|
||||
// more members than diff() keeps without allocating
|
||||
for (const std::size_t members :
|
||||
{
|
||||
16u, 17u, 40u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
|
||||
json source = json::object();
|
||||
json target = json::object();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = "key" + std::to_string(i);
|
||||
source[key] = i;
|
||||
// every third member differs
|
||||
target[key] = (i % 3 == 0) ? i + 1 : i;
|
||||
}
|
||||
// the same keys, a removed and an added one
|
||||
source["removed"] = true;
|
||||
target["added"] = false;
|
||||
|
||||
const json patch = json::diff(source, target);
|
||||
CHECK(source.patch(patch) == target);
|
||||
CHECK(patch.size() == ((members + 2) / 3) + 2);
|
||||
|
||||
const nlohmann::ordered_json ordered_source = source;
|
||||
const nlohmann::ordered_json ordered_target = target;
|
||||
CHECK(ordered_source.patch(nlohmann::ordered_json::diff(ordered_source, ordered_target)) == ordered_target);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive descent")
|
||||
{
|
||||
// the values at the innermost level are diffed without recursion: equal
|
||||
// leaves yield nothing, leaves of different types and different leaves
|
||||
// are replaced
|
||||
const std::size_t depth = 200;
|
||||
|
||||
json source = {1, "text", 3, {{"x", 1}, {"y", 2}}, true, nullptr};
|
||||
json target = {1, 2, 4, {{"x", 1}, {"y", 3}}, true, nullptr};
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
source = json::array({std::move(source)});
|
||||
target = json::array({std::move(target)});
|
||||
}
|
||||
|
||||
const json patch = json::diff(source, target);
|
||||
CHECK(patch.size() == 3);
|
||||
CHECK(source.patch(patch) == target);
|
||||
|
||||
// the same with objects as the outer levels
|
||||
json object_source = {{"equal", 1}, {"type", "text"}, {"value", 3}};
|
||||
json object_target = {{"equal", 1}, {"type", 2}, {"value", 4}};
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
object_source = json::object({{"a", std::move(object_source)}});
|
||||
object_target = json::object({{"a", std::move(object_target)}});
|
||||
}
|
||||
|
||||
const json object_patch = json::diff(object_source, object_target);
|
||||
CHECK(object_patch.size() == 2);
|
||||
CHECK(object_source.patch(object_patch) == object_target);
|
||||
}
|
||||
@@ -106,6 +106,12 @@ TEST_CASE("JSON pointers")
|
||||
CHECK_THROWS_WITH_AS(p.pop_back(),
|
||||
"[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
|
||||
|
||||
// the top of a pointer is its first reference token
|
||||
const json::json_pointer nonempty("/foo/bar/0");
|
||||
CHECK(nonempty.top() == json::json_pointer("/foo"));
|
||||
CHECK(nonempty.top().to_string() == "/foo");
|
||||
CHECK(json::json_pointer("/a~1b").top() == json::json_pointer("/a~1b"));
|
||||
|
||||
SECTION("array index error")
|
||||
{
|
||||
json v = {1, 2, 3, 4};
|
||||
@@ -697,6 +703,16 @@ TEST_CASE("JSON pointers")
|
||||
CHECK_THROWS_WITH_AS(j_error.unflatten(),
|
||||
"[json.exception.type_error.313] invalid value to unflatten", json::type_error&);
|
||||
|
||||
// error for an invalid escape sequence in a key
|
||||
CHECK_THROWS_WITH_AS(json({{"/a~2", 1}}).unflatten(),
|
||||
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
|
||||
CHECK_THROWS_WITH_AS(json({{"/a/b", 1}, {"/c~", 1}}).unflatten(),
|
||||
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
|
||||
|
||||
// error for an element of an array that is also a container
|
||||
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/0/x", 2}}).unflatten(),
|
||||
"[json.exception.type_error.313] invalid value to unflatten", json::type_error&);
|
||||
|
||||
// explicit roundtrip check
|
||||
CHECK(j.flatten().unflatten() == j);
|
||||
|
||||
|
||||
+203
-72
@@ -31,9 +31,11 @@ using json_view_test::has_duplicate_keys;
|
||||
#include <ios>
|
||||
#include <istream>
|
||||
#include <iterator>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <new>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -98,6 +100,71 @@ template<typename View>
|
||||
using bool_conversion_t = decltype(static_cast<bool>(std::declval<View>()));
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// an allocator that never has room
|
||||
template<typename T>
|
||||
struct failing_allocator
|
||||
{
|
||||
using value_type = T;
|
||||
// GCC 4.8's std::map takes these from the allocator instead of from
|
||||
// std::allocator_traits
|
||||
using pointer = T*;
|
||||
using const_pointer = const T*;
|
||||
using reference = T&;
|
||||
using const_reference = const T&;
|
||||
using size_type = std::size_t;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
template<typename U>
|
||||
struct rebind
|
||||
{
|
||||
using other = failing_allocator<U>;
|
||||
};
|
||||
|
||||
failing_allocator() = default;
|
||||
|
||||
template<typename U>
|
||||
explicit failing_allocator(const failing_allocator<U>& /*other*/) noexcept {}
|
||||
|
||||
[[noreturn]] T* allocate(std::size_t /*n*/) // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
void deallocate(T* /*p*/, std::size_t /*n*/) noexcept {} // NOLINT(readability-convert-member-functions-to-static)
|
||||
|
||||
// GCC 4.8's std::map calls these on the allocator directly instead of
|
||||
// going through std::allocator_traits
|
||||
std::size_t max_size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
return (std::numeric_limits<std::size_t>::max)() / sizeof(T);
|
||||
}
|
||||
|
||||
template<typename U, typename... Args>
|
||||
void construct(U* p, Args&& ... args) // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
::new (static_cast<void*>(p)) U(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
void destroy(U* p) noexcept // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
static_cast<void>(p); // unused for trivially destructible types
|
||||
p->~U();
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
bool operator==(const failing_allocator<U>& /*other*/) const noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
bool operator!=(const failing_allocator<U>& /*other*/) const noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// the exception parse() throws for a text, or "" if it accepts it
|
||||
std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false)
|
||||
{
|
||||
@@ -225,7 +292,7 @@ TEST_CASE("json_view")
|
||||
#if JSON_DIAGNOSTICS
|
||||
// the parents are set, so errors name the path
|
||||
const json m = materialized<json_document>(R"({"a":{"b":[1]}})");
|
||||
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), "[json.exception.type_error.304] (/a/b/0) cannot use at() with number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), json_view_test::message("type_error.304", "/a/b/0", 11, 12, "cannot use at() with number").c_str(), json::type_error&);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -235,7 +302,9 @@ TEST_CASE("json_view")
|
||||
{
|
||||
"", " ", "[", "]", "{", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e",
|
||||
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", "[1]x", "/", "/*", "[\n 1,\n x\n]", // NOLINT(modernize-raw-string-literal)
|
||||
"1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}"
|
||||
"1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}",
|
||||
// (an error after every kind of value: the error is found by replaying the parser)
|
||||
R"([null, true, -1, 1, 1.5, "s", {"a": 1} 2])", R"({"a": null, "b": -1, "c": 1.5, "d": "s", "e": {}, 1})"
|
||||
})
|
||||
{
|
||||
CAPTURE(text)
|
||||
@@ -368,6 +437,10 @@ TEST_CASE("json_view")
|
||||
CHECK(from_stream.root().materialize() == expected);
|
||||
const std::list<char> list(text.begin(), text.end());
|
||||
CHECK(json_document::parse(list.begin(), list.end()).owns_source());
|
||||
// (an input that is not valid)
|
||||
const std::list<char> bad_list{'[', '1', ','};
|
||||
json_document from_bad_list;
|
||||
CHECK_THROWS_AS(from_bad_list = json_document::parse(bad_list.begin(), bad_list.end()), json::parse_error&);
|
||||
CHECK(materialized<json_document>(list.begin(), list.end()) == expected);
|
||||
|
||||
// iterator pairs: pointers are borrowed, and so are contiguous library
|
||||
@@ -750,19 +823,6 @@ std::string exception_of(F f)
|
||||
return "";
|
||||
}
|
||||
|
||||
// the exception a call on a basic_json value throws, without the byte range of
|
||||
// the value (JSON_DIAGNOSTIC_POSITIONS), which the exceptions of views lack
|
||||
template<typename F>
|
||||
std::string json_exception_of(F f)
|
||||
{
|
||||
std::string what = exception_of(f);
|
||||
const std::size_t at = what.find("] (bytes ");
|
||||
if (at != std::string::npos)
|
||||
{
|
||||
what.erase(at + 2, what.find(") ", at) - at);
|
||||
}
|
||||
return what;
|
||||
}
|
||||
#endif
|
||||
|
||||
// compares a view with the ordered_json value materialize() gives for it:
|
||||
@@ -971,24 +1031,25 @@ TEST_CASE("json_view element access and iteration")
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
if (!j.is_object())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == json_exception_of([&] { static_cast<void>(j["a"]); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == exception_of([&] { static_cast<void>(j["a"]); }));
|
||||
}
|
||||
if (!j.is_array())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == json_exception_of([&] { static_cast<void>(j[0]); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == exception_of([&] { static_cast<void>(j[0]); }));
|
||||
}
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == json_exception_of([&] { static_cast<void>(j.at("a")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == json_exception_of([&] { static_cast<void>(j.at("missing")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == json_exception_of([&] { static_cast<void>(j.at(0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == json_exception_of([&] { static_cast<void>(j.at(5)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == exception_of([&] { static_cast<void>(j.at("a")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == exception_of([&] { static_cast<void>(j.at("missing")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == exception_of([&] { static_cast<void>(j.at(0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == exception_of([&] { static_cast<void>(j.at(5)); }));
|
||||
if (!(j.is_object() && j.empty())) // (key() of an end iterator)
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == json_exception_of([&] { static_cast<void>(j.begin().key()); }));
|
||||
// (without the context of basic_json: view iterators do not know the value they iterate over)
|
||||
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == (j.is_object() ? "" : "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators"));
|
||||
}
|
||||
if (!j.empty() || j.is_null())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == json_exception_of([&] { static_cast<void>(j.front()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == json_exception_of([&] { static_cast<void>(j.back()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == exception_of([&] { static_cast<void>(j.front()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == exception_of([&] { static_cast<void>(j.back()); }));
|
||||
}
|
||||
#endif
|
||||
CHECK(v.contains("a") == j.contains("a"));
|
||||
@@ -1000,8 +1061,8 @@ TEST_CASE("json_view element access and iteration")
|
||||
const json_document d = json_document::parse(R"({"a":[]})");
|
||||
CHECK(d.root()["b"].is_discarded());
|
||||
CHECK(d.root()["a"][0].is_discarded());
|
||||
CHECK_THROWS_WITH_AS(d.root()["a"].front(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.root()["a"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.root()["a"].front(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.root()["a"].back(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
|
||||
const json_view invalid{};
|
||||
CHECK(invalid.begin() == invalid.end());
|
||||
CHECK(std::string(invalid.type_name()) == "discarded");
|
||||
@@ -1032,10 +1093,10 @@ TEST_CASE("json_view element access and iteration")
|
||||
CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// type errors on values that are not discarded stay
|
||||
CHECK_THROWS_WITH_AS(v[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["s"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["s"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v[0], json_view_test::message("type_error.305", "", 0, 29, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], json_view_test::message("type_error.305", "/a/b", 10, 17, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["s"]["c"], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a string argument with string").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["s"][0], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a numeric argument with string").c_str(), json::type_error&);
|
||||
CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&);
|
||||
CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&);
|
||||
// at() keeps throwing on a discarded view
|
||||
@@ -1110,9 +1171,9 @@ TEST_CASE("json_view element access and iteration")
|
||||
CHECK(v[(std::numeric_limits<std::size_t>::max)()].is_discarded());
|
||||
CHECK(v[std::numeric_limits<int>::max()].is_discarded());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
CHECK_THROWS_WITH_AS(v.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(3u), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(3), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(3u), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_AS(v.at(-1), json::out_of_range&);
|
||||
CHECK_THROWS_AS(v.at(-1L), json::out_of_range&);
|
||||
CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&);
|
||||
@@ -1246,26 +1307,6 @@ TEST_CASE("json_view element access and iteration")
|
||||
|
||||
namespace
|
||||
{
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// an exception message without the context that basic_json adds with
|
||||
// JSON_DIAGNOSTICS ("(/path) ") and JSON_DIAGNOSTIC_POSITIONS ("(bytes 1-2) ");
|
||||
// the view's exceptions have no such context
|
||||
std::string without_path(std::string msg)
|
||||
{
|
||||
for (const char* prefix :
|
||||
{"] (/", "] (bytes "
|
||||
})
|
||||
{
|
||||
const std::size_t open = msg.find(prefix);
|
||||
if (open != std::string::npos)
|
||||
{
|
||||
msg.erase(open + 2, msg.find(") ", open) + 2 - (open + 2));
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
#endif
|
||||
|
||||
// the bits of a float, to compare values bit for bit
|
||||
std::uint64_t bits(double x)
|
||||
{
|
||||
@@ -1336,18 +1377,18 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std::
|
||||
// conversions to the wrong type throw what basic_json throws
|
||||
if (!j.is_number())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<int>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == exception_of([&] { static_cast<void>(j.get<int>()); }));
|
||||
}
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<bool>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::string>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == exception_of([&] { static_cast<void>(j.get<bool>()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == exception_of([&] { static_cast<void>(j.get<std::string>()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); }));
|
||||
if (!j.is_array())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); }));
|
||||
}
|
||||
if (!j.is_object())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); }));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1490,8 +1531,8 @@ TEST_CASE("json_view values")
|
||||
CHECK(v[3].number_token() == "123456789012345678901234567890");
|
||||
CHECK(v[4].number_token() == "-12");
|
||||
CHECK(v[5].number_token() == "7");
|
||||
CHECK_THROWS_WITH_AS(v[6].number_token(), "[json.exception.type_error.302] type must be number, but is string", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v.get_string(), "[json.exception.type_error.302] type must be string, but is array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v[6].number_token(), json_view_test::message("type_error.302", "/6", 56, 59, "type must be number, but is string").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v.get_string(), json_view_test::message("type_error.302", "", 0, 60, "type must be string, but is array").c_str(), json::type_error&);
|
||||
}
|
||||
|
||||
SECTION("conversions")
|
||||
@@ -1543,6 +1584,17 @@ TEST_CASE("json_view values")
|
||||
const json_document dup = json_document::parse(R"({"a":1,"a":2})");
|
||||
CHECK((dup.root().get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 2}}));
|
||||
|
||||
// a map of views from something that is not an object
|
||||
using view_map = std::map<std::string, json_view>;
|
||||
const json_document not_an_object = json_document::parse("[1,2]");
|
||||
CHECK_THROWS_WITH_AS(not_an_object.root().get<view_map>(), json_view_test::message("type_error.302", "", 0, 5, "type must be object, but is array").c_str(), json::type_error&);
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// (and one that cannot allocate its nodes)
|
||||
using failing_map = std::map<std::string, json_view, std::less<std::string>, failing_allocator<std::pair<const std::string, json_view>>>; // NOLINT(modernize-use-transparent-functors)
|
||||
const json_document one_member = json_document::parse(R"({"a":1})");
|
||||
CHECK_THROWS_AS(one_member.root().get<failing_map>(), std::bad_alloc&);
|
||||
#endif
|
||||
|
||||
const json_view invalid{};
|
||||
CHECK_THROWS_WITH_AS(invalid.get<int>(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&);
|
||||
CHECK(invalid.get<json>().is_discarded());
|
||||
@@ -1565,15 +1617,15 @@ TEST_CASE("json_view values")
|
||||
CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0));
|
||||
CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3));
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast<void>(j.value("s", 0)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", 0)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == exception_of([&] { static_cast<void>(j.value("s", 0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == exception_of([&] { static_cast<void>(j["n"].value("x", 0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); }));
|
||||
// string defaults: the member is not a string, or the value is neither an object nor an array
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value("n", "default")); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", "default")); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == exception_of([&] { static_cast<void>(j.value("n", "default")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == exception_of([&] { static_cast<void>(j["n"].value("x", "default")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); }));
|
||||
#endif
|
||||
// (defaults and members longer than the short string optimization)
|
||||
CHECK(v.value("s", "a default longer than fifteen characters") == "text");
|
||||
@@ -1583,6 +1635,85 @@ TEST_CASE("json_view values")
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// get<T>() of a boolean: the same value or the same exception in basic_json and in the view (#5988)
|
||||
template<typename T>
|
||||
void check_boolean_get_parity()
|
||||
{
|
||||
for (const bool b :
|
||||
{
|
||||
true, false
|
||||
})
|
||||
{
|
||||
const std::string text = b ? "true" : "false";
|
||||
const json j = json::parse(text);
|
||||
const json_document d = json_document::parse(text);
|
||||
CAPTURE(text)
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
T dom_value {};
|
||||
T view_value{};
|
||||
const std::string dom_error = json_view_test::exception_of_call([&] { dom_value = j.get<T>(); });
|
||||
const std::string view_error = json_view_test::exception_of_call([&] { view_value = d.root().template get<T>(); });
|
||||
CHECK(dom_error == view_error);
|
||||
if (dom_error.empty())
|
||||
{
|
||||
CHECK(dom_value == view_value);
|
||||
CHECK(view_value == static_cast<T>(b));
|
||||
}
|
||||
#else
|
||||
// without exceptions, the combinations that throw (number_integer_t, number_unsigned_t, number_float_t)
|
||||
// cannot be run: JSON_THROW aborts
|
||||
const bool throws = std::is_same<T, json::number_integer_t>::value
|
||||
|| std::is_same<T, json::number_unsigned_t>::value
|
||||
|| std::is_same<T, json::number_float_t>::value;
|
||||
if (!throws)
|
||||
{
|
||||
CHECK(j.get<T>() == d.root().template get<T>());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view get<T>() of a boolean (#5988)")
|
||||
{
|
||||
check_boolean_get_parity<bool>();
|
||||
check_boolean_get_parity<char>();
|
||||
check_boolean_get_parity<signed char>();
|
||||
check_boolean_get_parity<unsigned char>();
|
||||
check_boolean_get_parity<short>(); // NOLINT(google-runtime-int)
|
||||
check_boolean_get_parity<unsigned short>(); // NOLINT(google-runtime-int)
|
||||
check_boolean_get_parity<int>();
|
||||
check_boolean_get_parity<unsigned>();
|
||||
check_boolean_get_parity<long>(); // NOLINT(google-runtime-int)
|
||||
check_boolean_get_parity<unsigned long>(); // NOLINT(google-runtime-int)
|
||||
check_boolean_get_parity<long long>(); // NOLINT(google-runtime-int)
|
||||
check_boolean_get_parity<unsigned long long>(); // NOLINT(google-runtime-int)
|
||||
check_boolean_get_parity<std::int64_t>();
|
||||
check_boolean_get_parity<std::uint64_t>();
|
||||
check_boolean_get_parity<float>();
|
||||
check_boolean_get_parity<double>();
|
||||
check_boolean_get_parity<long double>();
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("the exceptions of the exact number types")
|
||||
{
|
||||
const json_document d = json_document::parse("true");
|
||||
CHECK_THROWS_WITH_AS(d.root().get<json::number_integer_t>(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.root().get<json::number_unsigned_t>(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.root().get<json::number_float_t>(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&);
|
||||
CHECK(d.root().get<int>() == 1);
|
||||
// get_to() and value() go through get<T>()
|
||||
json::number_float_t x = 0;
|
||||
CHECK_THROWS_AS(d.root().get_to(x), json::type_error&);
|
||||
const json_document o = json_document::parse("{\"b\":true}");
|
||||
CHECK_THROWS_AS(static_cast<void>(o.root().value("b", 0.0)), json::type_error&);
|
||||
CHECK(o.root().value("b", 0) == 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("json_view JSON pointers")
|
||||
{
|
||||
SECTION("every value of generated documents")
|
||||
@@ -1632,7 +1763,7 @@ TEST_CASE("json_view JSON pointers")
|
||||
{
|
||||
CAPTURE(pointer)
|
||||
const json::json_pointer p(pointer);
|
||||
const std::string at_error = without_path(exception_of([&] { static_cast<void>(j.at(p)); }));
|
||||
const std::string at_error = exception_of([&] { static_cast<void>(j.at(p)); });
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(p)); }) == at_error);
|
||||
if (at_error.empty())
|
||||
{
|
||||
@@ -1646,7 +1777,7 @@ TEST_CASE("json_view JSON pointers")
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == without_path(exception_of([&] { static_cast<void>(j[p]); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == exception_of([&] { static_cast<void>(j[p]); }));
|
||||
}
|
||||
// (basic_json::contains() throws out_of_range.404 for an empty
|
||||
// array index token, although it is not meant to throw; the view
|
||||
@@ -1657,7 +1788,7 @@ TEST_CASE("json_view JSON pointers")
|
||||
static_cast<void>(found);
|
||||
});
|
||||
CHECK(v.contains(p) == (contains_error.empty() && j.contains(p)));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast<void>(j.value(p, 5)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == exception_of([&] { static_cast<void>(j.value(p, 5)); }));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <nlohmann/detail/view/builder.hpp>
|
||||
#include <nlohmann/detail/view/materialize.hpp>
|
||||
#include <nlohmann/detail/view/string_ref.hpp>
|
||||
#include <nlohmann/json_view.hpp> // json_document::accept
|
||||
#else
|
||||
#include <nlohmann/json_view.hpp> // the single header contains the internal headers
|
||||
#endif
|
||||
@@ -59,7 +60,8 @@ built build(const std::string& text, bool comments, bool trailing_commas, bool s
|
||||
r.copy.assign(text.begin(), text.end());
|
||||
src = r.copy.data();
|
||||
}
|
||||
r.ok = nlohmann::detail::view::build < FloatType, !nlohmann::detail::abi_config::strict_nul_handling > (*r.data, src, text.size(), comments, trailing_commas, sentinel, r.failure);
|
||||
constexpr bool nul_is_end = !nlohmann::detail::abi_config::strict_nul_handling;
|
||||
r.ok = nlohmann::detail::view::build<FloatType, nul_is_end>(*r.data, src, text.size(), comments, trailing_commas, sentinel, false, r.failure);
|
||||
r.data->src = src;
|
||||
r.data->base[0] = src;
|
||||
r.data->base[1] = r.data->arena.data();
|
||||
@@ -172,6 +174,22 @@ void check_string(const std::string& content)
|
||||
}
|
||||
}
|
||||
|
||||
// accept() stores no nodes: it must accept and reject what json::accept does, for all options and for inputs with
|
||||
// and without a terminating NUL
|
||||
void check_accept(const std::string& text)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const std::vector<char> bytes(text.begin(), text.end());
|
||||
for (int options = 0; options < 4; ++options)
|
||||
{
|
||||
const bool comments = (options & 1) != 0;
|
||||
const bool trailing_commas = (options & 2) != 0;
|
||||
const bool accepted = json::accept(text, comments, trailing_commas);
|
||||
CHECK(nlohmann::json_document::accept(text, comments, trailing_commas) == accepted);
|
||||
CHECK(nlohmann::json_document::accept(bytes, comments, trailing_commas) == accepted);
|
||||
}
|
||||
}
|
||||
|
||||
// a small deterministic generator of documents
|
||||
struct generator
|
||||
{
|
||||
@@ -423,6 +441,133 @@ TEST_CASE("json_view builder")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("accept() without an index")
|
||||
{
|
||||
for (const char* text :
|
||||
{
|
||||
"", " ", "\n", "null", "nul", "true", "[", "]", "[]", "{}", "[1,2,3]", "[1,2,3", "[1,2,3,]", "{\"a\":1,}", "{\"a\"}", "{\"a\":}",
|
||||
"[1] x", "[1][2]", "\xEF\xBB\xBF[1]", "[\"a\\", R"(["\u12"])", R"(["\ud800"])", "[\"\x01\"]", "[\"\xff\"]", "[01]", "[1.]",
|
||||
"[.5]", "[-]", "[1e]", "// c\n[1]", "/* c */ {\"a\" /* x */ : 1}", "/* c", "[1] // c", "1e400", "-1e400", "123456789012345678901234567890",
|
||||
R"({"a":1,"a":2})", "[[[]]]", R"("\u00e4\n\ud83d\ude00")"
|
||||
})
|
||||
{
|
||||
check_accept(text);
|
||||
}
|
||||
// a null byte inside the text, and after it
|
||||
check_accept(std::string("[1,\0]", 5));
|
||||
check_accept(std::string("[1]\0", 4));
|
||||
check_accept(std::string("\0", 1));
|
||||
|
||||
// generated documents and damaged copies, as for the builder above
|
||||
generator g;
|
||||
for (int i = 0; i < 2000; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
check_accept(text);
|
||||
std::string damaged = text;
|
||||
const auto at = static_cast<std::size_t>(g.r(static_cast<int>(damaged.size())));
|
||||
static const char replacements[] = {'x', '"', '\\', ',', ':', ']', '}', '[', '{', '1', '-', '.', 'e', '\0', '\n', '/'}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
damaged[at] = replacements[g.r(16)];
|
||||
check_accept(damaged);
|
||||
check_accept(text.substr(0, at));
|
||||
}
|
||||
|
||||
// nesting beyond the 64 levels that are kept on the builder's own stack, closed and not closed
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
63u, 64u, 65u, 66u, 200u, 1000u
|
||||
})
|
||||
{
|
||||
check_accept(std::string(depth, '[') + std::string(depth, ']'));
|
||||
check_accept(std::string(depth, '[') + std::string(depth - 1, ']'));
|
||||
std::string objects;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
objects += "{\"k\":";
|
||||
}
|
||||
objects += "1" + std::string(depth, '}');
|
||||
check_accept(objects);
|
||||
check_accept(objects.substr(0, objects.size() - 1));
|
||||
}
|
||||
|
||||
// objects with enough members to get a hash index when they are indexed (and duplicate keys)
|
||||
std::string large = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
large += (i == 0 ? "\"k" : ",\"k") + std::to_string(i % 200) + "\":" + std::to_string(i);
|
||||
}
|
||||
check_accept(large + "}");
|
||||
check_accept(large);
|
||||
check_accept("[" + large + "},{" + large.substr(1) + "}]");
|
||||
|
||||
// the overflow check uses the floating-point type of the document
|
||||
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
||||
for (const char* text :
|
||||
{
|
||||
"1e39", "-1e39", "3.4028235e38", "3.4028236e38", "[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})", "340282356779733661637539395458142568448"
|
||||
})
|
||||
{
|
||||
CAPTURE(text)
|
||||
CHECK(nlohmann::basic_json_document<float_json>::accept(text) == float_json::accept(text));
|
||||
}
|
||||
|
||||
// strings are checked and not decoded: runs of text of every length from 0 to 40 between valid and invalid
|
||||
// escapes, UTF-8 sequences and control characters, in values and in keys, cut near the end of the text
|
||||
const std::array<const char*, 9> valid = {{"\\n", "\\\"", "\\\\", "\\/", "\\t", "\\u00e4", "\\u20ac", "\\ud83d\\ude00", "\\u0000"}};
|
||||
const std::array<const char*, 12> invalid = {{"\\x", "\\ud800", "\\udc00", "\\u12G4", "\\u12", "\\ud83dx", "\\ud83d\\u0041", "\\ud83d\\ud83d", "\\", "\x01", "\x1f", "\x7f"}};
|
||||
const std::array<const char*, 6> utf8 = {{"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xc3", "\xff", "\x80"}};
|
||||
std::mt19937 rng(2029); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
for (std::size_t run = 0; run <= 40; ++run)
|
||||
{
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
std::string content = std::string(rng() % 20, 'a');
|
||||
const auto pieces = 1 + (rng() % 6);
|
||||
for (unsigned k = 0; k < pieces; ++k)
|
||||
{
|
||||
content += valid[rng() % valid.size()];
|
||||
content += std::string(k == 0 ? run : rng() % 41, static_cast<char>('b' + (rng() % 20)));
|
||||
switch (rng() % 8)
|
||||
{
|
||||
case 0:
|
||||
content += utf8[rng() % 3];
|
||||
break;
|
||||
case 1:
|
||||
content += utf8[rng() % utf8.size()];
|
||||
break;
|
||||
case 2:
|
||||
content += invalid[rng() % invalid.size()];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const std::string& text :
|
||||
{
|
||||
"[\"" + content + "\"]", "{\"" + content + "\":1}", "[\"" + content, "[\"" + content.substr(0, content.size() - (rng() % 4)) + "\"]"
|
||||
})
|
||||
{
|
||||
check_accept(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("escapes in a row")
|
||||
{
|
||||
for (const char* content :
|
||||
{
|
||||
"\\n", R"(\n\t\\)", R"(\u00e9\u00e9\u4e2d)", "\\u00e9\\u00e", "\\u00e9\\", "\\n\\q", R"(\n\ud83d\ude00\n)",
|
||||
R"(\ud83d\ude00\ud83d)", R"(\n\n\n abc \n\n)", "\\n\x01", "\\n\xC3\x28", "\\n\\"
|
||||
})
|
||||
{
|
||||
check_accept(std::string("[\"") + content + "\"]");
|
||||
check_accept(std::string("[\"") + content);
|
||||
check_accept(std::string("{\"") + content + "\":1}");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("test files")
|
||||
{
|
||||
for (const char* name :
|
||||
@@ -523,6 +668,54 @@ TEST_CASE("json_view builder: strings across vector blocks")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view builder: fraction digits")
|
||||
{
|
||||
// The digits of a fraction are skipped 16 bytes at a time where there are
|
||||
// that many left (SSE2), and one at a time after that. Fractions of every
|
||||
// length from 0 to 40 digits, followed by every kind of byte that ends a
|
||||
// number or lies next to the digits in ASCII ('/' and ':'), as values, as
|
||||
// keys' neighbours, and at the very end of the text (a vector without a
|
||||
// terminating NUL). The length of the number is part of the value that
|
||||
// the test compares with json::parse().
|
||||
const std::string followers = ",]}/:.eE+-x \n\t\"09";
|
||||
std::mt19937 rng(2028); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
for (std::size_t digits = 0; digits <= 40; ++digits)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
std::string fraction;
|
||||
for (std::size_t k = 0; k < digits; ++k)
|
||||
{
|
||||
fraction += static_cast<char>('0' + (rng() % 10));
|
||||
}
|
||||
const std::string number = std::string(rng() % 2 == 0 ? "-" : "") + std::to_string(rng() % 100000) + "." + fraction;
|
||||
check_same(number);
|
||||
check_same("[" + number + "]");
|
||||
std::string pair;
|
||||
pair.reserve((number.size() * 2) + 3);
|
||||
pair += '[';
|
||||
pair += number;
|
||||
pair += ',';
|
||||
pair += number;
|
||||
pair += ']';
|
||||
check_same(pair);
|
||||
check_same("{\"a\":" + number + "}");
|
||||
for (const char follower : followers)
|
||||
{
|
||||
check_same(number + follower);
|
||||
check_same("[" + number + follower + "]");
|
||||
check_same("[" + number + follower + std::string(rng() % 20, '1'));
|
||||
}
|
||||
// everything near the end of the text: the number cut in every place
|
||||
for (std::size_t cut = 1; cut <= 18 && cut <= number.size(); ++cut)
|
||||
{
|
||||
check_same(number.substr(0, number.size() - cut));
|
||||
check_same("[" + number.substr(0, number.size() - cut));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view builder: escapes between runs of text")
|
||||
{
|
||||
// The text between escapes is copied 16 bytes at a time where there are
|
||||
@@ -575,6 +768,59 @@ TEST_CASE("json_view builder: escapes between runs of text")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view builder: chains of unicode escapes")
|
||||
{
|
||||
// Consecutive \uXXXX escapes are decoded in a loop of their own. Chains of
|
||||
// every length from 1 to 40 of escapes that give one, two, and three
|
||||
// bytes of UTF-8, in upper and lower case, followed by what ends a chain:
|
||||
// the end of the string, text, another escape, UTF-8, and everything that
|
||||
// is not a valid escape (a bad digit at every position of the last
|
||||
// escape, a lone surrogate, a surrogate pair, a cut in every place).
|
||||
const std::array<const char*, 8> chain = {{"\\u0041", "\\u00e4", "\\u00E4", "\\u20ac", "\\u3042", "\\uFFFF", "\\u0000", "\\u07ff"}};
|
||||
const std::array<const char*, 12> ends = {{"", "abc", "\\n", "\\u00e9", "\\ud83d\\ude00", "\\ud83d", "\\ude00", "\\ud83d\\u0041", "\\u12G4", "\\u12", "\xc3\xa4", "\\"}};
|
||||
std::mt19937 rng(2027); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
for (std::size_t length = 1; length <= 40; ++length)
|
||||
{
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
std::string content = std::string(rng() % 20, 'a');
|
||||
for (std::size_t k = 0; k < length; ++k)
|
||||
{
|
||||
content += chain[rng() % chain.size()];
|
||||
}
|
||||
const std::string whole = content;
|
||||
for (const char* end : ends)
|
||||
{
|
||||
check_string(whole + end);
|
||||
check_string(whole + end + std::string(rng() % 19, 'z'));
|
||||
}
|
||||
// a bad digit at every position of every escape near the end of the chain
|
||||
for (std::size_t back = 1; back <= 6 && back <= length; ++back)
|
||||
{
|
||||
for (std::size_t digit = 2; digit < 6; ++digit)
|
||||
{
|
||||
std::string broken = whole;
|
||||
broken[broken.size() - (6 * back) + digit] = "gG\"\x01 z"[rng() % 6];
|
||||
check_string(broken);
|
||||
}
|
||||
}
|
||||
// the text cut in every place of the last three escapes
|
||||
for (std::size_t cut = 0; cut <= 18 && cut <= whole.size(); ++cut)
|
||||
{
|
||||
check_string(whole.substr(0, whole.size() - cut));
|
||||
}
|
||||
}
|
||||
}
|
||||
// a chain longer than the first growth of the arena, in a key and in a value
|
||||
std::string long_chain;
|
||||
for (int i = 0; i < 3000; ++i)
|
||||
{
|
||||
long_chain += chain[static_cast<std::size_t>(i) % chain.size()];
|
||||
}
|
||||
check_string(long_chain);
|
||||
check_string(long_chain + "\\ud83d\\ude00" + long_chain);
|
||||
}
|
||||
|
||||
TEST_CASE("json_view node integer bits")
|
||||
{
|
||||
using nlohmann::detail::view::integer_bits;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
// These tests were split off unit-json_view_compare.cpp, whose object file got
|
||||
// too large for the MinGW assembler ("file too big").
|
||||
|
||||
namespace
|
||||
{
|
||||
// an object type that does not enumerate its members in ascending order of keys
|
||||
template<typename Key, typename Value, typename /*Compare*/, typename Allocator>
|
||||
using descending_object = std::map<Key, Value, std::greater<Key>, Allocator>;
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view comparison of differing values")
|
||||
{
|
||||
SECTION("values that differ in their first bytes, or are written differently")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
const bool equal = dx.root() == dy.root();
|
||||
CHECK(equal == (json::parse(x) == json::parse(y)));
|
||||
CHECK(equal == (dx.root() == json::parse(y)));
|
||||
const nlohmann::json_editable_document ex = nlohmann::json_editable_document::parse(x);
|
||||
CHECK(equal == (ex.root() == dy.root()));
|
||||
return equal;
|
||||
};
|
||||
// booleans
|
||||
CHECK(!same("true", "false"));
|
||||
CHECK(!same("[true]", "[false]"));
|
||||
CHECK(!same(R"({"a":false,"b":[1]})", R"({"a":true,"b":[1]})"));
|
||||
CHECK(same(R"({"a":true,"b":[1]})", R"({"a":true,"b":[1]})"));
|
||||
// strings and keys of equal length that differ in the first eight bytes (they are compared in words)
|
||||
CHECK(!same(R"("abcdefghijklmnopqrstuvwxyz")", R"("Xbcdefghijklmnopqrstuvwxyz")"));
|
||||
CHECK(!same(R"(["abcdefghijklmnopq",1])", R"(["abcdefghXjklmnopq",1])"));
|
||||
CHECK(!same(R"({"abcdefghijklmnopq":1})", R"({"Xbcdefghijklmnopq":1})"));
|
||||
CHECK(!same(R"({"abcdefghijklmnopq":[1]})", R"({"abcdefghijklmnopX":[1]})"));
|
||||
// floats of one layout and different tokens are the same value
|
||||
CHECK(same("[1.5e0, 2]", "[1.5E0, 2]"));
|
||||
CHECK(same("[1.5e0, 2]", "[1.5e+0, 2]"));
|
||||
CHECK(same("[1.50, 2]", "[1.5, 2]"));
|
||||
CHECK(!same("[1.5e0, 2]", "[1.6e0, 2]"));
|
||||
CHECK(!same("[1.5e0, 2]", "[1.5E1, 2]"));
|
||||
CHECK(same(R"({"a":1.25e0,"b":[3]})", R"({"a":1.25E0,"b":[3]})"));
|
||||
}
|
||||
|
||||
SECTION("a basic_json whose members are not sorted")
|
||||
{
|
||||
// (an object type that is not an ordered map, and does not keep its members sorted either)
|
||||
using descending_json = nlohmann::basic_json<descending_object>;
|
||||
using descending_document = nlohmann::basic_json_document<descending_json>;
|
||||
std::string text = "{";
|
||||
for (char key = 'a'; key <= 'p'; ++key)
|
||||
{
|
||||
text += (key != 'a' ? ",\"" : "\"") + std::string(1, key) + "\":[" + std::to_string(key) + "]";
|
||||
}
|
||||
text += "}";
|
||||
const descending_json j = descending_json::parse(text);
|
||||
const descending_document d = descending_document::parse(text);
|
||||
CHECK(d.root() == j);
|
||||
CHECK(j == d.root());
|
||||
descending_json other = j;
|
||||
other["k"] = 0;
|
||||
CHECK(d.root() != other);
|
||||
CHECK(other != d.root());
|
||||
}
|
||||
}
|
||||
+112
-672
@@ -35,9 +35,9 @@ using json_view_test::has_duplicate_keys;
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view.cpp, whose object file got too large
|
||||
// for the MinGW linker (see json_view_test_helpers.hpp). Like that file, this one
|
||||
// is also built with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
// These tests were split off unit-json_view.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view dump")
|
||||
{
|
||||
@@ -101,6 +101,10 @@ TEST_CASE("json_view dump")
|
||||
CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]");
|
||||
CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
|
||||
|
||||
CHECK(d.root().dump(0, ' ', true, json_view::number_format::source) == d.root().dump(0, ' ', false, json_view::number_format::source));
|
||||
const json_document escaped = json_document::parse("[\"\xC3\xA9\", 1.50, {\"\xF0\x9F\x98\x80\": 1E2}]");
|
||||
CHECK(escaped.root().dump(1, ' ', true, json_view::number_format::source) == "[\n \"\\u00e9\",\n 1.50,\n {\n \"\\ud83d\\ude00\": 1E2\n }\n]");
|
||||
|
||||
// float tokens of up to 17 significant digits in every spelling: those
|
||||
// of at most 15 digits are written from their digits, the others
|
||||
// through the conversion; both as dump() writes them
|
||||
@@ -150,6 +154,111 @@ TEST_CASE("json_view dump")
|
||||
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
|
||||
}
|
||||
|
||||
// tokens of 16 and 17 digits without an exponent (like GeoJSON
|
||||
// coordinates), which dump() converts in batches: doubles exactly
|
||||
// halfway between two candidates for the shortest digits, random
|
||||
// digits, and digits near a shorter decimal (...99999997,
|
||||
// ...00000003), whose rounding carries; in pairs and among other
|
||||
// values, so that the batches are interrupted; the last token near the
|
||||
// end of the text
|
||||
{
|
||||
std::mt19937_64 tokens(5627); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
const auto draw = [&tokens](std::size_t n)
|
||||
{
|
||||
const std::uint64_t r = tokens() % n;
|
||||
return static_cast<std::size_t>(r);
|
||||
};
|
||||
std::string coordinates = "[754570942.33984373,257769445.56835938,858106901.94921875,97944964.876953125,"
|
||||
"[0.5,1e-7],[1.5,-0.0,2.5],[1E2,12.5,0.1,3.25,4.5,5.75,6.125,7.0,8.5],[],[1.5],[1,2.5],[2.5,\"x\"],[[1.5],2.5],";
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
const std::size_t length = 16 + draw(2);
|
||||
std::string digits(1, static_cast<char>('1' + draw(9)));
|
||||
const std::size_t kept = draw(3) == 0 ? length : 2 + draw(length - 3);
|
||||
for (std::size_t k = 1; k < kept; ++k)
|
||||
{
|
||||
digits += static_cast<char>('0' + draw(10));
|
||||
}
|
||||
digits += std::string(length - kept, draw(2) == 0 ? '9' : '0');
|
||||
digits.back() = static_cast<char>('0' + draw(10));
|
||||
const std::size_t point = 1 + draw(9);
|
||||
const std::string token = (draw(2) == 0 ? "-" : "") + digits.substr(0, point) + "." + digits.substr(point);
|
||||
if (i != 0)
|
||||
{
|
||||
coordinates += ',';
|
||||
}
|
||||
if (draw(8) == 0)
|
||||
{
|
||||
coordinates += R"({"x":)";
|
||||
coordinates += token;
|
||||
coordinates += R"(,"s":"a"})";
|
||||
}
|
||||
else
|
||||
{
|
||||
coordinates += '[';
|
||||
coordinates += token;
|
||||
coordinates += ',';
|
||||
coordinates += token;
|
||||
coordinates += ']';
|
||||
}
|
||||
}
|
||||
coordinates += ",-65.613616999999977]";
|
||||
const json_document parsed = json_document::parse(coordinates);
|
||||
const ordered_json j = ordered_json::parse(coordinates);
|
||||
CHECK(parsed.root().dump() == j.dump());
|
||||
CHECK(parsed.root().dump(1) == j.dump(1));
|
||||
CHECK(parsed.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(parsed.root().back().get<double>() == j.back().get<double>());
|
||||
CHECK(parsed.root().back().dump() == "-65.61361699999998");
|
||||
std::size_t i = 0;
|
||||
for (const json_view v : parsed.root())
|
||||
{
|
||||
const ordered_json& e = j[i++];
|
||||
CHECK(v.dump() == e.dump());
|
||||
if (v.is_array() && !v.empty() && v[0].is_number())
|
||||
{
|
||||
CHECK(v[0].get<double>() == e[0].get<double>());
|
||||
}
|
||||
}
|
||||
|
||||
// an editable document: edits move some arrays, so that the walk
|
||||
// leaves the order of the parsed nodes
|
||||
ordered_json_editable_document editable = ordered_json_editable_document::parse(coordinates);
|
||||
ordered_json expected = j;
|
||||
for (std::size_t k = 0; k < 20000; k += 97)
|
||||
{
|
||||
if (expected[k].is_array())
|
||||
{
|
||||
editable.push_back(editable.root()[k], ordered_json(1.5));
|
||||
expected[k].push_back(1.5);
|
||||
}
|
||||
}
|
||||
CHECK(editable.root().dump() == expected.dump());
|
||||
}
|
||||
|
||||
// tokens of 16 and 17 digits with few digits after the point, and with a value of 0, which are
|
||||
// converted by the general algorithm; padded, as a token near the end of the text is not read in this way
|
||||
{
|
||||
const std::string padding(40, ' ');
|
||||
for (const char* token :
|
||||
{
|
||||
"123456789012.3456", "-123456789012.3456", "1234567890123.456", "12345678901234.56", "123456789012345.6", "1234567890123456.0",
|
||||
"0.0000000000000000", "-0.0000000000000000", "0.000000000000000", "1234567890123.4567", "99999999999999.999"
|
||||
})
|
||||
{
|
||||
CAPTURE(token)
|
||||
const std::string padded = std::string("[") + token + padding + "]";
|
||||
const json_document padded_document = json_document::parse(padded);
|
||||
const json padded_json = json::parse(padded);
|
||||
CHECK(padded_document.root()[0].get<double>() == padded_json[0].get<double>());
|
||||
CHECK(padded_document.root().dump() == padded_json.dump());
|
||||
// a float document converts the tokens as well
|
||||
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
||||
const nlohmann::basic_json_document<json_float> float_doc = nlohmann::basic_json_document<json_float>::parse(padded);
|
||||
CHECK(float_doc.root()[0].get<float>() == json_float::parse(padded)[0].get<float>());
|
||||
}
|
||||
}
|
||||
|
||||
// random doubles, written as parse() and dump() would
|
||||
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::string many = "[";
|
||||
@@ -171,673 +280,4 @@ TEST_CASE("json_view dump")
|
||||
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
|
||||
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
|
||||
}
|
||||
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::array<std::string, 3> texts =
|
||||
{
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
}
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
std::ostringstream compact;
|
||||
compact << d.root();
|
||||
CHECK(compact.str() == R"({"a":[1,2]})");
|
||||
std::ostringstream pretty;
|
||||
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
||||
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
||||
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("keys that differ in their last bytes are spread over the table")
|
||||
{
|
||||
// The hash must spread the usual families of keys (numbered names, hex
|
||||
// strings, dates, short names): a key farther than index_max_displacement
|
||||
// from its home slot would leave the object without a table. The keys
|
||||
// are placed with linear probing, as the table does.
|
||||
const auto max_displacement = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::size_t slots = 16;
|
||||
while (slots < 2 * keys.size())
|
||||
{
|
||||
slots *= 2;
|
||||
}
|
||||
std::vector<bool> used(slots, false);
|
||||
std::size_t farthest = 0;
|
||||
for (const std::string& key : keys)
|
||||
{
|
||||
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
|
||||
std::size_t distance = 0;
|
||||
while (used[i])
|
||||
{
|
||||
i = (i + 1) & (slots - 1);
|
||||
++distance;
|
||||
}
|
||||
used[i] = true;
|
||||
farthest = (std::max)(farthest, distance);
|
||||
}
|
||||
return farthest;
|
||||
};
|
||||
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
|
||||
// (a format string that is not a literal is a Flawfinder error)
|
||||
const auto numbered = [](const char* format, int count, int step)
|
||||
{
|
||||
const std::string pattern(format);
|
||||
const std::size_t percent = pattern.find('%');
|
||||
std::size_t conversion = percent + 1;
|
||||
std::size_t width = 0;
|
||||
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
|
||||
{
|
||||
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
|
||||
++conversion;
|
||||
}
|
||||
const bool hex = pattern[conversion] == 'x';
|
||||
std::vector<std::string> keys;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::ostringstream key;
|
||||
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
|
||||
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
|
||||
keys.push_back(key.str());
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
for (const char* format :
|
||||
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
|
||||
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
|
||||
})
|
||||
{
|
||||
CAPTURE(format)
|
||||
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
|
||||
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
std::set<std::string> distinct;
|
||||
// (there are 26 and 676 keys of one and two letters)
|
||||
std::size_t wanted = 700;
|
||||
if (length == 1)
|
||||
{
|
||||
wanted = 26;
|
||||
}
|
||||
else if (length == 2)
|
||||
{
|
||||
wanted = 500;
|
||||
}
|
||||
while (distinct.size() < wanted)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
distinct.insert(key);
|
||||
}
|
||||
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys of every length that differ in one byte")
|
||||
{
|
||||
// In a large object, a key is found by comparing its bytes with those of the member the
|
||||
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
|
||||
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
|
||||
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::vector<std::string> keys;
|
||||
std::set<std::string> distinct;
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (int copy = 0; copy < 5; ++copy)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
if (distinct.insert(key).second)
|
||||
{
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
CAPTURE(keys[i])
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
CHECK(v.at(keys[i]).get<std::size_t>() == i);
|
||||
CHECK(v.contains(keys[i]));
|
||||
for (std::size_t at = 0; at < keys[i].size(); ++at)
|
||||
{
|
||||
std::string other = keys[i];
|
||||
other[at] = other[at] == 'z' ? 'y' : 'z';
|
||||
CAPTURE(other)
|
||||
CHECK(v.contains(other) == (distinct.count(other) != 0));
|
||||
}
|
||||
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
|
||||
if (!keys[i].empty())
|
||||
{
|
||||
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view dump: escaping, nesting, and output buffers")
|
||||
{
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::array<std::string, 3> texts =
|
||||
{
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
}
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("the estimate for a value that is followed by many decoded strings")
|
||||
{
|
||||
// (the end of the value in the source is not known when the nodes after it have decoded strings)
|
||||
std::string text = "[7";
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
text += R"(,"\n")";
|
||||
}
|
||||
text += ",8]";
|
||||
const json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0].dump() == "7");
|
||||
CHECK(d.root()[1].dump() == R"("\n")");
|
||||
CHECK(d.root().dump() == json::parse(text).dump());
|
||||
// the same with a container
|
||||
const json_document nested = json_document::parse(R"([[7],)" + text.substr(3));
|
||||
CHECK(nested.root()[0].dump() == "[7]");
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the length of the run that ensure_ascii copies")
|
||||
{
|
||||
// (dump() only asks for strings with a byte to escape, so a run that ends the string is checked here)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{0}, std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{31}, std::size_t{32}, std::size_t{48}, std::size_t{50}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
// a run of printable characters, followed by a byte that is not copied and more text
|
||||
std::string text(length, 'a');
|
||||
text += "\"abcdefghijklmnopqrstuvwxyz";
|
||||
const auto* const bytes = reinterpret_cast<const unsigned char*>(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const auto* const end = bytes + text.size();
|
||||
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, end) == length);
|
||||
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 1, end) == length);
|
||||
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 5, end) == length);
|
||||
// the end of the buffer right after the run
|
||||
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, bytes + length) == length);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
std::ostringstream compact;
|
||||
compact << d.root();
|
||||
CHECK(compact.str() == R"({"a":[1,2]})");
|
||||
std::ostringstream pretty;
|
||||
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
||||
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
||||
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
||||
}
|
||||
}
|
||||
@@ -532,21 +532,25 @@ TEST_CASE("json_view edits: errors")
|
||||
|
||||
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), json_view_test::message("type_error.305", "/a", 21, 27, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), json_view_test::message("type_error.305", "/o", 6, 14, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index -1 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), json_view_test::message("type_error.308", "/o", 6, 14, "cannot use push_back() with object").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), json_view_test::message("type_error.309", "/n", 34, 35, "cannot use insert() with number").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 3 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), json_view_test::message("type_error.307", "/n", 34, 35, "cannot use erase() with number").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), json_view_test::message("type_error.307", "/o", 6, 14, "cannot use erase() with object").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), json_view_test::message("out_of_range.403", "", 0, 47, "key 'missing' not found").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root, json(json::value_t::discarded)), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
|
||||
// (an index that does not fit the document, in a null: basic_json would pad it with 4 Gi nulls)
|
||||
CHECK_THROWS_AS(d.set(json::json_pointer("/z/4294967295"), 1), json::out_of_range&);
|
||||
CHECK(root["z"].is_null());
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// invalid UTF-8 is rejected when it enters the document, with the error
|
||||
@@ -572,6 +576,19 @@ TEST_CASE("json_view edits: errors")
|
||||
static_cast<void>(other);
|
||||
}
|
||||
|
||||
TEST_CASE("json_view edits: a container that was added is replaced by a scalar")
|
||||
{
|
||||
// the view of an element that push_back() added is that of the new value itself, not of
|
||||
// the slot that links to it: replacing it by a scalar has to find the container it is in
|
||||
json_editable_document d = json_editable_document::parse(R"({"a": [1]})");
|
||||
d.push_back(d.root()["a"], json::array({2, 3}));
|
||||
d.push_back(d.root()["a"], json::object({{"k", json::array({4, 5})}}));
|
||||
d.set(d.root()["a"][1], 6);
|
||||
d.set(d.root()["a"][2]["k"], "s");
|
||||
CHECK(d.root().dump() == R"({"a":[1,6,{"k":"s"}]})");
|
||||
CHECK(d.root().materialize() == json::parse(R"({"a":[1,6,{"k":"s"}]})"));
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view edits: the size of a text arena")
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ using nlohmann::ordered_json_document;
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#endif
|
||||
|
||||
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose
|
||||
@@ -276,6 +277,17 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(true);
|
||||
}
|
||||
|
||||
SECTION("a map with enum keys from a view that is not an object")
|
||||
{
|
||||
// an object is read with the keys as the enum's names; any other
|
||||
// value is read as an array of pairs, like a map of any other key type
|
||||
const json_document d = json_document::parse(R"({"pairs": [["red", 1], ["green", 2]], "number": 1})");
|
||||
const json j = d.root().materialize();
|
||||
const json_view v = d.root();
|
||||
CHECK(same<std::map<color, int>>(v, j, "pairs"));
|
||||
CHECK((v["pairs"].get<std::map<color, int>>() == std::map<color, int> {{color::red, 1}, {color::green, 2}}));
|
||||
}
|
||||
|
||||
SECTION("std types: the same values as from the materialized value")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]],
|
||||
@@ -337,8 +349,7 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3);
|
||||
}
|
||||
|
||||
// (the exceptions of views have neither the path nor the byte range of the value)
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("std types: the same errors as from the materialized value")
|
||||
{
|
||||
const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])");
|
||||
@@ -403,10 +414,10 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(d.root()[4].get<strict_color>() == strict_color::red);
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
const json j = d.root().materialize();
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
|
||||
#if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
|
||||
#endif
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -443,6 +454,30 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(d.root().materialize().get<point>().x == 3);
|
||||
}
|
||||
|
||||
SECTION("escaped keys: members are found by their decoded names")
|
||||
{
|
||||
// keys are decoded before they are compared, as by parse(); a lookup
|
||||
// that compares the raw bytes of the source would miss these members
|
||||
const json_document d = json_document::parse(R"([{"\u0078": 1, "y": 2}, {"V\u0061lue": 3, "Te\u0078t": "t"}, {"\u0062": "escaped"}, {"x": 1, "\u0078": 3, "y": 2}])");
|
||||
const json j = d.root().materialize();
|
||||
CHECK(d.root()[0].get<point>().x == 1);
|
||||
CHECK(d.root()[0].get<point>().y == 2);
|
||||
CHECK((d.root()[0].get<point>() == j[0].get<point>()));
|
||||
CHECK(d.root()[1].get<with_names>().value == 3);
|
||||
CHECK(d.root()[1].get<with_names>().text == "t");
|
||||
CHECK(d.root()[2].get<with_defaults>().b == "escaped");
|
||||
CHECK(d.root()[2].get<with_defaults>().a == 1);
|
||||
// a duplicate key written once plain and once escaped: still the first member
|
||||
CHECK(d.root()[3].get<point>().x == 1);
|
||||
CHECK(j[3].get<point>().x == 3);
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
// an escaped backslash is part of the decoded key: "x\\" is not "x"
|
||||
const json_document e = json_document::parse(R"({"x\\": 1, "y": 2})");
|
||||
CHECK(exception_of_call([&] { static_cast<void>(e.root().get<point>()); }) == exception_of_call([&] { static_cast<void>(e.root().materialize().get<point>()); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(e.root().get<point>()); }) == "[json.exception.out_of_range.403] key 'x' not found");
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("editable documents and ordered_json")
|
||||
{
|
||||
json_editable_document e = json_editable_document::parse(R"({"x": 1, "y": 2})");
|
||||
@@ -452,11 +487,9 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(o.root().get<shape>().center.y == 2);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("errors are those of the same conversion of a json value")
|
||||
{
|
||||
// (with JSON_DIAGNOSTICS, those of json name the path of the value,
|
||||
// and with JSON_DIAGNOSTIC_POSITIONS its bytes)
|
||||
const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])");
|
||||
const json j = d.root().materialize();
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
@@ -469,5 +502,70 @@ TEST_CASE("json_view from_json")
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
namespace view_from_json
|
||||
{
|
||||
// a string_view member is not copied: it points into the source text or, for a
|
||||
// string with escape sequences, into the buffer of decoded strings of the document
|
||||
struct named_view
|
||||
{
|
||||
// initialized for GCC -Weffc++
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||
std::string_view name{};
|
||||
int n = 0;
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(named_view, name, n)
|
||||
};
|
||||
} // namespace view_from_json
|
||||
|
||||
TEST_CASE("json_view from_json: string_view members borrow from the document")
|
||||
{
|
||||
using view_from_json::named_view;
|
||||
|
||||
// whether all of s is inside the bytes of text
|
||||
const auto within = [](std::string_view s, std::string_view text)
|
||||
{
|
||||
const auto first = reinterpret_cast<std::uintptr_t>(text.data());
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(s.data());
|
||||
return begin >= first && begin + s.size() <= first + text.size();
|
||||
};
|
||||
|
||||
SECTION("a string without escapes points into the source text")
|
||||
{
|
||||
const std::string text = R"({"name": "hello", "n": 1})";
|
||||
const json_document d = json_document::parse(text);
|
||||
REQUIRE_FALSE(d.owns_source());
|
||||
const auto r = d.root().get<named_view>();
|
||||
CHECK(r.name == "hello");
|
||||
CHECK(r.n == 1);
|
||||
CHECK(within(r.name, text));
|
||||
CHECK(within(r.name, d.source()));
|
||||
}
|
||||
|
||||
SECTION("a string with escapes points into the document, not into the source text")
|
||||
{
|
||||
const std::string text = R"({"name": "a\nb", "n": 2})";
|
||||
json_document d = json_document::parse(text);
|
||||
REQUIRE_FALSE(d.owns_source());
|
||||
const auto r = d.root().get<named_view>();
|
||||
CHECK(r.name == "a\nb");
|
||||
CHECK(r.n == 2);
|
||||
CHECK_FALSE(within(r.name, text));
|
||||
// valid as long as the document is alive, even if the document is moved
|
||||
const json_document moved = std::move(d);
|
||||
CHECK(r.name == "a\nb");
|
||||
CHECK(moved.root().get<named_view>().name == r.name);
|
||||
}
|
||||
|
||||
SECTION("a document that owns its text keeps the members readable while it is alive")
|
||||
{
|
||||
const json_document d = json_document::parse(std::string(R"({"name": "owned", "n": 3})"));
|
||||
REQUIRE(d.owns_source());
|
||||
const auto r = d.root().get<named_view>();
|
||||
CHECK(r.name == "owned");
|
||||
CHECK(within(r.name, d.source()));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
DOCTEST_GCC_SUPPRESS_WARNING_POP
|
||||
@@ -819,6 +819,13 @@ TEST_CASE("json_view images: errors")
|
||||
static_cast<void>(d);
|
||||
}) == prefix + "too short");
|
||||
|
||||
// an image that is moved into the document is checked as well
|
||||
CHECK(exception_of([&]
|
||||
{
|
||||
const json_document d = json_document::load(std::vector<std::uint8_t>(10, 0));
|
||||
static_cast<void>(d);
|
||||
}) == prefix + "too short");
|
||||
|
||||
std::vector<std::uint8_t> bad = image;
|
||||
bad[0] = 'X';
|
||||
CHECK(load_result(bad, image_check::full) == prefix + "unknown format");
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
using json_view_test::generator;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), json_view_test::message("out_of_range.403", "", 0, text.size(), "key 'missing' not found").c_str(), json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("a lookup of a missing key stops at the end of the probe window")
|
||||
{
|
||||
// Every slot of the first half of the table holds a key, each at its home slot, so
|
||||
// that the table is built; a key that is not in the object but whose home slot is
|
||||
// the first slot finds a full window of slots with other keys.
|
||||
constexpr std::size_t members = 128;
|
||||
constexpr std::size_t table_size = 256; // the table size for 128 members: the next power of two >= 256
|
||||
for (const std::size_t prefix_length :
|
||||
{
|
||||
std::size_t{0}, std::size_t{20}
|
||||
})
|
||||
{
|
||||
CAPTURE(prefix_length)
|
||||
std::vector<std::string> keys(members);
|
||||
std::vector<bool> taken(members, false);
|
||||
std::string missing;
|
||||
std::size_t found = 0;
|
||||
for (std::uint64_t counter = 0; found < members || missing.empty(); ++counter)
|
||||
{
|
||||
std::string key(prefix_length, 'p');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key += static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const std::size_t slot = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (table_size - 1);
|
||||
if (slot < members && !taken[slot])
|
||||
{
|
||||
taken[slot] = true;
|
||||
keys[slot] = key;
|
||||
++found;
|
||||
}
|
||||
else if (slot == 0 && missing.empty())
|
||||
{
|
||||
missing = key;
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
}
|
||||
CHECK(!v.contains(missing));
|
||||
CHECK(v.find(missing) == v.end());
|
||||
CHECK(v.count(missing) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys that differ in their last bytes are spread over the table")
|
||||
{
|
||||
// The hash must spread the usual families of keys (numbered names, hex
|
||||
// strings, dates, short names): a key farther than index_max_displacement
|
||||
// from its home slot would leave the object without a table. The keys
|
||||
// are placed with linear probing, as the table does.
|
||||
const auto max_displacement = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::size_t slots = 16;
|
||||
while (slots < 2 * keys.size())
|
||||
{
|
||||
slots *= 2;
|
||||
}
|
||||
std::vector<bool> used(slots, false);
|
||||
std::size_t farthest = 0;
|
||||
for (const std::string& key : keys)
|
||||
{
|
||||
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
|
||||
std::size_t distance = 0;
|
||||
while (used[i])
|
||||
{
|
||||
i = (i + 1) & (slots - 1);
|
||||
++distance;
|
||||
}
|
||||
used[i] = true;
|
||||
farthest = (std::max)(farthest, distance);
|
||||
}
|
||||
return farthest;
|
||||
};
|
||||
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
|
||||
// (a format string that is not a literal is a Flawfinder error)
|
||||
const auto numbered = [](const char* format, int count, int step)
|
||||
{
|
||||
const std::string pattern(format);
|
||||
const std::size_t percent = pattern.find('%');
|
||||
std::size_t conversion = percent + 1;
|
||||
std::size_t width = 0;
|
||||
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
|
||||
{
|
||||
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
|
||||
++conversion;
|
||||
}
|
||||
const bool hex = pattern[conversion] == 'x';
|
||||
std::vector<std::string> keys;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::ostringstream key;
|
||||
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
|
||||
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
|
||||
keys.push_back(key.str());
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
for (const char* format :
|
||||
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
|
||||
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
|
||||
})
|
||||
{
|
||||
CAPTURE(format)
|
||||
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
|
||||
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
std::set<std::string> distinct;
|
||||
// (there are 26 and 676 keys of one and two letters)
|
||||
std::size_t wanted = 700;
|
||||
if (length == 1)
|
||||
{
|
||||
wanted = 26;
|
||||
}
|
||||
else if (length == 2)
|
||||
{
|
||||
wanted = 500;
|
||||
}
|
||||
while (distinct.size() < wanted)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
distinct.insert(key);
|
||||
}
|
||||
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys of every length that differ in one byte")
|
||||
{
|
||||
// In a large object, a key is found by comparing its bytes with those of the member the
|
||||
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
|
||||
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
|
||||
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::vector<std::string> keys;
|
||||
std::set<std::string> distinct;
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (int copy = 0; copy < 5; ++copy)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
if (distinct.insert(key).second)
|
||||
{
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
CAPTURE(keys[i])
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
CHECK(v.at(keys[i]).get<std::size_t>() == i);
|
||||
CHECK(v.contains(keys[i]));
|
||||
for (std::size_t at = 0; at < keys[i].size(); ++at)
|
||||
{
|
||||
std::string other = keys[i];
|
||||
other[at] = other[at] == 'z' ? 'y' : 'z';
|
||||
CAPTURE(other)
|
||||
CHECK(v.contains(other) == (distinct.count(other) != 0));
|
||||
}
|
||||
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
|
||||
if (!keys[i].empty())
|
||||
{
|
||||
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
#endif
|
||||
|
||||
// the view's own macros do not leak
|
||||
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) \
|
||||
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) || defined(NLOHMANN_VIEW_NOCLONE) \
|
||||
|| defined(NLOHMANN_VIEW_NODISCARD) || defined(NLOHMANN_VIEW_THROW) || defined(NLOHMANN_VIEW_HAS_CPP_17) || defined(NLOHMANN_VIEW_LITTLE_ENDIAN) \
|
||||
|| defined(NLOHMANN_VIEW_REPEAT16)
|
||||
#error "json_view.hpp leaks a macro"
|
||||
|
||||
@@ -20,6 +20,7 @@ using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_editable_document;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
@@ -81,6 +82,112 @@ std::string range(const json& j)
|
||||
{
|
||||
return j.start_pos() == npos && j.end_pos() == npos ? "none" : std::to_string(j.start_pos()) + "-" + std::to_string(j.end_pos());
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
using json_view_test::exception_of_call;
|
||||
|
||||
// the exceptions about every value of v are those about the value at the same
|
||||
// place in j, a value of the whole document from parse() or materialize() (so
|
||||
// that its parents and positions are set); members whose key repeats are
|
||||
// skipped (the view finds the first one, j holds the last)
|
||||
template<typename View>
|
||||
void check_exceptions(const View& v, const json& j)
|
||||
{
|
||||
CAPTURE(j.dump())
|
||||
if (!v.is_number())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.template get<int>()); }) == exception_of_call([&] { static_cast<void>(j.get<int>()); }));
|
||||
}
|
||||
if (!v.is_string())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::string>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::string>()); }));
|
||||
}
|
||||
if (!v.is_array())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::vector<int>>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::vector<int>>()); }));
|
||||
}
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.at("missing")); }) == exception_of_call([&] { static_cast<void>(j.at("missing")); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.at(1000)); }) == exception_of_call([&] { static_cast<void>(j.at(1000)); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/missing"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/missing"))); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/-"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/-"))); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.value("k", 0)); }) == exception_of_call([&] { static_cast<void>(j.value("k", 0)); }));
|
||||
if (!v.is_object())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v["k"]); }) == exception_of_call([&] { static_cast<void>(j["k"]); }));
|
||||
// (without the context of basic_json: view iterators do not know the value they iterate over)
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.begin().key()); }) == "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators");
|
||||
}
|
||||
if (!v.is_array())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v[0]); }) == exception_of_call([&] { static_cast<void>(j[0]); }));
|
||||
}
|
||||
if (v.is_null())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.front()); }) == exception_of_call([&] { static_cast<void>(j.front()); }));
|
||||
}
|
||||
if (v.is_object())
|
||||
{
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
const std::string key(it.key().data(), it.key().size());
|
||||
if (v.count(key) == 1)
|
||||
{
|
||||
check_exceptions(it.value(), j.at(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (v.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < v.size(); ++i)
|
||||
{
|
||||
check_exceptions(v[i], j[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// as check_exceptions, for edits that fail (they change neither d nor j)
|
||||
void check_edit_exceptions(json_editable_document& d, const json_editable_document::view_type& v, json& j)
|
||||
{
|
||||
CAPTURE(j.dump())
|
||||
if (!v.is_object() && !v.is_null())
|
||||
{
|
||||
CHECK(exception_of_call([&] { d.set(v, "k", 1); }) == exception_of_call([&] { j["k"] = 1; }));
|
||||
CHECK(exception_of_call([&] { d.erase(v, "k"); }) == exception_of_call([&] { j.erase("k"); }));
|
||||
}
|
||||
if (!v.is_array() && !v.is_null())
|
||||
{
|
||||
CHECK(exception_of_call([&] { d.set(v, 0, 1); }) == exception_of_call([&] { j[0] = 1; }));
|
||||
CHECK(exception_of_call([&] { d.push_back(v, 1); }) == exception_of_call([&] { j.push_back(1); }));
|
||||
}
|
||||
if (!v.is_array())
|
||||
{
|
||||
CHECK(exception_of_call([&] { d.insert(v, 0, 1); }) == exception_of_call([&] { j.insert(j.cbegin(), 1); }));
|
||||
CHECK(exception_of_call([&] { d.erase(v, 0); }) == exception_of_call([&] { j.erase(0); }));
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(exception_of_call([&] { d.erase(v, 1000); }) == exception_of_call([&] { j.erase(1000); }));
|
||||
}
|
||||
if (v.is_object())
|
||||
{
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
const std::string key(it.key().data(), it.key().size());
|
||||
if (v.count(key) == 1)
|
||||
{
|
||||
check_edit_exceptions(d, it.value(), j.at(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (v.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < v.size(); ++i)
|
||||
{
|
||||
check_edit_exceptions(d, v[i], j[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view materialize() source positions")
|
||||
@@ -194,6 +301,27 @@ TEST_CASE("json_view materialize() source positions")
|
||||
static_cast<void>(j.at("list").at(1).get<int>());
|
||||
}));
|
||||
}
|
||||
|
||||
SECTION("exception about the root value (#5987)")
|
||||
{
|
||||
const json_document root = json_document::parse("true");
|
||||
const json_document nested = json_document::parse("[1, true]");
|
||||
CHECK(json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(root.root().get<std::string>());
|
||||
}) == "[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean");
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK(json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(nested.root()[1].get<std::string>());
|
||||
}) == "[json.exception.type_error.302] (/1) (bytes 4-8) type must be string, but is boolean");
|
||||
#else
|
||||
CHECK(json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(nested.root()[1].get<std::string>());
|
||||
}) == "[json.exception.type_error.302] (bytes 4-8) type must be string, but is boolean");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("edited documents")
|
||||
@@ -321,6 +449,31 @@ TEST_CASE("json_view materialize() source positions")
|
||||
CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2]));
|
||||
}
|
||||
|
||||
SECTION("a damaged text: the end of a container is not found")
|
||||
{
|
||||
// (the text of an image is not checked with image_check::none)
|
||||
const std::string array_text = "[1,2]";
|
||||
const std::vector<std::uint8_t> original = json_document::parse(array_text).save();
|
||||
const auto at = std::search(original.begin(), original.end(), array_text.begin(), array_text.end());
|
||||
REQUIRE(at != original.end());
|
||||
const auto closing = static_cast<std::size_t>(at - original.begin()) + array_text.size() - 1;
|
||||
// not a closing bracket, a slash that starts no comment, and the end of the text
|
||||
for (const char replacement :
|
||||
{
|
||||
'x', '/', ' '
|
||||
})
|
||||
{
|
||||
CAPTURE(replacement)
|
||||
std::vector<std::uint8_t> damaged = original;
|
||||
damaged[closing] = static_cast<std::uint8_t>(replacement);
|
||||
const json_document d = json_document::load(damaged, json_document::image_check::none);
|
||||
const json m = d.root().materialize();
|
||||
CHECK(range(m) == "none");
|
||||
CHECK(range(m[0]) == "1-2");
|
||||
CHECK(range(m[1]) == "3-4");
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("a decoded string's position must be within the text")
|
||||
{
|
||||
@@ -345,3 +498,164 @@ TEST_CASE("json_view materialize() source positions")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view exceptions name the path and the bytes of the value")
|
||||
{
|
||||
SECTION("as those of basic_json")
|
||||
{
|
||||
const std::vector<std::string> texts =
|
||||
{
|
||||
"null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})",
|
||||
R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})",
|
||||
R"({"a\u0000b": [1, "x"], "c": {"d\u0000": 2}})", // (the path of a key ends at a null byte, as in basic_json)
|
||||
R"([1 /* ] */, {"a": [] // }
|
||||
, "b": {} } , ])",
|
||||
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
|
||||
};
|
||||
for (const auto& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const json j = json::parse(text, nullptr, true, true, true);
|
||||
const json_document d = json_document::parse(text, true, true, true);
|
||||
check_exceptions(d.root(), j);
|
||||
json_editable_document e = json_editable_document::parse(text, true, true, true);
|
||||
check_exceptions(e.root(), j);
|
||||
json m = e.root().materialize();
|
||||
check_edit_exceptions(e, e.root(), m);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("generated documents")
|
||||
{
|
||||
json_view_test::generator gen;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
std::string text;
|
||||
gen.value(text, 0);
|
||||
CAPTURE(text)
|
||||
const json_document d = json_document::parse(text);
|
||||
check_exceptions(d.root(), json::parse(text));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("edited documents: as those of materialize()")
|
||||
{
|
||||
const std::string text = R"({"a": [1, 2], "b": {"c": "\n", "d": true}, "e": [3, [4]], "f": "x"})";
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
d.set(d.root()["f"], 5);
|
||||
d.push_back(d.root()["a"], json::parse(R"({"n": [null]})"));
|
||||
d.erase(d.root()["e"], 0);
|
||||
d.set(d.root()["b"], "c2", d.root()["e"]);
|
||||
d.insert(d.root()["e"], 0, "new");
|
||||
json m = d.root().materialize();
|
||||
check_exceptions(d.root(), m);
|
||||
check_edit_exceptions(d, d.root(), m);
|
||||
// (a value inside a new container: a path, but no bytes)
|
||||
std::string expected = "[json.exception.type_error.302] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected += "(/a/2/n/0) ";
|
||||
#endif
|
||||
expected += "type must be number, but is null";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"][2]["n"][0].get<int>()); }) == expected);
|
||||
}
|
||||
|
||||
SECTION("views of values that are no longer part of the document, and of no value")
|
||||
{
|
||||
const std::string expected_string = "[json.exception.type_error.302] type must be number, but is string";
|
||||
static_cast<void>(expected_string);
|
||||
// (a value that is not found has no path, and with it no bytes)
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"(["x", ["y"]])");
|
||||
const auto removed = d.root()[0];
|
||||
d.erase(d.root(), 0);
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
|
||||
#endif
|
||||
// (the root is a scalar now: the value cannot be in it)
|
||||
d.set(d.root(), 5);
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
|
||||
#endif
|
||||
CHECK(d.root().get<int>() == 5);
|
||||
}
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"(["x"])");
|
||||
const auto removed = d.root()[0];
|
||||
d.set(d.root(), 5);
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
|
||||
#endif
|
||||
}
|
||||
// a view of no value has no context
|
||||
CHECK(exception_of_call([&] { static_cast<void>(nlohmann::json_view().get<int>()); }) == "[json.exception.type_error.302] type must be number, but is discarded");
|
||||
}
|
||||
|
||||
SECTION("an array whose last element was replaced by a new container has no bytes")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"({"a": [1, [2]], "b": 1})");
|
||||
d.set(d.root()["a"][1], json::array({3}));
|
||||
std::string expected = "[json.exception.type_error.302] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected += "(/a) ";
|
||||
#endif
|
||||
expected += "type must be number, but is array";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"].get<int>()); }) == expected);
|
||||
}
|
||||
|
||||
SECTION("deep documents with siblings after every nested container")
|
||||
{
|
||||
// the search for the path pushes and pops one frame per level (the
|
||||
// stack reallocates while it grows, and shrinks again at every
|
||||
// sibling): run under a sanitizer, a reference to a popped or moved
|
||||
// frame would show here
|
||||
std::string text = "7";
|
||||
for (int level = 0; level < 40; ++level)
|
||||
{
|
||||
text.insert(0, R"([[1, [2]], )");
|
||||
text += R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
|
||||
}
|
||||
CAPTURE(text)
|
||||
const json j = json::parse(text);
|
||||
const json_document d = json_document::parse(text);
|
||||
check_exceptions(d.root(), j);
|
||||
json_editable_document e = json_editable_document::parse(text);
|
||||
e.set(e.root()[0][0], 9);
|
||||
check_exceptions(e.root(), e.root().materialize());
|
||||
}
|
||||
|
||||
SECTION("examples")
|
||||
{
|
||||
const std::string text = R"({"people": [{"name": "a", "age": 1}, {"name": "c", "age": "x"}], "o": {"k": 1}})";
|
||||
const json_document d = json_document::parse(text);
|
||||
std::string expected_age = "[json.exception.type_error.302] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected_age += "(/people/1/age) ";
|
||||
#endif
|
||||
expected_age += "(bytes 58-61) type must be number, but is string";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()["people"][1]["age"].get<int>()); }) == expected_age);
|
||||
|
||||
std::string expected_zz = "[json.exception.out_of_range.403] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected_zz += "(/people/0) ";
|
||||
#endif
|
||||
expected_zz += "(bytes 12-35) key 'zz' not found";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz);
|
||||
|
||||
// the root has no path, but still its bytes (#5987)
|
||||
const std::string expected_missing = "[json.exception.out_of_range.403] (bytes 0-79) key 'missing' not found";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the value the view finds")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"x": "1", "x": "22"})");
|
||||
std::string expected = "[json.exception.type_error.302] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected += "(/x) ";
|
||||
#endif
|
||||
expected += "(bytes 6-9) type must be number, but is string";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()["x"].get<int>()); }) == expected);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -12,7 +12,10 @@
|
||||
using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
TEST_CASE("tests on very large JSONs")
|
||||
{
|
||||
@@ -140,6 +143,56 @@ TEST_CASE("tests on deeply nested JSONs")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("leaf values of every type below the bound of the recursive descent")
|
||||
{
|
||||
// the elements and members of the containers below the bound are
|
||||
// copied one by one, whatever their type
|
||||
const json leaves = json::array(
|
||||
{
|
||||
nullptr, true, false, -1, std::uint64_t{1}, 1.5, "text", json::binary({1, 2, 3})
|
||||
});
|
||||
const json members =
|
||||
{
|
||||
{"null", nullptr}, {"boolean", true}, {"integer", -1}, {"unsigned", std::uint64_t{1}},
|
||||
{"float", 1.5}, {"string", "text"}, {"binary", json::binary({1, 2, 3})}
|
||||
};
|
||||
|
||||
for (const bool objects :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
CAPTURE(objects)
|
||||
|
||||
json j = objects ? members : leaves;
|
||||
for (std::size_t i = 0; i < 300; ++i)
|
||||
{
|
||||
json wrapper = objects ? json::object() : json::array();
|
||||
if (objects)
|
||||
{
|
||||
wrapper["a"] = std::move(j);
|
||||
}
|
||||
else
|
||||
{
|
||||
wrapper.push_back(std::move(j));
|
||||
}
|
||||
j = std::move(wrapper);
|
||||
}
|
||||
|
||||
const json copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
|
||||
|
||||
const json* current = ©
|
||||
for (std::size_t i = 0; i < 300; ++i)
|
||||
{
|
||||
current = objects ? ¤t->at("a") : ¤t->front();
|
||||
}
|
||||
|
||||
// the innermost value is shallow: it is safe to compare
|
||||
CHECK(*current == (objects ? members : leaves));
|
||||
CHECK(current->size() == (objects ? members.size() : leaves.size()));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a value that is deep in one place only")
|
||||
{
|
||||
json j = json::object();
|
||||
|
||||
@@ -397,6 +397,14 @@ TEST_CASE("locale with a multi-byte decimal point")
|
||||
|
||||
// a value the locale-independent paths convert is not affected
|
||||
CHECK(json::parse("12.5") == 12.5);
|
||||
|
||||
// dump() of a long double that takes the snprintf path replaces the
|
||||
// whole decimal point, not only its first byte (#5993)
|
||||
CHECK(long_double_json(1.5L).dump() == "1.5");
|
||||
CHECK(long_double_json(2.0L).dump() == "2.0");
|
||||
CHECK(long_double_json(-0.25L).dump() == "-0.25");
|
||||
CHECK(long_double_json::parse(long_double_json(1.5e-7L).dump()) == long_double_json(1.5e-7L));
|
||||
CHECK(long_double_json::parse(ld.dump()) == expected_long_double);
|
||||
}
|
||||
if (!tested)
|
||||
{
|
||||
@@ -495,3 +503,206 @@ TEST_CASE("locale changes during a single dump() (#5709 item 3)")
|
||||
// fix #5597 made on the parser side
|
||||
CHECK(buf.data == "[\"" + padding + "\",1234.5]");
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// a token whose first data() call switches LC_NUMERIC, so that the locale
|
||||
// changes between convert_float_locale_aware() looking up the decimal point
|
||||
// and strtod reading the token (the race of #5198, made deterministic)
|
||||
struct locale_switching_token
|
||||
{
|
||||
using value_type = char;
|
||||
|
||||
locale_switching_token(std::string text_, std::string switch_to_)
|
||||
: text(std::move(text_)), switch_to(std::move(switch_to_))
|
||||
{}
|
||||
|
||||
std::size_t size() const
|
||||
{
|
||||
return text.size();
|
||||
}
|
||||
|
||||
char& operator[](std::size_t i)
|
||||
{
|
||||
return text[i];
|
||||
}
|
||||
|
||||
char* data()
|
||||
{
|
||||
if (!switched)
|
||||
{
|
||||
switched = std::setlocale(LC_NUMERIC, switch_to.c_str()) != nullptr;
|
||||
}
|
||||
return &text[0]; // NOLINT(readability-container-data-pointer): std::string::data() is const before C++17
|
||||
}
|
||||
|
||||
std::string text;
|
||||
std::string switch_to;
|
||||
bool switched = false;
|
||||
};
|
||||
|
||||
// set LC_NUMERIC to the first of @a names that is usable and has a decimal
|
||||
// point of @a min_size bytes at least (and at most @a max_size)
|
||||
bool use_decimal_point_locale(const std::vector<const char*>& names, std::size_t min_size, std::size_t max_size, const char*& used)
|
||||
{
|
||||
for (const char* name : names)
|
||||
{
|
||||
if (std::setlocale(LC_NUMERIC, name) == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t size = std::string(std::localeconv()->decimal_point).size();
|
||||
if (size >= min_size && size <= max_size)
|
||||
{
|
||||
used = name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// convert a token with convert_float_locale_aware(); the token must be given
|
||||
// back as it was
|
||||
template<typename FloatType>
|
||||
FloatType convert_locale_aware(const std::string& text)
|
||||
{
|
||||
std::string token = text;
|
||||
const std::size_t dot = token.find('.');
|
||||
FloatType value{};
|
||||
nlohmann::detail::convert_float_locale_aware(token, dot, value);
|
||||
CHECK(token == text);
|
||||
return value;
|
||||
}
|
||||
|
||||
template<typename FloatType>
|
||||
void check_locale_aware_conversions()
|
||||
{
|
||||
CHECK(convert_locale_aware<FloatType>("3.5") == FloatType(3.5));
|
||||
CHECK(convert_locale_aware<FloatType>("-0.25") == FloatType(-0.25));
|
||||
CHECK(convert_locale_aware<FloatType>("1.25e2") == FloatType(125));
|
||||
// no decimal point at all
|
||||
CHECK(convert_locale_aware<FloatType>("12e1") == FloatType(120));
|
||||
CHECK(convert_locale_aware<FloatType>("7") == FloatType(7));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("convert_float_locale_aware")
|
||||
{
|
||||
const std::vector<const char*> single_byte_names = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", "fr_FR", "ru_RU.UTF-8"};
|
||||
const std::vector<const char*> multi_byte_names = {"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"};
|
||||
const char* used = nullptr;
|
||||
|
||||
SECTION("decimal point '.'")
|
||||
{
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
check_locale_aware_conversions<float>();
|
||||
check_locale_aware_conversions<double>();
|
||||
check_locale_aware_conversions<long double>();
|
||||
}
|
||||
|
||||
SECTION("single-byte decimal point other than '.'")
|
||||
{
|
||||
if (use_decimal_point_locale(single_byte_names, 1, 1, used) && std::localeconv()->decimal_point[0] != '.')
|
||||
{
|
||||
CAPTURE(used)
|
||||
check_locale_aware_conversions<float>();
|
||||
check_locale_aware_conversions<double>();
|
||||
check_locale_aware_conversions<long double>();
|
||||
}
|
||||
else
|
||||
{
|
||||
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("multi-byte decimal point")
|
||||
{
|
||||
if (use_decimal_point_locale(multi_byte_names, 2, 8, used))
|
||||
{
|
||||
CAPTURE(used)
|
||||
check_locale_aware_conversions<float>();
|
||||
check_locale_aware_conversions<double>();
|
||||
check_locale_aware_conversions<long double>();
|
||||
}
|
||||
else
|
||||
{
|
||||
MESSAGE("no locale with a multi-byte decimal point is usable");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a token that is not converted completely")
|
||||
{
|
||||
// the locale does not change, so there is nothing to retry: the
|
||||
// value up to the garbage is kept
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
std::string token = "1.5x";
|
||||
double value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, token.find('.'), value);
|
||||
CHECK(value == 1.5);
|
||||
CHECK(token == "1.5x");
|
||||
}
|
||||
|
||||
SECTION("the locale changes before the conversion")
|
||||
{
|
||||
if (!use_decimal_point_locale(single_byte_names, 1, 1, used) || std::localeconv()->decimal_point[0] == '.')
|
||||
{
|
||||
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string comma_locale = used;
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
|
||||
// "C" -> comma locale: strtod stops at the '.', so the conversion is repeated
|
||||
{
|
||||
locale_switching_token token("1.5", comma_locale);
|
||||
double value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
||||
if (token.switched)
|
||||
{
|
||||
CHECK(value == 1.5);
|
||||
CHECK(token.text == "1.5");
|
||||
}
|
||||
}
|
||||
|
||||
// the same for the other floating-point types
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
{
|
||||
locale_switching_token token("2.25", comma_locale);
|
||||
float value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
||||
if (token.switched)
|
||||
{
|
||||
CHECK(value == 2.25f);
|
||||
}
|
||||
}
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
{
|
||||
locale_switching_token token("2.25", comma_locale);
|
||||
long double value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
||||
if (token.switched)
|
||||
{
|
||||
CHECK(value == 2.25L);
|
||||
}
|
||||
}
|
||||
|
||||
// comma locale -> multi-byte decimal point
|
||||
const char* multi_byte = nullptr;
|
||||
if (use_decimal_point_locale(multi_byte_names, 2, 8, multi_byte))
|
||||
{
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, comma_locale.c_str()) != nullptr);
|
||||
locale_switching_token token("1.5", multi_byte);
|
||||
double value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
||||
if (token.switched)
|
||||
{
|
||||
CHECK(value == 1.5);
|
||||
CHECK(token.text == "1.5");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ using nlohmann::json;
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Test suite for verifying MessagePack handling with std::byte input
|
||||
@@ -96,4 +97,34 @@ TEST_CASE("MessagePack with std::byte")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JSON text (not MessagePack) as std::byte input: the error position is
|
||||
// computed from the bytes consumed so far
|
||||
TEST_CASE("JSON text with std::byte")
|
||||
{
|
||||
const auto to_bytes = [](const std::string & text)
|
||||
{
|
||||
std::vector<std::byte> bytes;
|
||||
for (const char c : text)
|
||||
{
|
||||
bytes.push_back(static_cast<std::byte>(c));
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
SECTION("valid text")
|
||||
{
|
||||
const json j = json::parse(to_bytes("[1,\n2]"));
|
||||
CHECK(j == json({1, 2}));
|
||||
CHECK(json::accept(to_bytes("[1,\n2]")));
|
||||
}
|
||||
|
||||
SECTION("invalid text")
|
||||
{
|
||||
CHECK_FALSE(json::accept(to_bytes("[1,\n2,")));
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::parse(to_bytes("[1,\n2,")),
|
||||
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal", json::parse_error&);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -27,12 +27,15 @@ using nlohmann::json;
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "round_trip_corpus.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "sax_countdown.hpp"
|
||||
#include "sax_event_loggers.hpp"
|
||||
using utils::SaxCountdown;
|
||||
using utils::SaxEventLogger;
|
||||
|
||||
|
||||
TEST_CASE("MessagePack")
|
||||
@@ -2212,3 +2215,127 @@ TEST_CASE("MessagePack large strings and binaries (chunked reader)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("MessagePack arrays of scalars from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input reads the integers, floats, booleans, and nils an
|
||||
// array begins with in one loop, other input value by value; both must
|
||||
// give the same events, value, or error for the complete document and for
|
||||
// every truncation of it, and stop at every event a SAX handler rejects
|
||||
const std::vector<std::uint8_t> msgpack =
|
||||
{
|
||||
0x83, // map(3)
|
||||
0xA1, 'a', // "a"
|
||||
0xDC, 0x00, 0x16, // array 16 (22)
|
||||
0x00, 0x7F, 0xCC, 0xFF, 0xCD, 0x01, 0x00, // 0, 127, 255, 256
|
||||
0xCE, 0x00, 0x01, 0x00, 0x00, // 65536
|
||||
0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 18446744073709551615
|
||||
0xFF, 0xE0, 0xD0, 0x80, 0xD1, 0xFF, 0x00, // -1, -32, -128, -256
|
||||
0xD2, 0xFF, 0xFE, 0xFF, 0xFF, // -65537
|
||||
0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -9223372036854775808
|
||||
0xC2, 0xC3, 0xC0, // false, true, nil
|
||||
0xCA, 0x3F, 0xC0, 0x00, 0x00, // 1.5 (float 32)
|
||||
0xCB, 0xBF, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, // -0.1
|
||||
0xA1, 'x', // "x" (ends the loop)
|
||||
0x01, // 1
|
||||
0xCB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2.5
|
||||
0xD9, 0x01, 'y', // "y" (str 8)
|
||||
0xC0, // nil
|
||||
0xA1, 'b', // "b"
|
||||
0x93, // fixarray(3)
|
||||
0x92, 0xCB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
|
||||
0xCB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
|
||||
0x92, 0xCB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
|
||||
0xCB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
|
||||
0x90, // []
|
||||
0xA1, 'c', // "c"
|
||||
0x94, 0x80, 0x01, 0xC4, 0x01, 0x05, 0x91, 0x03 // [{}, 1, bin 8, [3]]
|
||||
};
|
||||
|
||||
const json j = json::from_msgpack(msgpack);
|
||||
CHECK(j["a"].size() == 22);
|
||||
CHECK(j["a"][5] == 18446744073709551615u);
|
||||
CHECK(j["a"][7] == -32);
|
||||
CHECK(j["a"][11] == (std::numeric_limits<std::int64_t>::min)());
|
||||
CHECK(j["a"][15] == 1.5);
|
||||
CHECK(j["a"][16] == -0.1);
|
||||
CHECK(j["a"][17] == "x");
|
||||
CHECK(j["b"] == json::parse("[[1.25, -3.75], [1.25, -3.75], []]"));
|
||||
CHECK(j["c"][2].is_binary());
|
||||
|
||||
for (std::size_t length = 0; length <= msgpack.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(msgpack.begin(), msgpack.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_msgpack(contiguous, true, false).dump() == json::from_msgpack(bytewise.begin(), bytewise.end(), true, false).dump());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string contiguous_error;
|
||||
std::string bytewise_error;
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_msgpack(contiguous);
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
contiguous_error = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_msgpack(bytewise.begin(), bytewise.end());
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
bytewise_error = e.what();
|
||||
}
|
||||
CHECK(contiguous_error == bytewise_error);
|
||||
#endif
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::msgpack)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
|
||||
for (int events = 0; events <= 60; ++events)
|
||||
{
|
||||
CAPTURE(events)
|
||||
const std::list<std::uint8_t> bytewise(msgpack.begin(), msgpack.end());
|
||||
SaxCountdown from_contiguous(events);
|
||||
SaxCountdown from_bytewise(events);
|
||||
CHECK(json::sax_parse(msgpack, &from_contiguous, json::input_format_t::msgpack)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("MessagePack arrays of one element from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input leaves an array of one element to the general path
|
||||
// rather than to the loop that reads arrays of scalars; both inputs must
|
||||
// still give the same events, value, or error for the complete document
|
||||
// and for every truncation of it, with enough bytes after each array that
|
||||
// the loop could have been entered
|
||||
const json j = json::parse(R"({"a": [[[[1.5]]]], "b": [7], "c": [{"x": 2.5}], "d": ["s"], "e": [[1, 2]], "f": [null], "g": 1.25})");
|
||||
const std::vector<std::uint8_t> bytes = json::to_msgpack(j);
|
||||
|
||||
CHECK(json::from_msgpack(bytes) == j);
|
||||
|
||||
for (std::size_t length = 0; length <= bytes.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_msgpack(contiguous, true, false).dump() == json::from_msgpack(bytewise.begin(), bytewise.end(), true, false).dump());
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::msgpack, true, false)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack, true, false));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using nlohmann::ordered_json;
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
TEST_CASE("ordered_json")
|
||||
{
|
||||
@@ -515,3 +516,89 @@ TEST_CASE("parsing random trees of arrays and objects")
|
||||
CHECK(json::parse(text).dump() == expected.dump());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("object_key_index")
|
||||
{
|
||||
// the hash table of the DOM parser finds the position of a key in an
|
||||
// ordered_json object; the parser creates it for exactly 64 elements, but
|
||||
// it is correct for any number of elements it is given
|
||||
using nlohmann::detail::object_key_index;
|
||||
const std::size_t not_found = object_key_index::npos;
|
||||
|
||||
for (const std::size_t initial :
|
||||
{
|
||||
std::size_t{0}, std::size_t{10}, std::size_t{64}, std::size_t{65}, std::size_t{300}
|
||||
})
|
||||
{
|
||||
CAPTURE(initial)
|
||||
|
||||
// the hashes are not distinct on purpose: elements with the same
|
||||
// hash are told apart by the caller
|
||||
const auto hash_of = [](std::size_t position) -> std::size_t
|
||||
{
|
||||
// truncated to 32 bits where std::size_t is that small
|
||||
return static_cast<std::size_t>((position % 50) * 0x9E3779B97F4A7C15ull);
|
||||
};
|
||||
|
||||
std::vector<std::size_t> hashes;
|
||||
hashes.reserve(initial);
|
||||
for (std::size_t i = 0; i < initial; ++i)
|
||||
{
|
||||
hashes.push_back(hash_of(i));
|
||||
}
|
||||
object_key_index index(std::move(hashes));
|
||||
|
||||
// the elements that are there
|
||||
for (std::size_t i = 0; i < initial; ++i)
|
||||
{
|
||||
CHECK(index.find(hash_of(i), [i](std::size_t position)
|
||||
{
|
||||
return position == i;
|
||||
}) == i);
|
||||
}
|
||||
CHECK(index.find(hash_of(1), [](std::size_t /*position*/)
|
||||
{
|
||||
return false;
|
||||
}) == not_found);
|
||||
|
||||
// elements that are added later, until the table grows
|
||||
for (std::size_t i = initial; i < initial + 600; ++i)
|
||||
{
|
||||
index.add(hash_of(i));
|
||||
CHECK(index.find(hash_of(i), [i](std::size_t position)
|
||||
{
|
||||
return position == i;
|
||||
}) == i);
|
||||
}
|
||||
CHECK(index.find(hash_of(initial), [initial](std::size_t position)
|
||||
{
|
||||
return position == initial;
|
||||
}) == initial);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON pointers on ordered_json")
|
||||
{
|
||||
using pointer = ordered_json::json_pointer;
|
||||
|
||||
SECTION("operator[] inserts a missing key at the end")
|
||||
{
|
||||
ordered_json j = {{"z", 1}, {"x", {{"y", 2}}}};
|
||||
|
||||
j[pointer("/a")] = 3;
|
||||
j[pointer("/x/b")] = 4;
|
||||
|
||||
CHECK(j == ordered_json({{"z", 1}, {"x", {{"y", 2}, {"b", 4}}}, {"a", 3}}));
|
||||
CHECK(j.dump() == R"({"z":1,"x":{"y":2,"b":4},"a":3})");
|
||||
}
|
||||
|
||||
SECTION("unflatten of a flattened object whose keys are not sorted")
|
||||
{
|
||||
// the prefixes /a and /c denote arrays; a key of /a follows one of /c
|
||||
const ordered_json flat = {{"/a/0/p", 1}, {"/c/0/x", 2}, {"/a/0/q", 3}, {"/a/1/p", 4}};
|
||||
const ordered_json expected = {{"a", {{{"p", 1}, {"q", 3}}, {{"p", 4}}}}, {"c", {{{"x", 2}}}}};
|
||||
|
||||
CHECK(flat.unflatten() == expected);
|
||||
CHECK(expected.flatten().unflatten() == expected);
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,13 @@ using nlohmann::json;
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <list>
|
||||
#include <limits>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
@@ -1519,6 +1521,76 @@ TEST_CASE("regression tests 1")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("operator[](json_pointer) continues with the member that emplace() found")
|
||||
{
|
||||
// see the section above: the lookup does not find "k", but emplace()
|
||||
// finds "x" and the pointer continues with the member of "x". Here
|
||||
// the member is a container, so the pointer can go on below it. Only
|
||||
// the outermost object may be a copy (with restarted timestamps): the
|
||||
// value of "x" is moved in, so that its own lookups work.
|
||||
const my_json orig = my_json::parse(R"({"x":null})");
|
||||
const auto make = [&orig]()
|
||||
{
|
||||
my_json::object_t top = orig.get_ref<const my_json::object_t&>();
|
||||
top.at("x") = my_json::parse(R"({"a":{"b":1},"n":null,"arr":[10,20],"objs":[{"m":5},{"m":6}]})");
|
||||
return my_json(std::move(top)); // NOLINT(modernize-return-braced-init-list): {top} would be an array
|
||||
};
|
||||
|
||||
SECTION("existing members")
|
||||
{
|
||||
// emplace() registers the key "k" in the object, so that every
|
||||
// pointer needs a new object to reach the member of "x" again
|
||||
const std::array<std::pair<const char*, const char*>, 4> pointers = {{{"/k/a/b", "1"}, {"/k/arr/1", "20"}, {"/k/n", "null"}, {"/k/objs/1/m", "6"}}};
|
||||
for (const auto& pointer : pointers)
|
||||
{
|
||||
CAPTURE(pointer.first)
|
||||
my_json j = make();
|
||||
CHECK(j[my_json::json_pointer(pointer.first)].dump() == pointer.second);
|
||||
CHECK(j.size() == 1);
|
||||
CHECK(j["x"].size() == 4);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a missing member of an object")
|
||||
{
|
||||
my_json j = make();
|
||||
#ifdef JSON_NO_THREAD_LOCAL
|
||||
// (set by the user, or by the library for clang with MinGW)
|
||||
// the objects below the outermost one are not copied by their
|
||||
// object_t here, but member by member, which keeps their
|
||||
// timestamps correct: "c" is not mistaken for "b" and is inserted
|
||||
CHECK(j[my_json::json_pointer("/k/a/c/d")].is_null());
|
||||
CHECK(j["x"]["a"].dump() == R"({"b":1,"c":{"d":null}})");
|
||||
#else
|
||||
// the timestamp of "c" is that of "b" again, so that emplace()
|
||||
// finds the member "b", a number, which has no member "d"
|
||||
CHECK_THROWS_AS(j[my_json::json_pointer("/k/a/c/d")], my_json::out_of_range&);
|
||||
CHECK(j["x"]["a"].dump() == R"({"b":1})");
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("a null value is converted to an object")
|
||||
{
|
||||
my_json j = make();
|
||||
j[my_json::json_pointer("/k/n/c")] = 3;
|
||||
CHECK(j["x"]["n"].dump() == R"({"c":3})");
|
||||
}
|
||||
|
||||
SECTION("the end of an array")
|
||||
{
|
||||
my_json j = make();
|
||||
j[my_json::json_pointer("/k/arr/-")] = 30;
|
||||
CHECK(j["x"]["arr"].dump() == "[10,20,30]");
|
||||
}
|
||||
|
||||
SECTION("an index past the end of an array")
|
||||
{
|
||||
my_json j = make();
|
||||
j[my_json::json_pointer("/k/arr/3")] = 40;
|
||||
CHECK(j["x"]["arr"].dump() == "[10,20,null,40]");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("issue #977 - Assigning between different json types")
|
||||
{
|
||||
foo_json lj = ns::foo{3};
|
||||
|
||||
@@ -441,6 +441,10 @@ TEST_CASE("regression tests 2")
|
||||
|
||||
float_json j2 = {1000.0, 2000.0, 3000.0};
|
||||
CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2);
|
||||
|
||||
// half-precision floats of both signs are converted to the float type
|
||||
CHECK(float_json::from_cbor(std::vector<std::uint8_t> {0xF9, 0xC0, 0x00}) == -2.0f);
|
||||
CHECK(float_json::from_cbor(std::vector<std::uint8_t> {0xF9, 0x3C, 0x00}) == 1.0f);
|
||||
}
|
||||
|
||||
SECTION("issue #1045 - Using STL algorithms with JSON containers with expected results?")
|
||||
@@ -1005,7 +1009,7 @@ TEST_CASE("regression test - excessive binary container size honors allow_except
|
||||
// allow_exceptions=true (the default) must still throw exactly as before.
|
||||
// The exact message text is not checked here: on platforms where
|
||||
// std::size_t is 32-bit, the CBOR reader's own length-narrowing check
|
||||
// (get_cbor_container_size(), unrelated to this fix) intercepts a
|
||||
// (get_container_size(), unrelated to this fix) intercepts a
|
||||
// declared length of 2^63 before it ever reaches the check this test
|
||||
// targets, with different (but equally valid, and already correct)
|
||||
// wording -- see unit-cbor.cpp for coverage of that message.
|
||||
|
||||
@@ -23,6 +23,7 @@ using nlohmann::json;
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -248,6 +249,51 @@ TEST_CASE_TEMPLATE("serialization for extreme integer values", T, int32_t, uint3
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("integers of every digit count are written like std::to_string")
|
||||
{
|
||||
// dump_integer() writes up to 16 digits in two groups of eight and 17 to
|
||||
// 20 digits as one to four digits and two groups of eight
|
||||
std::vector<std::uint64_t> values;
|
||||
std::uint64_t power = 1;
|
||||
for (int digits = 1; digits <= 20; ++digits)
|
||||
{
|
||||
values.push_back(power); // 1 followed by zeros
|
||||
values.push_back(power + 1);
|
||||
values.push_back((power * 2) - 1);
|
||||
if (digits < 20)
|
||||
{
|
||||
values.push_back((power * 10) - 1); // all 9s
|
||||
values.push_back((power * 10) - power); // 9 followed by zeros
|
||||
power *= 10;
|
||||
}
|
||||
}
|
||||
values.push_back((std::numeric_limits<std::uint64_t>::max)());
|
||||
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()));
|
||||
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1);
|
||||
std::uint64_t state = 0x9E3779B97F4A7C15u;
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
state = (state * 6364136223846793005u) + 1442695040888963407u;
|
||||
values.push_back(state >> (state % 61)); // every bit length
|
||||
}
|
||||
|
||||
for (const auto v : values)
|
||||
{
|
||||
CAPTURE(v)
|
||||
CHECK(json(v).dump() == std::to_string(v));
|
||||
if (v <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
|
||||
{
|
||||
const auto s = static_cast<std::int64_t>(v);
|
||||
CHECK(json(s).dump() == std::to_string(s));
|
||||
CHECK(json(-s).dump() == std::to_string(-s));
|
||||
}
|
||||
}
|
||||
CHECK(json((std::numeric_limits<std::int64_t>::min)()).dump() == "-9223372036854775808");
|
||||
|
||||
// in a container, so the digits are followed by other output
|
||||
CHECK(json::array({std::uint64_t{18446744073709551615u}, std::int64_t{-1234567890123456789}, 1}).dump() == "[18446744073709551615,-1234567890123456789,1]");
|
||||
}
|
||||
|
||||
TEST_CASE("dump with binary values")
|
||||
{
|
||||
auto binary = json::binary({1, 2, 3, 4});
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <streambuf>
|
||||
@@ -93,6 +95,34 @@ class chunked_streambuf : public std::streambuf
|
||||
std::size_t m_unbuffered;
|
||||
};
|
||||
|
||||
#if TEST_STREAM_BULK_SCAN
|
||||
// A streambuf that claims a get area of several gigabytes, of which only the
|
||||
// first bytes exist. It is only used to move the read position of the adapter,
|
||||
// never to read beyond them, so that skipping more than gbump() can bump at
|
||||
// once can be tested without allocating that much.
|
||||
class oversized_get_area_streambuf : public std::streambuf
|
||||
{
|
||||
public:
|
||||
explicit oversized_get_area_streambuf(std::size_t claimed_size)
|
||||
{
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
|
||||
setg(m_data.data(), m_data.data(), m_data.data() + claimed_size);
|
||||
}
|
||||
|
||||
// Give the get area back its real size, so that the adapter may read the
|
||||
// character at the read position when it is destroyed (with
|
||||
// JSON_PRECISE_STREAM_POSITION, it consumes the last character returned).
|
||||
void restore_get_area()
|
||||
{
|
||||
setg(m_data.data(), m_data.data(), m_data.data() + m_data.size());
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<char, 8> m_data{{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}};
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
// how a test reads from the stream
|
||||
enum class read_mode
|
||||
{
|
||||
@@ -433,6 +463,39 @@ TEST_CASE("parsing from a std::istream scans the get area in bulk")
|
||||
ia.copy_token(token, false);
|
||||
CHECK(token.empty());
|
||||
}
|
||||
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
// a get area larger than an int needs a 64-bit std::size_t (checked by the
|
||||
// preprocessor, as a constant condition triggers MSVC's C4127)
|
||||
SECTION("skipping more characters than gbump() takes at once")
|
||||
{
|
||||
// gbump() takes an int, but a get area can be larger
|
||||
const std::size_t claimed = std::size_t{3} << 30u;
|
||||
oversized_get_area_streambuf buf(claimed);
|
||||
if (static_cast<std::size_t>(buf.in_avail()) != claimed)
|
||||
{
|
||||
// a standard library whose streambuf stores the size of the get
|
||||
// area in an int (MSVC)
|
||||
MESSAGE("a get area larger than an int is not supported");
|
||||
}
|
||||
else
|
||||
{
|
||||
std::istream is(&buf);
|
||||
nlohmann::detail::input_stream_adapter ia(is);
|
||||
CHECK(ia.get_character() == 'a');
|
||||
CHECK(ia.bulk_remaining() == claimed - 1);
|
||||
|
||||
const std::size_t skipped = static_cast<std::size_t>((std::numeric_limits<int>::max)()) + 10;
|
||||
ia.bulk_skip(skipped);
|
||||
CHECK(ia.bulk_remaining() == claimed - 1 - skipped);
|
||||
|
||||
ia.bulk_skip(ia.bulk_remaining());
|
||||
CHECK(ia.bulk_remaining() == 0);
|
||||
|
||||
buf.restore_get_area();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
SECTION("SAX parsing from a stream")
|
||||
|
||||
@@ -928,3 +928,49 @@ TEST_CASE("short decimals")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("decimals of up to 17 digits")
|
||||
{
|
||||
// write_decimal() lays out digits * 10^exp as format_buffer() does; to_chars()
|
||||
// calls it for the doubles whose digits write_shortest() does not convert at
|
||||
// once, and the digits may be as many as 17
|
||||
const auto written = [](std::uint64_t digits, int exp) -> std::string
|
||||
{
|
||||
std::array<char, 64> buf{}; // (up to 41 bytes are written)
|
||||
char* const end = nlohmann::detail::dtoa_impl::write_decimal(buf.data(), digits, exp);
|
||||
return {buf.data(), end};
|
||||
};
|
||||
|
||||
SECTION("fixed notation")
|
||||
{
|
||||
CHECK(written(1, 0) == "1.0");
|
||||
CHECK(written(1234, -2) == "12.34");
|
||||
CHECK(written(123, 2) == "12300.0");
|
||||
CHECK(written(50000000000u, -1) == "5000000000.0");
|
||||
CHECK(written(100000000u, -8) == "1.0");
|
||||
CHECK(written(12345678u, -12) == "1.2345678e-05");
|
||||
CHECK(written(1234, 11) == "123400000000000.0");
|
||||
// the point moves left of the first digit
|
||||
CHECK(written(5, -3) == "0.005");
|
||||
CHECK(written(12345678901234567u, -20) == "0.00012345678901234567");
|
||||
}
|
||||
|
||||
SECTION("exponent notation")
|
||||
{
|
||||
CHECK(written(5, -5) == "5e-05");
|
||||
CHECK(written(1234, 12) == "1.234e+15");
|
||||
CHECK(written(5, -324) == "5e-324");
|
||||
}
|
||||
|
||||
SECTION("17 digits")
|
||||
{
|
||||
CHECK(written(12345678901234567u, -16) == "1.2345678901234567");
|
||||
CHECK(written(12345678901234567u, -19) == "0.0012345678901234567");
|
||||
CHECK(written(12345678901234567u, 5) == "1.2345678901234567e+21");
|
||||
CHECK(written(99999999999999999u, -17) == "0.99999999999999999");
|
||||
CHECK(written(10000000000000001u, -16) == "1.0000000000000001");
|
||||
// trailing zeros are not written
|
||||
CHECK(written(10000000000000000u, -17) == "0.1");
|
||||
CHECK(written(12000000000000000u, -15) == "12.0");
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,15 @@ using nlohmann::json;
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "round_trip_corpus.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "sax_countdown.hpp"
|
||||
#include "sax_event_loggers.hpp"
|
||||
using utils::SaxCountdown;
|
||||
using utils::SaxEventLogger;
|
||||
|
||||
|
||||
TEST_CASE("UBJSON")
|
||||
@@ -1670,6 +1673,31 @@ TEST_CASE("UBJSON")
|
||||
return true;
|
||||
});
|
||||
CHECK_THROWS_AS(scp2.start_array((std::numeric_limits<std::size_t>::max)() - 1), json::out_of_range&);
|
||||
|
||||
// without exceptions, the error is reported to the caller
|
||||
json l;
|
||||
nlohmann::detail::json_sax_dom_callback_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> scp3(l, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept
|
||||
{
|
||||
return true;
|
||||
}, false);
|
||||
CHECK_FALSE(scp3.start_array((std::numeric_limits<std::size_t>::max)() - 1));
|
||||
CHECK(scp3.is_errored());
|
||||
}
|
||||
|
||||
SECTION("array with a known size, read without exceptions")
|
||||
{
|
||||
std::vector<uint8_t> const v_ubjson = {'[', '#', 'i', 2, 'i', 1, 'i', 2};
|
||||
json j;
|
||||
nlohmann::detail::json_sax_dom_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> sax(j, false);
|
||||
CHECK(json::sax_parse(v_ubjson, &sax, json::input_format_t::ubjson));
|
||||
CHECK(j == json({1, 2}));
|
||||
|
||||
// the readers reject a size this large before they announce
|
||||
// it, so it can only reach start_array() directly
|
||||
json k;
|
||||
nlohmann::detail::json_sax_dom_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> sax2(k, false);
|
||||
CHECK_FALSE(sax2.start_array((std::numeric_limits<std::size_t>::max)() - 1));
|
||||
CHECK(sax2.is_errored());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2357,6 +2385,18 @@ TEST_CASE("UBJSON optimized arrays of a valueless type are bounded")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a count equal to SIZE_MAX is rejected")
|
||||
{
|
||||
// the reader marks a container without a count by SIZE_MAX, so a
|
||||
// count of that value must not be read as "no count"; UBJSON's
|
||||
// largest count type (int64) can only reach it where size_t is 32 bits
|
||||
#if SIZE_MAX == 0xffffffff
|
||||
const std::vector<uint8_t> input = {'[', '#', 'L', 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'};
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(input), "[json.exception.out_of_range.408] syntax error while parsing UBJSON size: integer value overflow", json::out_of_range&);
|
||||
CHECK(json::from_ubjson(input, true, false).is_discarded());
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("ordinary counts are unaffected")
|
||||
{
|
||||
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '$', 'Z', '#', 'i', 3})) == json({nullptr, nullptr, nullptr}));
|
||||
@@ -3400,3 +3440,72 @@ TEST_CASE("UBJSON optimized container type marker is validated for count 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("UBJSON optimized arrays from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input reads the numbers of an array with a type marker in
|
||||
// one loop, other input number by number; both must give the same events,
|
||||
// value, or error for the complete document and for every truncation of
|
||||
// it, and stop at every event a SAX handler rejects
|
||||
json document =
|
||||
{
|
||||
{"u8", {0, 200, 255}},
|
||||
{"i8", {-5, 100, -128}},
|
||||
{"i16", {-30000, 1000, 30000}},
|
||||
{"i32", {-2000000000, 5, 70000}},
|
||||
{"i64", {-9000000000000000000LL, 1, 5000000000LL}},
|
||||
{"f64", {1.5, -0.1, 2.5}},
|
||||
{"pairs", {{1.25, -3.75}, {1.25, -3.75}, json::array()}},
|
||||
{"mixed", {1, "a", 2.5, nullptr}},
|
||||
};
|
||||
const std::vector<std::uint8_t> encoded = json::to_ubjson(document, true, true);
|
||||
CHECK(json::from_ubjson(encoded) == document);
|
||||
|
||||
for (std::size_t length = 0; length <= encoded.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(encoded.begin(), encoded.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_ubjson(contiguous, true, false).dump() == json::from_ubjson(bytewise.begin(), bytewise.end(), true, false).dump());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string contiguous_error;
|
||||
std::string bytewise_error;
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_ubjson(contiguous);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
contiguous_error = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_ubjson(bytewise.begin(), bytewise.end());
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
bytewise_error = e.what();
|
||||
}
|
||||
CHECK(contiguous_error == bytewise_error);
|
||||
#endif
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::ubjson)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::ubjson));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
|
||||
for (int events = 0; events <= 80; ++events)
|
||||
{
|
||||
CAPTURE(events)
|
||||
const std::list<std::uint8_t> bytewise(encoded.begin(), encoded.end());
|
||||
SaxCountdown from_contiguous(events);
|
||||
SaxCountdown from_bytewise(events);
|
||||
CHECK(json::sax_parse(encoded, &from_contiguous, json::input_format_t::ubjson)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::ubjson));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "test_utils.hpp"
|
||||
@@ -1631,3 +1632,40 @@ TEST_CASE("Unicode RFC 3629")
|
||||
}
|
||||
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
|
||||
TEST_CASE("UTF-8 check of whole strings")
|
||||
{
|
||||
// valid_utf8_string() decides short strings with a word test and long ones
|
||||
// 16 bytes at a time where a vector check is available; it must agree with
|
||||
// the byte-wise check for every mix of ASCII, sequences of each length,
|
||||
// ill-formed and truncated sequences, at every length and offset
|
||||
const char* const pieces[] = // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
{
|
||||
"a", "\x7F", "\xC3\xA9", "\xE2\x82\xAC", "\xE0\xA0\x80", "\xEF\xBF\xBF", "\xF0\x9F\x98\x80", "\xF4\x8F\xBF\xBF",
|
||||
"\x80", "\xC0\xAF", "\xC3", "\xE2\x82", "\xED\xA0\x80", "\xF0\x9F", "\xF4\x90\x80\x80", "\xFF"
|
||||
};
|
||||
// std::mt19937's sequence is fixed by the standard; no distribution is used
|
||||
std::mt19937 rng(5); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::size_t valid = 0;
|
||||
for (int i = 0; i < 100000; ++i)
|
||||
{
|
||||
std::string s;
|
||||
const std::size_t length = rng() % 100;
|
||||
const auto ascii = static_cast<std::uint32_t>(rng() % 101);
|
||||
while (s.size() < length)
|
||||
{
|
||||
// mostly well-formed pieces (the first eight), so that long valid
|
||||
// strings are common
|
||||
s += (rng() % 100 < ascii) ? std::string("x") : std::string(pieces[rng() % (rng() % 8 == 0 ? 16 : 8)]);
|
||||
}
|
||||
const auto* data = reinterpret_cast<const unsigned char*>(s.data());
|
||||
const bool expected = nlohmann::detail::is_valid_utf8(s);
|
||||
valid += expected ? 1 : 0;
|
||||
CAPTURE(s)
|
||||
CHECK(nlohmann::detail::valid_utf8_string(data, s.size()) == expected);
|
||||
CHECK((nlohmann::detail::valid_utf8_prefix(data, s.size()) == s.size()) == expected);
|
||||
}
|
||||
// both outcomes are common enough to be tested
|
||||
CHECK(valid > 20000);
|
||||
CHECK(valid < 80000);
|
||||
}
|
||||
Reference in new issue
Block a user