Files
json/include/nlohmann/detail/view/builder.hpp
T
Niels Lohmann 122d718330 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>
2026-10-21 10:21:27 +02:00

1089 lines
39 KiB
C++

// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-FileCopyrightText: 2020 YaoYuan <https://github.com/ibireme/yyjson>
// SPDX-License-Identifier: MIT
#pragma once
#include <algorithm> // find, find_if, max, min
#include <array> // array
#include <cstddef> // size_t, ptrdiff_t
#include <cstdint> // int64_t, uint8_t, uint16_t, uint32_t, uint64_t
#include <cstring> // memcmp, memcpy
#include <limits> // numeric_limits
#include <string> // string
#include <vector> // vector
#include <nlohmann/json.hpp>
#include <nlohmann/detail/view/document_data.hpp>
#include <nlohmann/detail/view/macro_scope.hpp>
#include <nlohmann/detail/view/node.hpp>
#include <nlohmann/detail/view/scan.hpp>
// The view's parser: one pass over the input that emits the node index (see
// node.hpp). The table-driven decoding of \u escapes and the fast paths for
// ": " and indentation follow yyjson (https://github.com/ibireme/yyjson, MIT
// license).
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
namespace view
{
enum class error_code : std::uint8_t
{
none,
empty_input,
unexpected_value,
invalid_literal,
expected_key,
expected_colon,
expected_array_end,
expected_object_end,
trailing_characters,
number_after_minus,
number_after_dot,
number_after_exponent,
number_overflow,
string_missing_quote,
string_control_character,
string_utf8,
string_escape,
string_unicode_hex,
string_surrogate_high,
string_surrogate_low,
comment_start,
comment_unterminated,
input_too_large,
};
struct parse_failure
{
error_code code = error_code::none;
std::size_t offset = 0; ///< byte offset of the offending character
};
/// FloatType: the number_float_t of the document, whose overflow parse() rejects
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd, bool Sentinel>
class builder
{
public:
builder(document_data& d, const char* src, std::size_t size) noexcept
: m_doc(d)
, m_begin(reinterpret_cast<const unsigned char*>(src))
, m_end(m_begin + size)
, m_shallow()
{}
/// returns false and fills `failure` on error
bool run()
{
cursor c(*this);
return c.run();
}
builder(const builder&) = delete;
builder& operator=(const builder&) = delete;
builder(builder&&) = delete;
builder& operator=(builder&&) = delete;
~builder() = default;
/// where and why the parse failed (after run() returned false)
const parse_failure& failure() const noexcept
{
return m_failure;
}
private:
struct frame
{
std::uint32_t idx;
std::uint32_t count;
bool is_object;
};
/// 64 frames, left uninitialized: they are filled as containers open (the
/// user-provided constructor keeps the member initialization, which
/// -Weffc++ asks for, from zeroing them; that costs up to 30 % on small
/// documents)
struct frame_stack : std::array<frame, 64>
{
frame_stack() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above
};
document_data& m_doc;
const unsigned char* const m_begin;
const unsigned char* const m_end;
parse_failure m_failure{};
// the open array/object is in the cursor; enclosing ones on a stack that is
// inline for the first 64 levels
frame_stack m_shallow;
std::vector<frame> m_deep{};
NLOHMANN_VIEW_NOINLINE bool fail(error_code c, const unsigned char* at) noexcept
{
m_failure.code = c;
m_failure.offset = static_cast<std::size_t>(at - m_begin);
m_doc.node_count = 0;
return false;
}
/// a failure (recorded by fail) as a comment() result
const unsigned char* fail_at(error_code c, const unsigned char* at) noexcept
{
fail(c, at);
return nullptr;
}
/// a decoded string: p after its closing quote (nullptr: an error), and
/// its bytes in the arena
struct decoded
{
const unsigned char* p;
std::size_t start;
std::size_t len;
};
decoded failed(error_code c, const unsigned char* at) noexcept
{
fail(c, at);
return decoded{nullptr, 0, 0};
}
/// the comment at p (*p == '/'): the position after it, or nullptr on error
NLOHMANN_VIEW_NOINLINE const unsigned char* comment(const unsigned char* p)
{
if (m_end - p < 2)
{
++p;
return fail_at(error_code::comment_start, p);
}
if (p[1] == '/')
{
p += 2;
// (as in parse(), a null byte is the end of the input, so it is
// left for the caller to see)
while (p != m_end && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0))
{
++p;
}
return p;
}
if (p[1] == '*')
{
p += 2;
for (;;)
{
if (p == m_end || (NulIsEnd && *p == 0))
{
return fail_at(error_code::comment_unterminated, p);
}
if (*p == '*' && p + 1 != m_end && p[1] == '/')
{
p += 2;
return p;
}
++p;
}
}
++p;
return fail_at(error_code::comment_start, p);
}
/// the index is full (n nodes, parsed up to at): extrapolate the node
/// count from the nodes per input byte so far (with headroom, and at least
/// 1.5 times as many), so that dense inputs regrow once instead of
/// doubling repeatedly; returns the new node array
NLOHMANN_VIEW_NOINLINE node* grow(std::size_t n, const unsigned char* at)
{
// (n nodes are written; at the limit there is no room for another one)
const std::uint64_t limit = document_data::max_nodes();
// LCOV_EXCL_START (a node array that fills the address space)
if (NLOHMANN_VIEW_UNLIKELY(n >= limit))
{
document_data::throw_bad_alloc();
}
// LCOV_EXCL_STOP
const std::uint64_t done = static_cast<std::uint64_t>(at - m_begin) + 1;
const std::uint64_t guess = static_cast<std::uint64_t>(n) * static_cast<std::uint64_t>(m_end - m_begin + 1) / done;
const std::uint64_t grown = guess + (guess / 4) + 64; // a variable: GCC calls a cast of the sum useless where std::uint64_t is std::size_t
// (n is below 2^32: the input is smaller than 4 GiB; the sum cannot wrap)
const std::uint64_t wanted = (std::max)(grown, static_cast<std::uint64_t>(n) + (n / 2) + 64);
m_doc.node_count = n; // the number of nodes reserve() copies into the new array
// (a count beyond the limit is cut: the index does not grow beyond what can be addressed)
m_doc.reserve(static_cast<std::size_t>((std::min)(wanted, limit)));
return m_doc.nodes;
}
/// the four hex digits of a unicode escape at p as a code unit, and p
/// moved past them; or -1, and p at the first bad digit (one table lookup
/// per digit and a single check: the library's table, after yyjson's
/// read_hex_u16)
NLOHMANN_VIEW_ALWAYS_INLINE int hex4(const unsigned char*& p) noexcept
{
if (NLOHMANN_VIEW_LIKELY(m_end - p >= 4))
{
const int cp = hex_codepoint(p);
if (NLOHMANN_VIEW_LIKELY(cp >= 0))
{
p += 4;
return cp;
}
}
p = hex4_error(p);
return -1;
}
/// hex4() failed: the first bad digit (none: p)
NLOHMANN_VIEW_NOINLINE const unsigned char* hex4_error(const unsigned char* p) noexcept
{
if (m_end - p >= 4)
{
while (is_hex(*p))
{
++p;
}
}
return p;
}
/// s points to the start of the string, p to a backslash, a control
/// character, an ill-formed UTF-8 byte, or the end: decode the string into
/// the arena
NLOHMANN_VIEW_NOINLINE decoded slow_string(const unsigned char* s, const unsigned char* p)
{
// single-character escapes; 0: invalid (and 'u', handled separately)
static const std::array<char, 128> simple_escape =
{
{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, '"', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\\', 0, 0, 0,
0, 0, '\b', 0, 0, 0, '\f', 0, 0, 0, 0, 0, 0, 0, '\n', 0, 0, 0, '\r', 0, '\t', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}
};
const std::size_t start = arena_used();
arena_run(s, static_cast<std::size_t>(p - s));
for (;;)
{
if (p == m_end)
{
return failed(error_code::string_missing_quote, p);
}
const unsigned char c = *p;
if (c == '"')
{
++p;
return decoded{p, start, arena_used() - start};
}
if (c != '\\')
{
// (without JSON_STRICT_NUL_HANDLING, a NUL between values ends
// the input, as in json::parse; inside a string, a NUL is still
// a control character and an error)
return failed(c < 0x20 ? error_code::string_control_character : error_code::string_utf8, p);
}
++p;
if (p == m_end)
{
return failed(error_code::string_missing_quote, p);
}
const unsigned char d = *p++;
if (d == 'u')
{
int cp = hex4(p); // (moves p past the four digits)
if (NLOHMANN_VIEW_UNLIKELY(cp < 0))
{
return failed(error_code::string_unicode_hex, p);
}
if (NLOHMANN_VIEW_UNLIKELY((cp & 0xF800) == 0xD800)) // a surrogate
{
if (cp >= 0xDC00)
{
return failed(error_code::string_surrogate_low, p);
}
if (m_end - p < 2 || p[0] != '\\' || p[1] != 'u')
{
return failed(error_code::string_surrogate_high, p);
}
p += 2;
const int lo = hex4(p); // (moves p past the four digits)
if (lo < 0)
{
return failed(error_code::string_unicode_hex, p);
}
if (lo < 0xDC00 || lo > 0xDFFF)
{
return failed(error_code::string_surrogate_high, p);
}
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
}
arena_ensure(4);
m_arena_write = put_utf8(m_arena_write, cp);
}
else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0))
{
arena_ensure(1);
*m_arena_write++ = simple_escape[d];
}
else
{
--p;
return failed(error_code::string_escape, p);
}
if (p != m_end && *p == '\\')
{
continue; // consecutive escapes ("\u00e4\u00f6"): no run in between
}
const unsigned char* const r = p;
p = scan_string_run(p, m_end);
arena_run(r, static_cast<std::size_t>(p - r));
}
}
/// does the float token [s, p) overflow FloatType? (parse() rejects it)
NLOHMANN_VIEW_NOINLINE static bool float_overflows(const unsigned char* s, const unsigned char* p)
{
const auto* const first = reinterpret_cast<const char*>(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const auto* const last = reinterpret_cast<const char*>(p); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const char* const dot = std::find(first, last, '.');
const char* const exponent = std::find_if(first, last, [](char c)
{
return c == 'e' || c == 'E';
});
const auto v = convert_float<FloatType>(first, last, dot == last ? std::string::npos : static_cast<std::size_t>(dot - first),
static_cast<std::size_t>(exponent - first));
return v > (std::numeric_limits<FloatType>::max)() || v < -(std::numeric_limits<FloatType>::max)();
}
/// do the `digits` digits of a magnitude at d exceed `limit`, the largest
/// magnitude that fits (a string literal: N counts its terminating NUL, so
/// it has N - 1 digits)?
template<std::size_t N>
static NLOHMANN_VIEW_ALWAYS_INLINE bool digits_exceed(const unsigned char* d, std::size_t digits, const char (&limit)[N]) noexcept // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal
{
if (NLOHMANN_VIEW_LIKELY(digits < N - 1))
{
return false;
}
return digits > N - 1 || std::memcmp(d, limit, N - 1) > 0;
}
static bool is_hex(unsigned char c) noexcept
{
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
// decode arena: the vector in the document, written through a raw
// pointer (resized ahead in large steps; trimmed when parsing succeeds)
char* m_arena_write = nullptr;
char* m_arena_end = nullptr;
NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept
{
return m_arena_write != nullptr ? static_cast<std::size_t>(m_arena_write - m_doc.arena.data()) : 0;
}
NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(std::size_t n)
{
if (NLOHMANN_VIEW_UNLIKELY(static_cast<std::size_t>(m_arena_end - m_arena_write) < n))
{
arena_grow(n);
}
}
NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n)
{
const std::size_t used = arena_used();
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, used + n + 256));
m_arena_write = m_doc.arena.data() + used;
m_arena_end = m_doc.arena.data() + m_doc.arena.size();
}
/// append the run [r, r + n) to the arena; short runs as one fixed-size
/// 16-byte move when both sides have the room (no library call)
NLOHMANN_VIEW_ALWAYS_INLINE void arena_run(const unsigned char* r, std::size_t n)
{
if (n <= 16 && m_end - r >= 16)
{
arena_ensure(16);
std::memcpy(m_arena_write, r, 16);
}
else if (n != 0)
{
// (an empty run leaves an empty arena unallocated, and memcpy()
// must not be passed its null pointer even for zero bytes)
arena_ensure(n);
std::memcpy(m_arena_write, r, n);
}
m_arena_write += n;
}
/// UTF-8 encoding of cp at w (room for 4 bytes); returns the position after
/// it. (A returned pointer stays in a register; writing through a
/// reference to m_arena_write would store and reload it around every byte.)
static char* put_utf8(char* w, int cp) noexcept
{
if (cp < 0x80)
{
*w++ = static_cast<char>(cp);
}
else if (cp < 0x800)
{
*w++ = static_cast<char>(0xC0 | (cp >> 6));
*w++ = static_cast<char>(0x80 | (cp & 0x3F));
}
else if (cp < 0x10000)
{
*w++ = static_cast<char>(0xE0 | (cp >> 12));
*w++ = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
*w++ = static_cast<char>(0x80 | (cp & 0x3F));
}
else
{
*w++ = static_cast<char>(0xF0 | (cp >> 18));
*w++ = static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
*w++ = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
*w++ = static_cast<char>(0x80 | (cp & 0x3F));
}
return w;
}
/// a compile-time option as a runtime condition: testing the template
/// argument directly makes a condition like `TrailingCommas && c == ']'`
/// constant when the option is off, which MSVC reports as C4127
static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept
{
return option;
}
/// The parse state and the parser proper. The cursor is a local object of
/// run() whose address never escapes (everything it calls out of line is a
/// member of the builder and gets the positions it needs), so that the
/// compiler keeps the state in registers instead of reloading it from
/// memory after every node store and call.
struct cursor
{
explicit cursor(builder& owner) noexcept
: cold(owner)
, b(owner.m_begin)
, p(owner.m_begin)
, e(owner.m_end)
{}
builder& cold; ///< out-of-line helpers and state that needs no registers
const unsigned char* const b;
const unsigned char* p;
const unsigned char* const e;
node* base = nullptr;
node* out = nullptr;
node* cap = nullptr;
// the open array/object
std::uint32_t cur_idx = 0;
std::uint32_t cur_count = 0;
bool cur_is_object = false;
std::size_t depth = 0;
std::size_t max_depth = 0;
NLOHMANN_VIEW_ALWAYS_INLINE bool run()
{
cold.m_doc.reserve(estimate_nodes(reinterpret_cast<const char*>(b), static_cast<std::size_t>(e - b)));
base = cold.m_doc.nodes;
out = base;
cap = base + cold.m_doc.node_capacity;
if (e - p >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF)
{
p += 3; // byte order mark
}
if (!ws())
{
return false;
}
if (p == e || (NulIsEnd && *p == 0))
{
return fail(error_code::empty_input);
}
// root value
switch (cur())
{
case '{':
open(value_t::object);
++p;
goto obj_first;
case '[':
open(value_t::array);
++p;
goto arr_first;
default:
if (!scalar())
{
return false;
}
goto root_done;
}
// value dispatch, expanded once for array elements and once for member
// values: each jump has its own history (arrays tend to hold one kind of
// value), and the continuation needs no branch on the container kind;
// after a scalar, the switch is left to the label that follows it
#define NLOHMANN_VIEW_VALUE \
switch (cur()) \
{ \
case '"': \
if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \
break; \
case '{': \
open(value_t::object); \
++p; \
goto obj_first; \
case '[': \
open(value_t::array); \
++p; \
goto arr_first; \
case '-': \
if (NLOHMANN_VIEW_UNLIKELY(!number<true>())) { return false; } \
break; \
case '0': case '1': case '2': case '3': case '4': \
case '5': case '6': case '7': case '8': case '9': \
if (NLOHMANN_VIEW_UNLIKELY(!number<false>())) { return false; } \
break; \
case 't': \
if (NLOHMANN_VIEW_UNLIKELY(!literal_true())) { return false; } \
break; \
case 'f': \
if (NLOHMANN_VIEW_UNLIKELY(!literal_false())) { return false; } \
break; \
case 'n': \
if (NLOHMANN_VIEW_UNLIKELY(!literal_null())) { return false; } \
break; \
default: \
return fail(error_code::unexpected_value); \
}
arr_first:
if (!ws())
{
return false;
}
if (cur() == ']')
{
++p;
goto close_container;
}
value:
NLOHMANN_VIEW_VALUE
arr_next:
++cur_count;
if (!ws())
{
return false;
}
if (NLOHMANN_VIEW_LIKELY(cur() == ','))
{
++p;
if (!ws())
{
return false;
}
if (enabled(TrailingCommas) && cur() == ']')
{
++p;
goto close_container;
}
goto value;
}
if (cur() == ']')
{
++p;
goto close_container;
}
return fail(error_code::expected_array_end);
obj_first:
if (!ws())
{
return false;
}
if (cur() == '}')
{
++p;
goto close_container;
}
obj_key:
if (NLOHMANN_VIEW_UNLIKELY(cur() != '"'))
{
return fail(error_code::expected_key);
}
if (NLOHMANN_VIEW_UNLIKELY(!string()))
{
return false;
}
if (NLOHMANN_VIEW_LIKELY(cur() == ':' && (Sentinel || e - p >= 2) && p[1] == ' '))
{
p += 2; // ": " (pretty-printed input; a fast path of yyjson)
}
else
{
if (!ws())
{
return false;
}
if (NLOHMANN_VIEW_UNLIKELY(cur() != ':'))
{
return fail(error_code::expected_colon);
}
++p;
}
if (!ws())
{
return false;
}
NLOHMANN_VIEW_VALUE
#undef NLOHMANN_VIEW_VALUE
obj_next:
++cur_count;
if (!ws())
{
return false;
}
if (NLOHMANN_VIEW_LIKELY(cur() == ','))
{
++p;
if (!ws())
{
return false;
}
if (enabled(TrailingCommas) && cur() == '}')
{
++p;
goto close_container;
}
goto obj_key;
}
if (cur() == '}')
{
++p;
goto close_container;
}
return fail(error_code::expected_object_end);
close_container:
close();
if (NLOHMANN_VIEW_UNLIKELY(depth == 0))
{
goto root_done;
}
if (cur_is_object)
{
goto obj_next;
}
goto arr_next;
root_done:
if (!ws())
{
return false;
}
if (p != e && !(NulIsEnd && *p == 0))
{
return fail(error_code::trailing_characters);
}
cold.m_doc.node_count = static_cast<std::size_t>(out - base);
cold.m_doc.max_depth = max_depth;
// (so that size() is the number of bytes used: the arena grows ahead
// in large steps, and shrink_to_fit() keeps size() bytes)
cold.m_doc.arena.resize(cold.arena_used());
return true;
}
NLOHMANN_VIEW_ALWAYS_INLINE bool fail(error_code c) noexcept
{
return cold.fail(c, p);
}
/// the current byte, or 0 at the end. With a NUL-terminated input
/// (Sentinel) the terminator is read instead of checking the bounds; a 0
/// never matches a JSON token, so the error paths tell the end apart.
NLOHMANN_VIEW_ALWAYS_INLINE unsigned char cur() const noexcept
{
if (Sentinel)
{
return *p;
}
return p != e ? *p : 0;
}
/// root scalar
NLOHMANN_VIEW_ALWAYS_INLINE bool scalar()
{
switch (cur())
{
case '"':
return string();
case 't':
return literal_true();
case 'f':
return literal_false();
case 'n':
return literal_null();
case '-':
return number<true>();
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return number<false>();
default:
return fail(error_code::unexpected_value);
}
}
NLOHMANN_VIEW_ALWAYS_INLINE bool literal_true()
{
return literal("true", value_t::boolean, node_flags::is_true);
}
NLOHMANN_VIEW_ALWAYS_INLINE bool literal_false()
{
return literal("false", value_t::boolean, 0);
}
NLOHMANN_VIEW_ALWAYS_INLINE bool literal_null()
{
return literal("null", value_t::null, 0);
}
/// skip whitespace (and comments); false on a malformed comment
NLOHMANN_VIEW_ALWAYS_INLINE bool ws()
{
const unsigned char c = cur();
if (NLOHMANN_VIEW_LIKELY(c > ' ' && (!Comments || c != '/')))
{
return true; // no whitespace: the common case in minified input
}
return ws_slow();
}
NLOHMANN_VIEW_ALWAYS_INLINE bool ws_slow()
{
for (;;)
{
if (cur() == ' ' && (Sentinel || e - p >= 2) && p[1] > ' ' && (!Comments || p[1] != '/'))
{
++p; // single space, e.g. after ':' or ','
return true;
}
if (cur() == '\n' || cur() == '\r')
{
// (a branch, not an add of the comparison: p must not
// wait for the byte after the line break)
if (NLOHMANN_VIEW_UNLIKELY(cur() == '\r') && (Sentinel || e - p >= 2) && p[1] == '\n')
{
p += 2;
}
else
{
++p;
}
// indentation: two spaces per step, fixed offsets (after yyjson)
while (e - p >= 32)
{
#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(load16(p + (std::ptrdiff_t{2} * (i))) == 0x2020)) {} else { p += std::ptrdiff_t{2} * (i); goto indent_done; }
NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP)
#undef NLOHMANN_VIEW_STEP
p += 32;
}
indent_done:
;
}
for (unsigned char c = cur(); c == ' ' || c == '\n' || c == '\r' || c == '\t'; c = cur())
{
++p;
}
if (enabled(Comments) && cur() == '/')
{
const unsigned char* const q = cold.comment(p);
if (q == nullptr)
{
return false;
}
p = q;
continue;
}
return true;
}
}
/// append a node: (kind, flags, extra, off) and the second word (len, or
/// an integer's value); two stores on little-endian targets
NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(value_t k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second)
{
if (NLOHMANN_VIEW_UNLIKELY(out == cap))
{
const auto n = static_cast<std::size_t>(out - base);
base = cold.grow(n, p);
out = base + n;
cap = base + cold.m_doc.node_capacity;
}
node* n = out++;
#if NLOHMANN_VIEW_LITTLE_ENDIAN
const std::uint64_t first = static_cast<std::uint64_t>(k) | (static_cast<std::uint64_t>(flags) << 8)
| (static_cast<std::uint64_t>(extra) << 16) | (static_cast<std::uint64_t>(off) << 32);
std::memcpy(reinterpret_cast<unsigned char*>(n), &first, 8);
std::memcpy(reinterpret_cast<unsigned char*>(n) + 8, &second, 8);
#else
n->kind = static_cast<std::uint8_t>(k);
n->flags = flags;
n->extra = extra;
n->off = static_cast<std::uint32_t>(off);
// len is the low half of the second word, next the high half
// (not a native word over both, which swaps them on big-endian)
n->len = static_cast<std::uint32_t>(second);
n->next = static_cast<std::uint32_t>(second >> 32);
#endif
return n;
}
NLOHMANN_VIEW_ALWAYS_INLINE void open(value_t k)
{
// (base is read after push_node(), which moves the node array when it
// grows; in one expression the order of the two is unspecified)
const node* const n = push_node(k, 0, 0, static_cast<std::size_t>(p - b), 0);
const auto idx = static_cast<std::uint32_t>(n - base);
if (depth != 0)
{
const frame f = {cur_idx, cur_count, cur_is_object};
if (NLOHMANN_VIEW_LIKELY(depth <= 64))
{
cold.m_shallow[depth - 1] = f;
}
else
{
cold.m_deep.push_back(f);
}
}
++depth;
max_depth = (std::max)(max_depth, depth);
cur_idx = idx;
cur_count = 0;
cur_is_object = k == value_t::object;
}
NLOHMANN_VIEW_ALWAYS_INLINE void close()
{
node& n = base[cur_idx];
n.len = cur_count;
n.next = static_cast<std::uint32_t>(out - base) - cur_idx;
if (--depth != 0)
{
frame f{};
if (NLOHMANN_VIEW_LIKELY(depth <= 64))
{
f = cold.m_shallow[depth - 1];
}
else
{
f = cold.m_deep.back();
cold.m_deep.pop_back();
}
cur_idx = f.idx;
cur_count = f.count;
cur_is_object = f.is_object;
}
}
/// the literal `text` at p (N counts the terminating NUL)
template<std::size_t N>
NLOHMANN_VIEW_ALWAYS_INLINE bool literal(const char (&text)[N], value_t k, std::uint8_t flags) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal
{
const std::size_t n = N - 1;
if (NLOHMANN_VIEW_UNLIKELY(e - p < static_cast<std::ptrdiff_t>(n) || std::memcmp(p, text, n) != 0))
{
return fail(error_code::invalid_literal);
}
push_node(k, flags, 0, static_cast<std::size_t>(p - b), n);
p += n;
return true;
}
/// a number at p; the sign is known from the dispatch (so that p does
/// not have to wait for the first byte)
template<bool negative>
NLOHMANN_VIEW_ALWAYS_INLINE bool number()
{
const unsigned char* const s = p;
if (negative)
{
++p;
}
const unsigned char* const int_start = p;
if (p != e && *p == '0')
{
++p;
}
else if (NLOHMANN_VIEW_LIKELY(p != e && *p >= '1' && *p <= '9'))
{
p = skip_digits(p + 1, e);
}
else
{
return fail(error_code::number_after_minus);
}
const auto int_digits = static_cast<std::size_t>(p - int_start);
std::size_t frac_digits = 0;
bool is_float = false;
if (p != e && *p == '.')
{
++p;
const unsigned char* const f0 = p;
p = skip_digits(p, e);
if (NLOHMANN_VIEW_UNLIKELY(p == f0))
{
return fail(error_code::number_after_dot);
}
frac_digits = static_cast<std::size_t>(p - f0);
is_float = true;
}
std::int64_t exponent = 0;
if (p != e && (*p | 0x20) == 'e')
{
++p;
bool exp_negative = false;
if (p != e && (*p == '+' || *p == '-'))
{
exp_negative = *p == '-';
++p;
}
if (NLOHMANN_VIEW_UNLIKELY(p == e || !is_digit(*p)))
{
return fail(error_code::number_after_exponent);
}
while (p != e && is_digit(*p))
{
if (exponent < 100000)
{
exponent = (exponent * 10) + (*p - '0');
}
++p;
}
if (exp_negative)
{
exponent = -exponent;
}
is_float = true;
}
value_t kind = value_t::number_float;
if (!is_float)
{
// integers that do not fit become floats, as in parse()
if (negative)
{
kind = digits_exceed(int_start, int_digits, "9223372036854775808") ? value_t::number_float : value_t::number_integer;
}
else
{
kind = digits_exceed(int_start, int_digits, "18446744073709551615") ? value_t::number_float : value_t::number_unsigned;
}
}
// parse() rejects floats that overflow; only numbers whose magnitude
// could reach the largest FloatType (1e308 for double, 1e38 for
// float) need the conversion
if (NLOHMANN_VIEW_UNLIKELY(static_cast<std::int64_t>(int_digits) + exponent > std::numeric_limits<FloatType>::max_exponent10 - 8 && kind == value_t::number_float))
{
if (builder::float_overflows(s, p))
{
p = s;
return fail(error_code::number_overflow);
}
}
const auto layout = static_cast<std::uint16_t>((int_digits < 255 ? int_digits : 255) | ((frac_digits < 255 ? frac_digits : 255) << 8));
auto second = static_cast<std::uint64_t>(p - s);
if (kind != value_t::number_float)
{
// integers are converted now, while their digits are in cache
const std::uint64_t m = int_digits <= 19 ? parse_upto19(int_start, static_cast<unsigned>(int_digits), e)
: (parse_upto19(int_start, 19, e) * 10) + static_cast<std::uint64_t>(int_start[19] - '0');
// (0 - m rather than -m: unary minus on an unsigned type is
// MSVC's C4146, an error under /sdl; both give the bit pattern
// of the negative std::int64_t)
second = negative ? 0 - m : m;
}
push_node(kind, 0, layout, static_cast<std::size_t>(s - b), second);
return true;
}
/// a string (value or key) at p
NLOHMANN_VIEW_ALWAYS_INLINE bool string()
{
++p; // opening quote
const unsigned char* const s = p;
p = scan_string_run(p, e);
if (NLOHMANN_VIEW_LIKELY(p != e && *p == '"'))
{
push_node(value_t::string, 0, 0, static_cast<std::size_t>(s - b), static_cast<std::uint64_t>(p - s));
++p;
return true;
}
const decoded r = cold.slow_string(s, p);
if (r.p == nullptr)
{
return false;
}
p = r.p;
push_node(value_t::string, node_flags::escaped, 0, r.start, r.len);
return true;
}
};
};
/// run the builder with compile-time options
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd>
inline bool build_with(document_data& d, const char* src, std::size_t size, bool sentinel, parse_failure& failure)
{
if (sentinel)
{
builder<FloatType, Comments, TrailingCommas, NulIsEnd, true> bld(d, src, size);
const bool ok = bld.run();
failure = bld.failure();
return ok;
}
builder<FloatType, Comments, TrailingCommas, NulIsEnd, false> bld(d, src, size);
const bool ok = bld.run();
failure = bld.failure();
return ok;
}
/// sentinel: src[size] is readable and 0 (e.g. std::string); FloatType: the
/// number_float_t of the document
template<typename FloatType, bool NulIsEnd>
inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, parse_failure& failure)
{
if (comments)
{
return trailing_commas ? build_with<FloatType, true, true, NulIsEnd>(d, src, size, sentinel, failure)
: build_with<FloatType, true, false, NulIsEnd>(d, src, size, sentinel, failure);
}
return trailing_commas ? build_with<FloatType, false, true, NulIsEnd>(d, src, size, sentinel, failure)
: build_with<FloatType, false, false, NulIsEnd>(d, src, size, sentinel, failure);
}
} // namespace view
} // namespace detail
NLOHMANN_JSON_NAMESPACE_END