diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 4004cbd80..c0012b8e5 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -239,6 +239,72 @@ class json_pointer } private: + /*! + @brief result of @ref parse_array_index + + @ref array_index maps each value to the corresponding parse_error/out_of_range + exception; @ref contains and @ref get_checked_or_null, which must not throw for + an out-of-range or unrepresentable index, switch on it directly instead. + */ + enum class array_index_status + { + ok, ///< @a s is a valid, representable array index + leading_zero, ///< @a s begins with '0' but has more than one character + not_a_number, ///< @a s does not begin with a digit + unresolved, ///< @a s could not be converted to an integer + exceeds_size_type ///< @a s converts to an integer that exceeds size_type + }; + + /*! + @param[in] s reference token to be converted into an array index + @param[out] idx the integer representation of @a s if @ref array_index_status::ok + is returned; left unchanged otherwise + + @return whether @a s is a valid array index, and if not, why + + @note this function never throws; @ref array_index and the callers that must not + throw (@ref contains, @ref get_checked_or_null) build on it instead of each + re-implementing the RFC 6901 digit rules and the @a size_type range check + */ + template + static array_index_status parse_array_index(const string_t& s, typename BasicJsonType::size_type& idx) noexcept + { + using size_type = typename BasicJsonType::size_type; + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) + { + return array_index_status::leading_zero; + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) + { + return array_index_status::not_a_number; + } + + const char* p = s.data(); + char* p_end = nullptr; // NOLINT(misc-const-correctness) + errno = 0; // strtoull doesn't reset errno + const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) + if (p == p_end // invalid input or empty string + || errno == ERANGE // out of range + || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read + { + return array_index_status::unresolved; + } + + // only triggered on special platforms (like 32bit), see also + // https://github.com/nlohmann/json/pull/2203 + if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) + { + return array_index_status::exceeds_size_type; // LCOV_EXCL_LINE + } + + idx = static_cast(res); + return array_index_status::ok; + } + /*! @param[in] s reference token to be converted into an array index @@ -252,39 +318,30 @@ class json_pointer template static typename BasicJsonType::size_type array_index(const string_t& s) { - using size_type = typename BasicJsonType::size_type; + typename BasicJsonType::size_type idx{}; + const auto status = parse_array_index(s, idx); - // error condition (cf. RFC 6901, Sect. 4) - if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) + if (JSON_HEDLEY_UNLIKELY(status == array_index_status::leading_zero)) { JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", s, "' must not begin with '0'"), nullptr)); } - // error condition (cf. RFC 6901, Sect. 4) - if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) + if (JSON_HEDLEY_UNLIKELY(status == array_index_status::not_a_number)) { JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", s, "' is not a number"), nullptr)); } - const char* p = s.data(); - char* p_end = nullptr; // NOLINT(misc-const-correctness) - errno = 0; // strtoull doesn't reset errno - const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) - if (p == p_end // invalid input or empty string - || errno == ERANGE // out of range - || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read + if (JSON_HEDLEY_UNLIKELY(status == array_index_status::unresolved)) { JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", s, "'"), nullptr)); } - // only triggered on special platforms (like 32bit), see also - // https://github.com/nlohmann/json/pull/2203 - if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) + if (JSON_HEDLEY_UNLIKELY(status == array_index_status::exceeds_size_type)) { JSON_THROW(detail::out_of_range::create(410, detail::concat("array index ", s, " exceeds size_type"), nullptr)); // LCOV_EXCL_LINE } - return static_cast(res); + return idx; } JSON_PRIVATE_UNLESS_TESTED: @@ -621,18 +678,23 @@ class json_pointer return nullptr; } - // may throw parse_error.106/109 for a malformed index; an + // a malformed index still throws parse_error.106/109; an // index that is syntactically valid but cannot be // represented (out_of_range.404/410) is treated like an // out-of-range index below typename BasicJsonType::size_type idx{}; - JSON_TRY + switch (parse_array_index(reference_token, idx)) { - idx = array_index(reference_token); - } - JSON_INTERNAL_CATCH (detail::out_of_range&) - { - return nullptr; + case array_index_status::leading_zero: + JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", reference_token, "' must not begin with '0'"), nullptr)); + case array_index_status::not_a_number: + JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", reference_token, "' is not a number"), nullptr)); + case array_index_status::unresolved: + case array_index_status::exceeds_size_type: + return nullptr; + case array_index_status::ok: + default: + break; } if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) @@ -660,8 +722,8 @@ class json_pointer } /*! - @throw parse_error.106 if an array index begins with '0' - @throw parse_error.109 if an array index was not a number + @note unlike array_index(), this never throws: a malformed or unrepresentable + array index reference token is treated like a missing key (see #5395) */ template bool contains(const BasicJsonType* ptr) const @@ -689,43 +751,17 @@ class json_pointer // "-" always fails the range check return false; } - if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !("0" <= reference_token && reference_token <= "9"))) - { - // invalid char - return false; - } - if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1)) - { - if (JSON_HEDLEY_UNLIKELY(!('1' <= reference_token[0] && reference_token[0] <= '9'))) - { - // the first char should be between '1' and '9' - return false; - } - for (std::size_t i = 1; i < reference_token.size(); i++) - { - if (JSON_HEDLEY_UNLIKELY(!('0' <= reference_token[i] && reference_token[i] <= '9'))) - { - // other char should be between '0' and '9' - return false; - } - } - } - // the reference token consists only of digits at this point (cf. checks - // above); however, its numeric value might not be representable, in which - // case array_index() would throw out_of_range.404/410 -- contains() must - // not throw (see #5395), so such a reference token is treated as "not found" - errno = 0; // strtoull() does not reset errno on success - char* p_end = nullptr; // NOLINT(misc-const-correctness) - const unsigned long long magnitude = std::strtoull(reference_token.c_str(), &p_end, 10); // NOLINT(runtime/int) - if (JSON_HEDLEY_UNLIKELY(errno == ERANGE // the value exceeds ULLONG_MAX - || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) + // any parse failure (malformed index, or one that is syntactically + // valid but not representable as size_type) means the reference + // token cannot denote an existing array element -- contains() must + // not throw (see #5395), so it is treated as "not found" + typename BasicJsonType::size_type idx{}; + if (JSON_HEDLEY_UNLIKELY(parse_array_index(reference_token, idx) != array_index_status::ok)) { - // the array index cannot be represented as size_type return false; } - const auto idx = array_index(reference_token); if (idx >= ptr->size()) { // index out of range diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index c28971608..3754e0086 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -18923,6 +18923,72 @@ class json_pointer } private: + /*! + @brief result of @ref parse_array_index + + @ref array_index maps each value to the corresponding parse_error/out_of_range + exception; @ref contains and @ref get_checked_or_null, which must not throw for + an out-of-range or unrepresentable index, switch on it directly instead. + */ + enum class array_index_status + { + ok, ///< @a s is a valid, representable array index + leading_zero, ///< @a s begins with '0' but has more than one character + not_a_number, ///< @a s does not begin with a digit + unresolved, ///< @a s could not be converted to an integer + exceeds_size_type ///< @a s converts to an integer that exceeds size_type + }; + + /*! + @param[in] s reference token to be converted into an array index + @param[out] idx the integer representation of @a s if @ref array_index_status::ok + is returned; left unchanged otherwise + + @return whether @a s is a valid array index, and if not, why + + @note this function never throws; @ref array_index and the callers that must not + throw (@ref contains, @ref get_checked_or_null) build on it instead of each + re-implementing the RFC 6901 digit rules and the @a size_type range check + */ + template + static array_index_status parse_array_index(const string_t& s, typename BasicJsonType::size_type& idx) noexcept + { + using size_type = typename BasicJsonType::size_type; + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) + { + return array_index_status::leading_zero; + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) + { + return array_index_status::not_a_number; + } + + const char* p = s.data(); + char* p_end = nullptr; // NOLINT(misc-const-correctness) + errno = 0; // strtoull doesn't reset errno + const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) + if (p == p_end // invalid input or empty string + || errno == ERANGE // out of range + || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read + { + return array_index_status::unresolved; + } + + // only triggered on special platforms (like 32bit), see also + // https://github.com/nlohmann/json/pull/2203 + if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) + { + return array_index_status::exceeds_size_type; // LCOV_EXCL_LINE + } + + idx = static_cast(res); + return array_index_status::ok; + } + /*! @param[in] s reference token to be converted into an array index @@ -18936,39 +19002,30 @@ class json_pointer template static typename BasicJsonType::size_type array_index(const string_t& s) { - using size_type = typename BasicJsonType::size_type; + typename BasicJsonType::size_type idx{}; + const auto status = parse_array_index(s, idx); - // error condition (cf. RFC 6901, Sect. 4) - if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) + if (JSON_HEDLEY_UNLIKELY(status == array_index_status::leading_zero)) { JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", s, "' must not begin with '0'"), nullptr)); } - // error condition (cf. RFC 6901, Sect. 4) - if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) + if (JSON_HEDLEY_UNLIKELY(status == array_index_status::not_a_number)) { JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", s, "' is not a number"), nullptr)); } - const char* p = s.data(); - char* p_end = nullptr; // NOLINT(misc-const-correctness) - errno = 0; // strtoull doesn't reset errno - const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) - if (p == p_end // invalid input or empty string - || errno == ERANGE // out of range - || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read + if (JSON_HEDLEY_UNLIKELY(status == array_index_status::unresolved)) { JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", s, "'"), nullptr)); } - // only triggered on special platforms (like 32bit), see also - // https://github.com/nlohmann/json/pull/2203 - if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) + if (JSON_HEDLEY_UNLIKELY(status == array_index_status::exceeds_size_type)) { JSON_THROW(detail::out_of_range::create(410, detail::concat("array index ", s, " exceeds size_type"), nullptr)); // LCOV_EXCL_LINE } - return static_cast(res); + return idx; } JSON_PRIVATE_UNLESS_TESTED: @@ -19305,18 +19362,23 @@ class json_pointer return nullptr; } - // may throw parse_error.106/109 for a malformed index; an + // a malformed index still throws parse_error.106/109; an // index that is syntactically valid but cannot be // represented (out_of_range.404/410) is treated like an // out-of-range index below typename BasicJsonType::size_type idx{}; - JSON_TRY + switch (parse_array_index(reference_token, idx)) { - idx = array_index(reference_token); - } - JSON_INTERNAL_CATCH (detail::out_of_range&) - { - return nullptr; + case array_index_status::leading_zero: + JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", reference_token, "' must not begin with '0'"), nullptr)); + case array_index_status::not_a_number: + JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", reference_token, "' is not a number"), nullptr)); + case array_index_status::unresolved: + case array_index_status::exceeds_size_type: + return nullptr; + case array_index_status::ok: + default: + break; } if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) @@ -19344,8 +19406,8 @@ class json_pointer } /*! - @throw parse_error.106 if an array index begins with '0' - @throw parse_error.109 if an array index was not a number + @note unlike array_index(), this never throws: a malformed or unrepresentable + array index reference token is treated like a missing key (see #5395) */ template bool contains(const BasicJsonType* ptr) const @@ -19373,43 +19435,17 @@ class json_pointer // "-" always fails the range check return false; } - if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !("0" <= reference_token && reference_token <= "9"))) - { - // invalid char - return false; - } - if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1)) - { - if (JSON_HEDLEY_UNLIKELY(!('1' <= reference_token[0] && reference_token[0] <= '9'))) - { - // the first char should be between '1' and '9' - return false; - } - for (std::size_t i = 1; i < reference_token.size(); i++) - { - if (JSON_HEDLEY_UNLIKELY(!('0' <= reference_token[i] && reference_token[i] <= '9'))) - { - // other char should be between '0' and '9' - return false; - } - } - } - // the reference token consists only of digits at this point (cf. checks - // above); however, its numeric value might not be representable, in which - // case array_index() would throw out_of_range.404/410 -- contains() must - // not throw (see #5395), so such a reference token is treated as "not found" - errno = 0; // strtoull() does not reset errno on success - char* p_end = nullptr; // NOLINT(misc-const-correctness) - const unsigned long long magnitude = std::strtoull(reference_token.c_str(), &p_end, 10); // NOLINT(runtime/int) - if (JSON_HEDLEY_UNLIKELY(errno == ERANGE // the value exceeds ULLONG_MAX - || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) + // any parse failure (malformed index, or one that is syntactically + // valid but not representable as size_type) means the reference + // token cannot denote an existing array element -- contains() must + // not throw (see #5395), so it is treated as "not found" + typename BasicJsonType::size_type idx{}; + if (JSON_HEDLEY_UNLIKELY(parse_array_index(reference_token, idx) != array_index_status::ok)) { - // the array index cannot be represented as size_type return false; } - const auto idx = array_index(reference_token); if (idx >= ptr->size()) { // index out of range diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index 40e8216d5..46f606ffc 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -482,6 +482,16 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_array.value("/-"_json_pointer, 42) == 42); CHECK(j_array_const.value("/-"_json_pointer, 42) == 42); + // Test an index that is syntactically valid but exceeds size_type, and one + // with a trailing non-digit: both must yield the default value rather than + // throw, even with exceptions disabled (regression test for #5708 item 4 / + // #5672: get_checked_or_null() no longer relies on a JSON_TRY/ + // JSON_INTERNAL_CATCH around array_index() to turn these into "not found") + CHECK(j_array.value("/18446744073709551615"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/18446744073709551615"_json_pointer, 42) == 42); + CHECK(j_array.value("/1a"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/1a"_json_pointer, 42) == 42); + #if !defined(JSON_NOEXCEPTION) // Test malformed index (non-numeric) throws parse_error CHECK_THROWS_WITH_AS(j_array.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&);