mirror of
https://github.com/nlohmann/json.git
synced 2026-10-31 09:55:42 +00:00
* Compare json_view values without a vector per container and a basic_json per number
operator== of views (view/view, view/json, and editable documents) built two
vectors of members for every object it entered, sorted them with stable_sort
for every object, and converted each number to a basic_json value. It now:
- keeps the members of all open objects in one vector (reserved with
geometric growth, so that nested objects do not reallocate), and orders
them by insertion sort for up to 16 members, or not at all when the keys
ascend already;
- compares numbers of one type directly, and builds a basic_json only for
numbers of different types;
- compares short texts (keys, numbers, most strings) with two overlapping
loads (bytes_equal(), in node.hpp);
- first walks both documents in lockstep, in the order of their text, which
finds most pairs of equal values without the work of the comparison. The
walk only enters an object if the comparison would visit its members in the
same order (the object type keeps the order, the keys ascend, or all values
are scalars), so a difference is never found later than before, and it
records the containers it found different, so that the comparison does not
walk them again;
- rejects values of different types and arrays of different sizes before any
of this, and, for an object type that keeps the order, objects whose first
(up to four) members have different keys, which ends a comparison that
differs near the start without preparing the members.
Results are unchanged: numbers compare by value across types, duplicate keys
resolve as parse() resolves them, and objects compare in document order for
ordered_json and by key otherwise.
Instructions of one comparison of a document with an equal copy, macOS arm64
(develop -> now), json_document / ordered_json_document / editable:
twitter 16.1M -> 7.0M 17.7M -> 1.5M 16.6M -> 7.3M
citm 45.0M -> 3.4M 47.0M -> 3.2M 45.7M -> 4.3M
canada 111.4M -> 13.7M 111.4M -> 13.7M 113.3M -> 15.0M
jeopardy 26.2M -> 3.7M 27.3M -> 3.7M 26.6M -> 12.1M
and of a view with a basic_json value:
twitter 11.4M -> 5.9M 11.2M -> 7.3M 11.6M -> 5.9M
citm 33.9M -> 9.7M 32.6M -> 9.1M 34.6M -> 9.8M
canada 92.9M -> 32.1M 92.9M -> 32.1M 93.9M -> 32.8M
and Linux instruction counts (GCC 13 / Clang 18) of the same comparisons:
document with an equal copy json_document ordered_json_document editable
twitter -49 % / -52 % -89 % / -89 % -48 % / -50 %
citm -90 % / -89 % -91 % / -90 % -88 % / -87 %
jeopardy (4,000 questions) -85 % / -85 % -86 % / -86 % -46 % / -49 %
view with a basic_json value
twitter -41 % / -45 % -28 % / -35 % -45 % / -44 %
citm -63 % / -65 % -63 % / -66 % -65 % / -64 %
jeopardy -39 % / -43 % -14 % / -24 % -44 % / -43 %
A sweep that places one difference at about 2,100 positions of eight
documents (a token at an even spacing, the first and last tokens, the first,
middle and last member of the root removed, one appended, the document
wrapped) in five kinds of comparison (view/view of read-only and of editable
documents, view/json, json_document, editable/view) finds the same results as
before and no placement slower on the Mac (Clang); on Linux no placement is
slower than 3 % (Clang and GCC).
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Check the float tokens of a json_document image without a sort and a scan of every character
load(image_check::full) checked every float token of an image by sorting
the token ranges, scanning the token character by character, and comparing
the digit layout with the node's. Now:
- the ranges are sorted only if they are not sorted already (save() writes
them in the order of the nodes, so they are);
- a token of the shape [-]digits.digits whose digit counts are the ones the
node records (nearly all of them) is accepted by checking its digits eight
at a time, without the general scan;
- the general scan skips runs of digits eight at a time.
Damaged tokens are still rejected as before: the check accepts exactly the
tokens that the general scan accepts.
Instructions of load(image_check::full) (develop -> now):
macOS arm64 canada 44.6M -> 30.9M (-31 %), jeopardy 11.3M -> 10.7M,
citm 10.4M -> 10.0M, twitter 5.06M -> 4.94M
Linux GCC 13 canada -50 %, jeopardy -43 %, twitter -31 %, citm -28 %
Linux Clang 18 canada -42 %, jeopardy -48 %, twitter -35 %, citm -32 %
image_check::bounds is unchanged (within 0.4 %).
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Rename names that Flawfinder takes for the file function open()
The comparison cursors' open() is now enter(), and a test's open and
close strings are head and tail.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Fix the compiler and lint failures of the json_view comparison
- all_digits(): the lambda declares its constant itself; MSVC does not
let a lambda without captures read a constexpr local of the function
- equal(): the frame's "in document order" index is a namespace-scope
constant (document_order); the local constexpr npos in the default
member initializers of the local struct crashed GCC 9 and MinGW GCC
and was not found by MSVC
- keys_differ_early() keeps the first keys in std::arrays (cpplint:
variable-length arrays; clang-tidy: C arrays), and the basic_json
side's first_keys() is not noexcept (key() may throw)
- the numbers of one type are compared in an if chain instead of a
nested conditional operator
- the dump test avoids C arrays and temporary string concatenations
View comparison instructions are unchanged (Apple clang, twitter, citm,
canada: -0.1 % to +0.1 %).
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Keep the byte comparison helpers in compare.hpp
Address review comments: bytes_equal() and bytes_less() move from node.hpp
to compare.hpp, their only user; key_less() no longer splits the comment of
resolve_indices() from its function; and the loop counters of bytes_equal()
and all_digits() are scoped to their loops.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Dispatch on the compile-time conditions of the json_view comparison
MSVC (/W4 /WX) reported C4127 (conditional expression is constant) for
the template parameter EA and the object type's ordering in an if
condition; both are now tag-dispatched.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Remove a nested duplicate guard from the json_view image test
clang-tidy reported the inner #if !defined(JSON_NOEXCEPTION) as
redundant (the test case is in the same #if) and a find() != npos, which
gets the NOLINT the other tests use.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Mark Infer's null dereference in materialize() as a false positive
Infer (ci_infer, also red on develop since #5882) reports a null
dereference in json_sax_dom_parser::null() called from materialize(). The
member pointer the parser stores a value of an object in is set by the
key() before it, so it is not null there.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Move the json_view comparison tests into their own file
With the tests this branch adds, the object file of unit-json_view_dump.cpp
got too large for the MinGW x64 assembler ("file too big"). The test case
"json_view comparison" is now unit-json_view_compare.cpp, unchanged and
also built with C++17.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Build the DOM parser with Visual Studio 2015 again
Since #5882, the three VS 2015 jobs of AppVeyor fail on develop:
- the detection of keys that can be indexed instantiated std::hash for
every string type, which VS 2015 rejects with a static_assert for a type
it does not hash (alt_string): std::hash is now looked at only for
objects in a vector
- the frame stack copied its frames with std::copy to a raw pointer, which
VS 2015 reports as unsafe (C4996, an error with /WX): a loop now
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* json_view: no per-object allocations in ordered_json comparisons
For an object type that keeps the order of its members (ordered_json),
resolve_indices() allocated a vector per compared object, and
resolve_members() two. They now use one working vector owned by equal(),
whose capacity is reused, and resolve_indices() needs none when the keys
are unique. resolve_members() sorts small objects by insertion, as the
other paths do.
Instructions per comparison (callgrind, GCC 13 and clang 18, x86-64):
twitter, ordered_json view == ordered_json: 0.939 / 0.941; 20000 small
objects with a duplicate key: 0.741-0.768; all other rows (json,
citm_catalog, canada, view == view) within 0.988-1.003.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
---------
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1364 lines
56 KiB
C++
1364 lines
56 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json_view.hpp>
|
|
using nlohmann::json;
|
|
using nlohmann::ordered_json;
|
|
using nlohmann::json_document;
|
|
using nlohmann::json_editable_document;
|
|
using nlohmann::ordered_json_document;
|
|
using nlohmann::ordered_json_editable_document;
|
|
using image_check = json_document::image_check;
|
|
using nlohmann::detail::view::node;
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <test_data.hpp>
|
|
|
|
namespace json_view_image_test
|
|
{
|
|
// whether a document type has save() and load() (only on little-endian targets)
|
|
template<typename T, typename = void>
|
|
struct has_save : std::false_type {};
|
|
template<typename T>
|
|
struct has_save<T, decltype(static_cast<void>(std::declval<const T&>().save()))> : std::true_type {};
|
|
template<typename T, typename = void>
|
|
struct has_load : std::false_type {};
|
|
template<typename T>
|
|
struct has_load<T, decltype(static_cast<void>(T::load(std::declval<const std::vector<std::uint8_t>&>())))> : std::true_type {};
|
|
} // namespace json_view_image_test
|
|
|
|
#if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
|
|
|
namespace
|
|
{
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
std::string exception_of(const std::function<void()>& f)
|
|
{
|
|
try
|
|
{
|
|
f();
|
|
}
|
|
catch (const json::exception& e)
|
|
{
|
|
return e.what();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
const char* const check_failed = "[json.exception.parse_error.116] parse error: invalid json_document image: the check failed";
|
|
#endif
|
|
|
|
std::string read_file(const std::string& name)
|
|
{
|
|
const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary);
|
|
std::stringstream ss;
|
|
ss << f.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
// the dump of a loaded image, through a named document (root() of a temporary
|
|
// document does not compile, and its views would dangle)
|
|
std::string loaded_dump(const std::vector<std::uint8_t>& image, image_check check = image_check::full)
|
|
{
|
|
const json_document d = json_document::load(image, check);
|
|
return d.root().dump();
|
|
}
|
|
|
|
// the offsets of the parts of an image
|
|
constexpr std::size_t header_size = 64;
|
|
|
|
std::uint64_t header_field(const std::vector<std::uint8_t>& image, std::size_t offset)
|
|
{
|
|
std::uint64_t v = 0;
|
|
std::memcpy(&v, image.data() + offset, sizeof(v));
|
|
return v;
|
|
}
|
|
|
|
// a header field as a size; the cast is from a variable, which GCC's
|
|
// -Wuseless-cast does not flag where std::uint64_t and std::size_t coincide
|
|
std::size_t header_size_field(const std::vector<std::uint8_t>& image, std::size_t offset)
|
|
{
|
|
const std::uint64_t v = header_field(image, offset);
|
|
return static_cast<std::size_t>(v);
|
|
}
|
|
|
|
void set_header_field(std::vector<std::uint8_t>& image, std::size_t offset, std::uint64_t v)
|
|
{
|
|
std::memcpy(image.data() + offset, &v, sizeof(v));
|
|
}
|
|
|
|
std::size_t node_count(const std::vector<std::uint8_t>& image)
|
|
{
|
|
return header_size_field(image, 8);
|
|
}
|
|
|
|
std::size_t text_at(const std::vector<std::uint8_t>& image)
|
|
{
|
|
return header_size + (node_count(image) * sizeof(node));
|
|
}
|
|
|
|
node node_at(const std::vector<std::uint8_t>& image, std::size_t i)
|
|
{
|
|
node n{};
|
|
std::memcpy(&n, image.data() + header_size + (i * sizeof(node)), sizeof(node));
|
|
return n;
|
|
}
|
|
|
|
void set_node(std::vector<std::uint8_t>& image, std::size_t i, const node& n)
|
|
{
|
|
std::memcpy(image.data() + header_size + (i * sizeof(node)), &n, sizeof(node));
|
|
}
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
/// the result of loading an image with a check: "" or the exception message
|
|
std::string load_result(const std::vector<std::uint8_t>& image, image_check check)
|
|
{
|
|
return exception_of([&]
|
|
{
|
|
const json_document d = json_document::load(image, check);
|
|
static_cast<void>(d);
|
|
});
|
|
}
|
|
|
|
/// a copy of the image with node i changed by f
|
|
template<typename F>
|
|
std::vector<std::uint8_t> corrupted(const std::vector<std::uint8_t>& image, std::size_t i, F f)
|
|
{
|
|
std::vector<std::uint8_t> b = image;
|
|
node n = node_at(b, i);
|
|
f(n);
|
|
set_node(b, i, n);
|
|
return b;
|
|
}
|
|
#endif
|
|
|
|
/// a document and the documents loaded from its image must be equal
|
|
template<typename Document>
|
|
void check_round_trip(const Document& d)
|
|
{
|
|
const std::vector<std::uint8_t> image = d.save();
|
|
for (const image_check check :
|
|
{
|
|
image_check::full, image_check::bounds, image_check::none
|
|
})
|
|
{
|
|
const json_document l = json_document::load(image, check);
|
|
CHECK(l.root().dump() == d.root().dump());
|
|
CHECK(l.root().dump(2) == d.root().dump(2));
|
|
CHECK(l.root().materialize() == json(d.root().materialize()));
|
|
// an image of a loaded document is the same image
|
|
CHECK(l.save() == image);
|
|
}
|
|
// an editable document can be loaded, too
|
|
const ordered_json_editable_document e = ordered_json_editable_document::load(image);
|
|
CHECK(e.root().dump() == d.root().dump());
|
|
}
|
|
|
|
std::uint32_t rng()
|
|
{
|
|
static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
|
// result_type is std::uint_fast32_t, which may be wider than 32 bits
|
|
const std::mt19937::result_type value = generator();
|
|
return static_cast<std::uint32_t>(value);
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("json_view images: round trips")
|
|
{
|
|
static_assert(json_view_image_test::has_save<json_document>::value, "save() on a little-endian target");
|
|
static_assert(json_view_image_test::has_load<json_document>::value, "load() on a little-endian target");
|
|
static_assert(json_view_image_test::has_save<ordered_json_editable_document>::value, "save() on a little-endian target");
|
|
static_assert(json_view_image_test::has_load<ordered_json_editable_document>::value, "load() on a little-endian target");
|
|
|
|
SECTION("small documents")
|
|
{
|
|
for (const char* text :
|
|
{
|
|
"null", "true", "false", "0", "-0", "42", "-42", "18446744073709551615", "-9223372036854775808",
|
|
"123456789012345678901234567890", "1.5", "-1.25e-300", "1E308", "0.1000000000000000000000000001",
|
|
"\"\"", "\"text\"", R"("esc\"aped\n\u00e9\ud83d\ude00")", "\"\xc3\xa9\xe3\x81\x82\"",
|
|
"[]", "{}", "[[]]", "[{}]", "{\"\":{}}",
|
|
R"({"a": [1, 2.5, "x\ty", true, null, {"b": []}], "c": {"d": -3, "eA": "f"}})",
|
|
R"({"k": 1, "k": 2, "l": [], "k": 3})",
|
|
" [1 , 2 ] "
|
|
})
|
|
{
|
|
CAPTURE(text)
|
|
// false positive: parse() returns a document with a root
|
|
// @infer-ignore NULLPTR_DEREFERENCE
|
|
check_round_trip(json_document::parse(text));
|
|
// false positive: parse() returns a document with a root
|
|
// @infer-ignore NULLPTR_DEREFERENCE
|
|
check_round_trip(ordered_json_document::parse(text));
|
|
}
|
|
}
|
|
|
|
SECTION("files")
|
|
{
|
|
for (const char* name :
|
|
{
|
|
"/json_testsuite/sample.json", "/nativejson-benchmark/canada.json", "/nativejson-benchmark/citm_catalog.json",
|
|
"/nativejson-benchmark/twitter.json", "/json_tests/pass1.json", "/json_tests/pass2.json", "/json_tests/pass3.json"
|
|
})
|
|
{
|
|
CAPTURE(name)
|
|
const std::string text = read_file(name);
|
|
const json_document d = json_document::parse(text);
|
|
check_round_trip(d);
|
|
// what a loaded document reads is what parse() produces
|
|
const json_document l = json_document::load(d.save());
|
|
CHECK(l.root().materialize() == json::parse(text));
|
|
}
|
|
}
|
|
|
|
SECTION("images are deterministic")
|
|
{
|
|
const std::string text = R"({"b": [1, 2, {"c": "\u00e9"}], "a": 1.5})";
|
|
const json_document d = json_document::parse(text);
|
|
CHECK(d.save() == json_document::parse(text).save());
|
|
CHECK(d.save() == json_editable_document::parse(text).save());
|
|
CHECK(d.save() == ordered_json_document::parse(text).save());
|
|
const json_document copy = json_document::parse_copy(text);
|
|
CHECK(copy.save() == d.save());
|
|
}
|
|
|
|
SECTION("large objects get their hash index again")
|
|
{
|
|
std::string text = "{";
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i);
|
|
}
|
|
text += R"(,"k7":"a duplicate","inner":{)";
|
|
for (int i = 0; i < 200; ++i)
|
|
{
|
|
text += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(-i);
|
|
}
|
|
text += "}}";
|
|
const json_document d = json_document::parse(text);
|
|
const std::vector<std::uint8_t> image = d.save();
|
|
for (const image_check check :
|
|
{
|
|
image_check::full, image_check::none
|
|
})
|
|
{
|
|
const json_document l = json_document::load(image, check);
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
CHECK(l.root()["k" + std::to_string(i)] == d.root()["k" + std::to_string(i)]);
|
|
}
|
|
CHECK(l.root()["k7"].get<int>() == 7); // the first of duplicate keys
|
|
CHECK(l.root()["inner"]["m199"].get<int>() == -199);
|
|
CHECK(!l.root().contains("k1000"));
|
|
// the index is not part of the image
|
|
CHECK(l.save() == image);
|
|
}
|
|
// the nodes of objects in the image do not carry the number of an index
|
|
CHECK(node_at(image, 0).extra == 0);
|
|
}
|
|
|
|
SECTION("duplicate keys of a large object: lookups return the first member")
|
|
{
|
|
std::string text = "{";
|
|
for (int i = 0; i < 200; ++i)
|
|
{
|
|
text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i);
|
|
}
|
|
// three members for k5 (the first is a number), and duplicates of k0 and k199
|
|
text += R"(,"k5":"two","k0":null,"k199":[],"k5":"three"})";
|
|
const json_document d = json_document::parse(text);
|
|
REQUIRE(d.root().size() == 204);
|
|
CHECK(d.root()["k5"] == 5);
|
|
const std::vector<std::uint8_t> image = d.save();
|
|
for (const image_check check :
|
|
{
|
|
image_check::full, image_check::bounds, image_check::none
|
|
})
|
|
{
|
|
const json_document l = json_document::load(image, check);
|
|
CHECK(l.root().size() == 204);
|
|
CHECK(l.root()["k5"] == 5);
|
|
CHECK(l.root().at("k5") == 5);
|
|
CHECK(l.root().find("k5").value() == 5);
|
|
CHECK(l.root()["k0"] == 0);
|
|
CHECK(l.root()["k199"] == 199);
|
|
CHECK(l.root()["k100"] == 100);
|
|
// materialize() keeps the semantics of parse(): the last value
|
|
CHECK(l.root().materialize() == d.root().materialize());
|
|
CHECK(l.root().materialize()["k5"] == "three");
|
|
}
|
|
}
|
|
|
|
SECTION("objects with colliding keys")
|
|
{
|
|
// the hash is not seeded: keys can be found that land in one slot of
|
|
// a table (see json_view: "colliding keys")
|
|
const auto keys_for = [](std::size_t table_size, std::size_t colliding_count, std::size_t spread_count)
|
|
{
|
|
std::vector<std::string> colliding;
|
|
std::vector<std::string> spread;
|
|
for (std::uint64_t counter = 0; colliding.size() < colliding_count || spread.size() < spread_count; ++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() < colliding_count)
|
|
{
|
|
colliding.push_back(key);
|
|
}
|
|
else if (!lands_in_slot_zero && spread.size() < spread_count)
|
|
{
|
|
spread.push_back(key);
|
|
}
|
|
}
|
|
colliding.insert(colliding.end(), spread.begin(), spread.end());
|
|
return colliding;
|
|
};
|
|
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);
|
|
}
|
|
return text + "}";
|
|
};
|
|
|
|
// 300 keys in one slot (the table is 1024 slots): the table would
|
|
// exceed the probe limit, so the object has none and is searched
|
|
// linearly; 40 of 200 keys in one slot (512 slots): a table with a
|
|
// long chain
|
|
const struct // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): an unnamed type
|
|
{
|
|
std::size_t table_size;
|
|
std::size_t colliding;
|
|
std::size_t spread;
|
|
} cases[] = {{1024, 300, 0}, {512, 40, 160}};
|
|
for (const auto& c : cases)
|
|
{
|
|
CAPTURE(c.table_size)
|
|
std::vector<std::string> keys = keys_for(c.table_size, c.colliding, c.spread);
|
|
const std::size_t members = keys.size();
|
|
// a duplicate of a colliding key, of the first and of a spread key
|
|
const std::vector<std::string> duplicated = {keys[c.colliding - 1], keys[0], keys[members - 1]};
|
|
std::string text = make_text(keys);
|
|
text.pop_back();
|
|
for (const std::string& key : duplicated)
|
|
{
|
|
text += ",\"" + key + R"(":"last")";
|
|
}
|
|
text += "}";
|
|
const json_document d = json_document::parse(text);
|
|
const std::vector<std::uint8_t> image = d.save();
|
|
for (const image_check check :
|
|
{
|
|
image_check::full, image_check::bounds, image_check::none
|
|
})
|
|
{
|
|
const json_document l = json_document::load(image, check);
|
|
REQUIRE(l.root().size() == members + duplicated.size());
|
|
for (std::size_t i = 0; i < members; ++i)
|
|
{
|
|
CAPTURE(i)
|
|
// lookups find the first member, also of the duplicated keys
|
|
CHECK(l.root()[keys[i]] == i);
|
|
CHECK(l.root().at(keys[i]) == l.root()[keys[i]]);
|
|
CHECK(l.root().find(keys[i]).key() == keys[i]);
|
|
CHECK(!l.root().contains(keys[i] + "x"));
|
|
}
|
|
CHECK(!l.root().contains("missing"));
|
|
CHECK(l.root().materialize() == json::parse(text));
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("loading releases the list of large objects")
|
|
{
|
|
std::string text = "[";
|
|
for (int object = 0; object < 400; ++object)
|
|
{
|
|
text += object != 0 ? ",{" : "{";
|
|
for (int i = 0; i < 128; ++i)
|
|
{
|
|
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
|
}
|
|
text += '}';
|
|
}
|
|
text += "]";
|
|
json_document parsed = json_document::parse(text);
|
|
const std::vector<std::uint8_t> image = parsed.save();
|
|
json_document loaded = json_document::load(image);
|
|
CHECK(loaded.root()[399]["127"] == 127);
|
|
// Parsing and loading build the same tables, and keep nothing else:
|
|
// not the positions of the objects to index (2 KiB here). The slack
|
|
// covers the nodes in the header of the document, which are sized
|
|
// differently.
|
|
parsed.shrink_to_fit();
|
|
loaded.shrink_to_fit();
|
|
CHECK(loaded.memory_usage() <= parsed.memory_usage() + 512);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json_view images: edited documents")
|
|
{
|
|
const std::string text = R"({"name": "x", "n": 1, "f": 2.5, "list": [1, 2, 3], "obj": {"a": "\u00e9", "b": [true]}, "s": "a\"b"})";
|
|
|
|
SECTION("every kind of edit")
|
|
{
|
|
ordered_json_editable_document d = ordered_json_editable_document::parse(text);
|
|
d.set(d.root()["name"], "a new \"name\""); // string in the edit arena
|
|
d.set(d.root()["n"], -17); // negative integer
|
|
d.set(d.root(), "p", 5); // non-negative number_integer
|
|
d.set(d.root(), "u", 18446744073709551615u); // unsigned
|
|
d.set(d.root()["f"], 0.1); // float token
|
|
d.set(d.root(), "nan", std::numeric_limits<double>::quiet_NaN());
|
|
d.set(d.root(), "inf", -std::numeric_limits<double>::infinity());
|
|
d.push_back(d.root()["list"], "pushed"); // moved array
|
|
d.insert(d.root()["list"], 0, ordered_json::object({{"new", {1, 2}}}));
|
|
d.erase(d.root()["list"], 2);
|
|
d.erase(d.root(), "s");
|
|
d.set(d.root()["obj"], "c", ordered_json::array({1, "two", 3.5, nullptr, false})); // new object member with a new array
|
|
d.set(d.root(), "copy", d.root()["obj"]); // a copy of a subtree
|
|
d.set(d.root(), "key \xc3\xa9", true); // a key in the edit arena
|
|
|
|
const std::vector<std::uint8_t> image = d.save();
|
|
const ordered_json expected = ordered_json::parse(d.root().dump());
|
|
for (const image_check check :
|
|
{
|
|
image_check::full, image_check::bounds, image_check::none
|
|
})
|
|
{
|
|
const ordered_json_document l = ordered_json_document::load(image, check);
|
|
CHECK(l.root().dump() == d.root().dump());
|
|
CHECK(l.root().materialize() == expected);
|
|
CHECK(l.root()["nan"].is_null());
|
|
CHECK(l.root()["inf"].is_null());
|
|
CHECK(l.root()["p"].is_number_integer());
|
|
CHECK(l.root()["p"].get<int>() == 5);
|
|
CHECK(l.root()["f"].get<double>() == 0.1);
|
|
CHECK(l.root()["u"].get<std::uint64_t>() == 18446744073709551615u);
|
|
}
|
|
// the node index is in document order again: an image of the loaded
|
|
// document is the same image
|
|
CHECK(ordered_json_document::load(image).save() == image);
|
|
// number tokens of edits follow the source; the text is the source's
|
|
// prefix
|
|
const ordered_json_document l = ordered_json_document::load(image);
|
|
REQUIRE(l.source().size() > text.size());
|
|
CHECK(std::string(l.source().data(), text.size()) == text); // NOLINT(bugprone-suspicious-stringview-data-usage): the size is given and checked above
|
|
}
|
|
|
|
SECTION("a loaded document can be edited and saved again")
|
|
{
|
|
const std::vector<std::uint8_t> first = json_editable_document::parse(text).save();
|
|
json_editable_document d = json_editable_document::load(first);
|
|
d.set(d.root()["obj"]["a"], "changed");
|
|
d.push_back(d.root()["list"], 4);
|
|
d.set(d.root(), "z", json::array({json::object()}));
|
|
const std::vector<std::uint8_t> second = d.save();
|
|
const json_document l = json_document::load(second);
|
|
CHECK(l.root().dump() == d.root().dump());
|
|
CHECK(l.root()["obj"]["a"] == "changed");
|
|
CHECK(l.root()["list"].size() == 4);
|
|
}
|
|
|
|
SECTION("the targets of links do not reach the image")
|
|
{
|
|
// Edits that make an entry of a moved sequence link to a value mark the
|
|
// value as linked (node_flags::linked). An image has no links and no
|
|
// such flag: the full and the bounds check reject it.
|
|
const std::string nested = R"({"a": [1, 2, {"x": [3]}, "s", 1.5, true, null, [4, 5]], "b": {"c": 1, "d": [1, 2, 3], "e": {"f": "g"}}, "h": "str"})";
|
|
const auto check_image = [](const json_editable_document & d)
|
|
{
|
|
const std::vector<std::uint8_t> image = d.save();
|
|
for (std::size_t i = 0; i < node_count(image); ++i)
|
|
{
|
|
CAPTURE(i)
|
|
CHECK((node_at(image, i).flags & nlohmann::detail::view::node_flags::linked) == 0);
|
|
CHECK((node_at(image, i).kind != nlohmann::detail::view::node_kind::link));
|
|
}
|
|
for (const image_check check :
|
|
{
|
|
image_check::full, image_check::bounds
|
|
})
|
|
{
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
CHECK(load_result(image, check).empty());
|
|
#else
|
|
const json_document loaded = json_document::load(image, check);
|
|
CHECK(loaded.root().dump() == d.root().dump());
|
|
#endif
|
|
}
|
|
check_round_trip(d);
|
|
// a loaded document is edited and saved again
|
|
json_editable_document e = json_editable_document::load(image);
|
|
CHECK(e.root().dump() == d.root().dump());
|
|
e.set(e.root(), "after", 1);
|
|
CHECK(loaded_dump(e.save()) == e.root().dump());
|
|
};
|
|
|
|
SECTION("a container replaced by a scalar after an earlier edit")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(nested);
|
|
d.set(d.root()["a"][0], 9);
|
|
d.set(d.root()["a"][2], 5);
|
|
check_image(d);
|
|
d.set(d.root()["a"][2], "now a string");
|
|
check_image(d);
|
|
d.set(d.root()["a"][7], 0.25);
|
|
check_image(d);
|
|
d.set(d.root()["b"]["e"], true);
|
|
d.set(d.root()["b"]["d"], 7);
|
|
check_image(d);
|
|
}
|
|
|
|
SECTION("insert and push_back into parsed arrays")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(nested);
|
|
d.push_back(d.root()["a"], 6);
|
|
check_image(d);
|
|
d.insert(d.root()["a"], 0, "first");
|
|
check_image(d);
|
|
d.insert(d.root()["b"]["d"], 1, json::array({1, 2}));
|
|
d.push_back(d.root()["b"]["d"], json::object({{"k", nullptr}}));
|
|
check_image(d);
|
|
// links to values that are replaced afterwards
|
|
d.set(d.root()["a"][3], json::object());
|
|
d.set(d.root()["a"][4], false);
|
|
d.set(d.root()["a"][5], "replaced");
|
|
d.set(d.root()["a"][9], 1e300);
|
|
check_image(d);
|
|
}
|
|
|
|
SECTION("members set in parsed objects")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(nested);
|
|
d.set(d.root(), "new", 1);
|
|
d.set(d.root()["b"], "c", "changed");
|
|
d.set(d.root()["b"], "e", json::array({1, {{"z", 2}}}));
|
|
d.set(d.root()["b"], "more", d.root()["a"]);
|
|
check_image(d);
|
|
d.set(d.root()["a"][2], 3); // a link target in a copied subtree
|
|
d.erase(d.root()["b"], "c");
|
|
d.set(d.root(), "h", json::object());
|
|
check_image(d);
|
|
}
|
|
|
|
SECTION("erase")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(nested);
|
|
d.push_back(d.root()["a"], 6);
|
|
d.erase(d.root()["a"], 0);
|
|
d.erase(d.root()["a"], 1);
|
|
d.set(d.root()["a"][0], 1);
|
|
d.erase(d.root()["b"], "d");
|
|
check_image(d);
|
|
}
|
|
}
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
SECTION("invalid UTF-8 of a loaded image is not copied into an editable document")
|
|
{
|
|
// loading with the bounds check (or none) does not look at the encoding
|
|
// of strings: dump() of such a view throws, and an editable document
|
|
// must not take the string over
|
|
const auto patched = [](const std::string & json_text)
|
|
{
|
|
std::vector<std::uint8_t> image = json_document::parse(json_text).save();
|
|
bool found = false;
|
|
for (std::size_t i = 0; i + 1 < image.size(); ++i)
|
|
{
|
|
if (image[i] == 'Q' && image[i + 1] == 'Z')
|
|
{
|
|
image[i] = 0xC3;
|
|
image[i + 1] = 0x28; // an invalid sequence
|
|
found = true;
|
|
}
|
|
}
|
|
REQUIRE(found);
|
|
return image;
|
|
};
|
|
|
|
for (const image_check check :
|
|
{
|
|
image_check::bounds, image_check::none
|
|
})
|
|
{
|
|
// as a value
|
|
{
|
|
const json_document loaded = json_document::load(patched(R"(["abQZ"])"), check);
|
|
json_editable_document e = json_editable_document::parse("[]");
|
|
CHECK_THROWS_WITH_AS(e.push_back(e.root(), loaded.root()[0]), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(e.insert(e.root(), 0, loaded.root()[0]), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(e.set(e.root(), loaded.root()[0]), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(e.push_back(e.root(), loaded.root()), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&);
|
|
// nothing changed
|
|
CHECK(e.root().dump() == "[]");
|
|
CHECK(e.root().size() == 0);
|
|
CHECK(loaded_dump(e.save()) == "[]");
|
|
// the valid part of the same document can be copied
|
|
e.push_back(e.root(), loaded.root().size());
|
|
CHECK(e.root().dump() == "[1]");
|
|
}
|
|
// as a key, and in a nested value
|
|
{
|
|
const json_document loaded = json_document::load(patched(R"([{"abQZ": 1}, [["x", "QZ"]]])"), check);
|
|
json_editable_document e = json_editable_document::parse(R"({"keep": [1]})");
|
|
CHECK_THROWS_WITH_AS(e.set(e.root(), "k", loaded.root()[0]), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(e.set(e.root(), "k", loaded.root()[1]), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0x28", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(e.push_back(e.root()["keep"], loaded.root()[1]), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0x28", json::type_error&);
|
|
CHECK(e.root().dump() == R"({"keep":[1]})");
|
|
CHECK(loaded_dump(e.save()) == R"({"keep":[1]})");
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
SECTION("the root replaced")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(text);
|
|
d.set(d.root(), json::array({1, "x"}));
|
|
check_round_trip(d);
|
|
d.set(d.root(), 3.5);
|
|
check_round_trip(d);
|
|
d.set(d.root(), "text");
|
|
check_round_trip(d);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json_view images: ownership")
|
|
{
|
|
const std::string text = R"({"a": "esc\u00e9aped", "b": [1, 2]})";
|
|
const std::vector<std::uint8_t> image = json_document::parse(text).save();
|
|
|
|
SECTION("borrowed")
|
|
{
|
|
const json_document d = json_document::load(image);
|
|
CHECK(!d.owns_source());
|
|
CHECK(d.root()["a"] == "esc\xc3\xa9" "aped");
|
|
const json_document p = json_document::load(image.data(), image.size());
|
|
CHECK(!p.owns_source());
|
|
CHECK(p.root() == d.root());
|
|
// the text is the image's
|
|
CHECK(d.source().data() == reinterpret_cast<const char*>(image.data() + text_at(image)));
|
|
}
|
|
|
|
SECTION("owned")
|
|
{
|
|
std::vector<std::uint8_t> copy = image;
|
|
const std::uint8_t* const data = copy.data();
|
|
json_document d = json_document::load(std::move(copy));
|
|
CHECK(d.owns_source());
|
|
CHECK(d.source().data() == reinterpret_cast<const char*>(data + text_at(image)));
|
|
CHECK(d.memory_usage() >= image.size());
|
|
CHECK(d.root()["b"][1] == 2);
|
|
// read() replaces the image
|
|
d.read(std::string("[1]"));
|
|
CHECK(d.owns_source());
|
|
CHECK(d.root().dump() == "[1]");
|
|
const std::string borrowed = "[2]";
|
|
d.read(borrowed);
|
|
CHECK(!d.owns_source());
|
|
}
|
|
|
|
SECTION("shrink_to_fit keeps the decoded strings of the image")
|
|
{
|
|
json_document d = json_document::parse(R"(["\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9"])");
|
|
d.shrink_to_fit();
|
|
const std::vector<std::uint8_t> img = d.save();
|
|
json_document l = json_document::load(img);
|
|
l.shrink_to_fit();
|
|
CHECK(l.root().dump() == d.root().dump());
|
|
CHECK(l.save() == img);
|
|
}
|
|
}
|
|
|
|
// the remaining tests are about the exceptions of load() and save()
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
TEST_CASE("json_view images: errors")
|
|
{
|
|
SECTION("float tokens: a changed byte is accepted if the text is still a number with the digits the node records")
|
|
{
|
|
for (const char* const token :
|
|
{
|
|
"-12.375", "0.5", "123456.0", "-0.0", "9999999999999999999.5", "0.000000000000000000000001", "1.25e10", "5E-3"
|
|
})
|
|
{
|
|
CAPTURE(token)
|
|
const std::string text = std::string("[") + token + "]";
|
|
const json_document d = json_document::parse(text);
|
|
const std::vector<std::uint8_t> image = d.save();
|
|
const std::size_t at = text_at(image) + 1; // the first byte of the token
|
|
const std::uint16_t recorded = node_at(image, 1).extra;
|
|
CHECK(loaded_dump(image) == json::parse(text).dump());
|
|
for (std::size_t pos = 0; token[pos] != '\0'; ++pos)
|
|
{
|
|
for (unsigned int byte = 0; byte < 256; ++byte)
|
|
{
|
|
std::vector<std::uint8_t> changed = image;
|
|
changed[at + pos] = static_cast<std::uint8_t>(byte);
|
|
std::string t = token;
|
|
t[pos] = static_cast<char>(byte);
|
|
// the digits of the integer part and of the fraction of the changed token
|
|
std::size_t integer = 0;
|
|
std::size_t fraction = 0;
|
|
std::size_t k = (t[0] == '-') ? 1 : 0;
|
|
for (; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
|
{
|
|
++integer;
|
|
}
|
|
if (k < t.size() && t[k] == '.')
|
|
{
|
|
for (++k; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
|
{
|
|
++fraction;
|
|
}
|
|
}
|
|
// (as a JSON text, the token may have white space around it)
|
|
const bool float_token = json::accept(t) && t.find_first_of(std::string(" \t\r\n\0", 5)) == std::string::npos && (t.find('.') != std::string::npos || t.find_first_of("eE") != std::string::npos) // NOLINT(abseil-string-find-str-contains)
|
|
&& ((integer | (fraction << 8U)) == recorded);
|
|
CAPTURE(pos)
|
|
CAPTURE(byte)
|
|
std::string result;
|
|
try
|
|
{
|
|
result = loaded_dump(changed);
|
|
}
|
|
catch (const json::parse_error& e)
|
|
{
|
|
CHECK(std::string(e.what()) == check_failed);
|
|
result = "rejected";
|
|
}
|
|
CHECK((result != "rejected") == float_token);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
|
|
{
|
|
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
|
|
std::vector<std::uint8_t> image = d.save();
|
|
const std::size_t arena = text_at(image) + header_size_field(image, 16) + 1;
|
|
image[arena + 3] = 0x28; // the key is "k\n"; the value starts at 3: 0xC3 0x28 is invalid
|
|
const json_document loaded = json_document::load(image, image_check::bounds);
|
|
CHECK(loaded.root().dump().size() == 13);
|
|
for (const int indent :
|
|
{
|
|
0, 2
|
|
})
|
|
{
|
|
CHECK_THROWS_AS(loaded.root().dump(indent), json::type_error);
|
|
CHECK_THROWS_AS(loaded.root().dump(indent, ' ', true), json::type_error);
|
|
}
|
|
CHECK_THROWS_AS(loaded.root().dump(-1, ' ', true), json::type_error);
|
|
CHECK_THROWS_WITH_AS(loaded.root().dump(2), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0x28", json::type_error&);
|
|
}
|
|
|
|
SECTION("a literal as the root: dump() after loading")
|
|
{
|
|
for (const char* text :
|
|
{
|
|
"null", "true", "false"
|
|
})
|
|
{
|
|
std::vector<std::uint8_t> image = json_document::parse(text).save();
|
|
node n = node_at(image, 0);
|
|
n.off = static_cast<std::uint32_t>(image.size());
|
|
set_node(image, 0, n);
|
|
CHECK(load_result(image, image_check::full) == check_failed);
|
|
CHECK(load_result(image, image_check::bounds) == check_failed);
|
|
}
|
|
}
|
|
|
|
SECTION("saving a discarded document")
|
|
{
|
|
const json_document empty{};
|
|
CHECK(exception_of([&] { static_cast<void>(empty.save()); }) == "[json.exception.type_error.320] cannot save a discarded json_document");
|
|
const json_document failed = json_document::parse("[1,", false);
|
|
CHECK(exception_of([&] { static_cast<void>(failed.save()); }) == "[json.exception.type_error.320] cannot save a discarded json_document");
|
|
}
|
|
|
|
const std::vector<std::uint8_t> image = json_document::parse(R"({"a": [1, "\u00e9"]})").save();
|
|
const std::string prefix = "[json.exception.parse_error.116] parse error: invalid json_document image: ";
|
|
|
|
SECTION("header and sizes")
|
|
{
|
|
CHECK(exception_of([]
|
|
{
|
|
const json_document d = json_document::load(nullptr, 0);
|
|
static_cast<void>(d);
|
|
}) == prefix + "too short");
|
|
CHECK(exception_of([&]
|
|
{
|
|
const json_document d = json_document::load(image.data(), 63);
|
|
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");
|
|
bad = image;
|
|
bad[4] = 2; // version
|
|
CHECK(load_result(bad, image_check::full) == prefix + "unknown format");
|
|
for (std::size_t reserved = 32; reserved < 64; reserved += 8)
|
|
{
|
|
bad = image;
|
|
bad[reserved + 3] = 1;
|
|
CHECK(load_result(bad, image_check::none) == prefix + "unknown format");
|
|
}
|
|
|
|
const auto sizes = [&](std::size_t offset, std::uint64_t v)
|
|
{
|
|
std::vector<std::uint8_t> b = image;
|
|
set_header_field(b, offset, v);
|
|
return load_result(b, image_check::none);
|
|
};
|
|
CHECK(sizes(8, 0) == prefix + "sizes out of range"); // no nodes
|
|
CHECK(sizes(8, 1000) == prefix + "sizes out of range"); // more nodes than bytes
|
|
CHECK(sizes(8, 0xFFFFFFF0u) == prefix + "sizes out of range");
|
|
CHECK(sizes(16, 0xFFFFFFF0u) == prefix + "sizes out of range"); // text size
|
|
CHECK(sizes(16, header_field(image, 16) + 1) == prefix + "sizes out of range");
|
|
CHECK(sizes(16, image.size()) == prefix + "sizes out of range");
|
|
CHECK(sizes(24, 0xFFFFFFF0u) == prefix + "sizes out of range"); // decoded string size
|
|
CHECK(sizes(24, header_field(image, 24) - 1) == prefix + "sizes out of range");
|
|
|
|
// the NULs after the text and the decoded strings
|
|
bad = image;
|
|
bad[text_at(image) + header_size_field(image, 16)] = 'x';
|
|
CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range");
|
|
bad = image;
|
|
bad.back() = 'x';
|
|
CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range");
|
|
// nothing after the image
|
|
bad = image;
|
|
bad.push_back(0);
|
|
CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range");
|
|
// nodes, but not even room for the NULs
|
|
bad.assign(image.begin(), image.begin() + static_cast<std::ptrdiff_t>(text_at(image)));
|
|
CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json_view images: check")
|
|
{
|
|
// nodes: 0 { 1 "s" 2 "x\"y" (escaped) 3 "i" 4 -12 5 "u" 6 7 7 "f" 8 1.5e300 9 "b" 10 true 11 "n" 12 null
|
|
// 13 "a" 14 [ 15 "t" 16 {} ]
|
|
const std::string text = R"({"s":"x\"y","i":-12,"u":7,"f":1.5e300,"b":true,"n":null,"a":["t",{}]})";
|
|
const std::vector<std::uint8_t> image = json_document::parse(text).save();
|
|
REQUIRE(load_result(image, image_check::full).empty());
|
|
REQUIRE(node_count(image) == 17);
|
|
|
|
// bounds: rejected by both checks; content: only by the full one
|
|
const auto rejected = [&](const std::vector<std::uint8_t>& b, bool bounds)
|
|
{
|
|
CHECK(load_result(b, image_check::full) == check_failed);
|
|
CHECK(load_result(b, image_check::bounds) == (bounds ? check_failed : ""));
|
|
};
|
|
|
|
SECTION("kinds")
|
|
{
|
|
const std::array<std::uint8_t, 4> kinds = {{8, 9, 10, 200}}; // binary, discarded, link, unknown
|
|
for (const std::uint8_t kind : kinds)
|
|
{
|
|
rejected(corrupted(image, 12, [&](node & n)
|
|
{
|
|
n.kind = static_cast<nlohmann::detail::view::node_kind>(kind);
|
|
}), true);
|
|
}
|
|
// a key that is not a string
|
|
rejected(corrupted(image, 1, [](node & n)
|
|
{
|
|
n.kind = nlohmann::detail::view::node_kind::null;
|
|
n.len = 0;
|
|
n.off = 0;
|
|
}), true);
|
|
}
|
|
|
|
SECTION("flags and extra")
|
|
{
|
|
rejected(corrupted(image, 12, [](node & n)
|
|
{
|
|
n.flags = 4;
|
|
}), true);
|
|
rejected(corrupted(image, 12, [](node & n)
|
|
{
|
|
n.extra = 1;
|
|
}), true);
|
|
rejected(corrupted(image, 10, [](node & n)
|
|
{
|
|
n.flags = 5;
|
|
}), true);
|
|
rejected(corrupted(image, 10, [](node & n)
|
|
{
|
|
n.extra = 1;
|
|
}), true);
|
|
rejected(corrupted(image, 1, [](node & n)
|
|
{
|
|
n.flags = 2; // a string in the edit arena
|
|
}), true);
|
|
rejected(corrupted(image, 1, [](node & n)
|
|
{
|
|
n.extra = 3;
|
|
}), true);
|
|
rejected(corrupted(image, 4, [](node & n)
|
|
{
|
|
n.flags = 2;
|
|
}), true);
|
|
rejected(corrupted(image, 4, [](node & n)
|
|
{
|
|
n.extra = static_cast<std::uint16_t>(n.extra | 0x100u); // an integer with fraction digits
|
|
}), true);
|
|
rejected(corrupted(image, 0, [](node & n)
|
|
{
|
|
n.flags = 8; // moved
|
|
}), true);
|
|
rejected(corrupted(image, 0, [](node & n)
|
|
{
|
|
n.extra = 1; // a hash index
|
|
}), true);
|
|
// the flag of the targets of links (editable documents) is not part of an image
|
|
for (std::size_t i = 0; i < node_count(image); ++i)
|
|
{
|
|
CAPTURE(i)
|
|
rejected(corrupted(image, i, [](node & n)
|
|
{
|
|
n.flags = static_cast<std::uint8_t>(n.flags | nlohmann::detail::view::node_flags::linked);
|
|
}), true);
|
|
}
|
|
}
|
|
|
|
SECTION("bounds")
|
|
{
|
|
const std::size_t text_size = header_size_field(image, 16);
|
|
const std::size_t arena_size = header_size_field(image, 24);
|
|
rejected(corrupted(image, 1, [&](node & n)
|
|
{
|
|
n.off = static_cast<std::uint32_t>(text_size + 1);
|
|
}), true);
|
|
rejected(corrupted(image, 1, [&](node & n)
|
|
{
|
|
n.len = static_cast<std::uint32_t>(text_size);
|
|
}), true);
|
|
rejected(corrupted(image, 2, [&](node & n)
|
|
{
|
|
n.len = static_cast<std::uint32_t>(arena_size + 1);
|
|
}), true);
|
|
rejected(corrupted(image, 6, [&](node & n)
|
|
{
|
|
n.off = static_cast<std::uint32_t>(text_size);
|
|
}), true);
|
|
rejected(corrupted(image, 6, [&](node & n)
|
|
{
|
|
n.off = static_cast<std::uint32_t>(text_size + 5);
|
|
}), true);
|
|
rejected(corrupted(image, 6, [](node & n)
|
|
{
|
|
n.extra = 0; // no digits
|
|
}), true);
|
|
rejected(corrupted(image, 8, [&](node & n)
|
|
{
|
|
n.len = static_cast<std::uint32_t>(text_size);
|
|
}), true);
|
|
rejected(corrupted(image, 8, [](node & n)
|
|
{
|
|
n.len = 2; // shorter than the recorded digits
|
|
}), true);
|
|
rejected(corrupted(image, 14, [&](node & n)
|
|
{
|
|
n.off = static_cast<std::uint32_t>(text_size + 1);
|
|
}), true);
|
|
// literals: their offset sizes the output of dump()
|
|
rejected(corrupted(image, 10, [&](node & n)
|
|
{
|
|
n.off = static_cast<std::uint32_t>(text_size + 1);
|
|
}), true);
|
|
rejected(corrupted(image, 12, [&](node & n)
|
|
{
|
|
n.off = 0xFFFFFFFFu;
|
|
}), true);
|
|
}
|
|
|
|
SECTION("structure")
|
|
{
|
|
rejected(corrupted(image, 0, [](node & n)
|
|
{
|
|
n.next = 0;
|
|
}), true);
|
|
rejected(corrupted(image, 0, [](node & n)
|
|
{
|
|
n.next = 18; // beyond the image
|
|
}), true);
|
|
rejected(corrupted(image, 14, [](node & n)
|
|
{
|
|
n.next = 4; // beyond the enclosing object
|
|
}), true);
|
|
rejected(corrupted(image, 0, [](node & n)
|
|
{
|
|
n.len = 6; // member count
|
|
}), true);
|
|
rejected(corrupted(image, 14, [](node & n)
|
|
{
|
|
n.len = 3; // element count
|
|
}), true);
|
|
rejected(corrupted(image, 0, [](node & n)
|
|
{
|
|
n.next = 14; // the object ends after the key "a"
|
|
n.len = 7;
|
|
}), true);
|
|
rejected(corrupted(image, 0, [](node & n)
|
|
{
|
|
n.next = 13; // nodes after the root
|
|
n.len = 6;
|
|
}), true);
|
|
rejected(corrupted(image, 0, [](node & n)
|
|
{
|
|
n.kind = nlohmann::detail::view::node_kind::array; // an array: the "keys" are values, and the counts do not match
|
|
}), true);
|
|
const std::vector<std::uint8_t> as_array = corrupted(image, 16, [](node & n)
|
|
{
|
|
n.kind = nlohmann::detail::view::node_kind::array; // {} as []: fine
|
|
});
|
|
CHECK(load_result(as_array, image_check::full).empty());
|
|
const std::string expected_dump = R"({"s":"x\"y","i":-12,"u":7,"f":1.5e+300,"b":true,"n":null,"a":["t",[]]})";
|
|
CHECK(loaded_dump(as_array) == expected_dump);
|
|
}
|
|
|
|
SECTION("strings")
|
|
{
|
|
// a quote in a source string (the full check only)
|
|
std::vector<std::uint8_t> b = image;
|
|
const std::size_t t = text_at(image);
|
|
const node t15 = node_at(image, 15);
|
|
b[t + t15.off] = '"';
|
|
rejected(b, false);
|
|
// a control character
|
|
b[t + t15.off] = '\n';
|
|
rejected(b, false);
|
|
// invalid UTF-8 in a decoded string
|
|
b = image;
|
|
const node s2 = node_at(image, 2);
|
|
b[t + header_size_field(image, 16) + 1 + s2.off] = 0xFF;
|
|
rejected(b, false);
|
|
}
|
|
|
|
SECTION("numbers")
|
|
{
|
|
const std::size_t t = text_at(image);
|
|
const node i4 = node_at(image, 4);
|
|
const node u6 = node_at(image, 6);
|
|
const node f8 = node_at(image, 8);
|
|
const auto at_token = [&](const node & n, std::size_t k, std::uint8_t c)
|
|
{
|
|
std::vector<std::uint8_t> b = image;
|
|
b[t + n.off + k] = c;
|
|
return b;
|
|
};
|
|
rejected(at_token(i4, 1, 'x'), false); // -x2
|
|
rejected(at_token(i4, 1, '0'), false); // -02
|
|
rejected(at_token(i4, 0, '1'), false); // 112 != -12
|
|
rejected(at_token(f8, 1, 'x'), false); // 1x5e300
|
|
rejected(at_token(f8, 2, 'e'), false); // 1.ee300
|
|
rejected(at_token(f8, 4, 'x'), false); // 1.5ex00
|
|
rejected(at_token(f8, 3, '0'), false); // 1.50300: another layout
|
|
rejected(at_token(f8, 4, '9'), false); // 1.5e900: overflow
|
|
rejected(at_token(f8, 0, 'x'), false);
|
|
rejected(at_token(u6, 0, '8'), false); // 8 != 7
|
|
rejected(corrupted(image, 4, [](node & n)
|
|
{
|
|
n.kind = nlohmann::detail::view::node_kind::number_unsigned; // "-12" as unsigned: a sign
|
|
n.extra = 3;
|
|
}), false);
|
|
// a non-negative number_integer (as edits write it): fine
|
|
const std::vector<std::uint8_t> positive = corrupted(image, 6, [](node & n)
|
|
{
|
|
n.kind = nlohmann::detail::view::node_kind::number_integer;
|
|
n.extra = 0;
|
|
});
|
|
CHECK(load_result(positive, image_check::full).empty());
|
|
const json_document pos = json_document::load(positive);
|
|
CHECK(pos.root()["u"].is_number_integer());
|
|
rejected(corrupted(image, 8, [](node & n)
|
|
{
|
|
n.kind = nlohmann::detail::view::node_kind::number_unsigned; // a float token as integer
|
|
n.extra = 7;
|
|
}), false);
|
|
}
|
|
|
|
SECTION("float tokens of an image checked for bounds only")
|
|
{
|
|
// A float node whose layout records "many" digits is converted from
|
|
// its token alone; a token that is not a JSON number reads as 0.
|
|
const std::vector<std::uint8_t> img = json_document::parse("[1.5e300,2]").save();
|
|
const std::size_t t = text_at(img);
|
|
for (const char* token :
|
|
{
|
|
"x.5e300", "01.5e30", "1.xe300", "1.5ex00", "1.5e+x0", "1.5e30x", "-.5e300", "1.5E300"
|
|
})
|
|
{
|
|
CAPTURE(token)
|
|
std::vector<std::uint8_t> b = img;
|
|
node n = node_at(b, 1);
|
|
n.extra = 0xFFFFu;
|
|
set_node(b, 1, n);
|
|
std::memcpy(b.data() + t + n.off, token, n.len);
|
|
const json_document d = json_document::load(b, image_check::bounds);
|
|
const auto v = d.root()[0].get<double>();
|
|
CHECK(v == (std::string(token) == "1.5E300" ? 1.5e300 : 0.0));
|
|
CHECK(load_result(b, image_check::full) == (std::string(token) == "1.5E300" ? "" : check_failed));
|
|
}
|
|
}
|
|
|
|
SECTION("float tokens with fewer digits than the layout records")
|
|
{
|
|
// The bytes of the token are not digits (they read as zeros or as
|
|
// other values), so the value has fewer (or more) digits than the
|
|
// layout says: dump() must still write a number.
|
|
for (const auto& source : std::vector<std::pair<std::string, std::string>>
|
|
{
|
|
{"123456789012345678.5", std::string("@") + std::string(16, '0') + "1.1"}, // 19 digits
|
|
{"1234.5", "@001.1"}, // 5 digits
|
|
{"1234.5", "9??.??"} // more than 5 digits
|
|
})
|
|
{
|
|
CAPTURE(source.second)
|
|
const std::vector<std::uint8_t> img = json_document::parse("[" + source.first + "]").save();
|
|
const node n = node_at(img, 1);
|
|
REQUIRE(source.second.size() == n.len);
|
|
std::vector<std::uint8_t> b = img;
|
|
std::memcpy(b.data() + text_at(img) + n.off, source.second.data(), n.len);
|
|
const json_document d = json_document::load(b, image_check::bounds);
|
|
const std::string dumped = d.root().dump();
|
|
CAPTURE(dumped)
|
|
CHECK(json::parse(dumped)[0].is_number());
|
|
}
|
|
}
|
|
|
|
SECTION("nodes that share a range")
|
|
{
|
|
// [big string, then n strings made to point to the big string]: every
|
|
// node shares the one range (the check reads it once)
|
|
const std::size_t n = 200;
|
|
std::string long_text = "[\"" + std::string(5000, 'a') + "\"";
|
|
for (std::size_t k = 0; k < n; ++k)
|
|
{
|
|
long_text += ",\"x\"";
|
|
}
|
|
long_text += "]";
|
|
const std::vector<std::uint8_t> img = json_document::parse(long_text).save();
|
|
const node big = node_at(img, 1);
|
|
std::vector<std::uint8_t> b = img;
|
|
for (std::size_t k = 0; k < n; ++k)
|
|
{
|
|
set_node(b, 2 + k, big);
|
|
}
|
|
CHECK(load_result(b, image_check::full).empty());
|
|
const json_document d = json_document::load(b);
|
|
CHECK(d.root().size() == n + 1);
|
|
CHECK(d.root()[n].get<std::string>() == std::string(5000, 'a'));
|
|
CHECK(d.root().dump() == loaded_dump(b, image_check::none));
|
|
|
|
// the same for decoded strings and float tokens, with a node that
|
|
// records another digit layout than its token
|
|
const std::vector<std::uint8_t> img2 = json_document::parse(R"(["a\"b", "a\"b", 1.25, 1.25, 1.25e3])").save();
|
|
CHECK(load_result(img2, image_check::full).empty());
|
|
std::vector<std::uint8_t> same = img2;
|
|
set_node(same, 2, node_at(img2, 1));
|
|
set_node(same, 4, node_at(img2, 3));
|
|
CHECK(load_result(same, image_check::full).empty());
|
|
// (a raw string with a backslash must not be a macro argument: MSVC C2017)
|
|
const std::string expected_same = R"(["a\"b","a\"b",1.25,1.25,1250.0])";
|
|
CHECK(loaded_dump(same) == expected_same);
|
|
std::vector<std::uint8_t> layout = same;
|
|
node f4 = node_at(layout, 4);
|
|
f4.extra = 0x0100u; // the layout of "1.", and not that of "1.25"
|
|
set_node(layout, 4, f4);
|
|
rejected(layout, false);
|
|
f4.extra = 0xFFFFu; // "many" digits: fine, as compaction writes it
|
|
set_node(layout, 4, f4);
|
|
CHECK(load_result(layout, image_check::full).empty());
|
|
}
|
|
|
|
SECTION("ranges that overlap")
|
|
{
|
|
// save() writes every string and every token to a place of its own
|
|
// (nodes that share a value share the whole range): ranges that
|
|
// overlap without being identical are a damaged image, though each
|
|
// range is a valid string or token
|
|
// nodes: 0 [ 1 "abcdef" 2 "ghijkl" 3 1.2525 4 9.9 5 "a\"bcd" (decoded) 6 "e\"fgh" (decoded)
|
|
const std::vector<std::uint8_t> img = json_document::parse(R"(["abcdef", "ghijkl", 1.2525, 9.9, "a\"bcd", "e\"fgh"])").save();
|
|
REQUIRE(load_result(img, image_check::full).empty());
|
|
// node i with the range (off of node of + shift, length), and extra
|
|
const auto aliased = [&](std::size_t i, std::size_t of, std::uint32_t shift, std::uint32_t length, std::uint16_t extra)
|
|
{
|
|
std::vector<std::uint8_t> b = img;
|
|
node n = node_at(b, of);
|
|
n.off += shift;
|
|
n.len = length;
|
|
n.extra = extra;
|
|
set_node(b, i, n);
|
|
return b;
|
|
};
|
|
// source strings
|
|
rejected(aliased(2, 1, 0, 4, 0), false); // "abcd": the start of another string
|
|
rejected(aliased(2, 1, 1, 4, 0), false); // "bcde": inside
|
|
rejected(aliased(2, 1, 2, 4, 0), false); // "cdef": the end
|
|
CHECK(load_result(aliased(2, 1, 0, 6, 0), image_check::full).empty()); // the whole range: identical
|
|
// decoded strings: inside, and partially overlapping (the arena holds a"bcde"fgh)
|
|
rejected(aliased(6, 5, 1, 3, 0), false);
|
|
rejected(aliased(6, 5, 3, 5, 0), false);
|
|
CHECK(load_result(aliased(6, 5, 0, 5, 0), image_check::full).empty());
|
|
// float tokens: "1.25" and "525" (an integer token as a float) inside "1.2525"
|
|
rejected(aliased(4, 3, 0, 4, 0x0201u), false);
|
|
rejected(aliased(4, 3, 3, 3, 0x0003u), false);
|
|
CHECK(load_result(aliased(4, 3, 0, 6, 0x0401u), image_check::full).empty());
|
|
// a string and a float token may use the same bytes (each is checked by its own kind)
|
|
std::vector<std::uint8_t> shared = img;
|
|
node as_string = node_at(img, 2);
|
|
as_string.off = node_at(img, 3).off;
|
|
as_string.len = 6;
|
|
set_node(shared, 2, as_string);
|
|
CHECK(load_result(shared, image_check::full).empty());
|
|
// empty strings inside others are not ranges
|
|
CHECK(load_result(aliased(2, 1, 2, 0, 0), image_check::full).empty());
|
|
}
|
|
|
|
SECTION("copies within an edited document")
|
|
{
|
|
// copies within a document share the value: identical ranges
|
|
json_editable_document d = json_editable_document::parse(R"({"s": "ab", "e": "x\"y", "f": 1.5, "g": 1.5e300, "a": [1.5, "ab"]})");
|
|
d.set(d.root(), "c1", d.root()["a"]);
|
|
d.set(d.root(), "c2", d.root()["a"]);
|
|
d.set(d.root(), "c3", d.root());
|
|
d.push_back(d.root()["a"], d.root()["e"]);
|
|
d.push_back(d.root()["a"], d.root()["g"]);
|
|
d.push_back(d.root()["a"], d.root()["g"]);
|
|
d.set(d.root()["s"], "changed");
|
|
d.set(d.root()["f"], 7.25);
|
|
const std::vector<std::uint8_t> saved = d.save();
|
|
CHECK(load_result(saved, image_check::full).empty());
|
|
CHECK(loaded_dump(saved) == d.root().dump());
|
|
check_round_trip(d);
|
|
}
|
|
|
|
SECTION("integer ranges")
|
|
{
|
|
// tokens of many digits, which the parser stores as floats
|
|
const std::string big = R"([123456789012345678901234, 99999999999999999999, 9223372036854775808])";
|
|
const std::vector<std::uint8_t> img = json_document::parse(big).save();
|
|
const auto as_integer = [&](std::size_t i, std::uint8_t kind, std::uint16_t extra)
|
|
{
|
|
std::vector<std::uint8_t> b = img;
|
|
node n = node_at(b, i);
|
|
n.kind = static_cast<nlohmann::detail::view::node_kind>(kind);
|
|
n.extra = extra;
|
|
set_node(b, i, n);
|
|
return load_result(b, image_check::full);
|
|
};
|
|
CHECK(as_integer(1, 6, 24) == check_failed); // more than 20 digits
|
|
CHECK(as_integer(2, 6, 20) == check_failed); // more than 2^64 - 1
|
|
CHECK(as_integer(3, 5, 18) == check_failed); // more than 2^63 - 1 as number_integer
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json_view images: damaged images")
|
|
{
|
|
// A damaged image must be rejected, or read safely; with the full check,
|
|
// it also serializes to the JSON it reads as.
|
|
const std::vector<std::string> texts =
|
|
{
|
|
R"({"a": [1, -2, 3.25, "x\u00e9y", true, null], "b": {"c": "\"q\"", "d": 1e10}, "e": ""})",
|
|
R"([[[[]]], {"k": {"k": {"k": 12345678901234567890}}}, "\ud83d\ude00", -0.0, 0])",
|
|
};
|
|
for (const std::string& text : texts)
|
|
{
|
|
const std::vector<std::uint8_t> image = json_document::parse(text).save();
|
|
for (int round = 0; round < 3000; ++round)
|
|
{
|
|
std::vector<std::uint8_t> b = image;
|
|
const std::uint32_t flips = 1 + (rng() % 3);
|
|
for (std::uint32_t k = 0; k < flips; ++k)
|
|
{
|
|
// mostly the nodes, where the damage matters most
|
|
const std::size_t at = rng() % 4 != 0 ? header_size + (rng() % (b.size() - header_size)) : rng() % b.size();
|
|
b[at] = static_cast<std::uint8_t>(rng() % 3 == 0 ? rng() : b[at] ^ (1u << (rng() % 8)));
|
|
}
|
|
for (const image_check check :
|
|
{
|
|
image_check::full, image_check::bounds
|
|
})
|
|
{
|
|
json_document d;
|
|
try
|
|
{
|
|
d = json_document::load(b, check);
|
|
}
|
|
catch (const json::parse_error& e)
|
|
{
|
|
CHECK(e.id == 116);
|
|
continue;
|
|
}
|
|
std::string dumped;
|
|
std::string dumped_ascii;
|
|
try
|
|
{
|
|
dumped = d.root().dump();
|
|
dumped_ascii = d.root().dump(-1, ' ', true);
|
|
}
|
|
catch (const json::type_error& e)
|
|
{
|
|
// invalid UTF-8 (the bounds check only)
|
|
CHECK(check == image_check::bounds);
|
|
CHECK(e.id == 316);
|
|
continue;
|
|
}
|
|
const json j = d.root().materialize();
|
|
if (check == image_check::full)
|
|
{
|
|
CHECK(json::parse(dumped) == j);
|
|
CHECK(json::parse(dumped_ascii) == j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#else
|
|
|
|
TEST_CASE("json_view images: big-endian targets")
|
|
{
|
|
// the image format is little-endian: save() and load() do not exist here
|
|
static_assert(!json_view_image_test::has_save<json_document>::value, "no save() on a big-endian target");
|
|
static_assert(!json_view_image_test::has_load<json_document>::value, "no load() on a big-endian target");
|
|
static_assert(!json_view_image_test::has_save<json_editable_document>::value, "no save() on a big-endian target");
|
|
const json_document d = json_document::parse("[1]");
|
|
CHECK(d.root().size() == 1);
|
|
}
|
|
|
|
#endif
|