diff --git a/docs/mkdocs/docs/api/basic_json/at.md b/docs/mkdocs/docs/api/basic_json/at.md index 194b123ce..feb44f20e 100644 --- a/docs/mkdocs/docs/api/basic_json/at.md +++ b/docs/mkdocs/docs/api/basic_json/at.md @@ -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 diff --git a/docs/mkdocs/docs/api/basic_json/operator[].md b/docs/mkdocs/docs/api/basic_json/operator[].md index a4872dc39..6d16d6432 100644 --- a/docs/mkdocs/docs/api/basic_json/operator[].md +++ b/docs/mkdocs/docs/api/basic_json/operator[].md @@ -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. diff --git a/docs/mkdocs/docs/features/parsing/untrusted_input.md b/docs/mkdocs/docs/features/parsing/untrusted_input.md index 65f1bda25..f48c837d1 100644 --- a/docs/mkdocs/docs/features/parsing/untrusted_input.md +++ b/docs/mkdocs/docs/features/parsing/untrusted_input.md @@ -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 diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 457985480..f5c0a0db8 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -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 + 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 + 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 + 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 + 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 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(*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(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 + 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 rollback; + rollback.null_converted(ptr, &saved_null); + BasicJsonType& result = create_values(ptr, first); + rollback.commit(); + return result; + } + + get_unchecked_rollback 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 + 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(reference_token)); } } - else - { - // primitive value with a reference token left - throw_unresolved(reference_token, ptr); - } } return *ptr; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f0b788c9e..ac2c66bef 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -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 + 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 + 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 + 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 + 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 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(*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(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 + 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 rollback; + rollback.null_converted(ptr, &saved_null); + BasicJsonType& result = create_values(ptr, first); + rollback.commit(); + return result; + } + + get_unchecked_rollback 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 + 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(reference_token)); } } - else - { - // primitive value with a reference token left - throw_unresolved(reference_token, ptr); - } } return *ptr; diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 1a4e726ed..ff7a7f44d 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -29,9 +29,64 @@ using nlohmann::json; #include #include #include +#include #include #include +namespace +{ +// the non-const operator[] must throw for pointer and leave the value parsed +// from init unchanged (strong exception safety) +template +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 +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 +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("null", "/99999999999999999999999", unresolved); + check_strong_guarantee("null", "/a/99999999999999999999999", unresolved); + check_strong_guarantee(R"({"a":null})", "/a/b/-/99999999999999999999999", unresolved); + // the first change inserts a key into an object + check_strong_guarantee("{}", "/a/99999999999999999999999", unresolved); + check_strong_guarantee(R"({"a":{"b":1}})", "/a/c/d/99999999999999999999999", unresolved); + // the first change pads or appends to an array + check_strong_guarantee(R"({"a":[1]})", "/a/5/99999999999999999999999", unresolved); + check_strong_guarantee(R"({"a":[1]})", "/a/-/x/1/99999999999999999999999", unresolved); + // an index that does not fit into size_type + check_strong_guarantee("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::max)() - 1); + check_strong_guarantee_length_error("null", "/" + too_large); + check_strong_guarantee_length_error(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(); + check_strong_guarantee_cases(); + } + + 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")