From dea2a4434a5fa991c38a501f5b71af44d5b1def5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 30 Oct 2026 20:45:19 +0100 Subject: [PATCH] json_view: accept() checks the text without writing an index json_document::accept() ran the builder that writes the node index and discarded the index. For input without comments and trailing commas it now runs a second instantiation of the builder, with a template parameter Store that is false: the grammar, number, string, UTF-8 and nesting checks are the same code, but no node is written, the container nodes are not patched when they close, the node array is not reserved, and no list of large objects is kept. What only builds the arguments of a node (integer conversion, the digit counts of floats) is not run either. accept() with comments or trailing commas and parse() are not changed; the builder that stores is the one parse() always used. A flag of the document passes the request from accept() to build(). accept() on twitter, citm_catalog, canada and the two minified files, A/B in one binary, 6 code layouts, time after / before (geometric mean of the five; the other 14 files of the set are faster too, none is slower): - Apple M1 Max, Apple clang 21: 0.89 - x86-64 Haswell, GCC 16.2: 0.75 - x86-64 Haswell, Clang 23: 0.78 (semanticscholar-corpus.json: 0.52 on both x86 compilers.) parse() runs the same instructions as before; its timings moved by up to 3 % on a few small files with unchanged instruction counts (code placement). Tests: accept() against json::accept() for the same inputs with all four option combinations and with and without a terminating NUL: 2,000 generated documents and damaged copies of them, nesting from 63 to 1000 levels (closed and not), objects with 300 members, a null byte in the text, and float overflow for a float document. Checked against a deliberately broken validator. Signed-off-by: Niels Lohmann --- .../docs/api/basic_json_document/accept.md | 8 +- include/nlohmann/detail/view/builder.hpp | 50 ++++++++--- .../nlohmann/detail/view/document_data.hpp | 1 + include/nlohmann/json_view.hpp | 4 + single_include/nlohmann/json_view.hpp | 55 +++++++++--- tests/src/unit-json_view_builder.cpp | 88 +++++++++++++++++++ 6 files changed, 179 insertions(+), 27 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_document/accept.md b/docs/mkdocs/docs/api/basic_json_document/accept.md index 1f1070d51..c3cbf0f75 100644 --- a/docs/mkdocs/docs/api/basic_json_document/accept.md +++ b/docs/mkdocs/docs/api/basic_json_document/accept.md @@ -13,9 +13,11 @@ function never throws an exception for invalid input, and the returned `#!cpp bo is returned. The result is the same as that of `BasicJsonType::accept()` by design; the difference is how the check is made. This -function runs the document's index builder (and discards the index), while `BasicJsonType::accept()` runs the SAX -parser with a handler that ignores every event. Use this one if you go on to parse the same input with -[`parse()`](parse.md) anyway, so that both use the same code; either is fine for a plain yes/no answer. +function runs the document's builder, which checks the text as [`parse()`](parse.md) does but writes no index (for +input without comments and trailing commas; with either option it builds the index and discards it), while +`BasicJsonType::accept()` runs the SAX parser with a handler that ignores every event. Use this one if you go on to +parse the same input with [`parse()`](parse.md) anyway, so that both use the same code; either is fine for a plain +yes/no answer. ## Template parameters diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp index 89b5af2ab..a17b21fe3 100644 --- a/include/nlohmann/detail/view/builder.hpp +++ b/include/nlohmann/detail/view/builder.hpp @@ -69,7 +69,8 @@ struct parse_failure }; /// FloatType: the number_float_t of the document, whose overflow parse() rejects -template +/// Store: false for accept(): the grammar, number, string and UTF-8 checks run as for parse(), but no node is written +template class builder { public: @@ -610,10 +611,13 @@ run_found: NLOHMANN_VIEW_ALWAYS_INLINE bool run() { - cold.m_doc.reserve(estimate_nodes(reinterpret_cast(b), static_cast(e - b))); - base = cold.m_doc.nodes; - out = base; - cap = base + cold.m_doc.node_capacity; + if (enabled(Store)) + { + cold.m_doc.reserve(estimate_nodes(reinterpret_cast(b), static_cast(e - b))); + base = cold.m_doc.nodes; + out = base; + cap = base + cold.m_doc.node_capacity; + } if (e - p >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) { @@ -797,7 +801,10 @@ close_object: // an array pays nothing for this) if (NLOHMANN_VIEW_UNLIKELY(cur_count >= document_data::index_min_members)) { - cold.note_large_object(cur_idx); + if (enabled(Store)) + { + cold.note_large_object(cur_idx); + } } close_container: @@ -957,6 +964,17 @@ indent_done: /// an integer's value); two stores on little-endian targets NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(node_kind k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) { + if (!enabled(Store)) + { + // accept(): nothing is stored, so what computes the arguments is not run either. base and out stay + // null: their difference, which open() takes, is 0. + static_cast(k); + static_cast(flags); + static_cast(extra); + static_cast(off); + static_cast(second); + return out; + } if (NLOHMANN_VIEW_UNLIKELY(out == cap)) { const auto n = static_cast(out - base); @@ -1015,9 +1033,12 @@ indent_done: NLOHMANN_VIEW_ALWAYS_INLINE void close() { - node& n = base[cur_idx]; - n.len = cur_count; - n.next = static_cast(out - base) - cur_idx; + if (enabled(Store)) + { + node& n = base[cur_idx]; + n.len = cur_count; + n.next = static_cast(out - base) - cur_idx; + } if (--depth != 0) { if (NLOHMANN_VIEW_LIKELY(depth <= 64)) @@ -1192,17 +1213,17 @@ indent_done: }; /// run the builder with compile-time options -template +template inline bool build_with(document_data& d, const char* src, std::size_t size, bool sentinel, parse_failure& failure) { if (sentinel) { - builder bld(d, src, size); + builder bld(d, src, size); const bool ok = bld.run(); failure = bld.failure(); return ok; } - builder bld(d, src, size); + builder bld(d, src, size); const bool ok = bld.run(); failure = bld.failure(); return ok; @@ -1213,6 +1234,11 @@ inline bool build_with(document_data& d, const char* src, std::size_t size, bool template inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, parse_failure& failure) { + if (d.validate_only && !comments && !trailing_commas) + { + // accept() of plain JSON: nothing is stored (the other option sets keep the builder that stores) + return build_with(d, src, size, sentinel, failure); + } if (comments) { return trailing_commas ? build_with(d, src, size, sentinel, failure) diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp index 2c5f97767..fd0d6db44 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -36,6 +36,7 @@ struct document_data const char* src = nullptr; std::size_t size = 0; node* nodes = nullptr; ///< the node index (see node.hpp) + bool validate_only = false; ///< accept(): build() checks the text and stores no nodes std::size_t node_count = 0; std::size_t node_capacity = 0; node* inline_nodes = nullptr; ///< node array allocated together with this header diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 9d26d12ea..23e43f10c 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -1088,6 +1088,7 @@ class basic_json_document static bool accept(InputType&& input, const bool ignore_comments = false, const bool ignore_trailing_commas = false) { basic_json_document d; + d.m_validate_only = true; // (nothing is stored: only the result is used) d.read(std::forward(input), false, ignore_comments, ignore_trailing_commas); return !d.is_discarded(); } @@ -1532,7 +1533,9 @@ class basic_json_document } else { + d.validate_only = m_validate_only; ok = detail::view::build < typename BasicJsonType::number_float_t, !detail::abi_config::strict_nul_handling > (d, src, size, comments, trailing_commas, sentinel, failure); + d.validate_only = false; } if (NLOHMANN_VIEW_LIKELY(ok)) { @@ -1693,6 +1696,7 @@ class basic_json_document } std::unique_ptr m_data{}; // NOLINT(readability-redundant-member-init) + bool m_validate_only = false; ///< set by accept(): build() stores no nodes }; /*! diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 8ff21c77f..527d3aeea 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -366,6 +366,7 @@ struct document_data const char* src = nullptr; std::size_t size = 0; node* nodes = nullptr; ///< the node index (see node.hpp) + bool validate_only = false; ///< accept(): build() checks the text and stores no nodes std::size_t node_count = 0; std::size_t node_capacity = 0; node* inline_nodes = nullptr; ///< node array allocated together with this header @@ -1311,7 +1312,8 @@ struct parse_failure }; /// FloatType: the number_float_t of the document, whose overflow parse() rejects -template +/// Store: false for accept(): the grammar, number, string and UTF-8 checks run as for parse(), but no node is written +template class builder { public: @@ -1852,10 +1854,13 @@ run_found: NLOHMANN_VIEW_ALWAYS_INLINE bool run() { - cold.m_doc.reserve(estimate_nodes(reinterpret_cast(b), static_cast(e - b))); - base = cold.m_doc.nodes; - out = base; - cap = base + cold.m_doc.node_capacity; + if (enabled(Store)) + { + cold.m_doc.reserve(estimate_nodes(reinterpret_cast(b), static_cast(e - b))); + base = cold.m_doc.nodes; + out = base; + cap = base + cold.m_doc.node_capacity; + } if (e - p >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) { @@ -2039,7 +2044,10 @@ close_object: // an array pays nothing for this) if (NLOHMANN_VIEW_UNLIKELY(cur_count >= document_data::index_min_members)) { - cold.note_large_object(cur_idx); + if (enabled(Store)) + { + cold.note_large_object(cur_idx); + } } close_container: @@ -2199,6 +2207,17 @@ indent_done: /// an integer's value); two stores on little-endian targets NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(node_kind k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) { + if (!enabled(Store)) + { + // accept(): nothing is stored, so what computes the arguments is not run either. base and out stay + // null: their difference, which open() takes, is 0. + static_cast(k); + static_cast(flags); + static_cast(extra); + static_cast(off); + static_cast(second); + return out; + } if (NLOHMANN_VIEW_UNLIKELY(out == cap)) { const auto n = static_cast(out - base); @@ -2257,9 +2276,12 @@ indent_done: NLOHMANN_VIEW_ALWAYS_INLINE void close() { - node& n = base[cur_idx]; - n.len = cur_count; - n.next = static_cast(out - base) - cur_idx; + if (enabled(Store)) + { + node& n = base[cur_idx]; + n.len = cur_count; + n.next = static_cast(out - base) - cur_idx; + } if (--depth != 0) { if (NLOHMANN_VIEW_LIKELY(depth <= 64)) @@ -2434,17 +2456,17 @@ indent_done: }; /// run the builder with compile-time options -template +template inline bool build_with(document_data& d, const char* src, std::size_t size, bool sentinel, parse_failure& failure) { if (sentinel) { - builder bld(d, src, size); + builder bld(d, src, size); const bool ok = bld.run(); failure = bld.failure(); return ok; } - builder bld(d, src, size); + builder bld(d, src, size); const bool ok = bld.run(); failure = bld.failure(); return ok; @@ -2455,6 +2477,11 @@ inline bool build_with(document_data& d, const char* src, std::size_t size, bool template inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, parse_failure& failure) { + if (d.validate_only && !comments && !trailing_commas) + { + // accept() of plain JSON: nothing is stored (the other option sets keep the builder that stores) + return build_with(d, src, size, sentinel, failure); + } if (comments) { return trailing_commas ? build_with(d, src, size, sentinel, failure) @@ -10129,6 +10156,7 @@ class basic_json_document static bool accept(InputType&& input, const bool ignore_comments = false, const bool ignore_trailing_commas = false) { basic_json_document d; + d.m_validate_only = true; // (nothing is stored: only the result is used) d.read(std::forward(input), false, ignore_comments, ignore_trailing_commas); return !d.is_discarded(); } @@ -10573,7 +10601,9 @@ class basic_json_document } else { + d.validate_only = m_validate_only; ok = detail::view::build < typename BasicJsonType::number_float_t, !detail::abi_config::strict_nul_handling > (d, src, size, comments, trailing_commas, sentinel, failure); + d.validate_only = false; } if (NLOHMANN_VIEW_LIKELY(ok)) { @@ -10734,6 +10764,7 @@ class basic_json_document } std::unique_ptr m_data{}; // NOLINT(readability-redundant-member-init) + bool m_validate_only = false; ///< set by accept(): build() stores no nodes }; /*! diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index 4023c7848..0071280ae 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -172,6 +172,22 @@ void check_string(const std::string& content) } } +// accept() stores no nodes: it must accept and reject what json::accept does, for all options and for inputs with +// and without a terminating NUL +void check_accept(const std::string& text) +{ + CAPTURE(text) + const std::vector bytes(text.begin(), text.end()); + for (int options = 0; options < 4; ++options) + { + const bool comments = (options & 1) != 0; + const bool trailing_commas = (options & 2) != 0; + const bool accepted = json::accept(text, comments, trailing_commas); + CHECK(nlohmann::json_document::accept(text, comments, trailing_commas) == accepted); + CHECK(nlohmann::json_document::accept(bytes, comments, trailing_commas) == accepted); + } +} + // a small deterministic generator of documents struct generator { @@ -423,6 +439,78 @@ TEST_CASE("json_view builder") } } + SECTION("accept() without an index") + { + for (const char* text : + { + "", " ", "\n", "null", "nul", "true", "[", "]", "[]", "{}", "[1,2,3]", "[1,2,3", "[1,2,3,]", "{\"a\":1,}", "{\"a\"}", "{\"a\":}", + "[1] x", "[1][2]", "\xEF\xBB\xBF[1]", "[\"a\\", "[\"\\u12\"]", "[\"\\ud800\"]", "[\"\x01\"]", "[\"\xff\"]", "[01]", "[1.]", + "[.5]", "[-]", "[1e]", "// c\n[1]", "/* c */ {\"a\" /* x */ : 1}", "/* c", "[1] // c", "1e400", "-1e400", "123456789012345678901234567890", + "{\"a\":1,\"a\":2}", "[[[]]]", "\"\\u00e4\\n\\ud83d\\ude00\"" + }) + { + check_accept(text); + } + // a null byte inside the text, and after it + check_accept(std::string("[1,\0]", 5)); + check_accept(std::string("[1]\0", 4)); + check_accept(std::string("\0", 1)); + + // generated documents and damaged copies, as for the builder above + generator g; + for (int i = 0; i < 2000; ++i) + { + std::string text; + g.value(text, 0); + check_accept(text); + std::string damaged = text; + const auto at = static_cast(g.r(static_cast(damaged.size()))); + static const char replacements[] = {'x', '"', '\\', ',', ':', ']', '}', '[', '{', '1', '-', '.', 'e', '\0', '\n', '/'}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + damaged[at] = replacements[g.r(16)]; + check_accept(damaged); + check_accept(text.substr(0, at)); + } + + // nesting beyond the 64 levels that are kept on the builder's own stack, closed and not closed + for (const std::size_t depth : + { + 63, 64, 65, 66, 200, 1000 + }) + { + check_accept(std::string(depth, '[') + std::string(depth, ']')); + check_accept(std::string(depth, '[') + std::string(depth - 1, ']')); + std::string objects; + for (std::size_t i = 0; i < depth; ++i) + { + objects += "{\"k\":"; + } + objects += "1" + std::string(depth, '}'); + check_accept(objects); + check_accept(objects.substr(0, objects.size() - 1)); + } + + // objects with enough members to get a hash index when they are indexed (and duplicate keys) + std::string large = "{"; + for (int i = 0; i < 300; ++i) + { + large += (i == 0 ? "\"k" : ",\"k") + std::to_string(i % 200) + "\":" + std::to_string(i); + } + check_accept(large + "}"); + check_accept(large); + check_accept("[" + large + "},{" + large.substr(1) + "}]"); + + // the overflow check uses the floating-point type of the document + using float_json = nlohmann::basic_json; + for (const char* text : + { + "1e39", "-1e39", "3.4028235e38", "3.4028236e38", "[1.5e38, 3.5e38]", "{\"a\": 1e-50, \"b\": 1e39}", "340282356779733661637539395458142568448" + }) + { + CAPTURE(text) + CHECK(nlohmann::basic_json_document::accept(text) == float_json::accept(text)); + } + } + SECTION("test files") { for (const char* name :