mirror of
https://github.com/nlohmann/json.git
synced 2026-10-26 15:58:55 +00:00
Keep the value unchanged when operator[] with a JSON pointer throws
The non-const operator[](json_pointer) is documented with strong exception
safety, but get_unchecked created intermediate values while walking the
pointer and kept them when a later reference token failed, e.g.
json j; j["/a/99999999999999999999999"_json_pointer] threw 404 and left
{"a":[]}; padding an existing array or appending with "-" behaved alike,
and so did std::length_error/std::bad_alloc from a huge index.
get_unchecked now only walks existing values up to the last reference
token. The last token's key insertion or array growth is strongly
exception safe by itself. Any other change goes through
get_unchecked_create, which records the one change it makes to an existing
value (null converted, key inserted, array grown) in a RAII guard and
reverts it if a later step throws; everything else it creates lies below
that change. The guard needs no try/catch, so JSON_NOEXCEPTION builds are
unaffected. Successful calls create the same values as before.
Docs: state the guarantee in the exceptions list and version history of
operator[], fix the complexity of the pointer overloads of operator[] and
at, and warn that padding arrays up to an untrusted index allocates memory
proportional to the index (operator[].md, untrusted_input.md).
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
6 files changed
+617
-19
No files matched your search
@@ -90,7 +90,8 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
||||
1. Constant.
|
||||
2. Logarithmic in the size of the container.
|
||||
3. Logarithmic in the size of the container.
|
||||
4. Logarithmic in the size of the container.
|
||||
4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
|
||||
constant for an array.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -92,12 +92,18 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
||||
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
|
||||
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
|
||||
|
||||
If the **non-const** version throws, the value is left unchanged: values it created for earlier reference tokens
|
||||
of `ptr` are removed again. This also holds for exceptions such as `#!cpp std::bad_alloc` or
|
||||
`#!cpp std::length_error` thrown while padding an array up to a very large index.
|
||||
|
||||
## Complexity
|
||||
|
||||
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
|
||||
2. Logarithmic in the size of the container.
|
||||
3. Logarithmic in the size of the container.
|
||||
4. Logarithmic in the size of the container.
|
||||
4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
|
||||
constant for an array. When the non-const version pads an array with `#!json null` values, it is additionally
|
||||
linear in `idx - size()` for that array.
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -145,6 +151,14 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
||||
specified by the JSON Pointer RFC; it is this library's own, intentional disambiguation rule. See also
|
||||
[JSON Pointer](../../features/json_pointer.md).
|
||||
|
||||
!!! warning "Large array indices"
|
||||
|
||||
As the array is filled up with `#!json null` values to the given index, the memory used grows with the index
|
||||
and not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB.
|
||||
The library does not limit the index, so check the array indices of a JSON pointer from untrusted input
|
||||
before using it with the non-const version. See
|
||||
[Parsing untrusted input](../../features/parsing/untrusted_input.md#json-pointers-and-flattened-keys).
|
||||
|
||||
!!! warning "Deprecation"
|
||||
|
||||
Overload (4) also accepts a [`json_pointer`](../json_pointer/index.md) whose template argument is a `basic_json`
|
||||
@@ -294,4 +308,6 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
||||
4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
|
||||
version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
|
||||
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
|
||||
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
|
||||
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones. Fixed in version 3.13.0 to leave the value
|
||||
unchanged if the non-const version throws (strong exception safety, as documented); before, values created for earlier
|
||||
reference tokens of `ptr` were kept.
|
||||
@@ -130,6 +130,17 @@ parameters of [`parse()`](../../api/basic_json/parse.md) or [`accept()`](../../a
|
||||
enable either for input whose conformance you cannot otherwise control, since interoperability with strictly
|
||||
conforming JSON consumers is exactly what the default rejects.
|
||||
|
||||
## JSON Pointers and flattened keys
|
||||
|
||||
A [JSON Pointer](../json_pointer.md) can come from untrusted input as well, for instance as a path in a request or as a
|
||||
key of a flattened object. The non-const [`operator[]`](../../api/basic_json/operator%5B%5D.md) with a JSON Pointer and
|
||||
[`unflatten`](../../api/basic_json/unflatten.md) create missing values, and when an array index lies beyond the end of
|
||||
an array, they fill the array with `#!json null` values up to that index. The memory used therefore grows with the
|
||||
index, not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB, the index
|
||||
`100000000` about 1.6 GB. The library does not limit the index. For pointers from untrusted input, check the array
|
||||
indices (or the pointer as a whole) before using them, or use [`at`](../../api/basic_json/at.md) or
|
||||
[`contains`](../../api/basic_json/contains.md), which never add values.
|
||||
|
||||
## Checklist
|
||||
|
||||
- Wrap parsing in a `#!cpp try`/`#!cpp catch` block, or use `allow_exceptions=false`/`accept()` if your environment
|
||||
@@ -145,6 +156,8 @@ conforming JSON consumers is exactly what the default rejects.
|
||||
`JSON_STRICT_NUL_HANDLING`.
|
||||
- Decide whether duplicate object keys should be an error for your application, and add a callback if so.
|
||||
- Do not enable `ignore_comments` or `ignore_trailing_commas` for input that must be strictly conforming JSON.
|
||||
- Check the array indices of untrusted JSON Pointers and flattened keys before passing them to the non-const
|
||||
`operator[]` or `unflatten()`, which fill arrays with `#!json null` values up to the index.
|
||||
|
||||
For the broader design rationale and how it is tested (fuzzing, sanitizers, static analysis), see the
|
||||
[assurance case](../../community/assurance_case.md) and [quality assurance](../../community/quality_assurance.md). To
|
||||
|
||||
@@ -589,6 +589,121 @@ class json_pointer
|
||||
return object_member_impl(object, key, detail::priority_tag<1> {});
|
||||
}
|
||||
|
||||
/// @brief look up @a key in @a members without inserting it
|
||||
///
|
||||
/// Uses the same search as @ref object_member, so that both compile to the
|
||||
/// same code for std::map.
|
||||
template<typename ObjectType>
|
||||
static auto object_find_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||
-> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
|
||||
{
|
||||
const auto it = members.lower_bound(key);
|
||||
return (it != members.end() && !members.key_comp()(key, it->first)) ? it : members.end();
|
||||
}
|
||||
|
||||
template<typename ObjectType>
|
||||
static auto object_find_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/)
|
||||
-> decltype(members.find(key))
|
||||
{
|
||||
return members.find(key);
|
||||
}
|
||||
|
||||
template<typename ObjectType>
|
||||
static auto object_find(ObjectType& members, const string_t& key)
|
||||
-> decltype(object_find_impl(members, key, detail::priority_tag<1> {}))
|
||||
{
|
||||
return object_find_impl(members, key, detail::priority_tag<1> {});
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief undoes the change @ref get_unchecked_create made to an existing value
|
||||
|
||||
@ref get_unchecked walks existing values without changing them. At the
|
||||
first reference token it cannot resolve, @ref get_unchecked_create makes
|
||||
exactly one change to an existing value: it converts a null value, inserts
|
||||
a key into an object, or appends to an array. Everything it creates
|
||||
afterwards lies below that change. If a later step throws, the destructor
|
||||
of this guard reverts that one change, which removes everything created
|
||||
after it, so the value is left as it was (strong exception safety). The
|
||||
guard needs no try/catch and therefore also compiles with JSON_NOEXCEPTION.
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
class get_unchecked_rollback
|
||||
{
|
||||
public:
|
||||
get_unchecked_rollback() = default;
|
||||
get_unchecked_rollback(const get_unchecked_rollback&) = delete;
|
||||
get_unchecked_rollback(get_unchecked_rollback&&) = delete;
|
||||
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
|
||||
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
|
||||
|
||||
~get_unchecked_rollback()
|
||||
{
|
||||
switch (m_change)
|
||||
{
|
||||
case change::null_converted:
|
||||
// the copy of the null value also brings back its base
|
||||
// class and its positions
|
||||
*m_value = std::move(*m_saved_null);
|
||||
break;
|
||||
case change::key_inserted:
|
||||
m_value->m_data.m_value.object->erase(*m_key);
|
||||
break;
|
||||
case change::array_grown:
|
||||
m_value->m_data.m_value.array->resize(m_old_size);
|
||||
break;
|
||||
case change::none:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// record that the null value @a value, of which @a saved is a copy, is about to be converted
|
||||
void null_converted(BasicJsonType* value, BasicJsonType* saved) noexcept
|
||||
{
|
||||
m_value = value;
|
||||
m_saved_null = saved;
|
||||
m_change = change::null_converted;
|
||||
}
|
||||
|
||||
/// record that @a key, a missing key, is about to be inserted into the object @a value
|
||||
void key_inserted(BasicJsonType* value, const string_t* key) noexcept
|
||||
{
|
||||
m_value = value;
|
||||
m_key = key;
|
||||
m_change = change::key_inserted;
|
||||
}
|
||||
|
||||
/// record that the array @a value is about to grow from @a old_size elements
|
||||
void array_grown(BasicJsonType* value, typename BasicJsonType::size_type old_size) noexcept
|
||||
{
|
||||
m_value = value;
|
||||
m_old_size = old_size;
|
||||
m_change = change::array_grown;
|
||||
}
|
||||
|
||||
/// keep all changes
|
||||
void commit() noexcept
|
||||
{
|
||||
m_change = change::none;
|
||||
}
|
||||
|
||||
private:
|
||||
enum class change
|
||||
{
|
||||
none,
|
||||
null_converted,
|
||||
key_inserted,
|
||||
array_grown
|
||||
};
|
||||
|
||||
change m_change = change::none;
|
||||
BasicJsonType* m_value = nullptr;
|
||||
const string_t* m_key = nullptr;
|
||||
typename BasicJsonType::size_type m_old_size = 0;
|
||||
BasicJsonType* m_saved_null = nullptr;
|
||||
};
|
||||
|
||||
JSON_PRIVATE_UNLESS_TESTED:
|
||||
json_pointer top() const
|
||||
{
|
||||
@@ -972,6 +1087,12 @@ class json_pointer
|
||||
|
||||
Complexity: Linear in the length of the JSON pointer.
|
||||
|
||||
Exception safety: Strong guarantee. This function only walks existing
|
||||
values up to the last reference token. Inserting a key into an object or
|
||||
growing an array for the last reference token is strongly exception safe
|
||||
by itself; any other change is made by @ref get_unchecked_create, which
|
||||
undoes it if it throws.
|
||||
|
||||
@throw parse_error.106 if an array index begins with '0'
|
||||
@throw parse_error.109 if an array index was not a number
|
||||
@throw out_of_range.404 if the JSON pointer can not be resolved
|
||||
@@ -979,8 +1100,137 @@ class json_pointer
|
||||
template<typename BasicJsonType>
|
||||
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
||||
{
|
||||
for (const auto& reference_token : reference_tokens)
|
||||
if (reference_tokens.empty())
|
||||
{
|
||||
return *ptr;
|
||||
}
|
||||
if (ptr->is_null())
|
||||
{
|
||||
// the null value is converted to an array or an object
|
||||
return get_unchecked_create(ptr, reference_tokens.data());
|
||||
}
|
||||
|
||||
// all reference tokens but the last one: walk existing values only
|
||||
const auto& last_token = reference_tokens.back();
|
||||
for (const auto* token = reference_tokens.data(); token != &last_token; ++token)
|
||||
{
|
||||
if (ptr->is_object())
|
||||
{
|
||||
auto& members = *ptr->m_data.m_value.object;
|
||||
const auto it = object_find(members, *token);
|
||||
if (it == members.end())
|
||||
{
|
||||
// the key is inserted
|
||||
return get_unchecked_create(ptr, token);
|
||||
}
|
||||
ptr = &it->second;
|
||||
}
|
||||
else if (ptr->is_array())
|
||||
{
|
||||
if (is_dash(*token))
|
||||
{
|
||||
// an element is appended
|
||||
return get_unchecked_create(ptr, token);
|
||||
}
|
||||
const auto idx = array_index<BasicJsonType>(*token);
|
||||
if (idx >= ptr->m_data.m_value.array->size())
|
||||
{
|
||||
// the array is padded with null values
|
||||
return get_unchecked_create(ptr, token);
|
||||
}
|
||||
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||
}
|
||||
else if (ptr->is_null())
|
||||
{
|
||||
// the null value is converted to an array or an object
|
||||
return get_unchecked_create(ptr, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
// primitive value with a reference token left
|
||||
throw_unresolved(*token, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// the last reference token: insert a missing key or grow the array
|
||||
if (ptr->is_object())
|
||||
{
|
||||
// use unchecked object access
|
||||
return object_member(*ptr, last_token);
|
||||
}
|
||||
if (ptr->is_array())
|
||||
{
|
||||
// explicitly treat "-" as index beyond the end; otherwise convert
|
||||
// array index to number; unchecked access
|
||||
const auto idx = is_dash(last_token)
|
||||
? ptr->m_data.m_value.array->size()
|
||||
: array_index<BasicJsonType>(last_token);
|
||||
return ptr->operator[](idx);
|
||||
}
|
||||
if (ptr->is_null())
|
||||
{
|
||||
// the null value is converted to an array or an object
|
||||
return get_unchecked_create(ptr, &last_token);
|
||||
}
|
||||
// primitive value with a reference token left
|
||||
throw_unresolved(last_token, ptr);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief create the values for the reference tokens from @a first on
|
||||
|
||||
Continues @ref get_unchecked at the first reference token that does not
|
||||
select an existing value. That token changes @a ptr, an existing value
|
||||
(see @ref get_unchecked_rollback); all later tokens only change values
|
||||
created here.
|
||||
|
||||
@param[in] ptr the value the reference token @a first is applied to
|
||||
@param[in] first the first reference token to resolve (an element of
|
||||
@ref reference_tokens)
|
||||
|
||||
@throw parse_error.106 if an array index begins with '0'
|
||||
@throw parse_error.109 if an array index was not a number
|
||||
@throw out_of_range.404 if the JSON pointer can not be resolved
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
JSON_HEDLEY_NEVER_INLINE BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first) const
|
||||
{
|
||||
if (ptr->is_null())
|
||||
{
|
||||
// the copy keeps the base class and the positions of the null
|
||||
// value; it must outlive the guard
|
||||
BasicJsonType saved_null = *ptr;
|
||||
get_unchecked_rollback<BasicJsonType> rollback;
|
||||
rollback.null_converted(ptr, &saved_null);
|
||||
BasicJsonType& result = create_values(ptr, first);
|
||||
rollback.commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
get_unchecked_rollback<BasicJsonType> rollback;
|
||||
if (ptr->is_object())
|
||||
{
|
||||
rollback.key_inserted(ptr, first);
|
||||
}
|
||||
else
|
||||
{
|
||||
JSON_ASSERT(ptr->is_array());
|
||||
rollback.array_grown(ptr, ptr->m_data.m_value.array->size());
|
||||
}
|
||||
BasicJsonType& result = create_values(ptr, first);
|
||||
rollback.commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// the loop of @ref get_unchecked_create
|
||||
template<typename BasicJsonType>
|
||||
BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first) const
|
||||
{
|
||||
const auto* const end = reference_tokens.data() + reference_tokens.size();
|
||||
for (const auto* token = first; token != end; ++token)
|
||||
{
|
||||
const auto& reference_token = *token;
|
||||
|
||||
// convert null values to arrays or objects before continuing
|
||||
if (ptr->is_null())
|
||||
{
|
||||
@@ -1007,9 +1257,12 @@ class json_pointer
|
||||
// use unchecked object access
|
||||
ptr = &object_member(*ptr, reference_token);
|
||||
}
|
||||
else if (ptr->is_array())
|
||||
else
|
||||
{
|
||||
if (reference_token == "-")
|
||||
// only the first value is not created here, and it is no
|
||||
// primitive value (see get_unchecked)
|
||||
JSON_ASSERT(ptr->is_array());
|
||||
if (is_dash(reference_token))
|
||||
{
|
||||
// explicitly treat "-" as index beyond the end
|
||||
ptr = &ptr->operator[](ptr->m_data.m_value.array->size());
|
||||
@@ -1020,11 +1273,6 @@ class json_pointer
|
||||
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// primitive value with a reference token left
|
||||
throw_unresolved(reference_token, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
return *ptr;
|
||||
|
||||
Generated
+256
-8
@@ -24174,6 +24174,121 @@ class json_pointer
|
||||
return object_member_impl(object, key, detail::priority_tag<1> {});
|
||||
}
|
||||
|
||||
/// @brief look up @a key in @a members without inserting it
|
||||
///
|
||||
/// Uses the same search as @ref object_member, so that both compile to the
|
||||
/// same code for std::map.
|
||||
template<typename ObjectType>
|
||||
static auto object_find_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||
-> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
|
||||
{
|
||||
const auto it = members.lower_bound(key);
|
||||
return (it != members.end() && !members.key_comp()(key, it->first)) ? it : members.end();
|
||||
}
|
||||
|
||||
template<typename ObjectType>
|
||||
static auto object_find_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/)
|
||||
-> decltype(members.find(key))
|
||||
{
|
||||
return members.find(key);
|
||||
}
|
||||
|
||||
template<typename ObjectType>
|
||||
static auto object_find(ObjectType& members, const string_t& key)
|
||||
-> decltype(object_find_impl(members, key, detail::priority_tag<1> {}))
|
||||
{
|
||||
return object_find_impl(members, key, detail::priority_tag<1> {});
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief undoes the change @ref get_unchecked_create made to an existing value
|
||||
|
||||
@ref get_unchecked walks existing values without changing them. At the
|
||||
first reference token it cannot resolve, @ref get_unchecked_create makes
|
||||
exactly one change to an existing value: it converts a null value, inserts
|
||||
a key into an object, or appends to an array. Everything it creates
|
||||
afterwards lies below that change. If a later step throws, the destructor
|
||||
of this guard reverts that one change, which removes everything created
|
||||
after it, so the value is left as it was (strong exception safety). The
|
||||
guard needs no try/catch and therefore also compiles with JSON_NOEXCEPTION.
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
class get_unchecked_rollback
|
||||
{
|
||||
public:
|
||||
get_unchecked_rollback() = default;
|
||||
get_unchecked_rollback(const get_unchecked_rollback&) = delete;
|
||||
get_unchecked_rollback(get_unchecked_rollback&&) = delete;
|
||||
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
|
||||
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
|
||||
|
||||
~get_unchecked_rollback()
|
||||
{
|
||||
switch (m_change)
|
||||
{
|
||||
case change::null_converted:
|
||||
// the copy of the null value also brings back its base
|
||||
// class and its positions
|
||||
*m_value = std::move(*m_saved_null);
|
||||
break;
|
||||
case change::key_inserted:
|
||||
m_value->m_data.m_value.object->erase(*m_key);
|
||||
break;
|
||||
case change::array_grown:
|
||||
m_value->m_data.m_value.array->resize(m_old_size);
|
||||
break;
|
||||
case change::none:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// record that the null value @a value, of which @a saved is a copy, is about to be converted
|
||||
void null_converted(BasicJsonType* value, BasicJsonType* saved) noexcept
|
||||
{
|
||||
m_value = value;
|
||||
m_saved_null = saved;
|
||||
m_change = change::null_converted;
|
||||
}
|
||||
|
||||
/// record that @a key, a missing key, is about to be inserted into the object @a value
|
||||
void key_inserted(BasicJsonType* value, const string_t* key) noexcept
|
||||
{
|
||||
m_value = value;
|
||||
m_key = key;
|
||||
m_change = change::key_inserted;
|
||||
}
|
||||
|
||||
/// record that the array @a value is about to grow from @a old_size elements
|
||||
void array_grown(BasicJsonType* value, typename BasicJsonType::size_type old_size) noexcept
|
||||
{
|
||||
m_value = value;
|
||||
m_old_size = old_size;
|
||||
m_change = change::array_grown;
|
||||
}
|
||||
|
||||
/// keep all changes
|
||||
void commit() noexcept
|
||||
{
|
||||
m_change = change::none;
|
||||
}
|
||||
|
||||
private:
|
||||
enum class change
|
||||
{
|
||||
none,
|
||||
null_converted,
|
||||
key_inserted,
|
||||
array_grown
|
||||
};
|
||||
|
||||
change m_change = change::none;
|
||||
BasicJsonType* m_value = nullptr;
|
||||
const string_t* m_key = nullptr;
|
||||
typename BasicJsonType::size_type m_old_size = 0;
|
||||
BasicJsonType* m_saved_null = nullptr;
|
||||
};
|
||||
|
||||
JSON_PRIVATE_UNLESS_TESTED:
|
||||
json_pointer top() const
|
||||
{
|
||||
@@ -24557,6 +24672,12 @@ class json_pointer
|
||||
|
||||
Complexity: Linear in the length of the JSON pointer.
|
||||
|
||||
Exception safety: Strong guarantee. This function only walks existing
|
||||
values up to the last reference token. Inserting a key into an object or
|
||||
growing an array for the last reference token is strongly exception safe
|
||||
by itself; any other change is made by @ref get_unchecked_create, which
|
||||
undoes it if it throws.
|
||||
|
||||
@throw parse_error.106 if an array index begins with '0'
|
||||
@throw parse_error.109 if an array index was not a number
|
||||
@throw out_of_range.404 if the JSON pointer can not be resolved
|
||||
@@ -24564,8 +24685,137 @@ class json_pointer
|
||||
template<typename BasicJsonType>
|
||||
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
||||
{
|
||||
for (const auto& reference_token : reference_tokens)
|
||||
if (reference_tokens.empty())
|
||||
{
|
||||
return *ptr;
|
||||
}
|
||||
if (ptr->is_null())
|
||||
{
|
||||
// the null value is converted to an array or an object
|
||||
return get_unchecked_create(ptr, reference_tokens.data());
|
||||
}
|
||||
|
||||
// all reference tokens but the last one: walk existing values only
|
||||
const auto& last_token = reference_tokens.back();
|
||||
for (const auto* token = reference_tokens.data(); token != &last_token; ++token)
|
||||
{
|
||||
if (ptr->is_object())
|
||||
{
|
||||
auto& members = *ptr->m_data.m_value.object;
|
||||
const auto it = object_find(members, *token);
|
||||
if (it == members.end())
|
||||
{
|
||||
// the key is inserted
|
||||
return get_unchecked_create(ptr, token);
|
||||
}
|
||||
ptr = &it->second;
|
||||
}
|
||||
else if (ptr->is_array())
|
||||
{
|
||||
if (is_dash(*token))
|
||||
{
|
||||
// an element is appended
|
||||
return get_unchecked_create(ptr, token);
|
||||
}
|
||||
const auto idx = array_index<BasicJsonType>(*token);
|
||||
if (idx >= ptr->m_data.m_value.array->size())
|
||||
{
|
||||
// the array is padded with null values
|
||||
return get_unchecked_create(ptr, token);
|
||||
}
|
||||
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||
}
|
||||
else if (ptr->is_null())
|
||||
{
|
||||
// the null value is converted to an array or an object
|
||||
return get_unchecked_create(ptr, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
// primitive value with a reference token left
|
||||
throw_unresolved(*token, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// the last reference token: insert a missing key or grow the array
|
||||
if (ptr->is_object())
|
||||
{
|
||||
// use unchecked object access
|
||||
return object_member(*ptr, last_token);
|
||||
}
|
||||
if (ptr->is_array())
|
||||
{
|
||||
// explicitly treat "-" as index beyond the end; otherwise convert
|
||||
// array index to number; unchecked access
|
||||
const auto idx = is_dash(last_token)
|
||||
? ptr->m_data.m_value.array->size()
|
||||
: array_index<BasicJsonType>(last_token);
|
||||
return ptr->operator[](idx);
|
||||
}
|
||||
if (ptr->is_null())
|
||||
{
|
||||
// the null value is converted to an array or an object
|
||||
return get_unchecked_create(ptr, &last_token);
|
||||
}
|
||||
// primitive value with a reference token left
|
||||
throw_unresolved(last_token, ptr);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief create the values for the reference tokens from @a first on
|
||||
|
||||
Continues @ref get_unchecked at the first reference token that does not
|
||||
select an existing value. That token changes @a ptr, an existing value
|
||||
(see @ref get_unchecked_rollback); all later tokens only change values
|
||||
created here.
|
||||
|
||||
@param[in] ptr the value the reference token @a first is applied to
|
||||
@param[in] first the first reference token to resolve (an element of
|
||||
@ref reference_tokens)
|
||||
|
||||
@throw parse_error.106 if an array index begins with '0'
|
||||
@throw parse_error.109 if an array index was not a number
|
||||
@throw out_of_range.404 if the JSON pointer can not be resolved
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
JSON_HEDLEY_NEVER_INLINE BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first) const
|
||||
{
|
||||
if (ptr->is_null())
|
||||
{
|
||||
// the copy keeps the base class and the positions of the null
|
||||
// value; it must outlive the guard
|
||||
BasicJsonType saved_null = *ptr;
|
||||
get_unchecked_rollback<BasicJsonType> rollback;
|
||||
rollback.null_converted(ptr, &saved_null);
|
||||
BasicJsonType& result = create_values(ptr, first);
|
||||
rollback.commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
get_unchecked_rollback<BasicJsonType> rollback;
|
||||
if (ptr->is_object())
|
||||
{
|
||||
rollback.key_inserted(ptr, first);
|
||||
}
|
||||
else
|
||||
{
|
||||
JSON_ASSERT(ptr->is_array());
|
||||
rollback.array_grown(ptr, ptr->m_data.m_value.array->size());
|
||||
}
|
||||
BasicJsonType& result = create_values(ptr, first);
|
||||
rollback.commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// the loop of @ref get_unchecked_create
|
||||
template<typename BasicJsonType>
|
||||
BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first) const
|
||||
{
|
||||
const auto* const end = reference_tokens.data() + reference_tokens.size();
|
||||
for (const auto* token = first; token != end; ++token)
|
||||
{
|
||||
const auto& reference_token = *token;
|
||||
|
||||
// convert null values to arrays or objects before continuing
|
||||
if (ptr->is_null())
|
||||
{
|
||||
@@ -24592,9 +24842,12 @@ class json_pointer
|
||||
// use unchecked object access
|
||||
ptr = &object_member(*ptr, reference_token);
|
||||
}
|
||||
else if (ptr->is_array())
|
||||
else
|
||||
{
|
||||
if (reference_token == "-")
|
||||
// only the first value is not created here, and it is no
|
||||
// primitive value (see get_unchecked)
|
||||
JSON_ASSERT(ptr->is_array());
|
||||
if (is_dash(reference_token))
|
||||
{
|
||||
// explicitly treat "-" as index beyond the end
|
||||
ptr = &ptr->operator[](ptr->m_data.m_value.array->size());
|
||||
@@ -24605,11 +24858,6 @@ class json_pointer
|
||||
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// primitive value with a reference token left
|
||||
throw_unresolved(reference_token, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
return *ptr;
|
||||
|
||||
@@ -29,9 +29,64 @@ using nlohmann::json;
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
// the non-const operator[] must throw for pointer and leave the value parsed
|
||||
// from init unchanged (strong exception safety)
|
||||
template<typename Json>
|
||||
void check_strong_guarantee(const char* init, const std::string& pointer, const char* message)
|
||||
{
|
||||
CAPTURE(init)
|
||||
CAPTURE(pointer)
|
||||
Json j = Json::parse(init);
|
||||
const Json before = Json::parse(init);
|
||||
const typename Json::json_pointer ptr(pointer);
|
||||
CHECK_THROWS_WITH_AS(j[ptr], message, typename Json::exception&);
|
||||
CHECK(j == before);
|
||||
}
|
||||
|
||||
// as above, for an index that fits into size_type, but not into an array
|
||||
template<typename Json>
|
||||
void check_strong_guarantee_length_error(const char* init, const std::string& pointer)
|
||||
{
|
||||
CAPTURE(init)
|
||||
CAPTURE(pointer)
|
||||
Json j = Json::parse(init);
|
||||
const Json before = Json::parse(init);
|
||||
const typename Json::json_pointer ptr(pointer);
|
||||
CHECK_THROWS_AS(j[ptr], std::length_error&);
|
||||
CHECK(j == before);
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
void check_strong_guarantee_cases()
|
||||
{
|
||||
const char* const unresolved = "[json.exception.out_of_range.404] unresolved reference token '99999999999999999999999'";
|
||||
|
||||
// the first change converts a null value
|
||||
check_strong_guarantee<Json>("null", "/99999999999999999999999", unresolved);
|
||||
check_strong_guarantee<Json>("null", "/a/99999999999999999999999", unresolved);
|
||||
check_strong_guarantee<Json>(R"({"a":null})", "/a/b/-/99999999999999999999999", unresolved);
|
||||
// the first change inserts a key into an object
|
||||
check_strong_guarantee<Json>("{}", "/a/99999999999999999999999", unresolved);
|
||||
check_strong_guarantee<Json>(R"({"a":{"b":1}})", "/a/c/d/99999999999999999999999", unresolved);
|
||||
// the first change pads or appends to an array
|
||||
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/5/99999999999999999999999", unresolved);
|
||||
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/-/x/1/99999999999999999999999", unresolved);
|
||||
// an index that does not fit into size_type
|
||||
check_strong_guarantee<Json>("null", "/x/18446744073709551615",
|
||||
"[json.exception.out_of_range.410] array index 18446744073709551615 exceeds size_type");
|
||||
// an index that fits into size_type, but not into an array
|
||||
const auto too_large = std::to_string((std::numeric_limits<typename Json::size_type>::max)() - 1);
|
||||
check_strong_guarantee_length_error<Json>("null", "/" + too_large);
|
||||
check_strong_guarantee_length_error<Json>(R"({"a":[[1]]})", "/a/0/3/" + too_large);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("JSON pointers")
|
||||
{
|
||||
SECTION("errors")
|
||||
@@ -557,6 +612,23 @@ TEST_CASE("JSON pointers")
|
||||
CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer),
|
||||
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
||||
}
|
||||
|
||||
SECTION("a failing access leaves the value unchanged")
|
||||
{
|
||||
check_strong_guarantee_cases<json>();
|
||||
check_strong_guarantee_cases<nlohmann::ordered_json>();
|
||||
}
|
||||
|
||||
SECTION("a successful access still creates the same values")
|
||||
{
|
||||
json j = {{"a", {1}}};
|
||||
j["/a/3/x/-/y"_json_pointer] = 1;
|
||||
CHECK(j == json::parse(R"({"a":[1,null,null,{"x":[{"y":1}]}]})"));
|
||||
j["/a/-"_json_pointer] = 2;
|
||||
j["/a/0"_json_pointer] = 3;
|
||||
j["/b"_json_pointer] = 4;
|
||||
CHECK(j == json::parse(R"({"a":[3,null,null,{"x":[{"y":1}]},2],"b":4})"));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("flatten")
|
||||
|
||||
Reference in new issue
Block a user