From 1e9222f9554365f3bcbda749d752e88b9e116728 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:37:42 +0200 Subject: [PATCH 1/4] Bump dompurify from 3.4.16 to 3.4.17 in /docs/mkdocs/scripts/mermaid (#5904) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.16 to 3.4.17. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.16...3.4.17) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.17 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/mkdocs/scripts/mermaid/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/mkdocs/scripts/mermaid/package-lock.json b/docs/mkdocs/scripts/mermaid/package-lock.json index c114f9832..14bbc249e 100644 --- a/docs/mkdocs/scripts/mermaid/package-lock.json +++ b/docs/mkdocs/scripts/mermaid/package-lock.json @@ -1105,9 +1105,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", - "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" From e9c4fe3cdffb011bec83b323e89ea3b2b505ef94 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 24 Oct 2026 07:38:17 +0200 Subject: [PATCH 2/4] json_view: indented and ensure_ascii dumps through the fast writer (#5889) * Write the indented output of json_view's dump() with the fast writer The compact writer of dump() (cursor in a register, fixed-size copies of tokens) served dump(-1) only; every dump(indent), and everything with ensure_ascii that was indented, went through a generic walk that calls put() for each token and put_repeated() (a memset call) for each line break. The fast writer now writes indented output, too, with or without ensure_ascii, and the generic walk is gone. A line break is a copy of a 64-byte ruler (a line break and 63 indentation characters) when the line is at most 64 bytes, and a store plus memset beyond that. The depth is counted only in the indented instantiations (counting it in the compact one cost an editable document 5 % with Clang). The indented walks are out of line (dump_pretty()): inlined into dump() next to the compact ones, they made compact dumps of integer arrays 7 % slower. A decoded string (one with escape sequences) of a damaged image loaded with image_check::bounds can contain invalid UTF-8. As before, the compact dump writes such bytes unchanged, and the indented dump throws type_error.316; its runs are found by the lexer's validating scan (a scalar validation of every sequence made dump(2) of decoded CJK text twice as slow as before). dump(2) instructions per call, macOS arm64 (before -> after): twitter citm canada jeopardy(4k) json 3.75M -> 1.85M 8.28M -> 3.93M 67.1M -> 56.0M 9.56M -> 4.81M editable 3.82M -> 1.84M 8.48M -> 4.02M 68.1M -> 56.2M 9.66M -> 4.75M Linux instructions (GCC 13 / Clang 18), dump(2) and dump(2, ' ', true): twitter -25 % / -30 % -11 % / -11 % citm -29 % / -28 % -24 % / -24 % jeopardy(4k) -25 % / -32 % -20 % / -24 % CJK text with \u escapes -6 % / -11 % -2 % / -2 % The compact dump is unchanged (within +0.4 % on Linux, within noise on the Mac). Signed-off-by: Niels Lohmann * Scan the strings of json_view's dump() with ensure_ascii 16 bytes at a time With ensure_ascii, a string of the source that has a byte >= 0x7F was written by a generic path: put() per piece and a validate-and-decode step for every character beyond ASCII. It now goes through a writer with the cursor in a local variable. The part of the string that has nothing to escape is found 16 bytes at a time (SSE2 or NEON; the existing 8-byte scan without vector support) and copied; strings that are ASCII throughout are still decided by the 8-byte check of dump() with ensure_ascii (a 16-byte scan for them was 2-5 % slower for short strings). The two-byte sequences (lead C2..DF) and the three-byte sequences whose continuation bytes may have any value (lead E1..EF except ED) are decoded with a few compares; E0, ED, four-byte sequences, and damaged input go through validate_one_utf8() as before, so damaged images still throw type_error.316. dump(-1, ' ', true) instructions per call, macOS arm64 (develop -> now); the compact dump without ensure_ascii and dumps of ASCII text are unchanged: twitter 5.42M -> 3.57M citm 3.28M -> 3.24M jeopardy(4k) 5.78M -> 5.65M CJK text, raw 66.5M -> 35.3M escaped \u 32.7M -> 17.0M Latin text with accents 11.5M -> 9.4M ASCII text 4.40M -> 4.40M Linux instructions (GCC 13 / Clang 18), dump(-1, ' ', true): twitter -9 % / -33 % CJK text -14 % / -47 % Latin text -8 % / -18 % citm -0.2 % / -0.4 % ASCII text -0.1 % / -0.3 % The compact dump without ensure_ascii is unchanged (canada 0.0 %). The unit test covers every character of the Basic Multilingual Plane, and a character that needs an escape at each position of strings of 0..40 bytes. Signed-off-by: Niels Lohmann * Find the end of the plain run of a decoded string 16 bytes at a time with SSE2 The strings of a document that had escape sequences are written in runs without a quote, backslash, or control character; the compact dump found the end of a run 8 bytes at a time with a SWAR test. With SSE2 (x86-64) it now compares 16 bytes per step. NEON keeps the SWAR code: there the mask of a step costs more than it saves on the short runs that are typical here. Instructions of dump(), Linux (GCC 13 / Clang 18): twitter.json 1.99M -> 1.88M (-5.2 %) / 1.97M -> 1.92M (-3.0 %) CJK text with \u escapes 5.98M -> 4.52M (-24.5 %) / 5.30M -> 4.25M (-19.9 %) ASCII text with a few escapes 4.28M -> 4.09M (-4.4 %) / 4.24M -> 4.11M (-3.2 %) jeopardy (4,000 questions) 5.08M -> 4.78M (-5.8 %) / 4.97M -> 4.80M (-3.4 %) Signed-off-by: Niels Lohmann * Rename depth_counter's open() and close() to enter() and leave() Flawfinder took them for the file functions open() and close(). Signed-off-by: Niels Lohmann * Fix the clang-tidy findings in json_view's dump and its tests - the SSE2 loop of the string scan is within NOLINTBEGIN/END (portability-simd-intrinsics), as in to_chars.hpp - depth_counter's leave() and get() keep the interface of depth_counter instead of being static - write_ascii() compares the room left as a size_t variable - the test's texts are a std::array, and a JSON fragment with escapes is a raw string literal Signed-off-by: Niels Lohmann * Move dump_ascii() and dump_pretty() next to dump() Address a review comment: the two dispatchers that dump() calls now follow it directly. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/serializer.hpp | 629 +++++++++++-------- single_include/nlohmann/json_view.hpp | 630 ++++++++++++-------- tests/src/unit-json_view_dump.cpp | 127 ++++ tests/src/unit-json_view_image.cpp | 20 + 4 files changed, 894 insertions(+), 512 deletions(-) diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp index 7d393d0f6..dbaca6171 100644 --- a/include/nlohmann/detail/view/serializer.hpp +++ b/include/nlohmann/detail/view/serializer.hpp @@ -23,6 +23,7 @@ #include #include #include +#include NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -80,13 +81,6 @@ class output_buffer put(s, N - 1); } - void put_repeated(char c, std::size_t n) - { - reserve(n); - std::memset(m_pos, c, n); - m_pos += n; - } - /// the write position and the end of the writable space, for a writer /// that keeps the position in a local variable (set_cursor() hands it back) char* cursor() const noexcept @@ -143,9 +137,27 @@ class output_buffer /// image_check::bounds can have others, which are then written unchanged). inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept { + std::size_t i = 0; +#if NLOHMANN_VIEW_SSE2 + // (16 bytes per step; with NEON, the mask of a step costs more than it saves on the + // short runs that are typical here) + // NOLINTBEGIN(portability-simd-intrinsics) + for (; i + 16 <= n; i += 16) + { + const __m128i in = _mm_loadu_si128(static_cast(static_cast(s + i))); // NOLINT(bugprone-casting-through-void) + // (a byte is below 0x20 if it is its own minimum with 0x1F) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_min_epu8(in, _mm_set1_epi8(0x1F)), in)); + const auto mask = static_cast(static_cast(_mm_movemask_epi8(special))); + if (mask != 0) + { + return i + static_cast(count_trailing_zeros(mask)); + } + } + // NOLINTEND(portability-simd-intrinsics) +#endif constexpr std::uint64_t ones = 0x0101010101010101ull; constexpr std::uint64_t high = 0x8080808080808080ull; - std::size_t i = 0; for (; i + 8 <= n; i += 8) { const std::uint64_t v = read_eight_bytes(s + i); @@ -196,6 +208,70 @@ inline bool is_ascii_without_delete(const char* s, std::size_t n) noexcept return true; } +#if NLOHMANN_VIEW_VECTOR +/// the bytes of the 16 at p that ensure_ascii cannot copy (a quote, a +/// backslash, a control character, DEL, or not ASCII; a signed compare with +/// 0x20 finds control characters and non-ASCII bytes at once), one bit per +/// byte (x86) or one nibble per byte (NEON) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t ascii_stop_mask(const unsigned char* p) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vorrq_u8(vceqq_u8(in, vdupq_n_u8(0x7F)), vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)))); + return vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F)), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20)))); + return static_cast(static_cast(_mm_movemask_epi8(special))); +#endif +} + +/// the width in bits of a byte in the mask of ascii_stop_mask() +constexpr unsigned ascii_stop_bits() noexcept +{ + return NLOHMANN_VIEW_NEON ? 4u : 1u; +} +#endif + +/// As find_ascii_copyable_run(), 16 bytes per step. end is the end of the +/// buffer that holds the n bytes at p (a short run is read as 16 bytes if +/// they are in the buffer). +inline std::size_t ascii_copyable_run(const unsigned char* p, std::size_t n, const unsigned char* end) noexcept +{ +#if NLOHMANN_VIEW_VECTOR + if (n >= 16) + { + std::size_t i = 0; + for (; i + 16 <= n; i += 16) + { + const std::uint64_t mask = ascii_stop_mask(p + i); + if (mask != 0) + { + return i + (static_cast(count_trailing_zeros(mask)) / ascii_stop_bits()); + } + } + if (i == n) + { + return n; + } + // the last 16 bytes: those before i are known to be copyable + const std::uint64_t mask = ascii_stop_mask(p + n - 16); + return mask != 0 ? n - 16 + (static_cast(count_trailing_zeros(mask)) / ascii_stop_bits()) : n; + } + if (end - p >= 16) + { + // (the bytes after the run are masked out) + const std::uint64_t mask = ascii_stop_mask(p) & ((std::uint64_t{1} << (n * ascii_stop_bits())) - 1u); + return mask != 0 ? static_cast(count_trailing_zeros(mask)) / ascii_stop_bits() : n; + } +#else + static_cast(end); +#endif + return find_ascii_copyable_run(p, n); +} + /// A stack that starts in a buffer of the caller (a local array) and moves to /// the heap (a vector of the caller) only when that is full, so that dumps of /// shallow documents need no allocation. The top is a pointer, as in @@ -275,6 +351,39 @@ NLOHMANN_VIEW_ALWAYS_INLINE char* put_literal(char* w, const char (&s)[N]) noexc return w + N - 1; } +/// the depth of the innermost open container, counted only for indented output +template +struct depth_counter +{ + std::size_t value = 0; + void enter() noexcept + { + ++value; + } + std::size_t leave() noexcept + { + return --value; + } + std::size_t get() const noexcept + { + return value; + } +}; + +template<> +struct depth_counter +{ + void enter() noexcept {} + std::size_t leave() noexcept // NOLINT(readability-convert-member-functions-to-static): same interface as depth_counter + { + return 0; + } + std::size_t get() const noexcept // NOLINT(readability-convert-member-functions-to-static): same interface as depth_counter + { + return 0; + } +}; + /// how the view's dump() writes a value struct dump_style { @@ -311,139 +420,95 @@ class view_serializer void dump(const node* root) { - if (!m_style.pretty) + if (m_style.ensure_ascii) { - if (m_style.ensure_ascii) - { - dump_compact_ascii(root); - } - else if (m_style.source_numbers) - { - dump_compact(root); - } - else - { - dump_compact(root); - } - return; + dump_ascii(root); } - struct frame + else if (m_style.pretty) { - const node* pos; ///< next element, or key of the next member - const node* end; - bool object; - bool first; ///< nothing written yet - }; - std::array buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read - std::vector heap; - small_stack stack(buffer.data(), buffer.size(), heap); - const node* n = root; - for (;;) + dump_pretty(root); + } + else if (m_style.source_numbers) { - // write the value at n - if (is_container(*n)) - { - const bool object = n->kind == static_cast(value_t::object); - if (n->len == 0) - { - m_out.put(object ? "{}" : "[]", 2); - } - else - { - m_out.put(object ? '{' : '['); - stack.push_back(frame{nav::first(m_doc, n), nav::end(m_doc, n), object, true}); - } - } - else - { - write_scalar(*n); - } - - // go to the next value: close finished containers, then separate - for (;;) - { - if (stack.empty()) - { - m_out.finish(); - return; - } - if (stack.back().pos == stack.back().end) - { - const bool object = stack.back().object; - stack.pop_back(); - newline(stack.size()); - m_out.put(object ? '}' : ']'); - continue; - } - frame& f = stack.back(); - if (!f.first) - { - m_out.put(','); - } - f.first = false; - newline(stack.size()); - if (f.object) - { - write_string(*f.pos); - if (m_style.pretty) - { - m_out.put(": "); - } - else - { - m_out.put(':'); - } - n = nav::value(f.pos + 1); - f.pos = document_data::after(f.pos + 1); - } - else - { - n = nav::value(f.pos); - f.pos = document_data::after(f.pos); - } - break; - } + dump_text(root); + } + else + { + dump_text(root); } } private: + /// dump_text() with ensure_ascii, out of line: the walk without it is + /// faster when it is not merged with this one into dump() + NLOHMANN_VIEW_NOINLINE void dump_ascii(const node* root) + { + if (m_style.pretty) + { + if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + else if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + + /// dump_text() with indentation, out of line: the compact walk is faster + /// when it is not merged with the indented ones into dump() + NLOHMANN_VIEW_NOINLINE void dump_pretty(const node* root) + { + if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + /// a string that is not a plain string of the source (decoded, or written /// by an edit), without ensure_ascii: runs without characters to escape - /// are copied - NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n) + /// are copied. If validate is set, the string must be valid UTF-8: the + /// runs are found by the lexer's scan, which validates the sequences, and + /// invalid UTF-8 throws what basic_json::dump() throws. (The compact output + /// writes the bytes of a damaged image unchanged, and scans without + /// validating them.) + NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n, bool validate) { const auto* const s = reinterpret_cast(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) m_out.put('"'); for (std::size_t i = 0; i < n.len;) { - const std::size_t run = plain_output_run(s + i, n.len - i); + const std::size_t run = validate ? string_bulk_run(s + i, n.len - i) : plain_output_run(s + i, n.len - i); if (run != 0) { m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) i += run; continue; } + if (validate && s[i] >= 0x80) + { + invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8) + } write_codepoint(s[i], s + i, 1); // a quote, a backslash, or a control character ++i; } m_out.put('"'); } - /// dump_compact() with ensure_ascii, out of line: the walk without it is - /// faster when it is not merged with this one into dump() - NLOHMANN_VIEW_NOINLINE void dump_compact_ascii(const node* root) - { - if (m_style.source_numbers) - { - dump_compact(root); - } - else - { - dump_compact(root); - } - } - - /// whether dump_compact() copies a string or key from the source: one of + /// whether dump_text() copies a string or key from the source: one of /// the source, and with ensure_ascii one without bytes >= 0x7F (an overload /// each, so that the walk without ensure_ascii is compiled exactly as /// before: with GCC, a condition that only folds away later changed its @@ -458,13 +523,155 @@ class view_serializer return (n.flags & node_flags::storage) == 0 && is_ascii_without_delete(src + n.off, n.len); } - /// a string with ensure_ascii (dump_compact()), out of line - NLOHMANN_VIEW_NOINLINE void write_ascii_string(const node& n) + /*! + @brief a string with ensure_ascii: the characters that are not printable ASCII as \\uXXXX + (a character beyond U+FFFF as a surrogate pair); strings of a document + are valid UTF-8, except in a damaged image loaded with image_check::bounds, + for which this throws what basic_json::dump() throws for the string + */ + NLOHMANN_VIEW_NOINLINE void write_ascii(const node& n) { - write_string(n); + const auto* const s = reinterpret_cast(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const std::size_t size = n.len; + char* w = m_out.cursor(); + char* lim = m_out.limit(); + // room for k bytes + const auto room = [&](std::size_t k) + { + const auto left = static_cast(lim - w); + if (NLOHMANN_VIEW_UNLIKELY(left < k)) + { + m_out.set_cursor(w); + m_out.reserve(k); + w = m_out.cursor(); + lim = m_out.limit(); + } + }; + static constexpr const char* hex = "0123456789abcdef"; + // \\uXXXX + const auto u_escape = [&](std::uint32_t u) + { + w[0] = '\\'; + w[1] = 'u'; + w[2] = hex[(u >> 12u) & 0xFu]; + w[3] = hex[(u >> 8u) & 0xFu]; + w[4] = hex[(u >> 4u) & 0xFu]; + w[5] = hex[u & 0xFu]; + w += 6; + }; + room(1); + *w++ = '"'; + std::size_t i = 0; + // (a string of the source starts with a run that has nothing to escape, 16 bytes per step) + const std::size_t prefix = (n.flags & node_flags::storage) == 0 ? ascii_copyable_run(s, size, reinterpret_cast(m_doc.src + m_doc.size)) : 0; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + if (prefix != 0) + { + room(prefix); + std::memcpy(w, s, prefix); + w += prefix; + i = prefix; + } + while (i < size) + { + // (a character that needs an escape sequence is not worth a call) + const std::size_t run = is_ascii_copyable(s[i]) ? find_ascii_copyable_run(s + i, size - i) : 0; + if (run != 0) + { + room(run); + std::memcpy(w, s + i, run); + w += run; + i += run; + continue; + } + room(12); + if (s[i] >= 0x80) + { + const unsigned char lead = s[i]; + // (the two-byte sequences of Latin text and the three-byte sequences of most other + // text, with continuation bytes of any value: the ranges that are restricted (E0, + // ED, and the sequences of four bytes) go through validate_one_utf8()) + if (lead >= 0xE1 && lead <= 0xEF && lead != 0xED && size - i >= 3 && (s[i + 1] & 0xC0u) == 0x80 && (s[i + 2] & 0xC0u) == 0x80) + { + u_escape(((lead & 0x0Fu) << 12u) | ((s[i + 1] & 0x3Fu) << 6u) | (s[i + 2] & 0x3Fu)); + i += 3; + continue; + } + if (lead >= 0xC2 && lead <= 0xDF && size - i >= 2 && (s[i + 1] & 0xC0u) == 0x80) + { + u_escape(((lead & 0x1Fu) << 6u) | (s[i + 1] & 0x3Fu)); + i += 2; + continue; + } + const std::size_t len = validate_one_utf8(s + i, size - i); + if (NLOHMANN_VIEW_UNLIKELY(len == 0)) + { + m_out.set_cursor(w); + invalid_utf8(s, size); + return; // LCOV_EXCL_LINE (invalid_utf8 throws) + } + std::uint32_t codepoint = s[i] & (0xFFu >> (len + 1)); + for (std::size_t k = 1; k < len; ++k) + { + codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu); + } + i += len; + if (codepoint <= 0xFFFF) + { + u_escape(codepoint); + } + else + { + u_escape(0xD7C0u + (codepoint >> 10u)); + u_escape(0xDC00u + (codepoint & 0x3FFu)); + } + continue; + } + // a quote, a backslash, or a control character (or DEL) + const unsigned char c = s[i++]; + char escape = 0; + switch (c) + { + case 0x08: + escape = 'b'; + break; + case 0x09: + escape = 't'; + break; + case 0x0A: + escape = 'n'; + break; + case 0x0C: + escape = 'f'; + break; + case 0x0D: + escape = 'r'; + break; + case 0x22: + escape = '"'; + break; + case 0x5C: + escape = '\\'; + break; + default: + break; + } + if (escape != 0) + { + w[0] = '\\'; + w[1] = escape; + w += 2; + } + else + { + u_escape(c); + } + } + room(1); + *w++ = '"'; + m_out.set_cursor(w); } - /// the copies of dump_compact() that are not fixed-size moves (long + /// the copies of dump_text() that are not fixed-size moves (long /// strings, or near the end of the source); out of line, so that the /// compiler does not merge the fixed-size moves into this call NLOHMANN_VIEW_NOINLINE static void copy_long(char* to, const char* from, std::size_t n) noexcept @@ -481,7 +688,7 @@ class view_serializer } /*! - @brief the compact output (the default dump(), or with ensure_ascii) + @brief the output, compact (the default dump()) or indented, with or without ensure_ascii The same walk as dump(), with the write position in a local variable (stores through char pointers would otherwise force a reload of the @@ -489,11 +696,11 @@ class view_serializer the source copied by fixed-size moves of 32 bytes where the source has that many bytes left, instead of a library call per token. The buffer keeps 64 bytes of slack for the overshoot. With ensure_ascii (Ascii), a - string of the source is copied only if it has no byte >= 0x7F (see - is_ascii_without_delete()); any other string is written by write_string(). + string of the source is copied only if it has nothing to escape (see + copied()); any other string is written by write_ascii(). */ - template - void dump_compact(const node* root) + template + void dump_text(const node* root) { struct frame { @@ -508,6 +715,16 @@ class view_serializer const char* const src_end = src + m_doc.size; char* w = m_out.cursor(); char* lim = m_out.limit(); + // (indented output) a line break and 63 indentation characters, from + // which the line break of a level of up to 63 characters is a copy + std::array ruler; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled below if used + if (Pretty) + { + ruler.fill(m_style.indent_char); + ruler[0] = '\n'; + } + const std::size_t indent = m_style.indent; + depth_counter level; // depth of the innermost open container (indented output) // room for n bytes and the slack const auto room = [&](std::size_t n) { @@ -539,6 +756,22 @@ class view_serializer } w += n; }; + // (indented output) the line break and the indentation of a level + const auto newline = [&](std::size_t depth) + { + const std::size_t count = 1 + (depth * indent); + room(count); + if (count <= ruler.size()) + { + std::memcpy(w, ruler.data(), ruler.size()); + } + else + { + *w = '\n'; + std::memset(w + 1, m_style.indent_char, count - 1); + } + w += count; + }; // a string that is not a plain string of the source (out of line, so // that the cursor stays in a register here) const auto escaped = [&](const node & n) @@ -546,11 +779,11 @@ class view_serializer m_out.set_cursor(w); if (enabled(Ascii)) { - write_ascii_string(n); + write_ascii(n); } else { - write_decoded(n); + write_decoded(n, Pretty); } w = m_out.cursor(); lim = m_out.limit(); @@ -651,6 +884,7 @@ class view_serializer { *w++ = object ? '{' : '['; stack.push_back(cur); + level.enter(); if (Editable) { cur = frame{nav::first(m_doc, n), nav::end(m_doc, n), object}; @@ -693,6 +927,10 @@ class view_serializer { break; } + if (Pretty) + { + newline(level.leave()); + } room(1); *w++ = cur.object ? '}' : ']'; cur = stack.back(); @@ -701,13 +939,17 @@ class view_serializer room(1); *w++ = ','; } + if (Pretty) + { + newline(level.get()); + } const node* const at = Editable ? cur.pos : n; if (cur.object) { const node& key = *at; if (copied(key, src, std::integral_constant())) { - room(key.len + 3); + room(key.len + (Pretty ? 4 : 3)); *w++ = '"'; copy(src + key.off, key.len); w[0] = '"'; @@ -717,9 +959,13 @@ class view_serializer else { escaped(key); - room(1); + room(Pretty ? 2 : 1); *w++ = ':'; } + if (Pretty) + { + *w++ = ' '; + } if (Editable) { n = nav::value(at + 1); @@ -738,69 +984,6 @@ class view_serializer } } - void newline(std::size_t level) - { - if (m_style.pretty) - { - m_out.put('\n'); - m_out.put_repeated(m_style.indent_char, level * m_style.indent); - } - } - - void write_scalar(const node& n) - { - switch (static_cast(n.kind)) - { - case value_t::null: - m_out.put("null"); - break; - case value_t::boolean: - if ((n.flags & node_flags::is_true) != 0) - { - m_out.put("true"); - } - else - { - m_out.put("false"); - } - break; - case value_t::string: - write_string(n); - break; - case value_t::number_integer: - case value_t::number_unsigned: - { - const char* const token = m_doc.str(n); - const std::uint32_t len = number_length(n); - if (!m_style.source_numbers && len == 2 && token[0] == '-' && token[1] == '0') - { - m_out.put('0'); // parse() reads -0 as the integer 0 - } - else - { - m_out.put(token, len); - } - break; - } - case value_t::number_float: - if (m_style.source_numbers && (n.flags & node_flags::storage) != node_flags::edited) - { - m_out.put(m_doc.str(n), n.len); // (a float set by an edit is written as with shortest) - } - else - { - write_float_node(n); - } - break; - case value_t::object: // LCOV_EXCL_LINE (containers are written by dump()) - case value_t::array: // LCOV_EXCL_LINE - case value_t::binary: // LCOV_EXCL_LINE (not in a document) - case value_t::discarded: // LCOV_EXCL_LINE - default: // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE - } - } - /// a float node as dump() writes it void write_float_node(const node& n) { @@ -911,72 +1094,6 @@ class view_serializer m_out.put(s.data(), s.size()); } - void write_string(const node& n) - { - const char* const s = m_doc.str(n); - m_out.put('"'); - if ((n.flags & node_flags::storage) == 0 && (!m_style.ensure_ascii || is_ascii_without_delete(s, n.len))) - { - // a string of the source without escape sequences has nothing to - // escape (with ensure_ascii, if it is printable ASCII) - m_out.put(s, n.len); - } - else if (m_style.ensure_ascii) - { - write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - } - else - { - write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - } - m_out.put('"'); - } - - /// as serializer::dump_escaped(); strings of a document are valid UTF-8, - /// except in a damaged image loaded with image_check::bounds, for which - /// this throws what basic_json::dump() throws for the string - template - void write_escaped(const unsigned char* s, std::size_t n) - { - std::size_t i = 0; - while (i < n) - { - std::size_t run = 0; - if (!EnsureAscii) - { - run = string_bulk_run(s + i, n - i); - } - else if (is_ascii_copyable(s[i])) - { - run = find_ascii_copyable_run(s + i, n - i); - } - if (run != 0) - { - m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - i += run; - continue; - } - std::uint32_t codepoint = s[i]; - std::size_t len = 1; - if (codepoint >= 0x80) - { - len = validate_one_utf8(s + i, n - i); - if (NLOHMANN_VIEW_UNLIKELY(len == 0)) - { - invalid_utf8(s, n); - return; - } - codepoint &= 0xFFu >> (len + 1); - for (std::size_t k = 1; k < len; ++k) - { - codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu); - } - } - write_codepoint(codepoint, s + i, len); - i += len; - } - } - /// throw what basic_json::dump() throws for a string that is not valid UTF-8 NLOHMANN_VIEW_NOINLINE static void invalid_utf8(const unsigned char* s, std::size_t n) { diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 6220270c7..7b058469d 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -6089,6 +6089,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -6146,13 +6148,6 @@ class output_buffer put(s, N - 1); } - void put_repeated(char c, std::size_t n) - { - reserve(n); - std::memset(m_pos, c, n); - m_pos += n; - } - /// the write position and the end of the writable space, for a writer /// that keeps the position in a local variable (set_cursor() hands it back) char* cursor() const noexcept @@ -6209,9 +6204,27 @@ class output_buffer /// image_check::bounds can have others, which are then written unchanged). inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept { + std::size_t i = 0; +#if NLOHMANN_VIEW_SSE2 + // (16 bytes per step; with NEON, the mask of a step costs more than it saves on the + // short runs that are typical here) + // NOLINTBEGIN(portability-simd-intrinsics) + for (; i + 16 <= n; i += 16) + { + const __m128i in = _mm_loadu_si128(static_cast(static_cast(s + i))); // NOLINT(bugprone-casting-through-void) + // (a byte is below 0x20 if it is its own minimum with 0x1F) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_min_epu8(in, _mm_set1_epi8(0x1F)), in)); + const auto mask = static_cast(static_cast(_mm_movemask_epi8(special))); + if (mask != 0) + { + return i + static_cast(count_trailing_zeros(mask)); + } + } + // NOLINTEND(portability-simd-intrinsics) +#endif constexpr std::uint64_t ones = 0x0101010101010101ull; constexpr std::uint64_t high = 0x8080808080808080ull; - std::size_t i = 0; for (; i + 8 <= n; i += 8) { const std::uint64_t v = read_eight_bytes(s + i); @@ -6262,6 +6275,70 @@ inline bool is_ascii_without_delete(const char* s, std::size_t n) noexcept return true; } +#if NLOHMANN_VIEW_VECTOR +/// the bytes of the 16 at p that ensure_ascii cannot copy (a quote, a +/// backslash, a control character, DEL, or not ASCII; a signed compare with +/// 0x20 finds control characters and non-ASCII bytes at once), one bit per +/// byte (x86) or one nibble per byte (NEON) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t ascii_stop_mask(const unsigned char* p) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vorrq_u8(vceqq_u8(in, vdupq_n_u8(0x7F)), vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)))); + return vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F)), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20)))); + return static_cast(static_cast(_mm_movemask_epi8(special))); +#endif +} + +/// the width in bits of a byte in the mask of ascii_stop_mask() +constexpr unsigned ascii_stop_bits() noexcept +{ + return NLOHMANN_VIEW_NEON ? 4u : 1u; +} +#endif + +/// As find_ascii_copyable_run(), 16 bytes per step. end is the end of the +/// buffer that holds the n bytes at p (a short run is read as 16 bytes if +/// they are in the buffer). +inline std::size_t ascii_copyable_run(const unsigned char* p, std::size_t n, const unsigned char* end) noexcept +{ +#if NLOHMANN_VIEW_VECTOR + if (n >= 16) + { + std::size_t i = 0; + for (; i + 16 <= n; i += 16) + { + const std::uint64_t mask = ascii_stop_mask(p + i); + if (mask != 0) + { + return i + (static_cast(count_trailing_zeros(mask)) / ascii_stop_bits()); + } + } + if (i == n) + { + return n; + } + // the last 16 bytes: those before i are known to be copyable + const std::uint64_t mask = ascii_stop_mask(p + n - 16); + return mask != 0 ? n - 16 + (static_cast(count_trailing_zeros(mask)) / ascii_stop_bits()) : n; + } + if (end - p >= 16) + { + // (the bytes after the run are masked out) + const std::uint64_t mask = ascii_stop_mask(p) & ((std::uint64_t{1} << (n * ascii_stop_bits())) - 1u); + return mask != 0 ? static_cast(count_trailing_zeros(mask)) / ascii_stop_bits() : n; + } +#else + static_cast(end); +#endif + return find_ascii_copyable_run(p, n); +} + /// A stack that starts in a buffer of the caller (a local array) and moves to /// the heap (a vector of the caller) only when that is full, so that dumps of /// shallow documents need no allocation. The top is a pointer, as in @@ -6341,6 +6418,39 @@ NLOHMANN_VIEW_ALWAYS_INLINE char* put_literal(char* w, const char (&s)[N]) noexc return w + N - 1; } +/// the depth of the innermost open container, counted only for indented output +template +struct depth_counter +{ + std::size_t value = 0; + void enter() noexcept + { + ++value; + } + std::size_t leave() noexcept + { + return --value; + } + std::size_t get() const noexcept + { + return value; + } +}; + +template<> +struct depth_counter +{ + void enter() noexcept {} + std::size_t leave() noexcept // NOLINT(readability-convert-member-functions-to-static): same interface as depth_counter + { + return 0; + } + std::size_t get() const noexcept // NOLINT(readability-convert-member-functions-to-static): same interface as depth_counter + { + return 0; + } +}; + /// how the view's dump() writes a value struct dump_style { @@ -6377,139 +6487,95 @@ class view_serializer void dump(const node* root) { - if (!m_style.pretty) + if (m_style.ensure_ascii) { - if (m_style.ensure_ascii) - { - dump_compact_ascii(root); - } - else if (m_style.source_numbers) - { - dump_compact(root); - } - else - { - dump_compact(root); - } - return; + dump_ascii(root); } - struct frame + else if (m_style.pretty) { - const node* pos; ///< next element, or key of the next member - const node* end; - bool object; - bool first; ///< nothing written yet - }; - std::array buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read - std::vector heap; - small_stack stack(buffer.data(), buffer.size(), heap); - const node* n = root; - for (;;) + dump_pretty(root); + } + else if (m_style.source_numbers) { - // write the value at n - if (is_container(*n)) - { - const bool object = n->kind == static_cast(value_t::object); - if (n->len == 0) - { - m_out.put(object ? "{}" : "[]", 2); - } - else - { - m_out.put(object ? '{' : '['); - stack.push_back(frame{nav::first(m_doc, n), nav::end(m_doc, n), object, true}); - } - } - else - { - write_scalar(*n); - } - - // go to the next value: close finished containers, then separate - for (;;) - { - if (stack.empty()) - { - m_out.finish(); - return; - } - if (stack.back().pos == stack.back().end) - { - const bool object = stack.back().object; - stack.pop_back(); - newline(stack.size()); - m_out.put(object ? '}' : ']'); - continue; - } - frame& f = stack.back(); - if (!f.first) - { - m_out.put(','); - } - f.first = false; - newline(stack.size()); - if (f.object) - { - write_string(*f.pos); - if (m_style.pretty) - { - m_out.put(": "); - } - else - { - m_out.put(':'); - } - n = nav::value(f.pos + 1); - f.pos = document_data::after(f.pos + 1); - } - else - { - n = nav::value(f.pos); - f.pos = document_data::after(f.pos); - } - break; - } + dump_text(root); + } + else + { + dump_text(root); } } private: + /// dump_text() with ensure_ascii, out of line: the walk without it is + /// faster when it is not merged with this one into dump() + NLOHMANN_VIEW_NOINLINE void dump_ascii(const node* root) + { + if (m_style.pretty) + { + if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + else if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + + /// dump_text() with indentation, out of line: the compact walk is faster + /// when it is not merged with the indented ones into dump() + NLOHMANN_VIEW_NOINLINE void dump_pretty(const node* root) + { + if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + /// a string that is not a plain string of the source (decoded, or written /// by an edit), without ensure_ascii: runs without characters to escape - /// are copied - NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n) + /// are copied. If validate is set, the string must be valid UTF-8: the + /// runs are found by the lexer's scan, which validates the sequences, and + /// invalid UTF-8 throws what basic_json::dump() throws. (The compact output + /// writes the bytes of a damaged image unchanged, and scans without + /// validating them.) + NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n, bool validate) { const auto* const s = reinterpret_cast(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) m_out.put('"'); for (std::size_t i = 0; i < n.len;) { - const std::size_t run = plain_output_run(s + i, n.len - i); + const std::size_t run = validate ? string_bulk_run(s + i, n.len - i) : plain_output_run(s + i, n.len - i); if (run != 0) { m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) i += run; continue; } + if (validate && s[i] >= 0x80) + { + invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8) + } write_codepoint(s[i], s + i, 1); // a quote, a backslash, or a control character ++i; } m_out.put('"'); } - /// dump_compact() with ensure_ascii, out of line: the walk without it is - /// faster when it is not merged with this one into dump() - NLOHMANN_VIEW_NOINLINE void dump_compact_ascii(const node* root) - { - if (m_style.source_numbers) - { - dump_compact(root); - } - else - { - dump_compact(root); - } - } - - /// whether dump_compact() copies a string or key from the source: one of + /// whether dump_text() copies a string or key from the source: one of /// the source, and with ensure_ascii one without bytes >= 0x7F (an overload /// each, so that the walk without ensure_ascii is compiled exactly as /// before: with GCC, a condition that only folds away later changed its @@ -6524,13 +6590,155 @@ class view_serializer return (n.flags & node_flags::storage) == 0 && is_ascii_without_delete(src + n.off, n.len); } - /// a string with ensure_ascii (dump_compact()), out of line - NLOHMANN_VIEW_NOINLINE void write_ascii_string(const node& n) + /*! + @brief a string with ensure_ascii: the characters that are not printable ASCII as \\uXXXX + (a character beyond U+FFFF as a surrogate pair); strings of a document + are valid UTF-8, except in a damaged image loaded with image_check::bounds, + for which this throws what basic_json::dump() throws for the string + */ + NLOHMANN_VIEW_NOINLINE void write_ascii(const node& n) { - write_string(n); + const auto* const s = reinterpret_cast(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const std::size_t size = n.len; + char* w = m_out.cursor(); + char* lim = m_out.limit(); + // room for k bytes + const auto room = [&](std::size_t k) + { + const auto left = static_cast(lim - w); + if (NLOHMANN_VIEW_UNLIKELY(left < k)) + { + m_out.set_cursor(w); + m_out.reserve(k); + w = m_out.cursor(); + lim = m_out.limit(); + } + }; + static constexpr const char* hex = "0123456789abcdef"; + // \\uXXXX + const auto u_escape = [&](std::uint32_t u) + { + w[0] = '\\'; + w[1] = 'u'; + w[2] = hex[(u >> 12u) & 0xFu]; + w[3] = hex[(u >> 8u) & 0xFu]; + w[4] = hex[(u >> 4u) & 0xFu]; + w[5] = hex[u & 0xFu]; + w += 6; + }; + room(1); + *w++ = '"'; + std::size_t i = 0; + // (a string of the source starts with a run that has nothing to escape, 16 bytes per step) + const std::size_t prefix = (n.flags & node_flags::storage) == 0 ? ascii_copyable_run(s, size, reinterpret_cast(m_doc.src + m_doc.size)) : 0; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + if (prefix != 0) + { + room(prefix); + std::memcpy(w, s, prefix); + w += prefix; + i = prefix; + } + while (i < size) + { + // (a character that needs an escape sequence is not worth a call) + const std::size_t run = is_ascii_copyable(s[i]) ? find_ascii_copyable_run(s + i, size - i) : 0; + if (run != 0) + { + room(run); + std::memcpy(w, s + i, run); + w += run; + i += run; + continue; + } + room(12); + if (s[i] >= 0x80) + { + const unsigned char lead = s[i]; + // (the two-byte sequences of Latin text and the three-byte sequences of most other + // text, with continuation bytes of any value: the ranges that are restricted (E0, + // ED, and the sequences of four bytes) go through validate_one_utf8()) + if (lead >= 0xE1 && lead <= 0xEF && lead != 0xED && size - i >= 3 && (s[i + 1] & 0xC0u) == 0x80 && (s[i + 2] & 0xC0u) == 0x80) + { + u_escape(((lead & 0x0Fu) << 12u) | ((s[i + 1] & 0x3Fu) << 6u) | (s[i + 2] & 0x3Fu)); + i += 3; + continue; + } + if (lead >= 0xC2 && lead <= 0xDF && size - i >= 2 && (s[i + 1] & 0xC0u) == 0x80) + { + u_escape(((lead & 0x1Fu) << 6u) | (s[i + 1] & 0x3Fu)); + i += 2; + continue; + } + const std::size_t len = validate_one_utf8(s + i, size - i); + if (NLOHMANN_VIEW_UNLIKELY(len == 0)) + { + m_out.set_cursor(w); + invalid_utf8(s, size); + return; // LCOV_EXCL_LINE (invalid_utf8 throws) + } + std::uint32_t codepoint = s[i] & (0xFFu >> (len + 1)); + for (std::size_t k = 1; k < len; ++k) + { + codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu); + } + i += len; + if (codepoint <= 0xFFFF) + { + u_escape(codepoint); + } + else + { + u_escape(0xD7C0u + (codepoint >> 10u)); + u_escape(0xDC00u + (codepoint & 0x3FFu)); + } + continue; + } + // a quote, a backslash, or a control character (or DEL) + const unsigned char c = s[i++]; + char escape = 0; + switch (c) + { + case 0x08: + escape = 'b'; + break; + case 0x09: + escape = 't'; + break; + case 0x0A: + escape = 'n'; + break; + case 0x0C: + escape = 'f'; + break; + case 0x0D: + escape = 'r'; + break; + case 0x22: + escape = '"'; + break; + case 0x5C: + escape = '\\'; + break; + default: + break; + } + if (escape != 0) + { + w[0] = '\\'; + w[1] = escape; + w += 2; + } + else + { + u_escape(c); + } + } + room(1); + *w++ = '"'; + m_out.set_cursor(w); } - /// the copies of dump_compact() that are not fixed-size moves (long + /// the copies of dump_text() that are not fixed-size moves (long /// strings, or near the end of the source); out of line, so that the /// compiler does not merge the fixed-size moves into this call NLOHMANN_VIEW_NOINLINE static void copy_long(char* to, const char* from, std::size_t n) noexcept @@ -6547,7 +6755,7 @@ class view_serializer } /*! - @brief the compact output (the default dump(), or with ensure_ascii) + @brief the output, compact (the default dump()) or indented, with or without ensure_ascii The same walk as dump(), with the write position in a local variable (stores through char pointers would otherwise force a reload of the @@ -6555,11 +6763,11 @@ class view_serializer the source copied by fixed-size moves of 32 bytes where the source has that many bytes left, instead of a library call per token. The buffer keeps 64 bytes of slack for the overshoot. With ensure_ascii (Ascii), a - string of the source is copied only if it has no byte >= 0x7F (see - is_ascii_without_delete()); any other string is written by write_string(). + string of the source is copied only if it has nothing to escape (see + copied()); any other string is written by write_ascii(). */ - template - void dump_compact(const node* root) + template + void dump_text(const node* root) { struct frame { @@ -6574,6 +6782,16 @@ class view_serializer const char* const src_end = src + m_doc.size; char* w = m_out.cursor(); char* lim = m_out.limit(); + // (indented output) a line break and 63 indentation characters, from + // which the line break of a level of up to 63 characters is a copy + std::array ruler; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled below if used + if (Pretty) + { + ruler.fill(m_style.indent_char); + ruler[0] = '\n'; + } + const std::size_t indent = m_style.indent; + depth_counter level; // depth of the innermost open container (indented output) // room for n bytes and the slack const auto room = [&](std::size_t n) { @@ -6605,6 +6823,22 @@ class view_serializer } w += n; }; + // (indented output) the line break and the indentation of a level + const auto newline = [&](std::size_t depth) + { + const std::size_t count = 1 + (depth * indent); + room(count); + if (count <= ruler.size()) + { + std::memcpy(w, ruler.data(), ruler.size()); + } + else + { + *w = '\n'; + std::memset(w + 1, m_style.indent_char, count - 1); + } + w += count; + }; // a string that is not a plain string of the source (out of line, so // that the cursor stays in a register here) const auto escaped = [&](const node & n) @@ -6612,11 +6846,11 @@ class view_serializer m_out.set_cursor(w); if (enabled(Ascii)) { - write_ascii_string(n); + write_ascii(n); } else { - write_decoded(n); + write_decoded(n, Pretty); } w = m_out.cursor(); lim = m_out.limit(); @@ -6717,6 +6951,7 @@ class view_serializer { *w++ = object ? '{' : '['; stack.push_back(cur); + level.enter(); if (Editable) { cur = frame{nav::first(m_doc, n), nav::end(m_doc, n), object}; @@ -6759,6 +6994,10 @@ class view_serializer { break; } + if (Pretty) + { + newline(level.leave()); + } room(1); *w++ = cur.object ? '}' : ']'; cur = stack.back(); @@ -6767,13 +7006,17 @@ class view_serializer room(1); *w++ = ','; } + if (Pretty) + { + newline(level.get()); + } const node* const at = Editable ? cur.pos : n; if (cur.object) { const node& key = *at; if (copied(key, src, std::integral_constant())) { - room(key.len + 3); + room(key.len + (Pretty ? 4 : 3)); *w++ = '"'; copy(src + key.off, key.len); w[0] = '"'; @@ -6783,9 +7026,13 @@ class view_serializer else { escaped(key); - room(1); + room(Pretty ? 2 : 1); *w++ = ':'; } + if (Pretty) + { + *w++ = ' '; + } if (Editable) { n = nav::value(at + 1); @@ -6804,69 +7051,6 @@ class view_serializer } } - void newline(std::size_t level) - { - if (m_style.pretty) - { - m_out.put('\n'); - m_out.put_repeated(m_style.indent_char, level * m_style.indent); - } - } - - void write_scalar(const node& n) - { - switch (static_cast(n.kind)) - { - case value_t::null: - m_out.put("null"); - break; - case value_t::boolean: - if ((n.flags & node_flags::is_true) != 0) - { - m_out.put("true"); - } - else - { - m_out.put("false"); - } - break; - case value_t::string: - write_string(n); - break; - case value_t::number_integer: - case value_t::number_unsigned: - { - const char* const token = m_doc.str(n); - const std::uint32_t len = number_length(n); - if (!m_style.source_numbers && len == 2 && token[0] == '-' && token[1] == '0') - { - m_out.put('0'); // parse() reads -0 as the integer 0 - } - else - { - m_out.put(token, len); - } - break; - } - case value_t::number_float: - if (m_style.source_numbers && (n.flags & node_flags::storage) != node_flags::edited) - { - m_out.put(m_doc.str(n), n.len); // (a float set by an edit is written as with shortest) - } - else - { - write_float_node(n); - } - break; - case value_t::object: // LCOV_EXCL_LINE (containers are written by dump()) - case value_t::array: // LCOV_EXCL_LINE - case value_t::binary: // LCOV_EXCL_LINE (not in a document) - case value_t::discarded: // LCOV_EXCL_LINE - default: // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE - } - } - /// a float node as dump() writes it void write_float_node(const node& n) { @@ -6977,72 +7161,6 @@ class view_serializer m_out.put(s.data(), s.size()); } - void write_string(const node& n) - { - const char* const s = m_doc.str(n); - m_out.put('"'); - if ((n.flags & node_flags::storage) == 0 && (!m_style.ensure_ascii || is_ascii_without_delete(s, n.len))) - { - // a string of the source without escape sequences has nothing to - // escape (with ensure_ascii, if it is printable ASCII) - m_out.put(s, n.len); - } - else if (m_style.ensure_ascii) - { - write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - } - else - { - write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - } - m_out.put('"'); - } - - /// as serializer::dump_escaped(); strings of a document are valid UTF-8, - /// except in a damaged image loaded with image_check::bounds, for which - /// this throws what basic_json::dump() throws for the string - template - void write_escaped(const unsigned char* s, std::size_t n) - { - std::size_t i = 0; - while (i < n) - { - std::size_t run = 0; - if (!EnsureAscii) - { - run = string_bulk_run(s + i, n - i); - } - else if (is_ascii_copyable(s[i])) - { - run = find_ascii_copyable_run(s + i, n - i); - } - if (run != 0) - { - m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - i += run; - continue; - } - std::uint32_t codepoint = s[i]; - std::size_t len = 1; - if (codepoint >= 0x80) - { - len = validate_one_utf8(s + i, n - i); - if (NLOHMANN_VIEW_UNLIKELY(len == 0)) - { - invalid_utf8(s, n); - return; - } - codepoint &= 0xFFu >> (len + 1); - for (std::size_t k = 1; k < len; ++k) - { - codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu); - } - } - write_codepoint(codepoint, s + i, len); - i += len; - } - } - /// throw what basic_json::dump() throws for a string that is not valid UTF-8 NLOHMANN_VIEW_NOINLINE static void invalid_utf8(const unsigned char* s, std::size_t n) { diff --git a/tests/src/unit-json_view_dump.cpp b/tests/src/unit-json_view_dump.cpp index 3b9da6b89..850dba3c7 100644 --- a/tests/src/unit-json_view_dump.cpp +++ b/tests/src/unit-json_view_dump.cpp @@ -186,6 +186,133 @@ TEST_CASE("json_view dump") CHECK(deep_document.root().dump() == deep); } + SECTION("ensure_ascii: a character that needs an escape at each position of a string") + { + // string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text + for (const char* const special : + { + "\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\" + }) + { + for (std::size_t length = 0; length <= 40; ++length) + { + for (std::size_t position = 0; position <= length; ++position) + { + std::string content(length, 'a'); + content.insert(position, special); + const std::array texts = + { + { + "\"" + content + "\"", + "[\"" + content + "\"]", + "{\"" + content + "\":1}", + } + }; + for (const std::string& text : texts) + { + CAPTURE(text) + const ordered_json_document d = ordered_json_document::parse(text); + const ordered_json j = ordered_json::parse(text); + CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true)); + CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true)); + CHECK(d.root().dump() == j.dump()); + } + } + } + } + } + + SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond") + { + // (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF) + std::string text = "["; + std::size_t in_chunk = 0; + const auto append = [&](std::uint32_t cp) + { + if (cp < 0x80) + { + if (cp < 0x20 || cp == '"' || cp == '\\') + { + return; + } + text += static_cast(cp); + } + else if (cp < 0x800) + { + text += static_cast(0xC0 | (cp >> 6)); + text += static_cast(0x80 | (cp & 0x3F)); + } + else if (cp < 0x10000) + { + text += static_cast(0xE0 | (cp >> 12)); + text += static_cast(0x80 | ((cp >> 6) & 0x3F)); + text += static_cast(0x80 | (cp & 0x3F)); + } + else + { + text += static_cast(0xF0 | (cp >> 18)); + text += static_cast(0x80 | ((cp >> 12) & 0x3F)); + text += static_cast(0x80 | ((cp >> 6) & 0x3F)); + text += static_cast(0x80 | (cp & 0x3F)); + } + }; + for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp) + { + if (cp >= 0xD800 && cp < 0xE000) + { + continue; + } + if (in_chunk == 0) + { + text += text.size() > 1 ? ",\"" : "\""; + } + append(cp); + if (++in_chunk == 37) + { + text += "\""; + in_chunk = 0; + } + } + text += in_chunk != 0 ? "\"]" : "]"; + const ordered_json_document d = ordered_json_document::parse(text); + const ordered_json j = ordered_json::parse(text); + CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true)); + CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true)); + CHECK(d.root().dump() == j.dump()); + CHECK(d.root().dump(2) == j.dump(2)); + } + + SECTION("deep nesting, indented") + { + // indentations of up to and beyond the width of the writer's fixed-size copy + for (const int depth : + { + 1, 5, 20, 70, 300 + }) + { + std::string text; + for (int i = 0; i < depth; ++i) + { + text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)"; + } + text += "[]"; + for (int i = depth - 1; i >= 0; --i) + { + text += i % 2 == 0 ? "]" : "}"; + } + const ordered_json_document d = ordered_json_document::parse(text); + const ordered_json j = ordered_json::parse(text); + for (const int indent : + { + 0, 1, 2, 3, 8 + }) + { + CHECK(d.root().dump(indent) == j.dump(indent)); + CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t')); + } + } + } + SECTION("the output buffer of a small value is small") { // an escaped key after the value: its node lies in the arena, so the diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index 50023fe63..eaf27eb9f 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -682,6 +682,26 @@ TEST_CASE("json_view images: ownership") #if !defined(JSON_NOEXCEPTION) TEST_CASE("json_view images: errors") { + SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw") + { + const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})"); + std::vector image = d.save(); + const std::size_t arena = text_at(image) + header_size_field(image, 16) + 1; + image[arena + 3] = 0x28; // the key is "k\n"; the value starts at 3: 0xC3 0x28 is invalid + const json_document loaded = json_document::load(image, image_check::bounds); + CHECK(loaded.root().dump().size() == 13); + for (const int indent : + { + 0, 2 + }) + { + CHECK_THROWS_AS(loaded.root().dump(indent), json::type_error); + CHECK_THROWS_AS(loaded.root().dump(indent, ' ', true), json::type_error); + } + CHECK_THROWS_AS(loaded.root().dump(-1, ' ', true), json::type_error); + CHECK_THROWS_WITH_AS(loaded.root().dump(2), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0x28", json::type_error&); + } + SECTION("a literal as the root: dump() after loading") { for (const char* text : From dd7686a26937acfafcdaf7c1c484798c1dedefc8 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 24 Oct 2026 07:41:06 +0200 Subject: [PATCH 3/4] json_view: faster reading, indexing and C-stream parsing (#5888) * Read C streams into a json_document in blocks json_document::parse(FILE*) (and read(), accept(), parse_copy()) went through the library's file input adapter and appended the text character by character with fgetc. Read it with fread in blocks instead, like the std::istream overload already does. Parsing citm_catalog.json (1.7 MB) from a FILE*: Apple M1: 80.6 M -> 8.4 M instructions, 4.8 ms -> 0.55 ms (parse of the same text from a std::string: 8.7 M) Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 47.0 M -> 13.7 M / 45.8 M -> 14.2 M The parsed documents and the error cases are identical (checked over the test corpus and 30 mutants of each file). A null FILE* is handled as before (by the library's adapter). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Index arrays without subtrees directly operator[](idx), at(idx) and back() skipped the elements of an array one after the other, whatever they were. In an array none of whose elements is an array or object (numbers, strings, booleans, null) the elements are adjacent nodes, which the array node's own sizes tell: its subtree has exactly one node more than it has elements. Such an element is now the node idx after the array node; any other array is walked as before. Reading r[i] for every i of an array of 3000 integers: Apple M1: 28.8 M instructions, 3.1 ms -> 0.1 M instructions, 4 us Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 84.1 M -> 3.1 M / 75.0 M -> 3.0 M Arrays that contain arrays are unchanged. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Skip the bounds checks of number and string tokens in NUL-terminated input For a NUL-terminated input (std::string, C string, string literal), the parser reads the terminator instead of comparing p with the end, via cursor::cur(). Number and string scanning still tested `p != e` before each look at the next byte (after the integer digits for '.', for 'e', after the exponent sign and digits, and after a string for its closing quote), although the terminator can never be one of those characters. They now use cur(), which is the plain load for NUL-terminated input and the bounds check for ptr+len input, as before. Parsing, Apple M1 instructions per parse: twitter 3.126 M -> 3.089 M (-1.2 %), citm_catalog 8.70 M -> 8.58 M (-1.3 %), jeopardy 306.1 M -> 302.5 M (-1.2 %), signed integers 281 M -> 266 M (-5.4 %), canada and floats -0.1 % / -0.9 %. Linux x86-64 Ir, GCC 13 / Clang 18: twitter -0.5 % / -0.0 %, signed integers +0.2 % / -0.7 %. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Make the index text of items() on arrays lazy basic_json_view::items() on an array formatted every element's index with std::to_string when the item was made, whether or not the loop asks for the key. The item now keeps the position and makes the text on the first key() call. Objects are not affected. Iterating over the items of an array of 3000 numbers and reading only the values: Apple M1: 25.5 us -> 4.6 us (0.45 M -> ~0 net instructions) Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 14.9 M -> 4.1 M / 15.3 M -> 4.6 M Reading key() costs what it did (24.5 us). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Make the default of value() with a C string only if the member is missing basic_json_view::value(key, "text") and value(pointer, "text") built a std::string from the default first and passed it on, so a call allocated for the default even when the member exists, and copied it again when it does not. They now look the member up and make the result directly: from the member if there is one, else from the C string. Behavior, including the exceptions, is the same. Two calls (member found, member missing) with a 40 character default, Apple M1: 1.6 K -> 0.7 K instructions Linux x86-64 Ir (500 rounds), GCC 13 / Clang 18: 2.46 M -> 2.22 M / 2.40 M -> 2.15 M Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Materialize scalars directly and keep the stack of small subtrees local basic_json_view::materialize() (and get()) always set up the SAX handler that parse() uses and a std::vector for the open containers, sized from the document's deepest nesting. For a scalar, which is most of what is materialized one value at a time, neither is needed: the value is made directly (the same constructors the handler ends up calling). For arrays and objects the stack of open containers is now a local array for the first 16 levels, and the vector is only used for deeper documents. Materializing five scalars of a small document, Apple M1: 3.6 K -> 0.9 K instructions Linux x86-64 Ir (500 rounds), GCC 13 / Clang 18: 2.96 M -> 2.37 M / 2.90 M -> 2.29 M Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Copy the text between escapes while scanning it A string that contains an escape is decoded by builder::slow_string(). After each escape it scanned the next run of plain bytes (8 or 16 byte-by-byte steps, then a vector loop), and then copied that run to the decode arena in a second pass. The run is now copied 16 bytes at a time while it is scanned (one vector load, a store of the same 16 bytes, and the mask of the bytes that end the run), and the write position lives in a local variable, not in the builder, since a store of a character may alias the builder's members and made the compiler reload it after every escape. A run that ends at a quote needs no scan, and non-ASCII text goes through the UTF-8 check as before; the portable code is unchanged. Parsing, Apple M1 instructions: jeopardy.json 304.0 M -> 293.8 M (-3.4 %), 3000 strings with escapes 1.72 M -> 1.54 M (-10 %), twitter -0.6 %. Linux x86-64 Ir, GCC 13 / Clang 18: escaped strings -7.6 % / -11.4 %, twitter 0.0 % / 0.0 %. Documents and error messages are identical on 900 k random strings of escapes, UTF-8 and control characters (also under ASan), and on mutants of the test corpus. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Hash the last bytes of a key of a large object without a loop key_hash() mixed a key eight bytes at a time and then packed the remaining 0-7 bytes one by one, in a loop. The last word of a key of eight or more bytes is now read as the last eight bytes of the key (overlapping the word before), and a key of 2-7 bytes as two overlapping words of four or two bytes. A product only moves bits upwards, and the slot is taken from the low bits: the high bits of the last word are folded down before the multiplication, since keys that differ only in their last characters ("user_17", "user_18") would otherwise share slots. The hash is not stored, so its values may change. Looking up every key of an object of 1000 random keys, Apple M1 instructions per 1000 lookups: keys of 5 bytes 175 K -> 93 K, hex strings of 8 bytes 177 K -> 168 K, keys of 12 bytes 196 K -> 186 K Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 5-byte keys -4.7 % / -8.8 %, 12-byte keys -3.3 % / -7.4 %. Building the tables while parsing such objects is cheaper as well (5-byte keys: 13.3 us -> 10.1 us). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Compare short keys of a large object without a call of memcmp find_indexed() compared the key of a member the table points to with memcmp, a library call for any length. Up to 16 bytes, the bytes are now compared as two overlapping words (the first and the last 8, 4, or 2 bytes), as find_member() already does for the objects without a table; longer keys still use memcmp. Looking up every key of an object of 1000 random keys, Apple M1 instructions per 1000 lookups (with the previous commit): keys of 5 bytes 93 K -> 72 K, hex strings of 8 bytes 168 K -> 148 K, keys of 12 bytes 186 K -> 170 K, keys of 24 bytes 198 K -> 193 K (unchanged, memcmp). Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 5-byte keys -7.2 % / -7.3 %, 12-byte keys -5.9 % / -6.6 %. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Read an unedited editable document like a read-only one Views of a json_editable_document followed edits at every step: the lookup, the element access, back(), dump() and materialize() were built for nodes that can be moved or linked, though no edit changes the index until the first one (every edit creates the edit state before it touches a node). Until then these now take the read-only code: one check of the document's edit state per call, not per member or per node. After the first edit nothing changes. Views that were made earlier keep working, as the check is made when they are used. Unedited editable document, Apple M1: dump() of twitter.json 1.40 M -> 1.22 M instructions (-12.6 %); lookups unchanged. Linux x86-64 Ir, GCC 13 / Clang 18: dump -1.7 % / -1.0 %, lookups -0.1 % / 0.0 %. Walking with iterators is unchanged, and so is dump() after an edit. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Format the numbered test keys of json_view's index with a stream The keys were made with snprintf() and a format from a list, which Flawfinder reports as an error (CWE-134, a format string that is not a literal). A stream now formats them; the 30000 keys of the test are the same as before. Signed-off-by: Niels Lohmann * Put the key number of the json_view index test in parentheses astyle formatted i * step after << as a pointer declaration (i* step), which failed the amalgamation check. Signed-off-by: Niels Lohmann * Fix the compiler and lint findings of json_view's read side and tests - find() and contains() dispatch on Editable for the member lookup; for documents that cannot be edited both branches were the same call (GCC -Wduplicated-branches) - json_view.hpp includes for the FILE overload (cpplint) - the arena check macro of the builder is on one line (cpplint read it as an if body without braces) - tests: no nested conditional (also MSVC C4018), no useless cast, parentheses around mixed operators, raw string literals, += instead of temporary concatenations, and NOLINT for the fixed seeds and a find() != npos, as elsewhere in the tests Signed-off-by: Niels Lohmann * Grow the decode arena through a function and store its position back on every return slow_string() kept its write position in locals and grew the arena through the macro NLOHMANN_VIEW_ENSURE. The macro is now the always-inline member arena_ensure() (a lambda is not inlined at every call site by clang, which then keeps the position in memory: +11 % instructions on escape-heavy strings), and a guard stores the position back into m_arena_write on every return instead of by hand on the successful one. Instruction counts are unchanged (clang) or 1 % lower (GCC). Also word the complexity of back() as that of at(). Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/at.md | 3 +- docs/mkdocs/docs/api/basic_json_view/back.md | 3 +- .../docs/api/basic_json_view/operator[].md | 3 +- include/nlohmann/detail/view/builder.hpp | 210 +++++-- include/nlohmann/detail/view/input.hpp | 34 ++ include/nlohmann/detail/view/iterator.hpp | 18 +- include/nlohmann/detail/view/lookup.hpp | 8 + include/nlohmann/detail/view/materialize.hpp | 50 +- include/nlohmann/detail/view/object_index.hpp | 112 +++- include/nlohmann/detail/view/simd.hpp | 28 + include/nlohmann/json_view.hpp | 94 ++- single_include/nlohmann/json_view.hpp | 554 +++++++++++++++--- tests/src/unit-json_view.cpp | 127 ++++ tests/src/unit-json_view_builder.cpp | 52 ++ tests/src/unit-json_view_dump.cpp | 148 +++++ tests/src/unit-json_view_edit.cpp | 109 ++++ 16 files changed, 1386 insertions(+), 167 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_view/at.md b/docs/mkdocs/docs/api/basic_json_view/at.md index ce142c23f..afbf81620 100644 --- a/docs/mkdocs/docs/api/basic_json_view/at.md +++ b/docs/mkdocs/docs/api/basic_json_view/at.md @@ -80,7 +80,8 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics Objects with 128 or more members get a hash index of their keys while parsing, so that a lookup in them takes constant time on average. 2. Linear in `idx`: elements are skipped one at a time from the first one, since they are not a fixed size in the - index (unlike `BasicJsonType`'s array, which is random-access). + index (unlike `BasicJsonType`'s array, which is random-access). Constant if no element of the array is an array + or object, since the elements are then adjacent in the index. 3. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at that level (as 1.) or the index into the array (as 2.). diff --git a/docs/mkdocs/docs/api/basic_json_view/back.md b/docs/mkdocs/docs/api/basic_json_view/back.md index 1d70e9feb..acb6ebd65 100644 --- a/docs/mkdocs/docs/api/basic_json_view/back.md +++ b/docs/mkdocs/docs/api/basic_json_view/back.md @@ -28,7 +28,8 @@ This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics. Linear in the size of the array or object: unlike [`front()`](front.md), which only ever looks at the first element, `back()` has to walk every element to find where they end, since elements are not a fixed size in the -index. +index. Constant if no element of the array is an array or object, since the elements are then adjacent +in the index. ## Notes diff --git a/docs/mkdocs/docs/api/basic_json_view/operator[].md b/docs/mkdocs/docs/api/basic_json_view/operator[].md index 354813bdf..8aacf94c9 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator[].md +++ b/docs/mkdocs/docs/api/basic_json_view/operator[].md @@ -86,7 +86,8 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics Objects with 128 or more members get a hash index of their keys while parsing, so that a lookup in them takes constant time on average. 2. Linear in `idx`: elements are skipped one at a time from the first one, since they are not a fixed size in the - index (unlike `BasicJsonType`'s array, which is random-access). + index (unlike `BasicJsonType`'s array, which is random-access). Constant if no element of the array is an array + or object, since the elements are then adjacent in the index. 3. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at that level (as 1.) or the index into the array (as 2.). diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp index 3eb85ce47..3e4ea268a 100644 --- a/include/nlohmann/detail/view/builder.hpp +++ b/include/nlohmann/detail/view/builder.hpp @@ -275,11 +275,34 @@ class builder 0, 0, '\b', 0, 0, 0, '\f', 0, 0, 0, 0, 0, 0, 0, '\n', 0, 0, 0, '\r', 0, '\t', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }; + // The write position is kept in locals: the stores of characters into + // the arena may alias the members of the builder, which would then be + // reloaded after every one. It is stored back where the arena grows, + // and by `write_back` on every return. + const unsigned char* const end = m_end; const std::size_t start = arena_used(); - arena_run(s, static_cast(p - s)); + char* w = m_arena_write; + const char* w_end = m_arena_end; + const arena_write_back write_back(*this, w); + { + const auto n = static_cast(p - s); + if (n <= 16 && end - s >= 16) + { + arena_ensure(w, w_end, 16); + std::memcpy(w, s, 16); + } + else if (n != 0) + { + // (an empty run leaves an empty arena unallocated, and memcpy() + // must not be passed its null pointer even for zero bytes) + arena_ensure(w, w_end, n); + std::memcpy(w, s, n); + } + w += n; + } for (;;) { - if (p == m_end) + if (p == end) { return failed(error_code::string_missing_quote, p); } @@ -287,7 +310,9 @@ class builder if (c == '"') { ++p; - return decoded{p, start, arena_used() - start}; + // (from w: `write_back` stores it only after the return value + // is made; the arena is allocated, since an escape was written) + return decoded{p, start, static_cast(w - m_doc.arena.data()) - start}; } if (c != '\\') { @@ -297,7 +322,7 @@ class builder return failed(c < 0x20 ? error_code::string_control_character : error_code::string_utf8, p); } ++p; - if (p == m_end) + if (p == end) { return failed(error_code::string_missing_quote, p); } @@ -315,7 +340,7 @@ class builder { return failed(error_code::string_surrogate_low, p); } - if (m_end - p < 2 || p[0] != '\\' || p[1] != 'u') + if (end - p < 2 || p[0] != '\\' || p[1] != 'u') { return failed(error_code::string_surrogate_high, p); } @@ -331,26 +356,106 @@ class builder } cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } - arena_ensure(4); - m_arena_write = put_utf8(m_arena_write, cp); + arena_ensure(w, w_end, 4); + w = put_utf8(w, cp); } else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) { - arena_ensure(1); - *m_arena_write++ = simple_escape[d]; + arena_ensure(w, w_end, 1); + *w++ = simple_escape[d]; } else { --p; return failed(error_code::string_escape, p); } - if (p != m_end && *p == '\\') +#if NLOHMANN_VIEW_NEON + if (p != end && *p == '\\') { continue; // consecutive escapes ("\u00e4\u00f6"): no run in between } + // The first 16 bytes of the run are checked one by one, in predicted branches, and copied + // as one block: where the runs between escapes are short, the vector step below would put + // its longer dependency chain (load, compare, mask, count, add) on p for each of them, + // and be slower than these steps (the scanner of the keys works the same way) + { + if (end - p >= 16) + { + const std::uint8_t* const plain = string_plain(); + std::size_t steps = 16; +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { steps = (i); goto run_found; } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP +run_found: + arena_ensure(w, w_end, 16); + std::memcpy(w, p, 16); // (only the first `steps` bytes are kept) + w += steps; + p += steps; + if (steps < 16) + { + if (*p >= 0x80) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + continue; + } + } + } +#else + if (p != end && (*p == '\\' || *p == '"')) + { + continue; // consecutive escapes ("\u00e4\u00f6"), or the end of the string: no run in between + } +#endif +#if NLOHMANN_VIEW_VECTOR + // the plain bytes up to the next quote, backslash, or control + // character are copied as they are scanned, 16 bytes at a time + for (;;) + { + if (NLOHMANN_VIEW_UNLIKELY(end - p < 16)) + { + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + break; + } + arena_ensure(w, w_end, 16); + const unsigned k = copy_plain_run16(p, w); + w += k; + p += k; + if (k == 16) + { + continue; + } + if (NLOHMANN_VIEW_UNLIKELY(*p >= 0x80)) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + break; + } +#else const unsigned char* const r = p; - p = scan_string_run(p, m_end); - arena_run(r, static_cast(p - r)); + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; +#endif } } @@ -392,19 +497,49 @@ class builder char* m_arena_write = nullptr; char* m_arena_end = nullptr; + /// room for n more bytes at w, the write position kept in a local, with + /// w_end the end of the arena. (A lambda instead is not always inlined, + /// and then w would be kept in memory, as m_arena_write is.) + NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(char*& w, const char*& w_end, std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < n)) + { + m_arena_write = w; + arena_grow(n); + w = m_arena_write; + w_end = m_arena_end; + } + } + + /// stores a write position kept in a local back into m_arena_write when + /// the function that keeps it returns, on every path + class arena_write_back + { + public: + arena_write_back(builder& b, char*& w) noexcept + : m_builder(b) + , m_w(w) + {} + ~arena_write_back() + { + m_builder.m_arena_write = m_w; + } + + arena_write_back(const arena_write_back&) = delete; + arena_write_back& operator=(const arena_write_back&) = delete; + arena_write_back(arena_write_back&&) = delete; + arena_write_back& operator=(arena_write_back&&) = delete; + + private: + builder& m_builder; + char*& m_w; + }; + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept { return m_arena_write != nullptr ? static_cast(m_arena_write - m_doc.arena.data()) : 0; } - NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(std::size_t n) - { - if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_arena_end - m_arena_write) < n)) - { - arena_grow(n); - } - } - NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n) { const std::size_t used = arena_used(); @@ -413,25 +548,6 @@ class builder m_arena_end = m_doc.arena.data() + m_doc.arena.size(); } - /// append the run [r, r + n) to the arena; short runs as one fixed-size - /// 16-byte move when both sides have the room (no library call) - NLOHMANN_VIEW_ALWAYS_INLINE void arena_run(const unsigned char* r, std::size_t n) - { - if (n <= 16 && m_end - r >= 16) - { - arena_ensure(16); - std::memcpy(m_arena_write, r, 16); - } - else if (n != 0) - { - // (an empty run leaves an empty arena unallocated, and memcpy() - // must not be passed its null pointer even for zero bytes) - arena_ensure(n); - std::memcpy(m_arena_write, r, n); - } - m_arena_write += n; - } - /// UTF-8 encoding of cp at w (room for 4 bytes); returns the position after /// it. (A returned pointer stays in a register; writing through a /// reference to m_arena_write would store and reload it around every byte.) @@ -954,11 +1070,11 @@ indent_done: ++p; } const unsigned char* const int_start = p; - if (p != e && *p == '0') + if (cur() == '0') { ++p; } - else if (NLOHMANN_VIEW_LIKELY(p != e && *p >= '1' && *p <= '9')) + else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) { p = skip_digits(p + 1, e); } @@ -969,7 +1085,7 @@ indent_done: const auto int_digits = static_cast(p - int_start); std::size_t frac_digits = 0; bool is_float = false; - if (p != e && *p == '.') + if (cur() == '.') { ++p; const unsigned char* const f0 = p; @@ -982,20 +1098,20 @@ indent_done: is_float = true; } std::int64_t exponent = 0; - if (p != e && (*p | 0x20) == 'e') + if ((cur() | 0x20) == 'e') { ++p; bool exp_negative = false; - if (p != e && (*p == '+' || *p == '-')) + if (cur() == '+' || cur() == '-') { exp_negative = *p == '-'; ++p; } - if (NLOHMANN_VIEW_UNLIKELY(p == e || !is_digit(*p))) + if (NLOHMANN_VIEW_UNLIKELY(!is_digit(cur()))) { return fail(error_code::number_after_exponent); } - while (p != e && is_digit(*p)) + while (is_digit(cur())) { if (exponent < 100000) { @@ -1057,7 +1173,7 @@ indent_done: ++p; // opening quote const unsigned char* const s = p; p = scan_string_run(p, e); - if (NLOHMANN_VIEW_LIKELY(p != e && *p == '"')) + if (NLOHMANN_VIEW_LIKELY(cur() == '"')) { push_node(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); ++p; diff --git a/include/nlohmann/detail/view/input.hpp b/include/nlohmann/detail/view/input.hpp index 7e1fea09a..d028e81e2 100644 --- a/include/nlohmann/detail/view/input.hpp +++ b/include/nlohmann/detail/view/input.hpp @@ -10,6 +10,9 @@ #include // max #include // size_t +#ifndef JSON_NO_IO + #include // FILE, fread +#endif #include // istream, streambuf #include // basic_string, char_traits, string #include // decay, integral_constant, is_array, is_const, is_integral, is_lvalue_reference, is_pointer, is_same, remove_cv, remove_extent, remove_pointer, remove_reference @@ -154,6 +157,37 @@ inline void append_stream(std::string& buf, std::istream& is) is.clear(is.rdstate() | std::ios::eofbit); } +/// a pointer to a C stream (never with JSON_NO_IO, which leaves out ) +template +struct is_c_stream +#ifndef JSON_NO_IO + : std::is_same +#else + : std::false_type +#endif +{}; + +#ifndef JSON_NO_IO +/// append the rest of a C stream: in blocks with fread, not character by +/// character (the library's file adapter reads with fgetc); +/// f is not null +inline void append_file(std::string& buf, std::FILE* f) +{ + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::size_t got = std::fread(&buf[used], 1, room, f); + buf.resize(used + got); + if (got < room) + { + break; // end of file, or an error (as fgetc, which returns EOF for both) + } + } +} +#endif + } // namespace view } // namespace detail NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/iterator.hpp b/include/nlohmann/detail/view/iterator.hpp index 12906e9c1..069f50fd1 100644 --- a/include/nlohmann/detail/view/iterator.hpp +++ b/include/nlohmann/detail/view/iterator.hpp @@ -147,20 +147,21 @@ class view_item view_item(const iterator& it, std::size_t index) : m_it(it) - { - if (!it.is_object_iterator()) - { - m_index = std::to_string(index); - } - } + , m_position(index) + {} - /// the member key, or the element index for arrays + /// the member key, or the element index for arrays (as text made on the + /// first call, so that iterating over the values pays nothing for it) string_view_t key() const { if (m_it.is_object_iterator()) { return m_it.key(); } + if (m_index.empty()) + { + m_index = std::to_string(m_position); + } return string_view_t(m_index.data(), m_index.size()); } @@ -183,7 +184,8 @@ class view_item private: iterator m_it; - std::string m_index{}; // NOLINT(readability-redundant-member-init) + std::size_t m_position; ///< the index of an array element + mutable std::string m_index{}; ///< the text of m_position, once key() was called // NOLINT(readability-redundant-member-init) }; /// the range returned by basic_json_view::items() diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 10f7ab400..e2038a956 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -166,6 +166,10 @@ const node* element_at(const document_data& d, const node* array, std::size_t id { return e + idx; // a growable block: one link per element } + if (array->next == array->len + 1u && (!nav::editable() || (array->flags & node_flags::moved) == 0)) + { + return e + idx; // no element has a subtree: the elements are the nodes after the array + } for (std::size_t i = 0; i < idx; ++i) { e = document_data::after(e); @@ -181,6 +185,10 @@ const node* last_child(const document_data& d, const node* container) noexcept const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; const node* const end = navigation::end(d, container); const node* last = navigation::first(d, container); + if (value_offset == 0 && container->next == container->len + 1u && (!navigation::editable() || (container->flags & node_flags::moved) == 0)) + { + return last + (container->len - 1u); // an array without subtrees: the elements are the nodes after it + } for (const node* c = document_data::after(last + value_offset); c != end; c = document_data::after(c + value_offset)) { last = c; diff --git a/include/nlohmann/detail/view/materialize.hpp b/include/nlohmann/detail/view/materialize.hpp index b15bec551..deebbc098 100644 --- a/include/nlohmann/detail/view/materialize.hpp +++ b/include/nlohmann/detail/view/materialize.hpp @@ -8,7 +8,7 @@ #pragma once -#include // max +#include // array #include // size_t #include // int64_t, uint8_t #include // string @@ -44,8 +44,9 @@ BasicJsonType materialize(const document_data& d, const node* n) using nav = navigation; // the open containers: their next element (or key of the next member), - // their end, and whether they are objects; sized once from the - // document's deepest nesting (for a subtree an upper bound) + // their end, and whether they are objects; for the first 16 levels on the + // stack, else sized once from the document's deepest nesting (for a + // subtree an upper bound) struct frame { const node* pos; @@ -53,10 +54,43 @@ BasicJsonType materialize(const document_data& d, const node* n) bool object; }; + // a scalar needs neither the handler nor the stack of open containers + switch (static_cast(n->kind)) + { + case value_t::string: + return BasicJsonType(string_t(d.str(*n), n->len)); + case value_t::number_integer: + return BasicJsonType(static_cast(static_cast(integer_bits(*n)))); + case value_t::number_unsigned: + return BasicJsonType(static_cast(integer_bits(*n))); + case value_t::number_float: + return BasicJsonType(float_value(d, *n)); + case value_t::boolean: + return BasicJsonType((n->flags & node_flags::is_true) != 0); + case value_t::null: + return BasicJsonType(nullptr); + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + BasicJsonType result; sax_t sax(result, true); const string_t no_token{}; - std::vector open((std::max)(d.max_depth, std::size_t{1})); + // (shallow subtrees on the stack: a call for a small value then allocates only for the value) + std::array shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open + std::vector deep{}; + frame* open = shallow.data(); + std::size_t capacity = shallow.size(); + if (d.max_depth > capacity) + { + deep.resize(d.max_depth); + open = deep.data(); + capacity = deep.size(); + } std::size_t depth = 0; for (;;) { @@ -76,9 +110,13 @@ BasicJsonType materialize(const document_data& d, const node* n) { sax.start_array(n->len); } - if (NLOHMANN_VIEW_UNLIKELY(depth == open.size())) + if (NLOHMANN_VIEW_UNLIKELY(depth == capacity)) { - open.resize(depth * 2); // (deeper than the recorded nesting, e.g. after edits) + // (deeper than the recorded nesting, e.g. after edits) + deep.assign(open, open + depth); + deep.resize(depth * 2); + open = deep.data(); + capacity = deep.size(); } open[depth++] = frame{nav::first(d, n), nav::end(d, n), object}; break; diff --git a/include/nlohmann/detail/view/object_index.hpp b/include/nlohmann/detail/view/object_index.hpp index 5c7551fe9..9b0ba63ea 100644 --- a/include/nlohmann/detail/view/object_index.hpp +++ b/include/nlohmann/detail/view/object_index.hpp @@ -47,27 +47,96 @@ constexpr std::size_t index_max_displacement = 64; static_assert(document_data::index_min_members >= 2 * index_max_displacement, "a table must be much larger than the probe window of a lookup"); -/// hash of a key: its bytes, eight at a time, in a fixed byte order +/// hash of a key: its bytes, eight at a time (in a fixed byte order), where the +/// last word may overlap the one before (so that no loop over single bytes is +/// needed); a key of fewer than eight bytes is read as two overlapping words of +/// four or two bytes (in the native byte order: the hash is not stored, and the +/// same on both sides of a lookup) inline std::uint64_t key_hash(const char* s, std::size_t n) noexcept { std::uint64_t h = 0x9E3779B97F4A7C15u * (n + 1); const auto* p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - while (n >= 8) - { - h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; - h ^= h >> 29u; - p += 8; - n -= 8; - } std::uint64_t w = 0; - for (std::size_t i = 0; i < n; ++i) + if (n >= 8) { - w |= static_cast(p[i]) << (8u * i); + const unsigned char* const last = p + n - 8; + while (p < last) + { + h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; + h ^= h >> 29u; + p += 8; + } + w = read_eight_bytes(last); } - h = (h ^ w) * 0x94D049BB133111EBu; + else if (n >= 4) + { + std::uint32_t head = 0; + std::uint32_t tail = 0; + std::memcpy(&head, p, 4); + std::memcpy(&tail, p + n - 4, 4); + w = static_cast(head) | (static_cast(tail) << 32u); + } + else if (n >= 2) + { + std::uint16_t head = 0; + std::uint16_t tail = 0; + std::memcpy(&head, p, 2); + std::memcpy(&tail, p + n - 2, 2); + w = static_cast(head) | (static_cast(tail) << 16u); + } + else if (n == 1) + { + w = p[0]; + } + // (a product only carries bits upwards, and the slot is taken from the low + // bits: the high bits of the last word are folded down before the multiplication) + h = (h ^ w ^ (w >> 29u)) * 0x94D049BB133111EBu; return h ^ (h >> 31u); } +/// whether the n <= 16 bytes at a and b are equal, as two overlapping words +/// (the first and the last 8, 4, or 2 bytes) and without a call +NLOHMANN_VIEW_ALWAYS_INLINE bool same_short_bytes(const unsigned char* a, const unsigned char* b, std::size_t n) noexcept +{ + if (n >= 8) + { + std::uint64_t x0 = 0; + std::uint64_t x1 = 0; + std::uint64_t y0 = 0; + std::uint64_t y1 = 0; + std::memcpy(&x0, a, 8); + std::memcpy(&x1, a + n - 8, 8); + std::memcpy(&y0, b, 8); + std::memcpy(&y1, b + n - 8, 8); + return x0 == y0 && x1 == y1; + } + if (n >= 4) + { + std::uint32_t x0 = 0; + std::uint32_t x1 = 0; + std::uint32_t y0 = 0; + std::uint32_t y1 = 0; + std::memcpy(&x0, a, 4); + std::memcpy(&x1, a + n - 4, 4); + std::memcpy(&y0, b, 4); + std::memcpy(&y1, b + n - 4, 4); + return x0 == y0 && x1 == y1; + } + if (n >= 2) + { + std::uint16_t x0 = 0; + std::uint16_t x1 = 0; + std::uint16_t y0 = 0; + std::uint16_t y1 = 0; + std::memcpy(&x0, a, 2); + std::memcpy(&x1, a + n - 2, 2); + std::memcpy(&y0, b, 2); + std::memcpy(&y1, b + n - 2, 2); + return x0 == y0 && x1 == y1; + } + return n == 0 || *a == *b; +} + /// build the table of a large object inline void build_object_index(document_data& d, node* obj) { @@ -132,6 +201,27 @@ inline const node* find_indexed(const document_data& d, const node* obj, const c const document_data::object_index& ix = d.indexes[obj->extra - 1u]; const std::uint32_t* const table = d.index_slots.data() + ix.start; std::size_t slot = conditional_static_cast(key_hash(key, n)) & ix.mask; + const auto* const k_bytes = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // (the loop for longer keys comes first, as the code is laid out; it is not the more common case) + if (NLOHMANN_VIEW_UNLIKELY(n <= 16)) + { + // (up to 16 bytes, the bytes are compared as two words, without a call) + for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) + { + const std::uint32_t s = table[slot]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (k->len == n && same_short_bytes(reinterpret_cast(d.str(*k)), k_bytes, n)) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return k; + } + slot = (slot + 1) & ix.mask; + } + return nullptr; + } for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) { const std::uint32_t s = table[slot]; diff --git a/include/nlohmann/detail/view/simd.hpp b/include/nlohmann/detail/view/simd.hpp index b83cd5a3b..7a0248f63 100644 --- a/include/nlohmann/detail/view/simd.hpp +++ b/include/nlohmann/detail/view/simd.hpp @@ -116,6 +116,34 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned } return p; } + +/*! +@brief copy 16 bytes from p to w, and find the first quote, backslash, +control character, or non-ASCII byte among them + +The bytes are copied whether or not they are all plain (the caller has room +for 16 bytes and keeps only the plain ones). + +@return the offset of that byte, or 16 if there is none +*/ +NLOHMANN_VIEW_ALWAYS_INLINE unsigned copy_plain_run16(const unsigned char* p, char* w) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + vst1q_u8(reinterpret_cast(w), in); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + return bits != 0 ? static_cast(count_trailing_zeros(bits) >> 2u) : 16u; +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + _mm_storeu_si128(static_cast<__m128i*>(static_cast(w)), in); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + return bits != 0 ? static_cast(count_trailing_zeros(bits)) : 16u; +#endif +} #endif #if NLOHMANN_VIEW_SSSE3_DISPATCH diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index f0fc58dc9..86481d884 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -30,6 +30,7 @@ #include // array #include // nullptr_t, size_t // IWYU pragma: keep #include // uint8_t, uint32_t +#include // FILE #include // memcpy, strlen #include // istream // IWYU pragma: keep #include // distance, input_iterator_tag, iterator_traits @@ -295,7 +296,7 @@ class basic_json_view } detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } - return idx < m_node->len ? basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))) : basic_json_view(); + return idx < m_node->len ? element(idx) : basic_json_view(); } /// any other integer type (int, unsigned, long, std::int64_t, ...; a @@ -358,7 +359,7 @@ class basic_json_view { detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); } - return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + return element(idx); } /// any other integer type, see operator[]; negative values are out of @@ -392,7 +393,13 @@ class basic_json_view string_t value(string_view_t key, const char* default_value) const { - return value(key, string_t(default_value)); + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + // (the default is made only if there is no such member) + const basic_json_view r = lookup(key); + return r.is_discarded() ? string_t(default_value) : r.template get(); } /// the value a JSON pointer refers to converted to T, or the default @@ -411,7 +418,12 @@ class basic_json_view string_t value(const json_pointer& ptr, const char* default_value) const { - return value(ptr, string_t(default_value)); + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value); + return r.is_discarded() ? string_t(default_value) : r.template get(); } /// the first element or member value; a primitive value itself. Throws @@ -433,7 +445,8 @@ class basic_json_view { if (is_structured() && m_node->len != 0) { - return basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -450,7 +463,7 @@ class basic_json_view { return end(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = find_key(key); return k != nullptr ? iterator(m_doc, k, true) : end(); } @@ -467,7 +480,7 @@ class basic_json_view /// whether this is an object with a member with this key bool contains(string_view_t key) const { - return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; + return is_object() && find_key(key) != nullptr; } bool contains(const char* key) const @@ -627,7 +640,14 @@ class basic_json_view // the compact writer keeps 64 bytes of slack, so that it does not grow // the buffer just before the end const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160; - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + if (unedited()) + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + else + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } return out; } @@ -707,7 +727,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return detail::view::materialize(*m_doc, m_node); + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -785,10 +805,50 @@ class basic_json_view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { + if (unedited()) + { + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + } const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } + /// whether the document has not been edited yet: until the first edit, its index is walked like that of a + /// read-only document (nothing is moved or linked), which is cheaper + NLOHMANN_VIEW_ALWAYS_INLINE bool unedited() const noexcept + { + return !navigation::editable() || m_doc->edits == nullptr; + } + + /// the member of this object with @a key (the last one if the key occurs more than once), or nullptr + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key) const + { + return find_key(key, std::integral_constant {}); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::true_type /*editable*/) const + { + // (an editable document without edits is looked up as one that cannot be edited) + return unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::false_type /*editable*/) const + { + return detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + /// the element of an array at an index below its size + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view element(size_type idx) const + { + if (unedited()) + { + return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); + } + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + } + // --- get() dispatch --- bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const @@ -1528,7 +1588,8 @@ class basic_json_document // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 append_input_impl(buf, std::forward(input), std::integral_constant < int, detail::is_contiguous_byte_container::value ? 0 : std::is_base_of::value ? 1 - : detail::view::is_adapter_input::value ? 2 : 3 > {}); + : detail::view::is_adapter_input::value ? 2 + : detail::view::is_c_stream::value ? 4 : 3 > {}); // NOLINTEND(readability-avoid-nested-conditional-operator) } @@ -1550,6 +1611,19 @@ class basic_json_document detail::view::append_adapter(buf, std::move(input.ia)); } +#ifndef JSON_NO_IO + static void append_input_impl(std::string& buf, std::FILE* file, std::integral_constant /*C stream*/) + { + if (file == nullptr) + { + // (as before: the library's adapter decides what a null stream yields) + detail::view::append_adapter(buf, detail::input_adapter(file)); + return; + } + detail::view::append_file(buf, file); + } +#endif + template static void append_input_impl(std::string& buf, T&& input, std::integral_constant /*other*/) { diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 7b058469d..65459b072 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -30,6 +30,7 @@ #include // array #include // nullptr_t, size_t // IWYU pragma: keep #include // uint8_t, uint32_t +#include // FILE #include // memcpy, strlen #include // istream // IWYU pragma: keep #include // distance, input_iterator_tag, iterator_traits @@ -756,6 +757,34 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned } return p; } + +/*! +@brief copy 16 bytes from p to w, and find the first quote, backslash, +control character, or non-ASCII byte among them + +The bytes are copied whether or not they are all plain (the caller has room +for 16 bytes and keeps only the plain ones). + +@return the offset of that byte, or 16 if there is none +*/ +NLOHMANN_VIEW_ALWAYS_INLINE unsigned copy_plain_run16(const unsigned char* p, char* w) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + vst1q_u8(reinterpret_cast(w), in); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + return bits != 0 ? static_cast(count_trailing_zeros(bits) >> 2u) : 16u; +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + _mm_storeu_si128(static_cast<__m128i*>(static_cast(w)), in); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + return bits != 0 ? static_cast(count_trailing_zeros(bits)) : 16u; +#endif +} #endif #if NLOHMANN_VIEW_SSSE3_DISPATCH @@ -1437,11 +1466,34 @@ class builder 0, 0, '\b', 0, 0, 0, '\f', 0, 0, 0, 0, 0, 0, 0, '\n', 0, 0, 0, '\r', 0, '\t', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }; + // The write position is kept in locals: the stores of characters into + // the arena may alias the members of the builder, which would then be + // reloaded after every one. It is stored back where the arena grows, + // and by `write_back` on every return. + const unsigned char* const end = m_end; const std::size_t start = arena_used(); - arena_run(s, static_cast(p - s)); + char* w = m_arena_write; + const char* w_end = m_arena_end; + const arena_write_back write_back(*this, w); + { + const auto n = static_cast(p - s); + if (n <= 16 && end - s >= 16) + { + arena_ensure(w, w_end, 16); + std::memcpy(w, s, 16); + } + else if (n != 0) + { + // (an empty run leaves an empty arena unallocated, and memcpy() + // must not be passed its null pointer even for zero bytes) + arena_ensure(w, w_end, n); + std::memcpy(w, s, n); + } + w += n; + } for (;;) { - if (p == m_end) + if (p == end) { return failed(error_code::string_missing_quote, p); } @@ -1449,7 +1501,9 @@ class builder if (c == '"') { ++p; - return decoded{p, start, arena_used() - start}; + // (from w: `write_back` stores it only after the return value + // is made; the arena is allocated, since an escape was written) + return decoded{p, start, static_cast(w - m_doc.arena.data()) - start}; } if (c != '\\') { @@ -1459,7 +1513,7 @@ class builder return failed(c < 0x20 ? error_code::string_control_character : error_code::string_utf8, p); } ++p; - if (p == m_end) + if (p == end) { return failed(error_code::string_missing_quote, p); } @@ -1477,7 +1531,7 @@ class builder { return failed(error_code::string_surrogate_low, p); } - if (m_end - p < 2 || p[0] != '\\' || p[1] != 'u') + if (end - p < 2 || p[0] != '\\' || p[1] != 'u') { return failed(error_code::string_surrogate_high, p); } @@ -1493,26 +1547,106 @@ class builder } cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } - arena_ensure(4); - m_arena_write = put_utf8(m_arena_write, cp); + arena_ensure(w, w_end, 4); + w = put_utf8(w, cp); } else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) { - arena_ensure(1); - *m_arena_write++ = simple_escape[d]; + arena_ensure(w, w_end, 1); + *w++ = simple_escape[d]; } else { --p; return failed(error_code::string_escape, p); } - if (p != m_end && *p == '\\') +#if NLOHMANN_VIEW_NEON + if (p != end && *p == '\\') { continue; // consecutive escapes ("\u00e4\u00f6"): no run in between } + // The first 16 bytes of the run are checked one by one, in predicted branches, and copied + // as one block: where the runs between escapes are short, the vector step below would put + // its longer dependency chain (load, compare, mask, count, add) on p for each of them, + // and be slower than these steps (the scanner of the keys works the same way) + { + if (end - p >= 16) + { + const std::uint8_t* const plain = string_plain(); + std::size_t steps = 16; +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { steps = (i); goto run_found; } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP +run_found: + arena_ensure(w, w_end, 16); + std::memcpy(w, p, 16); // (only the first `steps` bytes are kept) + w += steps; + p += steps; + if (steps < 16) + { + if (*p >= 0x80) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + continue; + } + } + } +#else + if (p != end && (*p == '\\' || *p == '"')) + { + continue; // consecutive escapes ("\u00e4\u00f6"), or the end of the string: no run in between + } +#endif +#if NLOHMANN_VIEW_VECTOR + // the plain bytes up to the next quote, backslash, or control + // character are copied as they are scanned, 16 bytes at a time + for (;;) + { + if (NLOHMANN_VIEW_UNLIKELY(end - p < 16)) + { + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + break; + } + arena_ensure(w, w_end, 16); + const unsigned k = copy_plain_run16(p, w); + w += k; + p += k; + if (k == 16) + { + continue; + } + if (NLOHMANN_VIEW_UNLIKELY(*p >= 0x80)) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + break; + } +#else const unsigned char* const r = p; - p = scan_string_run(p, m_end); - arena_run(r, static_cast(p - r)); + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; +#endif } } @@ -1554,19 +1688,49 @@ class builder char* m_arena_write = nullptr; char* m_arena_end = nullptr; + /// room for n more bytes at w, the write position kept in a local, with + /// w_end the end of the arena. (A lambda instead is not always inlined, + /// and then w would be kept in memory, as m_arena_write is.) + NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(char*& w, const char*& w_end, std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < n)) + { + m_arena_write = w; + arena_grow(n); + w = m_arena_write; + w_end = m_arena_end; + } + } + + /// stores a write position kept in a local back into m_arena_write when + /// the function that keeps it returns, on every path + class arena_write_back + { + public: + arena_write_back(builder& b, char*& w) noexcept + : m_builder(b) + , m_w(w) + {} + ~arena_write_back() + { + m_builder.m_arena_write = m_w; + } + + arena_write_back(const arena_write_back&) = delete; + arena_write_back& operator=(const arena_write_back&) = delete; + arena_write_back(arena_write_back&&) = delete; + arena_write_back& operator=(arena_write_back&&) = delete; + + private: + builder& m_builder; + char*& m_w; + }; + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept { return m_arena_write != nullptr ? static_cast(m_arena_write - m_doc.arena.data()) : 0; } - NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(std::size_t n) - { - if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_arena_end - m_arena_write) < n)) - { - arena_grow(n); - } - } - NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n) { const std::size_t used = arena_used(); @@ -1575,25 +1739,6 @@ class builder m_arena_end = m_doc.arena.data() + m_doc.arena.size(); } - /// append the run [r, r + n) to the arena; short runs as one fixed-size - /// 16-byte move when both sides have the room (no library call) - NLOHMANN_VIEW_ALWAYS_INLINE void arena_run(const unsigned char* r, std::size_t n) - { - if (n <= 16 && m_end - r >= 16) - { - arena_ensure(16); - std::memcpy(m_arena_write, r, 16); - } - else if (n != 0) - { - // (an empty run leaves an empty arena unallocated, and memcpy() - // must not be passed its null pointer even for zero bytes) - arena_ensure(n); - std::memcpy(m_arena_write, r, n); - } - m_arena_write += n; - } - /// UTF-8 encoding of cp at w (room for 4 bytes); returns the position after /// it. (A returned pointer stays in a register; writing through a /// reference to m_arena_write would store and reload it around every byte.) @@ -2116,11 +2261,11 @@ indent_done: ++p; } const unsigned char* const int_start = p; - if (p != e && *p == '0') + if (cur() == '0') { ++p; } - else if (NLOHMANN_VIEW_LIKELY(p != e && *p >= '1' && *p <= '9')) + else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) { p = skip_digits(p + 1, e); } @@ -2131,7 +2276,7 @@ indent_done: const auto int_digits = static_cast(p - int_start); std::size_t frac_digits = 0; bool is_float = false; - if (p != e && *p == '.') + if (cur() == '.') { ++p; const unsigned char* const f0 = p; @@ -2144,20 +2289,20 @@ indent_done: is_float = true; } std::int64_t exponent = 0; - if (p != e && (*p | 0x20) == 'e') + if ((cur() | 0x20) == 'e') { ++p; bool exp_negative = false; - if (p != e && (*p == '+' || *p == '-')) + if (cur() == '+' || cur() == '-') { exp_negative = *p == '-'; ++p; } - if (NLOHMANN_VIEW_UNLIKELY(p == e || !is_digit(*p))) + if (NLOHMANN_VIEW_UNLIKELY(!is_digit(cur()))) { return fail(error_code::number_after_exponent); } - while (p != e && is_digit(*p)) + while (is_digit(cur())) { if (exponent < 100000) { @@ -2219,7 +2364,7 @@ indent_done: ++p; // opening quote const unsigned char* const s = p; p = scan_string_run(p, e); - if (NLOHMANN_VIEW_LIKELY(p != e && *p == '"')) + if (NLOHMANN_VIEW_LIKELY(cur() == '"')) { push_node(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); ++p; @@ -3168,27 +3313,96 @@ constexpr std::size_t index_max_displacement = 64; static_assert(document_data::index_min_members >= 2 * index_max_displacement, "a table must be much larger than the probe window of a lookup"); -/// hash of a key: its bytes, eight at a time, in a fixed byte order +/// hash of a key: its bytes, eight at a time (in a fixed byte order), where the +/// last word may overlap the one before (so that no loop over single bytes is +/// needed); a key of fewer than eight bytes is read as two overlapping words of +/// four or two bytes (in the native byte order: the hash is not stored, and the +/// same on both sides of a lookup) inline std::uint64_t key_hash(const char* s, std::size_t n) noexcept { std::uint64_t h = 0x9E3779B97F4A7C15u * (n + 1); const auto* p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - while (n >= 8) - { - h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; - h ^= h >> 29u; - p += 8; - n -= 8; - } std::uint64_t w = 0; - for (std::size_t i = 0; i < n; ++i) + if (n >= 8) { - w |= static_cast(p[i]) << (8u * i); + const unsigned char* const last = p + n - 8; + while (p < last) + { + h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; + h ^= h >> 29u; + p += 8; + } + w = read_eight_bytes(last); } - h = (h ^ w) * 0x94D049BB133111EBu; + else if (n >= 4) + { + std::uint32_t head = 0; + std::uint32_t tail = 0; + std::memcpy(&head, p, 4); + std::memcpy(&tail, p + n - 4, 4); + w = static_cast(head) | (static_cast(tail) << 32u); + } + else if (n >= 2) + { + std::uint16_t head = 0; + std::uint16_t tail = 0; + std::memcpy(&head, p, 2); + std::memcpy(&tail, p + n - 2, 2); + w = static_cast(head) | (static_cast(tail) << 16u); + } + else if (n == 1) + { + w = p[0]; + } + // (a product only carries bits upwards, and the slot is taken from the low + // bits: the high bits of the last word are folded down before the multiplication) + h = (h ^ w ^ (w >> 29u)) * 0x94D049BB133111EBu; return h ^ (h >> 31u); } +/// whether the n <= 16 bytes at a and b are equal, as two overlapping words +/// (the first and the last 8, 4, or 2 bytes) and without a call +NLOHMANN_VIEW_ALWAYS_INLINE bool same_short_bytes(const unsigned char* a, const unsigned char* b, std::size_t n) noexcept +{ + if (n >= 8) + { + std::uint64_t x0 = 0; + std::uint64_t x1 = 0; + std::uint64_t y0 = 0; + std::uint64_t y1 = 0; + std::memcpy(&x0, a, 8); + std::memcpy(&x1, a + n - 8, 8); + std::memcpy(&y0, b, 8); + std::memcpy(&y1, b + n - 8, 8); + return x0 == y0 && x1 == y1; + } + if (n >= 4) + { + std::uint32_t x0 = 0; + std::uint32_t x1 = 0; + std::uint32_t y0 = 0; + std::uint32_t y1 = 0; + std::memcpy(&x0, a, 4); + std::memcpy(&x1, a + n - 4, 4); + std::memcpy(&y0, b, 4); + std::memcpy(&y1, b + n - 4, 4); + return x0 == y0 && x1 == y1; + } + if (n >= 2) + { + std::uint16_t x0 = 0; + std::uint16_t x1 = 0; + std::uint16_t y0 = 0; + std::uint16_t y1 = 0; + std::memcpy(&x0, a, 2); + std::memcpy(&x1, a + n - 2, 2); + std::memcpy(&y0, b, 2); + std::memcpy(&y1, b + n - 2, 2); + return x0 == y0 && x1 == y1; + } + return n == 0 || *a == *b; +} + /// build the table of a large object inline void build_object_index(document_data& d, node* obj) { @@ -3253,6 +3467,27 @@ inline const node* find_indexed(const document_data& d, const node* obj, const c const document_data::object_index& ix = d.indexes[obj->extra - 1u]; const std::uint32_t* const table = d.index_slots.data() + ix.start; std::size_t slot = conditional_static_cast(key_hash(key, n)) & ix.mask; + const auto* const k_bytes = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // (the loop for longer keys comes first, as the code is laid out; it is not the more common case) + if (NLOHMANN_VIEW_UNLIKELY(n <= 16)) + { + // (up to 16 bytes, the bytes are compared as two words, without a call) + for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) + { + const std::uint32_t s = table[slot]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (k->len == n && same_short_bytes(reinterpret_cast(d.str(*k)), k_bytes, n)) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return k; + } + slot = (slot + 1) & ix.mask; + } + return nullptr; + } for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) { const std::uint32_t s = table[slot]; @@ -3421,6 +3656,10 @@ const node* element_at(const document_data& d, const node* array, std::size_t id { return e + idx; // a growable block: one link per element } + if (array->next == array->len + 1u && (!nav::editable() || (array->flags & node_flags::moved) == 0)) + { + return e + idx; // no element has a subtree: the elements are the nodes after the array + } for (std::size_t i = 0; i < idx; ++i) { e = document_data::after(e); @@ -3436,6 +3675,10 @@ const node* last_child(const document_data& d, const node* container) noexcept const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; const node* const end = navigation::end(d, container); const node* last = navigation::first(d, container); + if (value_offset == 0 && container->next == container->len + 1u && (!navigation::editable() || (container->flags & node_flags::moved) == 0)) + { + return last + (container->len - 1u); // an array without subtrees: the elements are the nodes after it + } for (const node* c = document_data::after(last + value_offset); c != end; c = document_data::after(c + value_offset)) { last = c; @@ -5301,6 +5544,9 @@ NLOHMANN_JSON_NAMESPACE_END #include // max #include // size_t +#ifndef JSON_NO_IO + #include // FILE, fread +#endif #include // istream, streambuf #include // basic_string, char_traits, string #include // decay, integral_constant, is_array, is_const, is_integral, is_lvalue_reference, is_pointer, is_same, remove_cv, remove_extent, remove_pointer, remove_reference @@ -5446,6 +5692,37 @@ inline void append_stream(std::string& buf, std::istream& is) is.clear(is.rdstate() | std::ios::eofbit); } +/// a pointer to a C stream (never with JSON_NO_IO, which leaves out ) +template +struct is_c_stream +#ifndef JSON_NO_IO + : std::is_same +#else + : std::false_type +#endif +{}; + +#ifndef JSON_NO_IO +/// append the rest of a C stream: in blocks with fread, not character by +/// character (the library's file adapter reads with fgetc); +/// f is not null +inline void append_file(std::string& buf, std::FILE* f) +{ + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::size_t got = std::fread(&buf[used], 1, room, f); + buf.resize(used + got); + if (got < room) + { + break; // end of file, or an error (as fgetc, which returns EOF for both) + } + } +} +#endif + } // namespace view } // namespace detail NLOHMANN_JSON_NAMESPACE_END @@ -5604,20 +5881,21 @@ class view_item view_item(const iterator& it, std::size_t index) : m_it(it) - { - if (!it.is_object_iterator()) - { - m_index = std::to_string(index); - } - } + , m_position(index) + {} - /// the member key, or the element index for arrays + /// the member key, or the element index for arrays (as text made on the + /// first call, so that iterating over the values pays nothing for it) string_view_t key() const { if (m_it.is_object_iterator()) { return m_it.key(); } + if (m_index.empty()) + { + m_index = std::to_string(m_position); + } return string_view_t(m_index.data(), m_index.size()); } @@ -5640,7 +5918,8 @@ class view_item private: iterator m_it; - std::string m_index{}; // NOLINT(readability-redundant-member-init) + std::size_t m_position; ///< the index of an array element + mutable std::string m_index{}; ///< the text of m_position, once key() was called // NOLINT(readability-redundant-member-init) }; /// the range returned by basic_json_view::items() @@ -5734,7 +6013,7 @@ NLOHMANN_JSON_NAMESPACE_END -#include // max +#include // array #include // size_t #include // int64_t, uint8_t #include // string @@ -5774,8 +6053,9 @@ BasicJsonType materialize(const document_data& d, const node* n) using nav = navigation; // the open containers: their next element (or key of the next member), - // their end, and whether they are objects; sized once from the - // document's deepest nesting (for a subtree an upper bound) + // their end, and whether they are objects; for the first 16 levels on the + // stack, else sized once from the document's deepest nesting (for a + // subtree an upper bound) struct frame { const node* pos; @@ -5783,10 +6063,43 @@ BasicJsonType materialize(const document_data& d, const node* n) bool object; }; + // a scalar needs neither the handler nor the stack of open containers + switch (static_cast(n->kind)) + { + case value_t::string: + return BasicJsonType(string_t(d.str(*n), n->len)); + case value_t::number_integer: + return BasicJsonType(static_cast(static_cast(integer_bits(*n)))); + case value_t::number_unsigned: + return BasicJsonType(static_cast(integer_bits(*n))); + case value_t::number_float: + return BasicJsonType(float_value(d, *n)); + case value_t::boolean: + return BasicJsonType((n->flags & node_flags::is_true) != 0); + case value_t::null: + return BasicJsonType(nullptr); + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + BasicJsonType result; sax_t sax(result, true); const string_t no_token{}; - std::vector open((std::max)(d.max_depth, std::size_t{1})); + // (shallow subtrees on the stack: a call for a small value then allocates only for the value) + std::array shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open + std::vector deep{}; + frame* open = shallow.data(); + std::size_t capacity = shallow.size(); + if (d.max_depth > capacity) + { + deep.resize(d.max_depth); + open = deep.data(); + capacity = deep.size(); + } std::size_t depth = 0; for (;;) { @@ -5806,9 +6119,13 @@ BasicJsonType materialize(const document_data& d, const node* n) { sax.start_array(n->len); } - if (NLOHMANN_VIEW_UNLIKELY(depth == open.size())) + if (NLOHMANN_VIEW_UNLIKELY(depth == capacity)) { - open.resize(depth * 2); // (deeper than the recorded nesting, e.g. after edits) + // (deeper than the recorded nesting, e.g. after edits) + deep.assign(open, open + depth); + deep.resize(depth * 2); + open = deep.data(); + capacity = deep.size(); } open[depth++] = frame{nav::first(d, n), nav::end(d, n), object}; break; @@ -7708,7 +8025,7 @@ class basic_json_view } detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } - return idx < m_node->len ? basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))) : basic_json_view(); + return idx < m_node->len ? element(idx) : basic_json_view(); } /// any other integer type (int, unsigned, long, std::int64_t, ...; a @@ -7771,7 +8088,7 @@ class basic_json_view { detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); } - return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + return element(idx); } /// any other integer type, see operator[]; negative values are out of @@ -7805,7 +8122,13 @@ class basic_json_view string_t value(string_view_t key, const char* default_value) const { - return value(key, string_t(default_value)); + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + // (the default is made only if there is no such member) + const basic_json_view r = lookup(key); + return r.is_discarded() ? string_t(default_value) : r.template get(); } /// the value a JSON pointer refers to converted to T, or the default @@ -7824,7 +8147,12 @@ class basic_json_view string_t value(const json_pointer& ptr, const char* default_value) const { - return value(ptr, string_t(default_value)); + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value); + return r.is_discarded() ? string_t(default_value) : r.template get(); } /// the first element or member value; a primitive value itself. Throws @@ -7846,7 +8174,8 @@ class basic_json_view { if (is_structured() && m_node->len != 0) { - return basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -7863,7 +8192,7 @@ class basic_json_view { return end(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = find_key(key); return k != nullptr ? iterator(m_doc, k, true) : end(); } @@ -7880,7 +8209,7 @@ class basic_json_view /// whether this is an object with a member with this key bool contains(string_view_t key) const { - return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; + return is_object() && find_key(key) != nullptr; } bool contains(const char* key) const @@ -8040,7 +8369,14 @@ class basic_json_view // the compact writer keeps 64 bytes of slack, so that it does not grow // the buffer just before the end const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160; - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + if (unedited()) + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + else + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } return out; } @@ -8120,7 +8456,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return detail::view::materialize(*m_doc, m_node); + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -8198,10 +8534,50 @@ class basic_json_view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { + if (unedited()) + { + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + } const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } + /// whether the document has not been edited yet: until the first edit, its index is walked like that of a + /// read-only document (nothing is moved or linked), which is cheaper + NLOHMANN_VIEW_ALWAYS_INLINE bool unedited() const noexcept + { + return !navigation::editable() || m_doc->edits == nullptr; + } + + /// the member of this object with @a key (the last one if the key occurs more than once), or nullptr + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key) const + { + return find_key(key, std::integral_constant {}); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::true_type /*editable*/) const + { + // (an editable document without edits is looked up as one that cannot be edited) + return unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::false_type /*editable*/) const + { + return detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + /// the element of an array at an index below its size + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view element(size_type idx) const + { + if (unedited()) + { + return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); + } + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + } + // --- get() dispatch --- bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const @@ -8941,7 +9317,8 @@ class basic_json_document // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 append_input_impl(buf, std::forward(input), std::integral_constant < int, detail::is_contiguous_byte_container::value ? 0 : std::is_base_of::value ? 1 - : detail::view::is_adapter_input::value ? 2 : 3 > {}); + : detail::view::is_adapter_input::value ? 2 + : detail::view::is_c_stream::value ? 4 : 3 > {}); // NOLINTEND(readability-avoid-nested-conditional-operator) } @@ -8963,6 +9340,19 @@ class basic_json_document detail::view::append_adapter(buf, std::move(input.ia)); } +#ifndef JSON_NO_IO + static void append_input_impl(std::string& buf, std::FILE* file, std::integral_constant /*C stream*/) + { + if (file == nullptr) + { + // (as before: the library's adapter decides what a null stream yields) + detail::view::append_adapter(buf, detail::input_adapter(file)); + return; + } + detail::view::append_file(buf, file); + } +#endif + template static void append_input_impl(std::string& buf, T&& input, std::integral_constant /*other*/) { diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index caadee629..e87162d98 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -185,6 +185,33 @@ TEST_CASE("json_view") // duplicate keys: the last value, at the position of the first key CHECK(materialized(R"({"a":1,"b":2,"a":3})") == json::parse(R"({"a":1,"b":2,"a":3})")); CHECK(materialized(R"({"a":1,"b":2,"a":3})").dump() == R"({"a":3,"b":2})"); + // scalars (made without the handler), and nesting around the depth up to which the stack of open containers is local + for (const char* scalar : + {"null", "true", "false", "0", "-7", "18446744073709551615", "1.5e300", "-0.0", "\"\"", "\"short\"", "\"a string that is longer than fifteen bytes\"", R"("\u00e4\n")" + }) + { + CAPTURE(scalar) + CHECK(materialized(scalar) == json::parse(scalar)); + const std::string in_array = std::string("[1,") + scalar + "]"; + const json_document d = json_document::parse(in_array); + CHECK(d.root()[1].materialize() == json::parse(scalar)); + CHECK(d.root()[1].get() == json::parse(scalar)); + } + for (const std::size_t levels : + { + std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{32}, std::size_t{33} + }) + { + CAPTURE(levels) + const std::string nested = std::string(levels, '[') + R"({"k":[1,{"j":2}]})" + std::string(levels, ']'); + CHECK(materialized(nested) == json::parse(nested)); + std::string wide = R"({"a":)"; + wide += nested; + wide += R"(,"b":)"; + wide += nested; + wide += "}"; + CHECK(materialized(wide) == json::parse(wide)); + } // very deep nesting (iterative, as parse()) const std::string deep = std::string(100000, '[') + std::string(100000, ']'); // (one check per step, so that a failure shows which step it is) @@ -410,6 +437,31 @@ TEST_CASE("json_view") std::istringstream rvalue_source("[1]"); CHECK(json_document::parse_copy(std::move(rvalue_source)).owns_source()); + // a C stream is read in blocks as well (more than one block, and a + // text whose length is a multiple of the block size) + for (const std::size_t extra : + { + std::size_t{0}, std::size_t{1} + }) + { + std::FILE* long_file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory) + if (long_file != nullptr) + { + std::string padded = text; + padded.append(((4096 - (padded.size() % 4096)) % 4096) + extra, ' '); + CHECK(std::fwrite(padded.data(), 1, padded.size(), long_file) == padded.size()); + CHECK(std::fseek(long_file, 0, SEEK_SET) == 0); + const json_document from_long_file = json_document::parse(long_file); + CHECK(from_long_file.source() == padded); + CHECK(from_long_file.root().materialize() == json::parse(text)); + CHECK(std::fclose(long_file) == 0); // NOLINT(cppcoreguidelines-owning-memory) + } + } + { + json_document from_null; + CHECK_THROWS_AS(from_null = json_document::parse(static_cast(nullptr)), json::parse_error&); + } + // a stream without a buffer is an empty input std::istream no_buffer(nullptr); CHECK(json_document::parse(no_buffer, false).is_discarded()); @@ -1006,6 +1058,49 @@ TEST_CASE("json_view element access and iteration") #endif } + SECTION("arrays whose elements have no subtrees: index and back()") + { + // (the elements are adjacent in the index; an array, an object, or an + // empty container anywhere makes the lookup skip element by element) + const std::vector texts = + { + "[]", "[1]", "[1,2,3]", "[1,\"two\",3.5,true,null,-7,18446744073709551615]", "[[]]", "[{}]", "[1,[2],3]", + "[1,2,{\"a\":3}]", "[[1,2],3,4]", "[1,2,3,[4,5,6,7,8],9,10,11,12]", "[{\"a\":[1]},2]", R"(["a\u00e4","b"])" + }; + for (const std::string& text : texts) + { + CAPTURE(text) + const json_document d = json_document::parse(text); + const json_view v = d.root(); + const json expected = json::parse(text); + for (std::size_t i = 0; i < expected.size(); ++i) + { + CHECK(v[i].materialize() == expected[i]); + CHECK(v.at(i).materialize() == expected[i]); + } + CHECK(v[expected.size()].is_discarded()); + if (!expected.empty()) + { + CHECK(v.back().materialize() == expected.back()); + } + } + // a long array (the direct lookup is not limited to short ones), also with a container at its end + std::string text = "[0"; + for (int i = 1; i < 1000; ++i) + { + text += "," + std::to_string(i); + } + const json_document scalars = json_document::parse(text + "]"); + CHECK(scalars.root()[999].get() == 999); + CHECK(scalars.root()[500].get() == 500); + CHECK(scalars.root().at(1).get() == 1); + CHECK(scalars.root().back().get() == 999); + const json_document container_last = json_document::parse(text + ",[1000]]"); + CHECK(container_last.root()[999].get() == 999); + CHECK(container_last.root()[1000][0].get() == 1000); + CHECK(container_last.root().back()[0].get() == 1000); + } + SECTION("iterators") { const json_document d = json_document::parse(R"({"x":[1,{"y":2}],"z":null})"); @@ -1045,6 +1140,28 @@ TEST_CASE("json_view element access and iteration") CHECK((*first++).key() == "0"); CHECK((*first).key() == "1"); CHECK(++first == items.end()); + + // the keys of array elements are their indexes, whether or not they are asked for + std::string numbers = "[0"; + for (int i = 1; i < 1234; ++i) + { + numbers += "," + std::to_string(i); + } + const json_document array = json_document::parse(numbers + "]"); + std::size_t position = 0; + for (const auto& item : array.root().items()) + { + CHECK(item.value().get() == position); + if (position % 3 != 0) // (not for every item: values alone do not make the key) + { + CHECK(item.key() == std::to_string(position)); + CHECK(item.key().data() == item.key().data()); // (the same text every time) + const json_view::item copy = item; + CHECK(copy.key() == std::to_string(position)); + } + ++position; + } + CHECK(position == 1234); #ifdef JSON_HAS_CPP_17 std::string pairs; for (const auto [key, value] : d.root().items()) @@ -1383,7 +1500,17 @@ TEST_CASE("json_view values") CHECK(exception_of([&] { static_cast(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast(j.value("s", 0)); }))); CHECK(exception_of([&] { static_cast(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", 0)); }))); CHECK(exception_of([&] { static_cast(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value(json::json_pointer("/x"), 0)); }))); + // string defaults: the member is not a string, or the value is neither an object nor an array + CHECK(exception_of([&] { static_cast(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast(j.value("n", "default")); }))); + CHECK(exception_of([&] { static_cast(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", "default")); }))); + CHECK(exception_of([&] { static_cast(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast(j.value(json::json_pointer("/n"), "default")); }))); + CHECK(exception_of([&] { static_cast(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast(j["s"].value(json::json_pointer("/x"), "default")); }))); #endif + // (defaults and members longer than the short string optimization) + CHECK(v.value("s", "a default longer than fifteen characters") == "text"); + CHECK(v.value("missing", "a default longer than fifteen characters") == "a default longer than fifteen characters"); + CHECK(v.value(json::json_pointer("/s"), "a default longer than fifteen characters") == "text"); + CHECK(v.value(json::json_pointer("/zz"), "a default longer than fifteen characters") == "a default longer than fifteen characters"); } } diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index b43af739d..6935981fb 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -523,6 +523,58 @@ TEST_CASE("json_view builder: strings across vector blocks") } } +TEST_CASE("json_view builder: escapes between runs of text") +{ + // The text between escapes is copied 16 bytes at a time where there are + // that many bytes left, and with the UTF-8 check where there is + // non-ASCII text. Runs of every length from 0 to 40 between valid and + // invalid escapes, UTF-8 sequences, and control characters, in strings + // that are cut at all places near the end of the text. + const std::array valid = {{"\\n", "\\\"", "\\\\", "\\/", "\\t", "\\u00e4", "\\u20ac", "\\ud83d\\ude00", "\\u0000"}}; + const std::array invalid = {{"\\x", "\\ud800", "\\udc00", "\\u12G4", "\\u12", "\\ud83dx", "\\ud83d\\u0041", "\\", "\x01"}}; + const std::array utf8 = {{"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xc3", "\xff", "\x80"}}; + std::mt19937 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + for (std::size_t run = 0; run <= 40; ++run) + { + for (int i = 0; i < 60; ++i) + { + std::string content = std::string(rng() % 20, 'a'); + const auto pieces = 1 + (rng() % 6); + for (unsigned k = 0; k < pieces; ++k) + { + content += valid[rng() % valid.size()]; + content += std::string(k == 0 ? run : rng() % 41, static_cast('b' + (rng() % 20))); + switch (rng() % 8) + { + case 0: + content += utf8[rng() % 3]; + break; + case 1: + content += utf8[rng() % utf8.size()]; + break; + case 2: + content += invalid[rng() % invalid.size()]; + break; + default: + break; + } + } + check_string(content); + check_string(content.substr(0, content.size() - (rng() % 4))); + } + } + // the text before the first escape, and runs and escapes at the end of the text + for (std::size_t before = 0; before <= 40; ++before) + { + for (const char* escape : valid) + { + check_string(std::string(before, 'a') + escape); + check_string(std::string(before, 'a') + escape + std::string(before % 19, 'z')); + check_string(std::string(before, 'a') + escape + escape + std::string(before % 19, 'z')); + } + } +} + TEST_CASE("json_view node integer bits") { using nlohmann::detail::view::integer_bits; diff --git a/tests/src/unit-json_view_dump.cpp b/tests/src/unit-json_view_dump.cpp index 850dba3c7..f3ccc004e 100644 --- a/tests/src/unit-json_view_dump.cpp +++ b/tests/src/unit-json_view_dump.cpp @@ -21,6 +21,7 @@ using nlohmann::ordered_json_view; using json_view_test::generator; using json_view_test::has_duplicate_keys; +#include #include #include #include @@ -29,6 +30,7 @@ using json_view_test::has_duplicate_keys; #include #include #include +#include #include #include #include @@ -725,6 +727,152 @@ TEST_CASE("json_view large objects") CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t))); } + SECTION("keys that differ in their last bytes are spread over the table") + { + // The hash must spread the usual families of keys (numbered names, hex + // strings, dates, short names): a key farther than index_max_displacement + // from its home slot would leave the object without a table. The keys + // are placed with linear probing, as the table does. + const auto max_displacement = [](const std::vector& keys) + { + std::size_t slots = 16; + while (slots < 2 * keys.size()) + { + slots *= 2; + } + std::vector used(slots, false); + std::size_t farthest = 0; + for (const std::string& key : keys) + { + std::size_t i = nlohmann::detail::conditional_static_cast(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1); + std::size_t distance = 0; + while (used[i]) + { + i = (i + 1) & (slots - 1); + ++distance; + } + used[i] = true; + farthest = (std::max)(farthest, distance); + } + return farthest; + }; + // the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream + // (a format string that is not a literal is a Flawfinder error) + const auto numbered = [](const char* format, int count, int step) + { + const std::string pattern(format); + const std::size_t percent = pattern.find('%'); + std::size_t conversion = percent + 1; + std::size_t width = 0; + while (pattern[conversion] >= '0' && pattern[conversion] <= '9') + { + width = (width * 10) + static_cast(pattern[conversion] - '0'); + ++conversion; + } + const bool hex = pattern[conversion] == 'x'; + std::vector keys; + for (int i = 0; i < count; ++i) + { + std::ostringstream key; + key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast(width)) + << (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1); + keys.push_back(key.str()); + } + return keys; + }; + for (const char* format : + {"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d", + "configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z" + }) + { + CAPTURE(format) + CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains) + CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement); + } + std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + for (const std::size_t length : + { + std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33} + }) + { + CAPTURE(length) + std::set distinct; + // (there are 26 and 676 keys of one and two letters) + std::size_t wanted = 700; + if (length == 1) + { + wanted = 26; + } + else if (length == 2) + { + wanted = 500; + } + while (distinct.size() < wanted) + { + std::string key(length, 'a'); + for (char& c : key) + { + c = static_cast('a' + (rng() % 26)); + } + distinct.insert(key); + } + CHECK(max_displacement(std::vector(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement); + } + } + + SECTION("keys of every length that differ in one byte") + { + // In a large object, a key is found by comparing its bytes with those of the member the + // table points to: up to 16 bytes as two overlapping words, longer ones with memcmp. + // Keys of every length from 0 to 40, and keys that differ from them in one byte. + std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + std::vector keys; + std::set distinct; + for (std::size_t length = 0; length <= 40; ++length) + { + for (int copy = 0; copy < 5; ++copy) + { + std::string key(length, 'a'); + for (char& c : key) + { + c = static_cast('a' + (rng() % 26)); + } + if (distinct.insert(key).second) + { + keys.push_back(key); + } + } + } + std::string text = "{"; + for (std::size_t i = 0; i < keys.size(); ++i) + { + text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i); + } + text += "}"; + REQUIRE(keys.size() >= 128); // (a table is built for 128 members) + const json_document d = json_document::parse(text); + const json_view v = d.root(); + for (std::size_t i = 0; i < keys.size(); ++i) + { + CAPTURE(keys[i]) + CHECK(v[keys[i]].get() == i); + CHECK(v.at(keys[i]).get() == i); + CHECK(v.contains(keys[i])); + for (std::size_t at = 0; at < keys[i].size(); ++at) + { + std::string other = keys[i]; + other[at] = other[at] == 'z' ? 'y' : 'z'; + CAPTURE(other) + CHECK(v.contains(other) == (distinct.count(other) != 0)); + } + CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0)); + if (!keys[i].empty()) + { + CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0)); + } + } + } + SECTION("shrink_to_fit releases the tables' spare capacity") { const auto make_text = [](int objects, int members) diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 5c3b4396a..4a6fbe69d 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -661,6 +661,115 @@ TEST_CASE("json_view edits: replacing arrays and objects by scalars") CHECK(d.root().materialize() == expected); } +TEST_CASE("json_view edits: arrays whose elements have no subtrees") +{ + // such an array is indexed directly while it is in its parsed layout; edits + // that replace elements by scalars keep it, others move it + json_editable_document d = json_editable_document::parse("[10,20,30,40]"); + json expected = json::array({10, 20, 30, 40}); + const auto check = [&] + { + CHECK(d.root().size() == expected.size()); + for (std::size_t i = 0; i < expected.size(); ++i) + { + CHECK(d.root()[i].materialize() == expected[i]); + CHECK(d.root().at(i).materialize() == expected[i]); + } + CHECK(d.root()[expected.size()].is_discarded()); + CHECK(d.root().back().materialize() == expected.back()); + CHECK(d.root().dump() == expected.dump()); + }; + check(); + d.set(d.root()[1], "x"); + expected[1] = "x"; + check(); + d.set(d.root()[3], 2.5); + expected[3] = 2.5; + check(); + d.set(d.root()[2], json::array({1, 2})); + expected[2] = json::array({1, 2}); + check(); + d.push_back(d.root(), 50); + expected.push_back(50); + check(); + d.insert(d.root(), 0, true); + expected.insert(expected.begin(), true); + check(); + d.erase(d.root(), 1); + expected.erase(expected.begin() + 1); + check(); + d.set(d.root()[2], nullptr); + expected[2] = nullptr; + check(); +} + +TEST_CASE("json_view edits: materializing values nested deeper than the document was parsed") +{ + // the stack of open containers of materialize() is sized from the nesting of the parsed text; + // a value added by an edit can be deeper than that (and than the stack that is local to the call) + json_editable_document d = json_editable_document::parse("[1,[2]]"); + json expected = json::array({1, json::array({2})}); + json nested = 3; + for (int level = 0; level < 40; ++level) + { + nested = json::array({nested, json{{"k", level}}}); + } + d.push_back(d.root(), nested); + expected.push_back(nested); + CHECK(d.root().materialize() == expected); + CHECK(d.root()[2].materialize() == nested); + d.set(d.root()[1][0], nested); + expected[1][0] = nested; + CHECK(d.root().materialize() == expected); + CHECK(d.root().get() == expected); +} + +TEST_CASE("json_view edits: an editable document before and after its first edit") +{ + // until the first edit, an editable document is read like a read-only one; views made before the edit + // keep working after it + const std::string text = R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"text","f":[]})"; + const json_document read_only = json_document::parse(text); + json_editable_document d = json_editable_document::parse(text); + const auto root = d.root(); + const auto same_as_read_only = [&] + { + CHECK(root.dump() == read_only.root().dump()); + CHECK(root.dump(2) == read_only.root().dump(2)); + CHECK(root.materialize() == read_only.root().materialize()); + CHECK(root["a"][2]["x"].get_string() == "y"); + CHECK(root["a"].at(3).get() == 4); + CHECK(root["a"].back().get() == 4); + CHECK(root.contains("b")); + CHECK(!root.contains("zz")); + CHECK(root.find("e")->get_string() == "text"); + CHECK(root.find("zz") == root.end()); + CHECK(root["b"]["c"].get()); + CHECK(root.value("e", "none") == "text"); + std::size_t count = 0; + for (const auto m : root["a"]) + { + CHECK(m.materialize() == read_only.root()["a"][count].materialize()); + ++count; + } + CHECK(count == 4); + }; + same_as_read_only(); + // an edit that changes a scalar in place, then one that moves an array + d.set(root["e"], "other"); + CHECK(root["e"].get_string() == "other"); + CHECK(root["a"][2]["x"].get_string() == "y"); + CHECK(root["a"].back().get() == 4); + d.push_back(root["f"], 7); + CHECK(root["f"][0].get() == 7); + CHECK(root["f"].back().get() == 7); + CHECK(root.dump() == R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"other","f":[7]})"); + d.set(root["a"], 2, json::array({1, 2})); + CHECK(root["a"][2][1].get() == 2); + CHECK(root["a"].dump() == "[1,2,[1,2],4]"); + CHECK(root.materialize() == json::parse(R"({"a":[1,2,[1,2],4],"b":{"c":true,"d":null},"e":"other","f":[7]})")); +} + namespace { // reads of "a" in an object that repeats it: the first member wins, however the From 9c50bdd140d80744ebfa8dd7e850794f2cce50e3 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 24 Oct 2026 07:41:21 +0200 Subject: [PATCH 4/4] Make the DOM parser cheaper per container and per member (#5882) * Reserve the parser's nesting stack once The stack of open arrays and objects was a std::vector that allocated at the first push_back and then reallocated at 1, 2, 4, 8 bytes, so a document nested four levels deep made four allocations before it parsed a value. A document that starts with a container now reserves 32 levels in one allocation; deeper documents grow as before. The loop that reads the tokens is the code it was, so large documents are not affected (a stack object with its own storage, and a reserve at every container, both cost the token loop instructions or cycles). Fixed cost of parsing a 124-byte object (macOS instructions): accept -7.6 %, DOM -2.5 %; no change for a single number. Signed-off-by: Niels Lohmann * Build arrays of the DOM parser in one exact-size allocation Reading an array of unknown length appended its elements to a std::vector one at a time, so a small array reallocated (and moved its elements) at sizes 1, 2, 4, ... The DOM SAX parser now collects the elements of arrays whose length is unknown on one stack shared by all open arrays and moves them into an array of exactly the right size when the closing bracket arrives. An array that grows past 16 elements switches to appending to its own vector, so large arrays pay no second copy. Binary formats, which announce the length, and builds with JSON_DIAGNOSTICS or JSON_DIAGNOSTIC_POSITIONS (whose values need their final place from the start) keep storing into the array directly. The stacks of open containers are reserved on first use instead of growing from one element. The binary readers get a DOM parser of their own, json_sax_dom_binary_parser: it is the plain parser that stores every value directly in its container (the code the readers used before). Their containers announce their size, so the bookkeeping of the collecting parser (frame entries, the destination of a finished container) was pure overhead for them: with the collecting parser FromBinaryShape nested was 3-12 % and containers up to 5 % more instructions (GCC) than before. With the plain parser Linux Ir of FromBinaryShape and FromBinaryBuffer is within 0.8 % of develop (Clang: identical). canada.json has 56 000 two-element arrays that took three allocations each and now take two. DOM parse Ir on Linux against develop (GCC / Clang): canada -7.0 % / -7.7 %, citm -0.8 % / -1.6 %, 124-byte object -5.4 % / -5.5 %; accept and SAX are untouched. Signed-off-by: Niels Lohmann * Reuse one value for the key events of the callback parser parse() with a callback built a new string value for every key it passed to the callback (a heap allocation and release per key), and copied the key a second time into the object. The callback parser now keeps one string value for key events, which it assigns, and moves the key into the object. Callback parse, Linux Ir (GCC / Clang): twitter -13.3 % / -11.7 %, citm -9.5 % / -7.6 %. (Earlier macOS measurement: twitter -19.5 % instructions.) Signed-off-by: Niels Lohmann * Remember where the DOM parser stores the next value The DOM SAX parser looked up the innermost open container for every value: the end of its frame stack, then the object or array pointer in the frame. Compilers have to reload all of that after each value is stored, since a store of a value may alias the frame. The parser now keeps the innermost container's kind and, for an array that stores directly, the array itself in members, updated when a container opens or closes. Flat arrays of numbers are the case that mattered: parsing floats.json (1 000 000 doubles) into a DOM took 2 % more cycles than before the previous commit on macOS and now takes 0.4 % fewer than the code before it (signed_ints.json -0.8 %). Linux Ir is unchanged (canada -6.1 % GCC / -6.9 % Clang, citm -0.3 % / -0.8 %, twitter +0.2 % / 0.0 %). Signed-off-by: Niels Lohmann * Look up the keys of big ordered_json objects in a hash table ordered_map keeps its elements in a vector and searches it linearly for every key it adds, so parsing an object with n keys into ordered_json took O(n^2) key comparisons: 33 billion instructions for one object with 30 000 keys, where json needs 245 million. From 64 keys on, the DOM parser now keeps a hash table of the keys of the open object (hashes and positions only; the elements stay in the vector) and finds duplicate keys there. The result is the same, including the position of a key that occurs twice. Smaller objects, std::map based objects and key types without std::hash are untouched. ordered_json parse, macOS instructions: object with 1 000 keys 64.8 M -> 14.6 M, 10 000 keys 5 104 M -> 38.7 M, 30 000 keys 33 358 M -> 89 M. Linux Ir of an object with 2 000 keys: 43.0 M -> 1.9 M (GCC), 37.9 M -> 1.8 M (Clang). Signed-off-by: Niels Lohmann * Trim the work the DOM parser does per container and per member Opening and closing an array or object cost the DOM parser more than before the exact-size array change, which showed on documents of many small objects (jeopardy.json: +1.4 % Ir with GCC). This takes it back: - the stack of open containers keeps its first eight entries in the parser object instead of in a vector, so it needs no capacity check beyond the push itself and no allocation for typical documents; - a container is created in the place it ends up in, instead of as a temporary that is then moved there; - a new object member, which is null, is constructed over the null instead of being swapped with it (only with the default base class of basic_json); - the key index that big ordered_json objects use is only a part of the stack entries of objects that can have one; - an array that outgrows the stack reserves twice what it has so far, as a vector that grew by itself would have. DOM parse Ir of the series up to here against develop, Linux GCC / Clang: canada -6.6 % / -7.3 %, citm +1.2 % / +0.2 %, jeopardy +0.4 % / -0.1 %, 124-byte object -6.5 % / -6.2 %. (twitter measures +6.8 % / +6.6 % at this point in steady state, almost all of it in glibc's malloc/consolidate for the new allocation sizes; with the last commit of the series it is -0.1 % / 0.0 %.) Signed-off-by: Niels Lohmann * Test parsing containers of the sizes where the DOM parser changes storage The DOM parser collects the elements of an array on a stack (up to 16) and the members of an ordered_json object (up to 32) before it moves them into the container, and uses a hash table for keys from 64 members on. Parse arrays and objects of the sizes around these limits, nested into each other, with repeated keys, and as random trees that are built next to their text, and compare json and ordered_json against the value built through the API. The tests pass on the code from before these changes as well. The random trees are what finds the mistakes that are easy to make here: while the collecting change was being written, a collected array that outgrew the stack at the moment its parent had just reached the limit left its elements in the parent, and only these tests (142 failed assertions) showed it. Signed-off-by: Niels Lohmann * Do not convert floats that cannot overflow in accept() accept() only has to find out whether a float token is finite: json_sax_acceptor ignores the value, and the parser rejects only a float that overflows. A float whose digits fit into 19 digits and whose decimal exponent is at most max_exponent10 - 19 is below 10^max_exponent10 and cannot overflow, so the lexer, told by accept() that the value is not needed, skips the conversion for those. A token that may overflow, with more digits, or in another float format is converted as before, so 1e309 and its neighbours are rejected as they were (the new test compares accept() with parse() on tokens around the limit). json::accept(): canada.json Ir -8.6 % (GCC) / -10.9 % (Clang), 124-byte object -3.2 %; cycles on the M1 (release benchmarks, LTO): canada -7.8 %, floats.json -9.0 %. The check tests the accept() flag first, so the parser that needs the value pays one load and branch: SaxParse canada +0.35 % Ir (GCC and Clang), DOM canada +0.4 %. Signed-off-by: Niels Lohmann * Collect the members of ordered_json objects on a stack until they are known An ordered_json object keeps its members in a vector and appends every new member to it, so reading an object of unknown size reallocated the vector at 1, 2, 4, 8, ... members, and ordered_map copies all keys on every reallocation. The DOM SAX parser now collects the members of such objects on one stack shared by all open objects, as it does the elements of arrays, and moves them into a vector of exactly the right size when the closing brace arrives. An object that gets past 32 members switches to appending to its own vector (and, from 64, to the key index of the earlier commit). The members are the same, in the same order, and a key that occurs twice keeps the place of its first occurrence. std::map based objects, objects whose size the input announces (binary formats), and builds with JSON_DIAGNOSTICS or JSON_DIAGNOSTIC_POSITIONS are untouched. What only ordered objects need (the collected members, the key indexes) is a member of the parser only for them, and the code that stores a value tries the array that takes its elements directly first. An array that outgrows the stack now reserves 32 elements, the capacity a vector that grew by itself has at that point; with the larger reserve before, parsing floats.json (1 000 000 numbers) was 2 % slower on Apple M1 than before this series. ordered_json parse, Linux Ir against develop (GCC / Clang): citm -11.6 % / -11.7 %, twitter -8.4 % / -8.4 %, jeopardy (12 000 objects of 7 keys) -10.5 % / -8.0 %, canada -3.9 % / -5.2 %, 124-byte object -20.9 % / -16.5 %. macOS time (M1, earlier base): citm -19 %, twitter -13 %, jeopardy -13 %, canada -14 %, 124-byte object -26 %. json parse: steady-state twitter -0.1 % (see the previous commits), citm -1.2 %. Signed-off-by: Niels Lohmann * Rename the key index's match callback so Flawfinder doesn't read it as std::equal Signed-off-by: Niels Lohmann * Bind the parser test's strings by reference in the range-for loop GCC and Clang 14 (-Wrange-loop-construct) reported the copy. Signed-off-by: Niels Lohmann * Collect container values only in basic_json's own parse functions A json_sax_dom_parser that a user constructs (as in the sax_no_exception example) must hold the values read before a parse error, as on develop: its value is used after the error. The values of containers of unknown size are now collected on a stack only with the new template parameter Collect (default false), which json::parse() and the json_view materialization set; without it, every value is stored in place as it arrives. A test checks the values kept after an error for json and ordered_json. Also fixes the clang-tidy, cpplint and Infer findings: the frame stack keeps its first frames in a std::array (left uninitialized) and the others in a std::unique_ptr, auto for create<>(), NOLINT for member initializers -Weffc++ needs, and an @infer-ignore for the nullptr that place_container() does not use. json::parse() and ordered_json::parse() instructions are unchanged (Apple clang; twitter, citm, canada, jeopardy: -0.03 % to +0.25 %). Signed-off-by: Niels Lohmann * Clarify the DOM parser's collection stacks and rename reenter_enclosing_container() Address review comments: explain why a closed container erases only the top of the shared stack, set the payload before the type, rename the helper that makes the enclosing container current again, and declare the parser's state stack right before reserving it. Signed-off-by: Niels Lohmann * Export json_sax_dom_parser from the C++20 module for MSVC MSVC needs the detail classes that basic_json befriends to be visible in the module (#3970, as json_sax_dom_callback_parser); with its new template parameter, json_sax_dom_parser failed in ci_module_cpp20 (C2039, not a member of nlohmann::detail). Signed-off-by: Niels Lohmann * Keep the overflow-limit numbers of the parser test in a std::vector clang-tidy (cppcoreguidelines-avoid-c-arrays) rejected the C array. Signed-off-by: Niels Lohmann * Fix the clang-tidy findings of the ordered_json parser tests The container sizes are a std::array, the expected text uses raw string literals, the generator's arithmetic is in parentheses, auto for the result of make<>(), and single characters are appended as characters. clang-tidy now reports nothing on the lines the branch adds to unit-ordered_json.cpp and unit-class_parser.cpp (apart from checks CI's clang-tidy does not have). Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/binary_reader.hpp | 2 +- include/nlohmann/detail/input/json_sax.hpp | 1070 ++++++++++++++- include/nlohmann/detail/input/lexer.hpp | 10 + include/nlohmann/detail/input/parser.hpp | 17 +- include/nlohmann/detail/view/materialize.hpp | 2 +- include/nlohmann/json.hpp | 8 +- single_include/nlohmann/json.hpp | 1213 ++++++++++++++++- single_include/nlohmann/json_view.hpp | 2 +- src/modules/json.cppm | 1 + tests/src/unit-class_parser.cpp | 78 ++ tests/src/unit-ordered_json.cpp | 278 ++++ 11 files changed, 2587 insertions(+), 94 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index fef2c0f93..6c7168d47 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -86,7 +86,7 @@ JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 2 /*! @brief deserialization of BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ -template> +template> class binary_reader { using number_integer_t = typename BasicJsonType::number_integer_t; diff --git a/include/nlohmann/detail/input/json_sax.hpp b/include/nlohmann/detail/input/json_sax.hpp index 966610e4a..db95685f0 100644 --- a/include/nlohmann/detail/input/json_sax.hpp +++ b/include/nlohmann/detail/input/json_sax.hpp @@ -8,9 +8,13 @@ #pragma once -#include // find_if, min +#include // copy, find_if, min +#include // array #include +#include // hash #include // numeric_limits +#include // unique_ptr +#include // operator new #include // string #include // enable_if_t #include // move, pair @@ -18,8 +22,10 @@ #include #include +#include #include #include +#include #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -315,20 +321,114 @@ struct diagnostic_positions }; #endif +/*! +@brief a hash table that finds the position of a key in an object whose + elements are kept in a vector (ordered_map) + +The elements stay where they are: the table only holds their hashes and +positions, and the caller compares the keys. +*/ +class object_key_index +{ + public: + static constexpr std::size_t npos = static_cast(-1); + + /// @param[in] element_hashes the hashes of the keys of the elements that are there already + explicit object_key_index(std::vector&& element_hashes) + : hashes(std::move(element_hashes)) + { + std::size_t size = 256; + while (size < 4 * hashes.size()) + { + size *= 2; + } + rebuild(size); + } + + /// @return the position of the element with @a hash for which @a matches(position) holds, or npos + template + std::size_t find(const std::size_t hash, Matches matches) const + { + for (std::size_t slot = hash & mask;; slot = (slot + 1) & mask) + { + const std::size_t entry = table[slot]; + if (entry == 0) + { + return npos; + } + if (hashes[entry - 1] == hash && matches(entry - 1)) + { + return entry - 1; + } + } + } + + /// add the next element, whose position is the number of elements so far + void add(const std::size_t hash) + { + hashes.push_back(hash); + if (hashes.size() * 2 > table.size()) + { + rebuild(table.size() * 2); + } + else + { + place(hash, hashes.size()); + } + } + + private: + void rebuild(const std::size_t size) + { + table.assign(size, 0); + mask = size - 1; + for (std::size_t i = 0; i < hashes.size(); ++i) + { + place(hashes[i], i + 1); + } + } + + void place(const std::size_t hash, const std::size_t entry) + { + std::size_t slot = hash & mask; + while (table[slot] != 0) + { + slot = (slot + 1) & mask; + } + table[slot] = entry; + } + + /// the hash of each element, by position + std::vector hashes; + /// open addressing, a power of two in size: position + 1 of an element, or 0 for a free slot + std::vector table {}; // NOLINT(readability-redundant-member-init) + std::size_t mask = 0; +}; + /*! @brief SAX implementation to create a JSON value from SAX events This class implements the @ref json_sax interface and processes the SAX events to create a JSON value which makes it basically a DOM parser. The structure or -hierarchy of the JSON value is managed by the stack `ref_stack` which contains -a pointer to the respective array or object for each recursion depth. +hierarchy of the JSON value is managed by the stack `frames` which contains +an entry for each open array or object. The parser of the text formats, for +which the size of a container is not known up front, collects the elements of +an array and the members of an `ordered_json` object on a stack and moves them +into a container of the exact size (with @a Collect, as basic_json's own parse +functions use it); the binary readers use the plain +@ref json_sax_dom_binary_parser instead. After successful parsing, the value that is passed by reference to the -constructor contains the parsed value. +constructor contains the parsed value. Without @a Collect, every value is +stored in place as it arrives, so after a parse error the value holds what was +read up to it. @tparam BasicJsonType the JSON type +@tparam Collect whether to collect the values of containers of unknown size on + a stack (only for a caller that does not use the value after a + parse error) */ -template +template class json_sax_dom_parser { public: @@ -401,6 +501,930 @@ class json_sax_dom_parser return true; } + bool start_object(std::size_t len) + { +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // the values need their final place from the start (their parent + // pointers and positions refer to it), so no buffering + return start_stored_object(len); +#else + return start_object(len, std::integral_constant {}); +#endif + } + + bool key(string_t& val) + { + JSON_ASSERT(!frames.empty()); + return key(val, std::integral_constant {}); + } + + bool end_object() + { + JSON_ASSERT(!frames.empty()); + return end_object(std::integral_constant {}); + } + + bool start_array(std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > array_t().max_size())) + { + // the array does not exist yet: report with an empty one + BasicJsonType too_large(BasicJsonType::value_t::array); + return container_too_large_error(*this, len, "array", &too_large); + } + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // the values need their final place from the start (their parent + // pointers and positions refer to it), so no buffering + start_stored_array(len); +#else + if (!collects(len)) + { + // the number of elements is known (or the elements must be in + // place as they arrive): store them directly + start_stored_array(len); + } + else + { + // the number of elements is not known, so growing the array + // would reallocate it (and, being a vector, move its elements) + // again and again. Collect the elements on a stack shared by all + // arrays instead, and copy them to an array of the exact size + // when the closing bracket arrives. + BasicJsonType* slot = nullptr; + std::size_t dest_member = 0; + container_destination(slot, dest_member); + reserve_elements(); + frames.push_back(frame{nullptr, nullptr, slot, elements.size(), ordered_state(dest_member, false)}); + in_object = false; + stored_array = nullptr; + } +#endif + return true; + } + + bool end_array() + { + JSON_ASSERT(!frames.empty()); + JSON_ASSERT(frames.back().object == nullptr); + const frame f = frames.back(); + frames.pop_back(); + reenter_enclosing_container(); + + if (f.array == nullptr) + { + // the elements are the top of the shared elements stack, from + // f.start on (the elements below belong to enclosing arrays that + // are still open): move them into the array and pop them + const auto first = elements.begin() + static_cast(f.start); + auto* const arr = BasicJsonType::template create(std::make_move_iterator(first), std::make_move_iterator(elements.end())); + BasicJsonType value; + value.m_data.m_value.array = arr; + value.m_data.m_type = BasicJsonType::value_t::array; + elements.erase(first, elements.end()); + + place_container(f, frames.empty() ? nullptr : &frames.back(), std::move(value), true); + return true; + } + +#if JSON_DIAGNOSTIC_POSITIONS + diagnostic_positions::set_container_end(*f.slot, m_lexer_ref); +#endif +#if JSON_DIAGNOSTICS + f.slot->set_parents(); +#endif + return true; + } + + template + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, + const Exception& ex) + { + errored = true; + static_cast(ex); + if (allow_exceptions) + { + JSON_THROW(ex); + } + return false; + } + + constexpr bool is_errored() const + { + return errored; + } + + private: + using object_t = typename BasicJsonType::object_t; + using array_t = typename BasicJsonType::array_t; + + /// whether objects keep their elements in a vector and a key can be hashed, + /// so that a big object can find the keys it has in a hash table instead of + /// searching all of them + template + using detect_indexable_keys = decltype(std::declval().emplace_new_key(std::declval(), std::declval()), + std::hash()(std::declval())); + static constexpr bool indexed_keys = is_detected::value; + + /// what a frame holds for objects that are kept in a vector: the key index + /// of an object with many keys, and for an object that is collected on the + /// stack the member of the enclosing object it will become + struct ordered_frame + { + /// the entry of key_indexes of an object that has one, else nullptr + object_key_index* index; + /// object or array without a slot in an object collected on the stack: + /// the index of the member in `members` it goes to + std::size_t dest_member; + /// whether this is an object collected on the stack + bool buffered; + }; + struct no_ordered_frame {}; + using ordered_frame_state = typename std::conditional::type; + + static ordered_frame make_ordered_state(const std::size_t dest_member, const bool buffered, std::true_type /*indexed*/) noexcept + { + ordered_frame state = {nullptr, dest_member, buffered}; + return state; + } + + static no_ordered_frame make_ordered_state(const std::size_t /*dest_member*/, const bool /*buffered*/, std::false_type /*indexed*/) noexcept + { + return no_ordered_frame(); + } + + static ordered_frame_state ordered_state(const std::size_t dest_member, const bool buffered) noexcept + { + return make_ordered_state(dest_member, buffered, std::integral_constant {}); + } + + /// an array or object that is being read + struct frame + { + /// the object, or nullptr for an array + object_t* object; + /// the array if its elements are stored in it as they arrive, or + /// nullptr for an array and an object + array_t* array; + /// array without `array`: where the finished array goes (nullptr: it + /// is an element of the enclosing array); otherwise: the value that + /// holds the object or array + BasicJsonType* slot; + /// array without `array`: the index of its first element in `elements` + std::size_t start; + /// what objects in a vector add (see ordered_frame) + ordered_frame_state ordered; + }; + + /// the open arrays and objects; the first few live in the object itself, so + /// that the common documents do not allocate for them + class frame_stack + { + public: + frame_stack() noexcept + : local() // NOLINT(readability-redundant-member-init) + {} + frame_stack(const frame_stack&) = delete; + frame_stack& operator=(const frame_stack&) = delete; + + frame_stack(frame_stack&& other) noexcept + : local() // NOLINT(readability-redundant-member-init) + { + take(other); + } + + frame_stack& operator=(frame_stack&& other) noexcept + { + if (this != &other) + { + release(); + take(other); + } + return *this; + } + + ~frame_stack() = default; + + bool empty() const noexcept + { + return count == 0; + } + + std::size_t size() const noexcept + { + return count; + } + + frame& back() noexcept + { + return data[count - 1]; + } + + frame& operator[](const std::size_t i) noexcept + { + return data[i]; + } + + void push_back(const frame& f) + { + if (JSON_HEDLEY_UNLIKELY(count == capacity)) + { + grow(); + } + data[count++] = f; + } + + void pop_back() noexcept + { + JSON_ASSERT(count > 0); + --count; + } + + private: + JSON_HEDLEY_NEVER_INLINE + void grow() + { + const std::size_t new_capacity = capacity * 2; + std::unique_ptr new_heap(new frame[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::copy(data, data + count, new_heap.get()); + heap = std::move(new_heap); + data = heap.get(); + capacity = new_capacity; + } + + void release() noexcept + { + heap.reset(); + data = local.data(); + capacity = local_capacity; + } + + /// make the contents of @a other our own + void take(frame_stack& other) noexcept + { + if (other.heap == nullptr) + { + std::copy(other.local.begin(), other.local.begin() + static_cast(other.count), local.begin()); + data = local.data(); + capacity = local_capacity; + } + else + { + heap = std::move(other.heap); + data = heap.get(); + capacity = other.capacity; + other.data = other.local.data(); + other.capacity = local_capacity; + } + count = other.count; + other.count = 0; + } + + static constexpr std::size_t local_capacity = 8; + + /// the first frames, left uninitialized: a frame is written before it is + /// read (the user-provided constructor keeps the member initialization, + /// which -Weffc++ asks for, from zeroing them) + struct local_frames : std::array + { + local_frames() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + + local_frames local; + /// the frames once there are more than local_capacity of them + std::unique_ptr heap {}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init) + frame* data = local.data(); + std::size_t count = 0; + std::size_t capacity = local_capacity; + }; + + void release_key_index(const frame& f, std::true_type /*indexed*/) + { + if (JSON_HEDLEY_UNLIKELY(f.ordered.index != nullptr)) + { + ordered_data.key_indexes.pop_back(); + } + } + + static void release_key_index(const frame& /*f*/, std::false_type /*indexed*/) noexcept {} + + /// the number of keys from which an object looks them up in a hash table + static constexpr std::size_t key_index_threshold = 64; + + /// add @a key to the object of @a f (or find it, if it is there), searching it in std::map + static BasicJsonType* insert_key(frame& f, string_t&& key, std::false_type /*indexed*/) + { + return &f.object->operator[](std::move(key)); + } + + /// add @a key to the object of @a f (or find it, if it is there), searching it in a vector + BasicJsonType* insert_key(frame& f, string_t&& key, std::true_type /*indexed*/) + { + object_t& obj = *f.object; + if (JSON_HEDLEY_LIKELY(obj.size() < key_index_threshold)) + { + return &obj[std::move(key)]; + } + return insert_key_with_index(f, std::move(key)); + } + + /// add @a key to the big object of @a f (or find it, if it is there), using its key index + JSON_HEDLEY_NEVER_INLINE + BasicJsonType* insert_key_with_index(frame& f, string_t&& key) + { + object_t& obj = *f.object; + const std::hash hasher {}; + const std::size_t hash = hasher(key); + if (JSON_HEDLEY_UNLIKELY(f.ordered.index == nullptr)) + { + std::vector hashes; + hashes.reserve(obj.size()); + for (const auto& element : obj) + { + hashes.push_back(hasher(element.first)); + } + ordered_data.key_indexes.emplace_back(new object_key_index(std::move(hashes))); + f.ordered.index = ordered_data.key_indexes.back().get(); + } + + const std::size_t position = f.ordered.index->find(hash, [&obj, &key](std::size_t i) + { + return (obj.begin() + static_cast(i))->first == key; + }); + if (position != object_key_index::npos) + { + return &(obj.begin() + static_cast(position))->second; + } + + obj.emplace_new_key(key, BasicJsonType()); + f.ordered.index->add(hash); + return &obj.back().second; + } + + /// where an array or object that starts now will go when it is finished: + /// @a slot is the value to assign it to (the root or a member of an object + /// that is stored directly), or nullptr if it is an element of the + /// enclosing array or a member of the enclosing object that is collected on + /// the stack, in which case @a dest_member is the member + void container_destination(BasicJsonType*& slot, std::size_t& dest_member) + { + if (frames.empty()) + { + slot = &root; + } + else if (frames.back().object != nullptr) + { + slot = object_element; + } + else + { + collected_object_member(frames.back(), dest_member, std::integral_constant {}); + } + } + + void collected_object_member(const frame& enclosing, std::size_t& dest_member, std::true_type /*indexed*/) const noexcept + { + if (enclosing.ordered.buffered) + { + dest_member = ordered_data.member_index; + } + } + + static void collected_object_member(const frame& /*enclosing*/, std::size_t& /*dest_member*/, std::false_type /*indexed*/) noexcept {} + + static bool is_collected_object(const frame& f, std::true_type /*indexed*/) noexcept + { + return f.ordered.buffered; + } + + static bool is_collected_object(const frame& /*f*/, std::false_type /*indexed*/) noexcept + { + return false; + } + + /// put a finished array or object that was collected on the stack, or + /// that outgrew it, where it belongs; @a enclosing is the container it is in. + /// @a enclosing_is_innermost tells whether that is the container that gets + /// the next value, so that it can be switched to storing its elements + /// directly if the value was the one too many. + void place_container(const frame& f, frame* enclosing, BasicJsonType&& value, const bool enclosing_is_innermost) + { + if (f.slot != nullptr) + { + *f.slot = std::move(value); + } + else if (is_collected_object(*enclosing, std::integral_constant {})) + { + place_member(f, std::move(value), std::integral_constant {}); + } + else if (enclosing->array != nullptr) + { + enclosing->array->emplace_back(std::move(value)); + } + else + { + elements.emplace_back(std::move(value)); + if (JSON_HEDLEY_UNLIKELY(enclosing_is_innermost && elements.size() - enclosing->start > buffer_limit)) + { + spill(*enclosing); + } + } + } + + void place_member(const frame& f, BasicJsonType&& value, std::true_type /*indexed*/) + { + ordered_data.members[f.ordered.dest_member].second = std::move(value); + } + + static void place_member(const frame& /*f*/, BasicJsonType&& /*value*/, std::false_type /*indexed*/) noexcept {} + + /// store the members of a new object directly in it + bool start_stored_object(std::size_t len) + { + BasicJsonType* const ref = store(BasicJsonType::value_t::object); + frames.push_back(frame{ref->m_data.m_value.object, nullptr, ref, 0, ordered_state(0, false)}); + in_object = true; + stored_array = nullptr; + +#if JSON_DIAGNOSTIC_POSITIONS + // Ensure this is after the call to store to ensure correct start position. + diagnostic_positions::set_container_start(*ref, m_lexer_ref); +#endif + + return check_container_size(*this, len, "object", ref); + } + + /// objects in std::map and the like have no use for collecting their members + bool start_object(std::size_t len, std::false_type /*indexed*/) + { + return start_stored_object(len); + } + + /// An object in a vector of members adds a member by appending it to the + /// vector, which grows it again and again (and copies the keys each time). + /// Collect the members of an object of unknown size on a stack shared by + /// all open objects instead, and move them into a vector of the exact size + /// when the closing brace arrives. + bool start_object(std::size_t len, std::true_type /*indexed*/) + { + if (!collects(len)) + { + return start_stored_object(len); + } + + BasicJsonType* slot = nullptr; + std::size_t dest_member = 0; + container_destination(slot, dest_member); + frames.push_back(frame{nullptr, nullptr, slot, ordered_data.members.size(), ordered_state(dest_member, true)}); + in_object = false; + stored_array = nullptr; + return true; + } + + bool key(string_t& val, std::false_type /*indexed*/) + { + JSON_ASSERT(frames.back().object != nullptr); + + // add null at the given key and store the reference for later; the + // key is moved, as the lexer starts the next token with reset() + object_element = insert_key(frames.back(), std::move(val), std::false_type {}); + return true; + } + + bool key(string_t& val, std::true_type /*indexed*/) + { + frame& f = frames.back(); + if (JSON_HEDLEY_UNLIKELY(f.object == nullptr)) + { + JSON_ASSERT(f.ordered.buffered); + if (collect_member(f, val)) + { + return true; + } + // the object has grown big: it is stored directly from here on + } + object_element = insert_key(f, std::move(val), std::true_type {}); + return true; + } + + /*! + @brief add a member to the object that is collected on the stack, or find the one with the key + @return false if the object has collected enough members to be stored + directly, which is done and which leaves the key to be inserted + */ + bool collect_member(frame& f, string_t& key) + { + for (std::size_t i = f.start; i < ordered_data.members.size(); ++i) + { + if (ordered_data.members[i].first == key) + { + ordered_data.member_index = i; + return true; + } + } + + if (JSON_HEDLEY_UNLIKELY(ordered_data.members.size() - f.start >= object_buffer_limit)) + { + spill_object(f); + return false; + } + + reserve_members(); + ordered_data.members.emplace_back(std::move(key), BasicJsonType()); + ordered_data.member_index = ordered_data.members.size() - 1; + return true; + } + + /// the members of an object that is collected on the stack as an object; + /// they are the top of the shared member stack, from f.start on (the + /// members below belong to enclosing objects that are still open) + object_t* members_to_object(const frame& f, BasicJsonType& value) + { + const auto first = ordered_data.members.begin() + static_cast(f.start); + auto* const obj = BasicJsonType::template create(std::make_move_iterator(first), std::make_move_iterator(ordered_data.members.end())); + value.m_data.m_value.object = obj; + value.m_data.m_type = BasicJsonType::value_t::object; + ordered_data.members.erase(first, ordered_data.members.end()); + return obj; + } + + /// switch an object that collects its members on the stack to storing them directly + JSON_HEDLEY_NEVER_INLINE + void spill_object(frame& f) + { + BasicJsonType value; + f.object = members_to_object(f, value); + f.ordered.buffered = false; + in_object = true; + // false positive: an object without an enclosing container is the root, + // which has a slot, so place_container() does not use the nullptr + // @infer-ignore NULLPTR_DEREFERENCE + place_container(f, frames.size() >= 2 ? &frames[frames.size() - 2] : nullptr, std::move(value), false); + } + + bool end_object(std::false_type /*indexed*/) + { + return end_stored_object(); + } + + bool end_object(std::true_type /*indexed*/) + { + if (JSON_HEDLEY_LIKELY(frames.back().object != nullptr)) + { + return end_stored_object(); + } + + // the members are on the stack: make them the object + const frame f = frames.back(); + frames.pop_back(); + reenter_enclosing_container(); + + BasicJsonType value; + members_to_object(f, value); + place_container(f, frames.empty() ? nullptr : &frames.back(), std::move(value), true); + return true; + } + + bool end_stored_object() + { + JSON_ASSERT(frames.back().object != nullptr); + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // objects are never collected, so their value is where it was stored + BasicJsonType* const ref = frames.back().slot; +#endif +#if JSON_DIAGNOSTIC_POSITIONS + diagnostic_positions::set_container_end(*ref, m_lexer_ref); +#endif +#if JSON_DIAGNOSTICS + ref->set_parents(); +#endif + + release_key_index(frames.back(), std::integral_constant {}); + frames.pop_back(); + reenter_enclosing_container(); + return true; + } + + /// the number of members an object collects on the stack before it + /// switches to storing them directly + static constexpr std::size_t object_buffer_limit = 32; + + void reserve_members() + { + if (JSON_HEDLEY_UNLIKELY(ordered_data.members.capacity() == 0)) + { + ordered_data.members.reserve(16); + } + } + + /// the number of elements an array collects on the stack before it + /// switches to storing them directly, so that it does not hold up the + /// stack with its elements (and move all of them in the end) + static constexpr std::size_t buffer_limit = 16; + + void reserve_elements() + { + if (JSON_HEDLEY_UNLIKELY(elements.capacity() == 0)) + { + elements.reserve(32); + } + } + + /// store the elements of a new array directly in it + void start_stored_array(const std::size_t len) + { + BasicJsonType* const ref = store(BasicJsonType::value_t::array); + frames.push_back(frame{nullptr, ref->m_data.m_value.array, ref, 0, ordered_state(0, false)}); + in_object = false; + stored_array = ref->m_data.m_value.array; + +#if JSON_DIAGNOSTIC_POSITIONS + // Ensure this is after the call to store to ensure correct start position. + diagnostic_positions::set_container_start(*ref, m_lexer_ref); +#endif + + if (len != detail::unknown_size()) + { + reserve_array(*ref->m_data.m_value.array, len, priority_tag<1> {}); + } + } + + /*! + @brief store a value as the next element of the open array or object (or + as the root) + @return the stored value + */ + template + JSON_HEDLEY_RETURNS_NON_NULL + BasicJsonType* store(Value&& v) + { + if (stored_array != nullptr) + { + stored_array->emplace_back(std::forward(v)); + return &stored_array->back(); + } + + if (in_object) + { + JSON_ASSERT(object_element); + set_member(*object_element, std::forward(v), std::is_same {}); + return object_element; + } + + return store_collected(std::forward(v), std::integral_constant {}); + } + + /// store a value that is the root, an element of an array that is collected + /// on the stack, or (in a vector) a member of an object that is + template + BasicJsonType* store_collected(Value&& v, std::true_type /*indexed*/) + { + if (!frames.empty() && frames.back().ordered.buffered) + { + BasicJsonType& member = ordered_data.members[ordered_data.member_index].second; + set_member(member, std::forward(v), std::is_same {}); + return &member; + } + return store_collected(std::forward(v), std::false_type {}); + } + + template + BasicJsonType* store_collected(Value&& v, std::false_type /*indexed*/) + { + if (frames.empty()) + { + root = BasicJsonType(std::forward(v)); + return &root; + } + + frame& f = frames.back(); + elements.emplace_back(std::forward(v)); + if (JSON_HEDLEY_UNLIKELY(elements.size() - f.start > buffer_limit)) + { + return spill(f); + } + return &elements.back(); + } + + /*! + @brief switch an array that collects its elements on the stack to storing + them directly + @return the last element of the array + */ + JSON_HEDLEY_NEVER_INLINE + BasicJsonType* spill(frame& f) + { + JSON_ASSERT(f.array == nullptr); + const auto first = elements.begin() + static_cast(f.start); + BasicJsonType value(BasicJsonType::value_t::array); + array_t& arr = *value.m_data.m_value.array; + // the capacity a vector that grew by itself has with this many elements + reserve_array(arr, 2 * buffer_limit, priority_tag<1> {}); + arr.insert(arr.end(), std::make_move_iterator(first), std::make_move_iterator(elements.end())); + elements.erase(first, elements.end()); + f.array = &arr; + stored_array = &arr; + + // the array takes the place its first element had on the stack + place_container(f, frames.size() >= 2 ? &frames[frames.size() - 2] : nullptr, std::move(value), false); + return &arr.back(); + } + + /// set the value of the member key() just added + template + JSON_HEDLEY_ALWAYS_INLINE + static void set_member(BasicJsonType& member, Value&& v, std::true_type /*default base*/) + { + if (JSON_HEDLEY_LIKELY(member.is_null())) + { + // a new member is null and holds nothing: construct the value + // over it instead of destroying and swapping + ::new (static_cast(&member)) BasicJsonType(std::forward(v)); + } + else + { + // the key occurred before + member = BasicJsonType(std::forward(v)); + } + } + + /// a custom base class may care about the lifetime of the value + template + static void set_member(BasicJsonType& member, Value&& v, std::false_type /*custom base*/) + { + member = BasicJsonType(std::forward(v)); + } + + /// whether the values of a container of @a len elements are collected on the stack + static bool collects(const std::size_t len) noexcept + { + return collects(len, std::integral_constant {}); + } + + static bool collects(const std::size_t len, std::true_type /*collect*/) noexcept + { + return len == detail::unknown_size(); + } + + static bool collects(const std::size_t /*len*/, std::false_type /*collect*/) noexcept + { + return false; + } + + /// after a container was closed: the one it was in is the one that gets the next value + void reenter_enclosing_container() + { + if (frames.empty()) + { + in_object = false; + stored_array = nullptr; + } + else + { + const frame& f = frames.back(); + in_object = f.object != nullptr; + stored_array = f.array; + } + } + + template + void handle_value(Value&& v) + { +#if JSON_DIAGNOSTIC_POSITIONS + BasicJsonType* const stored = store(std::forward(v)); + diagnostic_positions::set_from_lexer(*stored, m_lexer_ref); +#else + store(std::forward(v)); +#endif + } + + /// the parsed JSON value + BasicJsonType& root; + /// the open arrays and objects + frame_stack frames {}; + /// the elements of the open arrays that are not stored in them yet + std::vector elements {}; + /// what objects in a vector need besides the frames (std::map based objects + /// need nothing, so they do not carry it) + struct ordered_data_t + { + /// the members of the open objects in a vector that are not stored in them yet + std::vector> members {}; + /// the member of the innermost such object that key() added or found last + std::size_t member_index = 0; + /// the key indexes of the open objects that have one, innermost last + std::vector> key_indexes {}; // NOLINT(readability-redundant-member-init) + }; + struct no_ordered_data_t {}; + typename std::conditional::type ordered_data {}; + /// helper to hold the reference for the next object element + BasicJsonType* object_element = nullptr; + /// whether the innermost container is an object (frames.back().object) + bool in_object = false; + /// the array that stores the elements of the innermost array as they + /// arrive (frames.back().array), or nullptr + array_t* stored_array = nullptr; + /// whether a syntax error occurred + bool errored = false; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; + /// the lexer reference to obtain the current position + lexer_t* m_lexer_ref = nullptr; +}; + +/*! +@brief SAX implementation to create a JSON value from the events of a binary reader + +The DOM parser of the text formats (json_sax_dom_parser) collects the elements +of arrays and the members of objects on a stack and builds them in one +exact-size allocation, which pays off because a text parser does not know the +number of elements up front. Binary formats announce the size of their +containers (CBOR, MessagePack, BSON and BON8 do, and so do the optimized +containers of UBJSON and BJData), so they store each value directly in its +container, and the bookkeeping of the collecting parser would be pure +overhead for them. This is the plain parser: the structure or hierarchy of the +JSON value is managed by the stack `ref_stack` which contains a pointer to the +respective array or object for each recursion depth. + +After successful parsing, the value that is passed by reference to the +constructor contains the parsed value. + +@tparam BasicJsonType the JSON type +*/ +template +class json_sax_dom_binary_parser +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using lexer_t = lexer; + + /*! + @param[in,out] r reference to a JSON value that is manipulated while + parsing + @param[in] allow_exceptions_ whether parse errors yield exceptions + */ + explicit json_sax_dom_binary_parser(BasicJsonType& r, const bool allow_exceptions_ = true, lexer_t* lexer_ = nullptr) + : root(r), allow_exceptions(allow_exceptions_), m_lexer_ref(lexer_) + {} + + // make class move-only + json_sax_dom_binary_parser(const json_sax_dom_binary_parser&) = delete; + json_sax_dom_binary_parser(json_sax_dom_binary_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + json_sax_dom_binary_parser& operator=(const json_sax_dom_binary_parser&) = delete; + json_sax_dom_binary_parser& operator=(json_sax_dom_binary_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + ~json_sax_dom_binary_parser() = default; + + bool null() + { + handle_value(nullptr); + return true; + } + + bool boolean(bool val) + { + handle_value(val); + return true; + } + + bool number_integer(number_integer_t val) + { + handle_value(val); + return true; + } + + bool number_unsigned(number_unsigned_t val) + { + handle_value(val); + return true; + } + + bool number_float(number_float_t val, const string_t& /*unused*/) + { + handle_value(val); + return true; + } + + bool string(string_t& val) + { + // json_sax documents that the passed value may be moved from, + // so hand the buffer over instead of copying it + handle_value(std::move(val)); + return true; + } + + bool binary(binary_t& val) + { + // json_sax documents that the passed value may be moved from, + // so hand the buffer over instead of copying it + handle_value(std::move(val)); + return true; + } + bool start_object(std::size_t len) { ref_stack.push_back(handle_value(BasicJsonType::value_t::object)); @@ -663,16 +1687,13 @@ class json_sax_dom_callback_parser // handle_value() without touching the key stacks if (keep_stack.back()) { - BasicJsonType k = BasicJsonType(val); - static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, k)); + static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, key_value(val))); } return true; } - BasicJsonType k = BasicJsonType(val); - // check callback for the key - const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, k); + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, key_value(val)); key_keep_stack.push_back(keep); // remember the key so a rejected value can be erased without searching // the object for it (kept in lockstep with key_keep_stack) @@ -683,7 +1704,7 @@ class json_sax_dom_callback_parser { auto& obj = *ref_stack.back()->m_data.m_value.object; // a single lookup: insert the placeholder, or find the existing member - const auto inserted = obj.emplace(val, discarded); + const auto inserted = obj.emplace(std::move(val), discarded); const auto it = inserted.first; if (!inserted.second) { @@ -857,6 +1878,31 @@ class json_sax_dom_callback_parser private: + /*! + @brief the value of a key event for the callback + + A string value of its own for every key would allocate it every time, so + one is kept and set to the key. A callback that changed or moved it gets a + new one, as it would have got a new value for every key anyway. + */ + BasicJsonType& key_value(const string_t& key) + { +#if JSON_DIAGNOSTIC_POSITIONS + // a callback may have left positions on the value + key_scratch = BasicJsonType(key); +#else + if (JSON_HEDLEY_LIKELY(key_scratch.is_string())) + { + *key_scratch.m_data.m_value.string = key; + } + else + { + key_scratch = BasicJsonType(key); + } +#endif + return key_scratch; + } + /// if there is a pending duplicate-key stash entry for this exact slot, /// remove it from the stash; if restore_value is true, the stashed /// previous value is moved back into the slot first (use this when the @@ -1104,6 +2150,8 @@ class json_sax_dom_callback_parser const bool allow_exceptions = true; /// a discarded value for the callback BasicJsonType discarded = BasicJsonType::value_t::discarded; + /// the value of the key event for the callback (see key_value()) + BasicJsonType key_scratch {}; /// the lexer reference to obtain the current position lexer_t* m_lexer_ref = nullptr; }; diff --git a/include/nlohmann/detail/input/lexer.hpp b/include/nlohmann/detail/input/lexer.hpp index 8c13cd6d8..aec10c5d2 100644 --- a/include/nlohmann/detail/input/lexer.hpp +++ b/include/nlohmann/detail/input/lexer.hpp @@ -14,6 +14,7 @@ #include // snprintf #include // memcmp #include // initializer_list +#include // numeric_limits #include // char_traits, string #include // move #include // vector @@ -1816,6 +1817,15 @@ scan_number_done: /// significand (see scan_number_bulk_contiguous()) bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept { + // accept() only needs to know that the value is finite (see the parser): + // w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a + // value that underflows is a finite number as well. Only a token that + // may overflow has to be converted to find out. + if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits::max_exponent10)) + { + value_float = 0; + return true; + } value_float = decimal_to_float(s); return true; } diff --git a/include/nlohmann/detail/input/parser.hpp b/include/nlohmann/detail/input/parser.hpp index 38bc9cdc9..3017a29fd 100644 --- a/include/nlohmann/detail/input/parser.hpp +++ b/include/nlohmann/detail/input/parser.hpp @@ -56,8 +56,8 @@ using parser_callback_t = template struct sax_ignores_number_text : std::false_type {}; -template -struct sax_ignores_number_text> : std::true_type {}; +template +struct sax_ignores_number_text> : std::true_type {}; template struct sax_ignores_number_text> : std::true_type {}; @@ -131,7 +131,7 @@ class parser } else { - json_sax_dom_parser sdp(result, allow_exceptions, &m_lexer); + json_sax_dom_parser sdp(result, allow_exceptions, &m_lexer); // in case of an error, return a discarded value if (!parse_dom(sdp, strict)) @@ -243,12 +243,19 @@ class parser JSON_HEDLEY_NON_NULL(2) bool sax_parse_internal(SAX* sax) { + // value to avoid a goto (see comment where set to true) + bool skip_to_state_evaluation = false; + // stack to remember the hierarchy of structured values we are parsing // true = array; false = object (bytes, not std::vector, whose // push_back() is a call with GCC) std::vector states; - // value to avoid a goto (see comment where set to true) - bool skip_to_state_evaluation = false; + // a document that starts with a container pushes onto the stack at + // once: allocate it in one step instead of growing it from 1 byte + if (last_token == token_type::begin_object || last_token == token_type::begin_array) + { + states.reserve(32); + } while (true) { diff --git a/include/nlohmann/detail/view/materialize.hpp b/include/nlohmann/detail/view/materialize.hpp index deebbc098..006bdd0f3 100644 --- a/include/nlohmann/detail/view/materialize.hpp +++ b/include/nlohmann/detail/view/materialize.hpp @@ -40,7 +40,7 @@ template BasicJsonType materialize(const document_data& d, const node* n) { using string_t = typename BasicJsonType::string_t; - using sax_t = json_sax_dom_parser>; + using sax_t = json_sax_dom_parser, true>; using nav = navigation; // the open containers: their next element (or key of the next member), diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 6bfb26771..ff501460a 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -159,9 +159,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec friend class ::nlohmann::detail::binary_writer; template friend class ::nlohmann::detail::binary_reader; - template + template friend class ::nlohmann::detail::json_sax_dom_parser; template + friend class ::nlohmann::detail::json_sax_dom_binary_parser; + template friend class ::nlohmann::detail::json_sax_dom_callback_parser; #if JSON_DIAGNOSTIC_POSITIONS friend struct ::nlohmann::detail::diagnostic_positions; @@ -6288,7 +6290,7 @@ public: from_ubjson/from_bjdata/from_bon8/from_bson overloads Building the @ref binary_reader as a named local, rather than as a - temporary that @ref detail::json_sax_dom_parser::parse is called on in + temporary that @ref detail::json_sax_dom_binary_parser::parse is called on in the same expression, avoids a false-positive cppcheck accessMoved warning in each of the 16 callers. */ @@ -6299,7 +6301,7 @@ public: const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) { basic_json result; - detail::json_sax_dom_parser sdp(result, allow_exceptions); + detail::json_sax_dom_binary_parser sdp(result, allow_exceptions); binary_reader reader(std::move(ia), format, error_handler); if (!reader.sax_parse(&sdp, strict, tag_handler)) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f53ebbbf9..c0a5b82e5 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -9853,9 +9853,13 @@ NLOHMANN_JSON_NAMESPACE_END -#include // find_if, min +#include // copy, find_if, min +#include // array #include +#include // hash #include // numeric_limits +#include // unique_ptr +#include // operator new #include // string #include // enable_if_t #include // move, pair @@ -9880,6 +9884,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // snprintf #include // memcmp #include // initializer_list +#include // numeric_limits #include // char_traits, string #include // move #include // vector @@ -14228,6 +14233,15 @@ scan_number_done: /// significand (see scan_number_bulk_contiguous()) bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept { + // accept() only needs to know that the value is finite (see the parser): + // w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a + // value that underflows is a finite number as well. Only a token that + // may overflow has to be converted to find out. + if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits::max_exponent10)) + { + value_float = 0; + return true; + } value_float = decimal_to_float(s); return true; } @@ -15044,10 +15058,66 @@ scan_number_done: } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // conditional, is_same + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +/*! +@brief Default base class of the @ref basic_json class. + +So that the correct implementations of the copy / move ctors / assign operators +of @ref basic_json do not require complex case distinctions +(no base class / custom base class used as customization point), +@ref basic_json always has a base class. +By default, this class is used because it is empty and thus has no effect +on the behavior of @ref basic_json. + +@note This class intentionally lives in namespace @ref nlohmann rather than + @ref nlohmann::detail. Every @ref basic_json specialization derives from + it (via @ref detail::json_base_class) unless a custom base class is + supplied, which makes its namespace an associated namespace of + @ref basic_json for the purpose of argument-dependent lookup (ADL). If + it lived in `nlohmann::detail`, that namespace - and with it the + library's internal `to_json`/`from_json` overloads - would leak into + ADL for any unqualified `to_json`/`from_json` call a user makes + involving a @ref basic_json argument, silently shadowing the user's own + overloads in some cases. +*/ +struct json_default_base {}; + +namespace detail +{ + +template +using json_base_class = typename std::conditional < + std::is_same::value, + ::nlohmann::json_default_base, + T + >::type; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // #include +// #include + // #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -15344,20 +15414,114 @@ struct diagnostic_positions }; #endif +/*! +@brief a hash table that finds the position of a key in an object whose + elements are kept in a vector (ordered_map) + +The elements stay where they are: the table only holds their hashes and +positions, and the caller compares the keys. +*/ +class object_key_index +{ + public: + static constexpr std::size_t npos = static_cast(-1); + + /// @param[in] element_hashes the hashes of the keys of the elements that are there already + explicit object_key_index(std::vector&& element_hashes) + : hashes(std::move(element_hashes)) + { + std::size_t size = 256; + while (size < 4 * hashes.size()) + { + size *= 2; + } + rebuild(size); + } + + /// @return the position of the element with @a hash for which @a matches(position) holds, or npos + template + std::size_t find(const std::size_t hash, Matches matches) const + { + for (std::size_t slot = hash & mask;; slot = (slot + 1) & mask) + { + const std::size_t entry = table[slot]; + if (entry == 0) + { + return npos; + } + if (hashes[entry - 1] == hash && matches(entry - 1)) + { + return entry - 1; + } + } + } + + /// add the next element, whose position is the number of elements so far + void add(const std::size_t hash) + { + hashes.push_back(hash); + if (hashes.size() * 2 > table.size()) + { + rebuild(table.size() * 2); + } + else + { + place(hash, hashes.size()); + } + } + + private: + void rebuild(const std::size_t size) + { + table.assign(size, 0); + mask = size - 1; + for (std::size_t i = 0; i < hashes.size(); ++i) + { + place(hashes[i], i + 1); + } + } + + void place(const std::size_t hash, const std::size_t entry) + { + std::size_t slot = hash & mask; + while (table[slot] != 0) + { + slot = (slot + 1) & mask; + } + table[slot] = entry; + } + + /// the hash of each element, by position + std::vector hashes; + /// open addressing, a power of two in size: position + 1 of an element, or 0 for a free slot + std::vector table {}; // NOLINT(readability-redundant-member-init) + std::size_t mask = 0; +}; + /*! @brief SAX implementation to create a JSON value from SAX events This class implements the @ref json_sax interface and processes the SAX events to create a JSON value which makes it basically a DOM parser. The structure or -hierarchy of the JSON value is managed by the stack `ref_stack` which contains -a pointer to the respective array or object for each recursion depth. +hierarchy of the JSON value is managed by the stack `frames` which contains +an entry for each open array or object. The parser of the text formats, for +which the size of a container is not known up front, collects the elements of +an array and the members of an `ordered_json` object on a stack and moves them +into a container of the exact size (with @a Collect, as basic_json's own parse +functions use it); the binary readers use the plain +@ref json_sax_dom_binary_parser instead. After successful parsing, the value that is passed by reference to the -constructor contains the parsed value. +constructor contains the parsed value. Without @a Collect, every value is +stored in place as it arrives, so after a parse error the value holds what was +read up to it. @tparam BasicJsonType the JSON type +@tparam Collect whether to collect the values of containers of unknown size on + a stack (only for a caller that does not use the value after a + parse error) */ -template +template class json_sax_dom_parser { public: @@ -15430,6 +15594,930 @@ class json_sax_dom_parser return true; } + bool start_object(std::size_t len) + { +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // the values need their final place from the start (their parent + // pointers and positions refer to it), so no buffering + return start_stored_object(len); +#else + return start_object(len, std::integral_constant {}); +#endif + } + + bool key(string_t& val) + { + JSON_ASSERT(!frames.empty()); + return key(val, std::integral_constant {}); + } + + bool end_object() + { + JSON_ASSERT(!frames.empty()); + return end_object(std::integral_constant {}); + } + + bool start_array(std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > array_t().max_size())) + { + // the array does not exist yet: report with an empty one + BasicJsonType too_large(BasicJsonType::value_t::array); + return container_too_large_error(*this, len, "array", &too_large); + } + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // the values need their final place from the start (their parent + // pointers and positions refer to it), so no buffering + start_stored_array(len); +#else + if (!collects(len)) + { + // the number of elements is known (or the elements must be in + // place as they arrive): store them directly + start_stored_array(len); + } + else + { + // the number of elements is not known, so growing the array + // would reallocate it (and, being a vector, move its elements) + // again and again. Collect the elements on a stack shared by all + // arrays instead, and copy them to an array of the exact size + // when the closing bracket arrives. + BasicJsonType* slot = nullptr; + std::size_t dest_member = 0; + container_destination(slot, dest_member); + reserve_elements(); + frames.push_back(frame{nullptr, nullptr, slot, elements.size(), ordered_state(dest_member, false)}); + in_object = false; + stored_array = nullptr; + } +#endif + return true; + } + + bool end_array() + { + JSON_ASSERT(!frames.empty()); + JSON_ASSERT(frames.back().object == nullptr); + const frame f = frames.back(); + frames.pop_back(); + reenter_enclosing_container(); + + if (f.array == nullptr) + { + // the elements are the top of the shared elements stack, from + // f.start on (the elements below belong to enclosing arrays that + // are still open): move them into the array and pop them + const auto first = elements.begin() + static_cast(f.start); + auto* const arr = BasicJsonType::template create(std::make_move_iterator(first), std::make_move_iterator(elements.end())); + BasicJsonType value; + value.m_data.m_value.array = arr; + value.m_data.m_type = BasicJsonType::value_t::array; + elements.erase(first, elements.end()); + + place_container(f, frames.empty() ? nullptr : &frames.back(), std::move(value), true); + return true; + } + +#if JSON_DIAGNOSTIC_POSITIONS + diagnostic_positions::set_container_end(*f.slot, m_lexer_ref); +#endif +#if JSON_DIAGNOSTICS + f.slot->set_parents(); +#endif + return true; + } + + template + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, + const Exception& ex) + { + errored = true; + static_cast(ex); + if (allow_exceptions) + { + JSON_THROW(ex); + } + return false; + } + + constexpr bool is_errored() const + { + return errored; + } + + private: + using object_t = typename BasicJsonType::object_t; + using array_t = typename BasicJsonType::array_t; + + /// whether objects keep their elements in a vector and a key can be hashed, + /// so that a big object can find the keys it has in a hash table instead of + /// searching all of them + template + using detect_indexable_keys = decltype(std::declval().emplace_new_key(std::declval(), std::declval()), + std::hash()(std::declval())); + static constexpr bool indexed_keys = is_detected::value; + + /// what a frame holds for objects that are kept in a vector: the key index + /// of an object with many keys, and for an object that is collected on the + /// stack the member of the enclosing object it will become + struct ordered_frame + { + /// the entry of key_indexes of an object that has one, else nullptr + object_key_index* index; + /// object or array without a slot in an object collected on the stack: + /// the index of the member in `members` it goes to + std::size_t dest_member; + /// whether this is an object collected on the stack + bool buffered; + }; + struct no_ordered_frame {}; + using ordered_frame_state = typename std::conditional::type; + + static ordered_frame make_ordered_state(const std::size_t dest_member, const bool buffered, std::true_type /*indexed*/) noexcept + { + ordered_frame state = {nullptr, dest_member, buffered}; + return state; + } + + static no_ordered_frame make_ordered_state(const std::size_t /*dest_member*/, const bool /*buffered*/, std::false_type /*indexed*/) noexcept + { + return no_ordered_frame(); + } + + static ordered_frame_state ordered_state(const std::size_t dest_member, const bool buffered) noexcept + { + return make_ordered_state(dest_member, buffered, std::integral_constant {}); + } + + /// an array or object that is being read + struct frame + { + /// the object, or nullptr for an array + object_t* object; + /// the array if its elements are stored in it as they arrive, or + /// nullptr for an array and an object + array_t* array; + /// array without `array`: where the finished array goes (nullptr: it + /// is an element of the enclosing array); otherwise: the value that + /// holds the object or array + BasicJsonType* slot; + /// array without `array`: the index of its first element in `elements` + std::size_t start; + /// what objects in a vector add (see ordered_frame) + ordered_frame_state ordered; + }; + + /// the open arrays and objects; the first few live in the object itself, so + /// that the common documents do not allocate for them + class frame_stack + { + public: + frame_stack() noexcept + : local() // NOLINT(readability-redundant-member-init) + {} + frame_stack(const frame_stack&) = delete; + frame_stack& operator=(const frame_stack&) = delete; + + frame_stack(frame_stack&& other) noexcept + : local() // NOLINT(readability-redundant-member-init) + { + take(other); + } + + frame_stack& operator=(frame_stack&& other) noexcept + { + if (this != &other) + { + release(); + take(other); + } + return *this; + } + + ~frame_stack() = default; + + bool empty() const noexcept + { + return count == 0; + } + + std::size_t size() const noexcept + { + return count; + } + + frame& back() noexcept + { + return data[count - 1]; + } + + frame& operator[](const std::size_t i) noexcept + { + return data[i]; + } + + void push_back(const frame& f) + { + if (JSON_HEDLEY_UNLIKELY(count == capacity)) + { + grow(); + } + data[count++] = f; + } + + void pop_back() noexcept + { + JSON_ASSERT(count > 0); + --count; + } + + private: + JSON_HEDLEY_NEVER_INLINE + void grow() + { + const std::size_t new_capacity = capacity * 2; + std::unique_ptr new_heap(new frame[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::copy(data, data + count, new_heap.get()); + heap = std::move(new_heap); + data = heap.get(); + capacity = new_capacity; + } + + void release() noexcept + { + heap.reset(); + data = local.data(); + capacity = local_capacity; + } + + /// make the contents of @a other our own + void take(frame_stack& other) noexcept + { + if (other.heap == nullptr) + { + std::copy(other.local.begin(), other.local.begin() + static_cast(other.count), local.begin()); + data = local.data(); + capacity = local_capacity; + } + else + { + heap = std::move(other.heap); + data = heap.get(); + capacity = other.capacity; + other.data = other.local.data(); + other.capacity = local_capacity; + } + count = other.count; + other.count = 0; + } + + static constexpr std::size_t local_capacity = 8; + + /// the first frames, left uninitialized: a frame is written before it is + /// read (the user-provided constructor keeps the member initialization, + /// which -Weffc++ asks for, from zeroing them) + struct local_frames : std::array + { + local_frames() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + + local_frames local; + /// the frames once there are more than local_capacity of them + std::unique_ptr heap {}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init) + frame* data = local.data(); + std::size_t count = 0; + std::size_t capacity = local_capacity; + }; + + void release_key_index(const frame& f, std::true_type /*indexed*/) + { + if (JSON_HEDLEY_UNLIKELY(f.ordered.index != nullptr)) + { + ordered_data.key_indexes.pop_back(); + } + } + + static void release_key_index(const frame& /*f*/, std::false_type /*indexed*/) noexcept {} + + /// the number of keys from which an object looks them up in a hash table + static constexpr std::size_t key_index_threshold = 64; + + /// add @a key to the object of @a f (or find it, if it is there), searching it in std::map + static BasicJsonType* insert_key(frame& f, string_t&& key, std::false_type /*indexed*/) + { + return &f.object->operator[](std::move(key)); + } + + /// add @a key to the object of @a f (or find it, if it is there), searching it in a vector + BasicJsonType* insert_key(frame& f, string_t&& key, std::true_type /*indexed*/) + { + object_t& obj = *f.object; + if (JSON_HEDLEY_LIKELY(obj.size() < key_index_threshold)) + { + return &obj[std::move(key)]; + } + return insert_key_with_index(f, std::move(key)); + } + + /// add @a key to the big object of @a f (or find it, if it is there), using its key index + JSON_HEDLEY_NEVER_INLINE + BasicJsonType* insert_key_with_index(frame& f, string_t&& key) + { + object_t& obj = *f.object; + const std::hash hasher {}; + const std::size_t hash = hasher(key); + if (JSON_HEDLEY_UNLIKELY(f.ordered.index == nullptr)) + { + std::vector hashes; + hashes.reserve(obj.size()); + for (const auto& element : obj) + { + hashes.push_back(hasher(element.first)); + } + ordered_data.key_indexes.emplace_back(new object_key_index(std::move(hashes))); + f.ordered.index = ordered_data.key_indexes.back().get(); + } + + const std::size_t position = f.ordered.index->find(hash, [&obj, &key](std::size_t i) + { + return (obj.begin() + static_cast(i))->first == key; + }); + if (position != object_key_index::npos) + { + return &(obj.begin() + static_cast(position))->second; + } + + obj.emplace_new_key(key, BasicJsonType()); + f.ordered.index->add(hash); + return &obj.back().second; + } + + /// where an array or object that starts now will go when it is finished: + /// @a slot is the value to assign it to (the root or a member of an object + /// that is stored directly), or nullptr if it is an element of the + /// enclosing array or a member of the enclosing object that is collected on + /// the stack, in which case @a dest_member is the member + void container_destination(BasicJsonType*& slot, std::size_t& dest_member) + { + if (frames.empty()) + { + slot = &root; + } + else if (frames.back().object != nullptr) + { + slot = object_element; + } + else + { + collected_object_member(frames.back(), dest_member, std::integral_constant {}); + } + } + + void collected_object_member(const frame& enclosing, std::size_t& dest_member, std::true_type /*indexed*/) const noexcept + { + if (enclosing.ordered.buffered) + { + dest_member = ordered_data.member_index; + } + } + + static void collected_object_member(const frame& /*enclosing*/, std::size_t& /*dest_member*/, std::false_type /*indexed*/) noexcept {} + + static bool is_collected_object(const frame& f, std::true_type /*indexed*/) noexcept + { + return f.ordered.buffered; + } + + static bool is_collected_object(const frame& /*f*/, std::false_type /*indexed*/) noexcept + { + return false; + } + + /// put a finished array or object that was collected on the stack, or + /// that outgrew it, where it belongs; @a enclosing is the container it is in. + /// @a enclosing_is_innermost tells whether that is the container that gets + /// the next value, so that it can be switched to storing its elements + /// directly if the value was the one too many. + void place_container(const frame& f, frame* enclosing, BasicJsonType&& value, const bool enclosing_is_innermost) + { + if (f.slot != nullptr) + { + *f.slot = std::move(value); + } + else if (is_collected_object(*enclosing, std::integral_constant {})) + { + place_member(f, std::move(value), std::integral_constant {}); + } + else if (enclosing->array != nullptr) + { + enclosing->array->emplace_back(std::move(value)); + } + else + { + elements.emplace_back(std::move(value)); + if (JSON_HEDLEY_UNLIKELY(enclosing_is_innermost && elements.size() - enclosing->start > buffer_limit)) + { + spill(*enclosing); + } + } + } + + void place_member(const frame& f, BasicJsonType&& value, std::true_type /*indexed*/) + { + ordered_data.members[f.ordered.dest_member].second = std::move(value); + } + + static void place_member(const frame& /*f*/, BasicJsonType&& /*value*/, std::false_type /*indexed*/) noexcept {} + + /// store the members of a new object directly in it + bool start_stored_object(std::size_t len) + { + BasicJsonType* const ref = store(BasicJsonType::value_t::object); + frames.push_back(frame{ref->m_data.m_value.object, nullptr, ref, 0, ordered_state(0, false)}); + in_object = true; + stored_array = nullptr; + +#if JSON_DIAGNOSTIC_POSITIONS + // Ensure this is after the call to store to ensure correct start position. + diagnostic_positions::set_container_start(*ref, m_lexer_ref); +#endif + + return check_container_size(*this, len, "object", ref); + } + + /// objects in std::map and the like have no use for collecting their members + bool start_object(std::size_t len, std::false_type /*indexed*/) + { + return start_stored_object(len); + } + + /// An object in a vector of members adds a member by appending it to the + /// vector, which grows it again and again (and copies the keys each time). + /// Collect the members of an object of unknown size on a stack shared by + /// all open objects instead, and move them into a vector of the exact size + /// when the closing brace arrives. + bool start_object(std::size_t len, std::true_type /*indexed*/) + { + if (!collects(len)) + { + return start_stored_object(len); + } + + BasicJsonType* slot = nullptr; + std::size_t dest_member = 0; + container_destination(slot, dest_member); + frames.push_back(frame{nullptr, nullptr, slot, ordered_data.members.size(), ordered_state(dest_member, true)}); + in_object = false; + stored_array = nullptr; + return true; + } + + bool key(string_t& val, std::false_type /*indexed*/) + { + JSON_ASSERT(frames.back().object != nullptr); + + // add null at the given key and store the reference for later; the + // key is moved, as the lexer starts the next token with reset() + object_element = insert_key(frames.back(), std::move(val), std::false_type {}); + return true; + } + + bool key(string_t& val, std::true_type /*indexed*/) + { + frame& f = frames.back(); + if (JSON_HEDLEY_UNLIKELY(f.object == nullptr)) + { + JSON_ASSERT(f.ordered.buffered); + if (collect_member(f, val)) + { + return true; + } + // the object has grown big: it is stored directly from here on + } + object_element = insert_key(f, std::move(val), std::true_type {}); + return true; + } + + /*! + @brief add a member to the object that is collected on the stack, or find the one with the key + @return false if the object has collected enough members to be stored + directly, which is done and which leaves the key to be inserted + */ + bool collect_member(frame& f, string_t& key) + { + for (std::size_t i = f.start; i < ordered_data.members.size(); ++i) + { + if (ordered_data.members[i].first == key) + { + ordered_data.member_index = i; + return true; + } + } + + if (JSON_HEDLEY_UNLIKELY(ordered_data.members.size() - f.start >= object_buffer_limit)) + { + spill_object(f); + return false; + } + + reserve_members(); + ordered_data.members.emplace_back(std::move(key), BasicJsonType()); + ordered_data.member_index = ordered_data.members.size() - 1; + return true; + } + + /// the members of an object that is collected on the stack as an object; + /// they are the top of the shared member stack, from f.start on (the + /// members below belong to enclosing objects that are still open) + object_t* members_to_object(const frame& f, BasicJsonType& value) + { + const auto first = ordered_data.members.begin() + static_cast(f.start); + auto* const obj = BasicJsonType::template create(std::make_move_iterator(first), std::make_move_iterator(ordered_data.members.end())); + value.m_data.m_value.object = obj; + value.m_data.m_type = BasicJsonType::value_t::object; + ordered_data.members.erase(first, ordered_data.members.end()); + return obj; + } + + /// switch an object that collects its members on the stack to storing them directly + JSON_HEDLEY_NEVER_INLINE + void spill_object(frame& f) + { + BasicJsonType value; + f.object = members_to_object(f, value); + f.ordered.buffered = false; + in_object = true; + // false positive: an object without an enclosing container is the root, + // which has a slot, so place_container() does not use the nullptr + // @infer-ignore NULLPTR_DEREFERENCE + place_container(f, frames.size() >= 2 ? &frames[frames.size() - 2] : nullptr, std::move(value), false); + } + + bool end_object(std::false_type /*indexed*/) + { + return end_stored_object(); + } + + bool end_object(std::true_type /*indexed*/) + { + if (JSON_HEDLEY_LIKELY(frames.back().object != nullptr)) + { + return end_stored_object(); + } + + // the members are on the stack: make them the object + const frame f = frames.back(); + frames.pop_back(); + reenter_enclosing_container(); + + BasicJsonType value; + members_to_object(f, value); + place_container(f, frames.empty() ? nullptr : &frames.back(), std::move(value), true); + return true; + } + + bool end_stored_object() + { + JSON_ASSERT(frames.back().object != nullptr); + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // objects are never collected, so their value is where it was stored + BasicJsonType* const ref = frames.back().slot; +#endif +#if JSON_DIAGNOSTIC_POSITIONS + diagnostic_positions::set_container_end(*ref, m_lexer_ref); +#endif +#if JSON_DIAGNOSTICS + ref->set_parents(); +#endif + + release_key_index(frames.back(), std::integral_constant {}); + frames.pop_back(); + reenter_enclosing_container(); + return true; + } + + /// the number of members an object collects on the stack before it + /// switches to storing them directly + static constexpr std::size_t object_buffer_limit = 32; + + void reserve_members() + { + if (JSON_HEDLEY_UNLIKELY(ordered_data.members.capacity() == 0)) + { + ordered_data.members.reserve(16); + } + } + + /// the number of elements an array collects on the stack before it + /// switches to storing them directly, so that it does not hold up the + /// stack with its elements (and move all of them in the end) + static constexpr std::size_t buffer_limit = 16; + + void reserve_elements() + { + if (JSON_HEDLEY_UNLIKELY(elements.capacity() == 0)) + { + elements.reserve(32); + } + } + + /// store the elements of a new array directly in it + void start_stored_array(const std::size_t len) + { + BasicJsonType* const ref = store(BasicJsonType::value_t::array); + frames.push_back(frame{nullptr, ref->m_data.m_value.array, ref, 0, ordered_state(0, false)}); + in_object = false; + stored_array = ref->m_data.m_value.array; + +#if JSON_DIAGNOSTIC_POSITIONS + // Ensure this is after the call to store to ensure correct start position. + diagnostic_positions::set_container_start(*ref, m_lexer_ref); +#endif + + if (len != detail::unknown_size()) + { + reserve_array(*ref->m_data.m_value.array, len, priority_tag<1> {}); + } + } + + /*! + @brief store a value as the next element of the open array or object (or + as the root) + @return the stored value + */ + template + JSON_HEDLEY_RETURNS_NON_NULL + BasicJsonType* store(Value&& v) + { + if (stored_array != nullptr) + { + stored_array->emplace_back(std::forward(v)); + return &stored_array->back(); + } + + if (in_object) + { + JSON_ASSERT(object_element); + set_member(*object_element, std::forward(v), std::is_same {}); + return object_element; + } + + return store_collected(std::forward(v), std::integral_constant {}); + } + + /// store a value that is the root, an element of an array that is collected + /// on the stack, or (in a vector) a member of an object that is + template + BasicJsonType* store_collected(Value&& v, std::true_type /*indexed*/) + { + if (!frames.empty() && frames.back().ordered.buffered) + { + BasicJsonType& member = ordered_data.members[ordered_data.member_index].second; + set_member(member, std::forward(v), std::is_same {}); + return &member; + } + return store_collected(std::forward(v), std::false_type {}); + } + + template + BasicJsonType* store_collected(Value&& v, std::false_type /*indexed*/) + { + if (frames.empty()) + { + root = BasicJsonType(std::forward(v)); + return &root; + } + + frame& f = frames.back(); + elements.emplace_back(std::forward(v)); + if (JSON_HEDLEY_UNLIKELY(elements.size() - f.start > buffer_limit)) + { + return spill(f); + } + return &elements.back(); + } + + /*! + @brief switch an array that collects its elements on the stack to storing + them directly + @return the last element of the array + */ + JSON_HEDLEY_NEVER_INLINE + BasicJsonType* spill(frame& f) + { + JSON_ASSERT(f.array == nullptr); + const auto first = elements.begin() + static_cast(f.start); + BasicJsonType value(BasicJsonType::value_t::array); + array_t& arr = *value.m_data.m_value.array; + // the capacity a vector that grew by itself has with this many elements + reserve_array(arr, 2 * buffer_limit, priority_tag<1> {}); + arr.insert(arr.end(), std::make_move_iterator(first), std::make_move_iterator(elements.end())); + elements.erase(first, elements.end()); + f.array = &arr; + stored_array = &arr; + + // the array takes the place its first element had on the stack + place_container(f, frames.size() >= 2 ? &frames[frames.size() - 2] : nullptr, std::move(value), false); + return &arr.back(); + } + + /// set the value of the member key() just added + template + JSON_HEDLEY_ALWAYS_INLINE + static void set_member(BasicJsonType& member, Value&& v, std::true_type /*default base*/) + { + if (JSON_HEDLEY_LIKELY(member.is_null())) + { + // a new member is null and holds nothing: construct the value + // over it instead of destroying and swapping + ::new (static_cast(&member)) BasicJsonType(std::forward(v)); + } + else + { + // the key occurred before + member = BasicJsonType(std::forward(v)); + } + } + + /// a custom base class may care about the lifetime of the value + template + static void set_member(BasicJsonType& member, Value&& v, std::false_type /*custom base*/) + { + member = BasicJsonType(std::forward(v)); + } + + /// whether the values of a container of @a len elements are collected on the stack + static bool collects(const std::size_t len) noexcept + { + return collects(len, std::integral_constant {}); + } + + static bool collects(const std::size_t len, std::true_type /*collect*/) noexcept + { + return len == detail::unknown_size(); + } + + static bool collects(const std::size_t /*len*/, std::false_type /*collect*/) noexcept + { + return false; + } + + /// after a container was closed: the one it was in is the one that gets the next value + void reenter_enclosing_container() + { + if (frames.empty()) + { + in_object = false; + stored_array = nullptr; + } + else + { + const frame& f = frames.back(); + in_object = f.object != nullptr; + stored_array = f.array; + } + } + + template + void handle_value(Value&& v) + { +#if JSON_DIAGNOSTIC_POSITIONS + BasicJsonType* const stored = store(std::forward(v)); + diagnostic_positions::set_from_lexer(*stored, m_lexer_ref); +#else + store(std::forward(v)); +#endif + } + + /// the parsed JSON value + BasicJsonType& root; + /// the open arrays and objects + frame_stack frames {}; + /// the elements of the open arrays that are not stored in them yet + std::vector elements {}; + /// what objects in a vector need besides the frames (std::map based objects + /// need nothing, so they do not carry it) + struct ordered_data_t + { + /// the members of the open objects in a vector that are not stored in them yet + std::vector> members {}; + /// the member of the innermost such object that key() added or found last + std::size_t member_index = 0; + /// the key indexes of the open objects that have one, innermost last + std::vector> key_indexes {}; // NOLINT(readability-redundant-member-init) + }; + struct no_ordered_data_t {}; + typename std::conditional::type ordered_data {}; + /// helper to hold the reference for the next object element + BasicJsonType* object_element = nullptr; + /// whether the innermost container is an object (frames.back().object) + bool in_object = false; + /// the array that stores the elements of the innermost array as they + /// arrive (frames.back().array), or nullptr + array_t* stored_array = nullptr; + /// whether a syntax error occurred + bool errored = false; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; + /// the lexer reference to obtain the current position + lexer_t* m_lexer_ref = nullptr; +}; + +/*! +@brief SAX implementation to create a JSON value from the events of a binary reader + +The DOM parser of the text formats (json_sax_dom_parser) collects the elements +of arrays and the members of objects on a stack and builds them in one +exact-size allocation, which pays off because a text parser does not know the +number of elements up front. Binary formats announce the size of their +containers (CBOR, MessagePack, BSON and BON8 do, and so do the optimized +containers of UBJSON and BJData), so they store each value directly in its +container, and the bookkeeping of the collecting parser would be pure +overhead for them. This is the plain parser: the structure or hierarchy of the +JSON value is managed by the stack `ref_stack` which contains a pointer to the +respective array or object for each recursion depth. + +After successful parsing, the value that is passed by reference to the +constructor contains the parsed value. + +@tparam BasicJsonType the JSON type +*/ +template +class json_sax_dom_binary_parser +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using lexer_t = lexer; + + /*! + @param[in,out] r reference to a JSON value that is manipulated while + parsing + @param[in] allow_exceptions_ whether parse errors yield exceptions + */ + explicit json_sax_dom_binary_parser(BasicJsonType& r, const bool allow_exceptions_ = true, lexer_t* lexer_ = nullptr) + : root(r), allow_exceptions(allow_exceptions_), m_lexer_ref(lexer_) + {} + + // make class move-only + json_sax_dom_binary_parser(const json_sax_dom_binary_parser&) = delete; + json_sax_dom_binary_parser(json_sax_dom_binary_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + json_sax_dom_binary_parser& operator=(const json_sax_dom_binary_parser&) = delete; + json_sax_dom_binary_parser& operator=(json_sax_dom_binary_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + ~json_sax_dom_binary_parser() = default; + + bool null() + { + handle_value(nullptr); + return true; + } + + bool boolean(bool val) + { + handle_value(val); + return true; + } + + bool number_integer(number_integer_t val) + { + handle_value(val); + return true; + } + + bool number_unsigned(number_unsigned_t val) + { + handle_value(val); + return true; + } + + bool number_float(number_float_t val, const string_t& /*unused*/) + { + handle_value(val); + return true; + } + + bool string(string_t& val) + { + // json_sax documents that the passed value may be moved from, + // so hand the buffer over instead of copying it + handle_value(std::move(val)); + return true; + } + + bool binary(binary_t& val) + { + // json_sax documents that the passed value may be moved from, + // so hand the buffer over instead of copying it + handle_value(std::move(val)); + return true; + } + bool start_object(std::size_t len) { ref_stack.push_back(handle_value(BasicJsonType::value_t::object)); @@ -15692,16 +16780,13 @@ class json_sax_dom_callback_parser // handle_value() without touching the key stacks if (keep_stack.back()) { - BasicJsonType k = BasicJsonType(val); - static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, k)); + static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, key_value(val))); } return true; } - BasicJsonType k = BasicJsonType(val); - // check callback for the key - const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, k); + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, key_value(val)); key_keep_stack.push_back(keep); // remember the key so a rejected value can be erased without searching // the object for it (kept in lockstep with key_keep_stack) @@ -15712,7 +16797,7 @@ class json_sax_dom_callback_parser { auto& obj = *ref_stack.back()->m_data.m_value.object; // a single lookup: insert the placeholder, or find the existing member - const auto inserted = obj.emplace(val, discarded); + const auto inserted = obj.emplace(std::move(val), discarded); const auto it = inserted.first; if (!inserted.second) { @@ -15886,6 +16971,31 @@ class json_sax_dom_callback_parser private: + /*! + @brief the value of a key event for the callback + + A string value of its own for every key would allocate it every time, so + one is kept and set to the key. A callback that changed or moved it gets a + new one, as it would have got a new value for every key anyway. + */ + BasicJsonType& key_value(const string_t& key) + { +#if JSON_DIAGNOSTIC_POSITIONS + // a callback may have left positions on the value + key_scratch = BasicJsonType(key); +#else + if (JSON_HEDLEY_LIKELY(key_scratch.is_string())) + { + *key_scratch.m_data.m_value.string = key; + } + else + { + key_scratch = BasicJsonType(key); + } +#endif + return key_scratch; + } + /// if there is a pending duplicate-key stash entry for this exact slot, /// remove it from the stash; if restore_value is true, the stashed /// previous value is moved back into the slot first (use this when the @@ -16133,6 +17243,8 @@ class json_sax_dom_callback_parser const bool allow_exceptions = true; /// a discarded value for the callback BasicJsonType discarded = BasicJsonType::value_t::discarded; + /// the value of the key event for the callback (see key_value()) + BasicJsonType key_scratch {}; /// the lexer reference to obtain the current position lexer_t* m_lexer_ref = nullptr; }; @@ -16415,7 +17527,7 @@ JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 2 /*! @brief deserialization of BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ -template> +template> class binary_reader { using number_integer_t = typename BasicJsonType::number_integer_t; @@ -20872,8 +21984,8 @@ using parser_callback_t = template struct sax_ignores_number_text : std::false_type {}; -template -struct sax_ignores_number_text> : std::true_type {}; +template +struct sax_ignores_number_text> : std::true_type {}; template struct sax_ignores_number_text> : std::true_type {}; @@ -20947,7 +22059,7 @@ class parser } else { - json_sax_dom_parser sdp(result, allow_exceptions, &m_lexer); + json_sax_dom_parser sdp(result, allow_exceptions, &m_lexer); // in case of an error, return a discarded value if (!parse_dom(sdp, strict)) @@ -21059,12 +22171,19 @@ class parser JSON_HEDLEY_NON_NULL(2) bool sax_parse_internal(SAX* sax) { + // value to avoid a goto (see comment where set to true) + bool skip_to_state_evaluation = false; + // stack to remember the hierarchy of structured values we are parsing // true = array; false = object (bytes, not std::vector, whose // push_back() is a call with GCC) std::vector states; - // value to avoid a goto (see comment where set to true) - bool skip_to_state_evaluation = false; + // a document that starts with a container pushes onto the stack at + // once: allocate it in one step instead of growing it from 1 byte + if (last_token == token_type::begin_object || last_token == token_type::begin_array) + { + states.reserve(32); + } while (true) { @@ -22446,58 +23565,6 @@ NLOHMANN_JSON_NAMESPACE_END // #include // #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.12.0 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // conditional, is_same - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN - -/*! -@brief Default base class of the @ref basic_json class. - -So that the correct implementations of the copy / move ctors / assign operators -of @ref basic_json do not require complex case distinctions -(no base class / custom base class used as customization point), -@ref basic_json always has a base class. -By default, this class is used because it is empty and thus has no effect -on the behavior of @ref basic_json. - -@note This class intentionally lives in namespace @ref nlohmann rather than - @ref nlohmann::detail. Every @ref basic_json specialization derives from - it (via @ref detail::json_base_class) unless a custom base class is - supplied, which makes its namespace an associated namespace of - @ref basic_json for the purpose of argument-dependent lookup (ADL). If - it lived in `nlohmann::detail`, that namespace - and with it the - library's internal `to_json`/`from_json` overloads - would leak into - ADL for any unqualified `to_json`/`from_json` call a user makes - involving a @ref basic_json argument, silently shadowing the user's own - overloads in some cases. -*/ -struct json_default_base {}; - -namespace detail -{ - -template -using json_base_class = typename std::conditional < - std::is_same::value, - ::nlohmann::json_default_base, - T - >::type; - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ @@ -34995,9 +36062,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec friend class ::nlohmann::detail::binary_writer; template friend class ::nlohmann::detail::binary_reader; - template + template friend class ::nlohmann::detail::json_sax_dom_parser; template + friend class ::nlohmann::detail::json_sax_dom_binary_parser; + template friend class ::nlohmann::detail::json_sax_dom_callback_parser; #if JSON_DIAGNOSTIC_POSITIONS friend struct ::nlohmann::detail::diagnostic_positions; @@ -41124,7 +42193,7 @@ public: from_ubjson/from_bjdata/from_bon8/from_bson overloads Building the @ref binary_reader as a named local, rather than as a - temporary that @ref detail::json_sax_dom_parser::parse is called on in + temporary that @ref detail::json_sax_dom_binary_parser::parse is called on in the same expression, avoids a false-positive cppcheck accessMoved warning in each of the 16 callers. */ @@ -41135,7 +42204,7 @@ public: const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) { basic_json result; - detail::json_sax_dom_parser sdp(result, allow_exceptions); + detail::json_sax_dom_binary_parser sdp(result, allow_exceptions); binary_reader reader(std::move(ia), format, error_handler); if (!reader.sax_parse(&sdp, strict, tag_handler)) { diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 65459b072..1e2f8f64c 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -6049,7 +6049,7 @@ template BasicJsonType materialize(const document_data& d, const node* n) { using string_t = typename BasicJsonType::string_t; - using sax_t = json_sax_dom_parser>; + using sax_t = json_sax_dom_parser, true>; using nav = navigation; // the open containers: their next element (or key of the next member), diff --git a/src/modules/json.cppm b/src/modules/json.cppm index ffc9e00e2..3917e0722 100644 --- a/src/modules/json.cppm +++ b/src/modules/json.cppm @@ -60,6 +60,7 @@ inline namespace json_literals namespace detail { using NLOHMANN_JSON_NAMESPACE::detail::json_sax_dom_callback_parser; + using NLOHMANN_JSON_NAMESPACE::detail::json_sax_dom_parser; using NLOHMANN_JSON_NAMESPACE::detail::unknown_size; } // namespace detail diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index 1d1b5f40b..dd78f7ff4 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -2952,3 +2952,81 @@ TEST_CASE("diagnostic positions: value lifetime, input adapters, and SAX") } } #endif + +TEST_CASE("accept() agrees with parse() on floats around the overflow limit") +{ + // accept() does not convert a float that cannot overflow; the tokens + // around the limit (and beyond it) must still be rejected exactly as + // parse() rejects them + const std::vector numbers = + { + "1e0", "123.456e-5", "1e-400", "-1e-400", "1e307", "1e308", "1e309", "-1e309", + "10e307", "100e307", "0.1e309", "0.1e310", "1E+308", "1E+309", + "1.7976931348623157e308", "1.7976931348623159e308", "1.8e308", "-1.7976931348623157e308", + "12345678901234567e291", "12345678901234567e292", "123456789012345678e290", "123456789012345678e291", + "1234567890123456789e289", "1234567890123456789e290", "9999999999999999999e289", "9999999999999999999e290", + "0.00000000000000000001e328", "0.00000000000000000001e329" + }; + + for (const char* number : numbers) + { + for (const std::string& text : + { + std::string(number), "[" + std::string(number) + "]", "{\"a\":" + std::string(number) + "}" + }) + { + INFO(text); + const bool parses = !json::parse(text, nullptr, false).is_discarded(); + CHECK(json::accept(text) == parses); + } + } +} + +namespace +{ +// a DOM parser that reports parse errors without throwing, as in the sax_no_exception example +template +class quiet_dom_parser : public nlohmann::detail::json_sax_dom_parser +{ + public: + explicit quiet_dom_parser(BasicJsonType& j) + : nlohmann::detail::json_sax_dom_parser(j, false) + {} + + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const nlohmann::detail::exception& /*unused*/) + { + return false; + } +}; + +template +std::string read_before_error(const std::string& text) +{ + BasicJsonType j; + quiet_dom_parser sax(j); + CHECK(!BasicJsonType::sax_parse(text, &sax)); + return j.dump(); +} +} // namespace + +TEST_CASE("a json_sax_dom_parser keeps the values read before a parse error") +{ + // a json_sax_dom_parser that a user constructs stores every value in place, so a + // handler that does not throw on a parse error sees what was read before it + const std::vector> cases = + { + {"[1,2,3,]", "[1,2,3]"}, + {"[[1,[2,3],4],[5,", "[[1,[2,3],4],[5]]"}, + {R"({"a":1,"b":[1,2,{"c":)", R"({"a":1,"b":[1,2,{"c":null}]})"}, + {R"({"a":1,"a":)", R"({"a":1})"}, + {"[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,]", "[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]"}, + {R"({"k0":0,"k1":1,"k2":{"x":[true,)", R"({"k0":0,"k1":1,"k2":{"x":[true]}})"}, + {R"([{},[],{"a":[]},x)", R"([{},[],{"a":[]}])"}, + }; + for (const auto& c : cases) + { + CAPTURE(c.first) + CHECK(read_before_error(c.first) == c.second); + CHECK(read_before_error(c.first) == c.second); + } +} diff --git a/tests/src/unit-ordered_json.cpp b/tests/src/unit-ordered_json.cpp index 135dedade..c328d8aea 100644 --- a/tests/src/unit-ordered_json.cpp +++ b/tests/src/unit-ordered_json.cpp @@ -12,6 +12,11 @@ using nlohmann::json; using nlohmann::ordered_json; +#include +#include +#include +#include + TEST_CASE("ordered_json") { json j; @@ -237,3 +242,276 @@ TEST_CASE("regression test for issue #5673 - ordered_json::emplace with a non-rv CHECK(oj.dump() == "{\"a\":1}"); } } + +namespace +{ +// the sizes around which the DOM parser changes how it stores the elements of +// an array (16) and the members of an object in a vector (32, and 64 for the +// key index) +const std::array container_sizes = {{0, 1, 2, 3, 15, 16, 17, 18, 31, 32, 33, 34, 63, 64, 65, 66, 200}}; + +template +std::string array_text(const std::size_t n, const std::size_t depth) +{ + std::string text = "["; + for (std::size_t i = 0; i < n; ++i) + { + text += (i == 0 ? "" : ","); + // alternate scalars, empty containers and nested containers + switch (i % 4) + { + case 0: + text += std::to_string(i); + break; + case 1: + text += "\"s" + std::to_string(i) + "\""; + break; + case 2: + text += depth == 0 ? "[]" : array_text(n / 2, depth - 1); + break; + default: + text += depth == 0 ? "{}" : "{\"a\":" + array_text(n / 3, depth - 1) + ",\"b\":null}"; + break; + } + } + return text + "]"; +} + +template +std::string object_text(const std::size_t n, const std::size_t depth, const bool duplicates) +{ + std::string text = "{"; + for (std::size_t i = 0; i < n; ++i) + { + text += (i == 0 ? "" : ","); + text += "\"key" + std::to_string(i) + "\":"; + switch (i % 4) + { + case 0: + text += std::to_string(i); + break; + case 1: + text += depth == 0 ? "[]" : array_text(n / 2, depth - 1); + break; + case 2: + text += depth == 0 ? "{}" : object_text(n / 2, depth - 1, duplicates); + break; + default: + text += "\"value\""; + break; + } + } + if (duplicates) + { + // keys that are there already, the first and the last one, with other values + text += n == 0 ? "" : ","; + text += R"("key0":[1,2],"key)"; + text += std::to_string(n - 1); + text += R"(":{"again":true})"; + } + return text + "}"; +} + +template +void check_same_dump(const std::string& text) +{ + // the text is what dump() writes for the parsed value, and the value + // survives a second round trip: arrays keep their elements in order, an + // ordered_json keeps its members in order of appearance + const Json parsed = Json::parse(text); + const Json again = Json::parse(parsed.dump()); + CHECK(parsed == again); + CHECK(parsed.dump() == again.dump()); + CHECK(Json::accept(text)); +} +} // namespace + +TEST_CASE("parsing containers of sizes around the limits of the DOM parser") +{ + SECTION("arrays") + { + for (const std::size_t n : container_sizes) + { + const std::string text = array_text(n, 2); + INFO("size ", n); + + const json parsed = json::parse(text); + CHECK(parsed.is_array()); + CHECK(parsed.size() == n); + for (std::size_t i = 0; i < n; i += 4) + { + CHECK(parsed[i] == i); + } + check_same_dump(text); + check_same_dump(text); + CHECK(ordered_json::parse(text).dump() == parsed.dump()); + } + } + + SECTION("objects") + { + for (const bool duplicates : + { + false, true + }) + { + for (const std::size_t n : container_sizes) + { + const std::string text = object_text(n, 2, duplicates); + INFO("size ", n, " duplicates ", duplicates); + + const json parsed = json::parse(text); + const ordered_json ordered = ordered_json::parse(text); + CHECK(parsed.is_object()); + CHECK(ordered.is_object()); + CHECK(parsed.size() == ordered.size()); + // the same members, in the order of their first appearance + std::size_t i = 0; + for (auto it = ordered.begin(); it != ordered.end(); ++it, ++i) + { + if (i < n) + { + CHECK(it.key() == "key" + std::to_string(i)); + } + CHECK(parsed.at(it.key()) == json(it.value())); + } + if (duplicates && n > 1) + { + // the later value replaced the earlier one in its place + CHECK(ordered.at("key0") == ordered_json::parse("[1,2]")); + CHECK(ordered.at("key" + std::to_string(n - 1)) == ordered_json::parse("{\"again\":true}")); + CHECK(ordered.begin().key() == "key0"); + CHECK(ordered.size() == n); + } + check_same_dump(text); + check_same_dump(text); + } + } + } + + SECTION("objects with many different and repeated keys") + { + // 300 members of which a third repeat an earlier key + std::string text = "{"; + for (std::size_t i = 0; i < 300; ++i) + { + text += (i == 0 ? "" : ","); + text += "\"k" + std::to_string(i % 200) + "\":" + std::to_string(i); + } + text += '}'; + + const ordered_json ordered = ordered_json::parse(text); + const json parsed = json::parse(text); + CHECK(ordered.size() == 200); + CHECK(parsed.size() == 200); + std::size_t i = 0; + for (auto it = ordered.begin(); it != ordered.end(); ++it, ++i) + { + CHECK(it.key() == "k" + std::to_string(i)); + // the last value written + CHECK(it.value() == (i < 100 ? i + 200 : i)); + CHECK(parsed.at(it.key()) == json(it.value())); + } + } +} + +namespace +{ +// a deterministic source of structure for parsing tests: builds a JSON text +// and, at the same time, the value it has to parse to +class tree_generator +{ + public: + explicit tree_generator(std::uint32_t seed) : state(seed) {} + + template + Json make(std::string& text, const std::size_t depth) + { + switch (next(depth == 0 ? 5 : 9)) + { + case 0: + text += "null"; + return Json(nullptr); + case 1: + text += "true"; + return Json(true); + case 2: + { + const int n = static_cast(next(2000)) - 1000; + text += std::to_string(n); + return Json(n); + } + case 3: + text += "\"text\""; + return Json("text"); + case 4: + text += "2.5"; + return Json(2.5); + case 5: + case 6: + case 7: + { + Json array = Json::array(); + text += '['; + const std::size_t n = size(); + for (std::size_t i = 0; i < n; ++i) + { + text += (i == 0 ? "" : ","); + array.push_back(make(text, depth - 1)); + } + text += ']'; + return array; + } + default: + { + Json object = Json::object(); + text += '{'; + const std::size_t n = size(); + for (std::size_t i = 0; i < n; ++i) + { + const std::string key = "k" + std::to_string(i); + text += (i == 0 ? "\"" : ",\"") + key + "\":"; + object[key] = make(text, depth - 1); + } + text += '}'; + return object; + } + } + } + + private: + std::uint32_t next(const std::uint32_t bound) + { + state = (state * 1664525U) + 1013904223U; + return (state >> 8) % bound; + } + + std::size_t size() + { + return container_sizes[next(static_cast(container_sizes.size()))]; + } + + std::uint32_t state; +}; +} // namespace + +TEST_CASE("parsing random trees of arrays and objects") +{ + for (std::uint32_t seed = 1; seed <= 150; ++seed) + { + INFO("seed ", seed); + + std::string text; + std::string ordered_text; + const json expected = tree_generator(seed).make(text, 3); + const auto ordered_expected = tree_generator(seed).make(ordered_text, 3); + CHECK(text == ordered_text); + + CHECK(json::parse(text) == expected); + CHECK(json::accept(text)); + const ordered_json ordered = ordered_json::parse(text); + CHECK(ordered == ordered_expected); + CHECK(ordered.dump() == ordered_expected.dump()); + CHECK(json::parse(text).dump() == expected.dump()); + } +}