Compare commits

..
Author SHA1 Message Date
Niels Lohmann ab0aafbadf Fix CI: bind j_nested_const by reference in the json_pointer test
clang-tidy (ci_clang_tidy) flagged the new test with
performance-unnecessary-copy-initialization: the local copy
j_nested_const of j_nested is never modified. Bind it as a const
reference instead; it still exercises the const overloads of at() and
contains().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 16:39:09 +02:00
Niels Lohmann c1945cb8d5 Do not throw in contains() for an empty array reference token
json_pointer::contains() rejected malformed array indices, but an empty
reference token (e.g. "/a/" where "a" is an array, or "/" on an array)
passed every check and reached array_index(), which throws
out_of_range.404. contains() must not throw (cf. #5395), so it now
returns false for an empty token. at() still throws out_of_range.404.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 06:37:09 +02:00
5 changed files with 56 additions and 28 deletions
+6
View File
@@ -746,6 +746,12 @@ class json_pointer
// "-" always fails the range check
return false;
}
if (JSON_HEDLEY_UNLIKELY(reference_token.empty()))
{
// an empty reference token is not an array index; array_index()
// would throw out_of_range.404 -- contains() must not throw (see #5395)
return false;
}
if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !("0" <= reference_token && reference_token <= "9")))
{
// invalid char
@@ -19,17 +19,11 @@ namespace detail
/*!
@brief the number of nesting levels an operation recurses into
Operations that walk a value (copying, comparing, serializing, hashing, merging,
...) recurse once
Operations that walk a value (serializing, hashing, merging, ...) recurse once
per nesting level, which is fastest, but a value nested deeply enough would
exhaust the call stack. So they recurse only this many levels deep and finish
whatever lies below with an explicit stack. All of them share this limit.
Most of them pass the depth down as an argument. The copy constructor and the
comparison operators cannot, as their signatures are fixed, so they count it
in basic_json::nesting_depth() instead, a byte per thread; the limit must
therefore stay below 255.
@sa https://github.com/nlohmann/json/issues/5387
*/
constexpr std::size_t recursion_depth_limit() noexcept
+11 -7
View File
@@ -898,8 +898,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
#ifndef JSON_NO_THREAD_LOCAL
// nesting_depth() is a byte and may exceed the limit by one level
static_assert(detail::recursion_depth_limit() < 255, "the nesting depth count must fit in a byte");
/// the number of levels an operation descends into before it finishes the
/// value below it without the call stack
static constexpr std::uint8_t nesting_depth_limit()
{
return 128;
}
/*!
@brief how many levels the operation going on in this thread has descended into
@@ -941,7 +945,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
static_cast<void>(may_descend);
return true;
#else
return !may_descend || nesting_depth() >= detail::recursion_depth_limit();
return !may_descend || nesting_depth() >= nesting_depth_limit();
#endif
}
@@ -965,7 +969,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
#ifdef JSON_NO_THREAD_LOCAL
: m_okay(false)
#else
: m_okay(nesting_depth() < detail::recursion_depth_limit())
: m_okay(nesting_depth() < nesting_depth_limit())
#endif
{
#ifndef JSON_NO_THREAD_LOCAL
@@ -1169,7 +1173,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
The values whose copy has not been created yet are kept on an explicit
worklist rather than on the call stack. This is only reached for values
nested deeper than @ref detail::recursion_depth_limit levels, which is why it copies
nested deeper than @ref nesting_depth_limit levels, which is why it copies
every container by hand instead of letting the container do it: the fast
ways of doing so would descend into the elements and defeat the purpose.
*/
@@ -1235,7 +1239,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
Copying a container copies its elements, so a value nested deeply enough
used to exhaust the call stack. The descent is bounded here: the first
@ref detail::recursion_depth_limit levels are copied by the containers themselves, just
@ref nesting_depth_limit levels are copied by the containers themselves, just
as they always were, and anything below that is copied without the call
stack by @ref copy_iteratively. Copying a value can therefore no longer
exhaust the stack, however deeply it is nested, just like destroying one
@@ -1373,7 +1377,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/*!
@brief compare @a lhs and @a rhs without descending into them
Reached once a comparison has descended @ref detail::recursion_depth_limit levels, so
Reached once a comparison has descended @ref nesting_depth_limit levels, so
that comparing values cannot exhaust the call stack however deeply they are
nested. The two values are walked in lockstep on an explicit stack and
compared lexicographically, element by element in the order the containers
+18 -14
View File
@@ -7281,17 +7281,11 @@ namespace detail
/*!
@brief the number of nesting levels an operation recurses into
Operations that walk a value (copying, comparing, serializing, hashing, merging,
...) recurse once
Operations that walk a value (serializing, hashing, merging, ...) recurse once
per nesting level, which is fastest, but a value nested deeply enough would
exhaust the call stack. So they recurse only this many levels deep and finish
whatever lies below with an explicit stack. All of them share this limit.
Most of them pass the depth down as an argument. The copy constructor and the
comparison operators cannot, as their signatures are fixed, so they count it
in basic_json::nesting_depth() instead, a byte per thread; the limit must
therefore stay below 255.
@sa https://github.com/nlohmann/json/issues/5387
*/
constexpr std::size_t recursion_depth_limit() noexcept
@@ -19594,6 +19588,12 @@ class json_pointer
// "-" always fails the range check
return false;
}
if (JSON_HEDLEY_UNLIKELY(reference_token.empty()))
{
// an empty reference token is not an array index; array_index()
// would throw out_of_range.404 -- contains() must not throw (see #5395)
return false;
}
if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !("0" <= reference_token && reference_token <= "9")))
{
// invalid char
@@ -26985,8 +26985,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
#ifndef JSON_NO_THREAD_LOCAL
// nesting_depth() is a byte and may exceed the limit by one level
static_assert(detail::recursion_depth_limit() < 255, "the nesting depth count must fit in a byte");
/// the number of levels an operation descends into before it finishes the
/// value below it without the call stack
static constexpr std::uint8_t nesting_depth_limit()
{
return 128;
}
/*!
@brief how many levels the operation going on in this thread has descended into
@@ -27028,7 +27032,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
static_cast<void>(may_descend);
return true;
#else
return !may_descend || nesting_depth() >= detail::recursion_depth_limit();
return !may_descend || nesting_depth() >= nesting_depth_limit();
#endif
}
@@ -27052,7 +27056,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
#ifdef JSON_NO_THREAD_LOCAL
: m_okay(false)
#else
: m_okay(nesting_depth() < detail::recursion_depth_limit())
: m_okay(nesting_depth() < nesting_depth_limit())
#endif
{
#ifndef JSON_NO_THREAD_LOCAL
@@ -27256,7 +27260,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
The values whose copy has not been created yet are kept on an explicit
worklist rather than on the call stack. This is only reached for values
nested deeper than @ref detail::recursion_depth_limit levels, which is why it copies
nested deeper than @ref nesting_depth_limit levels, which is why it copies
every container by hand instead of letting the container do it: the fast
ways of doing so would descend into the elements and defeat the purpose.
*/
@@ -27322,7 +27326,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
Copying a container copies its elements, so a value nested deeply enough
used to exhaust the call stack. The descent is bounded here: the first
@ref detail::recursion_depth_limit levels are copied by the containers themselves, just
@ref nesting_depth_limit levels are copied by the containers themselves, just
as they always were, and anything below that is copied without the call
stack by @ref copy_iteratively. Copying a value can therefore no longer
exhaust the stack, however deeply it is nested, just like destroying one
@@ -27460,7 +27464,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/*!
@brief compare @a lhs and @a rhs without descending into them
Reached once a comparison has descended @ref detail::recursion_depth_limit levels, so
Reached once a comparison has descended @ref nesting_depth_limit levels, so
that comparing values cannot exhaust the call stack however deeply they are
nested. The two values are walked in lockstep on an explicit stack and
compared lexicographically, element by element in the order the containers
+20
View File
@@ -380,6 +380,26 @@ TEST_CASE("JSON pointers")
DOCTEST_MSVC_SUPPRESS_WARNING_POP
{
// contains() must not throw for an empty reference token if the current
// value is an array (cf. #5395) -- at() still reports out_of_range.404
json j_nested = {{"a", {1, 2}}};
const json& j_nested_const = j_nested;
json::json_pointer const jp("/a/");
std::string const throw_msg = "[json.exception.out_of_range.404] unresolved reference token ''";
CHECK_THROWS_WITH_AS(j_nested.at(jp), throw_msg.c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(j_nested_const.at(jp), throw_msg.c_str(), json::out_of_range&);
CHECK(j_nested.contains(json::json_pointer("/a/1")));
CHECK(!j_nested.contains(jp));
CHECK(!j_nested_const.contains(jp));
// same for an empty reference token on a top-level array
CHECK(!j.contains(json::json_pointer("/")));
CHECK(!j_const.contains(json::json_pointer("/")));
}
CHECK_THROWS_WITH_AS(j.at("/one"_json_pointer) = 1,
"[json.exception.parse_error.109] parse error: array index 'one' is not a number", json::parse_error&);
CHECK_THROWS_WITH_AS(j_const.at("/one"_json_pointer) == 1,