Compare commits

..
Author SHA1 Message Date
Niels Lohmann bd442d5d07 Merge develop
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-28 07:11:43 +01:00
Niels Lohmann da83aa92c8 Reconcile -Weffc++ and readability-redundant-member-init in unit-bson.cpp (#5953) 2026-10-28 07:11:04 +01:00
Niels Lohmann eae3c8217b Split the checks of check_path_filters.py into smaller functions
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 19:39:20 +01:00
Niels Lohmann 960c234cc6 Do not report the excluded files that check_path_filters.py lists itself
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 18:58:36 +01:00
Niels Lohmann b6e24d19a9 Skip the compiler workflows for pull requests that change no build input
ubuntu.yml, macos.yml and windows.yml run about 155 jobs on every pull
request, including those that change only the documentation. They now
have a pull_request path filter: it starts with '**' and excludes the
documentation, Markdown files, GitHub metadata, other workflows and the
license files, so a new file runs them unless it is excluded on purpose.
Pushes to develop, master and release/* are not filtered.

The documentation jobs and the REUSE check move from ubuntu.yml to the
new repository_checks.yml, which has no path filter and also runs
tools/check_path_filters. That check keeps the three filters identical
and fails when a file the compiler jobs keep mentions an excluded file.
It found two: tools/check_build_options reads the Meson section of
package_managers.md, and the swiftpm job builds the Swift example from
the documentation; both are included in the filter.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 18:35:03 +01:00
25 changed files with 559 additions and 1121 deletions

No files matched your search

+5
View File
@@ -154,6 +154,11 @@ and each page's title (H1) and declaration by
[`docs/docset/generate_docset.py`](https://github.com/nlohmann/json/blob/develop/docs/docset/generate_docset.py);
`make build` reports API pages that cannot be classified.
A pull request that changes only the documentation, Markdown files, GitHub metadata, or license files skips the
compiler workflows (Ubuntu, macOS, Windows); the documentation and license checks still run. See
[`tools/check_path_filters`](https://github.com/nlohmann/json/tree/develop/tools/check_path_filters) for the list of
skipped files and what to do when a compiler job starts reading one of them.
### Amalgamate the source code
The single-header files
+19
View File
@@ -7,6 +7,25 @@ on:
- master
- release/*
pull_request:
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/macos.yml'
workflow_dispatch:
concurrency:
+78
View File
@@ -0,0 +1,78 @@
name: Repository checks
# Checks that concern files the compiler workflows (ubuntu.yml, macos.yml,
# windows.yml) skip with their path filter: the documentation, the license
# information, and the path filter itself. This workflow has no path filter.
on:
push:
branches:
- develop
- master
- release/*
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
check_path_filters:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check the path filters of the compiler workflows
run: python3 tools/check_path_filters/check_path_filters.py
ci_reuse_compliance:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_reuse_compliance
ci_test_documentation:
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# the git-revision-date-localized plugin needs the history; a shallow clone makes the strict build fail
fetch-depth: ${{ matrix.target == 'ci_test_build_documentation' && '0' || '1' }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: matrix.target == 'ci_test_documentation_mermaid'
with:
node-version: 24
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
+20 -26
View File
@@ -7,6 +7,25 @@ on:
- master
- release/*
pull_request:
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/ubuntu.yml'
workflow_dispatch:
permissions:
@@ -109,7 +128,7 @@ jobs:
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_single_header, ci_cppcheck, ci_cpplint, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata, ci_reuse_compliance, ci_test_valgrind]
target: [ci_test_single_header, ci_cppcheck, ci_cpplint, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata, ci_test_valgrind]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
@@ -449,31 +468,6 @@ jobs:
- name: Build
run: cmake --build build
ci_test_documentation:
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# the git-revision-date-localized plugin needs the history; a shallow clone makes the strict build fail
fetch-depth: ${{ matrix.target == 'ci_test_build_documentation' && '0' || '1' }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: matrix.target == 'ci_test_documentation_mermaid'
with:
node-version: 24
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
# Native Linux ARM64 runner with the latest GCC. The ubuntu-24.04-arm label is
# only available for public repositories.
ci_test_arm64:
+19
View File
@@ -7,6 +7,25 @@ on:
- master
- release/*
pull_request:
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/windows.yml'
workflow_dispatch:
permissions:
@@ -32,24 +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
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
positions [`BasicJsonType::parse()`](../basic_json/parse.md) would give them, also for the subtree of a view (the
positions are those in the document's whole source text), and exceptions about them name the same bytes. For values of
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).
This differs from a `basic_json` value, whose positions are not updated when it is changed: they stay as they were
parsed, even if they no longer match the value (see [`start_pos()`](../basic_json/start_pos.md)).
Documents [loaded](../basic_json_document/load.md) from an image have the positions of the saved document. Its strings
with escapes have positions only if the document was parsed with `JSON_DIAGNOSTIC_POSITIONS`: the position of their
opening quote is recorded while parsing, and `save()` and `load()` keep it whether or not they are compiled with the
macro.
Unlike parsing with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) enabled, the values
produced by `materialize()` do not carry source positions. The index has the start offset of every value, but not the
end of arrays, objects, and escaped strings, which the positions also need.
Calling `materialize()` on the same view repeatedly builds a new, independent `BasicJsonType` value each time; it
never caches the result.
@@ -77,7 +62,8 @@ never caches the result.
- [`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
- [`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
## Version history
@@ -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
- [`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
## 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.
- **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.
- **Fewer source positions after edits.** With
- **No source positions on `materialize()`.** Even with
[`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()`.
In an [editable document](../api/json_editable_document.md), a value written by an edit has none, and neither has
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).
[`materialize()`](../api/basic_json_view/materialize.md) does not set them. The index has the start offset of every
value, but not the end of arrays, objects, and escaped strings, which the positions also need.
- **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
members in the order they appear in the source text. `basic_json`'s default `object_t` is a `std::map`, which
+2 -8
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 |
| 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 |
| 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 if the document was parsed with [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), or 0 if it was not; all other values: 0 |
| 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>
@@ -218,12 +218,6 @@ packet-beta
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
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 almost nothing while parsing: the only work is
storing the offset of the opening quote of a string with escapes in its `next`, which is otherwise unused. 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
([`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
@@ -283,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
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`
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.
## Input adapters
+3 -22
View File
@@ -230,10 +230,7 @@ struct diagnostic_positions
{
// Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position.
// (npos: a value without a source position, see materialize() of
// json_view)
const std::size_t read = lexer->get_chars_read_total();
v.start_position = read == std::string::npos ? std::string::npos : read - 1;
v.start_position = lexer->get_chars_read_total() - 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.
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 = std::string::npos;
}
}
}
@@ -270,12 +261,6 @@ struct diagnostic_positions
// The start position will be set below based on the length of the string representation
// of the value.
v.end_position = lexer->get_chars_read_total();
if (v.end_position == std::string::npos)
{
// a value without a source position
v.start_position = std::string::npos;
return;
}
switch (v.type())
{
@@ -488,12 +473,8 @@ read up to it.
@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
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
{
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 string_t = typename BasicJsonType::string_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
+2 -2
View File
@@ -56,8 +56,8 @@ using parser_callback_t =
template<typename SAX>
struct sax_ignores_number_text : std::false_type {};
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType>
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect, LexerType>> : std::true_type {};
template<typename BasicJsonType, typename InputAdapterType, bool Collect>
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect>> : std::true_type {};
template<typename BasicJsonType, typename InputAdapterType>
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
+21 -13
View File
@@ -173,16 +173,31 @@ class builder
}
if (p[1] == '/')
{
return skip_line_comment<NulIsEnd>(p + 2, m_end);
p += 2;
// (as in parse(), a null byte is the end of the input, so it is
// left for the caller to see)
while (p != m_end && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0))
{
++p;
}
return p;
}
if (p[1] == '*')
{
p = find_block_comment_end<NulIsEnd>(p + 2, m_end);
if (p == m_end || *p != '*')
p += 2;
for (;;)
{
return fail_at(error_code::comment_unterminated, p);
if (p == m_end || (NulIsEnd && *p == 0))
{
return fail_at(error_code::comment_unterminated, p);
}
if (*p == '*' && p + 1 != m_end && p[1] == '/')
{
p += 2;
return p;
}
++p;
}
return p + 2;
}
++p;
return fail_at(error_code::comment_start, p);
@@ -1178,14 +1193,7 @@ indent_done:
return false;
}
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()
const std::uint64_t second = static_cast<std::uint64_t>(r.len) | (static_cast<std::uint64_t>(s - b) << 32);
#else
const std::uint64_t second = r.len;
#endif
push_node(node_kind::string, node_flags::escaped, 0, r.start, second);
push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len);
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
/// 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
/// are not finite become null, as dump() writes them. Nodes keep the flag
/// is_new: they have no source position (JSON_DIAGNOSTIC_POSITIONS).
/// are not finite become null, as dump() writes them.
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
@@ -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);
r.flags = node_flags::escaped;
}
r.flags = static_cast<std::uint8_t>(r.flags | (s.flags & node_flags::is_new));
return r;
};
const auto copy_node = [&](const node * 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))
{
case value_t::object:
case value_t::array:
r.flags = is_new;
r.flags = 0;
r.extra = 0;
r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off;
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());
text_tail.append(d.str(*v), number_length(*v));
}
r.flags = is_new;
r.flags = 0;
break;
case value_t::number_float:
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.kind = node_kind::null;
r.flags = is_new;
break;
}
r.off = static_cast<std::uint32_t>(d.size + text_tail.size());
text_tail.append(t, v->len);
r.extra = 0xFFFFu; // the digit layout is not recorded
}
r.flags = is_new;
r.flags = 0;
break;
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;
case value_t::null:
case value_t::binary:
case value_t::discarded:
default:
r.flags = is_new;
r.flags = 0;
break;
}
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;
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;
}
const bool decoded = (n.flags & node_flags::escaped) != 0;
const std::size_t limit = decoded ? arena_size : text_size;
// (next: the source offset of a decoded string's opening quote plus
// one, or 0; see node)
if (n.off > limit || n.len > limit - n.off || n.next > text_size)
if (n.off > limit || n.len > limit - n.off)
{
return false;
}
@@ -599,13 +593,13 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
{
case value_t::null:
// (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;
}
break;
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;
}
@@ -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_unsigned:
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;
}
@@ -641,7 +635,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
case value_t::object:
{
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;
}
+5 -263
View File
@@ -19,7 +19,6 @@
#include <nlohmann/detail/view/macro_scope.hpp>
#include <nlohmann/detail/view/node.hpp>
#include <nlohmann/detail/view/number.hpp>
#include <nlohmann/detail/view/scan.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
@@ -27,189 +26,6 @@ namespace detail
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. One position_source serves a whole
materialize() call: before each value is handed to the handler, the range of
that value is set (or cleared), overwriting that of the value before.
*/
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
{
if (first == std::string::npos || last == std::string::npos)
{
clear();
return;
}
start = first;
end = last;
}
/// no range: the next value has no source position
void clear() noexcept
{
start = std::string::npos;
end = 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
{
const auto* const b = reinterpret_cast<const unsigned char*>(src); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the text as bytes, as the builder reads it
const unsigned char* const e = b + size;
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 = static_cast<std::size_t>(skip_line_comment(b + pos + 2, e) - b);
break;
}
if (pos + 1 < size && src[pos + 1] == '*')
{
// (unterminated: pos is size, which ends the loop)
pos = static_cast<std::size_t>(find_block_comment_end(b + pos + 2, e) - b) + 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
{
if ((n.flags & node_flags::is_new) != 0)
{
clear();
return;
}
std::size_t first = std::string::npos;
std::size_t last = std::string::npos;
switch (n.kind)
{
case node_kind::string:
if ((n.flags & node_flags::storage) == 0)
{
// off is the first byte after the opening quote
if (n.off != 0)
{
first = n.off - 1;
last = n.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], d.size, first);
}
break;
case node_kind::number_integer:
case node_kind::number_unsigned:
case node_kind::number_float:
if ((n.flags & node_flags::storage) == 0)
{
first = n.off;
last = n.off + number_length(n);
}
break;
case node_kind::boolean:
first = n.off;
last = n.off + ((n.flags & node_flags::is_true) != 0 ? 4u : 5u);
break;
case node_kind::null:
first = n.off;
last = n.off + 4u;
break;
case node_kind::object:
case node_kind::array:
case node_kind::link:
default:
break;
}
// (a crafted image can point past the text)
if (last == std::string::npos || last > d.size)
{
clear();
}
else
{
set(first, last);
}
}
};
#endif
/*!
@brief the basic_json value of the subtree at n
@@ -217,24 +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
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().
With JSON_DIAGNOSTIC_POSITIONS, the values get their source positions as with
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).
Without a lexer the handler records no source positions
(JSON_DIAGNOSTIC_POSITIONS).
*/
template<typename BasicJsonType, bool Editable>
BasicJsonType materialize(const document_data& d, const node* n)
{
using string_t = typename BasicJsonType::string_t;
using input_t = iterator_input_adapter<const char*>;
#if JSON_DIAGNOSTIC_POSITIONS
using lexer_t = position_source;
#else
using lexer_t = lexer<BasicJsonType, input_t>; // (the default: unused, as no lexer is passed)
#endif
using sax_t = json_sax_dom_parser<BasicJsonType, input_t, true, lexer_t>;
using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true>;
using nav = navigation<Editable>;
// the open containers: their next element (or key of the next member),
@@ -246,15 +52,9 @@ BasicJsonType materialize(const document_data& d, const node* n)
const node* pos;
const node* end;
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
// (with positions, it goes through the handler, which sets them)
switch (static_cast<value_t>(n->kind))
{
case value_t::string:
@@ -276,16 +76,10 @@ BasicJsonType materialize(const document_data& d, const node* n)
default:
break;
}
#endif
BasicJsonType result;
#if JSON_DIAGNOSTIC_POSITIONS
position_source positions;
sax_t sax(result, true, &positions);
#else
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)
std::array<frame, 16> shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open
std::vector<frame> deep{};
@@ -300,16 +94,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
std::size_t depth = 0;
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
// @infer-ignore NULLPTR_DEREFERENCE
switch (static_cast<value_t>(n->kind))
@@ -318,17 +102,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
case value_t::array:
{
const bool object = n->kind == node_kind::object;
#if JSON_DIAGNOSTIC_POSITIONS
// (moved containers are also new)
if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size)
{
positions.clear();
}
else
{
positions.set(std::size_t{n->off}, std::size_t{n->off} + 1);
}
#endif
if (object)
{
sax.start_object(n->len);
@@ -345,13 +118,7 @@ BasicJsonType materialize(const document_data& d, const node* n)
open = deep.data();
capacity = deep.size();
}
// (with positions: 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
#if JSON_DIAGNOSTIC_POSITIONS
, positions.start, positions.end
#endif
};
open[depth++] = frame{nav::first(d, n), nav::end(d, n), object};
break;
}
case value_t::string:
@@ -392,31 +159,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
frame& f = open[depth - 1];
if (f.pos == f.end)
{
#if JSON_DIAGNOSTIC_POSITIONS
if (f.start == std::string::npos || f.last_end == std::string::npos)
{
positions.clear();
}
else
{
const std::size_t end = position_source::container_end(d.base[0], d.size, f.last_end);
if (end == std::string::npos)
{
positions.clear();
}
else
{
// the closing bracket, as the lexer reports it (end_object()
// and end_array() only read its end; the start of the
// container was set when it was opened)
positions.set(end - 1, end);
}
}
if (depth > 1)
{
open[depth - 2].last_end = positions.end;
}
#endif
if (f.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::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 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");
-26
View File
@@ -172,32 +172,6 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits(const unsigned char
return p;
}
/// advance over the text of a line comment (after its "//") to the line
/// break or the end (with NulIsEnd also a null byte, which is left for the
/// caller to see, as in parse())
template<bool NulIsEnd = false>
inline const unsigned char* skip_line_comment(const unsigned char* p, const unsigned char* e) noexcept
{
while (p != e && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0))
{
++p;
}
return p;
}
/// the "*/" that ends a block comment whose text starts at p (after its "/*");
/// if it is unterminated, the end (with NulIsEnd also a null byte), which is
/// where the caller reports the error
template<bool NulIsEnd = false>
inline const unsigned char* find_block_comment_end(const unsigned char* p, const unsigned char* e) noexcept
{
while (p != e && !(NulIsEnd && *p == 0) && !(*p == '*' && p + 1 != e && p[1] == '/'))
{
++p;
}
return p;
}
/// powers of ten up to 10^19 as integers
inline std::uint64_t int_pow10(unsigned k) noexcept
{
+1 -1
View File
@@ -159,7 +159,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
friend class ::nlohmann::detail::binary_writer;
template<typename BasicJsonType, typename InputType, typename SAX>
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;
template<typename BasicJsonType, typename InputAdapterType>
friend class ::nlohmann::detail::json_sax_dom_binary_parser;
+6 -25
View File
@@ -15629,10 +15629,7 @@ struct diagnostic_positions
{
// Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position.
// (npos: a value without a source position, see materialize() of
// json_view)
const std::size_t read = lexer->get_chars_read_total();
v.start_position = read == std::string::npos ? std::string::npos : read - 1;
v.start_position = lexer->get_chars_read_total() - 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.
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 = std::string::npos;
}
}
}
@@ -15669,12 +15660,6 @@ struct diagnostic_positions
// The start position will be set below based on the length of the string representation
// of the value.
v.end_position = lexer->get_chars_read_total();
if (v.end_position == std::string::npos)
{
// a value without a source position
v.start_position = std::string::npos;
return;
}
switch (v.type())
{
@@ -15887,12 +15872,8 @@ read up to it.
@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
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
{
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 string_t = typename BasicJsonType::string_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
@@ -22534,8 +22515,8 @@ using parser_callback_t =
template<typename SAX>
struct sax_ignores_number_text : std::false_type {};
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType>
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect, LexerType>> : std::true_type {};
template<typename BasicJsonType, typename InputAdapterType, bool Collect>
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect>> : std::true_type {};
template<typename BasicJsonType, typename InputAdapterType>
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
@@ -36929,7 +36910,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
friend class ::nlohmann::detail::binary_writer;
template<typename BasicJsonType, typename InputType, typename SAX>
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;
template<typename BasicJsonType, typename InputAdapterType>
friend class ::nlohmann::detail::json_sax_dom_binary_parser;
+39 -322
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::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 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");
@@ -1185,32 +1185,6 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits(const unsigned char
return p;
}
/// advance over the text of a line comment (after its "//") to the line
/// break or the end (with NulIsEnd also a null byte, which is left for the
/// caller to see, as in parse())
template<bool NulIsEnd = false>
inline const unsigned char* skip_line_comment(const unsigned char* p, const unsigned char* e) noexcept
{
while (p != e && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0))
{
++p;
}
return p;
}
/// the "*/" that ends a block comment whose text starts at p (after its "/*");
/// if it is unterminated, the end (with NulIsEnd also a null byte), which is
/// where the caller reports the error
template<bool NulIsEnd = false>
inline const unsigned char* find_block_comment_end(const unsigned char* p, const unsigned char* e) noexcept
{
while (p != e && !(NulIsEnd && *p == 0) && !(*p == '*' && p + 1 != e && p[1] == '/'))
{
++p;
}
return p;
}
/// powers of ten up to 10^19 as integers
inline std::uint64_t int_pow10(unsigned k) noexcept
{
@@ -1408,16 +1382,31 @@ class builder
}
if (p[1] == '/')
{
return skip_line_comment<NulIsEnd>(p + 2, m_end);
p += 2;
// (as in parse(), a null byte is the end of the input, so it is
// left for the caller to see)
while (p != m_end && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0))
{
++p;
}
return p;
}
if (p[1] == '*')
{
p = find_block_comment_end<NulIsEnd>(p + 2, m_end);
if (p == m_end || *p != '*')
p += 2;
for (;;)
{
return fail_at(error_code::comment_unterminated, p);
if (p == m_end || (NulIsEnd && *p == 0))
{
return fail_at(error_code::comment_unterminated, p);
}
if (*p == '*' && p + 1 != m_end && p[1] == '/')
{
p += 2;
return p;
}
++p;
}
return p + 2;
}
++p;
return fail_at(error_code::comment_start, p);
@@ -2413,14 +2402,7 @@ indent_done:
return false;
}
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()
const std::uint64_t second = static_cast<std::uint64_t>(r.len) | (static_cast<std::uint64_t>(s - b) << 32);
#else
const std::uint64_t second = r.len;
#endif
push_node(node_kind::string, node_flags::escaped, 0, r.start, second);
push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len);
return true;
}
};
@@ -4961,8 +4943,7 @@ constexpr std::uint64_t image_limit = 0xFFFFFFF0u;
/// 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
/// 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
/// is_new: they have no source position (JSON_DIAGNOSTIC_POSITIONS).
/// are not finite become null, as dump() writes them.
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
@@ -4985,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);
r.flags = node_flags::escaped;
}
r.flags = static_cast<std::uint8_t>(r.flags | (s.flags & node_flags::is_new));
return r;
};
const auto copy_node = [&](const node * 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))
{
case value_t::object:
case value_t::array:
r.flags = is_new;
r.flags = 0;
r.extra = 0;
r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off;
r.len = 0; // counted below
@@ -5013,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());
text_tail.append(d.str(*v), number_length(*v));
}
r.flags = is_new;
r.flags = 0;
break;
case value_t::number_float:
if ((v->flags & node_flags::storage) == node_flags::edited)
@@ -5023,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.kind = node_kind::null;
r.flags = is_new;
break;
}
r.off = static_cast<std::uint32_t>(d.size + text_tail.size());
text_tail.append(t, v->len);
r.extra = 0xFFFFu; // the digit layout is not recorded
}
r.flags = is_new;
r.flags = 0;
break;
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;
case value_t::null:
case value_t::binary:
case value_t::discarded:
default:
r.flags = is_new;
r.flags = 0;
break;
}
out.push_back(r);
@@ -5400,15 +5378,13 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
std::vector<float_range> floats;
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;
}
const bool decoded = (n.flags & node_flags::escaped) != 0;
const std::size_t limit = decoded ? arena_size : text_size;
// (next: the source offset of a decoded string's opening quote plus
// one, or 0; see node)
if (n.off > limit || n.len > limit - n.off || n.next > text_size)
if (n.off > limit || n.len > limit - n.off)
{
return false;
}
@@ -5480,13 +5456,13 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
{
case value_t::null:
// (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;
}
break;
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;
}
@@ -5500,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_unsigned:
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;
}
@@ -5522,7 +5498,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
case value_t::object:
{
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;
}
@@ -6125,8 +6101,6 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/view/number.hpp>
// #include <nlohmann/detail/view/scan.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
@@ -6134,189 +6108,6 @@ namespace detail
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. One position_source serves a whole
materialize() call: before each value is handed to the handler, the range of
that value is set (or cleared), overwriting that of the value before.
*/
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
{
if (first == std::string::npos || last == std::string::npos)
{
clear();
return;
}
start = first;
end = last;
}
/// no range: the next value has no source position
void clear() noexcept
{
start = std::string::npos;
end = 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
{
const auto* const b = reinterpret_cast<const unsigned char*>(src); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the text as bytes, as the builder reads it
const unsigned char* const e = b + size;
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 = static_cast<std::size_t>(skip_line_comment(b + pos + 2, e) - b);
break;
}
if (pos + 1 < size && src[pos + 1] == '*')
{
// (unterminated: pos is size, which ends the loop)
pos = static_cast<std::size_t>(find_block_comment_end(b + pos + 2, e) - b) + 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
{
if ((n.flags & node_flags::is_new) != 0)
{
clear();
return;
}
std::size_t first = std::string::npos;
std::size_t last = std::string::npos;
switch (n.kind)
{
case node_kind::string:
if ((n.flags & node_flags::storage) == 0)
{
// off is the first byte after the opening quote
if (n.off != 0)
{
first = n.off - 1;
last = n.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], d.size, first);
}
break;
case node_kind::number_integer:
case node_kind::number_unsigned:
case node_kind::number_float:
if ((n.flags & node_flags::storage) == 0)
{
first = n.off;
last = n.off + number_length(n);
}
break;
case node_kind::boolean:
first = n.off;
last = n.off + ((n.flags & node_flags::is_true) != 0 ? 4u : 5u);
break;
case node_kind::null:
first = n.off;
last = n.off + 4u;
break;
case node_kind::object:
case node_kind::array:
case node_kind::link:
default:
break;
}
// (a crafted image can point past the text)
if (last == std::string::npos || last > d.size)
{
clear();
}
else
{
set(first, last);
}
}
};
#endif
/*!
@brief the basic_json value of the subtree at n
@@ -6324,24 +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
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().
With JSON_DIAGNOSTIC_POSITIONS, the values get their source positions as with
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).
Without a lexer the handler records no source positions
(JSON_DIAGNOSTIC_POSITIONS).
*/
template<typename BasicJsonType, bool Editable>
BasicJsonType materialize(const document_data& d, const node* n)
{
using string_t = typename BasicJsonType::string_t;
using input_t = iterator_input_adapter<const char*>;
#if JSON_DIAGNOSTIC_POSITIONS
using lexer_t = position_source;
#else
using lexer_t = lexer<BasicJsonType, input_t>; // (the default: unused, as no lexer is passed)
#endif
using sax_t = json_sax_dom_parser<BasicJsonType, input_t, true, lexer_t>;
using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true>;
using nav = navigation<Editable>;
// the open containers: their next element (or key of the next member),
@@ -6353,15 +6134,9 @@ BasicJsonType materialize(const document_data& d, const node* n)
const node* pos;
const node* end;
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
// (with positions, it goes through the handler, which sets them)
switch (static_cast<value_t>(n->kind))
{
case value_t::string:
@@ -6383,16 +6158,10 @@ BasicJsonType materialize(const document_data& d, const node* n)
default:
break;
}
#endif
BasicJsonType result;
#if JSON_DIAGNOSTIC_POSITIONS
position_source positions;
sax_t sax(result, true, &positions);
#else
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)
std::array<frame, 16> shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open
std::vector<frame> deep{};
@@ -6407,16 +6176,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
std::size_t depth = 0;
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
// @infer-ignore NULLPTR_DEREFERENCE
switch (static_cast<value_t>(n->kind))
@@ -6425,17 +6184,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
case value_t::array:
{
const bool object = n->kind == node_kind::object;
#if JSON_DIAGNOSTIC_POSITIONS
// (moved containers are also new)
if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size)
{
positions.clear();
}
else
{
positions.set(std::size_t{n->off}, std::size_t{n->off} + 1);
}
#endif
if (object)
{
sax.start_object(n->len);
@@ -6452,13 +6200,7 @@ BasicJsonType materialize(const document_data& d, const node* n)
open = deep.data();
capacity = deep.size();
}
// (with positions: 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
#if JSON_DIAGNOSTIC_POSITIONS
, positions.start, positions.end
#endif
};
open[depth++] = frame{nav::first(d, n), nav::end(d, n), object};
break;
}
case value_t::string:
@@ -6499,31 +6241,6 @@ BasicJsonType materialize(const document_data& d, const node* n)
frame& f = open[depth - 1];
if (f.pos == f.end)
{
#if JSON_DIAGNOSTIC_POSITIONS
if (f.start == std::string::npos || f.last_end == std::string::npos)
{
positions.clear();
}
else
{
const std::size_t end = position_source::container_end(d.base[0], d.size, f.last_end);
if (end == std::string::npos)
{
positions.clear();
}
else
{
// the closing bracket, as the lexer reports it (end_object()
// and end_array() only read its end; the start of the
// container was set when it was opened)
positions.set(end - 1, end);
}
}
if (depth > 1)
{
open[depth - 2].last_end = positions.end;
}
#endif
if (f.object)
{
sax.end_object();
+2
View File
@@ -1311,6 +1311,8 @@ TEST_CASE("BSON declared document size bounds the content")
// records the events that a SAX parser receives
struct recorder : nlohmann::json_sax<json>
{
// GCC's -Weffc++ wants the member initialized in the member initialization list
// NOLINTNEXTLINE(readability-redundant-member-init)
std::string events {};
bool null() override
+9 -23
View File
@@ -749,20 +749,6 @@ std::string exception_of(F f)
}
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
// 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 (!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())
{
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("missing")); }) == json_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(5)); }) == json_exception_of([&] { static_cast<void>(j.at(5)); }));
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")); }) == exception_of([&] { static_cast<void>(j.at("missing")); }));
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)); }) == exception_of([&] { static_cast<void>(j.at(5)); }));
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())
{
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == json_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.front()); }) == exception_of([&] { static_cast<void>(j.front()); }));
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == exception_of([&] { static_cast<void>(j.back()); }));
}
#endif
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);
}
// (the exceptions of views have neither the path nor the byte range of the value)
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS
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])");
@@ -393,7 +392,7 @@ TEST_CASE("json_view from_json")
#if !defined(JSON_NOEXCEPTION)
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\"");
#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>()); }));
#endif
#endif
@@ -441,11 +440,10 @@ TEST_CASE("json_view from_json")
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")
{
// (with JSON_DIAGNOSTICS, those of json name the path of the value,
// and with JSON_DIAGNOSTIC_POSITIONS its bytes)
// (with JSON_DIAGNOSTICS, those of json name the path of the value)
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();
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
}
+34
View File
@@ -0,0 +1,34 @@
# check_path_filters
The compiler workflows [`ubuntu.yml`](../../.github/workflows/ubuntu.yml),
[`macos.yml`](../../.github/workflows/macos.yml), and [`windows.yml`](../../.github/workflows/windows.yml) skip pull
requests that change only files no compiler job reads: the documentation, Markdown files, GitHub metadata, other
workflows, and the license files. Pushes to `develop`, `master`, and `release/*` always run them. The documentation,
license, and path-filter checks run in [`repository_checks.yml`](../../.github/workflows/repository_checks.yml), which
has no path filter.
The filter starts with `'**'` and excludes these files explicitly, so a new file runs the compiler workflows unless it
is excluded on purpose. [`check_path_filters.py`](check_path_filters.py) holds the filter (`PATHS`) and checks that
- each of the three workflows has exactly this filter, with its own workflow file included,
- the CI targets that read excluded files (`UNFILTERED_TARGETS`) do not run in these workflows, and
- no file the filter keeps (sources, tests, build files, tools, workflows) mentions an excluded file or directory
outside a comment line, unless the mention is listed in `KNOWN_REFERENCES` with a reason.
When the check reports a mention, decide whether a compiler job reads that file:
- If it does, include the file in `PATHS` after the exclusions (as `package_managers.md` and the Swift example are) and
copy the new filter into the three workflows.
- If it does not, add the mention to `KNOWN_REFERENCES` with the reason.
Run the check with
```shell
python3 tools/check_path_filters/check_path_filters.py
```
To see which workflows a pull request would run, pass the changed files:
```shell
python3 tools/check_path_filters/check_path_filters.py --changed $(git diff --name-only origin/develop)
```
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""Check the path filters of the compiler workflows.
ubuntu.yml, macos.yml and windows.yml skip pull requests that change only files
no compiler job reads (documentation, Markdown files, GitHub metadata, license
files). The filter starts with '**' and excludes these files explicitly, so a
new file runs the workflows unless it is excluded on purpose.
The script checks that
- each of the three workflows has exactly the filter defined in PATHS below,
with its own workflow file included, and
- no tracked file that the filter keeps (sources, tests, build files, tools,
workflows) mentions a file the filter excludes, unless the mention is listed
in KNOWN_REFERENCES with the reason why it does not matter.
A mention found this way must either be added to PATHS as an included path
(the file is read by a compiler job) or to KNOWN_REFERENCES (it is not).
With --changed FILE..., the script prints instead which workflows a pull
request changing these files would run.
"""
import argparse
import os
import re
import subprocess # nosec B404
import sys
REPO_ROOT = os.path.normpath(os.path.join(sys.path[0], '..', '..'))
SELF = 'tools/check_path_filters/check_path_filters.py'
WORKFLOWS = ['.github/workflows/ubuntu.yml', '.github/workflows/macos.yml', '.github/workflows/windows.yml']
# The pull_request paths filter of the workflows above; '{workflow}' is replaced
# with the workflow's own file. GitHub applies the patterns in order: a file
# matching a later pattern is included ('pattern') or excluded ('!pattern').
PATHS = [
'**',
'!docs/**',
'!**.md',
'!.github/*',
'!.github/ISSUE_TEMPLATE/**',
'!.github/external_ci/**',
'!.github/workflows/**',
'!.reuse/**',
'!LICENSES/**',
'!CITATION.cff',
'!REUSE.toml',
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
'docs/mkdocs/docs/integration/package_managers.md',
# built by the swiftpm job (macos.yml)
'docs/mkdocs/docs/integration/swift/**',
'{workflow}',
]
# Mentions of excluded files that no job of the three workflows depends on:
# (file that mentions it, the mention, reason)
KNOWN_REFERENCES = [
('cmake/ci.cmake', 'docs/', 'ci_test_examples; runs in repository_checks.yml, which has no path filter'),
('cmake/ci.cmake', 'docs/mkdocs/', 'ci_test_build_documentation and ci_test_documentation_mermaid; they run in '
'repository_checks.yml, which has no path filter'),
('cmake/ci.cmake', 'docs/mkdocs/docs/examples/', 'indentation check of ci_test_amalgamation; the same check runs in '
'check_amalgamation.yml, which has no path filter'),
('Makefile', 'docs/mkdocs/docs/examples/', 'make pretty; runs in check_amalgamation.yml, which has no path filter'),
('Makefile', 'ChangeLog.md', 'make ChangeLog.md writes the file; no CI job runs it'),
('Makefile', 'tests/fuzzing.md', 'help text'),
('Makefile', 'tools/generate_natvis/README.md', 'help text'),
('tests/src/make_test_data_available.hpp', 'README.md', 'a file of the external test data repository'),
('tools/check_build_options/check_build_options.py', 'docs/', 'builds the path of package_managers.md, which '
'PATHS includes'),
]
# CI targets that read excluded files; they must run in a workflow without a path filter
UNFILTERED_TARGETS = ['ci_test_examples', 'ci_test_build_documentation', 'ci_test_documentation_mermaid',
'ci_test_amalgamation', 'ci_reuse_compliance']
def git_files(root):
# fixed command, no user input; includes new files not yet added, so a local run
# sees what CI sees after the commit
out = subprocess.check_output(['git', 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], cwd=root) # nosec B603, B607
return [f for f in out.decode('utf-8').split('\0') if f]
def glob_to_regex(pattern):
"""Translate a GitHub Actions path pattern to a regular expression."""
regex = ''
i = 0
while i < len(pattern):
if pattern.startswith('**', i):
regex += '.*'
i += 2
elif pattern[i] == '*':
regex += '[^/]*'
i += 1
elif pattern[i] == '?':
regex += '[^/]'
i += 1
else:
regex += re.escape(pattern[i])
i += 1
return re.compile(regex + r'\Z')
def compile_filter(paths, workflow):
return [(p.startswith('!'), glob_to_regex(p.lstrip('!').replace('{workflow}', workflow))) for p in paths]
def included(compiled, path):
result = False
for negated, regex in compiled:
if regex.match(path):
result = not negated
return result
def read_filter(root, workflow):
"""Return the pull_request paths list of a workflow file."""
with open(os.path.join(root, workflow), encoding='utf-8') as f:
lines = f.read().splitlines()
paths = None
in_pull_request = False
for line in lines:
if re.match(r'^ pull_request:', line):
in_pull_request = True
continue
if in_pull_request:
if re.match(r'^ \S', line) or re.match(r'^\S', line):
break
if re.match(r'^ paths:\s*$', line):
paths = []
continue
m = re.match(r"^ - '(.*)'\s*(#.*)?$", line)
if paths is not None and m:
paths.append(m.group(1))
elif paths is not None and line.strip() and not line.strip().startswith('#'):
break
return paths
TOKEN = re.compile(r'(["\'/]?)([\w.-]+(?:/[\w.-]+)*)')
COMMENT_LINE = re.compile(r'^[ \t]*(//|#|/?\*|--).*$', re.MULTILINE)
class Mentions:
"""Finds the words and paths in a text that name an excluded file or directory."""
def __init__(self, files, excluded, is_excluded):
self.files = set(files)
self.excluded = excluded
self.dirs = {'/'.join(f.split('/')[:n]) for f in files for n in range(1, f.count('/') + 1)}
# an excluded directory: a new file in it would be excluded
self.excluded_dirs = {d for d in self.dirs if is_excluded(d + '/new_file')}
# base names that only excluded files have, e.g. 'ChangeLog.md' but not 'CMakeLists.txt'
self.names = {os.path.basename(f) for f in excluded} - {os.path.basename(f) for f in files if f not in excluded}
def find(self, text):
"""Yield (offset, mention) for every word or path in text that names an excluded file or directory."""
for m in TOKEN.finditer(text):
parts = m.group(2).rstrip('/').split('/')
# a single word names a directory only in a path context, e.g. 'docs' in os.path.join('docs', ...)
path_context = len(parts) > 1 or m.group(1)
for i in range(len(parts)):
path = '/'.join(parts[i:])
if path in self.files:
if path in self.excluded:
yield m.start(2), path
break
if path in self.dirs and path_context:
if path in self.excluded_dirs:
yield m.start(2), path + '/'
break
else:
if parts[-1] in self.names and '.' in parts[-1]:
yield m.start(2), parts[-1]
def known(path, mention):
return any(path == file and mention == known_mention for file, known_mention, _ in KNOWN_REFERENCES)
def check_workflows(root):
"""Return the errors in the filters of the workflows and in the targets they run."""
errors = []
for workflow in WORKFLOWS:
expected = [p.replace('{workflow}', workflow) for p in PATHS]
actual = read_filter(root, workflow)
if actual != expected:
errors.append('{}: the pull_request paths filter differs from tools/check_path_filters/check_path_filters.py\n'
' expected: {}\n actual: {}'.format(workflow, expected, actual))
with open(os.path.join(root, workflow), encoding='utf-8') as f:
text = f.read()
errors += ['{}: runs {}, which reads excluded files; run it in repository_checks.yml'.format(workflow, target)
for target in UNFILTERED_TARGETS if re.search(r'\b' + target + r'\b', text)]
return errors
def scanned_text(root, path):
"""Return the text of a kept file without comment lines and without the filter itself, or None to skip it."""
# third-party code and Markdown files read nothing; this script lists the excluded files
if path.startswith('tests/thirdparty/') or path.endswith('.md') or path == SELF:
return None
try:
with open(os.path.join(root, path), encoding='utf-8') as f:
# a mention in a comment line reads nothing; keep the line numbers
text = COMMENT_LINE.sub('', f.read())
except (UnicodeDecodeError, OSError):
return None
if path in WORKFLOWS:
# the filter itself lists the excluded files
for p in PATHS:
text = text.replace(" - '{}'".format(p.replace('{workflow}', path)), '')
return text
def check_mentions(root, files, excluded, finder):
"""Return an error for every mention of an excluded file in a kept file that KNOWN_REFERENCES does not list."""
errors = []
for path in sorted(set(files) - excluded):
text = scanned_text(root, path)
reported = set()
for offset, mention in finder.find(text or ''):
if mention not in reported and not known(path, mention):
reported.add(mention)
line = text.count('\n', 0, offset) + 1
errors.append('{}:{}: mentions excluded {}; include it in PATHS if a compiler job reads it, '
'or add it to KNOWN_REFERENCES'.format(path, line, mention))
return errors
def check(root):
files = git_files(root)
union = [compile_filter(PATHS, w) for w in WORKFLOWS]
def is_excluded(path):
return not any(included(c, path) for c in union)
excluded = {f for f in files if is_excluded(f)}
errors = check_workflows(root) + check_mentions(root, files, excluded, Mentions(files, excluded, is_excluded))
for e in errors:
print(e)
if not errors:
print('path filters OK: {} of {} tracked files skip the compiler workflows'.format(len(excluded), len(files)))
return 1 if errors else 0
def explain(changed):
for workflow in WORKFLOWS:
compiled = compile_filter(PATHS, workflow)
hits = [f for f in changed if included(compiled, f)]
print('{}: {}'.format(workflow, 'runs ({})'.format(', '.join(hits[:3]) + (' ...' if len(hits) > 3 else ''))
if hits else 'skipped'))
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument('--root', default=REPO_ROOT, help='repository root')
parser.add_argument('--changed', nargs='+', metavar='FILE', help='show which workflows these changed files run')
args = parser.parse_args()
return explain(args.changed) if args.changed else check(args.root)
if __name__ == '__main__':
sys.exit(main())