mirror of
https://github.com/nlohmann/json.git
synced 2026-09-30 14:05:18 +00:00
* Add BON8 support Add to_bon8/from_bon8 and input_format_t::bon8 for BON8, a binary format that uses the byte values that cannot begin a UTF-8 character as type markers, so strings need no length prefix. It is the most compact of the supported binary formats on the benchmark files. The reader is non-recursive like the other binary readers. A string ends at the first byte that cannot continue it, so the reader hands the one or two bytes it reads past a string back to the value that follows. The writer produces the canonical representation of the specification, except for NFC normalization; its output is identical to that of the reference implementation (HikoGUI) on all files of the test data. The round-trip tests need the .bon8 files of json_test_data 3.2.0. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Address review comments - Reuse detail::validate_one_utf8 to check strings in to_bon8; the error now names the first byte of the invalid sequence. - Document that to_bon8 leaves bytes in the output adapter on an exception, and that string_open is only an output of write_bon8_marker. - Explain why the pushback buffer of the BON8 reader cannot overflow. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Select the BON8 float prefix by type get_bon8_float_prefix only depends on the type of its argument, so make the type a template parameter instead of passing an unused value. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Rename a test variable that Flawfinder mistakes for read() Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix the BON8 CI failures - compare the float in write_bon8_float with number_float_t constants, so GCC does not warn about a float-to-double conversion - mark check_bon8_utf8's context as used when exceptions are disabled - choose the compact float prefix in a helper rather than with nested conditional operators (clang-tidy) - use auto for the cast in the BON8 integer reader (clang-tidy) - write the int32 minimum test values as long long literals (MSVC C4146) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Amalgamate Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Read BON8 strings in bulk from contiguous input - copy the valid UTF-8 of a string in one step when the input is contiguous (twitter.json is read in 1.68 instead of 2.52 ms, jeopardy.json in 196 instead of 297 ms, close to CBOR and MessagePack) - share the new valid_utf8_prefix() with the writer's UTF-8 check, which now skips ASCII 8 bytes at a time - let the fuzzer check that contiguous and stream input give the same value or error, and test both paths in the unit tests - clarify that a second 0xFF after a string is an empty string Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Link the BON8 functions from the other binary format pages Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Name the bulk scan flag after the input, not BON8 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Read BSON keys in bulk from contiguous input BSON keys (and array indices) are C-style strings, which were read byte by byte. For contiguous input they are now read up to their \x00-byte in one step, using the same bulk_scan flag as BON8 strings: twitter.json is read in 1.46 instead of 2.01 ms, citm_catalog.json in 2.93 instead of 3.33 ms, jeopardy.json in 182 instead of 207 ms. canada.json, whose keys are almost all one-digit array indices, takes 2 % longer. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix the BON8 CI failures of the bulk-read tests - skip the contiguous-versus-stream tests of BON8 strings and BSON keys when exceptions are disabled: they catch the parse errors of invalid input, and without exceptions the library aborts instead - use static_cast for the int64 test value (google-readability-casting) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Move the explicit basic_json instantiation into its own test file Linking test-regression3_cpp20 with clang and MinGW failed with "relocation truncated to fit: IMAGE_REL_AMD64_REL32 against `.rdata'", as test-regression2 did before #5511. The explicit instantiation of basic_json<> for #4825 compiles every member function, including the BON8 reader and writer, into that object, and it was already close to the limit (2,226,104 bytes on develop, 2,234,960 with BON8; clang -O1, C++20). Give the instantiation a file of its own: unit-regression3 is now 1,594,736 bytes and unit-explicit_instantiation 1,095,064. The new file mentions JSON_HAS_CPP_17 and JSON_HAS_CPP_20 so it keeps being built for the C++17 standard the regression was about. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Convert the bytes of the BON8 test strings explicitly The str() helper constructed a std::string from a byte range, which converts each unsigned char implicitly; -fsanitize=integer reports that for bytes of 0x80 and above (ci_test_clang_sanitizer). Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
506 lines
15 KiB
C++
506 lines
15 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-FileCopyrightText: 2018 Vitaliy Manushkin <agri@akamo.info>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// This file closes a test-coverage gap described in GitHub issue #5421:
|
|
// nlohmann::ordered_json (and other non-default basic_json specializations,
|
|
// such as the alt_string-based one from unit-alt-string.cpp) were never
|
|
// exercised through the binary formats (CBOR/MessagePack/UBJSON/BSON/BJData)
|
|
// or through flatten()/unflatten()/diff()/patch()/merge_patch().
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using nlohmann::json;
|
|
using nlohmann::ordered_json;
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// alt_json: a second, independent copy of the custom-string_t basic_json
|
|
// specialization defined in unit-alt-string.cpp.
|
|
//
|
|
// It is duplicated here (rather than shared via a header) because every
|
|
// unit-*.cpp file in this test suite is compiled into its own standalone
|
|
// executable (see tests/CMakeLists.txt), so there is no ODR concern in
|
|
// having the same class name defined in multiple translation units.
|
|
//
|
|
// Two members had to be added relative to the original alt_string
|
|
// (a constructor from std::string, and a find(char, pos) overload) because
|
|
// the original type was never used with the binary writers/readers before
|
|
// this file: BSON's array/document writer converts std::to_string() results
|
|
// and checks for embedded NUL characters via find(char), and the UBJSON/BSON
|
|
// high-precision-number path constructs the SAX string_t argument from a
|
|
// std::string. Neither path is exercised anywhere else in the test suite for
|
|
// this type, which is presumably why the gap was never noticed.
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
class alt_string;
|
|
bool operator<(const char* op1, const alt_string& op2) noexcept; // NOLINT(misc-use-internal-linkage)
|
|
void int_to_string(alt_string& target, std::size_t value); // NOLINT(misc-use-internal-linkage)
|
|
|
|
class alt_string
|
|
{
|
|
public:
|
|
using value_type = std::string::value_type;
|
|
|
|
static constexpr auto npos = (std::numeric_limits<std::size_t>::max)();
|
|
|
|
alt_string(const char* str): str_impl(str) {}
|
|
alt_string(const char* str, std::size_t count): str_impl(str, count) {}
|
|
alt_string(std::string str): str_impl(std::move(str)) {}
|
|
alt_string(size_t count, char chr): str_impl(count, chr) {}
|
|
alt_string() = default;
|
|
|
|
alt_string& append(char ch)
|
|
{
|
|
str_impl.push_back(ch);
|
|
return *this;
|
|
}
|
|
|
|
alt_string& append(const alt_string& str)
|
|
{
|
|
str_impl.append(str.str_impl);
|
|
return *this;
|
|
}
|
|
|
|
alt_string& append(const char* s, std::size_t length)
|
|
{
|
|
str_impl.append(s, length);
|
|
return *this;
|
|
}
|
|
|
|
void push_back(char c)
|
|
{
|
|
str_impl.push_back(c);
|
|
}
|
|
|
|
template <typename op_type>
|
|
bool operator==(const op_type& op) const
|
|
{
|
|
return str_impl == op;
|
|
}
|
|
|
|
bool operator==(const alt_string& op) const
|
|
{
|
|
return str_impl == op.str_impl;
|
|
}
|
|
|
|
template <typename op_type>
|
|
bool operator!=(const op_type& op) const
|
|
{
|
|
return str_impl != op;
|
|
}
|
|
|
|
bool operator!=(const alt_string& op) const
|
|
{
|
|
return str_impl != op.str_impl;
|
|
}
|
|
|
|
std::size_t size() const noexcept
|
|
{
|
|
return str_impl.size();
|
|
}
|
|
|
|
void resize(std::size_t n)
|
|
{
|
|
str_impl.resize(n);
|
|
}
|
|
|
|
void resize(std::size_t n, char c)
|
|
{
|
|
str_impl.resize(n, c);
|
|
}
|
|
|
|
template <typename op_type>
|
|
bool operator<(const op_type& op) const noexcept
|
|
{
|
|
return str_impl < op;
|
|
}
|
|
|
|
bool operator<(const alt_string& op) const noexcept
|
|
{
|
|
return str_impl < op.str_impl;
|
|
}
|
|
|
|
const char* c_str() const
|
|
{
|
|
return str_impl.c_str();
|
|
}
|
|
|
|
char& operator[](std::size_t index)
|
|
{
|
|
return str_impl[index];
|
|
}
|
|
|
|
const char& operator[](std::size_t index) const
|
|
{
|
|
return str_impl[index];
|
|
}
|
|
|
|
char& back()
|
|
{
|
|
return str_impl.back();
|
|
}
|
|
|
|
const char& back() const
|
|
{
|
|
return str_impl.back();
|
|
}
|
|
|
|
void clear()
|
|
{
|
|
str_impl.clear();
|
|
}
|
|
|
|
const value_type* data() const
|
|
{
|
|
return str_impl.data();
|
|
}
|
|
|
|
bool empty() const
|
|
{
|
|
return str_impl.empty();
|
|
}
|
|
|
|
std::size_t find(const alt_string& str, std::size_t pos = 0) const
|
|
{
|
|
return str_impl.find(str.str_impl, pos);
|
|
}
|
|
|
|
// needed by binary_writer's BSON support, which probes string keys for
|
|
// embedded NUL characters via find(char)
|
|
std::size_t find(char c, std::size_t pos = 0) const
|
|
{
|
|
return str_impl.find(c, pos);
|
|
}
|
|
|
|
std::size_t find_first_of(char c, std::size_t pos = 0) const
|
|
{
|
|
return str_impl.find_first_of(c, pos);
|
|
}
|
|
|
|
alt_string substr(std::size_t pos = 0, std::size_t count = npos) const
|
|
{
|
|
const std::string s = str_impl.substr(pos, count);
|
|
return {s.data(), s.size()};
|
|
}
|
|
|
|
alt_string& replace(std::size_t pos, std::size_t count, const alt_string& str)
|
|
{
|
|
str_impl.replace(pos, count, str.str_impl);
|
|
return *this;
|
|
}
|
|
|
|
void reserve(std::size_t new_cap = 0)
|
|
{
|
|
str_impl.reserve(new_cap);
|
|
}
|
|
|
|
private:
|
|
std::string str_impl {}; // NOLINT(readability-redundant-member-init)
|
|
|
|
friend bool operator<(const char* /*op1*/, const alt_string& /*op2*/) noexcept;
|
|
};
|
|
|
|
void int_to_string(alt_string& target, std::size_t value)
|
|
{
|
|
target = std::to_string(value).c_str();
|
|
}
|
|
|
|
using alt_json = nlohmann::basic_json <
|
|
std::map,
|
|
std::vector,
|
|
alt_string,
|
|
bool,
|
|
std::int64_t,
|
|
std::uint64_t,
|
|
double,
|
|
std::allocator,
|
|
nlohmann::adl_serializer >;
|
|
|
|
bool operator<(const char* op1, const alt_string& op2) noexcept
|
|
{
|
|
return op1 < op2.str_impl;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
// collects the object keys of j, in iteration order
|
|
std::vector<std::string> collect_keys(const ordered_json& j)
|
|
{
|
|
std::vector<std::string> result;
|
|
for (auto it = j.cbegin(); it != j.cend(); ++it)
|
|
{
|
|
result.push_back(it.key());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// a nested object/array value with keys inserted in non-alphabetical order,
|
|
// used to check both round-trip equality and (for ordered_json) that
|
|
// insertion order survives a trip through a binary format
|
|
ordered_json make_rich_ordered_json()
|
|
{
|
|
ordered_json j;
|
|
j["zebra"] = 1;
|
|
j["apple"] = ordered_json::array({1, 2, 3});
|
|
j["mango"]["z_nested"] = true;
|
|
j["mango"]["a_nested"] = nullptr;
|
|
j["banana"] = "some text";
|
|
j["cherry"] = 3.14;
|
|
return j;
|
|
}
|
|
|
|
alt_json make_rich_alt_json()
|
|
{
|
|
alt_json j;
|
|
j["zebra"] = 1;
|
|
j["apple"] = alt_json::array({1, 2, 3});
|
|
j["mango"]["z_nested"] = true;
|
|
j["mango"]["a_nested"] = nullptr;
|
|
j["banana"] = "some text";
|
|
j["cherry"] = 3.14;
|
|
return j;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("ordered_json across binary formats")
|
|
{
|
|
const ordered_json original = make_rich_ordered_json();
|
|
const std::vector<std::string> original_keys = collect_keys(original);
|
|
const std::vector<std::string> original_mango_keys = collect_keys(original["mango"]);
|
|
|
|
SECTION("CBOR")
|
|
{
|
|
const auto bytes = ordered_json::to_cbor(original);
|
|
const auto restored = ordered_json::from_cbor(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("MessagePack")
|
|
{
|
|
const auto bytes = ordered_json::to_msgpack(original);
|
|
const auto restored = ordered_json::from_msgpack(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("UBJSON")
|
|
{
|
|
const auto bytes = ordered_json::to_ubjson(original);
|
|
const auto restored = ordered_json::from_ubjson(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("BSON")
|
|
{
|
|
const auto bytes = ordered_json::to_bson(original);
|
|
const auto restored = ordered_json::from_bson(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("BJData")
|
|
{
|
|
const auto bytes = ordered_json::to_bjdata(original);
|
|
const auto restored = ordered_json::from_bjdata(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("BON8")
|
|
{
|
|
const auto bytes = ordered_json::to_bon8(original);
|
|
const auto restored = ordered_json::from_bon8(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("alt_json (custom string_t) across binary formats")
|
|
{
|
|
const alt_json original = make_rich_alt_json();
|
|
|
|
SECTION("CBOR")
|
|
{
|
|
const auto bytes = alt_json::to_cbor(original);
|
|
const auto restored = alt_json::from_cbor(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("MessagePack")
|
|
{
|
|
const auto bytes = alt_json::to_msgpack(original);
|
|
const auto restored = alt_json::from_msgpack(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("UBJSON")
|
|
{
|
|
const auto bytes = alt_json::to_ubjson(original);
|
|
const auto restored = alt_json::from_ubjson(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("BON8")
|
|
{
|
|
const auto bytes = alt_json::to_bon8(original);
|
|
const auto restored = alt_json::from_bon8(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("BSON")
|
|
{
|
|
const auto bytes = alt_json::to_bson(original);
|
|
const auto restored = alt_json::from_bson(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("BJData")
|
|
{
|
|
const auto bytes = alt_json::to_bjdata(original);
|
|
const auto restored = alt_json::from_bjdata(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ordered_json operator== is sensitive to key order")
|
|
{
|
|
// Unlike nlohmann::json (whose object_t is a std::map, so equality never
|
|
// depends on insertion order), ordered_json's object_t (ordered_map) is a
|
|
// std::vector<std::pair<Key, T>> under the hood, and does not define its
|
|
// own operator==: it inherits std::vector's element-wise comparison. As a
|
|
// result, two ordered_json objects holding the very same key/value pairs
|
|
// in different insertion order compare *unequal*. This is the property
|
|
// that makes the round-trip `CHECK(restored == original)` checks above a
|
|
// meaningful order-preservation check by themselves (the explicit
|
|
// collect_keys() comparisons make that check explicit/readable, and
|
|
// guard against this operator== behavior ever changing).
|
|
ordered_json a;
|
|
a["x"] = 1;
|
|
a["y"] = 2;
|
|
|
|
ordered_json b;
|
|
b["y"] = 2;
|
|
b["x"] = 1;
|
|
|
|
CHECK(a.size() == b.size());
|
|
CHECK(a["x"] == b["x"]);
|
|
CHECK(a["y"] == b["y"]);
|
|
CHECK_FALSE(a == b);
|
|
}
|
|
|
|
TEST_CASE("duplicate keys in a binary-encoded object")
|
|
{
|
|
// CBOR encoding of a map with two entries under the same key "a": {"a": 1, "a": 2}
|
|
const std::vector<std::uint8_t> cbor_bytes
|
|
{
|
|
0xA2, 0x61, 'a', 0x01, 0x61, 'a', 0x02
|
|
};
|
|
|
|
// Both json (std::map, via operator[]) and ordered_json (ordered_map, via
|
|
// operator[]) build binary-decoded objects by looking up/creating the
|
|
// entry for each incoming key and then assigning the value into it. This
|
|
// means a repeated key does *not* produce two entries in either case;
|
|
// instead, the *first* occurrence's position is kept (relevant only for
|
|
// ordered_json) while the *last* occurrence's value wins (for both) --
|
|
// this matches operator[]'s "assign the referenced slot" semantics, and
|
|
// is worth noting because it differs from the initializer-list
|
|
// construction path (`ordered_json{{"a",1},{"a",2}}`), which builds
|
|
// through insert()/emplace() and therefore keeps the *first* value, not
|
|
// the last (see the "There are no dup keys..." case in
|
|
// unit-ordered_json.cpp).
|
|
const auto j = json::from_cbor(cbor_bytes);
|
|
const auto oj = ordered_json::from_cbor(cbor_bytes);
|
|
|
|
CHECK(j.size() == 1);
|
|
CHECK(oj.size() == 1);
|
|
CHECK(j["a"] == 2);
|
|
CHECK(oj["a"] == 2);
|
|
CHECK(j == json(oj));
|
|
}
|
|
|
|
TEST_CASE("ordered_json through flatten/unflatten")
|
|
{
|
|
const ordered_json original = make_rich_ordered_json();
|
|
const std::vector<std::string> original_keys = collect_keys(original);
|
|
const std::vector<std::string> original_mango_keys = collect_keys(original["mango"]);
|
|
|
|
const ordered_json flat = original.flatten();
|
|
const ordered_json unflattened = flat.unflatten();
|
|
|
|
CHECK(unflattened == original);
|
|
// flatten() walks the value depth-first in iteration order and
|
|
// unflatten() re-inserts each flattened key via operator[] in the flat
|
|
// object's iteration order, so for ordered_json the original key order
|
|
// (both top-level and nested) is preserved end-to-end.
|
|
CHECK(collect_keys(unflattened) == original_keys);
|
|
CHECK(collect_keys(unflattened["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
TEST_CASE("ordered_json through diff/patch/patch_inplace")
|
|
{
|
|
ordered_json original;
|
|
original["one"] = 1;
|
|
original["two"] = 2;
|
|
original["three"] = 3;
|
|
|
|
ordered_json target = original;
|
|
target["one"] = 100; // replace
|
|
target.erase("two"); // remove
|
|
target["four"] = 4; // add
|
|
|
|
const ordered_json patch = ordered_json::diff(original, target);
|
|
|
|
SECTION("patch")
|
|
{
|
|
const ordered_json patched = original.patch(patch);
|
|
CHECK(patched == target);
|
|
}
|
|
|
|
SECTION("patch_inplace")
|
|
{
|
|
ordered_json copy = original;
|
|
copy.patch_inplace(patch);
|
|
CHECK(copy == target);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ordered_json through merge_patch")
|
|
{
|
|
ordered_json original;
|
|
original["a"] = 1;
|
|
original["b"] = 2;
|
|
|
|
const ordered_json patch = {{"b", nullptr}, {"c", 3}};
|
|
|
|
original.merge_patch(patch);
|
|
|
|
ordered_json expected;
|
|
expected["a"] = 1;
|
|
expected["c"] = 3;
|
|
|
|
CHECK(original == expected);
|
|
CHECK(collect_keys(original) == collect_keys(expected));
|
|
}
|