Add json_document and json_view: node index, parser, and document (#5620)

* Add json_document and json_view: node index, parser, and document

Add json_document and json_view, a read-only, zero-copy index of a
JSON text, as the first public slice of the zero-copy view (#5295).

A parse produces a flat array of 16-byte nodes in document order,
one per value and one per object key. Strings stay in the source
text; escaped strings are decoded into an arena. Integers are
converted while their digits are in the cache; floats keep only
their digit layout and are converted on read. Containers store the
size of their subtree, so a reader can step over one in constant
time. A document makes a handful of allocations, however many
values it has.

The parser accepts exactly what json::parse accepts, with every
combination of ignore_comments and ignore_trailing_commas, with and
without a trailing NUL, and under JSON_STRICT_NUL_HANDLING. It is
portable C++11 and does not depend on byte order.

basic_json_document adds parse, parse_copy, accept, read (reuses a
document's memory), root, is_discarded, source, owns_source,
node_count, memory_usage, and shrink_to_fit. basic_json_view adds
type, the is_* queries, operator bool, size, empty, materialize,
and source_offset. A parse error throws the same exception
basic_json::parse would throw for the same input, message and
position included.

detail::abi_config keeps JSON_STRICT_NUL_HANDLING and
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON readable after json.hpp
undefines them, in the ABI namespace so they always match the
basic_json in use.

A NUL byte that ends a // comment is the end of the input, as in
parse() since #5696.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix document_data for old Clang (3.4-3.6)

Drop the empty braced NSDMIs of the std::string members: old Clang
rejects the defaulted constructor when it is used by a member
initializer before the end of the class definition.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Exclude libstdc++'s string_view comparison from the sign-change check

basic_string_view::_S_compare stores the difference of two lengths in a
signed difference_type on purpose, which -fsanitize=integer reports for
every comparison with a shorter view (test-json_view_cpp17).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Reject integer lengths in json_document::parse

parse(ptr, len) compiled: len converted to allow_exceptions, and ptr was
read as a C string, past the end of a buffer without a terminating NUL.
Delete the overloads of parse, parse_copy, accept, and read that take an
integer other than bool where the flags are expected.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Copy const rvalue strings and document the accepted inputs

json_document::parse(std::move(const_string)) failed to compile with
"no matching read_kind": only a non-const rvalue std::string can be moved
from. Treat a const rvalue as a copied byte container.

parse() does not accept everything BasicJsonType::parse() does: a FILE*
and pointers to or arrays of wide characters are rejected at compile time.
List the supported inputs instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Remove the conversion to bool from json_view

explicit operator bool meant "refers to a value", which silently differs
from what a basic_json converts to. Use !v.is_discarded() instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Delete json_document::root() on temporary documents

auto v = json_document::parse(text).root() compiled and left the view
dangling. Delete the overload for rvalue documents, take a named document
in the tests, and document the lifetime rule.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Store node integers by halves on big-endian targets

The non-little-endian path of the node writer wrote the integer's native
word over len and next, so len got the high half there. Compose and split
the value explicitly (len is the low half, next the high half); the
little-endian path stays a plain memcpy.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Check the node array size for overflow

reserve(n) and the growth of the index computed n * sizeof(node) without a
check, which wraps around on 32-bit targets for inputs of about 1 GiB and
allocates a too small array. Throw std::bad_alloc for a count beyond the
address space and clamp the growth step to it.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* State the exact input size limit of json_document

The check rejects inputs of 0xFFFFFFF0 bytes or more, but the exception
message and the documentation said 4 GiB. Name the limit once
(max_input_size), and state 4 GiB minus 16 bytes in the message and the
documentation. Test the limit with a container that only claims the size.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Remove the unused legacy_discarded_value_comparison from abi_config

Nothing reads it: the view does not compare values.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Align the banner comment of json_view.hpp

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fuzz json_document with exact-size buffers and parse options

The fuzzer only parsed a std::string with default options. Also parse an
exact-size byte vector, which has no NUL after its last byte and takes the
bounds-checked path, and derive ignore_comments and ignore_trailing_commas
from the first input byte, comparing against json::parse and json::accept
with the same options.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Give document_data a user-provided constructor

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix clang-tidy findings in unit-json_view_builder.cpp

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Annotate intentional patterns in unit-json_view.cpp for clang-tidy

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Suppress Flawfinder's read() finding on the deleted overload

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Check the steps of the deep-nesting test separately

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Make max_input_size a function (GCC -Wunused-const-variable)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Make json_view.hpp pass include-what-you-use

ci_single_binaries runs IWYU with --error on every header. For json_view.hpp
it suggested adding <array> (std::array is used), the headers that json.hpp
already provides (abi_config, abi_macros, input_adapters, json_pointer,
cpp_future, string_concat, value_t, json_fwd), and <version> for
std::nullptr_t, and removing <cstddef>.

- include <array>
- keep <cstddef> (nullptr_t, size_t; IWYU attributes them to <version> and <cstring>)
- tell IWYU not to suggest the headers that json.hpp provides: the amalgamated
  json_view.hpp only includes json.hpp, so including them here would duplicate
  their definitions
- export json.hpp, keep macro_unscope.hpp, and drop the unused forward declaration of the document class

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Read the node array base after emit() in the view builder's open()

emit() moves the node array when it grows, and the subtraction read base
in the same expression, so the order was unspecified. MSVC Release builds
without forced inlining read the old base; the container index then
pointed outside the array and close() wrote out of bounds.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Silence a clang-tidy use-after-move finding in the json_view test

const_text is const, so std::move does not move from it; the second call
is part of the const rvalue test.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Regenerate the amalgamated headers

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Address the review of the view builder and document

Changes promised in the replies to the review of #5620:

- read() reuses the owned copy: a copied input (an rvalue byte container,
  a stream, FILE*, wide characters, an iterator range) is appended to the
  cleared owned buffer instead of a fresh std::string being move-assigned
  over it. A moved-in std::string still replaces it.
- The error replay runs the library parser with a SAX handler that stores
  nothing and throws the reported exception, instead of
  BasicJsonType::parse(), which built a DOM up to the error.
- Pointers to and arrays of wide characters, and FILE*, take the input
  adapter path (converted to UTF-8 into an owned buffer) instead of
  failing a static_assert. Wide pointers are NUL-terminated.
- Streams are read in blocks through rdbuf() into the buffer, instead of
  character by character through the input adapter; eofbit is set at the
  end, as by the library's stream adapter.
- tape/tape_size/tape_cap are now nodes/node_count/node_capacity.
- The builder's members get descriptive, m_-prefixed names.
- The shallow frame stack is a std::array (in a wrapper with a
  user-provided constructor: -Weffc++ wants the member initialized, and
  value-initializing the 64 frames costs up to 30 % on small documents).
- The value dispatch macro has no NEXT parameter; the switch is left with
  break, and the macro is undefined after its last use.
- literal() and digits_exceed() deduce the length from the string literal;
  each literal has one helper; the two `if (!is_float)` blocks are merged.
- arena_ensure() sits right before the writes; the decode arena is a
  std::vector<char>.
- The builder records the deepest nesting, and materialize() sizes its
  stack from it once (growing it only if a subtree is deeper).
- Comment fixes (hex4, slow_string, the NUL rule, put_utf8, 0 - m, the
  arena resize, grow()) and formatting (macro backslashes, digit cases,
  one power of ten per line, value_t assertion next to is_container()).
- build_with's template parameters follow the builder's order.

Greg's idea of keeping the source offset in `off` and the arena offset in
`next` for escaped strings was measured and not adopted: string access got
7-18 % slower with a select, and 3.5-18 % slower with a branch-free field
pick (Apple M-series, clang -O3), parsing was unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Apply the review's wording fixes to the json_view documentation

- Copying is disabled because of its cost, not because of two documents
  borrowing the same buffer; the unique_ptr follows from move-only.
- Inputs: streams, FILE*, and wide characters (strings, arrays, pointers)
  are all supported and owned; point to parse_copy() for owned copies.
- The error replay only runs for failing input, builds no values, and is
  skipped with allow_exceptions = false.
- read() writes copied inputs into the previous owned copy.
- Views can dangle like std::string_view, and why there is no
  shared/weak pointer design.
- The 4 GiB limit (16-byte nodes, 16-byte scanner steps), streams being
  read to the end first, which macros apply, why materialize() has no
  source positions, and wording nits (accept() vs basic_json::accept(),
  is_binary, is_discarded, see-also lists, size table, "back into").

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Say that json_view converts integers while parsing

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Address the review: null inline nodes, trait spelling, and max() in json_view

- inline_nodes is null when the document has no inline nodes, instead of
  pointing past the end of the allocation; shrink_to_fit() no longer
  passes it to memcpy() for an empty node array
- is_byte_char and is_wide_char use conjunction/disjunction, which
  astyle leaves unspaced
- append_stream() uses (std::max) for the count read

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Include materialize.hpp in the view builder test

The test calls detail::view::materialize(), which only the single header
pulled in; the multiple-header build (JSON_TEST_USING_MULTIPLE_HEADERS)
failed with "'materialize' is not a member of detail::view".

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Rename emit() in the view builder for Qt compatibility

Qt defines emit as an empty macro, so a Qt header included before
json_view.hpp broke the declaration of the builder's emit(), see
issue #5865. It is now called push_node().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Do not copy an empty run into an unallocated arena

memcpy() must not be passed a null pointer even for zero bytes, which
arena_run() did for an empty run while the arena was still empty
(ci_test_clang_sanitizer: "null pointer passed as argument 1" in
builder.hpp). Only the slow path can see such a run.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Exclude the view builder test from Infer's reports

Infer's std::unique_ptr model confuses the values that the test's build()
helper returns by value and reports USE_AFTER_DELETE for one of them after
the destructor of an unrelated temporary (ci_infer, 12 reports, all in
tests/src/unit-json_view_builder.cpp). AddressSanitizer is clean on the
same tests.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Skip the deleted-overload checks of json_view on MSVC 2015

MSVC 2015 does not treat selecting a deleted function in decltype as a
substitution failure, so is_detected reports the deleted
parse/parse_copy/accept/read(ptr, len) overloads as callable and the
static_asserts in "integer arguments do not compile" fail (AppVeyor
VS2015 jobs). Calling them still fails to compile there. Guard the
negative checks as unit-delete_deprecated_functions.cpp does.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann authored and GitHub committed 2026-10-21 10:21:27 +02:00
1 parent a4974e72c1
commit 122d718330
146 files changed
+10787 -17

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
// __ _____ _____ _____
// __| | __| | | | 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
/*
This file implements a parser test suitable for fuzz testing. It checks that
json_document (the zero-copy, read-only view of a parsed JSON text declared in
json_view.hpp) agrees with basic_json on every input, for the parse options
selected by the low bits of the first input byte (bit 0: ignore_comments, bit 1:
ignore_trailing_commas; the byte stays part of the text):
- json_document::accept(data) must equal json::accept(data)
- if the input is accepted, json_document::parse(data).root().materialize()
must equal json::parse(data)
- if the input is rejected, json_document::parse(data) (with exceptions
enabled) must throw a json::parse_error or json::out_of_range whose what()
is identical to the one json::parse(data) throws
This is checked for two kinds of input: a std::string, which the document
borrows and which ends in the NUL the parser uses as sentinel, and an
exact-size byte vector, which has no NUL after its last byte and takes the
parser's bounds-checked path (AddressSanitizer reports any read past the end).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include <nlohmann/json_view.hpp>
// the checks below are assertions; NDEBUG would compile them away
#ifdef NDEBUG
#error "the fuzzer drivers must be built without NDEBUG"
#endif
using json = nlohmann::json;
using json_document = nlohmann::json_document;
namespace
{
// what json::parse does with a text: the value, or the message of the exception
struct reference_result
{
bool accepted = false;
json value{};
std::string what{}; // NOLINT(readability-redundant-member-init)
};
reference_result parse_reference(const std::uint8_t* data, std::size_t size, bool comments, bool trailing_commas)
{
reference_result r;
r.accepted = json::accept(data, data + size, comments, trailing_commas);
bool json_threw = false;
try
{
r.value = json::parse(data, data + size, nullptr, true, comments, trailing_commas);
}
catch (const json::parse_error& e)
{
r.what = e.what();
json_threw = true;
}
catch (const json::out_of_range& e)
{
r.what = e.what();
json_threw = true;
}
// json::accept and json::parse must agree
assert(json_threw == !r.accepted);
static_cast<void>(json_threw);
return r;
}
// json_document must agree with the reference for this input (a container
// that json_document::parse borrows)
template<typename Input>
void check_input(const Input& input, const reference_result& expected, bool comments, bool trailing_commas)
{
// json_document::accept must agree with json::accept on every input
const bool accepted_by_view = json_document::accept(input, comments, trailing_commas);
assert(expected.accepted == accepted_by_view);
static_cast<void>(accepted_by_view);
if (expected.accepted)
{
// both parsers must agree on the resulting value
json_document const doc = json_document::parse(input, true, comments, trailing_commas);
assert(!doc.is_discarded());
json const j2 = doc.root().materialize();
assert(expected.value == j2);
static_cast<void>(j2);
// (without exceptions, the same document)
json_document const quiet = json_document::parse(input, false, comments, trailing_commas);
assert(!quiet.is_discarded());
assert(quiet.node_count() == doc.node_count());
}
else
{
// both parsers must reject the input the same way when exceptions are used
bool view_threw = false;
try
{
static_cast<void>(json_document::parse(input, true, comments, trailing_commas));
}
catch (const json::parse_error& e)
{
assert(e.what() == expected.what);
view_threw = true;
}
catch (const json::out_of_range& e)
{
assert(e.what() == expected.what);
view_threw = true;
}
assert(view_threw);
static_cast<void>(view_threw);
// and without exceptions, the document is discarded
json_document const quiet = json_document::parse(input, false, comments, trailing_commas);
assert(quiet.is_discarded());
static_cast<void>(quiet);
}
}
} // namespace
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
// the parse options are taken from the low bits of the first byte
const bool comments = size > 0 && (data[0] & 1U) != 0;
const bool trailing_commas = size > 0 && (data[0] & 2U) != 0;
const reference_result expected = parse_reference(data, size, comments, trailing_commas);
// a std::string: borrowed, with the NUL of std::string as sentinel
const std::string input(reinterpret_cast<const char*>(data), size); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
check_input(input, expected, comments, trailing_commas);
// an exact-size byte vector: borrowed, with nothing after its last byte
const std::vector<std::uint8_t> exact(data, data + size);
check_input(exact, expected, comments, trailing_commas);
// return 0 - non-zero return values are reserved for future use
return 0;
}
+666
View File
@@ -0,0 +1,666 @@
// __ _____ _____ _____
// __| | __| | | | 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_view;
using nlohmann::ordered_json_document;
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <ios>
#include <istream>
#include <list>
#include <map>
#include <random>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#ifdef JSON_HAS_CPP_17
#include <string_view>
#endif
namespace
{
// the value of a text, through a named document: the views of a temporary
// document would dangle (root() of an rvalue document does not compile)
template<typename Document, typename... Args>
auto materialized(Args&& ... args) -> decltype(std::declval<typename Document::view_type>().materialize())
{
const Document d = Document::parse(std::forward<Args>(args)...);
return d.root().materialize();
}
template<typename Document, typename Input>
auto materialized_copy(Input&& input) -> decltype(std::declval<typename Document::view_type>().materialize())
{
const Document d = Document::parse_copy(std::forward<Input>(input));
return d.root().materialize();
}
// a "byte container" that claims to hold `size` bytes, to reach the limit on
// the size of the input without allocating gigabytes; nothing past the first
// bytes is ever read, because the size is checked before the parse starts
struct oversized_input
{
using value_type = char;
std::size_t claimed;
const char* data() const // NOLINT(readability-convert-member-functions-to-static): container interface
{
return "[1]";
}
std::size_t size() const
{
return claimed;
}
};
// detection of calls that must not compile
template<typename... Args>
using parse_call_t = decltype(json_document::parse(std::declval<Args>()...));
template<typename... Args>
using parse_copy_call_t = decltype(json_document::parse_copy(std::declval<Args>()...));
template<typename... Args>
using accept_call_t = decltype(json_document::accept(std::declval<Args>()...));
template<typename... Args>
using read_call_t = decltype(std::declval<json_document&>().read(std::declval<Args>()...));
template<typename Document>
using root_call_t = decltype(std::declval<Document>().root());
template<typename View>
using bool_conversion_t = decltype(static_cast<bool>(std::declval<View>()));
#if !defined(JSON_NOEXCEPTION)
// 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)
{
try
{
const json j = json::parse(text, nullptr, true, comments, trailing_commas);
static_cast<void>(j);
}
catch (const json::exception& e)
{
return e.what();
}
return "";
}
std::string view_exception(const std::string& text, bool comments = false, bool trailing_commas = false)
{
try
{
const json_document d = json_document::parse(text, true, comments, trailing_commas);
static_cast<void>(d);
}
catch (const json::exception& e)
{
return e.what();
}
return "";
}
#endif
// a small deterministic generator of documents
struct generator
{
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
int r(int n)
{
return static_cast<int>(rng() % static_cast<unsigned>(n));
}
void str(std::string& o)
{
static const char* const pieces[] = {"a", "Z", " ", "\\n", "\\\"", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "long text beyond the first sixteen bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
o += '"';
for (int n = r(5); n > 0; --n)
{
o += pieces[r(10)];
}
o += '"';
}
void value(std::string& o, int depth)
{
static const char* const scalars[] = {"0", "-1", "123456789012", "18446744073709551615", "18446744073709551616", "-9223372036854775809", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
"1.5", "-2.25e-3", "1E2", "0.1", "true", "false", "null"
};
const int k = depth > 5 ? 2 + r(4) : r(6);
if (k < 2)
{
const bool object = k == 0;
o += object ? '{' : '[';
for (int i = r(5); i > 0; --i)
{
if (object)
{
str(o);
o += r(2) == 0 ? ":" : " : ";
}
value(o, depth + 1);
o += i > 1 ? ", " : "";
}
o += object ? '}' : ']';
}
else if (k < 4)
{
str(o);
}
else
{
o += scalars[r(13)];
}
}
};
} // namespace
TEST_CASE("json_view")
{
SECTION("types and capacity")
{
for (const char* text :
{"null", "true", "false", "0", "-1", "18446744073709551615", "-9223372036854775808", "18446744073709551616", "1.5",
"\"\"", "\"text\"", "[]", "[1,2,3]", "{}", "{\"a\":1,\"b\":2}" // NOLINT(modernize-raw-string-literal)
})
{
CAPTURE(text)
const json j = json::parse(text);
const json_document d = json_document::parse(text);
const json_view v = d.root();
CHECK(v.type() == j.type());
CHECK(v.is_null() == j.is_null());
CHECK(v.is_boolean() == j.is_boolean());
CHECK(v.is_number() == j.is_number());
CHECK(v.is_number_integer() == j.is_number_integer());
CHECK(v.is_number_unsigned() == j.is_number_unsigned());
CHECK(v.is_number_float() == j.is_number_float());
CHECK(v.is_string() == j.is_string());
CHECK(v.is_array() == j.is_array());
CHECK(v.is_object() == j.is_object());
CHECK(v.is_binary() == j.is_binary());
CHECK(v.is_primitive() == j.is_primitive());
CHECK(v.is_structured() == j.is_structured());
CHECK(!v.is_discarded());
CHECK(v.size() == j.size());
CHECK(v.empty() == j.empty());
CHECK(v.materialize() == j);
}
const json_view invalid{};
CHECK(invalid.is_discarded());
CHECK(invalid.type() == json::value_t::discarded);
CHECK(invalid.size() == 0);
CHECK(invalid.empty());
CHECK(invalid.materialize().is_discarded());
CHECK(invalid.source_offset() == static_cast<std::size_t>(-1));
}
SECTION("materialize")
{
generator g;
for (int i = 0; i < 2000; ++i)
{
std::string text;
g.value(text, 0);
CAPTURE(text)
CHECK(materialized<json_document>(text) == json::parse(text));
// member order as ordered_json::parse keeps it
CHECK(materialized<ordered_json_document>(text).dump() == ordered_json::parse(text).dump());
}
// duplicate keys: the last value, at the position of the first key
CHECK(materialized<json_document>(R"({"a":1,"b":2,"a":3})") == json::parse(R"({"a":1,"b":2,"a":3})"));
CHECK(materialized<ordered_json_document>(R"({"a":1,"b":2,"a":3})").dump() == R"({"a":3,"b":2})");
// very deep nesting (iterative, as parse())
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
// (one check per step, so that a failure shows which step it is)
const json_document deep_document = json_document::parse(deep);
CHECK(deep_document.root().is_array());
const json deep_value = deep_document.root().materialize();
CHECK(deep_value.is_array());
const json deep_parsed = json::parse(deep);
CHECK(deep_parsed.is_array());
CHECK(deep_value == deep_parsed);
#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&);
#endif
}
SECTION("parse errors are those of parse()")
{
for (const char* text :
{
"", " ", "[", "]", "{", "[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]}}"
})
{
CAPTURE(text)
#if !defined(JSON_NOEXCEPTION)
const std::string expected = parse_exception(text);
REQUIRE(!expected.empty());
CHECK(view_exception(text) == expected);
#endif
CHECK(!json_document::accept(text));
const json_document d = json_document::parse(text, false);
CHECK(d.is_discarded());
CHECK(d.root().is_discarded());
CHECK(d.node_count() == 0);
}
// the exception types
json_document _;
CHECK_THROWS_AS(_ = json_document::parse("[1,"), json::parse_error&);
CHECK_THROWS_AS(_ = json_document::parse("1e400"), json::out_of_range&);
}
SECTION("parse options")
{
for (const char* text :
{"// c\n[1]", "[1, /* c */ 2]", "[1,]", "{\"a\":1,}", "[1,/* c */]", "/", "/* ", "[1,,]"
})
{
CAPTURE(text)
for (int options = 0; options < 4; ++options)
{
const bool comments = (options & 1) != 0;
const bool trailing_commas = (options & 2) != 0;
CHECK(json_document::accept(text, comments, trailing_commas) == json::accept(text, comments, trailing_commas));
#if !defined(JSON_NOEXCEPTION)
CHECK(view_exception(text, comments, trailing_commas) == parse_exception(text, comments, trailing_commas));
#endif
}
}
}
SECTION("overflow of the floating-point type")
{
// the overflow is that of the document's number_float_t: 1e39
// overflows a float, the largest float 3.4028235e38 does not, but
// 3.4028236e38 rounds beyond it; a double document is not affected
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
using float_document = nlohmann::basic_json_document<json_float>;
CHECK_FALSE(json_float::accept("1e39"));
CHECK(json_float::accept("3.4028235e38"));
CHECK_FALSE(json_float::accept("3.4028236e38"));
for (const char* text :
{
"1e39", "-1e39", "[3.4028235e38]", "[3.4028236e38]", "{\"a\": [1e38, -3.4028235e38]}", "{\"a\": [1e-50, 1e39]}"
})
{
CAPTURE(text)
const bool accepted = json_float::accept(text);
CHECK(float_document::accept(text) == accepted);
CHECK(float_document::parse(text, false).is_discarded() == !accepted);
CHECK(json_document::accept(text));
if (accepted)
{
CHECK(materialized<float_document>(text) == json_float::parse(text));
}
}
float_document f;
CHECK_THROWS_WITH_AS(f = float_document::parse("1e39"), "[json.exception.out_of_range.406] number overflow parsing '1e39'", json::out_of_range&);
CHECK_THROWS_WITH_AS(f = float_document::parse("[3.4028236e38]"), "[json.exception.out_of_range.406] number overflow parsing '3.4028236e38'", json::out_of_range&);
}
SECTION("NUL and BOM")
{
const std::string with_nul("[1]\0garbage", 11);
CHECK(json_document::accept(with_nul) == json::accept(with_nul));
const std::string nul_in_comment("[1, // c\0\n2]", 12);
CHECK(json_document::accept(nul_in_comment, true) == json::accept(nul_in_comment, true));
CHECK(materialized<json_document>("\xEF\xBB\xBF[1]") == json::parse("\xEF\xBB\xBF[1]"));
#if !defined(JSON_NOEXCEPTION)
CHECK(view_exception("\xEF\xBB") == parse_exception("\xEF\xBB"));
#endif
}
SECTION("inputs")
{
const std::string text = R"([1, "two", {"three": 3.5}])";
const json expected = json::parse(text);
// borrowed: the text must outlive the document
const json_document borrowed = json_document::parse(text);
CHECK(!borrowed.owns_source());
CHECK(borrowed.source().data() == text.data());
CHECK(borrowed.root().materialize() == expected);
CHECK(materialized<json_document>(text.c_str()) == expected);
CHECK(materialized<json_document>(R"([1, "two", {"three": 3.5}])") == expected);
CHECK(materialized<json_document>(text.data(), text.data() + text.size()) == expected);
const std::vector<char> chars(text.begin(), text.end());
CHECK(!json_document::parse(chars).owns_source());
CHECK(materialized<json_document>(chars) == expected);
const std::vector<std::uint8_t> bytes(text.begin(), text.end());
CHECK(materialized<json_document>(bytes) == expected);
#ifdef JSON_HAS_CPP_17
const std::string_view sv = text;
CHECK(!json_document::parse(sv).owns_source());
CHECK(materialized<json_document>(sv) == expected);
#endif
// owned
std::string moved = text;
const json_document from_rvalue = json_document::parse(std::move(moved));
CHECK(from_rvalue.owns_source());
CHECK(from_rvalue.root().materialize() == expected);
CHECK(json_document::parse(std::vector<char>(text.begin(), text.end())).owns_source());
// a const rvalue cannot be moved from, and is not borrowed (it may be a
// temporary): it is copied, as is a const rvalue of any container
const std::string const_text = text; // NOLINT(performance-unnecessary-copy-initialization)
const json_document from_const_rvalue = json_document::parse(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
CHECK(from_const_rvalue.owns_source());
CHECK(from_const_rvalue.source().data() != const_text.data()); // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved): const, not moved from
CHECK(from_const_rvalue.root().materialize() == expected);
json_document read_const_rvalue;
read_const_rvalue.read(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg,bugprone-use-after-move,hicpp-invalid-access-moved)
CHECK(read_const_rvalue.owns_source());
CHECK(read_const_rvalue.root().materialize() == expected);
const std::vector<char> const_chars(text.begin(), text.end());
CHECK(json_document::parse(std::move(const_chars)).owns_source()); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
CHECK(json_document::parse_copy(text).owns_source());
CHECK(materialized_copy<json_document>(text) == expected);
std::istringstream stream(text);
const json_document from_stream = json_document::parse(stream);
CHECK(from_stream.owns_source());
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());
CHECK(materialized<json_document>(list.begin(), list.end()) == expected);
// iterator pairs: pointers are borrowed, and so are contiguous library
// iterators where the input adapter detects them (C++20)
CHECK(!json_document::parse(text.data(), text.data() + text.size()).owns_source());
const bool contiguous = nlohmann::detail::iterator_input_adapter<std::vector<char>::const_iterator>::supports_bulk_scan;
const json_document from_iterators = json_document::parse(chars.cbegin(), chars.cend());
CHECK(from_iterators.owns_source() != contiguous);
CHECK((from_iterators.source().data() == chars.data()) == contiguous);
CHECK(from_iterators.root().materialize() == expected);
const std::string padded = "x" + text + "x";
CHECK(materialized<json_document>(padded.begin() + 1, padded.end() - 1) == expected);
CHECK(json_document::parse(chars.cbegin(), chars.cbegin(), false).is_discarded());
const std::wstring wide = L"[\"\u00e4\u20ac\", 1]";
CHECK(materialized<json_document>(wide) == json::parse(wide));
CHECK(json_document::parse(static_cast<const char*>(nullptr), false).is_discarded());
CHECK(json_document::parse("", false).is_discarded());
// wide-character arrays and pointers are converted to UTF-8, as by
// parse(); the pointers are NUL-terminated
CHECK(materialized<json_document>(L"[\"ä€\", 1]") == json::parse(L"[\"ä€\", 1]"));
CHECK(materialized<json_document>(u"[\"ä€\", 1]") == json::parse(u"[\"ä€\", 1]"));
CHECK(materialized<json_document>(U"[\"ä€\", 1]") == json::parse(U"[\"ä€\", 1]"));
const wchar_t* const wide_pointer = wide.c_str();
const json_document from_wide_pointer = json_document::parse(wide_pointer);
CHECK(from_wide_pointer.owns_source());
CHECK(from_wide_pointer.root().materialize() == json::parse(wide));
const std::u16string wide16 = u"{\"\U0001F600\": [true]}";
CHECK(materialized<json_document>(wide16.c_str()) == json::parse(wide16));
const std::u32string wide32 = U"{\"\U0001F600\": [true]}";
CHECK(materialized<json_document>(wide32.c_str()) == json::parse(wide32));
CHECK(json_document::parse(static_cast<const wchar_t*>(nullptr), false).is_discarded());
// FILE*: read to its end
std::FILE* file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory)
if (file != nullptr)
{
CHECK(std::fputs(text.c_str(), file) >= 0);
CHECK(std::fseek(file, 0, SEEK_SET) == 0);
const json_document from_file = json_document::parse(file);
CHECK(from_file.owns_source());
CHECK(from_file.root().materialize() == expected);
CHECK(std::fclose(file) == 0); // NOLINT(cppcoreguidelines-owning-memory)
}
}
SECTION("streams")
{
// longer than one block of the bulk read
std::string text = "[";
for (int i = 0; i < 5000; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\"";
}
text += ']';
std::istringstream stream(text);
const json_document from_stream = json_document::parse(stream);
CHECK(from_stream.source() == text);
CHECK(from_stream.root().materialize() == json::parse(text));
// the stream is read to its end, and eofbit set, as by parse()
CHECK(stream.eof());
CHECK(!stream.bad());
std::istringstream empty_stream;
CHECK(json_document::parse(empty_stream, false).is_discarded());
CHECK(empty_stream.eof());
std::istringstream rvalue_source("[1]");
CHECK(json_document::parse_copy(std::move(rvalue_source)).owns_source());
// a stream without a buffer is an empty input
std::istream no_buffer(nullptr);
CHECK(json_document::parse(no_buffer, false).is_discarded());
#if !defined(JSON_NOEXCEPTION)
// an exception while reading the input leaves a discarded document
json_document d = json_document::parse(std::vector<char>(text.begin(), text.end()));
std::istringstream throwing("[1]");
throwing.exceptions(std::ios::eofbit);
CHECK_THROWS_AS(d.read(throwing), std::ios_base::failure&);
CHECK(d.is_discarded());
CHECK(d.root().is_discarded());
d.read("[2]");
CHECK(d.root().materialize() == json::parse("[2]"));
#endif
}
SECTION("read() reuses the owned copy")
{
std::string big = "[";
for (int i = 0; i < 1000; ++i)
{
big += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\"";
}
big += ']';
const std::string small = "{\"a\": [1, 2]}";
// copied inputs are written into the existing buffer
json_document d = json_document::parse(std::vector<char>(big.begin(), big.end()));
REQUIRE(d.owns_source());
const char* const buffer = d.source().data();
d.read(std::vector<char>(small.begin(), small.end()));
CHECK(d.owns_source());
CHECK(d.source().data() == buffer);
CHECK(d.root().materialize() == json::parse(small));
std::istringstream stream(small);
d.read(stream);
CHECK(d.source().data() == buffer);
CHECK(d.root().materialize() == json::parse(small));
d.read(std::wstring(L"[3]"));
CHECK(d.source().data() == buffer);
CHECK(d.root().materialize() == json::parse("[3]"));
// a moved-in string replaces it (long enough to be on the heap)
std::string moved = big;
const char* const moved_buffer = moved.data();
d.read(std::move(moved));
CHECK(d.source().data() == moved_buffer);
CHECK(d.root().materialize() == json::parse(big));
// a borrowed input leaves nothing owned
d.read(big);
CHECK(!d.owns_source());
CHECK(d.source().data() == big.data());
}
SECTION("integer arguments do not compile")
{
using nlohmann::detail::is_detected;
static_assert(is_detected<parse_call_t, const char*, bool>::value, "parse(ptr, bool) is valid");
static_assert(is_detected<parse_call_t, const char*, bool, bool, bool>::value, "parse(ptr, bool, bool, bool) is valid");
static_assert(is_detected<parse_call_t, const char*, const char*>::value, "parse(first, last) is valid");
static_assert(is_detected<parse_call_t, const char*, const char*, bool>::value, "parse(first, last, bool) is valid");
static_assert(is_detected<parse_copy_call_t, const char*, bool>::value, "parse_copy(ptr, bool) is valid");
static_assert(is_detected<accept_call_t, const char*, bool>::value, "accept(ptr, bool) is valid");
static_assert(is_detected<read_call_t, const char*, bool>::value, "read(ptr, bool) is valid");
// a length is not a flag: parse(ptr, len) would convert len to
// allow_exceptions and read ptr as a C string, which need not end
//
// MSVC 2015 does not treat selecting a deleted function in decltype as
// a substitution failure, so it detects the deleted overloads as
// callable; calling them still fails to compile
#if !(defined(_MSC_VER) && _MSC_VER < 1910)
static_assert(!is_detected<parse_call_t, const char*, std::size_t>::value, "parse(ptr, len) must not compile");
static_assert(!is_detected<parse_call_t, const char*, int>::value, "parse(ptr, int) must not compile");
static_assert(!is_detected<parse_call_t, const char*, char>::value, "parse(ptr, char) must not compile");
static_assert(!is_detected<parse_call_t, const char*, std::size_t, bool>::value, "parse(ptr, len, bool) must not compile");
static_assert(!is_detected<parse_call_t, const std::string&, std::size_t>::value, "parse(string, len) must not compile");
static_assert(!is_detected<parse_call_t, const std::vector<char>&, std::size_t>::value, "parse(vector, len) must not compile");
static_assert(!is_detected<parse_copy_call_t, const char*, std::size_t>::value, "parse_copy(ptr, len) must not compile");
static_assert(!is_detected<accept_call_t, const char*, std::size_t>::value, "accept(ptr, len) must not compile");
static_assert(!is_detected<read_call_t, const char*, std::size_t>::value, "read(ptr, len) must not compile");
#endif
// json_view has no conversion to bool: unlike basic_json's, it would
// mean "exists", not "is not null"; use is_discarded()
static_assert(!is_detected<bool_conversion_t, json_view>::value, "json_view must not convert to bool");
// the valid calls still work
const char* const text = "[1]";
CHECK(materialized<json_document>(text, true) == json::parse(text));
CHECK(json_document::accept(text, true, true));
}
SECTION("root of a temporary document does not compile")
{
using nlohmann::detail::is_detected;
// the view would dangle: auto v = json_document::parse(text).root();
static_assert(is_detected<root_call_t, json_document&>::value, "root() of an lvalue is valid");
static_assert(is_detected<root_call_t, const json_document&>::value, "root() of a const lvalue is valid");
static_assert(!is_detected<root_call_t, json_document>::value, "root() of an rvalue must not compile");
static_assert(!is_detected < root_call_t, json_document && >::value, "root() of an rvalue must not compile");
static_assert(!is_detected < root_call_t, const json_document && >::value, "root() of a const rvalue must not compile");
static_assert(!is_detected<root_call_t, ordered_json_document>::value, "root() of an rvalue must not compile");
// a named document is fine, also after a move
json_document d = json_document::parse("[1]");
CHECK(d.root().size() == 1);
const json_document moved = std::move(d);
CHECK(moved.root().size() == 1);
}
SECTION("input size limit")
{
// 32-bit offsets: the limit is 4 GiB minus 16 bytes (a margin below 2^32),
// which is what the exception message and the documentation say
const std::size_t limit = nlohmann::detail::view::max_input_size();
CHECK(limit == std::size_t{4294967279u});
const oversized_input input{limit + 1};
CHECK(!json_document::accept(input));
CHECK(json_document::parse(input, false).is_discarded());
#if !defined(JSON_NOEXCEPTION)
json_document d;
CHECK_THROWS_WITH_AS(d = json_document::parse(input), "[json.exception.out_of_range.416] input of 4294967280 bytes or more is not supported by json_document", json::out_of_range&);
#endif
}
SECTION("document lifetime and reuse")
{
json_document d;
CHECK(d.is_discarded());
CHECK(d.root().is_discarded());
CHECK(d.node_count() == 0);
CHECK(d.memory_usage() == 0);
CHECK(d.source().empty());
const std::string a = "[1,2,3]";
const std::string b = "{\"x\":[true]}";
d.read(a);
CHECK(d.node_count() == 4);
CHECK(d.root().materialize() == json::parse(a));
d.read(b);
CHECK(d.node_count() == 4);
CHECK(d.root().materialize() == json::parse(b));
d.read("[", false);
CHECK(d.is_discarded());
// views stay valid when the document moves
json_document first = json_document::parse(a);
const json_view root = first.root();
const json_document second = std::move(first);
CHECK(root.materialize() == json::parse(a));
CHECK(second.root().materialize() == json::parse(a));
}
SECTION("memory")
{
std::string big = "[";
for (int i = 0; i < 10000; ++i)
{
big += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\"";
}
big += ']';
json_document d = json_document::parse(big);
CHECK(d.node_count() == 10001);
const std::size_t before = d.memory_usage();
d.shrink_to_fit(); // (invalidates views, like std::vector::shrink_to_fit)
CHECK(d.memory_usage() <= before);
CHECK(d.node_count() == 10001);
CHECK(d.root().materialize() == json::parse(big));
CHECK(d.root().size() == 10000);
d.shrink_to_fit(); // nothing left to release
// after reading a smaller text, both the index and the decoded strings
// shrink, and the strings are found in their new place
std::string escaped = "[";
for (int i = 0; i < 1000; ++i)
{
escaped += (i != 0 ? ",\"a\\n" : "\"a\\n") + std::to_string(i) + "\"";
}
escaped += ']';
json_document reused = json_document::parse(escaped);
const std::string smaller = "[\"x\\ty\", [true, \"\\u00e4\"]]"; // NOLINT(modernize-raw-string-literal)
reused.read(smaller);
const std::size_t grown = reused.memory_usage();
reused.shrink_to_fit();
CHECK(reused.memory_usage() < grown);
CHECK(reused.root().materialize() == json::parse(smaller));
// an empty document has nothing to release
json_document empty;
empty.shrink_to_fit();
CHECK(empty.memory_usage() == 0);
// a small document stays in the storage block of the header
json_document small = json_document::parse("[1,[2,3],{\"a\":\"b\\n\"}]"); // NOLINT(modernize-raw-string-literal)
small.shrink_to_fit();
CHECK(small.root().materialize() == json::parse("[1,[2,3],{\"a\":\"b\\n\"}]"));
}
SECTION("source offsets")
{
const std::string text = R"( {"key": "value", "escaped": "a\nb", "n": 42})";
const json_document d = json_document::parse(text);
CHECK(d.root().source_offset() == 2);
// (element access comes with a later change; the offsets of the
// string nodes are checked through materialize() above)
}
}
+505
View File
@@ -0,0 +1,505 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
#if JSON_TEST_USING_MULTIPLE_HEADERS
#include <nlohmann/detail/view/builder.hpp>
#include <nlohmann/detail/view/materialize.hpp>
#include <nlohmann/detail/view/string_ref.hpp>
#else
#include <nlohmann/json_view.hpp> // the single header contains the internal headers
#endif
using nlohmann::json;
#include <cstdint>
#include <fstream>
#include <limits>
#include <map>
#include <memory>
#include <new>
#include <random>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include <test_data.hpp>
namespace
{
using nlohmann::detail::view::document_data;
using nlohmann::detail::view::node;
// the node index of a text; a vector input has no terminating NUL, so that
// AddressSanitizer catches any read past the last byte
struct built
{
std::unique_ptr<document_data, document_data::deleter> data{}; // NOLINT(readability-redundant-member-init)
std::vector<char> copy{}; // NOLINT(readability-redundant-member-init)
bool ok = false;
nlohmann::detail::view::parse_failure failure{};
};
template<typename FloatType = double>
built build(const std::string& text, bool comments, bool trailing_commas, bool sentinel)
{
built r;
r.data.reset(document_data::create(nlohmann::detail::view::estimate_nodes(text.data(), text.size())));
const char* src = text.c_str();
if (!sentinel)
{
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);
r.data->src = src;
r.data->base[0] = src;
r.data->base[1] = r.data->arena.data();
return r;
}
// the value of a subtree, as json::parse would build it
json value_of(const document_data& d, const node*& n)
{
const node& x = *n;
++n;
switch (static_cast<json::value_t>(x.kind))
{
case json::value_t::object:
{
json o = json::object();
const node* const end = &x + x.next;
while (n != end)
{
const std::string key(d.str(*n), n->len);
++n;
o[key] = value_of(d, n);
}
return o;
}
case json::value_t::array:
{
json a = json::array();
const node* const end = &x + x.next;
while (n != end)
{
a.push_back(value_of(d, n));
}
return a;
}
case json::value_t::string:
return std::string(d.str(x), x.len);
case json::value_t::boolean:
return (x.flags & nlohmann::detail::view::node_flags::is_true) != 0;
case json::value_t::number_integer:
return static_cast<std::int64_t>(nlohmann::detail::view::integer_bits(x));
case json::value_t::number_unsigned:
return nlohmann::detail::view::integer_bits(x);
case json::value_t::number_float:
return json::parse(std::string(d.src + x.off, x.len)).get<double>();
case json::value_t::null:
case json::value_t::binary:
case json::value_t::discarded:
default:
return nullptr;
}
}
json value_of(const built& b)
{
const node* n = b.data->nodes;
json v = value_of(*b.data, n);
CHECK(n == b.data->nodes + b.data->node_count);
return v;
}
// accept/reject and the value must match json::parse, for all options and
// with and without a NUL after the text
void check_same(const std::string& text)
{
CAPTURE(text)
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);
for (const bool sentinel :
{
true, false
})
{
const built b = build(text, comments, trailing_commas, sentinel);
CHECK(b.ok == accepted);
if (b.ok && accepted)
{
CHECK(value_of(b) == json::parse(text, nullptr, true, comments, trailing_commas));
}
}
}
}
// a small deterministic generator of documents
struct generator
{
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
int r(int n)
{
return static_cast<int>(rng() % static_cast<unsigned>(n));
}
void ws(std::string& o)
{
for (int n = r(4) == 0 ? r(12) : r(2); n > 0; --n)
{
o += " \n\t\r "[r(6)];
}
}
void str(std::string& o)
{
static const char* const pieces[] = {"a", "Z", " ", "~", "\\n", "\\\"", "\\\\", "\\/", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\\u001f", "long enough text to leave the first 16 bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
o += '"';
for (int n = r(3) == 0 ? r(20) : r(6); n > 0; --n)
{
o += pieces[r(16)];
}
o += '"';
}
void num(std::string& o)
{
static const char* const numbers[] = {"0", "-0", "1", "-1", "12", "123456789", "1234567890123456789", "9223372036854775807", "-9223372036854775808", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
"9223372036854775808", "18446744073709551615", "18446744073709551616", "-9223372036854775809",
"1.5", "-2.25e-3", "1e10", "1E+2", "0.000001", "3.141592653589793238462643", "1e308", "-1e-400", "123.456e7"
};
o += numbers[r(22)];
}
void value(std::string& o, int depth)
{
ws(o);
const int k = depth > 5 ? 2 + r(6) : r(8);
if (k == 0 || k == 1)
{
const bool object = k == 0;
o += object ? '{' : '[';
for (int i = r(5); i > 0; --i)
{
ws(o);
if (object)
{
str(o);
ws(o);
o += ':';
}
value(o, depth + 1);
o += i > 1 ? "," : "";
}
ws(o);
o += object ? '}' : ']';
}
else if (k < 4)
{
str(o);
}
else if (k < 6)
{
num(o);
}
else
{
static const char* const literals[] = {"true", "false", "null"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
o += literals[r(3)];
}
ws(o);
}
};
} // namespace
TEST_CASE("json_view string_ref")
{
// std::string_view in C++17, a stand-in with the same members before
using nlohmann::detail::view::string_ref;
const std::string text = "abc";
const string_ref r(text);
CHECK(r.length() == 3);
CHECK(std::string(r.begin(), r.end()) == "abc");
CHECK(r[1] == 'b');
CHECK(r != string_ref("abd"));
CHECK_FALSE(r != string_ref("abcd", 3));
std::ostringstream o;
o << r;
CHECK(o.str() == "abc");
}
TEST_CASE("json_view builder")
{
SECTION("scalars and containers")
{
for (const char* text :
{
"null", "true", "false", "0", "-0", "42", "-42", "1.5", "\"\"", "\"abc\"", "[]", "{}", "[1,2,3]", "{\"a\":1,\"b\":[true,null]}", // NOLINT(modernize-raw-string-literal)
" [ 1 , 2 ] ", "{\"a\" : {\"b\" : {}}}", "[[[]]]", "\"\\u00e4\\n\\ud83d\\ude00\"", "{\"a\":1,\"a\":2}", "18446744073709551616", // NOLINT(modernize-raw-string-literal)
"-9223372036854775809", "123456789012345678901234567890", "1e400", "-1e400", "1.7976931348623157e308"
})
{
check_same(text);
}
// the midpoint between the largest double and 2^1024 rounds to
// infinity (an overflow), one less to the largest double: with more
// than 19 digits, Eisel-Lemire cannot decide these, and the overflow
// check needs the exact comparison with the midpoint
const std::string midpoint = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792";
const std::string below = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497791";
check_same(midpoint);
check_same("-" + midpoint);
check_same(below);
check_same("[" + below + "," + midpoint + "]");
// the check uses the floating-point type of the document: with float,
// the view rejects what parse() rejects (out_of_range.406), and a
// double document is not affected
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
CHECK_FALSE(float_json::accept("1e39"));
CHECK(float_json::accept("3.4028235e38"));
CHECK_FALSE(float_json::accept("3.4028236e38"));
for (const char* text :
{
"1e39", "-1e39", "3.4028235e38", "-3.4028235e38", "3.4028236e38", "-3.4028236e38", "3.4028234663852886e38", "1e38",
"340282356779733661637539395458142568448", "340282356779733661637539395458142568447.99", "0.00034028236e42",
"[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})"
})
{
CAPTURE(text)
const bool float_accepted = float_json::accept(text);
for (const bool sentinel :
{
true, false
})
{
const built f = build<float>(text, false, false, sentinel);
CHECK(f.ok == float_accepted);
if (!f.ok)
{
CHECK(f.failure.code == nlohmann::detail::view::error_code::number_overflow);
}
CHECK(build<double>(text, false, false, sentinel).ok == json::accept(text));
}
}
}
SECTION("malformed input")
{
for (const char* text :
{
"", " ", "[", "]", "{", "}", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e", "1e+",
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\u12G4\"", "\"\\ud800\"", "\"\\udc00\"", "\"\\ud800\\u0041\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", // NOLINT(modernize-raw-string-literal)
"\"\xe0\x80\x80\"", "\"\xed\xa0\x80\"", "[1]x", "[1] [2]", "/", "/*", "/* */ 1", "// c\n1", "1 // c", "[1,/*c*/2]", "[1,2,]"
})
{
check_same(text);
}
}
SECTION("NUL, BOM, and whitespace")
{
// a NUL inside a string is a control character, as for json::parse
// (where a NUL ends the input, it does so only between values)
for (const bool sentinel :
{
true, false
})
{
const built b = build(std::string("[\"ab\0cd\"]", 9), false, false, sentinel);
CHECK(!b.ok);
CHECK(b.failure.code == nlohmann::detail::view::error_code::string_control_character);
CHECK(b.failure.offset == 4);
}
check_same(std::string("[1]\0garbage", 11));
check_same(std::string("[1\0]", 4));
check_same(std::string("[1, // c\0\n2]", 12));
check_same(std::string("[1, /* c\0 */ 2]", 15));
check_same("\xEF\xBB\xBF[1]");
check_same("\xEF\xBB[1]");
check_same(" \t\r\n 7 \n");
for (const char* text :
{"[1]\r", "[1]\n", "[1]\r\n", "[1,\r2]", "[1,\r\n2]", "7\r", "\"x\"\r", "{\"a\":\r\n1}\r", "[\n 1,\n 2\n]", "{\n \"a\": [\n 1\n ]\n}"
})
{
check_same(text);
}
}
SECTION("deep nesting")
{
// the deepest nesting is recorded (materialize() sizes its stack from it)
CHECK(build("1", false, false, false).data->max_depth == 0);
CHECK(build("[]", false, false, false).data->max_depth == 1);
CHECK(build(R"([[1],{"a":[[]]},[]])", false, false, false).data->max_depth == 4);
// a stack that turns out too small still grows
const std::string nested = R"([[1],{"a":[[{"b":[]}]]},[]])";
const built shallow = build(nested, false, false, false);
REQUIRE(shallow.ok);
shallow.data->max_depth = 1;
CHECK(nlohmann::detail::view::materialize<json>(*shallow.data, shallow.data->nodes) == json::parse(nested));
// the open containers beyond 64 levels live on the heap
for (const std::size_t depth :
{
63u, 64u, 65u, 1000u, 100000u
})
{
const std::string arrays = std::string(depth, '[') + std::string(depth, ']');
const built b = build(arrays, false, false, false);
REQUIRE(b.ok);
CHECK(b.data->node_count == depth);
CHECK(b.data->nodes[0].next == depth);
CHECK(b.data->max_depth == depth);
std::string objects;
for (std::size_t i = 0; i < depth; ++i)
{
objects += "{\"a\":";
}
objects += '1' + std::string(depth, '}');
const built o = build(objects, false, false, false);
REQUIRE(o.ok);
CHECK(o.data->node_count == (2 * depth) + 1);
CHECK(o.data->max_depth == depth);
CHECK(!build(std::string(depth, '[') + std::string(depth - 1, ']'), false, false, false).ok);
}
}
SECTION("generated documents and damaged copies")
{
generator g;
for (int i = 0; i < 3000; ++i)
{
std::string text;
g.value(text, 0);
check_same(text);
// damage: flip one byte, or cut the 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_same(damaged);
check_same(text.substr(0, at));
}
}
SECTION("test files")
{
for (const char* name :
{
"/json.org/1.json", "/json.org/2.json", "/json.org/3.json", "/json.org/4.json", "/json.org/5.json",
"/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::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary);
std::stringstream ss;
ss << f.rdbuf();
const std::string text = ss.str();
REQUIRE(!text.empty());
const built b = build(text, false, false, true);
REQUIRE(b.ok);
CHECK(value_of(b) == json::parse(text));
}
}
}
TEST_CASE("json_view node integer bits")
{
using nlohmann::detail::view::integer_bits;
using nlohmann::detail::view::set_integer_bits;
// an integer lives in len (low half) and next (high half), on any byte
// order; a big-endian target must not store the native word over both
SECTION("set_integer_bits and integer_bits")
{
node n = {};
for (const std::uint64_t v :
{
std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{0xFFFFFFFFu}, std::uint64_t{0x100000000u},
std::uint64_t{0x0000000200000003u}, std::uint64_t{0x0123456789ABCDEFu}, std::uint64_t{0xFFFFFFFFFFFFFFFEu}
})
{
CAPTURE(v)
n.kind = 0x5A;
n.flags = 0xA5;
n.extra = 0x1234;
n.off = 0x89ABCDEFu;
set_integer_bits(n, v);
CHECK(integer_bits(n) == v);
CHECK(n.len == static_cast<std::uint32_t>(v));
CHECK(n.next == static_cast<std::uint32_t>(v >> 32));
// the other fields are untouched
CHECK(n.kind == 0x5A);
CHECK(n.flags == 0xA5);
CHECK(n.extra == 0x1234);
CHECK(n.off == 0x89ABCDEFu);
}
}
SECTION("parsed integers")
{
struct integer_case
{
const char* text;
std::uint64_t bits;
};
for (const integer_case c :
{
integer_case{"[8589934595]", 0x0000000200000003u}, integer_case{"[4294967296]", 0x100000000u}, integer_case{"[4294967295]", 0xFFFFFFFFu},
integer_case{"[-2]", 0xFFFFFFFFFFFFFFFEu}, integer_case{"[-4294967297]", 0xFFFFFFFEFFFFFFFFu}, integer_case{"[18446744073709551615]", 0xFFFFFFFFFFFFFFFFu},
integer_case{"[7]", 7u}
})
{
CAPTURE(c.text)
const built b = build(c.text, false, false, true);
REQUIRE(b.ok);
const node& n = b.data->nodes[1];
CHECK(integer_bits(n) == c.bits);
CHECK(n.len == static_cast<std::uint32_t>(c.bits));
CHECK(n.next == static_cast<std::uint32_t>(c.bits >> 32));
}
}
}
TEST_CASE("json_view node array size limit")
{
// a node array larger than the address space is refused, not wrapped to a
// small allocation (the size computation overflows on 32-bit targets, and
// for absurd counts everywhere)
std::unique_ptr<document_data, document_data::deleter> d(document_data::create(0));
d->reserve(8);
REQUIRE(d->node_capacity >= 8);
d->node_count = 2;
const std::size_t cap = d->node_capacity;
node* const nodes = d->nodes;
#if !defined(JSON_NOEXCEPTION)
const std::size_t too_many = document_data::max_nodes() + 1;
CHECK_THROWS_AS(d->reserve(too_many), std::bad_alloc&);
CHECK_THROWS_AS(d->reserve((std::numeric_limits<std::size_t>::max)()), std::bad_alloc&);
// the array is unchanged
CHECK(d->nodes == nodes);
CHECK(d->node_capacity == cap);
CHECK(d->node_count == 2);
#endif
// the largest count that fits is not refused by the check (nothing is
// allocated for a count that is already there)
d->reserve(cap);
CHECK(d->nodes == nodes);
}
+47
View File
@@ -0,0 +1,47 @@
// __ _____ _____ _____
// __| | __| | | | 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"
// All other tests keep the library's macros (JSON_TEST_KEEP_MACROS). This one
// includes json_view.hpp as users do, so that json.hpp undefines its macros
// (JSON_HAS_CPP_17, JSON_STRICT_NUL_HANDLING, ...) before the view is compiled.
#undef JSON_TEST_KEEP_MACROS
#include <nlohmann/json_view.hpp>
#include <string>
#include <type_traits>
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
#include <string_view>
#define JSON_VIEW_TEST_HAS_STRING_VIEW 1
#else
#define JSON_VIEW_TEST_HAS_STRING_VIEW 0
#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) \
|| 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"
#endif
TEST_CASE("json_view without the library's macros")
{
// (this file also gets C++17 builds: it mentions JSON_HAS_CPP_17)
#if JSON_VIEW_TEST_HAS_STRING_VIEW
CHECK(std::is_same<nlohmann::json_view::string_view_t, std::string_view>::value);
#endif
const std::string text = "[1, 2.5, \"x\"]";
const nlohmann::json_document d = nlohmann::json_document::parse(text);
CHECK(d.root().size() == 3);
CHECK(d.root().materialize() == nlohmann::json::parse(text));
// the NUL handling of the library's configuration
const std::string with_nul("[1]\0x", 5);
CHECK(nlohmann::json_document::accept(with_nul) == nlohmann::json::accept(with_nul));
}