Compare commits

..
Author SHA1 Message Date
Niels Lohmann 95519dbbcb Detect a patch stored inside the patched value with JSON_DIAGNOSTICS
patch_inplace applied a copy only when the patch was the value itself. With JSON_DIAGNOSTICS the parent pointers make it cheap to also detect a patch nested inside the value, so that case is handled the same way. Without diagnostics it stays documented undefined behavior.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 17:18:36 +01:00
Niels Lohmann 359c889ed2 Fix JSON_DIAGNOSTICS test crash and clang-tidy hit in patch_inplace self-aliasing
The spare-capacity test reallocated non-empty elements through get_ref(), which leaves stale parent pointers under JSON_DIAGNOSTICS; reserve while the array is empty instead. Suppress performance-unnecessary-copy-initialization for the copy that is needed because *this is modified.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 14:36:57 +01:00
Niels Lohmann 16793d6884 Merge develop
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 14:34:36 +01:00
Niels Lohmann 9edff5292d Merge branch 'develop' into fix-self-aliasing-pointer-patch
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 07:29:17 +01:00
Niels Lohmann 8dd8f5ce8e Merge origin/develop into fix-self-aliasing-pointer-patch
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 21:39:59 +01:00
Niels Lohmann 0babaa1a4e Fix self-aliasing in json_pointer::operator/= and patch_inplace
json_pointer::operator/=(const json_pointer&) passed iterators into its
own token vector to insert() when a pointer was appended to itself
(p /= p), which is undefined behavior; libstdc++ produced "/a/b//" for
"/a/b". The pointer now appends a copy in that case.

basic_json::patch_inplace iterated the patch while changing *this, so
j.patch_inplace(j) read freed memory. It now applies a copy of the
patch when the patch is the value itself. A patch stored inside the
value remains undefined behavior and is documented as such; copying the
patch unconditionally would double the time of a large patch.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 12:10:55 +01:00
21 changed files with 299 additions and 949 deletions

No files matched your search

