json/include/nlohmann/detail/json_pointer.hpp
Thomas Braun ecbb2756fd json_pointer::array_index: Use unsigned values for the array index when parsing
The current code uses std::stoi to convert the input string to an int
array_index. This limits the maximum addressable array size to ~2GB on
most platforms.

But all callers immediately convert the result of array_index to
BasicJsonType::size_type.

So let's parse it as unsigned long long, which allows us to have as
big arrays as available memory. And also makes the call sites nicer to
read.

One complication arises on platforms where size_type is smaller than
unsigned long long. We need to bail out on these if the parsed array
index does not fit into size_type.
2020-06-22 13:42:55 +02:00

983 lines
32 KiB
C++

#pragma once
#include <algorithm> // all_of
#include <cassert> // assert
#include <cctype> // isdigit
#include <limits> // max
#include <numeric> // accumulate
#include <string> // string
#include <utility> // move
#include <vector> // vector
#include <nlohmann/detail/exceptions.hpp>
#include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/value_t.hpp>
namespace nlohmann
{
template<typename BasicJsonType>
class json_pointer
{
// allow basic_json to access private members
NLOHMANN_BASIC_JSON_TPL_DECLARATION
friend class basic_json;
public:
/*!
@brief create JSON pointer
Create a JSON pointer according to the syntax described in
[Section 3 of RFC6901](https://tools.ietf.org/html/rfc6901#section-3).
@param[in] s string representing the JSON pointer; if omitted, the empty
string is assumed which references the whole JSON value
@throw parse_error.107 if the given JSON pointer @a s is nonempty and does
not begin with a slash (`/`); see example below
@throw parse_error.108 if a tilde (`~`) in the given JSON pointer @a s is
not followed by `0` (representing `~`) or `1` (representing `/`); see
example below
@liveexample{The example shows the construction several valid JSON pointers
as well as the exceptional behavior.,json_pointer}
@since version 2.0.0
*/
explicit json_pointer(const std::string& s = "")
: reference_tokens(split(s))
{}
/*!
@brief return a string representation of the JSON pointer
@invariant For each JSON pointer `ptr`, it holds:
@code {.cpp}
ptr == json_pointer(ptr.to_string());
@endcode
@return a string representation of the JSON pointer
@liveexample{The example shows the result of `to_string`.,json_pointer__to_string}
@since version 2.0.0
*/
std::string to_string() const
{
return std::accumulate(reference_tokens.begin(), reference_tokens.end(),
std::string{},
[](const std::string & a, const std::string & b)
{
return a + "/" + escape(b);
});
}
/// @copydoc to_string()
operator std::string() const
{
return to_string();
}
/*!
@brief append another JSON pointer at the end of this JSON pointer
@param[in] ptr JSON pointer to append
@return JSON pointer with @a ptr appended
@liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add}
@complexity Linear in the length of @a ptr.
@sa @ref operator/=(std::string) to append a reference token
@sa @ref operator/=(std::size_t) to append an array index
@sa @ref operator/(const json_pointer&, const json_pointer&) for a binary operator
@since version 3.6.0
*/
json_pointer& operator/=(const json_pointer& ptr)
{
reference_tokens.insert(reference_tokens.end(),
ptr.reference_tokens.begin(),
ptr.reference_tokens.end());
return *this;
}
/*!
@brief append an unescaped reference token at the end of this JSON pointer
@param[in] token reference token to append
@return JSON pointer with @a token appended without escaping @a token
@liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add}
@complexity Amortized constant.
@sa @ref operator/=(const json_pointer&) to append a JSON pointer
@sa @ref operator/=(std::size_t) to append an array index
@sa @ref operator/(const json_pointer&, std::size_t) for a binary operator
@since version 3.6.0
*/
json_pointer& operator/=(std::string token)
{
push_back(std::move(token));
return *this;
}
/*!
@brief append an array index at the end of this JSON pointer
@param[in] array_idx array index to append
@return JSON pointer with @a array_idx appended
@liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add}
@complexity Amortized constant.
@sa @ref operator/=(const json_pointer&) to append a JSON pointer
@sa @ref operator/=(std::string) to append a reference token
@sa @ref operator/(const json_pointer&, std::string) for a binary operator
@since version 3.6.0
*/
json_pointer& operator/=(std::size_t array_idx)
{
return *this /= std::to_string(array_idx);
}
/*!
@brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer
@param[in] lhs JSON pointer
@param[in] rhs JSON pointer
@return a new JSON pointer with @a rhs appended to @a lhs
@liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary}
@complexity Linear in the length of @a lhs and @a rhs.
@sa @ref operator/=(const json_pointer&) to append a JSON pointer
@since version 3.6.0
*/
friend json_pointer operator/(const json_pointer& lhs,
const json_pointer& rhs)
{
return json_pointer(lhs) /= rhs;
}
/*!
@brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
@param[in] ptr JSON pointer
@param[in] token reference token
@return a new JSON pointer with unescaped @a token appended to @a ptr
@liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary}
@complexity Linear in the length of @a ptr.
@sa @ref operator/=(std::string) to append a reference token
@since version 3.6.0
*/
friend json_pointer operator/(const json_pointer& ptr, std::string token)
{
return json_pointer(ptr) /= std::move(token);
}
/*!
@brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer
@param[in] ptr JSON pointer
@param[in] array_idx array index
@return a new JSON pointer with @a array_idx appended to @a ptr
@liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary}
@complexity Linear in the length of @a ptr.
@sa @ref operator/=(std::size_t) to append an array index
@since version 3.6.0
*/
friend json_pointer operator/(const json_pointer& ptr, std::size_t array_idx)
{
return json_pointer(ptr) /= array_idx;
}
/*!
@brief returns the parent of this JSON pointer
@return parent of this JSON pointer; in case this JSON pointer is the root,
the root itself is returned
@complexity Linear in the length of the JSON pointer.
@liveexample{The example shows the result of `parent_pointer` for different
JSON Pointers.,json_pointer__parent_pointer}
@since version 3.6.0
*/
json_pointer parent_pointer() const
{
if (empty())
{
return *this;
}
json_pointer res = *this;
res.pop_back();
return res;
}
/*!
@brief remove last reference token
@pre not `empty()`
@liveexample{The example shows the usage of `pop_back`.,json_pointer__pop_back}
@complexity Constant.
@throw out_of_range.405 if JSON pointer has no parent
@since version 3.6.0
*/
void pop_back()
{
if (JSON_HEDLEY_UNLIKELY(empty()))
{
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
}
reference_tokens.pop_back();
}
/*!
@brief return last reference token
@pre not `empty()`
@return last reference token
@liveexample{The example shows the usage of `back`.,json_pointer__back}
@complexity Constant.
@throw out_of_range.405 if JSON pointer has no parent
@since version 3.6.0
*/
const std::string& back() const
{
if (JSON_HEDLEY_UNLIKELY(empty()))
{
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
}
return reference_tokens.back();
}
/*!
@brief append an unescaped token at the end of the reference pointer
@param[in] token token to add
@complexity Amortized constant.
@liveexample{The example shows the result of `push_back` for different
JSON Pointers.,json_pointer__push_back}
@since version 3.6.0
*/
void push_back(const std::string& token)
{
reference_tokens.push_back(token);
}
/// @copydoc push_back(const std::string&)
void push_back(std::string&& token)
{
reference_tokens.push_back(std::move(token));
}
/*!
@brief return whether pointer points to the root document
@return true iff the JSON pointer points to the root document
@complexity Constant.
@exceptionsafety No-throw guarantee: this function never throws exceptions.
@liveexample{The example shows the result of `empty` for different JSON
Pointers.,json_pointer__empty}
@since version 3.6.0
*/
bool empty() const noexcept
{
return reference_tokens.empty();
}
private:
/*!
@param[in] s reference token to be converted into an array index
@return integer representation of @a s
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index begins not with a digit
@throw out_of_range.404 if string @a s could not be converted to an integer
@throw out_of_range.410 if an array index exceeds size_type
*/
static typename BasicJsonType::size_type array_index(const std::string& s)
{
using size_type = typename BasicJsonType::size_type;
// error condition (cf. RFC 6901, Sect. 4)
if (JSON_HEDLEY_UNLIKELY(s.size() > 1 and s[0] == '0'))
{
JSON_THROW(detail::parse_error::create(106, 0,
"array index '" + s +
"' must not begin with '0'"));
}
// error condition (cf. RFC 6901, Sect. 4)
if (JSON_HEDLEY_UNLIKELY(s.size() > 1 and not (s[0] >= '1' and s[0] <= '9')))
{
JSON_THROW(detail::parse_error::create(109, 0, "array index '" + s + "' is not a number"));
}
std::size_t processed_chars = 0;
unsigned long long res = 0;
JSON_TRY
{
res = std::stoull(s, &processed_chars);
}
JSON_CATCH(std::out_of_range&)
{
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'"));
}
// check if the string was completely read
if (JSON_HEDLEY_UNLIKELY(processed_chars != s.size()))
{
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'"));
}
// only triggered on special platforms (like 32bit), see also
// https://github.com/nlohmann/json/pull/2203
if (res >= static_cast<unsigned long long>((std::numeric_limits<size_type>::max)()))
{
JSON_THROW(detail::out_of_range::create(410, "array index " + s + " exceeds size_type")); // LCOV_EXCL_LINE
}
return static_cast<size_type>(res);
}
json_pointer top() const
{
if (JSON_HEDLEY_UNLIKELY(empty()))
{
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
}
json_pointer result = *this;
result.reference_tokens = {reference_tokens[0]};
return result;
}
/*!
@brief create and return a reference to the pointed to value
@complexity Linear in the number of reference tokens.
@throw parse_error.109 if array index is not a number
@throw type_error.313 if value cannot be unflattened
*/
BasicJsonType& get_and_create(BasicJsonType& j) const
{
using size_type = typename BasicJsonType::size_type;
auto result = &j;
// in case no reference tokens exist, return a reference to the JSON value
// j which will be overwritten by a primitive value
for (const auto& reference_token : reference_tokens)
{
switch (result->type())
{
case detail::value_t::null:
{
if (reference_token == "0")
{
// start a new array if reference token is 0
result = &result->operator[](0);
}
else
{
// start a new object otherwise
result = &result->operator[](reference_token);
}
break;
}
case detail::value_t::object:
{
// create an entry in the object
result = &result->operator[](reference_token);
break;
}
case detail::value_t::array:
{
// create an entry in the array
result = &result->operator[](array_index(reference_token));
break;
}
/*
The following code is only reached if there exists a reference
token _and_ the current value is primitive. In this case, we have
an error situation, because primitive values may only occur as
single value; that is, with an empty list of reference tokens.
*/
default:
JSON_THROW(detail::type_error::create(313, "invalid value to unflatten"));
}
}
return *result;
}
/*!
@brief return a reference to the pointed to value
@note This version does not throw if a value is not present, but tries to
create nested values instead. For instance, calling this function
with pointer `"/this/that"` on a null value is equivalent to calling
`operator[]("this").operator[]("that")` on that value, effectively
changing the null value to an object.
@param[in] ptr a JSON value
@return reference to the JSON value pointed to by the JSON pointer
@complexity Linear in the length of the 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
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
{
using size_type = typename BasicJsonType::size_type;
for (const auto& reference_token : reference_tokens)
{
// convert null values to arrays or objects before continuing
if (ptr->is_null())
{
// check if reference token is a number
const bool nums =
std::all_of(reference_token.begin(), reference_token.end(),
[](const unsigned char x)
{
return std::isdigit(x);
});
// change value to array for numbers or "-" or to object otherwise
*ptr = (nums or reference_token == "-")
? detail::value_t::array
: detail::value_t::object;
}
switch (ptr->type())
{
case detail::value_t::object:
{
// use unchecked object access
ptr = &ptr->operator[](reference_token);
break;
}
case detail::value_t::array:
{
if (reference_token == "-")
{
// explicitly treat "-" as index beyond the end
ptr = &ptr->operator[](ptr->m_value.array->size());
}
else
{
// convert array index to number; unchecked access
ptr = &ptr->operator[](array_index(reference_token));
}
break;
}
default:
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
}
}
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
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
BasicJsonType& get_checked(BasicJsonType* ptr) const
{
using size_type = typename BasicJsonType::size_type;
for (const auto& reference_token : reference_tokens)
{
switch (ptr->type())
{
case detail::value_t::object:
{
// note: at performs range check
ptr = &ptr->at(reference_token);
break;
}
case detail::value_t::array:
{
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" always fails the range check
JSON_THROW(detail::out_of_range::create(402,
"array index '-' (" + std::to_string(ptr->m_value.array->size()) +
") is out of range"));
}
// note: at performs range check
ptr = &ptr->at(array_index(reference_token));
break;
}
default:
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
}
}
return *ptr;
}
/*!
@brief return a const reference to the pointed to value
@param[in] ptr a JSON value
@return const reference to the JSON value pointed to by the 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
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
const BasicJsonType& get_unchecked(const BasicJsonType* ptr) const
{
using size_type = typename BasicJsonType::size_type;
for (const auto& reference_token : reference_tokens)
{
switch (ptr->type())
{
case detail::value_t::object:
{
// use unchecked object access
ptr = &ptr->operator[](reference_token);
break;
}
case detail::value_t::array:
{
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" cannot be used for const access
JSON_THROW(detail::out_of_range::create(402,
"array index '-' (" + std::to_string(ptr->m_value.array->size()) +
") is out of range"));
}
// use unchecked array access
ptr = &ptr->operator[](array_index(reference_token));
break;
}
default:
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
}
}
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
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
const BasicJsonType& get_checked(const BasicJsonType* ptr) const
{
using size_type = typename BasicJsonType::size_type;
for (const auto& reference_token : reference_tokens)
{
switch (ptr->type())
{
case detail::value_t::object:
{
// note: at performs range check
ptr = &ptr->at(reference_token);
break;
}
case detail::value_t::array:
{
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" always fails the range check
JSON_THROW(detail::out_of_range::create(402,
"array index '-' (" + std::to_string(ptr->m_value.array->size()) +
") is out of range"));
}
// note: at performs range check
ptr = &ptr->at(array_index(reference_token));
break;
}
default:
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
}
}
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
*/
bool contains(const BasicJsonType* ptr) const
{
using size_type = typename BasicJsonType::size_type;
for (const auto& reference_token : reference_tokens)
{
switch (ptr->type())
{
case detail::value_t::object:
{
if (not ptr->contains(reference_token))
{
// we did not find the key in the object
return false;
}
ptr = &ptr->operator[](reference_token);
break;
}
case detail::value_t::array:
{
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" always fails the range check
return false;
}
if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 and not ("0" <= reference_token and reference_token <= "9")))
{
// invalid char
return false;
}
if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1))
{
if (JSON_HEDLEY_UNLIKELY(not ('1' <= reference_token[0] and reference_token[0] <= '9')))
{
// 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(not ('0' <= reference_token[i] and reference_token[i] <= '9')))
{
// other char should be between '0' and '9'
return false;
}
}
}
const auto idx = array_index(reference_token);
if (idx >= ptr->size())
{
// index out of range
return false;
}
ptr = &ptr->operator[](idx);
break;
}
default:
{
// we do not expect primitive values if there is still a
// reference token to process
return false;
}
}
}
// no reference token left means we found a primitive value
return true;
}
/*!
@brief split the string input to reference tokens
@note This function is only called by the json_pointer constructor.
All exceptions below are documented there.
@throw parse_error.107 if the pointer is not empty or begins with '/'
@throw parse_error.108 if character '~' is not followed by '0' or '1'
*/
static std::vector<std::string> split(const std::string& reference_string)
{
std::vector<std::string> result;
// special case: empty reference string -> no reference tokens
if (reference_string.empty())
{
return result;
}
// check if nonempty reference string begins with slash
if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/'))
{
JSON_THROW(detail::parse_error::create(107, 1,
"JSON pointer must be empty or begin with '/' - was: '" +
reference_string + "'"));
}
// extract the reference tokens:
// - slash: position of the last read slash (or end of string)
// - start: position after the previous slash
for (
// search for the first slash after the first character
std::size_t slash = reference_string.find_first_of('/', 1),
// set the beginning of the first reference token
start = 1;
// we can stop if start == 0 (if slash == std::string::npos)
start != 0;
// set the beginning of the next reference token
// (will eventually be 0 if slash == std::string::npos)
start = (slash == std::string::npos) ? 0 : slash + 1,
// find next slash
slash = reference_string.find_first_of('/', start))
{
// use the text between the beginning of the reference token
// (start) and the last slash (slash).
auto reference_token = reference_string.substr(start, slash - start);
// check reference tokens are properly escaped
for (std::size_t pos = reference_token.find_first_of('~');
pos != std::string::npos;
pos = reference_token.find_first_of('~', pos + 1))
{
assert(reference_token[pos] == '~');
// ~ must be followed by 0 or 1
if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 or
(reference_token[pos + 1] != '0' and
reference_token[pos + 1] != '1')))
{
JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'"));
}
}
// finally, store the reference token
unescape(reference_token);
result.push_back(reference_token);
}
return result;
}
/*!
@brief replace all occurrences of a substring by another string
@param[in,out] s the string to manipulate; changed so that all
occurrences of @a f are replaced with @a t
@param[in] f the substring to replace with @a t
@param[in] t the string to replace @a f
@pre The search string @a f must not be empty. **This precondition is
enforced with an assertion.**
@since version 2.0.0
*/
static void replace_substring(std::string& s, const std::string& f,
const std::string& t)
{
assert(not f.empty());
for (auto pos = s.find(f); // find first occurrence of f
pos != std::string::npos; // make sure f was found
s.replace(pos, f.size(), t), // replace with t, and
pos = s.find(f, pos + t.size())) // find next occurrence of f
{}
}
/// escape "~" to "~0" and "/" to "~1"
static std::string escape(std::string s)
{
replace_substring(s, "~", "~0");
replace_substring(s, "/", "~1");
return s;
}
/// unescape "~1" to tilde and "~0" to slash (order is important!)
static void unescape(std::string& s)
{
replace_substring(s, "~1", "/");
replace_substring(s, "~0", "~");
}
/*!
@param[in] reference_string the reference string to the current value
@param[in] value the value to consider
@param[in,out] result the result object to insert values to
@note Empty objects or arrays are flattened to `null`.
*/
static void flatten(const std::string& reference_string,
const BasicJsonType& value,
BasicJsonType& result)
{
switch (value.type())
{
case detail::value_t::array:
{
if (value.m_value.array->empty())
{
// flatten empty array as null
result[reference_string] = nullptr;
}
else
{
// iterate array and use index as reference string
for (std::size_t i = 0; i < value.m_value.array->size(); ++i)
{
flatten(reference_string + "/" + std::to_string(i),
value.m_value.array->operator[](i), result);
}
}
break;
}
case detail::value_t::object:
{
if (value.m_value.object->empty())
{
// flatten empty object as null
result[reference_string] = nullptr;
}
else
{
// iterate object and use keys as reference string
for (const auto& element : *value.m_value.object)
{
flatten(reference_string + "/" + escape(element.first), element.second, result);
}
}
break;
}
default:
{
// add primitive value with its reference string
result[reference_string] = value;
break;
}
}
}
/*!
@param[in] value flattened JSON
@return unflattened JSON
@throw parse_error.109 if array index is not a number
@throw type_error.314 if value is not an object
@throw type_error.315 if object values are not primitive
@throw type_error.313 if value cannot be unflattened
*/
static BasicJsonType
unflatten(const BasicJsonType& value)
{
if (JSON_HEDLEY_UNLIKELY(not value.is_object()))
{
JSON_THROW(detail::type_error::create(314, "only objects can be unflattened"));
}
BasicJsonType result;
// iterate the JSON object values
for (const auto& element : *value.m_value.object)
{
if (JSON_HEDLEY_UNLIKELY(not element.second.is_primitive()))
{
JSON_THROW(detail::type_error::create(315, "values in object must be primitive"));
}
// assign value to reference pointed to by JSON pointer; Note that if
// the JSON pointer is "" (i.e., points to the whole value), function
// get_and_create returns a reference to result itself. An assignment
// will then create a primitive value.
json_pointer(element.first).get_and_create(result) = element.second;
}
return result;
}
/*!
@brief compares two JSON pointers for equality
@param[in] lhs JSON pointer to compare
@param[in] rhs JSON pointer to compare
@return whether @a lhs is equal to @a rhs
@complexity Linear in the length of the JSON pointer
@exceptionsafety No-throw guarantee: this function never throws exceptions.
*/
friend bool operator==(json_pointer const& lhs,
json_pointer const& rhs) noexcept
{
return lhs.reference_tokens == rhs.reference_tokens;
}
/*!
@brief compares two JSON pointers for inequality
@param[in] lhs JSON pointer to compare
@param[in] rhs JSON pointer to compare
@return whether @a lhs is not equal @a rhs
@complexity Linear in the length of the JSON pointer
@exceptionsafety No-throw guarantee: this function never throws exceptions.
*/
friend bool operator!=(json_pointer const& lhs,
json_pointer const& rhs) noexcept
{
return not (lhs == rhs);
}
/// the reference tokens
std::vector<std::string> reference_tokens;
};
} // namespace nlohmann