Files
json/include/nlohmann/detail/view/edit_storage.hpp
T
Niels Lohmann fe5e401870 Consolidate duplicated detail helpers and bodies (#5891) (#5901)
* Add shared detail helpers for the #5891 cleanup

Add detail::is_primitive(value_t), detail::hex4_lower(), detail::utf8_shape(),
detail::input_format_name(), detail::empty_input_message(), the exception ids
116, 319, 320 and 416 to detail::exception_id, and make
detail::is_ascii_copyable() constexpr. Later commits switch the duplicated
code over to them.

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

* 1.4: BSON writers share one write-pass UTF-8 sanitizer (#5891)

write_bson_entry_header and write_bson_string re-derived the error-handler
policy differently. Both now use sanitize_utf8_for_bson_write, which treats
strict like keep (the size pass already validated) and sanitizes replace/ignore
into the bytes the size pass measured. No context plumbing is needed.

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

* 2.1.1: one MessagePack container prefix helper (#5891)

Array and object prefixes differed only in the fix marker and the 16/32-bit
markers; both now delegate to write_msgpack_container_prefix. Output is
byte-identical.

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

* 2.1.2: one iterative walker for CBOR and MessagePack (#5891)

write_cbor_iterative and write_msgpack_iterative (and their *_value_or_push
helpers) were identical but for the head, scalar, and key writers. They are now
one template over a small policy struct, so everything inlines. BON8 keeps its
own walker (open string run, closing marker), noted in a comment.

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

* 2.1.3, 2.1.8: one UBJSON container start helper, shared frame base (#5891)

write_ubjson_start_array/_object are now write_ubjson_start_container<IsArray>,
keeping the array-only begin()+1 scan and excessive_valueless rule.
ubjson_frame derives from binary_container_frame instead of duplicating it.

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

* 2.1.4: write_bytes helper for the byte-string write sites (#5891)

The 13 oa.write_characters(reinterpret_cast<const CharType*>(x.data()), n)
sites now call write_bytes(const void*, size_t); copy_characters sites and the
buffer-based writes are untouched.

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

* 2.1.5: shared object-key writer for CBOR and MessagePack (#5891)

write_cbor_key and write_msgpack_key were the same SFINAE pair apart from the
string/scalar writers; both now delegate to write_object_key<Walk>. UBJSON key
and to_format_length stay as they are.

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

* 2.3.10: writer-side format names from input_format_name (#5891)

The CBOR/MessagePack/UBJSON/BSON/BJData/BON8 literals in throw_on_discarded and
throw_integer_out_of_range calls now come from detail::input_format_name; the
texts are identical, and UBJSON vs BJData still follows use_bjdata.

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

* serializer: constexpr ascii table, hex4_lower, dedupe helpers (#5891)

1.1: build ascii_copyable() at compile time from is_ascii_copyable and
test find_ascii_copyable_run against it for every byte. 1.2/2.5.4: use
detail::hex4_lower in both \uXXXX writers. 2.1.6: factor the repeated
flush/emit blocks in dump_escaped_impl into always-inline helpers.
2.1.7: comment why the escape copy does not use put_buffer.

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

* 1.6: use string_t-aware to_string/concat in meta() (#5891)

Replaces the four std::to_string sites; rendered text unchanged.

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

* 1.7: use detail::is_primitive in json.hpp switches (#5891)

has_no_children, set_parents, max_size and the two diff walkers.

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

* 2.2.1: factor insert()/iterator throw helpers (#5891)

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

* 2.2.2: share swap() overloads via swap_impl (#5891)

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

* 2.2.4: factor primitive reset and range check in erase()/range ctor (#5891)

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

* 2.2.6: share push_back array bodies via push_back_array_impl (#5891)

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

* 2.2.7: route parse/accept/sax_parse through *_impl helpers (#5891)

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

* 2.2.8: share the vector-returning to_* bodies via to_binary_vector (#5891)

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

* 2.2.10: merge destroy_string/destroy_binary into destroy_pointer (#5891)

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

* 2.4.1/1.7: exceptions: share what_impl, flatten diagnostics switch (#5891)

The four create() bodies build their message through one protected
static helper; the diagnostics parent walk handles array/object and
ignores all other types.

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

* 2.4.2/2.4.3/1.7: json_pointer: share slash and escape checks, drop primitive case lists (#5891)

The leading-slash and invalid '~' escape checks are shared helpers; the
reference-token walks test object and array first and treat everything
else as primitive.

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

* 1.7/2.4.9: iter_impl, iteration_proxy: drop primitive case lists, share compare preamble (#5891)

Object and array stay the first branches; everything else is the
primitive case. operator== and operator< share the different-container
check.

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

* 2.4.5/2.4.6: from_json: share get_number_value and reserve_if_possible (#5891)

The three number cases of get_arithmetic_value and the arithmetic
from_json share one helper (boolean stays accepted only by the latter);
the array and object reserve overload pairs become one.

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

* 2.4.8: ordered_map: share erase_impl between the key erase overloads (#5891)

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

* 1.7: json_pointer: flatten walk drops the primitive case list (#5891)

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

* 1.2/1.3/1.5/2.5.1/2.5.2/2.5.5: share helpers in the view code (#5891)

- detail::exception_id at every numeric id; throw_* helpers take it
- hex_byte, hex4_lower, utf8_shape replace local hex/table code
- throw_array_index_out_of_range/_past_end, throw_key_not_found
- has_native_float_format replaces the hand-written IEEE check
No change of messages, ids or output.

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

* 1.5: use utf8_shape in get_bon8_string, add exhaustive UTF-8 validator test (#5891)

get_bon8_string takes the valid range of the second byte from utf8_shape()
(0xF5..0xF7 stay errors, unget behavior unchanged). validate_one_utf8 keeps its
if-chain (hot bulk path); the new test checks decode, validate_one_utf8,
utf8_shape, json::accept and the BON8 reader against each other for every lead
and second byte.

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

* 2.3.1: get_cbor_sized_bytes for the CBOR chunk getters (#5891)

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

* 2.3.2: get_string_with_length for the plain-order string sites (#5891)

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

* 2.3.3, 2.3.4, 2.3.10: unsupported_key_type, empty_input_message, input_format_name in the readers (#5891)

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

* 2.3.7, 2.3.9: read_object_key and leave_container_and_check for CBOR/MessagePack/BON8 (#5891)

UBJSON keys (different getter signature) and its container tail stay as is.

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

* 2.3.5: json_sax_dom_error_state base for the DOM SAX parsers (#5891)

Holds errored, allow_exceptions, parse_error() and is_errored(). The scalar
handlers stay in each class (parse hot path).

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

* 2.3.6: table of control-character messages in the lexer (#5891)

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

* 2.3.8: finish_input helper for the strict-EOF block in the parser (#5891)

sax_parse keeps its if (result) guard and return value. Also adds the
clang-tidy NOLINT for the 2.3.6 message table.

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

* 2.5.7: share parse_array_index between json_pointer and json_view (#5891)

Move json_pointer::parse_array_index() and its status enum to namespace detail
(json_pointer keeps a forwarding member), and let the json_view pointer
resolution use it instead of its own copy of the RFC 6901 index grammar. Both
accepted the same tokens with the same outcome. Also update the comment on
detail::decode(), which listed four of the five UTF-8 validators and now points
to the test that keeps them in sync.

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

* Amalgamate (#5891)

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

* 2.4.5: keep the inlined number cases in from_json(ArithmeticType&) (#5891)

Calling get_number_value() from from_json(ArithmeticType&) made
GetArray<std::array<int, 1000>> 2.4x slower (it changes the inlining of the
std::array conversion), so that overload keeps its own switch, with a comment.
get_arithmetic_value() still uses the shared helper.

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

* 2.5.7: force-inline detail::parse_array_index (#5891)

Moving parse_array_index out of json_pointer stopped clang from inlining it
into the pointer resolution (JsonPointerAt/citm_catalog +17%,
JsonPointerAt/twitter +8%); JSON_HEDLEY_ALWAYS_INLINE restores the baseline.

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

* 1.5: keep get_bon8_string's own UTF-8 ranges (#5891)

Taking the second-byte range from utf8_shape() made BON8 decoding 2-3% slower
(FromBinaryShape/scalars, FromBinaryBuffer, alignment-pinned builds), so the byte
path keeps its hand-written ranges. The exhaustive validator test still checks
that it accepts the same sequences as the other validators.

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

* Amalgamate (#5891)

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

* Fix CI failures: constexpr, NRVO, cppcheck, IWYU (#5891)

- json_sax_dom_error_state::is_errored() cannot be constexpr: the class
  is not a literal type (GCC 4.8, GCC -Wpedantic, Clang 3.5).
- to_binary_vector: destroy the vector writer before returning, as the
  output_vector_sink sets the size of the vector in its destructor.
  Without NRVO (MSVC Debug in C++14 mode), the vector was moved out
  before it was resized.
- parser::sax_parse: return false instead of result after the early exit
  (cppcheck identicalConditionAfterEarlyExit).
- json.hpp: export detail/exceptions.hpp for IWYU, so json_view.hpp can
  name exception_id without including a detail header.

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

* Address review: throw helpers take input_format_t, simpler type checks

- throw_on_discarded() and throw_integer_out_of_range() take an
  input_format_t and call input_format_name() themselves
- has_no_children(), set_parents() and max_size() check array, then
  object, instead of calling is_primitive() first
- parse_impl() chains like accept_impl(); sax_parse_impl() uses if/else
- std::array for the control character messages; negation<> in
  write_object_key(); clarify the is_primitive() comment

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

* json_view: use exception_id in the value() overloads

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

* json_sax: assert the invariant of place_container for the static analyzer

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 06:17:50 +01:00

282 lines
11 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-License-Identifier: MIT
#pragma once
#include <algorithm> // max, min
#include <cstddef> // size_t
#include <cstdint> // uint8_t, uint32_t
#include <cstring> // memcpy
#include <functional> // less
#include <memory> // unique_ptr
#include <utility> // move
#include <nlohmann/json.hpp>
#include <nlohmann/detail/view/document_data.hpp>
#include <nlohmann/detail/view/errors.hpp>
#include <nlohmann/detail/view/macro_scope.hpp>
#include <nlohmann/detail/view/node.hpp>
// The storage of edits. Edits never move or resize the parsed index: every
// value keeps its node, so views stay valid. New values and element sequences
// live in chunks that never move; strings and number tokens written by edits
// live in the edit arena. An array/object whose elements change gets
// node_flags::moved: its elements then live in a separate sequence (a header
// node, then the entries), whose entries link to the values (kind_link).
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
namespace view
{
inline document_data::edit_state& edit_state_of(document_data& d)
{
if (!d.edits)
{
d.edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): owned by the unique_ptr; std::make_unique is C++14
}
return *d.edits;
}
/// k consecutive nodes that never move (new values and blocks)
inline node* alloc_nodes(document_data& d, std::size_t k)
{
document_data::edit_state& e = edit_state_of(d);
if (NLOHMANN_VIEW_UNLIKELY(static_cast<std::size_t>(e.chunk_end - e.chunk_cur) < k))
{
if (k >= e.chunk_next / 2)
{
// a large request (half of the next chunk or more) gets a chunk of
// its own, so that the rest of the current chunk stays available
// for smaller ones
std::unique_ptr<node[]> own(new node[k]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
e.chunks.push_back(std::move(own));
e.bytes += k * sizeof(node);
return e.chunks.back().get();
}
// a smaller request leaves the rest of the current chunk unused
// (fewer than k nodes, and k is less than half of the new chunk)
const std::size_t count = e.chunk_next;
std::unique_ptr<node[]> fresh(new node[count]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
e.chunks.push_back(std::move(fresh));
e.chunk_cur = e.chunks.back().get();
e.chunk_end = e.chunk_cur + count;
e.chunk_next = (std::min)(e.chunk_next * 2, std::size_t{65536});
e.bytes += count * sizeof(node);
}
node* const r = e.chunk_cur;
e.chunk_cur += k;
return r;
}
/// The capacity of the edit arena after it grows by n bytes (`used` of `cap`
/// are taken): doubled, or what is needed plus some room, but never more than
/// the 4 GiB - 1 bytes that the 32-bit offsets of nodes can address. An error
/// if n more bytes do not fit even then.
inline std::size_t text_capacity(std::size_t cap, std::size_t used, std::size_t n)
{
constexpr std::size_t limit = 0xFFFFFFFFu;
if (NLOHMANN_VIEW_UNLIKELY(used > limit || n > limit - used))
{
throw_out_of_range(exception_id::document_too_large, "edits of 4 GiB or more are not supported by json_document");
}
const std::size_t needed = used + n;
const std::size_t wanted = needed + (std::min)(limit - needed, std::size_t{256});
const std::size_t doubled = cap > limit / 2 ? limit : cap * 2;
return (std::max)(doubled, wanted);
}
/// copy n bytes into the edit arena and return their offset; a new buffer
/// leaves the old one alive, so that string views into it remain valid; n is
/// 0 for an empty string (written by an edit, or the key of a new member)
inline std::uint32_t append_text(document_data& d, const char* s, std::size_t n)
{
document_data::edit_state& e = edit_state_of(d);
if (NLOHMANN_VIEW_UNLIKELY(e.text_cap - e.text_used < n))
{
const std::size_t cap = text_capacity(e.text_cap, e.text_used, n);
std::unique_ptr<char[]> fresh(new char[cap]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
if (NLOHMANN_VIEW_LIKELY(e.text_used != 0)) // (0 only for the first buffer, and then texts is empty)
{
std::memcpy(fresh.get(), e.texts.back().get(), e.text_used);
}
e.texts.push_back(std::move(fresh));
e.text_cap = cap;
e.bytes += cap;
d.base[2] = e.texts.back().get();
}
const auto off = static_cast<std::uint32_t>(e.text_used);
if (n != 0)
{
std::memcpy(e.texts.back().get() + e.text_used, s, n);
}
e.text_used += n;
return off;
}
/// the capacity in nodes of the block of a moved container (0: a fixed
/// sequence, the elements of a new value)
inline std::size_t moved_capacity(const document_data& d, const node* n) noexcept
{
return d.edits->moved_cap[n->off];
}
/// Make room for one more moved container. This is the part of set_moved()
/// that can throw: a caller that changes a node before it calls set_moved()
/// calls this first, so that a failure leaves the node as it was.
inline void reserve_moved(document_data& d)
{
document_data::edit_state& e = edit_state_of(d);
if (e.moved.size() >= 0xFFFFFFFFu)
{
throw_out_of_range(exception_id::document_too_large, "more than 4294967295 edited arrays and objects are not supported by json_document"); // LCOV_EXCL_LINE
}
if (e.moved.size() == e.moved.capacity() || e.moved_cap.size() == e.moved_cap.capacity())
{
// both grow before either changes, so that the push_backs cannot throw
// (both conditions are needed: reserve() may allocate more than asked,
// so the two capacities can differ)
e.moved.reserve((e.moved.size() * 2) + 16);
e.moved_cap.reserve((e.moved.size() * 2) + 16);
}
}
/// let container n take its elements from `seq` (header node first); cannot
/// throw if n is moved already or reserve_moved() was called
inline void set_moved(document_data& d, node* n, node* seq, std::size_t cap)
{
document_data::edit_state& e = edit_state_of(d);
if ((n->flags & node_flags::moved) != 0)
{
e.moved[n->off] = seq;
e.moved_cap[n->off] = cap;
return;
}
reserve_moved(d);
e.moved.push_back(seq);
e.moved_cap.push_back(cap);
n->off = static_cast<std::uint32_t>(e.moved.size() - 1);
n->flags = static_cast<std::uint8_t>(n->flags | node_flags::moved | node_flags::is_new);
}
/// Make the elements of container n a growable block with room for `extra`
/// more nodes, and return its header. The entries link to the existing
/// values, which stay where they are. A block that grows is copied (its old
/// space is not reused).
inline node* block_of(document_data& d, node* n, std::size_t extra)
{
if ((n->flags & node_flags::moved) != 0 && moved_capacity(d, n) != 0)
{
node* const h = d.edits->moved[n->off];
if (h->next + extra <= moved_capacity(d, n))
{
return h;
}
const std::size_t cap = (std::max)(2 * moved_capacity(d, n), h->next + extra);
node* const nh = alloc_nodes(d, cap);
std::memcpy(nh, h, h->next * sizeof(node));
set_moved(d, n, nh, cap);
return nh;
}
reserve_moved(d); // (so that set_moved() below cannot throw: the links are marked before)
const bool object = n->kind == static_cast<std::uint8_t>(value_t::object);
const std::size_t used = 1 + (static_cast<std::size_t>(n->len) * (object ? 2 : 1));
const std::size_t cap = used + extra;
node* const h = alloc_nodes(d, cap);
*h = node{};
h->kind = n->kind;
h->len = n->len;
h->next = static_cast<std::uint32_t>(used);
node* o = h + 1;
for (const node* c = d.first_child_edited(n), *e = d.child_end_edited(n); c != e;)
{
if (object)
{
*o++ = *c++; // the key
}
make_link(*o, const_cast<node*>(document_data::deref(c))); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document
++o;
c = document_data::after(c);
}
set_moved(d, n, h, cap);
return h;
}
/// The container whose elements include `target`; nullptr for the root, for
/// a value that is no longer part of the document, and for a value that is
/// only reached through a link. Values never move between allocations, so
/// the path to `target` stays inside the allocation that holds it (the parsed
/// index, or one new value), where the extent of each container (`next`)
/// still covers its original subtree.
inline node* find_parent(const document_data& d, const node* target)
{
const std::less<const node*> lt{};
const node* lo = d.nodes;
const node* hi = d.nodes + d.node_count;
const node* c = d.nodes;
if (lt(target, lo) || !lt(target, hi))
{
if (!d.edits)
{
return nullptr; // LCOV_EXCL_LINE (nodes outside the index exist only after edits)
}
auto it = d.edits->regions.upper_bound(target);
if (it == d.edits->regions.begin())
{
return nullptr; // LCOV_EXCL_LINE (an array/object with elements is in the index or a new value)
}
--it;
lo = it->first;
hi = lo + lo->next;
if (!lt(target, hi))
{
return nullptr; // LCOV_EXCL_LINE (a single-node value, reached through a link)
}
// the root of a new value is the element sequence of its owner, or a linked value
c = it->second != nullptr ? it->second : lo;
}
if (target == lo)
{
return nullptr;
}
for (;;)
{
if (!is_container(*c))
{
return nullptr; // LCOV_EXCL_LINE (the value is inside c)
}
const bool object = c->kind == static_cast<std::uint8_t>(value_t::object);
const node* down = nullptr;
for (const node* p = d.first_child_edited(c), *e = d.child_end_edited(c); p != e;)
{
const node* const at = object ? p + 1 : p;
const node* const v = document_data::deref(at);
if (v == target)
{
return const_cast<node*>(c); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document
}
if (is_container(*v) && !lt(v, lo) && lt(v, target) && lt(target, v + v->next))
{
down = v;
break;
}
p = document_data::after(at);
}
if (down == nullptr)
{
return nullptr;
}
c = down;
}
}
} // namespace view
} // namespace detail
NLOHMANN_JSON_NAMESPACE_END