diff --git a/docs/mkdocs/docs/api/basic_json/value.md b/docs/mkdocs/docs/api/basic_json/value.md index b0fd64989..7d1cac8cf 100644 --- a/docs/mkdocs/docs/api/basic_json/value.md +++ b/docs/mkdocs/docs/api/basic_json/value.md @@ -94,8 +94,12 @@ changes to any JSON value. 3. The function can throw the following exceptions: - Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if `default_value` does not match the type of the value at `ptr` - - Throws [`type_error.306`](../../home/exceptions.md#jsonexceptiontype_error306) if the JSON value is not an object; - in that case, using `value()` with a JSON pointer makes no sense. + - Throws [`type_error.306`](../../home/exceptions.md#jsonexceptiontype_error306) if the JSON value is not an array + or object; in that case, using `value()` with a JSON pointer makes no sense. + - Throws [`parse_error.106`](../../home/exceptions.md#jsonexceptionparse_error106) if an array index in the passed + JSON pointer `ptr` begins with '0'. + - Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in the passed + JSON pointer `ptr` is not a number. ## Complexity @@ -180,4 +184,4 @@ changes to any JSON value. 1. Added in version 1.0.0. Changed parameter `default_value` type from `const ValueType&` to `ValueType&&` in version 3.11.0. 2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2. -3. Added in version 2.0.2. +3. Added in version 2.0.2. Extended to work with arrays in version 3.12.x. diff --git a/docs/mkdocs/docs/features/element_access/default_value.md b/docs/mkdocs/docs/features/element_access/default_value.md index 2603a2d18..01d6863c4 100644 --- a/docs/mkdocs/docs/features/element_access/default_value.md +++ b/docs/mkdocs/docs/features/element_access/default_value.md @@ -33,7 +33,8 @@ you want to access and a default value in case there is no value stored with tha !!! failure "Exceptions" - - `value` can only be used with objects. For other types, a [`basic_json::type_error`](../../home/exceptions.md#jsonexceptiontype_error306) is thrown. + - With string keys, `value` can only be used with objects. For other types, a [`basic_json::type_error`](../../home/exceptions.md#jsonexceptiontype_error306) is thrown. + - With JSON Pointers, `value` can be used with both objects and arrays. For other types (null, boolean, number, string), a [`basic_json::type_error`](../../home/exceptions.md#jsonexceptiontype_error306) is thrown. !!! warning "Return type" diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 0767484f8..576ba62cc 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -480,8 +480,14 @@ class json_pointer ") is out of range"), ptr)); } - // note: at performs range check - ptr = &ptr->at(array_index(reference_token)); + const auto idx = array_index(reference_token); + // Bounds check before access to avoid exception with JSON_NOEXCEPTION + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + JSON_THROW(detail::out_of_range::create(401, detail::concat( + "array index ", std::to_string(idx), " is out of range"), ptr)); + } + ptr = &ptr->operator[](idx); break; } @@ -587,8 +593,14 @@ class json_pointer ") is out of range"), ptr)); } - // note: at performs range check - ptr = &ptr->at(array_index(reference_token)); + const auto idx = array_index(reference_token); + // Bounds check before access to avoid exception with JSON_NOEXCEPTION + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + JSON_THROW(detail::out_of_range::create(401, detail::concat( + "array index ", std::to_string(idx), " is out of range"), ptr)); + } + ptr = &ptr->operator[](idx); break; } @@ -608,6 +620,82 @@ class json_pointer return *ptr; } + /*! + @brief return a pointer to the pointed to value, or `nullptr` if the + pointer cannot be resolved because a key is missing, an array + index is out of range, or the array index is "-" + + @note unlike get_checked(), this never throws for those cases, so it + can be used to implement a non-throwing fallback (e.g. value()) + that also works when exceptions are disabled + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + */ + template + const BasicJsonType* get_checked_or_null(const BasicJsonType* ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + const auto it = ptr->find(reference_token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->end())) + { + return nullptr; + } + ptr = &*it; + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + return nullptr; + } + + // may throw parse_error.106/109 for a malformed index; 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 + { + idx = array_index(reference_token); + } + JSON_INTERNAL_CATCH (detail::out_of_range&) + { + return nullptr; + } + + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + return nullptr; + } + ptr = &ptr->operator[](idx); + break; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + return nullptr; + } + } + + return ptr; + } + /*! @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if an array index was not a number diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 581972363..afbf9b1b9 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -2393,19 +2393,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec && !std::is_same>::value, int > = 0 > ValueType value(const json_pointer& ptr, const ValueType& default_value) const { - // value only works for objects - if (JSON_HEDLEY_LIKELY(is_object())) + // value only works for arrays and objects + if (JSON_HEDLEY_LIKELY(is_structured())) { // If the pointer resolves to a value, return it. Otherwise, return // 'default_value'. - JSON_TRY + const auto* res = ptr.get_checked_or_null(this); + if (JSON_HEDLEY_LIKELY(res != nullptr)) { - return ptr.get_checked(this).template get(); - } - JSON_INTERNAL_CATCH (out_of_range&) - { - return default_value; + return res->template get(); } + + return default_value; } JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this)); @@ -2419,19 +2418,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec && !std::is_same>::value, int > = 0 > ReturnType value(const json_pointer& ptr, ValueType && default_value) const { - // value only works for objects - if (JSON_HEDLEY_LIKELY(is_object())) + // value only works for arrays and objects + if (JSON_HEDLEY_LIKELY(is_structured())) { // If the pointer resolves to a value, return it. Otherwise, return // 'default_value'. - JSON_TRY + const auto* res = ptr.get_checked_or_null(this); + if (JSON_HEDLEY_LIKELY(res != nullptr)) { - return ptr.get_checked(this).template get(); - } - JSON_INTERNAL_CATCH (out_of_range&) - { - return std::forward(default_value); + return res->template get(); } + + return std::forward(default_value); } JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this)); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index dcfb57f35..d6f065ece 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -15401,8 +15401,14 @@ class json_pointer ") is out of range"), ptr)); } - // note: at performs range check - ptr = &ptr->at(array_index(reference_token)); + const auto idx = array_index(reference_token); + // Bounds check before access to avoid exception with JSON_NOEXCEPTION + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + JSON_THROW(detail::out_of_range::create(401, detail::concat( + "array index ", std::to_string(idx), " is out of range"), ptr)); + } + ptr = &ptr->operator[](idx); break; } @@ -15508,8 +15514,14 @@ class json_pointer ") is out of range"), ptr)); } - // note: at performs range check - ptr = &ptr->at(array_index(reference_token)); + const auto idx = array_index(reference_token); + // Bounds check before access to avoid exception with JSON_NOEXCEPTION + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + JSON_THROW(detail::out_of_range::create(401, detail::concat( + "array index ", std::to_string(idx), " is out of range"), ptr)); + } + ptr = &ptr->operator[](idx); break; } @@ -15529,6 +15541,82 @@ class json_pointer return *ptr; } + /*! + @brief return a pointer to the pointed to value, or `nullptr` if the + pointer cannot be resolved because a key is missing, an array + index is out of range, or the array index is "-" + + @note unlike get_checked(), this never throws for those cases, so it + can be used to implement a non-throwing fallback (e.g. value()) + that also works when exceptions are disabled + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + */ + template + const BasicJsonType* get_checked_or_null(const BasicJsonType* ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + const auto it = ptr->find(reference_token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->end())) + { + return nullptr; + } + ptr = &*it; + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + return nullptr; + } + + // may throw parse_error.106/109 for a malformed index; 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 + { + idx = array_index(reference_token); + } + JSON_INTERNAL_CATCH (detail::out_of_range&) + { + return nullptr; + } + + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + return nullptr; + } + ptr = &ptr->operator[](idx); + break; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + return nullptr; + } + } + + return ptr; + } + /*! @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if an array index was not a number @@ -22911,19 +22999,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec && !std::is_same>::value, int > = 0 > ValueType value(const json_pointer& ptr, const ValueType& default_value) const { - // value only works for objects - if (JSON_HEDLEY_LIKELY(is_object())) + // value only works for arrays and objects + if (JSON_HEDLEY_LIKELY(is_structured())) { // If the pointer resolves to a value, return it. Otherwise, return // 'default_value'. - JSON_TRY + const auto* res = ptr.get_checked_or_null(this); + if (JSON_HEDLEY_LIKELY(res != nullptr)) { - return ptr.get_checked(this).template get(); - } - JSON_INTERNAL_CATCH (out_of_range&) - { - return default_value; + return res->template get(); } + + return default_value; } JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this)); @@ -22937,19 +23024,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec && !std::is_same>::value, int > = 0 > ReturnType value(const json_pointer& ptr, ValueType && default_value) const { - // value only works for objects - if (JSON_HEDLEY_LIKELY(is_object())) + // value only works for arrays and objects + if (JSON_HEDLEY_LIKELY(is_structured())) { // If the pointer resolves to a value, return it. Otherwise, return // 'default_value'. - JSON_TRY + const auto* res = ptr.get_checked_or_null(this); + if (JSON_HEDLEY_LIKELY(res != nullptr)) { - return ptr.get_checked(this).template get(); - } - JSON_INTERNAL_CATCH (out_of_range&) - { - return std::forward(default_value); + return res->template get(); } + + return std::forward(default_value); } JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this)); diff --git a/tests/module_cpp20/main.cpp b/tests/module_cpp20/main.cpp index 390398f1b..a844e657d 100644 --- a/tests/module_cpp20/main.cpp +++ b/tests/module_cpp20/main.cpp @@ -55,5 +55,5 @@ int main() // use every result so the references cannot be optimized away return (a == 1 && last == 3 && b == 2 && lit.size() == 3 && m.size() == 1 && !dumped.empty() && !os.str().empty()) - ? 0 : 1; + ? 0 : 1; } diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index c4e25bf70..c7c1b824d 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -419,14 +419,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK_THROWS_WITH_AS(j_nonobject_const.value("/foo"_json_pointer, 1), "[json.exception.type_error.306] cannot use value() with string", typename Json::type_error&); } - SECTION("array") - { - Json j_nonobject(Json::value_t::array); - const Json j_nonobject_const(Json::value_t::array); - CHECK_THROWS_WITH_AS(j_nonobject.value("/foo"_json_pointer, 1), "[json.exception.type_error.306] cannot use value() with array", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.value("/foo"_json_pointer, 1), "[json.exception.type_error.306] cannot use value() with array", typename Json::type_error&); - } - SECTION("number (integer)") { Json j_nonobject(Json::value_t::number_integer); @@ -451,6 +443,55 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK_THROWS_WITH_AS(j_nonobject_const.value("/foo"_json_pointer, 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); } } + + SECTION("access on array type") + { + Json j_array = Json::array({j}); + const Json j_array_const = Json::array({j}); + + CHECK(j_array.value("/0/integer"_json_pointer, 2) == 1); + CHECK(j_array.value("/0/integer"_json_pointer, 1.0) == Approx(1)); + CHECK(j_array.value("/0/unsigned"_json_pointer, 2) == 1u); + CHECK(j_array.value("/0/unsigned"_json_pointer, 1.0) == Approx(1u)); + CHECK(j_array.value("/0/null"_json_pointer, Json(1)) == Json()); + CHECK(j_array.value("/0/boolean"_json_pointer, false) == true); + CHECK(j_array.value("/0/string"_json_pointer, "bar") == "hello world"); + CHECK(j_array.value("/0/string"_json_pointer, std::string("bar")) == "hello world"); + CHECK(j_array.value("/0/floating"_json_pointer, 12.34) == Approx(42.23)); + CHECK(j_array.value("/0/floating"_json_pointer, 12) == 42); + CHECK(j_array.value("/0/object"_json_pointer, Json({{"foo", "bar"}})) == Json::object()); + CHECK(j_array.value("/0/array"_json_pointer, Json({10, 100})) == Json({1, 2, 3})); + + CHECK(j_array_const.value("/0/integer"_json_pointer, 2) == 1); + CHECK(j_array_const.value("/0/integer"_json_pointer, 1.0) == Approx(1)); + CHECK(j_array_const.value("/0/unsigned"_json_pointer, 2) == 1u); + CHECK(j_array_const.value("/0/unsigned"_json_pointer, 1.0) == Approx(1u)); + CHECK(j_array_const.value("/0/boolean"_json_pointer, false) == true); + CHECK(j_array_const.value("/0/string"_json_pointer, "bar") == "hello world"); + CHECK(j_array_const.value("/0/string"_json_pointer, std::string("bar")) == "hello world"); + CHECK(j_array_const.value("/0/floating"_json_pointer, 12.34) == Approx(42.23)); + CHECK(j_array_const.value("/0/floating"_json_pointer, 12) == 42); + CHECK(j_array_const.value("/0/object"_json_pointer, Json({{"foo", "bar"}})) == Json::object()); + CHECK(j_array_const.value("/0/array"_json_pointer, Json({10, 100})) == Json({1, 2, 3})); + + // Test out-of-range array index + CHECK(j_array.value("/10"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/10"_json_pointer, 42) == 42); + + // Test "-" index (append position is invalid) + CHECK(j_array.value("/-"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/-"_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&); + CHECK_THROWS_WITH_AS(j_array_const.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&); + + // Test leading-zero index throws parse_error + CHECK_THROWS_WITH_AS(j_array.value("/01"_json_pointer, 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", typename Json::parse_error&); + CHECK_THROWS_WITH_AS(j_array_const.value("/01"_json_pointer, 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", typename Json::parse_error&); +#endif + } } }