diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index da5a0263e..8a6a0a6d5 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -467,11 +467,13 @@ class binary_reader const std::size_t element_type_parse_position = chars_read; // an array's elements are named "0", "1", ... in the wire format, - // and those names are not passed on: they are skipped unless - // error_handler_t::strict must reject ill-formed UTF-8 in them + // and those names are not passed on: they are skipped, and only + // checked if error_handler_t::strict must reject ill-formed UTF-8 const bool is_object = container_stack.back().is_object; key.clear(); - if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key, is_object || error_handler == error_handler_t::strict))) + if (JSON_HEDLEY_UNLIKELY(!(is_object || error_handler != error_handler_t::strict + ? get_bson_cstr(key, is_object) + : check_bson_cstr(key, std::integral_constant {})))) { return false; } @@ -495,7 +497,7 @@ class binary_reader @param[in] store whether the characters are needed; if not (the names of an array's elements), they are skipped and @a result may be left unchanged, and they are not checked against - @ref error_handler + @ref error_handler (@ref check_bson_cstr checks them) @return `true` if the \\x00-byte indicating the end of the string was encountered before the EOF; false` indicates an unexpected EOF. */ @@ -554,6 +556,42 @@ class binary_reader return true; } + /*! + @brief skip a C-style string that error_handler_t::strict must check + + The names of an array's elements are not passed on. On contiguous input, + a well-formed name is checked in place and skipped without being copied. + Otherwise (no \\x00-byte in the input that can be scanned, or ill-formed + UTF-8), it is read by @ref get_bson_cstr, which reports the error. + + @param[in,out] result an empty string, which may receive the name + @return whether the name was read and is well-formed + */ + bool check_bson_cstr(string_t& result, std::true_type /*bulk*/) + { + const std::size_t remaining = ia.bulk_remaining(); + if (remaining != 0) + { + const auto* const data = reinterpret_cast(ia.bulk_data()); + const std::size_t length = find_zero_byte(data, remaining); + if (length != remaining && JSON_HEDLEY_LIKELY(valid_utf8_string(data, length))) + { + // consume the string and its \x00-byte, as get_bson_cstr does + ia.bulk_skip(length + 1); + chars_read += length + 1; + current = 0x00; + return true; + } + } + return get_bson_cstr(result); + } + + /// input that is not contiguous: the name is read and checked byte by byte + bool check_bson_cstr(string_t& result, std::false_type /*bulk*/) + { + return get_bson_cstr(result); + } + /// input that is not contiguous: C-style strings are read byte by byte bool get_bson_cstr_bulk(string_t& /*result*/, const bool /*store*/, std::false_type /*bulk*/) const noexcept { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 1fc214c5f..693320ed5 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -18474,11 +18474,13 @@ class binary_reader const std::size_t element_type_parse_position = chars_read; // an array's elements are named "0", "1", ... in the wire format, - // and those names are not passed on: they are skipped unless - // error_handler_t::strict must reject ill-formed UTF-8 in them + // and those names are not passed on: they are skipped, and only + // checked if error_handler_t::strict must reject ill-formed UTF-8 const bool is_object = container_stack.back().is_object; key.clear(); - if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key, is_object || error_handler == error_handler_t::strict))) + if (JSON_HEDLEY_UNLIKELY(!(is_object || error_handler != error_handler_t::strict + ? get_bson_cstr(key, is_object) + : check_bson_cstr(key, std::integral_constant {})))) { return false; } @@ -18502,7 +18504,7 @@ class binary_reader @param[in] store whether the characters are needed; if not (the names of an array's elements), they are skipped and @a result may be left unchanged, and they are not checked against - @ref error_handler + @ref error_handler (@ref check_bson_cstr checks them) @return `true` if the \\x00-byte indicating the end of the string was encountered before the EOF; false` indicates an unexpected EOF. */ @@ -18561,6 +18563,42 @@ class binary_reader return true; } + /*! + @brief skip a C-style string that error_handler_t::strict must check + + The names of an array's elements are not passed on. On contiguous input, + a well-formed name is checked in place and skipped without being copied. + Otherwise (no \\x00-byte in the input that can be scanned, or ill-formed + UTF-8), it is read by @ref get_bson_cstr, which reports the error. + + @param[in,out] result an empty string, which may receive the name + @return whether the name was read and is well-formed + */ + bool check_bson_cstr(string_t& result, std::true_type /*bulk*/) + { + const std::size_t remaining = ia.bulk_remaining(); + if (remaining != 0) + { + const auto* const data = reinterpret_cast(ia.bulk_data()); + const std::size_t length = find_zero_byte(data, remaining); + if (length != remaining && JSON_HEDLEY_LIKELY(valid_utf8_string(data, length))) + { + // consume the string and its \x00-byte, as get_bson_cstr does + ia.bulk_skip(length + 1); + chars_read += length + 1; + current = 0x00; + return true; + } + } + return get_bson_cstr(result); + } + + /// input that is not contiguous: the name is read and checked byte by byte + bool check_bson_cstr(string_t& result, std::false_type /*bulk*/) + { + return get_bson_cstr(result); + } + /// input that is not contiguous: C-style strings are read byte by byte bool get_bson_cstr_bulk(string_t& /*result*/, const bool /*store*/, std::false_type /*bulk*/) const noexcept { diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index f1f43ccd7..ac8e2b24f 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -23,6 +23,7 @@ using nlohmann::json; #include #include #include +#include #include #include #include "make_test_data_available.hpp" @@ -1199,6 +1200,31 @@ TEST_CASE("BSON keys from contiguous and stream input") from_stream = e.what(); } CHECK(from_vector == from_stream); + + // error_handler_t::strict checks the array element names in place on + // contiguous input and byte by byte otherwise; the names are + // well-formed, so both give the same value or error as above + std::string strict_from_vector; + std::string strict_from_list; + try + { + strict_from_vector = json::from_bson(input, true, true, json::error_handler_t::strict).dump(); + } + catch (const json::parse_error& e) + { + strict_from_vector = e.what(); + } + try + { + const std::list list(input.begin(), input.end()); + strict_from_list = json::from_bson(list.begin(), list.end(), true, true, json::error_handler_t::strict).dump(); + } + catch (const json::parse_error& e) + { + strict_from_list = e.what(); + } + CHECK(strict_from_vector == from_vector); + CHECK(strict_from_list == from_vector); } } @@ -1227,6 +1253,18 @@ TEST_CASE("BSON array element names") std::istringstream stream(std::string(reinterpret_cast(bson.data()), bson.size())); CHECK(json::from_bson(stream) == expected); + + // input that is not contiguous reads the names byte by byte and must reject + // the same name + const std::list list(bson.begin(), bson.end()); + CHECK_THROWS_WITH_AS(_ = json::from_bson(list.begin(), list.end(), true, true, json::error_handler_t::strict), "[json.exception.parse_error.113] parse error at byte 14: syntax error while parsing BSON key: invalid string: ill-formed UTF-8 byte", json::parse_error&); + + // without exceptions, the rejected name ends the parse + CHECK(json::from_bson(bson, true, false, json::error_handler_t::strict).is_discarded()); + + // well-formed names pass the check without being stored + const json valid = {{"a", {true, false, {{"k", json::array({1, 2})}}}}}; + CHECK(json::from_bson(json::to_bson(valid), true, true, json::error_handler_t::strict) == valid); } #endif