From a1e91474a8918f5cde4a44e24187b84c70add9fc Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 19 Aug 2026 22:43:17 +0200 Subject: [PATCH] Split the write-buffer helpers and write indentation directly Follow-up to @gregmarr's review: put_chars() was doing four unrelated jobs, so give the two that can be made safe their own entry points. - put_literal(): takes the literal by reference and deduces the length from the array bound, so the 27 hand-counted lengths at the call sites can no longer drift from the literals they describe. A literal is checked at compile time to fit the buffer, so this path needs no write-through branch. - put_buffer(): takes the fixed-size buffer itself rather than a bare pointer, so the length can be checked against the buffer's own bound. - put_indent(): memsets the indentation into the write buffer, filling and flushing it as needed. This removes indent_string entirely, and with it both bugs of #5186: the indentation string was grown by doubling, which is not enough when indent_step more than doubles it (a heap over-read - dump(2000) read 2000 bytes out of a 1024-byte string), and the grown part was filled with a space instead of the configured indent_char. next_indent() keeps that PR's assertion against the unsigned indentation accumulation wrapping on deep nesting. put_chars() keeps the two cases that are genuinely a pointer and a count: the run-length copies out of the string being escaped, and to_chars() output. Tests cover an indent_step wider than the write buffer, a non-space indentation character past the old growth point, and nesting whose accumulated indentation spans several buffer-fulls. All three fail against develop. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/output/serializer.hpp | 189 ++++++++++++------ single_include/nlohmann/json.hpp | 189 ++++++++++++------ tests/src/unit-serialization.cpp | 48 +++++ 3 files changed, 296 insertions(+), 130 deletions(-) diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 948ae420a..7cdf85b6c 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -9,14 +9,14 @@ #pragma once -#include // reverse, remove, fill, find, none_of +#include // reverse, remove, fill, find, none_of, min #include // array #include // localeconv, lconv #include // labs, isfinite, isnan, signbit #include // size_t, ptrdiff_t #include // uint8_t #include // snprintf -#include // memcpy +#include // memcpy, memset #include // numeric_limits #include // string, char_traits #include // setfill, setw @@ -73,7 +73,6 @@ class serializer , thousands_sep(loc->thousands_sep == nullptr ? '\0' : std::char_traits::to_char_type(* (loc->thousands_sep))) , decimal_point(loc->decimal_point == nullptr ? '\0' : std::char_traits::to_char_type(* (loc->decimal_point))) , indent_char(ichar) - , indent_string(512, indent_char) , error_handler(error_handler_) {} @@ -137,44 +136,40 @@ class serializer { if (val.m_data.m_value.object->empty()) { - put_chars("{}", 2); + put_literal("{}"); return; } if (pretty_print) { - put_chars("{\n", 2); + put_literal("{\n"); // variable to hold indentation for recursive calls - const auto new_indent = current_indent + indent_step; - if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) - { - indent_string.resize(indent_string.size() * 2, ' '); - } + const auto new_indent = next_indent(current_indent, indent_step); // first n-1 elements auto i = val.m_data.m_value.object->cbegin(); for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) { - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); put_char('\"'); dump_escaped(i->first, ensure_ascii); - put_chars("\": ", 3); + put_literal("\": "); dump_internal(i->second, true, ensure_ascii, indent_step, new_indent); - put_chars(",\n", 2); + put_literal(",\n"); } // last element JSON_ASSERT(i != val.m_data.m_value.object->cend()); JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); put_char('\"'); dump_escaped(i->first, ensure_ascii); - put_chars("\": ", 3); + put_literal("\": "); dump_internal(i->second, true, ensure_ascii, indent_step, new_indent); put_char('\n'); - put_chars(indent_string.c_str(), current_indent); + put_indent(current_indent); put_char('}'); } else @@ -187,7 +182,7 @@ class serializer { put_char('\"'); dump_escaped(i->first, ensure_ascii); - put_chars("\":", 2); + put_literal("\":"); dump_internal(i->second, false, ensure_ascii, indent_step, current_indent); put_char(','); } @@ -197,7 +192,7 @@ class serializer JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); put_char('\"'); dump_escaped(i->first, ensure_ascii); - put_chars("\":", 2); + put_literal("\":"); dump_internal(i->second, false, ensure_ascii, indent_step, current_indent); put_char('}'); @@ -210,37 +205,33 @@ class serializer { if (val.m_data.m_value.array->empty()) { - put_chars("[]", 2); + put_literal("[]"); return; } if (pretty_print) { - put_chars("[\n", 2); + put_literal("[\n"); // variable to hold indentation for recursive calls - const auto new_indent = current_indent + indent_step; - if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) - { - indent_string.resize(indent_string.size() * 2, ' '); - } + const auto new_indent = next_indent(current_indent, indent_step); // first n-1 elements for (auto i = val.m_data.m_value.array->cbegin(); i != val.m_data.m_value.array->cend() - 1; ++i) { - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); dump_internal(*i, true, ensure_ascii, indent_step, new_indent); - put_chars(",\n", 2); + put_literal(",\n"); } // last element JSON_ASSERT(!val.m_data.m_value.array->empty()); - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); dump_internal(val.m_data.m_value.array->back(), true, ensure_ascii, indent_step, new_indent); put_char('\n'); - put_chars(indent_string.c_str(), current_indent); + put_indent(current_indent); put_char(']'); } else @@ -277,18 +268,14 @@ class serializer { if (pretty_print) { - put_chars("{\n", 2); + put_literal("{\n"); // variable to hold indentation for recursive calls - const auto new_indent = current_indent + indent_step; - if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) - { - indent_string.resize(indent_string.size() * 2, ' '); - } + const auto new_indent = next_indent(current_indent, indent_step); - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); - put_chars("\"bytes\": [", 10); + put_literal("\"bytes\": ["); if (!val.m_data.m_value.binary->empty()) { @@ -296,30 +283,30 @@ class serializer i != val.m_data.m_value.binary->cend() - 1; ++i) { dump_integer(*i); - put_chars(", ", 2); + put_literal(", "); } dump_integer(val.m_data.m_value.binary->back()); } - put_chars("],\n", 3); - put_chars(indent_string.c_str(), new_indent); + put_literal("],\n"); + put_indent(new_indent); - put_chars("\"subtype\": ", 11); + put_literal("\"subtype\": "); if (val.m_data.m_value.binary->has_subtype()) { dump_integer(val.m_data.m_value.binary->subtype()); } else { - put_chars("null", 4); + put_literal("null"); } put_char('\n'); - put_chars(indent_string.c_str(), current_indent); + put_indent(current_indent); put_char('}'); } else { - put_chars("{\"bytes\":[", 10); + put_literal("{\"bytes\":["); if (!val.m_data.m_value.binary->empty()) { @@ -332,7 +319,7 @@ class serializer dump_integer(val.m_data.m_value.binary->back()); } - put_chars("],\"subtype\":", 12); + put_literal("],\"subtype\":"); if (val.m_data.m_value.binary->has_subtype()) { dump_integer(val.m_data.m_value.binary->subtype()); @@ -340,7 +327,7 @@ class serializer } else { - put_chars("null}", 5); + put_literal("null}"); } } return; @@ -350,11 +337,11 @@ class serializer { if (val.m_data.m_value.boolean) { - put_chars("true", 4); + put_literal("true"); } else { - put_chars("false", 5); + put_literal("false"); } return; } @@ -379,13 +366,13 @@ class serializer case value_t::discarded: { - put_chars("", 11); + put_literal(""); return; } case value_t::null: { - put_chars("null", 4); + put_literal("null"); return; } @@ -394,6 +381,19 @@ class serializer } } + /*! + @brief the indentation level to use for the children of the current value + + A very large @a indent_step can wrap the unsigned accumulation on deep + nesting, which would silently truncate the indentation. + */ + static unsigned int next_indent(const unsigned int current_indent, const unsigned int indent_step) + { + const unsigned int new_indent = current_indent + indent_step; + JSON_ASSERT(new_indent >= current_indent); + return new_indent; + } + JSON_PRIVATE_UNLESS_TESTED: /*! @brief dump escaped string @@ -446,7 +446,7 @@ class serializer // preserve output order, then write the run directly if (bytes != 0) { - put_chars(string_buffer.data(), bytes); + put_buffer(string_buffer, bytes); bytes = 0; } put_chars(s.data() + i, run); @@ -548,7 +548,7 @@ class serializer // written ("\uxxxx\uxxxx\0") for one code point if (string_buffer.size() - bytes < 13) { - put_chars(string_buffer.data(), bytes); + put_buffer(string_buffer, bytes); bytes = 0; } @@ -607,7 +607,7 @@ class serializer // written ("\uxxxx\uxxxx\0") for one code point if (string_buffer.size() - bytes < 13) { - put_chars(string_buffer.data(), bytes); + put_buffer(string_buffer, bytes); bytes = 0; } @@ -646,7 +646,7 @@ class serializer // write buffer if (bytes > 0) { - put_chars(string_buffer.data(), bytes); + put_buffer(string_buffer, bytes); } } else @@ -662,22 +662,22 @@ class serializer case error_handler_t::ignore: { // write all accepted bytes - put_chars(string_buffer.data(), bytes_after_last_accept); + put_buffer(string_buffer, bytes_after_last_accept); break; } case error_handler_t::replace: { // write all accepted bytes - put_chars(string_buffer.data(), bytes_after_last_accept); + put_buffer(string_buffer, bytes_after_last_accept); // add a replacement character if (ensure_ascii) { - put_chars("\\ufffd", 6); + put_literal("\\ufffd"); } else { - put_chars("\xEF\xBF\xBD", 3); + put_literal("\xEF\xBF\xBD"); } break; } @@ -713,6 +713,66 @@ class serializer adapter (after flushing what is pending), so large string/number payloads are not copied an extra time. */ + /*! + @brief append @a indent indentation characters to the write buffer + + Writes the indentation straight into the buffer instead of copying it out of + a pre-grown indentation string, so no auxiliary string has to be sized, + resized, or kept in sync with the deepest nesting level reached. An + indentation wider than the buffer simply fills and flushes it repeatedly. + */ + void put_indent(unsigned int indent) + { + while (indent > 0) + { + if (JSON_HEDLEY_UNLIKELY(write_buffer_pos == write_buffer.size())) + { + flush(); + } + + const std::size_t chunk = (std::min)(static_cast(indent), + write_buffer.size() - write_buffer_pos); + std::memset(write_buffer.data() + write_buffer_pos, indent_char, chunk); + write_buffer_pos += chunk; + indent -= static_cast(chunk); + } + } + + /*! + @brief append a string literal to the write buffer + + The length comes from the array bound rather than a hand-written count, so + it cannot drift out of sync with the literal. A literal always fits into the + buffer (checked at compile time), so unlike @ref put_chars this needs no + write-through path for oversized runs. + */ + template + void put_literal(const char (&s)[N]) + { + static_assert(N >= 2, "put_literal expects a non-empty string literal"); + static_assert(N - 1 < write_buffer_size, "string literal must fit into the write buffer"); + + if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + (N - 1) > write_buffer.size())) + { + flush(); + } + std::memcpy(write_buffer.data() + write_buffer_pos, s, N - 1); + write_buffer_pos += N - 1; + } + + /*! + @brief append the first @a length characters of a fixed-size buffer + + Same as @ref put_chars, but the buffer carries its own bound, so the length + can be checked against it - which a bare pointer plus count cannot do. + */ + template + void put_buffer(const std::array& buffer, std::size_t length) + { + JSON_ASSERT(length <= N); + put_chars(buffer.data(), length); + } + JSON_HEDLEY_NON_NULL(2) void put_chars(const char* s, std::size_t length) { @@ -925,7 +985,7 @@ class serializer *(--buffer_ptr) = static_cast('0' + abs_value); } - put_chars(number_buffer.data(), n_chars); + put_buffer(number_buffer, n_chars); } /*! @@ -941,7 +1001,7 @@ class serializer // NaN / inf if (!std::isfinite(x)) { - put_chars("null", 4); + put_literal("null"); return; } @@ -1013,7 +1073,7 @@ class serializer } } - put_chars(number_buffer.data(), static_cast(len)); + put_buffer(number_buffer, static_cast(len)); // determine if we need to append ".0" const bool value_is_int_like = @@ -1025,7 +1085,7 @@ class serializer if (value_is_int_like) { - put_chars(".0", 2); + put_literal(".0"); } } @@ -1130,15 +1190,14 @@ class serializer /// the indentation character const char indent_char; - /// the indentation string - string_t indent_string; /// error_handler how to react on decoding errors const error_handler_t error_handler; /// buffer collecting output before it is flushed to the output adapter, so /// that the many small structural writes become few bulk writes - std::array write_buffer{{}}; + static constexpr std::size_t write_buffer_size = 1024; + std::array write_buffer{{}}; /// number of valid bytes currently held in @ref write_buffer std::size_t write_buffer_pos = 0; }; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 2b2ca8a4d..4f11e2b98 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19762,14 +19762,14 @@ NLOHMANN_JSON_NAMESPACE_END -#include // reverse, remove, fill, find, none_of +#include // reverse, remove, fill, find, none_of, min #include // array #include // localeconv, lconv #include // labs, isfinite, isnan, signbit #include // size_t, ptrdiff_t #include // uint8_t #include // snprintf -#include // memcpy +#include // memcpy, memset #include // numeric_limits #include // string, char_traits #include // setfill, setw @@ -20954,7 +20954,6 @@ class serializer , thousands_sep(loc->thousands_sep == nullptr ? '\0' : std::char_traits::to_char_type(* (loc->thousands_sep))) , decimal_point(loc->decimal_point == nullptr ? '\0' : std::char_traits::to_char_type(* (loc->decimal_point))) , indent_char(ichar) - , indent_string(512, indent_char) , error_handler(error_handler_) {} @@ -21018,44 +21017,40 @@ class serializer { if (val.m_data.m_value.object->empty()) { - put_chars("{}", 2); + put_literal("{}"); return; } if (pretty_print) { - put_chars("{\n", 2); + put_literal("{\n"); // variable to hold indentation for recursive calls - const auto new_indent = current_indent + indent_step; - if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) - { - indent_string.resize(indent_string.size() * 2, ' '); - } + const auto new_indent = next_indent(current_indent, indent_step); // first n-1 elements auto i = val.m_data.m_value.object->cbegin(); for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) { - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); put_char('\"'); dump_escaped(i->first, ensure_ascii); - put_chars("\": ", 3); + put_literal("\": "); dump_internal(i->second, true, ensure_ascii, indent_step, new_indent); - put_chars(",\n", 2); + put_literal(",\n"); } // last element JSON_ASSERT(i != val.m_data.m_value.object->cend()); JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); put_char('\"'); dump_escaped(i->first, ensure_ascii); - put_chars("\": ", 3); + put_literal("\": "); dump_internal(i->second, true, ensure_ascii, indent_step, new_indent); put_char('\n'); - put_chars(indent_string.c_str(), current_indent); + put_indent(current_indent); put_char('}'); } else @@ -21068,7 +21063,7 @@ class serializer { put_char('\"'); dump_escaped(i->first, ensure_ascii); - put_chars("\":", 2); + put_literal("\":"); dump_internal(i->second, false, ensure_ascii, indent_step, current_indent); put_char(','); } @@ -21078,7 +21073,7 @@ class serializer JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); put_char('\"'); dump_escaped(i->first, ensure_ascii); - put_chars("\":", 2); + put_literal("\":"); dump_internal(i->second, false, ensure_ascii, indent_step, current_indent); put_char('}'); @@ -21091,37 +21086,33 @@ class serializer { if (val.m_data.m_value.array->empty()) { - put_chars("[]", 2); + put_literal("[]"); return; } if (pretty_print) { - put_chars("[\n", 2); + put_literal("[\n"); // variable to hold indentation for recursive calls - const auto new_indent = current_indent + indent_step; - if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) - { - indent_string.resize(indent_string.size() * 2, ' '); - } + const auto new_indent = next_indent(current_indent, indent_step); // first n-1 elements for (auto i = val.m_data.m_value.array->cbegin(); i != val.m_data.m_value.array->cend() - 1; ++i) { - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); dump_internal(*i, true, ensure_ascii, indent_step, new_indent); - put_chars(",\n", 2); + put_literal(",\n"); } // last element JSON_ASSERT(!val.m_data.m_value.array->empty()); - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); dump_internal(val.m_data.m_value.array->back(), true, ensure_ascii, indent_step, new_indent); put_char('\n'); - put_chars(indent_string.c_str(), current_indent); + put_indent(current_indent); put_char(']'); } else @@ -21158,18 +21149,14 @@ class serializer { if (pretty_print) { - put_chars("{\n", 2); + put_literal("{\n"); // variable to hold indentation for recursive calls - const auto new_indent = current_indent + indent_step; - if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) - { - indent_string.resize(indent_string.size() * 2, ' '); - } + const auto new_indent = next_indent(current_indent, indent_step); - put_chars(indent_string.c_str(), new_indent); + put_indent(new_indent); - put_chars("\"bytes\": [", 10); + put_literal("\"bytes\": ["); if (!val.m_data.m_value.binary->empty()) { @@ -21177,30 +21164,30 @@ class serializer i != val.m_data.m_value.binary->cend() - 1; ++i) { dump_integer(*i); - put_chars(", ", 2); + put_literal(", "); } dump_integer(val.m_data.m_value.binary->back()); } - put_chars("],\n", 3); - put_chars(indent_string.c_str(), new_indent); + put_literal("],\n"); + put_indent(new_indent); - put_chars("\"subtype\": ", 11); + put_literal("\"subtype\": "); if (val.m_data.m_value.binary->has_subtype()) { dump_integer(val.m_data.m_value.binary->subtype()); } else { - put_chars("null", 4); + put_literal("null"); } put_char('\n'); - put_chars(indent_string.c_str(), current_indent); + put_indent(current_indent); put_char('}'); } else { - put_chars("{\"bytes\":[", 10); + put_literal("{\"bytes\":["); if (!val.m_data.m_value.binary->empty()) { @@ -21213,7 +21200,7 @@ class serializer dump_integer(val.m_data.m_value.binary->back()); } - put_chars("],\"subtype\":", 12); + put_literal("],\"subtype\":"); if (val.m_data.m_value.binary->has_subtype()) { dump_integer(val.m_data.m_value.binary->subtype()); @@ -21221,7 +21208,7 @@ class serializer } else { - put_chars("null}", 5); + put_literal("null}"); } } return; @@ -21231,11 +21218,11 @@ class serializer { if (val.m_data.m_value.boolean) { - put_chars("true", 4); + put_literal("true"); } else { - put_chars("false", 5); + put_literal("false"); } return; } @@ -21260,13 +21247,13 @@ class serializer case value_t::discarded: { - put_chars("", 11); + put_literal(""); return; } case value_t::null: { - put_chars("null", 4); + put_literal("null"); return; } @@ -21275,6 +21262,19 @@ class serializer } } + /*! + @brief the indentation level to use for the children of the current value + + A very large @a indent_step can wrap the unsigned accumulation on deep + nesting, which would silently truncate the indentation. + */ + static unsigned int next_indent(const unsigned int current_indent, const unsigned int indent_step) + { + const unsigned int new_indent = current_indent + indent_step; + JSON_ASSERT(new_indent >= current_indent); + return new_indent; + } + JSON_PRIVATE_UNLESS_TESTED: /*! @brief dump escaped string @@ -21327,7 +21327,7 @@ class serializer // preserve output order, then write the run directly if (bytes != 0) { - put_chars(string_buffer.data(), bytes); + put_buffer(string_buffer, bytes); bytes = 0; } put_chars(s.data() + i, run); @@ -21429,7 +21429,7 @@ class serializer // written ("\uxxxx\uxxxx\0") for one code point if (string_buffer.size() - bytes < 13) { - put_chars(string_buffer.data(), bytes); + put_buffer(string_buffer, bytes); bytes = 0; } @@ -21488,7 +21488,7 @@ class serializer // written ("\uxxxx\uxxxx\0") for one code point if (string_buffer.size() - bytes < 13) { - put_chars(string_buffer.data(), bytes); + put_buffer(string_buffer, bytes); bytes = 0; } @@ -21527,7 +21527,7 @@ class serializer // write buffer if (bytes > 0) { - put_chars(string_buffer.data(), bytes); + put_buffer(string_buffer, bytes); } } else @@ -21543,22 +21543,22 @@ class serializer case error_handler_t::ignore: { // write all accepted bytes - put_chars(string_buffer.data(), bytes_after_last_accept); + put_buffer(string_buffer, bytes_after_last_accept); break; } case error_handler_t::replace: { // write all accepted bytes - put_chars(string_buffer.data(), bytes_after_last_accept); + put_buffer(string_buffer, bytes_after_last_accept); // add a replacement character if (ensure_ascii) { - put_chars("\\ufffd", 6); + put_literal("\\ufffd"); } else { - put_chars("\xEF\xBF\xBD", 3); + put_literal("\xEF\xBF\xBD"); } break; } @@ -21594,6 +21594,66 @@ class serializer adapter (after flushing what is pending), so large string/number payloads are not copied an extra time. */ + /*! + @brief append @a indent indentation characters to the write buffer + + Writes the indentation straight into the buffer instead of copying it out of + a pre-grown indentation string, so no auxiliary string has to be sized, + resized, or kept in sync with the deepest nesting level reached. An + indentation wider than the buffer simply fills and flushes it repeatedly. + */ + void put_indent(unsigned int indent) + { + while (indent > 0) + { + if (JSON_HEDLEY_UNLIKELY(write_buffer_pos == write_buffer.size())) + { + flush(); + } + + const std::size_t chunk = (std::min)(static_cast(indent), + write_buffer.size() - write_buffer_pos); + std::memset(write_buffer.data() + write_buffer_pos, indent_char, chunk); + write_buffer_pos += chunk; + indent -= static_cast(chunk); + } + } + + /*! + @brief append a string literal to the write buffer + + The length comes from the array bound rather than a hand-written count, so + it cannot drift out of sync with the literal. A literal always fits into the + buffer (checked at compile time), so unlike @ref put_chars this needs no + write-through path for oversized runs. + */ + template + void put_literal(const char (&s)[N]) + { + static_assert(N >= 2, "put_literal expects a non-empty string literal"); + static_assert(N - 1 < write_buffer_size, "string literal must fit into the write buffer"); + + if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + (N - 1) > write_buffer.size())) + { + flush(); + } + std::memcpy(write_buffer.data() + write_buffer_pos, s, N - 1); + write_buffer_pos += N - 1; + } + + /*! + @brief append the first @a length characters of a fixed-size buffer + + Same as @ref put_chars, but the buffer carries its own bound, so the length + can be checked against it - which a bare pointer plus count cannot do. + */ + template + void put_buffer(const std::array& buffer, std::size_t length) + { + JSON_ASSERT(length <= N); + put_chars(buffer.data(), length); + } + JSON_HEDLEY_NON_NULL(2) void put_chars(const char* s, std::size_t length) { @@ -21806,7 +21866,7 @@ class serializer *(--buffer_ptr) = static_cast('0' + abs_value); } - put_chars(number_buffer.data(), n_chars); + put_buffer(number_buffer, n_chars); } /*! @@ -21822,7 +21882,7 @@ class serializer // NaN / inf if (!std::isfinite(x)) { - put_chars("null", 4); + put_literal("null"); return; } @@ -21894,7 +21954,7 @@ class serializer } } - put_chars(number_buffer.data(), static_cast(len)); + put_buffer(number_buffer, static_cast(len)); // determine if we need to append ".0" const bool value_is_int_like = @@ -21906,7 +21966,7 @@ class serializer if (value_is_int_like) { - put_chars(".0", 2); + put_literal(".0"); } } @@ -22011,15 +22071,14 @@ class serializer /// the indentation character const char indent_char; - /// the indentation string - string_t indent_string; /// error_handler how to react on decoding errors const error_handler_t error_handler; /// buffer collecting output before it is flushed to the output adapter, so /// that the many small structural writes become few bulk writes - std::array write_buffer{{}}; + static constexpr std::size_t write_buffer_size = 1024; + std::array write_buffer{{}}; /// number of valid bytes currently held in @ref write_buffer std::size_t write_buffer_pos = 0; }; diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index 1d34e8002..504044278 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -470,3 +470,51 @@ TEST_CASE("serialization of strings (bulk fast path)") CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"validmore\""); } } + +TEST_CASE("indentation is written straight into the write buffer") +{ + // put_indent() memsets the indentation into the write buffer instead of + // copying it out of a pre-grown indentation string. These cases cover an + // indentation wider than the buffer, a non-space indentation character, and + // nesting deep enough that the accumulated indentation spans several + // buffer-fulls - the situations the old grow-a-string approach got wrong. + + SECTION("indent_step wider than the write buffer") + { + const json j = {{"a", 1}}; + // 2000 > the 1024-byte write buffer, and > the 512 the indentation + // string used to start at + CHECK(j.dump(2000) == "{\n" + std::string(2000, ' ') + "\"a\": 1\n}"); + } + + SECTION("a non-space indentation character is used throughout") + { + const json j = {{"a", 1}}; + // 600 is past the point where the indentation used to be grown, which + // is where a hard-coded space would have shown up + CHECK(j.dump(600, '\t') == "{\n" + std::string(600, '\t') + "\"a\": 1\n}"); + CHECK(j.dump(3, '.') == "{\n...\"a\": 1\n}"); + } + + SECTION("accumulated indentation spans several buffer-fulls") + { + // five levels deep at 400 per level: the innermost value is indented by + // 2000 characters, reached in steps that each straddle the buffer end + json j = json::array({1}); + for (int i = 0; i < 4; ++i) + { + j = json::array({j}); + } + + const std::string out = j.dump(400); + CHECK(out.find(std::string("\n") + std::string(2000, ' ') + "1\n") != std::string::npos); + CHECK(json::parse(out) == j); + } + + SECTION("indentation is unchanged for ordinary widths") + { + const json j = {{"a", {1, 2}}, {"b", nullptr}}; + CHECK(j.dump(2) == "{\n \"a\": [\n 1,\n 2\n ],\n \"b\": null\n}"); + CHECK(j.dump(0) == "{\n\"a\": [\n1,\n2\n],\n\"b\": null\n}"); + } +}