@@ -69,6 +69,19 @@ Duplicate member names inside an operation object are resolved by the parser bef
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
`remove` operation. `remove` operation.
The JSON patch may be the JSON value itself (`#!cpp j.patch_inplace(j)`): the operations are then read from a copy of
the patch made before the first operation is applied, so the result is the same as that of
`#!cpp j = j.patch(j)`, and the exception safety is the same as above.
If [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) is enabled, the same holds for a JSON patch stored inside the
JSON value (e.g., `#!cpp j.patch_inplace(j["patch"])`): it is detected via the parent pointers.
!!! danger "Undefined behavior"
Without [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md), the JSON patch must not be a value stored inside the
JSON value (e.g., `#!cpp j.patch_inplace(j["patch"])`), as the operations may change or destroy it while it is
read. Pass a copy (`#!cpp j.patch_inplace(json(j["patch"]))`) or use [`patch`](patch.md) instead.
## Examples ## Examples
??? example "Example: apply a JSON patch in place" ??? example "Example: apply a JSON patch in place"
@@ -122,3 +135,5 @@ is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op":
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of - 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 [`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. not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
- Applying a JSON value as patch to itself (or, with `JSON_DIAGNOSTICS`, to a value stored inside it) no longer results
in undefined behavior in version 3.13.0.
@@ -32,20 +32,9 @@ uses, so the result matches it exactly — including, for an object, that a repe
last value. The replay is iterative, so it is not limited by the call stack the way a naive recursive conversion last value. The replay is iterative, so it is not limited by the call stack the way a naive recursive conversion
would be; the JSON nesting depth is limited only by available memory, as for `BasicJsonType::parse()` itself. would be; the JSON nesting depth is limited only by available memory, as for `BasicJsonType::parse()` itself.
With [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) enabled, the values get the source Unlike parsing with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) enabled, the values
positions [`BasicJsonType::parse()`](../basic_json/parse.md) would give them, also for the subtree of a view (the produced by `materialize()` do not carry source positions. The index has the start offset of every value, but not the
positions are those in the document's whole source text), and exceptions about them name the same bytes. For values of end of arrays, objects, and escaped strings, which the positions also need.
an [editable document](../json_editable_document.md), positions are all-or-nothing, and [`start_pos()`](../basic_json/start_pos.md)
and [`end_pos()`](../basic_json/end_pos.md) return `std::string::npos` for
- a value written by an edit (a copy of an array or object of the document is also new; copies of its scalars keep
their positions),
- an array or object whose elements were changed (its elements keep theirs), and
- an array or object whose last element has no positions (its end is found from that element).
A `basic_json` value, in contrast, keeps its positions when it is changed. Documents [loaded](../basic_json_document/load.md)
from an image have the positions of the saved document, except that its strings with escapes have none if it was saved
without `JSON_DIAGNOSTIC_POSITIONS`.
Calling `materialize()` on the same view repeatedly builds a new, independent `BasicJsonType` value each time; it Calling `materialize()` on the same view repeatedly builds a new, independent `BasicJsonType` value each time; it
never caches the result. never caches the result.
@@ -73,7 +62,8 @@ never caches the result.
- [`root`](../basic_json_document/root.md) - the view of a document's root value - [`root`](../basic_json_document/root.md) - the view of a document's root value
- [`BasicJsonType::parse`](../basic_json/parse.md) - build a `BasicJsonType` value directly from a JSON text - [`BasicJsonType::parse`](../basic_json/parse.md) - build a `BasicJsonType` value directly from a JSON text
- [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) - source positions on parsed values - [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) - source positions on parsed values (not
produced by `materialize()`)
- [Zero-copy JSON views](../../features/json_view.md#getting-values-out-without-copying) - when to build a basic_json value from a view - [Zero-copy JSON views](../../features/json_view.md#getting-values-out-without-copying) - when to build a basic_json value from a view
## Version history ## Version history
@@ -18,7 +18,8 @@ json_pointer& operator/=(std::size_t array_idx)
## Parameters ## Parameters
`ptr` (in) `ptr` (in)
: JSON pointer to append : JSON pointer to append; may be the JSON pointer itself (`#!cpp p /= p`), which then appends a copy of its
reference tokens
`token` (in) `token` (in)
: reference token to append : reference token to append
@@ -69,6 +70,7 @@ json_pointer& operator/=(std::size_t array_idx)
## Version history ## Version history
1. Added in version 3.6.0. 1. Added in version 3.6.0. Appending a JSON pointer to itself no longer results in undefined behavior in version
3.13.0.
2. Added in version 3.6.0. Changed type of `token` to `string_t` in version 3.11.0. 2. Added in version 3.6.0. Changed type of `token` to `string_t` in version 3.11.0.
3. Added in version 3.6.0. 3. Added in version 3.6.0.
@@ -113,7 +113,6 @@ When the macro is not defined, the library will define it to its default value.
- [:simple-cmake: JSON_Diagnostic_Positions](../../integration/cmake.md#json_diagnostic_positions) - CMake option to control the macro - [:simple-cmake: JSON_Diagnostic_Positions](../../integration/cmake.md#json_diagnostic_positions) - CMake option to control the macro
- [`JSON_DIAGNOSTICS`](json_diagnostics.md) - macro to control extended diagnostics - [`JSON_DIAGNOSTICS`](json_diagnostics.md) - macro to control extended diagnostics
- [`materialize()`](../basic_json_view/materialize.md) - values of a JSON view, with the same positions
- [Parsing and Exceptions](../../features/parsing/parse_exceptions.md) - handling parse errors and locating them in the input - [Parsing and Exceptions](../../features/parsing/parse_exceptions.md) - handling parse errors and locating them in the input
## Version history ## Version history
+3 -5
View File
@@ -149,12 +149,10 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
end must still fit. end must still fit.
- **A stream is always read to its end.** There is no partial/streaming read of an `#!cpp std::istream`: the stream - **A stream is always read to its end.** There is no partial/streaming read of an `#!cpp std::istream`: the stream
is read to its end into a buffer the document owns, and parsing starts after that. is read to its end into a buffer the document owns, and parsing starts after that.
- **Fewer source positions after edits.** With - **No source positions on `materialize()`.** Even with
[`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md) enabled, [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md) enabled,
[`materialize()`](../api/basic_json_view/materialize.md) sets the same positions as `#!cpp basic_json::parse()`. [`materialize()`](../api/basic_json_view/materialize.md) does not set them. The index has the start offset of every
In an [editable document](../api/json_editable_document.md), a value written by an edit has none, and neither has value, but not the end of arrays, objects, and escaped strings, which the positions also need.
an array or object whose elements changed or whose last element has none; a `basic_json` value keeps its positions
when it is changed. See [`materialize()`](../api/basic_json_view/materialize.md#notes).
- **Objects iterate in document order.** [`begin()`](../api/basic_json_view/begin.md)/ - **Objects iterate in document order.** [`begin()`](../api/basic_json_view/begin.md)/
[`end()`](../api/basic_json_view/end.md) and [`items()`](../api/basic_json_view/items.md) visit an object's [`end()`](../api/basic_json_view/end.md) and [`items()`](../api/basic_json_view/items.md) visit an object's
members in the order they appear in the source text. `basic_json`'s default `object_t` is a `std::map`, which members in the order they appear in the source text. `basic_json`'s default `object_t` is a `std::map`, which
+2 -7
View File
@@ -203,7 +203,7 @@ packet-beta
| 2-3 | `extra` | `uint16_t` | numbers: the number of integer digits (low byte) and fraction digits (high byte), 255 for more; objects: the number of their key hash index (1-based), or 0; otherwise 0 | | 2-3 | `extra` | `uint16_t` | numbers: the number of integer digits (low byte) and fraction digits (high byte), 255 for more; objects: the number of their key hash index (1-based), or 0; otherwise 0 |
| 4-7 | `off` | `uint32_t` | where the value starts: the first byte after a string's opening quote (or its position in the buffer of decoded strings), the first byte of a number or literal, the bracket of an array or object | | 4-7 | `off` | `uint32_t` | where the value starts: the first byte after a string's opening quote (or its position in the buffer of decoded strings), the first byte of a number or literal, the bracket of an array or object |
| 8-11 | `len` | `uint32_t` | strings: the length after decoding; floats and literals: the length of the token; arrays and objects: the number of elements | | 8-11 | `len` | `uint32_t` | strings: the length after decoding; floats and literals: the length of the token; arrays and objects: the number of elements |
| 12-15 | `next` | `uint32_t` | arrays and objects: the number of nodes of the subtree, including the node itself; strings with escapes: the position of the opening quote in the source plus one, or 0 (only with [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md)) | | 12-15 | `next` | `uint32_t` | arrays and objects: the number of nodes of the subtree, including the node itself |
<p class="nj-table-caption" markdown>Fields of a node, by their byte offset within the 16-byte node. Integers use bytes 8-15 differently, see below.</p> <p class="nj-table-caption" markdown>Fields of a node, by their byte offset within the 16-byte node. Integers use bytes 8-15 differently, see below.</p>
@@ -218,11 +218,6 @@ packet-beta
step from element to element this way and skip whole subtrees in constant time. step from element to element this way and skip whole subtrees in constant time.
- **Offsets** are 32 bits wide, so a document is limited to 4294967279 bytes, 4 GiB minus 16 bytes (a margin below - **Offsets** are 32 bits wide, so a document is limited to 4294967279 bytes, 4 GiB minus 16 bytes (a margin below
2^32 for positions one scanner step past the end of the text; `out_of_range.416`). 2^32 for positions one scanner step past the end of the text; `out_of_range.416`).
- **Source positions** ([`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md)) are derived by
[`materialize()`](../api/basic_json_view/materialize.md) and cost nothing while parsing: a scalar's range follows
from `off` and its length, a string with escapes is scanned from its opening quote (`next`) to the closing one,
and the end of an array or object is found by scanning from the end of its last element over whitespace, comments,
and a trailing comma. `materialize()` hands these positions to the SAX builder of `parse()` in place of its lexer.
- **Large objects** (128 members or more) get a hash index for their keys after parsing - **Large objects** (128 members or more) get a hash index for their keys after parsing
([`detail/view/object_index.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/object_index.hpp)): ([`detail/view/object_index.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/object_index.hpp)):
an open-addressing table whose slots hold the distance from the object's node to a key's node, so that a lookup does an open-addressing table whose slots hold the distance from the object's node to a key's node, so that a lookup does
@@ -282,7 +277,7 @@ Images ([`save`](../api/basic_json_document/save.md) and [`load`](../api/basic_j
the nodes as they are: a 64-byte header (the magic bytes `NJVI`, a format version, the sizes, and reserved bytes that the nodes as they are: a 64-byte header (the magic bytes `NJVI`, a format version, the sizes, and reserved bytes that
must be zero), the nodes, the text, and the decoded strings. An edited document is first written in document order, as must be zero), the nodes, the text, and the decoded strings. An edited document is first written in document order, as
the parser would have written it (without links), and the numbers of the hash indexes are cleared, since `load` the parser would have written it (without links), and the numbers of the hash indexes are cleared, since `load`
rebuilds the indexes. The flag *new* is kept, as these values have no position in the text. So a change of the node layout is a change of the image format: it must raise `image_version`, rebuilds the indexes. So a change of the node layout is a change of the image format: it must raise `image_version`,
and `load` then rejects images of other versions (`parse_error.116`) instead of misreading them. and `load` then rejects images of other versions (`parse_error.116`) instead of misreading them.
## Input adapters ## Input adapters
+3 -22
View File
@@ -230,10 +230,7 @@ struct diagnostic_positions
{ {
// Lexer has read the first character of the container, so // Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position. // subtract 1 from the position to get the correct start position.
// (npos: a value without a source position, see materialize() of v.start_position = lexer->get_chars_read_total() - 1;
// json_view)
const std::size_t read = lexer->get_chars_read_total();
v.start_position = read == std::string::npos ? read : read - 1;
} }
} }
@@ -248,12 +245,6 @@ struct diagnostic_positions
{ {
// Lexer's position is past the closing brace or bracket, so set that as the end position. // Lexer's position is past the closing brace or bracket, so set that as the end position.
v.end_position = lexer->get_chars_read_total(); v.end_position = lexer->get_chars_read_total();
if (v.end_position == std::string::npos)
{
// a range has both ends or none (exception::diagnostics()
// prints only complete ones)
v.start_position = v.end_position;
}
} }
} }
@@ -270,12 +261,6 @@ struct diagnostic_positions
// The start position will be set below based on the length of the string representation // The start position will be set below based on the length of the string representation
// of the value. // of the value.
v.end_position = lexer->get_chars_read_total(); v.end_position = lexer->get_chars_read_total();
if (v.end_position == std::string::npos)
{
// a value without a source position
v.start_position = v.end_position;
return;
}
switch (v.type()) switch (v.type())
{ {
@@ -488,12 +473,8 @@ read up to it.
@tparam Collect whether to collect the values of containers of unknown size on @tparam Collect whether to collect the values of containers of unknown size on
a stack (only for a caller that does not use the value after a a stack (only for a caller that does not use the value after a
parse error) parse error)
@tparam LexerType the type of the lexer that reports the source positions
(JSON_DIAGNOSTIC_POSITIONS); only diagnostic_positions uses
it, so any type with its three functions works (as
materialize() of json_view uses it)
*/ */
template<typename BasicJsonType, typename InputAdapterType, bool Collect = false, typename LexerType = lexer<BasicJsonType, InputAdapterType>> template<typename BasicJsonType, typename InputAdapterType, bool Collect = false>
class json_sax_dom_parser : public json_sax_dom_error_state class json_sax_dom_parser : public json_sax_dom_error_state
{ {
public: public:
@@ -502,7 +483,7 @@ class json_sax_dom_parser : public json_sax_dom_error_state
using number_float_t = typename BasicJsonType::number_float_t; using number_float_t = typename BasicJsonType::number_float_t;
using string_t = typename BasicJsonType::string_t; using string_t = typename BasicJsonType::string_t;
using binary_t = typename BasicJsonType::binary_t; using binary_t = typename BasicJsonType::binary_t;
using lexer_t = LexerType; using lexer_t = lexer<BasicJsonType, InputAdapterType>;
/*! /*!
@param[in,out] r reference to a JSON value that is manipulated while @param[in,out] r reference to a JSON value that is manipulated while
+2 -2
View File
@@ -56,8 +56,8 @@ using parser_callback_t =
template<typename SAX> template<typename SAX>
struct sax_ignores_number_text : std::false_type {}; struct sax_ignores_number_text : std::false_type {};
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType> template<typename BasicJsonType, typename InputAdapterType, bool Collect>
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect, LexerType>> : std::true_type {}; struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect>> : std::true_type {};
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType>
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {}; struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
+7
View File
@@ -243,6 +243,13 @@ class json_pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/ /// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/
json_pointer& operator/=(const json_pointer& ptr) json_pointer& operator/=(const json_pointer& ptr)
{ {
// insert() must not be given iterators into the vector it inserts
// into: append a copy when a pointer is appended to itself
if (JSON_HEDLEY_UNLIKELY(&ptr == this))
{
return *this /= json_pointer(ptr);
}
reference_tokens.insert(reference_tokens.end(), reference_tokens.insert(reference_tokens.end(),
ptr.reference_tokens.begin(), ptr.reference_tokens.begin(),
ptr.reference_tokens.end()); ptr.reference_tokens.end());
-6
View File
@@ -1193,13 +1193,7 @@ indent_done:
return false; return false;
} }
p = r.p; p = r.p;
#if JSON_DIAGNOSTIC_POSITIONS
// the decoded string is in the arena: next keeps the source offset
// of the opening quote plus one (0: unknown) for materialize()
push_node(node_kind::string, node_flags::escaped, 0, r.start, static_cast<std::uint64_t>(r.len) | (static_cast<std::uint64_t>(s - b) << 32));
#else
push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len); push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len);
#endif
return true; return true;
} }
}; };
+12 -18
View File
@@ -80,8 +80,7 @@ constexpr std::uint64_t image_limit = 0xFFFFFFF0u;
/// Copy the current structure of an edited document into nodes in document /// Copy the current structure of an edited document into nodes in document
/// order, as the parser would have written them. Text written by edits is /// order, as the parser would have written them. Text written by edits is
/// appended to text_tail (number tokens) and arena_tail (strings); floats that /// appended to text_tail (number tokens) and arena_tail (strings); floats that
/// are not finite become null, as dump() writes them. Nodes keep the flag /// are not finite become null, as dump() writes them.
/// is_new: they have no source position (JSON_DIAGNOSTIC_POSITIONS).
inline void compact_nodes(const document_data& d, std::size_t arena_size, std::vector<node>& out, std::string& text_tail, std::string& arena_tail) inline void compact_nodes(const document_data& d, std::size_t arena_size, std::vector<node>& out, std::string& text_tail, std::string& arena_tail)
{ {
struct frame struct frame
@@ -104,18 +103,16 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
arena_tail.append(d.str(s), s.len); arena_tail.append(d.str(s), s.len);
r.flags = node_flags::escaped; r.flags = node_flags::escaped;
} }
r.flags = static_cast<std::uint8_t>(r.flags | (s.flags & node_flags::is_new));
return r; return r;
}; };
const auto copy_node = [&](const node * v) const auto copy_node = [&](const node * v)
{ {
node r = *v; node r = *v;
const auto is_new = static_cast<std::uint8_t>(v->flags & node_flags::is_new);
switch (static_cast<value_t>(v->kind)) switch (static_cast<value_t>(v->kind))
{ {
case value_t::object: case value_t::object:
case value_t::array: case value_t::array:
r.flags = is_new; r.flags = 0;
r.extra = 0; r.extra = 0;
r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off; r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off;
r.len = 0; // counted below r.len = 0; // counted below
@@ -132,7 +129,7 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
r.off = static_cast<std::uint32_t>(d.size + text_tail.size()); r.off = static_cast<std::uint32_t>(d.size + text_tail.size());
text_tail.append(d.str(*v), number_length(*v)); text_tail.append(d.str(*v), number_length(*v));
} }
r.flags = is_new; r.flags = 0;
break; break;
case value_t::number_float: case value_t::number_float:
if ((v->flags & node_flags::storage) == node_flags::edited) if ((v->flags & node_flags::storage) == node_flags::edited)
@@ -142,23 +139,22 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
{ {
r = node{}; // nan and infinity: null, as dump() writes them r = node{}; // nan and infinity: null, as dump() writes them
r.kind = node_kind::null; r.kind = node_kind::null;
r.flags = is_new;
break; break;
} }
r.off = static_cast<std::uint32_t>(d.size + text_tail.size()); r.off = static_cast<std::uint32_t>(d.size + text_tail.size());
text_tail.append(t, v->len); text_tail.append(t, v->len);
r.extra = 0xFFFFu; // the digit layout is not recorded r.extra = 0xFFFFu; // the digit layout is not recorded
} }
r.flags = is_new; r.flags = 0;
break; break;
case value_t::boolean: case value_t::boolean:
r.flags = static_cast<std::uint8_t>(v->flags & (node_flags::is_true | node_flags::is_new)); r.flags = static_cast<std::uint8_t>(v->flags & node_flags::is_true);
break; break;
case value_t::null: case value_t::null:
case value_t::binary: case value_t::binary:
case value_t::discarded: case value_t::discarded:
default: default:
r.flags = is_new; r.flags = 0;
break; break;
} }
out.push_back(r); out.push_back(r);
@@ -519,15 +515,13 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
std::vector<float_range> floats; std::vector<float_range> floats;
const auto check_string = [&](const node & n) -> bool const auto check_string = [&](const node & n) -> bool
{ {
if ((n.flags & ~(node_flags::escaped | node_flags::is_new)) != 0 || n.extra != 0) if ((n.flags & ~node_flags::escaped) != 0 || n.extra != 0)
{ {
return false; return false;
} }
const bool decoded = (n.flags & node_flags::escaped) != 0; const bool decoded = (n.flags & node_flags::escaped) != 0;
const std::size_t limit = decoded ? arena_size : text_size; const std::size_t limit = decoded ? arena_size : text_size;
// (next: the source offset of a decoded string's opening quote plus if (n.off > limit || n.len > limit - n.off)
// one, or 0; see node)
if (n.off > limit || n.len > limit - n.off || n.next > text_size)
{ {
return false; return false;
} }
@@ -599,13 +593,13 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
{ {
case value_t::null: case value_t::null:
// (the offset of a literal is read to size the output of dump()) // (the offset of a literal is read to size the output of dump())
if ((n.flags & ~node_flags::is_new) != 0 || n.extra != 0 || n.off > text_size) if (n.flags != 0 || n.extra != 0 || n.off > text_size)
{ {
return false; return false;
} }
break; break;
case value_t::boolean: case value_t::boolean:
if ((n.flags & ~(node_flags::is_true | node_flags::is_new)) != 0 || n.extra != 0 || n.off > text_size) if ((n.flags & ~node_flags::is_true) != 0 || n.extra != 0 || n.off > text_size)
{ {
return false; return false;
} }
@@ -619,7 +613,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
case value_t::number_integer: case value_t::number_integer:
case value_t::number_unsigned: case value_t::number_unsigned:
case value_t::number_float: case value_t::number_float:
if ((n.flags & ~node_flags::is_new) != 0 || !number_in_bounds(n)) if (n.flags != 0 || !number_in_bounds(n))
{ {
return false; return false;
} }
@@ -641,7 +635,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
case value_t::object: case value_t::object:
{ {
const std::size_t limit = stack.empty() ? count : stack.back().end; const std::size_t limit = stack.empty() ? count : stack.back().end;
if ((n.flags & ~node_flags::is_new) != 0 || n.extra != 0 || n.next == 0 || n.next > limit - i || n.off > text_size) if (n.flags != 0 || n.extra != 0 || n.next == 0 || n.next > limit - i || n.off > text_size)
{ {
return false; return false;
} }
+4 -225
View File
@@ -26,176 +26,6 @@ namespace detail
namespace view namespace view
{ {
#if JSON_DIAGNOSTIC_POSITIONS
/*!
@brief the source positions of the values materialize() replays
Stands in for the lexer of json_sax_dom_parser, which reads the positions of
the values it stores from these three functions (diagnostic_positions). npos
is a value without a source position.
*/
struct position_source
{
/// the token, of which diagnostic_positions only reads the length
struct token
{
std::size_t length;
std::size_t size() const noexcept
{
return length;
}
};
std::size_t start = std::string::npos; ///< the first byte of the next value
std::size_t end = std::string::npos; ///< one past the next value (start_object/start_array: one past the bracket)
std::size_t get_chars_read_total() const noexcept
{
return end;
}
std::size_t get_token_start_position() const noexcept
{
return start;
}
token get_string() const noexcept
{
return token{end - start};
}
/// the range [first, last), or none if either is npos
void set(std::size_t first, std::size_t last) noexcept
{
const bool known = first != std::string::npos && last != std::string::npos;
start = known ? first : std::string::npos;
end = known ? last : std::string::npos;
}
/// one past the closing quote of the string whose opening quote is at
/// src[quote], or npos (the scan is bounded by size, as images can be
/// loaded without a check)
static std::size_t string_end(const char* src, std::size_t size, std::size_t quote) noexcept
{
for (std::size_t i = quote + 1; i < size; ++i)
{
if (src[i] == '\\')
{
++i;
}
else if (src[i] == '"')
{
return i + 1;
}
}
return std::string::npos;
}
/// one past the closing bracket of the array or object whose last element
/// ends at src[pos] (or whose opening bracket is at src[pos - 1]), or npos;
/// in between, there can only be whitespace, comments, and a trailing comma
/// (the source was validated when it was parsed)
static std::size_t container_end(const char* src, std::size_t size, std::size_t pos) noexcept
{
while (pos < size)
{
switch (src[pos])
{
case ' ':
case '\t':
case '\n':
case '\r':
case ',':
++pos;
break;
case ']':
case '}':
return pos + 1;
case '/':
if (pos + 1 < size && src[pos + 1] == '/')
{
pos += 2;
while (pos < size && src[pos] != '\n' && src[pos] != '\r')
{
++pos;
}
break;
}
if (pos + 1 < size && src[pos + 1] == '*')
{
pos += 2;
while (pos < size && !(src[pos] == '*' && pos + 1 < size && src[pos + 1] == '/'))
{
++pos;
}
pos += 2;
break;
}
return std::string::npos;
default:
return std::string::npos;
}
}
return std::string::npos;
}
/// the range of the scalar n of document d
void set_scalar(const document_data& d, const node& n) noexcept
{
const std::size_t size = d.size;
const std::size_t off = n.off;
std::size_t first = std::string::npos;
std::size_t last = std::string::npos;
if ((n.flags & node_flags::is_new) == 0)
{
switch (static_cast<value_t>(n.kind))
{
case value_t::string:
if ((n.flags & node_flags::storage) == 0)
{
// off is the first byte after the opening quote
if (off != 0)
{
first = off - 1;
last = off + std::size_t{n.len} + 1;
}
}
else if ((n.flags & node_flags::storage) == node_flags::escaped && n.next != 0)
{
// next is the offset of the opening quote plus one
first = n.next - 1;
last = string_end(d.base[0], size, first);
}
break;
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
if ((n.flags & node_flags::storage) == 0)
{
first = off;
last = off + number_length(n);
}
break;
case value_t::boolean:
first = off;
last = off + ((n.flags & node_flags::is_true) != 0 ? 4u : 5u);
break;
case value_t::null:
first = off;
last = off + 4u;
break;
case value_t::object:
case value_t::array:
case value_t::binary:
case value_t::discarded:
default:
break;
}
}
// (a crafted image can point past the text)
set(first, last != std::string::npos && last <= size ? last : std::string::npos);
}
};
#endif
/*! /*!
@brief the basic_json value of the subtree at n @brief the basic_json value of the subtree at n
@@ -203,22 +33,14 @@ The subtree is replayed into the SAX handler that parse() uses to build its
values, so the result is the value parse() would produce: duplicate keys keep values, so the result is the value parse() would produce: duplicate keys keep
the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is
iterative, so the nesting depth is limited by memory only, as for parse(). iterative, so the nesting depth is limited by memory only, as for parse().
Without a lexer the handler records no source positions
With JSON_DIAGNOSTIC_POSITIONS, the values get their source positions as with (JSON_DIAGNOSTIC_POSITIONS).
parse(): position_source stands in for the lexer. Values written by edits have
none (npos), and neither has an array or object whose elements changed, or
whose last element has none (the end of its closing bracket is found by
scanning from the end of the last element).
*/ */
template<typename BasicJsonType, bool Editable> template<typename BasicJsonType, bool Editable>
BasicJsonType materialize(const document_data& d, const node* n) BasicJsonType materialize(const document_data& d, const node* n)
{ {
using string_t = typename BasicJsonType::string_t; using string_t = typename BasicJsonType::string_t;
#if JSON_DIAGNOSTIC_POSITIONS
using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true, position_source>;
#else
using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true>; using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true>;
#endif
using nav = navigation<Editable>; using nav = navigation<Editable>;
// the open containers: their next element (or key of the next member), // the open containers: their next element (or key of the next member),
@@ -230,15 +52,9 @@ BasicJsonType materialize(const document_data& d, const node* n)
const node* pos; const node* pos;
const node* end; const node* end;
bool object; bool object;
#if JSON_DIAGNOSTIC_POSITIONS
std::size_t start; ///< the opening bracket (npos: unknown)
std::size_t last_end; ///< the end of the last element read (npos: unknown)
#endif
}; };
#if !JSON_DIAGNOSTIC_POSITIONS
// a scalar needs neither the handler nor the stack of open containers // a scalar needs neither the handler nor the stack of open containers
// (with positions, it goes through the handler, which sets them)
switch (static_cast<value_t>(n->kind)) switch (static_cast<value_t>(n->kind))
{ {
case value_t::string: case value_t::string:
@@ -260,16 +76,10 @@ BasicJsonType materialize(const document_data& d, const node* n)
default: default:
break; break;
} }
#endif
BasicJsonType result; BasicJsonType result;
#if JSON_DIAGNOSTIC_POSITIONS
position_source positions;
sax_t sax(result, true, &positions);
#else
sax_t sax(result, true); sax_t sax(result, true);
#endif const string_t no_token{};
const string_t no_token {};
// (shallow subtrees on the stack: a call for a small value then allocates only for the value) // (shallow subtrees on the stack: a call for a small value then allocates only for the value)
std::array<frame, 16> shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open std::array<frame, 16> shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open
std::vector<frame> deep{}; std::vector<frame> deep{};
@@ -284,16 +94,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
std::size_t depth = 0; std::size_t depth = 0;
for (;;) for (;;)
{ {
#if JSON_DIAGNOSTIC_POSITIONS
if (!is_container(*n))
{
positions.set_scalar(d, *n);
if (depth != 0)
{
open[depth - 1].last_end = positions.end;
}
}
#endif
// false positive: n comes from nav::value(), which never returns null for a valid index // false positive: n comes from nav::value(), which never returns null for a valid index
// @infer-ignore NULLPTR_DEREFERENCE // @infer-ignore NULLPTR_DEREFERENCE
switch (static_cast<value_t>(n->kind)) switch (static_cast<value_t>(n->kind))
@@ -302,12 +102,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
case value_t::array: case value_t::array:
{ {
const bool object = n->kind == node_kind::object; const bool object = n->kind == node_kind::object;
#if JSON_DIAGNOSTIC_POSITIONS
// (moved containers are also new)
const std::size_t start = (n->flags & node_flags::is_new) == 0 && n->off < d.size ? std::size_t{n->off} :
std::string::npos;
positions.set(start, start + 1);
#endif
if (object) if (object)
{ {
sax.start_object(n->len); sax.start_object(n->len);
@@ -324,14 +118,8 @@ BasicJsonType materialize(const document_data& d, const node* n)
open = deep.data(); open = deep.data();
capacity = deep.size(); capacity = deep.size();
} }
#if JSON_DIAGNOSTIC_POSITIONS open[depth++] = frame{nav::first(d, n), nav::end(d, n), object};
// (until an element is read, the container's own bracket is the last thing read)
open[depth++] = frame{nav::first(d, n), nav::end(d, n), object, start, positions.end};
break; break;
#else
open[depth++] = frame {nav::first(d, n), nav::end(d, n), object};
break;
#endif
} }
case value_t::string: case value_t::string:
{ {
@@ -371,15 +159,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
frame& f = open[depth - 1]; frame& f = open[depth - 1];
if (f.pos == f.end) if (f.pos == f.end)
{ {
#if JSON_DIAGNOSTIC_POSITIONS
const std::size_t end = f.start == std::string::npos || f.last_end == std::string::npos ? std::string::npos : position_source::container_end(d.base[0], d.size, f.last_end);
// (an end of npos clears the start, see diagnostic_positions)
positions.set(end == std::string::npos ? end : end - 1, end);
if (depth > 1)
{
open[depth - 2].last_end = end;
}
#endif
if (f.object) if (f.object)
{ {
sax.end_object(); sax.end_object();
+1 -1
View File
@@ -69,7 +69,7 @@ struct node
std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0 std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0
std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved
std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count
std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence); escaped string: source offset of the opening quote plus one (JSON_DIAGNOSTIC_POSITIONS; 0: unknown) std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence)
}; };
static_assert(sizeof(node) == 16, "node must stay 16 bytes"); static_assert(sizeof(node) == 16, "node must stay 16 bytes");
+19 -1
View File
@@ -159,7 +159,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
friend class ::nlohmann::detail::binary_writer; friend class ::nlohmann::detail::binary_writer;
template<typename BasicJsonType, typename InputType, typename SAX> template<typename BasicJsonType, typename InputType, typename SAX>
friend class ::nlohmann::detail::binary_reader; friend class ::nlohmann::detail::binary_reader;
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType> template<typename BasicJsonType, typename InputAdapterType, bool Collect>
friend class ::nlohmann::detail::json_sax_dom_parser; friend class ::nlohmann::detail::json_sax_dom_parser;
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType>
friend class ::nlohmann::detail::json_sax_dom_binary_parser; friend class ::nlohmann::detail::json_sax_dom_binary_parser;
@@ -6950,6 +6950,24 @@ public:
/// @sa https://json.nlohmann.me/api/basic_json/patch/ /// @sa https://json.nlohmann.me/api/basic_json/patch/
void patch_inplace(const basic_json& json_patch) void patch_inplace(const basic_json& json_patch)
{ {
// the operations below change *this while iterating json_patch: apply
// a copy when a value is applied as patch to itself or, with
// JSON_DIAGNOSTICS, to a value stored inside it
bool patch_aliases_this = (&json_patch == this);
#if JSON_DIAGNOSTICS
for (const basic_json* p = json_patch.m_parent; !patch_aliases_this && p != nullptr; p = p->m_parent)
{
patch_aliases_this = (p == this);
}
#endif
if (JSON_HEDLEY_UNLIKELY(patch_aliases_this))
{
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): *this is modified below
const basic_json json_patch_copy = json_patch;
patch_inplace(json_patch_copy);
return;
}
basic_json& result = *this; basic_json& result = *this;
// the valid JSON Patch operations // the valid JSON Patch operations
enum class patch_operations {add, remove, replace, move, copy, test, invalid}; enum class patch_operations {add, remove, replace, move, copy, test, invalid};
+31 -25
View File
@@ -15629,10 +15629,7 @@ struct diagnostic_positions
{ {
// Lexer has read the first character of the container, so // Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position. // subtract 1 from the position to get the correct start position.
// (npos: a value without a source position, see materialize() of v.start_position = lexer->get_chars_read_total() - 1;
// json_view)
const std::size_t read = lexer->get_chars_read_total();
v.start_position = read == std::string::npos ? read : read - 1;
} }
} }
@@ -15647,12 +15644,6 @@ struct diagnostic_positions
{ {
// Lexer's position is past the closing brace or bracket, so set that as the end position. // Lexer's position is past the closing brace or bracket, so set that as the end position.
v.end_position = lexer->get_chars_read_total(); v.end_position = lexer->get_chars_read_total();
if (v.end_position == std::string::npos)
{
// a range has both ends or none (exception::diagnostics()
// prints only complete ones)
v.start_position = v.end_position;
}
} }
} }
@@ -15669,12 +15660,6 @@ struct diagnostic_positions
// The start position will be set below based on the length of the string representation // The start position will be set below based on the length of the string representation
// of the value. // of the value.
v.end_position = lexer->get_chars_read_total(); v.end_position = lexer->get_chars_read_total();
if (v.end_position == std::string::npos)
{
// a value without a source position
v.start_position = v.end_position;
return;
}
switch (v.type()) switch (v.type())
{ {
@@ -15887,12 +15872,8 @@ read up to it.
@tparam Collect whether to collect the values of containers of unknown size on @tparam Collect whether to collect the values of containers of unknown size on
a stack (only for a caller that does not use the value after a a stack (only for a caller that does not use the value after a
parse error) parse error)
@tparam LexerType the type of the lexer that reports the source positions
(JSON_DIAGNOSTIC_POSITIONS); only diagnostic_positions uses
it, so any type with its three functions works (as
materialize() of json_view uses it)
*/ */
template<typename BasicJsonType, typename InputAdapterType, bool Collect = false, typename LexerType = lexer<BasicJsonType, InputAdapterType>> template<typename BasicJsonType, typename InputAdapterType, bool Collect = false>
class json_sax_dom_parser : public json_sax_dom_error_state class json_sax_dom_parser : public json_sax_dom_error_state
{ {
public: public:
@@ -15901,7 +15882,7 @@ class json_sax_dom_parser : public json_sax_dom_error_state
using number_float_t = typename BasicJsonType::number_float_t; using number_float_t = typename BasicJsonType::number_float_t;
using string_t = typename BasicJsonType::string_t; using string_t = typename BasicJsonType::string_t;
using binary_t = typename BasicJsonType::binary_t; using binary_t = typename BasicJsonType::binary_t;
using lexer_t = LexerType; using lexer_t = lexer<BasicJsonType, InputAdapterType>;
/*! /*!
@param[in,out] r reference to a JSON value that is manipulated while @param[in,out] r reference to a JSON value that is manipulated while
@@ -22534,8 +22515,8 @@ using parser_callback_t =
template<typename SAX> template<typename SAX>
struct sax_ignores_number_text : std::false_type {}; struct sax_ignores_number_text : std::false_type {};
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType> template<typename BasicJsonType, typename InputAdapterType, bool Collect>
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect, LexerType>> : std::true_type {}; struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect>> : std::true_type {};
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType>
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {}; struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
@@ -24326,6 +24307,13 @@ class json_pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/ /// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/
json_pointer& operator/=(const json_pointer& ptr) json_pointer& operator/=(const json_pointer& ptr)
{ {
// insert() must not be given iterators into the vector it inserts
// into: append a copy when a pointer is appended to itself
if (JSON_HEDLEY_UNLIKELY(&ptr == this))
{
return *this /= json_pointer(ptr);
}
reference_tokens.insert(reference_tokens.end(), reference_tokens.insert(reference_tokens.end(),
ptr.reference_tokens.begin(), ptr.reference_tokens.begin(),
ptr.reference_tokens.end()); ptr.reference_tokens.end());
@@ -36473,7 +36461,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
friend class ::nlohmann::detail::binary_writer; friend class ::nlohmann::detail::binary_writer;
template<typename BasicJsonType, typename InputType, typename SAX> template<typename BasicJsonType, typename InputType, typename SAX>
friend class ::nlohmann::detail::binary_reader; friend class ::nlohmann::detail::binary_reader;
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType> template<typename BasicJsonType, typename InputAdapterType, bool Collect>
friend class ::nlohmann::detail::json_sax_dom_parser; friend class ::nlohmann::detail::json_sax_dom_parser;
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType>
friend class ::nlohmann::detail::json_sax_dom_binary_parser; friend class ::nlohmann::detail::json_sax_dom_binary_parser;
@@ -43264,6 +43252,24 @@ public:
/// @sa https://json.nlohmann.me/api/basic_json/patch/ /// @sa https://json.nlohmann.me/api/basic_json/patch/
void patch_inplace(const basic_json& json_patch) void patch_inplace(const basic_json& json_patch)
{ {
// the operations below change *this while iterating json_patch: apply
// a copy when a value is applied as patch to itself or, with
// JSON_DIAGNOSTICS, to a value stored inside it
bool patch_aliases_this = (&json_patch == this);
#if JSON_DIAGNOSTICS
for (const basic_json* p = json_patch.m_parent; !patch_aliases_this && p != nullptr; p = p->m_parent)
{
patch_aliases_this = (p == this);
}
#endif
if (JSON_HEDLEY_UNLIKELY(patch_aliases_this))
{
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): *this is modified below
const basic_json json_patch_copy = json_patch;
patch_inplace(json_patch_copy);
return;
}
basic_json& result = *this; basic_json& result = *this;
// the valid JSON Patch operations // the valid JSON Patch operations
enum class patch_operations {add, remove, replace, move, copy, test, invalid}; enum class patch_operations {add, remove, replace, move, copy, test, invalid};
+17 -250
View File
@@ -269,7 +269,7 @@ struct node
std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0 std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0
std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved
std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count
std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence); escaped string: source offset of the opening quote plus one (JSON_DIAGNOSTIC_POSITIONS; 0: unknown) std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence)
}; };
static_assert(sizeof(node) == 16, "node must stay 16 bytes"); static_assert(sizeof(node) == 16, "node must stay 16 bytes");
@@ -2402,13 +2402,7 @@ indent_done:
return false; return false;
} }
p = r.p; p = r.p;
#if JSON_DIAGNOSTIC_POSITIONS
// the decoded string is in the arena: next keeps the source offset
// of the opening quote plus one (0: unknown) for materialize()
push_node(node_kind::string, node_flags::escaped, 0, r.start, static_cast<std::uint64_t>(r.len) | (static_cast<std::uint64_t>(s - b) << 32));
#else
push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len); push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len);
#endif
return true; return true;
} }
}; };
@@ -4949,8 +4943,7 @@ constexpr std::uint64_t image_limit = 0xFFFFFFF0u;
/// Copy the current structure of an edited document into nodes in document /// Copy the current structure of an edited document into nodes in document
/// order, as the parser would have written them. Text written by edits is /// order, as the parser would have written them. Text written by edits is
/// appended to text_tail (number tokens) and arena_tail (strings); floats that /// appended to text_tail (number tokens) and arena_tail (strings); floats that
/// are not finite become null, as dump() writes them. Nodes keep the flag /// are not finite become null, as dump() writes them.
/// is_new: they have no source position (JSON_DIAGNOSTIC_POSITIONS).
inline void compact_nodes(const document_data& d, std::size_t arena_size, std::vector<node>& out, std::string& text_tail, std::string& arena_tail) inline void compact_nodes(const document_data& d, std::size_t arena_size, std::vector<node>& out, std::string& text_tail, std::string& arena_tail)
{ {
struct frame struct frame
@@ -4973,18 +4966,16 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
arena_tail.append(d.str(s), s.len); arena_tail.append(d.str(s), s.len);
r.flags = node_flags::escaped; r.flags = node_flags::escaped;
} }
r.flags = static_cast<std::uint8_t>(r.flags | (s.flags & node_flags::is_new));
return r; return r;
}; };
const auto copy_node = [&](const node * v) const auto copy_node = [&](const node * v)
{ {
node r = *v; node r = *v;
const auto is_new = static_cast<std::uint8_t>(v->flags & node_flags::is_new);
switch (static_cast<value_t>(v->kind)) switch (static_cast<value_t>(v->kind))
{ {
case value_t::object: case value_t::object:
case value_t::array: case value_t::array:
r.flags = is_new; r.flags = 0;
r.extra = 0; r.extra = 0;
r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off; r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off;
r.len = 0; // counted below r.len = 0; // counted below
@@ -5001,7 +4992,7 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
r.off = static_cast<std::uint32_t>(d.size + text_tail.size()); r.off = static_cast<std::uint32_t>(d.size + text_tail.size());
text_tail.append(d.str(*v), number_length(*v)); text_tail.append(d.str(*v), number_length(*v));
} }
r.flags = is_new; r.flags = 0;
break; break;
case value_t::number_float: case value_t::number_float:
if ((v->flags & node_flags::storage) == node_flags::edited) if ((v->flags & node_flags::storage) == node_flags::edited)
@@ -5011,23 +5002,22 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
{ {
r = node{}; // nan and infinity: null, as dump() writes them r = node{}; // nan and infinity: null, as dump() writes them
r.kind = node_kind::null; r.kind = node_kind::null;
r.flags = is_new;
break; break;
} }
r.off = static_cast<std::uint32_t>(d.size + text_tail.size()); r.off = static_cast<std::uint32_t>(d.size + text_tail.size());
text_tail.append(t, v->len); text_tail.append(t, v->len);
r.extra = 0xFFFFu; // the digit layout is not recorded r.extra = 0xFFFFu; // the digit layout is not recorded
} }
r.flags = is_new; r.flags = 0;
break; break;
case value_t::boolean: case value_t::boolean:
r.flags = static_cast<std::uint8_t>(v->flags & (node_flags::is_true | node_flags::is_new)); r.flags = static_cast<std::uint8_t>(v->flags & node_flags::is_true);
break; break;
case value_t::null: case value_t::null:
case value_t::binary: case value_t::binary:
case value_t::discarded: case value_t::discarded:
default: default:
r.flags = is_new; r.flags = 0;
break; break;
} }
out.push_back(r); out.push_back(r);
@@ -5388,15 +5378,13 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
std::vector<float_range> floats; std::vector<float_range> floats;
const auto check_string = [&](const node & n) -> bool const auto check_string = [&](const node & n) -> bool
{ {
if ((n.flags & ~(node_flags::escaped | node_flags::is_new)) != 0 || n.extra != 0) if ((n.flags & ~node_flags::escaped) != 0 || n.extra != 0)
{ {
return false; return false;
} }
const bool decoded = (n.flags & node_flags::escaped) != 0; const bool decoded = (n.flags & node_flags::escaped) != 0;
const std::size_t limit = decoded ? arena_size : text_size; const std::size_t limit = decoded ? arena_size : text_size;
// (next: the source offset of a decoded string's opening quote plus if (n.off > limit || n.len > limit - n.off)
// one, or 0; see node)
if (n.off > limit || n.len > limit - n.off || n.next > text_size)
{ {
return false; return false;
} }
@@ -5468,13 +5456,13 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
{ {
case value_t::null: case value_t::null:
// (the offset of a literal is read to size the output of dump()) // (the offset of a literal is read to size the output of dump())
if ((n.flags & ~node_flags::is_new) != 0 || n.extra != 0 || n.off > text_size) if (n.flags != 0 || n.extra != 0 || n.off > text_size)
{ {
return false; return false;
} }
break; break;
case value_t::boolean: case value_t::boolean:
if ((n.flags & ~(node_flags::is_true | node_flags::is_new)) != 0 || n.extra != 0 || n.off > text_size) if ((n.flags & ~node_flags::is_true) != 0 || n.extra != 0 || n.off > text_size)
{ {
return false; return false;
} }
@@ -5488,7 +5476,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
case value_t::number_integer: case value_t::number_integer:
case value_t::number_unsigned: case value_t::number_unsigned:
case value_t::number_float: case value_t::number_float:
if ((n.flags & ~node_flags::is_new) != 0 || !number_in_bounds(n)) if (n.flags != 0 || !number_in_bounds(n))
{ {
return false; return false;
} }
@@ -5510,7 +5498,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
case value_t::object: case value_t::object:
{ {
const std::size_t limit = stack.empty() ? count : stack.back().end; const std::size_t limit = stack.empty() ? count : stack.back().end;
if ((n.flags & ~node_flags::is_new) != 0 || n.extra != 0 || n.next == 0 || n.next > limit - i || n.off > text_size) if (n.flags != 0 || n.extra != 0 || n.next == 0 || n.next > limit - i || n.off > text_size)
{ {
return false; return false;
} }
@@ -6120,176 +6108,6 @@ namespace detail
namespace view namespace view
{ {
#if JSON_DIAGNOSTIC_POSITIONS
/*!
@brief the source positions of the values materialize() replays
Stands in for the lexer of json_sax_dom_parser, which reads the positions of
the values it stores from these three functions (diagnostic_positions). npos
is a value without a source position.
*/
struct position_source
{
/// the token, of which diagnostic_positions only reads the length
struct token
{
std::size_t length;
std::size_t size() const noexcept
{
return length;
}
};
std::size_t start = std::string::npos; ///< the first byte of the next value
std::size_t end = std::string::npos; ///< one past the next value (start_object/start_array: one past the bracket)
std::size_t get_chars_read_total() const noexcept
{
return end;
}
std::size_t get_token_start_position() const noexcept
{
return start;
}
token get_string() const noexcept
{
return token{end - start};
}
/// the range [first, last), or none if either is npos
void set(std::size_t first, std::size_t last) noexcept
{
const bool known = first != std::string::npos && last != std::string::npos;
start = known ? first : std::string::npos;
end = known ? last : std::string::npos;
}
/// one past the closing quote of the string whose opening quote is at
/// src[quote], or npos (the scan is bounded by size, as images can be
/// loaded without a check)
static std::size_t string_end(const char* src, std::size_t size, std::size_t quote) noexcept
{
for (std::size_t i = quote + 1; i < size; ++i)
{
if (src[i] == '\\')
{
++i;
}
else if (src[i] == '"')
{
return i + 1;
}
}
return std::string::npos;
}
/// one past the closing bracket of the array or object whose last element
/// ends at src[pos] (or whose opening bracket is at src[pos - 1]), or npos;
/// in between, there can only be whitespace, comments, and a trailing comma
/// (the source was validated when it was parsed)
static std::size_t container_end(const char* src, std::size_t size, std::size_t pos) noexcept
{
while (pos < size)
{
switch (src[pos])
{
case ' ':
case '\t':
case '\n':
case '\r':
case ',':
++pos;
break;
case ']':
case '}':
return pos + 1;
case '/':
if (pos + 1 < size && src[pos + 1] == '/')
{
pos += 2;
while (pos < size && src[pos] != '\n' && src[pos] != '\r')
{
++pos;
}
break;
}
if (pos + 1 < size && src[pos + 1] == '*')
{
pos += 2;
while (pos < size && !(src[pos] == '*' && pos + 1 < size && src[pos + 1] == '/'))
{
++pos;
}
pos += 2;
break;
}
return std::string::npos;
default:
return std::string::npos;
}
}
return std::string::npos;
}
/// the range of the scalar n of document d
void set_scalar(const document_data& d, const node& n) noexcept
{
const std::size_t size = d.size;
const std::size_t off = n.off;
std::size_t first = std::string::npos;
std::size_t last = std::string::npos;
if ((n.flags & node_flags::is_new) == 0)
{
switch (static_cast<value_t>(n.kind))
{
case value_t::string:
if ((n.flags & node_flags::storage) == 0)
{
// off is the first byte after the opening quote
if (off != 0)
{
first = off - 1;
last = off + std::size_t{n.len} + 1;
}
}
else if ((n.flags & node_flags::storage) == node_flags::escaped && n.next != 0)
{
// next is the offset of the opening quote plus one
first = n.next - 1;
last = string_end(d.base[0], size, first);
}
break;
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
if ((n.flags & node_flags::storage) == 0)
{
first = off;
last = off + number_length(n);
}
break;
case value_t::boolean:
first = off;
last = off + ((n.flags & node_flags::is_true) != 0 ? 4u : 5u);
break;
case value_t::null:
first = off;
last = off + 4u;
break;
case value_t::object:
case value_t::array:
case value_t::binary:
case value_t::discarded:
default:
break;
}
}
// (a crafted image can point past the text)
set(first, last != std::string::npos && last <= size ? last : std::string::npos);
}
};
#endif
/*! /*!
@brief the basic_json value of the subtree at n @brief the basic_json value of the subtree at n
@@ -6297,22 +6115,14 @@ The subtree is replayed into the SAX handler that parse() uses to build its
values, so the result is the value parse() would produce: duplicate keys keep values, so the result is the value parse() would produce: duplicate keys keep
the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is
iterative, so the nesting depth is limited by memory only, as for parse(). iterative, so the nesting depth is limited by memory only, as for parse().
Without a lexer the handler records no source positions
With JSON_DIAGNOSTIC_POSITIONS, the values get their source positions as with (JSON_DIAGNOSTIC_POSITIONS).
parse(): position_source stands in for the lexer. Values written by edits have
none (npos), and neither has an array or object whose elements changed, or
whose last element has none (the end of its closing bracket is found by
scanning from the end of the last element).
*/ */
template<typename BasicJsonType, bool Editable> template<typename BasicJsonType, bool Editable>
BasicJsonType materialize(const document_data& d, const node* n) BasicJsonType materialize(const document_data& d, const node* n)
{ {
using string_t = typename BasicJsonType::string_t; using string_t = typename BasicJsonType::string_t;
#if JSON_DIAGNOSTIC_POSITIONS
using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true, position_source>;
#else
using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true>; using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true>;
#endif
using nav = navigation<Editable>; using nav = navigation<Editable>;
// the open containers: their next element (or key of the next member), // the open containers: their next element (or key of the next member),
@@ -6324,15 +6134,9 @@ BasicJsonType materialize(const document_data& d, const node* n)
const node* pos; const node* pos;
const node* end; const node* end;
bool object; bool object;
#if JSON_DIAGNOSTIC_POSITIONS
std::size_t start; ///< the opening bracket (npos: unknown)
std::size_t last_end; ///< the end of the last element read (npos: unknown)
#endif
}; };
#if !JSON_DIAGNOSTIC_POSITIONS
// a scalar needs neither the handler nor the stack of open containers // a scalar needs neither the handler nor the stack of open containers
// (with positions, it goes through the handler, which sets them)
switch (static_cast<value_t>(n->kind)) switch (static_cast<value_t>(n->kind))
{ {
case value_t::string: case value_t::string:
@@ -6354,16 +6158,10 @@ BasicJsonType materialize(const document_data& d, const node* n)
default: default:
break; break;
} }
#endif
BasicJsonType result; BasicJsonType result;
#if JSON_DIAGNOSTIC_POSITIONS
position_source positions;
sax_t sax(result, true, &positions);
#else
sax_t sax(result, true); sax_t sax(result, true);
#endif const string_t no_token{};
const string_t no_token {};
// (shallow subtrees on the stack: a call for a small value then allocates only for the value) // (shallow subtrees on the stack: a call for a small value then allocates only for the value)
std::array<frame, 16> shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open std::array<frame, 16> shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open
std::vector<frame> deep{}; std::vector<frame> deep{};
@@ -6378,16 +6176,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
std::size_t depth = 0; std::size_t depth = 0;
for (;;) for (;;)
{ {
#if JSON_DIAGNOSTIC_POSITIONS
if (!is_container(*n))
{
positions.set_scalar(d, *n);
if (depth != 0)
{
open[depth - 1].last_end = positions.end;
}
}
#endif
// false positive: n comes from nav::value(), which never returns null for a valid index // false positive: n comes from nav::value(), which never returns null for a valid index
// @infer-ignore NULLPTR_DEREFERENCE // @infer-ignore NULLPTR_DEREFERENCE
switch (static_cast<value_t>(n->kind)) switch (static_cast<value_t>(n->kind))
@@ -6396,12 +6184,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
case value_t::array: case value_t::array:
{ {
const bool object = n->kind == node_kind::object; const bool object = n->kind == node_kind::object;
#if JSON_DIAGNOSTIC_POSITIONS
// (moved containers are also new)
const std::size_t start = (n->flags & node_flags::is_new) == 0 && n->off < d.size ? std::size_t{n->off} :
std::string::npos;
positions.set(start, start + 1);
#endif
if (object) if (object)
{ {
sax.start_object(n->len); sax.start_object(n->len);
@@ -6418,14 +6200,8 @@ BasicJsonType materialize(const document_data& d, const node* n)
open = deep.data(); open = deep.data();
capacity = deep.size(); capacity = deep.size();
} }
#if JSON_DIAGNOSTIC_POSITIONS open[depth++] = frame{nav::first(d, n), nav::end(d, n), object};
// (until an element is read, the container's own bracket is the last thing read)
open[depth++] = frame{nav::first(d, n), nav::end(d, n), object, start, positions.end};
break; break;
#else
open[depth++] = frame {nav::first(d, n), nav::end(d, n), object};
break;
#endif
} }
case value_t::string: case value_t::string:
{ {
@@ -6465,15 +6241,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
frame& f = open[depth - 1]; frame& f = open[depth - 1];
if (f.pos == f.end) if (f.pos == f.end)
{ {
#if JSON_DIAGNOSTIC_POSITIONS
const std::size_t end = f.start == std::string::npos || f.last_end == std::string::npos ? std::string::npos : position_source::container_end(d.base[0], d.size, f.last_end);
// (an end of npos clears the start, see diagnostic_positions)
positions.set(end == std::string::npos ? end : end - 1, end);
if (depth > 1)
{
open[depth - 2].last_end = end;
}
#endif
if (f.object) if (f.object)
{ {
sax.end_object(); sax.end_object();
+123
View File
@@ -826,6 +826,129 @@ TEST_CASE("JSON patch")
CHECK(doc.at("foo") == "bar"); CHECK(doc.at("foo") == "bar");
} }
#endif // !defined(JSON_NOEXCEPTION) #endif // !defined(JSON_NOEXCEPTION)
SECTION("applying a value as patch to itself")
{
// a JSON patch is an array, so an array of operations can be
// applied to itself; the operations are read from the patch as
// it was before the first operation, as patch() does it
const json copy_op = {{"op", "copy"}, {"from", "/0"}, {"path", "/-"}};
json copy_ops = json::array();
for (int i = 0; i < 5; ++i)
{
copy_ops.push_back(copy_op);
}
json expected_copies = copy_ops;
for (int i = 0; i < 5; ++i)
{
expected_copies.push_back(copy_op);
}
SECTION("copy operations without spare capacity")
{
// a copy has exactly as much capacity as elements
json doc = copy_ops;
CHECK(doc.patch(doc) == expected_copies);
doc.patch_inplace(doc);
CHECK(doc == expected_copies);
}
#if JSON_DIAGNOSTICS
SECTION("patch stored inside the patched value")
{
// the parent pointers of JSON_DIAGNOSTICS make the aliasing detectable
const json op = {{"op", "replace"}, {"path", "/patch"}, {"value", 1}};
json doc = {{"a", 1}, {"patch", json::array({op})}};
const json expected = doc.patch(doc["patch"]);
doc.patch_inplace(doc["patch"]);
CHECK(doc == expected);
}
#endif
SECTION("copy operations with spare capacity")
{
// reserve while the array is empty: reallocating non-empty
// elements behind the library's back leaves stale parent
// pointers with JSON_DIAGNOSTICS
json doc = json::array();
doc.get_ref<json::array_t&>().reserve(100);
for (int i = 0; i < 5; ++i)
{
doc.push_back(copy_op);
}
CHECK(doc.patch(doc) == expected_copies);
doc.patch_inplace(doc);
CHECK(doc == expected_copies);
}
SECTION("remove operations")
{
json doc = R"([
{"op": "remove", "path": "/0"},
{"op": "remove", "path": "/0"},
{"op": "remove", "path": "/0"}
])"_json;
CHECK(doc.patch(doc) == json::array());
doc.patch_inplace(doc);
CHECK(doc == json::array());
}
SECTION("operations that read a changed element")
{
// the second operation is still a "test" operation, although
// the first one replaced it in the document
json doc = R"([
{"op": "replace", "path": "/1", "value": 1},
{"op": "test", "path": "/1", "value": 1},
{"op": "add", "path": "/-", "value": 2}
])"_json;
const json expected = R"([
{"op": "replace", "path": "/1", "value": 1},
1,
{"op": "add", "path": "/-", "value": 2},
2
])"_json;
CHECK(doc.patch(doc) == expected);
doc.patch_inplace(doc);
CHECK(doc == expected);
}
SECTION("replacing the root")
{
json doc = R"([
{"op": "add", "path": "", "value": [1, 2]},
{"op": "add", "path": "/-", "value": 3},
{"op": "replace", "path": "", "value": {"a": 4}}
])"_json;
const json expected = {{"a", 4}};
CHECK(doc.patch(doc) == expected);
doc.patch_inplace(doc);
CHECK(doc == expected);
}
#if !defined(JSON_NOEXCEPTION)
SECTION("failing operation")
{
// the operations before the failing one remain applied
json doc = R"([
{"op": "add", "path": "/-", "value": 1},
{"op": "test", "path": "/0/op", "value": "remove"}
])"_json;
CHECK_THROWS_AS(doc.patch(doc), json::other_error&);
CHECK_THROWS_AS(doc.patch_inplace(doc), json::other_error&);
CHECK(doc.size() == 3);
CHECK(doc.back() == 1);
}
#endif // !defined(JSON_NOEXCEPTION)
}
} }
SECTION("errors") SECTION("errors")
+38
View File
@@ -845,6 +845,44 @@ TEST_CASE("JSON pointers")
CHECK(ptr.to_string() == "/object/~1"); CHECK(ptr.to_string() == "/object/~1");
} }
SECTION("appending a pointer to itself")
{
// operator/= must not pass iterators into its own token vector to
// insert(); each pointer is appended to itself and must yield the
// pointer whose string representation is the original one twice
const std::vector<std::string> pointers =
{
"",
"/",
"/a",
"/a/b",
"/foo/0/-",
"/a~1b/~0c/~01",
"/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17"
};
for (const auto& s : pointers)
{
CAPTURE(s)
json::json_pointer ptr(s);
const json::json_pointer expected(s + s);
json::json_pointer& ret = (ptr /= ptr);
CHECK(&ret == &ptr);
CHECK(ptr == expected);
CHECK(ptr.to_string() == s + s);
// appending to itself again doubles the pointer once more
ptr /= ptr;
CHECK(ptr.to_string() == s + s + s + s);
// operator/ appends to a copy and leaves the operand unchanged
const json::json_pointer original(s);
CHECK((original / original) == expected);
CHECK(original.to_string() == s);
}
}
SECTION("equality comparison") SECTION("equality comparison")
{ {
std::string ptr_string{"/foo/bar"}; std::string ptr_string{"/foo/bar"};
+9 -23
View File
@@ -749,20 +749,6 @@ std::string exception_of(F f)
} }
return ""; return "";
} }
// the exception a call on a basic_json value throws, without the byte range of
// the value (JSON_DIAGNOSTIC_POSITIONS), which the exceptions of views lack
template<typename F>
std::string json_exception_of(F f)
{
std::string what = exception_of(f);
const std::size_t at = what.find("] (bytes ");
if (at != std::string::npos)
{
what.erase(at + 2, what.find(") ", at) - at);
}
return what;
}
#endif #endif
// compares a view with the ordered_json value materialize() gives for it: // compares a view with the ordered_json value materialize() gives for it:
@@ -971,24 +957,24 @@ TEST_CASE("json_view element access and iteration")
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
if (!j.is_object()) if (!j.is_object())
{ {
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == json_exception_of([&] { static_cast<void>(j["a"]); })); CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == exception_of([&] { static_cast<void>(j["a"]); }));
} }
if (!j.is_array()) if (!j.is_array())
{ {
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == json_exception_of([&] { static_cast<void>(j[0]); })); CHECK(exception_of([&] { static_cast<void>(v[0]); }) == exception_of([&] { static_cast<void>(j[0]); }));
} }
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == json_exception_of([&] { static_cast<void>(j.at("a")); })); CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == exception_of([&] { static_cast<void>(j.at("a")); }));
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == json_exception_of([&] { static_cast<void>(j.at("missing")); })); CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == exception_of([&] { static_cast<void>(j.at("missing")); }));
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == json_exception_of([&] { static_cast<void>(j.at(0)); })); CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == exception_of([&] { static_cast<void>(j.at(0)); }));
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == json_exception_of([&] { static_cast<void>(j.at(5)); })); CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == exception_of([&] { static_cast<void>(j.at(5)); }));
if (!(j.is_object() && j.empty())) // (key() of an end iterator) if (!(j.is_object() && j.empty())) // (key() of an end iterator)
{ {
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == json_exception_of([&] { static_cast<void>(j.begin().key()); })); CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == exception_of([&] { static_cast<void>(j.begin().key()); }));
} }
if (!j.empty() || j.is_null()) if (!j.empty() || j.is_null())
{ {
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == json_exception_of([&] { static_cast<void>(j.front()); })); CHECK(exception_of([&] { static_cast<void>(v.front()); }) == exception_of([&] { static_cast<void>(j.front()); }));
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == json_exception_of([&] { static_cast<void>(j.back()); })); CHECK(exception_of([&] { static_cast<void>(v.back()); }) == exception_of([&] { static_cast<void>(j.back()); }));
} }
#endif #endif
CHECK(v.contains("a") == j.contains("a")); CHECK(v.contains("a") == j.contains("a"));
+4 -6
View File
@@ -326,8 +326,7 @@ TEST_CASE("json_view from_json")
CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3); CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3);
} }
// (the exceptions of views have neither the path nor the byte range of the value) #if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
SECTION("std types: the same errors as from the materialized value") SECTION("std types: the same errors as from the materialized value")
{ {
const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])"); const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])");
@@ -393,7 +392,7 @@ TEST_CASE("json_view from_json")
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
const json j = d.root().materialize(); const json j = d.root().materialize();
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\""); CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
#if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS #if !JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); })); CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
#endif #endif
#endif #endif
@@ -441,11 +440,10 @@ TEST_CASE("json_view from_json")
CHECK(o.root().get<shape>().center.y == 2); CHECK(o.root().get<shape>().center.y == 2);
} }
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS #if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS
SECTION("errors are those of the same conversion of a json value") SECTION("errors are those of the same conversion of a json value")
{ {
// (with JSON_DIAGNOSTICS, those of json name the path of the value, // (with JSON_DIAGNOSTICS, those of json name the path of the value)
// and with JSON_DIAGNOSTIC_POSITIONS its bytes)
const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])"); const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])");
const json j = d.root().materialize(); const json j = d.root().materialize();
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
-340
View File
@@ -1,340 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// source positions of materialize() (JSON_DIAGNOSTIC_POSITIONS, #5864)
#include "doctest_compatibility.h"
#ifndef JSON_DIAGNOSTICS
#define JSON_DIAGNOSTICS 1
#endif
#define JSON_DIAGNOSTIC_POSITIONS 1
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
#include <cstdint>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include "json_view_test_helpers.hpp"
#include <test_data.hpp>
namespace
{
constexpr std::size_t npos = std::string::npos;
// whether every value of a has the source positions of the value of b at the
// same place (a and b are equal; shallow test documents only)
template<typename BasicJsonType>
bool same_positions(const BasicJsonType& a, const BasicJsonType& b)
{
if (a.start_pos() != b.start_pos() || a.end_pos() != b.end_pos() || a.type() != b.type() || a.size() != b.size())
{
return false;
}
if (a.is_object())
{
for (auto it = a.begin(); it != a.end(); ++it)
{
if (!same_positions(it.value(), b.at(it.key())))
{
return false;
}
}
}
else if (a.is_array())
{
for (std::size_t i = 0; i < a.size(); ++i)
{
if (!same_positions(a[i], b[i]))
{
return false;
}
}
}
return true;
}
// the materialized root of text has the positions of parse()
template<typename BasicJsonType>
bool positions_as_parse(const std::string& text, bool comments = false, bool trailing_commas = false)
{
const auto j = BasicJsonType::parse(text, nullptr, true, comments, trailing_commas);
const auto d = nlohmann::basic_json_document<BasicJsonType>::parse(text, true, comments, trailing_commas);
const auto e = nlohmann::basic_json_document<BasicJsonType, nlohmann::view_mode::editable>::parse(text, true, comments, trailing_commas);
return same_positions(d.root().materialize(), j) && same_positions(e.root().materialize(), j);
}
std::string range(const json& j)
{
return j.start_pos() == npos && j.end_pos() == npos ? "none" : std::to_string(j.start_pos()) + "-" + std::to_string(j.end_pos());
}
} // namespace
TEST_CASE("json_view materialize() source positions")
{
SECTION("as parse()")
{
const std::vector<std::string> texts =
{
"null", "true", "false", "0", "-12", "3.25e-2", "18446744073709551615", "-9223372036854775809", R"("a")",
R"("\n")", R"("\u00e9")", R"("\ud83d\ude00")", "\"\xf0\x9f\x98\x80\"", R"("a\"b\\")", R"("x\\")",
" [ ] ", "{}", "[[[]]]", R"({"a":{"b":[1,{"c":[]}]}})",
R"({"\u0041": "v\n", "k": "\"", "\t":[ "\/" ]})",
"[1, 2.5, true, false, null, \"s\", \"\\u0000\", -0]",
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
"\xef\xbb\xbf[1, {\"a\": \"b\"}]", "\xef\xbb\xbf\"\\n\"",
};
for (const auto& text : texts)
{
CAPTURE(text)
CHECK(positions_as_parse<json>(text));
CHECK(positions_as_parse<ordered_json>(text));
}
}
SECTION("comments and trailing commas")
{
const std::vector<std::string> texts =
{
"[1 /* ] */]", "[1 // ]\n]", "{\"a\": 1 /* } */ , }", "[1, 2, ]", "[[1,],[2 , /**/ ] , ]",
"{\"a\": [] // }\n, \"b\": {} /* } ] */ }", "[ /* empty */ ]", "{ // }\n}", "/* a */ [1] // b",
"[\"\\n\" /* \" */ , ]",
};
for (const auto& text : texts)
{
CAPTURE(text)
CHECK(positions_as_parse<json>(text, true, true));
}
}
SECTION("generated documents")
{
json_view_test::generator gen;
for (int i = 0; i < 300; ++i)
{
std::string text;
gen.value(text, 0);
CAPTURE(text)
CHECK(positions_as_parse<json>(text));
CHECK(positions_as_parse<ordered_json>(text));
}
}
SECTION("test data")
{
for (const char* name :
{
"/json_testsuite/sample.json", "/nativejson-benchmark/canada.json", "/nativejson-benchmark/citm_catalog.json",
"/nativejson-benchmark/twitter.json", "/json_tests/pass1.json"
})
{
CAPTURE(name)
const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary);
std::stringstream ss;
ss << f.rdbuf();
CHECK(positions_as_parse<json>(ss.str()));
}
}
SECTION("subtrees")
{
const std::string text = R"({"a": [1, "\n", {"b": null}], "s": "x\ty", "n": -1.5})";
const json j = json::parse(text);
const json_document d = json_document::parse(text);
CHECK(same_positions(d.root()["a"].materialize(), j["a"]));
CHECK(same_positions(d.root()["a"][2].materialize(), j["a"][2]));
CHECK(same_positions(d.root()["a"][1].materialize(), j["a"][1]));
CHECK(same_positions(d.root()["s"].materialize(), j["s"]));
CHECK(same_positions(d.root()["n"].materialize(), j["n"]));
CHECK(range(d.root()["a"][1].materialize()) == "10-14");
CHECK(range(d.root()["s"].materialize()) == "35-41");
}
#if !defined(JSON_NOEXCEPTION)
SECTION("exception messages")
{
const std::string text = "{\n \"address\": {\"street\": \"Fake\\tStreet\", \"housenumber\": \"1\"},\n \"list\": [1, [true]]\n}";
const json j = json::parse(text);
const json_document d = json_document::parse(text);
const json m = d.root().materialize();
const std::string expected = json_view_test::exception_of_call([&]
{
static_cast<void>(j.at("address").at("housenumber").get<int>());
});
CHECK(expected.find("(bytes ") != std::string::npos);
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(m.at("address").at("housenumber").get<int>());
}) == expected);
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(m.at("address").at("street").get<int>());
}) == json_view_test::exception_of_call([&]
{
static_cast<void>(j.at("address").at("street").get<int>());
}));
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(m.at("list").at(1).get<int>());
}) == json_view_test::exception_of_call([&]
{
static_cast<void>(j.at("list").at(1).get<int>());
}));
}
#endif
SECTION("edited documents")
{
const std::string text = R"({"a": [1, 2], "b": {"c": "\n", "d": true}, "e": [3, [4]], "f": "x"})";
const json j = json::parse(text);
SECTION("new scalars have none")
{
json_editable_document d = json_editable_document::parse(text);
d.set(d.root()["f"], 5);
d.set(d.root()["b"]["d"], "s");
const json m = d.root().materialize();
CHECK(range(m["f"]) == "none");
CHECK(range(m["b"]["d"]) == "none");
// the object whose last member is new has none; the others keep theirs
CHECK(range(m) == "none");
CHECK(range(m["b"]) == "none");
CHECK(same_positions(m["a"], j["a"]));
CHECK(same_positions(m["e"], j["e"]));
CHECK(same_positions(m["b"]["c"], j["b"]["c"]));
}
SECTION("containers whose elements change have none")
{
json_editable_document d = json_editable_document::parse(text);
d.push_back(d.root()["a"], 7);
d.erase(d.root()["e"], 0);
const json m = d.root().materialize();
CHECK(range(m["a"]) == "none");
CHECK(range(m["a"][2]) == "none");
CHECK(range(m["e"]) == "none");
// the elements kept keep theirs
CHECK(range(m["a"][0]) == range(j["a"][0]));
CHECK(range(m["a"][1]) == range(j["a"][1]));
CHECK(same_positions(m["e"][0], j["e"][1]));
CHECK(same_positions(m["b"], j["b"]));
// the root's elements did not change
CHECK(range(m) == range(j));
}
SECTION("new containers have none")
{
json_editable_document d = json_editable_document::parse(text);
d.set(d.root()["b"], json::parse(R"({"x": [1]})"));
const json m = d.root().materialize();
CHECK(range(m["b"]) == "none");
CHECK(range(m["b"]["x"]) == "none");
CHECK(range(m["b"]["x"][0]) == "none");
CHECK(range(m) == range(j));
}
SECTION("copies of scalars of the document keep theirs")
{
// (a copy of an array or object is a new one; its scalars are the
// parsed ones)
json_editable_document d = json_editable_document::parse(text);
d.set(d.root()["f"], d.root()["e"]);
d.set(d.root()["a"], d.root()["b"]["c"]);
const json m = d.root().materialize();
CHECK(range(m["a"]) == range(j["b"]["c"]));
CHECK(range(m["f"]) == "none");
CHECK(range(m["f"][1]) == "none");
CHECK(range(m["f"][0]) == range(j["e"][0]));
CHECK(range(m["f"][1][0]) == range(j["e"][1][0]));
}
}
#if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
SECTION("images")
{
const std::string text = R"({"a\n": ["\u00e9", 1.5, {"b": [true, null]}], "c": "d"})";
const json j = json::parse(text);
SECTION("round trip")
{
const json_document d = json_document::load(json_document::parse(text).save());
CHECK(same_positions(d.root().materialize(), j));
}
SECTION("edited")
{
json_editable_document e = json_editable_document::parse(text);
e.set(e.root()["c"], 1);
e.push_back(e.root()[std::string("a") + '\n'], false);
e.set(e.root()[std::string("a") + '\n'][2]["b"][0], "\"");
const json_document d = json_document::load(e.save());
const json m = d.root().materialize();
CHECK(range(m["c"]) == "none");
CHECK(range(m) == "none");
const json& a = m[std::string("a") + '\n'];
const json& ja = j[std::string("a") + '\n'];
CHECK(range(a) == "none");
CHECK(range(a[3]) == "none");
CHECK(range(a[0]) == range(ja[0]));
CHECK(range(a[1]) == range(ja[1]));
CHECK(range(a[2]["b"][0]) == "none");
CHECK(range(a[2]["b"][1]) == range(ja[2]["b"][1]));
// the last element of b is still the parsed one
CHECK(range(a[2]["b"]) == range(ja[2]["b"]));
CHECK(range(a[2]) == range(ja[2]));
}
SECTION("without positions")
{
// an image saved without JSON_DIAGNOSTIC_POSITIONS: next is 0 for
// the strings with escapes
std::vector<std::uint8_t> image = json_document::parse(text).save();
std::uint64_t count = 0;
std::memcpy(&count, image.data() + 8, sizeof(count));
for (std::size_t i = 0; i < count; ++i)
{
nlohmann::detail::view::node n{};
std::memcpy(&n, image.data() + 64 + (i * sizeof(n)), sizeof(n));
if (n.kind == nlohmann::detail::view::node_kind::string)
{
n.next = 0;
}
std::memcpy(image.data() + 64 + (i * sizeof(n)), &n, sizeof(n));
}
const json_document d = json_document::load(image);
const json m = d.root().materialize();
CHECK(range(m[std::string("a") + '\n'][0]) == "none");
CHECK(range(m["c"]) == range(j["c"]));
CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2]));
}
#if !defined(JSON_NOEXCEPTION)
SECTION("a decoded string's position must be within the text")
{
std::vector<std::uint8_t> image = json_document::parse(R"(["\n"])").save();
nlohmann::detail::view::node n{};
std::memcpy(&n, image.data() + 64 + sizeof(n), sizeof(n));
REQUIRE(n.next == 2);
n.next = 7;
std::memcpy(image.data() + 64 + sizeof(n), &n, sizeof(n));
CHECK_THROWS_AS(json_document::load(image), json::parse_error&);
// without the check, the position is out of bounds and ignored
const json_document d = json_document::load(image, json_document::image_check::none);
CHECK(range(d.root()[0].materialize()) == "none");
}
#endif
}
#endif
}