Files
json/include/nlohmann/detail/view/edit_storage.hpp
T
Niels Lohmann 76897a9b9b Add editable documents with set() and push_back()
basic_json_document<BasicJsonType, true> (json_editable_document,
ordered_json_editable_document) can be edited:

- set(view, value): replace a value
- set(object, key, value): assign a member, or add it (a null becomes an
  object); with duplicate keys, the first is assigned and the others go
- set(array, index, value): assign an element
- set(json_pointer, value): the member, element, or ("-", or the size of
  the array) the end of an array a pointer names
- push_back(array, value): append (a null becomes an array)

Values are views (of any document, copied), BasicJsonType values, and
everything BasicJsonType can be constructed from. The source text is never
written, and the parsed index never moves: new values and element
sequences go to storage owned by the document (edit_storage.hpp), so views
stay valid, and a view keeps referring to its value (after an assignment,
it sees the new one). Read-only documents are unchanged; editing one does
not compile.

Errors are those of basic_json where the operation corresponds
(type_error.305/308, out_of_range.401/403/405, parse_error.106/109); a view
of another document is invalid_iterator.202. Strings are checked for UTF-8
when they enter the document, with the type_error.316 that
basic_json::dump() throws for the same string, so that a document only
holds valid UTF-8. Binary values cannot be stored (the new
type_error.319), and edits of 4 GiB or more end with out_of_range.416.
Views of editable and read-only documents compare with each other.

Tests (unit-json_view_edit.cpp): random assignments, member and element
changes, copies within and between documents, and pushes, applied to an
ordered_json_editable_document and to the ordered_json value; after every
edit both must serialize (also indented and with ensure_ascii),
materialize, compare, and read back the same. Further: the errors, strings
that stay valid while the edit arena grows, numbers (NaN, infinities,
extremes; number_format::source), nulls that become containers, the root
replaced, duplicate keys, values of other documents, large objects, and
documents reused with read().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 14:03:11 +02:00

250 lines
8.3 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());
}
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))
{
const std::size_t count = (std::max)(k, e.chunk_next);
std::unique_ptr<node[]> fresh(new node[count]());
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;
}
/// 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
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 = (std::max)(e.text_cap * 2, e.text_used + n + 256);
if (cap > 0xFFFFFFFFu)
{
throw_out_of_range(416, "edits of 4 GiB or more are not supported by json_document");
}
std::unique_ptr<char[]> fresh(new char[cap]);
if (e.text_used != 0)
{
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];
}
/// let container n take its elements from `seq` (header node first)
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;
}
if (e.moved.size() >= 0xFFFFFFFFu)
{
throw_out_of_range(416, "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
e.moved.reserve((2 * e.moved.size()) + 16);
e.moved_cap.reserve((2 * e.moved.size()) + 16);
}
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;
}
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
}
if (c->kind == kind_link)
{
*o = *c;
}
else
{
make_link(*o, c);
}
++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.tape;
const node* hi = d.tape + d.tape_size;
const node* c = d.tape;
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;
}
--it;
lo = it->first;
hi = lo + lo->next;
if (!lt(target, hi))
{
return nullptr; // 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;
}
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