Compare commits

..
Author SHA1 Message Date
Niels Lohmann 8e4842cbf7 Merge branch 'develop' into copy-scratch-allocator
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 18:07:28 +02:00
Niels Lohmann f386f367df Merge branch 'develop' into copy-scratch-allocator
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:24:19 +02:00
Niels Lohmann abf827a5bd Count allocate_at_least in the scratch-counting test allocator
From C++23 on, libc++'s containers allocate through allocate_at_least when
the allocator has one. The test allocator inherited it from std::allocator,
so the scratch allocations were not counted and the test failed on Xcode.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:20:13 +02:00
Niels Lohmann a335a81ba7 Allocate the deep copy's key scratch space with the provided allocator
The iterative deep copy builds each object's keys in a temporary vector of
key/value pairs before handing them to the object's range constructor. That
vector holds basic_json values, so like the values themselves it now uses
AllocatorType instead of std::allocator.

Also document that AllocatorType covers the JSON values, while most
temporary storage still uses std::allocator.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 22:10:07 +02:00
7 changed files with 293 additions and 513 deletions
+3 -3
View File
@@ -46,8 +46,9 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
## Complexity ## Complexity
Linear in the size of the JSON value `j`. The length prefixes of all nested documents and arrays are computed in one Proportional to the size of the JSON value `j` multiplied by its maximum nesting
pass before anything is written. depth, `O(n × d)`. BSON length prefixes are computed recursively before nested
values are written.
## Examples ## Examples
@@ -76,4 +77,3 @@ pass before anything is written.
## Version history ## Version history
- Added in version 3.4.0. - Added in version 3.4.0.
- Linear in the size of `j`, and no longer limited by the call stack for deeply nested values, since version 3.13.0.
@@ -562,7 +562,11 @@ binary32 or binary64 field and have no encoding for `#!cpp long double`.
## `AllocatorType` ## `AllocatorType`
`AllocatorType` is instantiated with **one** argument, for each of `object_t`, `array_t`, `string_t`, `binary_t`, `AllocatorType` is instantiated with **one** argument, for each of `object_t`, `array_t`, `string_t`, `binary_t`,
`basic_json`, and `#!cpp std::pair<const StringType, basic_json>`. `basic_json`, `#!cpp std::pair<const StringType, basic_json>`, and `#!cpp std::pair<StringType, basic_json>`.
`AllocatorType` is not the only allocator a `basic_json` uses. It allocates the JSON values themselves, but most
temporary storage is allocated with `#!cpp std::allocator`. This includes the parser's stacks and the stacks that
process deeply nested values without recursion.
### Always required ### Always required
+102 -221
View File
@@ -122,7 +122,7 @@ class binary_writer
{ {
case value_t::object: case value_t::object:
{ {
write_bson_document(j); write_bson_object(*j.m_data.m_value.object);
break; break;
} }
@@ -1197,6 +1197,35 @@ class binary_writer
} }
} }
/*!
@brief Writes a BSON element with key @a name and object @a value
*/
void write_bson_object_entry(const string_t& name,
const typename BasicJsonType::object_t& value)
{
write_bson_entry_header(name, 0x03); // object
write_bson_object(value);
}
/*!
@return The size of the BSON-encoded array @a value
*/
static std::size_t calc_bson_array_size(const typename BasicJsonType::array_t& value)
{
std::size_t array_index = 0ul;
const std::size_t embedded_document_size = std::accumulate(std::begin(value), std::end(value), static_cast<std::size_t>(0), [&array_index](std::size_t result, const typename BasicJsonType::array_t::value_type & el)
{
// the index is built as a std::string, while calc_bson_element_size
// takes a string_t; convert explicitly, as the two are only
// implicitly convertible for some string types
const auto key = std::to_string(array_index++);
return result + calc_bson_element_size(string_t(key.data(), key.size()), el);
});
return sizeof(std::int32_t) + embedded_document_size + 1ul;
}
/*! /*!
@return The size of the BSON-encoded binary array @a value @return The size of the BSON-encoded binary array @a value
*/ */
@@ -1205,6 +1234,29 @@ class binary_writer
return sizeof(std::int32_t) + value.size() + 1ul; return sizeof(std::int32_t) + value.size() + 1ul;
} }
/*!
@brief Writes a BSON element with key @a name and array @a value
*/
void write_bson_array(const string_t& name,
const typename BasicJsonType::array_t& value)
{
write_bson_entry_header(name, 0x04); // array
write_number<std::int32_t>(to_bson_length(calc_bson_array_size(value)), true);
std::size_t array_index = 0ul;
for (const auto& el : value)
{
// the index is built as a std::string, while write_bson_element takes
// a string_t; convert explicitly, as the two are only implicitly
// convertible for some string types
const auto key = std::to_string(array_index++);
write_bson_element(string_t(key.data(), key.size()), el);
}
oa.write_character(to_char_type(0x00));
}
/*! /*!
@brief Writes a BSON element with key @a name and binary value @a value @brief Writes a BSON element with key @a name and binary value @a value
*/ */
@@ -1226,37 +1278,43 @@ class binary_writer
} }
/*! /*!
@return The size of the value of the BSON document entry for @a j, which @brief Calculates the size necessary to serialize the JSON value @a j with its @a name
is neither an object nor an array @return The calculated size for the BSON document entry for @a j with the given @a name.
*/ */
static std::size_t calc_bson_value_size(const BasicJsonType& j) static std::size_t calc_bson_element_size(const string_t& name,
const BasicJsonType& j)
{ {
const auto header_size = calc_bson_entry_header_size(name, j);
switch (j.type()) switch (j.type())
{ {
case value_t::object:
return header_size + calc_bson_object_size(*j.m_data.m_value.object);
case value_t::array:
return header_size + calc_bson_array_size(*j.m_data.m_value.array);
case value_t::binary: case value_t::binary:
return calc_bson_binary_size(*j.m_data.m_value.binary); return header_size + calc_bson_binary_size(*j.m_data.m_value.binary);
case value_t::boolean: case value_t::boolean:
return 1ul; return header_size + 1ul;
case value_t::number_float: case value_t::number_float:
return 8ul; return header_size + 8ul;
case value_t::number_integer: case value_t::number_integer:
return calc_bson_integer_size(j.m_data.m_value.number_integer); return header_size + calc_bson_integer_size(j.m_data.m_value.number_integer);
case value_t::number_unsigned: case value_t::number_unsigned:
return calc_bson_unsigned_size(j.m_data.m_value.number_unsigned); return header_size + calc_bson_unsigned_size(j.m_data.m_value.number_unsigned);
case value_t::string: case value_t::string:
return calc_bson_string_size(*j.m_data.m_value.string); return header_size + calc_bson_string_size(*j.m_data.m_value.string);
case value_t::null: case value_t::null:
return 0ul; return header_size + 0ul;
// LCOV_EXCL_START // LCOV_EXCL_START
case value_t::object:
case value_t::array:
case value_t::discarded: case value_t::discarded:
default: default:
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert)
@@ -1266,13 +1324,22 @@ class binary_writer
} }
/*! /*!
@brief Writes the BSON document entry with key @a name for @a j, which is @brief Serializes the JSON value @a j to BSON and associates it with the
neither an object nor an array key @a name.
@param name The name to associate with the JSON entity @a j within the
current BSON document
*/ */
void write_bson_value(const string_t& name, const BasicJsonType& j) void write_bson_element(const string_t& name,
const BasicJsonType& j)
{ {
switch (j.type()) switch (j.type())
{ {
case value_t::object:
return write_bson_object_entry(name, *j.m_data.m_value.object);
case value_t::array:
return write_bson_array(name, *j.m_data.m_value.array);
case value_t::binary: case value_t::binary:
return write_bson_binary(name, *j.m_data.m_value.binary); return write_bson_binary(name, *j.m_data.m_value.binary);
@@ -1295,8 +1362,6 @@ class binary_writer
return write_bson_null(name); return write_bson_null(name);
// LCOV_EXCL_START // LCOV_EXCL_START
case value_t::object:
case value_t::array:
case value_t::discarded: case value_t::discarded:
default: default:
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert)
@@ -1305,221 +1370,37 @@ class binary_writer
} }
} }
/// @brief an object or array of the BSON document being sized or written
struct bson_frame
{
explicit bson_frame(const BasicJsonType* value_, const std::size_t size_slot_ = 0)
: value(value_)
, size_slot(size_slot_)
{
if (value->is_object())
{
member = value->m_data.m_value.object->cbegin();
}
}
/// the object or array
const BasicJsonType* value;
/// objects: the next member
typename BasicJsonType::object_t::const_iterator member{};
/// arrays: the index of the next element
std::size_t index = 0;
/// @ref calc_bson_sizes only: where its size goes in the table
std::size_t size_slot;
/// @ref calc_bson_sizes only: the size of its entries seen so far
std::size_t entries_size = 0;
};
/*! /*!
@brief creates the name BSON gives the array element with index @a index @brief Calculates the size of the BSON serialization of the given
@param[out] name receives the decimal index JSON-object @a j.
@param[in] value JSON value to serialize
@pre value.type() == value_t::object
*/ */
static void create_bson_index_name(const std::size_t index, string_t& name) static std::size_t calc_bson_object_size(const typename BasicJsonType::object_t& value)
{ {
// the index is built as a std::string; convert explicitly, as the const std::size_t document_size = std::accumulate(value.begin(), value.end(), static_cast<std::size_t>(0),
// two are only implicitly convertible for some string types [](size_t result, const typename BasicJsonType::object_t::value_type & el)
const auto key = std::to_string(index); {
name = string_t(key.data(), key.size()); return result += calc_bson_element_size(el.first, el.second);
});
return sizeof(std::int32_t) + document_size + 1ul;
} }
/*! /*!
@brief Calculates the size of every object and array in the BSON document @param[in] value JSON value to serialize
@a document, including the document itself. @pre value.type() == value_t::object
BSON prefixes every document and array with its size, so all of them have
to be known before the first byte is written. They are computed in a
single pass, each one from the sizes of its entries, which keeps
serializing linear in the size of the document; computing each size by
walking the entire value below it made it quadratic in the nesting depth.
The pass keeps the objects and arrays it has entered on an explicit stack,
so a deeply nested value cannot exhaust the call stack.
@param[in] document the JSON object to serialize
@param[out] nested_sizes the sizes of the objects and arrays in
@a document, in the order they are written
@return the size of @a document
@throw out_of_range.409 if a key contains U+0000, before anything is
written
*/ */
static std::size_t calc_bson_sizes(const BasicJsonType& document, std::vector<std::size_t>& nested_sizes) void write_bson_object(const typename BasicJsonType::object_t& value)
{ {
// the object or array whose entries are being sized, and the ones it write_number<std::int32_t>(to_bson_length(calc_bson_object_size(value)), true);
// is in; nothing is allocated unless the document nests
bson_frame current(&document);
std::vector<bson_frame> parents;
// string_t need not be default constructible
string_t index_name("", 0);
while (true) for (const auto& el : value)
{ {
// size entries until the current object or array is done, or an write_bson_element(el.first, el.second);
// entry is an object or array itself
const BasicJsonType* nested = nullptr;
if (current.value->is_object())
{
const auto& object = *current.value->m_data.m_value.object;
while (nested == nullptr && current.member != object.cend())
{
const auto& el = *current.member;
++current.member;
current.entries_size += calc_bson_entry_header_size(el.first, el.second);
if (el.second.is_structured())
{
nested = &el.second;
}
else
{
current.entries_size += calc_bson_value_size(el.second);
}
}
}
else
{
const auto& array = *current.value->m_data.m_value.array;
while (nested == nullptr && current.index < array.size())
{
const BasicJsonType& el = array[current.index];
create_bson_index_name(current.index, index_name);
current.entries_size += calc_bson_entry_header_size(index_name, el);
++current.index;
if (el.is_structured())
{
nested = &el;
}
else
{
current.entries_size += calc_bson_value_size(el);
}
}
}
if (nested != nullptr)
{
// its size is added to the current one's once it is done
nested_sizes.push_back(0);
parents.push_back(std::move(current));
current = bson_frame(nested, nested_sizes.size() - 1);
continue;
}
// the int32 size, the entries, and the terminating null byte
const std::size_t size = sizeof(std::int32_t) + current.entries_size + 1ul;
if (parents.empty())
{
return size;
}
nested_sizes[current.size_slot] = size;
current = std::move(parents.back());
parents.pop_back();
current.entries_size += size;
} }
}
/*! oa.write_character(to_char_type(0x00));
@brief Serializes the JSON object @a document as a BSON document
Writes the objects and arrays in it without the call stack, keeping the
ones it has entered on an explicit stack, so a deeply nested value
cannot exhaust the call stack.
@param[in] document the JSON object to serialize
@pre document.type() == value_t::object
*/
void write_bson_document(const BasicJsonType& document)
{
std::vector<std::size_t> nested_sizes;
const std::size_t document_size = calc_bson_sizes(document, nested_sizes);
write_number<std::int32_t>(to_bson_length(document_size), true);
// the object or array whose entries are being written, and the ones
// it is in
bson_frame current(&document);
std::vector<bson_frame> parents;
std::size_t next_size = 0;
// string_t need not be default constructible
string_t index_name("", 0);
while (true)
{
// write entries until the current object or array is done, or an
// entry is an object or array itself
const string_t* nested_name = nullptr;
const BasicJsonType* nested = nullptr;
if (current.value->is_object())
{
const auto& object = *current.value->m_data.m_value.object;
while (nested == nullptr && current.member != object.cend())
{
const auto& el = *current.member;
++current.member;
if (el.second.is_structured())
{
nested_name = &el.first;
nested = &el.second;
}
else
{
write_bson_value(el.first, el.second);
}
}
}
else
{
const auto& array = *current.value->m_data.m_value.array;
while (nested == nullptr && current.index < array.size())
{
const BasicJsonType& el = array[current.index];
create_bson_index_name(current.index, index_name);
++current.index;
if (el.is_structured())
{
nested_name = &index_name;
nested = &el;
}
else
{
write_bson_value(index_name, el);
}
}
}
if (nested != nullptr)
{
write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04);
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
parents.push_back(std::move(current));
current = bson_frame(nested);
continue;
}
oa.write_character(to_char_type(0x00));
if (parents.empty())
{
return;
}
current = std::move(parents.back());
parents.pop_back();
}
} }
////////// //////////
+2 -1
View File
@@ -1003,7 +1003,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
using copy_worklist_t = std::vector<std::pair<const basic_json*, basic_json*>>; using copy_worklist_t = std::vector<std::pair<const basic_json*, basic_json*>>;
/// scratch space to build the key skeleton of an object copy in one go /// scratch space to build the key skeleton of an object copy in one go
using copy_scratch_t = std::vector<std::pair<typename object_t::key_type, basic_json>>; using copy_scratch_value_t = std::pair<typename object_t::key_type, basic_json>;
using copy_scratch_t = std::vector<copy_scratch_value_t, AllocatorType<copy_scratch_value_t>>;
/// @brief copy everything of @a src into @a dst but its type and value /// @brief copy everything of @a src into @a dst but its type and value
static void copy_metadata(const basic_json& src, basic_json& dst) static void copy_metadata(const basic_json& src, basic_json& dst)
+104 -222
View File
@@ -19337,7 +19337,7 @@ class binary_writer
{ {
case value_t::object: case value_t::object:
{ {
write_bson_document(j); write_bson_object(*j.m_data.m_value.object);
break; break;
} }
@@ -20412,6 +20412,35 @@ class binary_writer
} }
} }
/*!
@brief Writes a BSON element with key @a name and object @a value
*/
void write_bson_object_entry(const string_t& name,
const typename BasicJsonType::object_t& value)
{
write_bson_entry_header(name, 0x03); // object
write_bson_object(value);
}
/*!
@return The size of the BSON-encoded array @a value
*/
static std::size_t calc_bson_array_size(const typename BasicJsonType::array_t& value)
{
std::size_t array_index = 0ul;
const std::size_t embedded_document_size = std::accumulate(std::begin(value), std::end(value), static_cast<std::size_t>(0), [&array_index](std::size_t result, const typename BasicJsonType::array_t::value_type & el)
{
// the index is built as a std::string, while calc_bson_element_size
// takes a string_t; convert explicitly, as the two are only
// implicitly convertible for some string types
const auto key = std::to_string(array_index++);
return result + calc_bson_element_size(string_t(key.data(), key.size()), el);
});
return sizeof(std::int32_t) + embedded_document_size + 1ul;
}
/*! /*!
@return The size of the BSON-encoded binary array @a value @return The size of the BSON-encoded binary array @a value
*/ */
@@ -20420,6 +20449,29 @@ class binary_writer
return sizeof(std::int32_t) + value.size() + 1ul; return sizeof(std::int32_t) + value.size() + 1ul;
} }
/*!
@brief Writes a BSON element with key @a name and array @a value
*/
void write_bson_array(const string_t& name,
const typename BasicJsonType::array_t& value)
{
write_bson_entry_header(name, 0x04); // array
write_number<std::int32_t>(to_bson_length(calc_bson_array_size(value)), true);
std::size_t array_index = 0ul;
for (const auto& el : value)
{
// the index is built as a std::string, while write_bson_element takes
// a string_t; convert explicitly, as the two are only implicitly
// convertible for some string types
const auto key = std::to_string(array_index++);
write_bson_element(string_t(key.data(), key.size()), el);
}
oa.write_character(to_char_type(0x00));
}
/*! /*!
@brief Writes a BSON element with key @a name and binary value @a value @brief Writes a BSON element with key @a name and binary value @a value
*/ */
@@ -20441,37 +20493,43 @@ class binary_writer
} }
/*! /*!
@return The size of the value of the BSON document entry for @a j, which @brief Calculates the size necessary to serialize the JSON value @a j with its @a name
is neither an object nor an array @return The calculated size for the BSON document entry for @a j with the given @a name.
*/ */
static std::size_t calc_bson_value_size(const BasicJsonType& j) static std::size_t calc_bson_element_size(const string_t& name,
const BasicJsonType& j)
{ {
const auto header_size = calc_bson_entry_header_size(name, j);
switch (j.type()) switch (j.type())
{ {
case value_t::object:
return header_size + calc_bson_object_size(*j.m_data.m_value.object);
case value_t::array:
return header_size + calc_bson_array_size(*j.m_data.m_value.array);
case value_t::binary: case value_t::binary:
return calc_bson_binary_size(*j.m_data.m_value.binary); return header_size + calc_bson_binary_size(*j.m_data.m_value.binary);
case value_t::boolean: case value_t::boolean:
return 1ul; return header_size + 1ul;
case value_t::number_float: case value_t::number_float:
return 8ul; return header_size + 8ul;
case value_t::number_integer: case value_t::number_integer:
return calc_bson_integer_size(j.m_data.m_value.number_integer); return header_size + calc_bson_integer_size(j.m_data.m_value.number_integer);
case value_t::number_unsigned: case value_t::number_unsigned:
return calc_bson_unsigned_size(j.m_data.m_value.number_unsigned); return header_size + calc_bson_unsigned_size(j.m_data.m_value.number_unsigned);
case value_t::string: case value_t::string:
return calc_bson_string_size(*j.m_data.m_value.string); return header_size + calc_bson_string_size(*j.m_data.m_value.string);
case value_t::null: case value_t::null:
return 0ul; return header_size + 0ul;
// LCOV_EXCL_START // LCOV_EXCL_START
case value_t::object:
case value_t::array:
case value_t::discarded: case value_t::discarded:
default: default:
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert)
@@ -20481,13 +20539,22 @@ class binary_writer
} }
/*! /*!
@brief Writes the BSON document entry with key @a name for @a j, which is @brief Serializes the JSON value @a j to BSON and associates it with the
neither an object nor an array key @a name.
@param name The name to associate with the JSON entity @a j within the
current BSON document
*/ */
void write_bson_value(const string_t& name, const BasicJsonType& j) void write_bson_element(const string_t& name,
const BasicJsonType& j)
{ {
switch (j.type()) switch (j.type())
{ {
case value_t::object:
return write_bson_object_entry(name, *j.m_data.m_value.object);
case value_t::array:
return write_bson_array(name, *j.m_data.m_value.array);
case value_t::binary: case value_t::binary:
return write_bson_binary(name, *j.m_data.m_value.binary); return write_bson_binary(name, *j.m_data.m_value.binary);
@@ -20510,8 +20577,6 @@ class binary_writer
return write_bson_null(name); return write_bson_null(name);
// LCOV_EXCL_START // LCOV_EXCL_START
case value_t::object:
case value_t::array:
case value_t::discarded: case value_t::discarded:
default: default:
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert)
@@ -20520,221 +20585,37 @@ class binary_writer
} }
} }
/// @brief an object or array of the BSON document being sized or written
struct bson_frame
{
explicit bson_frame(const BasicJsonType* value_, const std::size_t size_slot_ = 0)
: value(value_)
, size_slot(size_slot_)
{
if (value->is_object())
{
member = value->m_data.m_value.object->cbegin();
}
}
/// the object or array
const BasicJsonType* value;
/// objects: the next member
typename BasicJsonType::object_t::const_iterator member{};
/// arrays: the index of the next element
std::size_t index = 0;
/// @ref calc_bson_sizes only: where its size goes in the table
std::size_t size_slot;
/// @ref calc_bson_sizes only: the size of its entries seen so far
std::size_t entries_size = 0;
};
/*! /*!
@brief creates the name BSON gives the array element with index @a index @brief Calculates the size of the BSON serialization of the given
@param[out] name receives the decimal index JSON-object @a j.
@param[in] value JSON value to serialize
@pre value.type() == value_t::object
*/ */
static void create_bson_index_name(const std::size_t index, string_t& name) static std::size_t calc_bson_object_size(const typename BasicJsonType::object_t& value)
{ {
// the index is built as a std::string; convert explicitly, as the const std::size_t document_size = std::accumulate(value.begin(), value.end(), static_cast<std::size_t>(0),
// two are only implicitly convertible for some string types [](size_t result, const typename BasicJsonType::object_t::value_type & el)
const auto key = std::to_string(index); {
name = string_t(key.data(), key.size()); return result += calc_bson_element_size(el.first, el.second);
});
return sizeof(std::int32_t) + document_size + 1ul;
} }
/*! /*!
@brief Calculates the size of every object and array in the BSON document @param[in] value JSON value to serialize
@a document, including the document itself. @pre value.type() == value_t::object
BSON prefixes every document and array with its size, so all of them have
to be known before the first byte is written. They are computed in a
single pass, each one from the sizes of its entries, which keeps
serializing linear in the size of the document; computing each size by
walking the entire value below it made it quadratic in the nesting depth.
The pass keeps the objects and arrays it has entered on an explicit stack,
so a deeply nested value cannot exhaust the call stack.
@param[in] document the JSON object to serialize
@param[out] nested_sizes the sizes of the objects and arrays in
@a document, in the order they are written
@return the size of @a document
@throw out_of_range.409 if a key contains U+0000, before anything is
written
*/ */
static std::size_t calc_bson_sizes(const BasicJsonType& document, std::vector<std::size_t>& nested_sizes) void write_bson_object(const typename BasicJsonType::object_t& value)
{ {
// the object or array whose entries are being sized, and the ones it write_number<std::int32_t>(to_bson_length(calc_bson_object_size(value)), true);
// is in; nothing is allocated unless the document nests
bson_frame current(&document);
std::vector<bson_frame> parents;
// string_t need not be default constructible
string_t index_name("", 0);
while (true) for (const auto& el : value)
{ {
// size entries until the current object or array is done, or an write_bson_element(el.first, el.second);
// entry is an object or array itself
const BasicJsonType* nested = nullptr;
if (current.value->is_object())
{
const auto& object = *current.value->m_data.m_value.object;
while (nested == nullptr && current.member != object.cend())
{
const auto& el = *current.member;
++current.member;
current.entries_size += calc_bson_entry_header_size(el.first, el.second);
if (el.second.is_structured())
{
nested = &el.second;
}
else
{
current.entries_size += calc_bson_value_size(el.second);
}
}
}
else
{
const auto& array = *current.value->m_data.m_value.array;
while (nested == nullptr && current.index < array.size())
{
const BasicJsonType& el = array[current.index];
create_bson_index_name(current.index, index_name);
current.entries_size += calc_bson_entry_header_size(index_name, el);
++current.index;
if (el.is_structured())
{
nested = &el;
}
else
{
current.entries_size += calc_bson_value_size(el);
}
}
}
if (nested != nullptr)
{
// its size is added to the current one's once it is done
nested_sizes.push_back(0);
parents.push_back(std::move(current));
current = bson_frame(nested, nested_sizes.size() - 1);
continue;
}
// the int32 size, the entries, and the terminating null byte
const std::size_t size = sizeof(std::int32_t) + current.entries_size + 1ul;
if (parents.empty())
{
return size;
}
nested_sizes[current.size_slot] = size;
current = std::move(parents.back());
parents.pop_back();
current.entries_size += size;
} }
}
/*! oa.write_character(to_char_type(0x00));
@brief Serializes the JSON object @a document as a BSON document
Writes the objects and arrays in it without the call stack, keeping the
ones it has entered on an explicit stack, so a deeply nested value
cannot exhaust the call stack.
@param[in] document the JSON object to serialize
@pre document.type() == value_t::object
*/
void write_bson_document(const BasicJsonType& document)
{
std::vector<std::size_t> nested_sizes;
const std::size_t document_size = calc_bson_sizes(document, nested_sizes);
write_number<std::int32_t>(to_bson_length(document_size), true);
// the object or array whose entries are being written, and the ones
// it is in
bson_frame current(&document);
std::vector<bson_frame> parents;
std::size_t next_size = 0;
// string_t need not be default constructible
string_t index_name("", 0);
while (true)
{
// write entries until the current object or array is done, or an
// entry is an object or array itself
const string_t* nested_name = nullptr;
const BasicJsonType* nested = nullptr;
if (current.value->is_object())
{
const auto& object = *current.value->m_data.m_value.object;
while (nested == nullptr && current.member != object.cend())
{
const auto& el = *current.member;
++current.member;
if (el.second.is_structured())
{
nested_name = &el.first;
nested = &el.second;
}
else
{
write_bson_value(el.first, el.second);
}
}
}
else
{
const auto& array = *current.value->m_data.m_value.array;
while (nested == nullptr && current.index < array.size())
{
const BasicJsonType& el = array[current.index];
create_bson_index_name(current.index, index_name);
++current.index;
if (el.is_structured())
{
nested_name = &index_name;
nested = &el;
}
else
{
write_bson_value(index_name, el);
}
}
}
if (nested != nullptr)
{
write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04);
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
parents.push_back(std::move(current));
current = bson_frame(nested);
continue;
}
oa.write_character(to_char_type(0x00));
if (parents.empty())
{
return;
}
current = std::move(parents.back());
parents.pop_back();
}
} }
////////// //////////
@@ -25767,7 +25648,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
using copy_worklist_t = std::vector<std::pair<const basic_json*, basic_json*>>; using copy_worklist_t = std::vector<std::pair<const basic_json*, basic_json*>>;
/// scratch space to build the key skeleton of an object copy in one go /// scratch space to build the key skeleton of an object copy in one go
using copy_scratch_t = std::vector<std::pair<typename object_t::key_type, basic_json>>; using copy_scratch_value_t = std::pair<typename object_t::key_type, basic_json>;
using copy_scratch_t = std::vector<copy_scratch_value_t, AllocatorType<copy_scratch_value_t>>;
/// @brief copy everything of @a src into @a dst but its type and value /// @brief copy everything of @a src into @a dst but its type and value
static void copy_metadata(const basic_json& src, basic_json& dst) static void copy_metadata(const basic_json& src, basic_json& dst)
+76
View File
@@ -270,6 +270,82 @@ TEST_CASE("controlled bad_alloc")
} }
} }
namespace
{
// counts the allocations of pairs with a non-const first member: the object
// types store std::pair<const Key, T>, so only the scratch space of the
// iterative deep copy allocates std::pair<Key, T>
std::size_t scratch_pair_allocations = 0;
template<class T>
struct is_scratch_pair : std::false_type {};
template<class K, class V>
struct is_scratch_pair<std::pair<K, V>> : std::integral_constant < bool, !std::is_const<K>::value > {};
template<class T>
struct scratch_counting_allocator : std::allocator<T>
{
using std::allocator<T>::allocator;
T* allocate(std::size_t n)
{
if (is_scratch_pair<T>::value)
{
++scratch_pair_allocations;
}
return std::allocator<T>::allocate(n);
}
#ifdef __cpp_lib_allocate_at_least
// std::allocator<T>::allocate_at_least would bypass the counting, and
// libc++'s containers prefer it over allocate from C++23 on
auto allocate_at_least(std::size_t n)
{
if (is_scratch_pair<T>::value)
{
++scratch_pair_allocations;
}
return std::allocator<T>::allocate_at_least(n);
}
#endif
template <class U>
struct rebind
{
using other = scratch_counting_allocator<U>;
};
};
} // namespace
TEST_CASE("deep copy uses the provided allocator")
{
using counting_json = nlohmann::basic_json<std::map,
std::vector,
std::string,
bool,
std::int64_t,
std::uint64_t,
double,
scratch_counting_allocator>;
// deeper than the 128 levels the copy constructor descends into, so the
// innermost objects are copied by the iterative deep copy
counting_json j = 1;
for (std::size_t i = 0; i < 300; ++i)
{
counting_json wrapper = counting_json::object();
wrapper["a"] = std::move(j);
j = std::move(wrapper);
}
scratch_pair_allocations = 0;
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is tested
const counting_json copy(j);
CHECK(scratch_pair_allocations > 0);
CHECK(copy == j);
}
namespace namespace
{ {
template<class T> template<class T>
+1 -65
View File
@@ -49,7 +49,7 @@ using huge_binary_json = nlohmann::basic_json <
// for *object keys* (e.g. "s" or "nested" below). Only the designated test // for *object keys* (e.g. "s" or "nested" below). Only the designated test
// value is meant to lie about its size - if every huge_string_t (including // value is meant to lie about its size - if every huge_string_t (including
// keys) reported a huge size, the running totals computed while walking the // keys) reported a huge size, the running totals computed while walking the
// BSON document (see calc_bson_sizes in binary_writer.hpp) // BSON document (see calc_bson_object_size & friends in binary_writer.hpp)
// would need more than 32 bits, and on platforms where std::size_t is only // would need more than 32 bits, and on platforms where std::size_t is only
// 32 bits wide that arithmetic would silently wrap around, producing wrong // 32 bits wide that arithmetic would silently wrap around, producing wrong
// (or even unguarded) lengths. The fake size is therefore opt-in via // (or even unguarded) lengths. The fake size is therefore opt-in via
@@ -1697,67 +1697,3 @@ TEST_CASE("BSON roundtrips" * doctest::skip())
} }
} }
} }
TEST_CASE("BSON: deeply nested values")
{
SECTION("documents and arrays round-trip at every depth")
{
// nested documents and arrays, with siblings on every level, so
// every length prefix covers entries of both kinds
json value = "leaf";
for (std::size_t depth = 0; depth <= 300; ++depth)
{
CAPTURE(depth);
const json document = {{"value", value}, {"n", depth}};
CHECK(json::from_bson(json::to_bson(document)) == document);
value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} :
json::array({std::move(value), depth, json::object()});
}
}
SECTION("a key containing U+0000 is rejected before anything is written")
{
json value = json::object({{std::string("bad\0key", 7), 1}});
for (std::size_t depth = 0; depth < 200; ++depth)
{
value = json{{"a", {{"b", 1}}}, {"z", std::move(value)}};
}
std::vector<std::uint8_t> output;
CHECK_THROWS_AS(json::to_bson(value, output), json::out_of_range&);
CHECK(output.empty());
}
SECTION("values nested too deeply for the call stack (#5392)")
{
// serializing recursed once per nesting level, and computed every
// nested document's length by walking everything below it again.
// The values are only parsed, serialized and walked, never copied or
// compared, since those recurse too.
const std::size_t depth = 100000;
for (const bool objects :
{
false, true
})
{
CAPTURE(objects);
std::string text = "{\"a\":";
for (std::size_t i = 0; i < depth; ++i)
{
text += objects ? "{\"a\":" : "[";
}
text += "1";
text.append(depth, objects ? '}' : ']');
text += "}";
const auto bson = json::to_bson(json::parse(text));
const auto result = json::from_bson(bson);
const json* p = &result.at("a");
for (std::size_t i = 0; i < depth; ++i)
{
p = objects ? &p->at("a") : &p->at(0);
}
CHECK(*p == 1);
}
}
}