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 :