Compare commits

..
Author SHA1 Message Date
Niels Lohmann ae792a4c95 Keep the byte range in exceptions about the root value (#5987)
With JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS both set, exception::diagnostics()
and the json_view diagnostics() returned early for a value without a path (the
root), dropping "(bytes a-b)". Build the "(path) " prefix only when there are
tokens and always append the byte range.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 21:03:07 +01:00
23 changed files with 247 additions and 420 deletions

No files matched your search

+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
permissions: permissions:
actions: write actions: write
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
+1 -1
View File
@@ -19,7 +19,7 @@ permissions:
jobs: jobs:
save: save:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2 uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
+1 -1
View File
@@ -16,7 +16,7 @@ concurrency:
jobs: jobs:
check_docs_links: check_docs_links:
if: github.repository == 'nlohmann/json' if: github.repository == 'nlohmann/json'
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: Harden Runner - name: Harden Runner
@@ -12,7 +12,7 @@ jobs:
comment: comment:
# push runs on develop have no PR to comment on (and no "pr" artifact) # push runs on develop have no PR to comment on (and no "pr" artifact)
if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }} if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
actions: read actions: read
+1 -1
View File
@@ -18,7 +18,7 @@ permissions:
jobs: jobs:
dependency-review: dependency-review:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2 uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
+1 -1
View File
@@ -24,7 +24,7 @@ on:
jobs: jobs:
flawfinder: flawfinder:
name: Flawfinder name: Flawfinder
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
permissions: permissions:
actions: read actions: read
contents: read contents: read
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
contents: write contents: write
if: github.repository == 'nlohmann/json' if: github.repository == 'nlohmann/json'
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2 uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
+1 -1
View File
@@ -24,7 +24,7 @@ permissions:
jobs: jobs:
analysis: analysis:
name: Scorecard analysis name: Scorecard analysis
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
permissions: permissions:
# Needed to upload the results to code-scanning dashboard. # Needed to upload the results to code-scanning dashboard.
security-events: write security-events: write
+1 -1
View File
@@ -8,7 +8,7 @@ permissions:
jobs: jobs:
stale: stale:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
permissions: permissions:
issues: write issues: write
+13 -13
View File
@@ -18,7 +18,7 @@ concurrency:
jobs: jobs:
ci_test_gcc: ci_test_gcc:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
container: gcc:latest container: gcc:latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -130,7 +130,7 @@ jobs:
run: cmake --build build --target ${{ matrix.target }} run: cmake --build build --target ${{ matrix.target }}
ci_static_analysis_clang: ci_static_analysis_clang:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
container: silkeh/clang:22 container: silkeh/clang:22
strategy: strategy:
matrix: matrix:
@@ -149,7 +149,7 @@ jobs:
run: cmake --build build --target ${{ matrix.target }} run: cmake --build build --target ${{ matrix.target }}
ci_cmake_options: ci_cmake_options:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
container: ubuntu:24.04 container: ubuntu:24.04
strategy: strategy:
matrix: matrix:
@@ -209,7 +209,7 @@ jobs:
fail-on-error: false fail-on-error: false
ci_test_compilers_gcc_old: ci_test_compilers_gcc_old:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
compiler: ['4.8', '4.9', '5', '6'] compiler: ['4.8', '4.9', '5', '6']
@@ -251,7 +251,7 @@ jobs:
run: cmake --build build --target ci_test_compiler_g++-${{ matrix.compiler }} run: cmake --build build --target ci_test_compiler_g++-${{ matrix.compiler }}
ci_test_compilers_gcc: ci_test_compilers_gcc:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
# older GCC docker images (4, 5, 6) fail to check out code # older GCC docker images (4, 5, 6) fail to check out code
@@ -269,7 +269,7 @@ jobs:
run: cmake --build build --target ci_test_compiler_default run: cmake --build build --target ci_test_compiler_default
ci_test_compilers_clang: ci_test_compilers_clang:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
compiler: ['3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest'] compiler: ['3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest']
@@ -289,7 +289,7 @@ jobs:
run: cmake --build build --target ci_test_compiler_default run: cmake --build build --target ci_test_compiler_default
ci_test_standards_gcc: ci_test_standards_gcc:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
container: gcc:latest container: gcc:latest
strategy: strategy:
matrix: matrix:
@@ -306,7 +306,7 @@ jobs:
run: cmake --build build --target ci_test_gcc_cxx${{ matrix.standard }} run: cmake --build build --target ci_test_gcc_cxx${{ matrix.standard }}
ci_test_standards_clang: ci_test_standards_clang:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
container: silkeh/clang:latest container: silkeh/clang:latest
strategy: strategy:
matrix: matrix:
@@ -330,7 +330,7 @@ jobs:
if: ${{ matrix.stdlib == 'libstdcxx' }} if: ${{ matrix.stdlib == 'libstdcxx' }}
ci_cuda_example: ci_cuda_example:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -356,7 +356,7 @@ jobs:
strategy: strategy:
matrix: matrix:
container: ['gcc:latest', 'silkeh/clang:latest'] container: ['gcc:latest', 'silkeh/clang:latest']
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
container: ${{ matrix.container }} container: ${{ matrix.container }}
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -383,7 +383,7 @@ jobs:
run: cmake --build build --target ci_module_cpp20 run: cmake --build build --target ci_module_cpp20
ci_icpc: ci_icpc:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
# Intel discontinued the classic icc/icpc compiler in oneAPI 2024.0; this is # Intel discontinued the classic icc/icpc compiler in oneAPI 2024.0; this is
# Intel's own last officially published image that still includes it. # Intel's own last officially published image that still includes it.
container: intel/oneapi-hpckit:2023.2.1-devel-ubuntu22.04 container: intel/oneapi-hpckit:2023.2.1-devel-ubuntu22.04
@@ -402,7 +402,7 @@ jobs:
run: cmake --build build --target ci_icpc run: cmake --build build --target ci_icpc
ci_icpx: ci_icpx:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
container: intel/oneapi-hpckit:latest container: intel/oneapi-hpckit:latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -416,7 +416,7 @@ jobs:
run: cmake --build build --target ci_icpx run: cmake --build build --target ci_icpx
ci_nvhpc: ci_nvhpc:
runs-on: ubuntu-26.04 runs-on: ubuntu-latest
container: nvcr.io/nvidia/nvhpc:25.5-devel-cuda12.9-ubuntu22.04 container: nvcr.io/nvidia/nvhpc:25.5-devel-cuda12.9-ubuntu22.04
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -35,6 +35,9 @@ Note that enabling this macro increases the size of every JSON value by two `std
overhead to parsing, copying JSON value objects, and the generation of error messages for exceptions. It also causes overhead to parsing, copying JSON value objects, and the generation of error messages for exceptions. It also causes
these values to be reported in those error messages. these values to be reported in those error messages.
If [`JSON_DIAGNOSTICS`](json_diagnostics.md) is also set, the message contains the path and the byte range, such as
`(/1) (bytes 4-8)`. An exception about the root value has no path and only the range, such as `(bytes 0-4)`.
## Default definition ## Default definition
The default value is `0` (position diagnostics are switched off). The default value is `0` (position diagnostics are switched off).
+10 -9
View File
@@ -198,18 +198,19 @@ class exception : public std::exception
// all other parent types (primitives) contribute no token // all other parent types (primitives) contribute no token
} }
if (tokens.empty()) // the root value has no path, but still has a byte range
std::string prefix;
if (!tokens.empty())
{ {
return ""; const auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
[](const std::string & a, const std::string & b)
{
return concat(a, '/', detail::escape(b));
});
prefix = concat('(', str, ") ");
} }
auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{}, return concat(prefix, get_byte_positions(leaf_element));
[](const std::string & a, const std::string & b)
{
return concat(a, '/', detail::escape(b));
});
return concat('(', str, ") ", get_byte_positions(leaf_element));
#else #else
return get_byte_positions(leaf_element); return get_byte_positions(leaf_element);
#endif #endif
@@ -467,13 +467,11 @@ class binary_reader
const std::size_t element_type_parse_position = chars_read; const std::size_t element_type_parse_position = chars_read;
// an array's elements are named "0", "1", ... in the wire format, // an array's elements are named "0", "1", ... in the wire format,
// and those names are not passed on: they are skipped, and only // and those names are not passed on: they are skipped unless
// checked if error_handler_t::strict must reject ill-formed UTF-8 // error_handler_t::strict must reject ill-formed UTF-8 in them
const bool is_object = container_stack.back().is_object; const bool is_object = container_stack.back().is_object;
key.clear(); key.clear();
if (JSON_HEDLEY_UNLIKELY(!(is_object || error_handler != error_handler_t::strict if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key, is_object || error_handler == error_handler_t::strict)))
? get_bson_cstr(key, is_object)
: check_bson_cstr(key, std::integral_constant<bool, bulk_scan> {}))))
{ {
return false; return false;
} }
@@ -497,7 +495,7 @@ class binary_reader
@param[in] store whether the characters are needed; if not (the names of @param[in] store whether the characters are needed; if not (the names of
an array's elements), they are skipped and @a result may an array's elements), they are skipped and @a result may
be left unchanged, and they are not checked against be left unchanged, and they are not checked against
@ref error_handler (@ref check_bson_cstr checks them) @ref error_handler
@return `true` if the \\x00-byte indicating the end of the string was @return `true` if the \\x00-byte indicating the end of the string was
encountered before the EOF; false` indicates an unexpected EOF. encountered before the EOF; false` indicates an unexpected EOF.
*/ */
@@ -556,42 +554,6 @@ class binary_reader
return true; return true;
} }
/*!
@brief skip a C-style string that error_handler_t::strict must check
The names of an array's elements are not passed on. On contiguous input,
a well-formed name is checked in place and skipped without being copied.
Otherwise (no \\x00-byte in the input that can be scanned, or ill-formed
UTF-8), it is read by @ref get_bson_cstr, which reports the error.
@param[in,out] result an empty string, which may receive the name
@return whether the name was read and is well-formed
*/
bool check_bson_cstr(string_t& result, std::true_type /*bulk*/)
{
const std::size_t remaining = ia.bulk_remaining();
if (remaining != 0)
{
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
const std::size_t length = find_zero_byte(data, remaining);
if (length != remaining && JSON_HEDLEY_LIKELY(valid_utf8_string(data, length)))
{
// consume the string and its \x00-byte, as get_bson_cstr does
ia.bulk_skip(length + 1);
chars_read += length + 1;
current = 0x00;
return true;
}
}
return get_bson_cstr(result);
}
/// input that is not contiguous: the name is read and checked byte by byte
bool check_bson_cstr(string_t& result, std::false_type /*bulk*/)
{
return get_bson_cstr(result);
}
/// input that is not contiguous: C-style strings are read byte by byte /// input that is not contiguous: C-style strings are read byte by byte
bool get_bson_cstr_bulk(string_t& /*result*/, const bool /*store*/, std::false_type /*bulk*/) const noexcept bool get_bson_cstr_bulk(string_t& /*result*/, const bool /*store*/, std::false_type /*bulk*/) const noexcept
{ {
+10 -12
View File
@@ -234,8 +234,7 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std:
/// the context basic_json puts in front of the message of an exception about /// the context basic_json puts in front of the message of an exception about
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then /// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only /// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (also for the root)
/// if there is a path)
/// ///
/// This and the throw helpers that take an error_site are templates, so that /// This and the throw helpers that take an error_site are templates, so that
/// the path and range code is instantiated only where an exception can be /// the path and range code is instantiated only where an exception can be
@@ -257,18 +256,17 @@ std::string diagnostics(const error_site site)
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
std::vector<std::string> tokens; std::vector<std::string> tokens;
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens); const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found || tokens.empty()) if (found && !tokens.empty())
{ {
// context is still empty; returning it keeps the copy elided (-Wnrvo) // the root value has no path, but still has a byte range
return context; std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
} }
std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
#endif #endif
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
std::size_t start = 0; std::size_t start = 0;
+63 -67
View File
@@ -543,10 +543,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
//////////////////////// ////////////////////////
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
// the type and value of a basic_json without its base class (defined
// below); the destruction helpers of json_value use it as a stand-in
struct data;
/*! /*!
@brief a JSON value @brief a JSON value
@@ -723,28 +719,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
explicit json_value(array_t* value) noexcept : array(value) {} explicit json_value(array_t* value) noexcept : array(value) {}
private: private:
// raw, allocation-free transfer of the type and value from src to // raw, allocation-free transfer of m_data from src to dst: no
// dst: no set_parents()/assert_invariant() (the former is // set_parents()/assert_invariant() (the former is O(#children) per
// O(#children) per call under JSON_DIAGNOSTICS, which would make the // call under JSON_DIAGNOSTICS, which would make the walk below
// walk below quadratic); dst takes ownership, src is left as // quadratic); dst takes ownership, src is left as value_t::null.
// value_t::null. static void take(basic_json& dst, basic_json& src) noexcept
static void take(data& dst, data& src) noexcept
{ {
dst.m_type = src.m_type; dst.m_data.m_type = src.m_data.m_type;
dst.m_value = src.m_value; dst.m_data.m_value = src.m_data.m_value;
src.m_type = value_t::null; src.m_data.m_type = value_t::null;
} }
// true if v is not an array/object, or is an already-empty one // true if v is not an array/object, or is an already-empty one
static bool has_no_children(const data& v) noexcept static bool has_no_children(const basic_json& v) noexcept
{ {
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
return v.m_value.array->empty(); return v.m_data.m_value.array->empty();
} }
if (v.m_type == value_t::object) if (v.m_data.m_type == value_t::object)
{ {
return v.m_value.object->empty(); return v.m_data.m_value.object->empty();
} }
return true; return true;
} }
@@ -753,21 +748,21 @@ private:
// its own, i.e. v can be freed in one go (its destructor then // its own, i.e. v can be freed in one go (its destructor then
// destroys scalars and empty containers only, so it recurses at most // destroys scalars and empty containers only, so it recurses at most
// one level deep); stops at the first structured child it meets // one level deep); stops at the first structured child it meets
static bool all_children_leaves(const data& v) noexcept static bool all_children_leaves(const basic_json& v) noexcept
{ {
// array_t only needs forward iterators (see custom_array_type) // array_t only needs forward iterators (see custom_array_type)
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
const auto& a = *v.m_value.array; const auto& a = *v.m_data.m_value.array;
return std::all_of(a.begin(), a.end(), [](const basic_json & el) return std::all_of(a.begin(), a.end(), [](const basic_json & el)
{ {
return has_no_children(el.m_data); return has_no_children(el);
}); });
} }
const auto& o = *v.m_value.object; const auto& o = *v.m_data.m_value.object;
return std::all_of(o.begin(), o.end(), [](const typename object_t::value_type & el) return std::all_of(o.begin(), o.end(), [](const typename object_t::value_type & el)
{ {
return has_no_children(el.second.m_data); return has_no_children(el.second);
}); });
} }
@@ -799,30 +794,30 @@ private:
// the child of a non-empty array/object v that the walk in // the child of a non-empty array/object v that the walk in
// destroy_container() continues with (see walk_child_it() above) // destroy_container() continues with (see walk_child_it() above)
static data& walk_child(data& v) static basic_json& walk_child(basic_json& v)
{ {
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
return v.m_value.array->back().m_data; return v.m_data.m_value.array->back();
} }
JSON_ASSERT(v.m_type == value_t::object); JSON_ASSERT(v.m_data.m_type == value_t::object);
return walk_child_it(*v.m_value.object)->second.m_data; return walk_child_it(*v.m_data.m_value.object)->second;
} }
// removes walk_child(v) from a non-empty array/object v; this never // removes walk_child(v) from a non-empty array/object v; this never
// allocates, and since it is only ever called when that child is a // allocates, and since it is only ever called when that child is a
// scalar or an already-empty array/object, destroying it never // scalar or an already-empty array/object, destroying it never
// recurses more than one level deep (see destroy() below) // recurses more than one level deep (see destroy() below)
static void pop_walk_child(data& v) static void pop_walk_child(basic_json& v)
{ {
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
v.m_value.array->pop_back(); v.m_data.m_value.array->pop_back();
} }
else else
{ {
JSON_ASSERT(v.m_type == value_t::object); JSON_ASSERT(v.m_data.m_type == value_t::object);
v.m_value.object->erase(walk_child_it(*v.m_value.object)); v.m_data.m_value.object->erase(walk_child_it(*v.m_data.m_value.object));
} }
} }
@@ -831,23 +826,23 @@ private:
// recurses at most one level deep); this is the same allocator-based // recurses at most one level deep); this is the same allocator-based
// free the old recursive implementation used, just factored out so // free the old recursive implementation used, just factored out so
// every level of the walk in destroy() can share it // every level of the walk in destroy() can share it
static void free_container(data& v) noexcept static void free_container(basic_json& v) noexcept
{ {
JSON_ASSERT(has_no_children(v) || all_children_leaves(v)); JSON_ASSERT(has_no_children(v) || all_children_leaves(v));
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
AllocatorType<array_t> alloc; AllocatorType<array_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_value.array); std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.array);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_value.array, 1); std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.array, 1);
} }
else else
{ {
JSON_ASSERT(v.m_type == value_t::object); JSON_ASSERT(v.m_data.m_type == value_t::object);
AllocatorType<object_t> alloc; AllocatorType<object_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_value.object); std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.object);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_value.object, 1); std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.object, 1);
} }
v.m_type = value_t::null; // avoid a double free if v is later destructed v.m_data.m_type = value_t::null; // avoid a double free if v is later destructed
} }
public: public:
@@ -878,16 +873,14 @@ public:
// This json_value is not itself a basic_json, so the top-level // This json_value is not itself a basic_json, so the top-level
// container is first moved into a local stand-in ("cur"); a // container is first moved into a local stand-in ("cur"); a
// default-constructed data has a null pointer in its m_value // default-constructed basic_json has a null pointer in its
// (see data::m_value's initializer), so swapping it with *this // m_value (see data::m_value's initializer), so swapping it
// leaves this union's own pointer null, and it is never looked // with *this leaves this union's own pointer null, and it is
// at or freed a second time. The stand-ins are data, not // never looked at or freed a second time.
// basic_json, so that destroying a value never constructs a basic_json cur;
// CustomBaseClass, whose constructor may allocate (#5995). cur.m_data.m_type = t;
data cur;
cur.m_type = t;
using std::swap; using std::swap;
swap(cur.m_value, *this); swap(cur.m_data.m_value, *this);
destroy_bounded(cur, 0); destroy_bounded(cur, 0);
} }
@@ -896,7 +889,7 @@ public:
// The first recursion_depth_limit() levels are destroyed by plain // The first recursion_depth_limit() levels are destroyed by plain
// recursion, which is the fastest way and cannot throw; anything // recursion, which is the fastest way and cannot throw; anything
// nested deeper is handed to destroy_walk(), which uses no call stack. // nested deeper is handed to destroy_walk(), which uses no call stack.
static void destroy_bounded(data& v, std::size_t depth) noexcept static void destroy_bounded(basic_json& v, std::size_t depth) noexcept
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{ {
@@ -904,23 +897,23 @@ public:
return; return;
} }
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
for (auto& child : *v.m_value.array) for (auto& child : *v.m_data.m_value.array)
{ {
if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object) if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object)
{ {
destroy_bounded(child.m_data, depth + 1); destroy_bounded(child, depth + 1);
} }
} }
} }
else else
{ {
for (auto& child : *v.m_value.object) for (auto& child : *v.m_data.m_value.object)
{ {
if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object) if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object)
{ {
destroy_bounded(child.second.m_data, depth + 1); destroy_bounded(child.second, depth + 1);
} }
} }
} }
@@ -947,15 +940,18 @@ public:
// array/object, which neither allocates nor recurses more // array/object, which neither allocates nor recurses more
// than one level deep. // than one level deep.
// //
// Like destroy_container(), the walk keeps the containers it // This json_value is not itself a basic_json, so the
// holds in data stand-ins (cur, prev, gp, tmp), which never // top-level container is first moved into a local stand-in
// construct a CustomBaseClass. // ("cur"); a default-constructed basic_json has a null
static void destroy_walk(data& top) noexcept // pointer in its m_value (see data::m_value's initializer),
// so swapping it with *this leaves this union's own pointer
// null, and it is never looked at or freed a second time.
static void destroy_walk(basic_json& top) noexcept
{ {
data cur; basic_json cur;
take(cur, top); take(cur, top);
data prev; // value_t::null: no parent basic_json prev; // value_t::null: no parent
// true right after cur was entered: its children have not been // true right after cur was entered: its children have not been
// looked at yet. If none of them has children of its own, cur is // looked at yet. If none of them has children of its own, cur is
@@ -969,7 +965,7 @@ public:
fresh = false; fresh = false;
if (has_no_children(cur) || (fresh_now && all_children_leaves(cur))) if (has_no_children(cur) || (fresh_now && all_children_leaves(cur)))
{ {
if (prev.m_type == value_t::null) if (prev.m_data.m_type == value_t::null)
{ {
free_container(cur); free_container(cur);
return; // back at the top with nothing left to do return; // back at the top with nothing left to do
@@ -978,7 +974,7 @@ public:
// ascend: detach the grandparent link from prev's // ascend: detach the grandparent link from prev's
// walk_child() slot, drop that (now null) slot, free cur // walk_child() slot, drop that (now null) slot, free cur
// (it is empty), then move up one level // (it is empty), then move up one level
data gp; basic_json gp;
take(gp, walk_child(prev)); take(gp, walk_child(prev));
pop_walk_child(prev); pop_walk_child(prev);
@@ -989,7 +985,7 @@ public:
continue; continue;
} }
data& cur_child_ref = walk_child(cur); basic_json& cur_child_ref = walk_child(cur);
if (has_no_children(cur_child_ref)) if (has_no_children(cur_child_ref))
{ {
@@ -1001,7 +997,7 @@ public:
// descend into the non-empty child, reversing the // descend into the non-empty child, reversing the
// link: its slot takes over prev, and the child becomes // link: its slot takes over prev, and the child becomes
// the new cur // the new cur
data tmp; basic_json tmp;
take(tmp, cur_child_ref); take(tmp, cur_child_ref);
take(cur_child_ref, prev); take(cur_child_ref, prev);
take(prev, cur); take(prev, cur);
+77 -118
View File
@@ -5695,18 +5695,19 @@ class exception : public std::exception
// all other parent types (primitives) contribute no token // all other parent types (primitives) contribute no token
} }
if (tokens.empty()) // the root value has no path, but still has a byte range
std::string prefix;
if (!tokens.empty())
{ {
return ""; const auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
[](const std::string & a, const std::string & b)
{
return concat(a, '/', detail::escape(b));
});
prefix = concat('(', str, ") ");
} }
auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{}, return concat(prefix, get_byte_positions(leaf_element));
[](const std::string & a, const std::string & b)
{
return concat(a, '/', detail::escape(b));
});
return concat('(', str, ") ", get_byte_positions(leaf_element));
#else #else
return get_byte_positions(leaf_element); return get_byte_positions(leaf_element);
#endif #endif
@@ -18474,13 +18475,11 @@ class binary_reader
const std::size_t element_type_parse_position = chars_read; const std::size_t element_type_parse_position = chars_read;
// an array's elements are named "0", "1", ... in the wire format, // an array's elements are named "0", "1", ... in the wire format,
// and those names are not passed on: they are skipped, and only // and those names are not passed on: they are skipped unless
// checked if error_handler_t::strict must reject ill-formed UTF-8 // error_handler_t::strict must reject ill-formed UTF-8 in them
const bool is_object = container_stack.back().is_object; const bool is_object = container_stack.back().is_object;
key.clear(); key.clear();
if (JSON_HEDLEY_UNLIKELY(!(is_object || error_handler != error_handler_t::strict if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key, is_object || error_handler == error_handler_t::strict)))
? get_bson_cstr(key, is_object)
: check_bson_cstr(key, std::integral_constant<bool, bulk_scan> {}))))
{ {
return false; return false;
} }
@@ -18504,7 +18503,7 @@ class binary_reader
@param[in] store whether the characters are needed; if not (the names of @param[in] store whether the characters are needed; if not (the names of
an array's elements), they are skipped and @a result may an array's elements), they are skipped and @a result may
be left unchanged, and they are not checked against be left unchanged, and they are not checked against
@ref error_handler (@ref check_bson_cstr checks them) @ref error_handler
@return `true` if the \\x00-byte indicating the end of the string was @return `true` if the \\x00-byte indicating the end of the string was
encountered before the EOF; false` indicates an unexpected EOF. encountered before the EOF; false` indicates an unexpected EOF.
*/ */
@@ -18563,42 +18562,6 @@ class binary_reader
return true; return true;
} }
/*!
@brief skip a C-style string that error_handler_t::strict must check
The names of an array's elements are not passed on. On contiguous input,
a well-formed name is checked in place and skipped without being copied.
Otherwise (no \\x00-byte in the input that can be scanned, or ill-formed
UTF-8), it is read by @ref get_bson_cstr, which reports the error.
@param[in,out] result an empty string, which may receive the name
@return whether the name was read and is well-formed
*/
bool check_bson_cstr(string_t& result, std::true_type /*bulk*/)
{
const std::size_t remaining = ia.bulk_remaining();
if (remaining != 0)
{
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
const std::size_t length = find_zero_byte(data, remaining);
if (length != remaining && JSON_HEDLEY_LIKELY(valid_utf8_string(data, length)))
{
// consume the string and its \x00-byte, as get_bson_cstr does
ia.bulk_skip(length + 1);
chars_read += length + 1;
current = 0x00;
return true;
}
}
return get_bson_cstr(result);
}
/// input that is not contiguous: the name is read and checked byte by byte
bool check_bson_cstr(string_t& result, std::false_type /*bulk*/)
{
return get_bson_cstr(result);
}
/// input that is not contiguous: C-style strings are read byte by byte /// input that is not contiguous: C-style strings are read byte by byte
bool get_bson_cstr_bulk(string_t& /*result*/, const bool /*store*/, std::false_type /*bulk*/) const noexcept bool get_bson_cstr_bulk(string_t& /*result*/, const bool /*store*/, std::false_type /*bulk*/) const noexcept
{ {
@@ -38373,10 +38336,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
//////////////////////// ////////////////////////
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
// the type and value of a basic_json without its base class (defined
// below); the destruction helpers of json_value use it as a stand-in
struct data;
/*! /*!
@brief a JSON value @brief a JSON value
@@ -38553,28 +38512,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
explicit json_value(array_t* value) noexcept : array(value) {} explicit json_value(array_t* value) noexcept : array(value) {}
private: private:
// raw, allocation-free transfer of the type and value from src to // raw, allocation-free transfer of m_data from src to dst: no
// dst: no set_parents()/assert_invariant() (the former is // set_parents()/assert_invariant() (the former is O(#children) per
// O(#children) per call under JSON_DIAGNOSTICS, which would make the // call under JSON_DIAGNOSTICS, which would make the walk below
// walk below quadratic); dst takes ownership, src is left as // quadratic); dst takes ownership, src is left as value_t::null.
// value_t::null. static void take(basic_json& dst, basic_json& src) noexcept
static void take(data& dst, data& src) noexcept
{ {
dst.m_type = src.m_type; dst.m_data.m_type = src.m_data.m_type;
dst.m_value = src.m_value; dst.m_data.m_value = src.m_data.m_value;
src.m_type = value_t::null; src.m_data.m_type = value_t::null;
} }
// true if v is not an array/object, or is an already-empty one // true if v is not an array/object, or is an already-empty one
static bool has_no_children(const data& v) noexcept static bool has_no_children(const basic_json& v) noexcept
{ {
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
return v.m_value.array->empty(); return v.m_data.m_value.array->empty();
} }
if (v.m_type == value_t::object) if (v.m_data.m_type == value_t::object)
{ {
return v.m_value.object->empty(); return v.m_data.m_value.object->empty();
} }
return true; return true;
} }
@@ -38583,21 +38541,21 @@ private:
// its own, i.e. v can be freed in one go (its destructor then // its own, i.e. v can be freed in one go (its destructor then
// destroys scalars and empty containers only, so it recurses at most // destroys scalars and empty containers only, so it recurses at most
// one level deep); stops at the first structured child it meets // one level deep); stops at the first structured child it meets
static bool all_children_leaves(const data& v) noexcept static bool all_children_leaves(const basic_json& v) noexcept
{ {
// array_t only needs forward iterators (see custom_array_type) // array_t only needs forward iterators (see custom_array_type)
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
const auto& a = *v.m_value.array; const auto& a = *v.m_data.m_value.array;
return std::all_of(a.begin(), a.end(), [](const basic_json & el) return std::all_of(a.begin(), a.end(), [](const basic_json & el)
{ {
return has_no_children(el.m_data); return has_no_children(el);
}); });
} }
const auto& o = *v.m_value.object; const auto& o = *v.m_data.m_value.object;
return std::all_of(o.begin(), o.end(), [](const typename object_t::value_type & el) return std::all_of(o.begin(), o.end(), [](const typename object_t::value_type & el)
{ {
return has_no_children(el.second.m_data); return has_no_children(el.second);
}); });
} }
@@ -38629,30 +38587,30 @@ private:
// the child of a non-empty array/object v that the walk in // the child of a non-empty array/object v that the walk in
// destroy_container() continues with (see walk_child_it() above) // destroy_container() continues with (see walk_child_it() above)
static data& walk_child(data& v) static basic_json& walk_child(basic_json& v)
{ {
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
return v.m_value.array->back().m_data; return v.m_data.m_value.array->back();
} }
JSON_ASSERT(v.m_type == value_t::object); JSON_ASSERT(v.m_data.m_type == value_t::object);
return walk_child_it(*v.m_value.object)->second.m_data; return walk_child_it(*v.m_data.m_value.object)->second;
} }
// removes walk_child(v) from a non-empty array/object v; this never // removes walk_child(v) from a non-empty array/object v; this never
// allocates, and since it is only ever called when that child is a // allocates, and since it is only ever called when that child is a
// scalar or an already-empty array/object, destroying it never // scalar or an already-empty array/object, destroying it never
// recurses more than one level deep (see destroy() below) // recurses more than one level deep (see destroy() below)
static void pop_walk_child(data& v) static void pop_walk_child(basic_json& v)
{ {
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
v.m_value.array->pop_back(); v.m_data.m_value.array->pop_back();
} }
else else
{ {
JSON_ASSERT(v.m_type == value_t::object); JSON_ASSERT(v.m_data.m_type == value_t::object);
v.m_value.object->erase(walk_child_it(*v.m_value.object)); v.m_data.m_value.object->erase(walk_child_it(*v.m_data.m_value.object));
} }
} }
@@ -38661,23 +38619,23 @@ private:
// recurses at most one level deep); this is the same allocator-based // recurses at most one level deep); this is the same allocator-based
// free the old recursive implementation used, just factored out so // free the old recursive implementation used, just factored out so
// every level of the walk in destroy() can share it // every level of the walk in destroy() can share it
static void free_container(data& v) noexcept static void free_container(basic_json& v) noexcept
{ {
JSON_ASSERT(has_no_children(v) || all_children_leaves(v)); JSON_ASSERT(has_no_children(v) || all_children_leaves(v));
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
AllocatorType<array_t> alloc; AllocatorType<array_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_value.array); std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.array);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_value.array, 1); std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.array, 1);
} }
else else
{ {
JSON_ASSERT(v.m_type == value_t::object); JSON_ASSERT(v.m_data.m_type == value_t::object);
AllocatorType<object_t> alloc; AllocatorType<object_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_value.object); std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.object);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_value.object, 1); std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.object, 1);
} }
v.m_type = value_t::null; // avoid a double free if v is later destructed v.m_data.m_type = value_t::null; // avoid a double free if v is later destructed
} }
public: public:
@@ -38708,16 +38666,14 @@ public:
// This json_value is not itself a basic_json, so the top-level // This json_value is not itself a basic_json, so the top-level
// container is first moved into a local stand-in ("cur"); a // container is first moved into a local stand-in ("cur"); a
// default-constructed data has a null pointer in its m_value // default-constructed basic_json has a null pointer in its
// (see data::m_value's initializer), so swapping it with *this // m_value (see data::m_value's initializer), so swapping it
// leaves this union's own pointer null, and it is never looked // with *this leaves this union's own pointer null, and it is
// at or freed a second time. The stand-ins are data, not // never looked at or freed a second time.
// basic_json, so that destroying a value never constructs a basic_json cur;
// CustomBaseClass, whose constructor may allocate (#5995). cur.m_data.m_type = t;
data cur;
cur.m_type = t;
using std::swap; using std::swap;
swap(cur.m_value, *this); swap(cur.m_data.m_value, *this);
destroy_bounded(cur, 0); destroy_bounded(cur, 0);
} }
@@ -38726,7 +38682,7 @@ public:
// The first recursion_depth_limit() levels are destroyed by plain // The first recursion_depth_limit() levels are destroyed by plain
// recursion, which is the fastest way and cannot throw; anything // recursion, which is the fastest way and cannot throw; anything
// nested deeper is handed to destroy_walk(), which uses no call stack. // nested deeper is handed to destroy_walk(), which uses no call stack.
static void destroy_bounded(data& v, std::size_t depth) noexcept static void destroy_bounded(basic_json& v, std::size_t depth) noexcept
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{ {
@@ -38734,23 +38690,23 @@ public:
return; return;
} }
if (v.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
for (auto& child : *v.m_value.array) for (auto& child : *v.m_data.m_value.array)
{ {
if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object) if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object)
{ {
destroy_bounded(child.m_data, depth + 1); destroy_bounded(child, depth + 1);
} }
} }
} }
else else
{ {
for (auto& child : *v.m_value.object) for (auto& child : *v.m_data.m_value.object)
{ {
if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object) if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object)
{ {
destroy_bounded(child.second.m_data, depth + 1); destroy_bounded(child.second, depth + 1);
} }
} }
} }
@@ -38777,15 +38733,18 @@ public:
// array/object, which neither allocates nor recurses more // array/object, which neither allocates nor recurses more
// than one level deep. // than one level deep.
// //
// Like destroy_container(), the walk keeps the containers it // This json_value is not itself a basic_json, so the
// holds in data stand-ins (cur, prev, gp, tmp), which never // top-level container is first moved into a local stand-in
// construct a CustomBaseClass. // ("cur"); a default-constructed basic_json has a null
static void destroy_walk(data& top) noexcept // pointer in its m_value (see data::m_value's initializer),
// so swapping it with *this leaves this union's own pointer
// null, and it is never looked at or freed a second time.
static void destroy_walk(basic_json& top) noexcept
{ {
data cur; basic_json cur;
take(cur, top); take(cur, top);
data prev; // value_t::null: no parent basic_json prev; // value_t::null: no parent
// true right after cur was entered: its children have not been // true right after cur was entered: its children have not been
// looked at yet. If none of them has children of its own, cur is // looked at yet. If none of them has children of its own, cur is
@@ -38799,7 +38758,7 @@ public:
fresh = false; fresh = false;
if (has_no_children(cur) || (fresh_now && all_children_leaves(cur))) if (has_no_children(cur) || (fresh_now && all_children_leaves(cur)))
{ {
if (prev.m_type == value_t::null) if (prev.m_data.m_type == value_t::null)
{ {
free_container(cur); free_container(cur);
return; // back at the top with nothing left to do return; // back at the top with nothing left to do
@@ -38808,7 +38767,7 @@ public:
// ascend: detach the grandparent link from prev's // ascend: detach the grandparent link from prev's
// walk_child() slot, drop that (now null) slot, free cur // walk_child() slot, drop that (now null) slot, free cur
// (it is empty), then move up one level // (it is empty), then move up one level
data gp; basic_json gp;
take(gp, walk_child(prev)); take(gp, walk_child(prev));
pop_walk_child(prev); pop_walk_child(prev);
@@ -38819,7 +38778,7 @@ public:
continue; continue;
} }
data& cur_child_ref = walk_child(cur); basic_json& cur_child_ref = walk_child(cur);
if (has_no_children(cur_child_ref)) if (has_no_children(cur_child_ref))
{ {
@@ -38831,7 +38790,7 @@ public:
// descend into the non-empty child, reversing the // descend into the non-empty child, reversing the
// link: its slot takes over prev, and the child becomes // link: its slot takes over prev, and the child becomes
// the new cur // the new cur
data tmp; basic_json tmp;
take(tmp, cur_child_ref); take(tmp, cur_child_ref);
take(cur_child_ref, prev); take(cur_child_ref, prev);
take(prev, cur); take(prev, cur);
+10 -12
View File
@@ -5076,8 +5076,7 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std:
/// the context basic_json puts in front of the message of an exception about /// the context basic_json puts in front of the message of an exception about
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then /// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only /// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (also for the root)
/// if there is a path)
/// ///
/// This and the throw helpers that take an error_site are templates, so that /// This and the throw helpers that take an error_site are templates, so that
/// the path and range code is instantiated only where an exception can be /// the path and range code is instantiated only where an exception can be
@@ -5099,18 +5098,17 @@ std::string diagnostics(const error_site site)
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
std::vector<std::string> tokens; std::vector<std::string> tokens;
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens); const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found || tokens.empty()) if (found && !tokens.empty())
{ {
// context is still empty; returning it keeps the copy elided (-Wnrvo) // the root value has no path, but still has a byte range
return context; std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
} }
std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
#endif #endif
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
std::size_t start = 0; std::size_t start = 0;
-38
View File
@@ -23,7 +23,6 @@ using nlohmann::json;
#include <cstdint> #include <cstdint>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
#include <list>
#include <sstream> #include <sstream>
#include <vector> #include <vector>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
@@ -1200,31 +1199,6 @@ TEST_CASE("BSON keys from contiguous and stream input")
from_stream = e.what(); from_stream = e.what();
} }
CHECK(from_vector == from_stream); CHECK(from_vector == from_stream);
// error_handler_t::strict checks the array element names in place on
// contiguous input and byte by byte otherwise; the names are
// well-formed, so both give the same value or error as above
std::string strict_from_vector;
std::string strict_from_list;
try
{
strict_from_vector = json::from_bson(input, true, true, json::error_handler_t::strict).dump();
}
catch (const json::parse_error& e)
{
strict_from_vector = e.what();
}
try
{
const std::list<std::uint8_t> list(input.begin(), input.end());
strict_from_list = json::from_bson(list.begin(), list.end(), true, true, json::error_handler_t::strict).dump();
}
catch (const json::parse_error& e)
{
strict_from_list = e.what();
}
CHECK(strict_from_vector == from_vector);
CHECK(strict_from_list == from_vector);
} }
} }
@@ -1253,18 +1227,6 @@ TEST_CASE("BSON array element names")
std::istringstream stream(std::string(reinterpret_cast<const char*>(bson.data()), bson.size())); std::istringstream stream(std::string(reinterpret_cast<const char*>(bson.data()), bson.size()));
CHECK(json::from_bson(stream) == expected); CHECK(json::from_bson(stream) == expected);
// input that is not contiguous reads the names byte by byte and must reject
// the same name
const std::list<std::uint8_t> list(bson.begin(), bson.end());
CHECK_THROWS_WITH_AS(_ = json::from_bson(list.begin(), list.end(), true, true, json::error_handler_t::strict), "[json.exception.parse_error.113] parse error at byte 14: syntax error while parsing BSON key: invalid string: ill-formed UTF-8 byte", json::parse_error&);
// without exceptions, the rejected name ends the parse
CHECK(json::from_bson(bson, true, false, json::error_handler_t::strict).is_discarded());
// well-formed names pass the check without being stored
const json valid = {{"a", {true, false, {{"k", json::array({1, 2})}}}}};
CHECK(json::from_bson(json::to_bson(valid), true, true, json::error_handler_t::strict) == valid);
} }
#endif #endif
-90
View File
@@ -7,7 +7,6 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#include <algorithm> #include <algorithm>
#include <memory>
#include <set> #include <set>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -496,92 +495,3 @@ TEST_CASE("Regression test for issue #5674 - copy construction must not require
CHECK(*c == 1); CHECK(*c == 1);
} }
} }
// counts the constructions and destructions of the base class (#5995)
struct counting_base
{
static std::size_t constructions;
static std::size_t destructions;
counting_base() noexcept
{
++constructions;
}
counting_base(const counting_base& /*unused*/) noexcept
{
++constructions;
}
counting_base(counting_base&& /*unused*/) noexcept
{
++constructions;
}
counting_base& operator=(const counting_base& /*unused*/) noexcept = default;
counting_base& operator=(counting_base&& /*unused*/) noexcept = default;
~counting_base()
{
++destructions;
}
static void reset() noexcept
{
constructions = 0;
destructions = 0;
}
};
std::size_t counting_base::constructions = 0;
std::size_t counting_base::destructions = 0;
using json_with_counting_base = nlohmann::json::with_base_class_t<counting_base>;
TEST_CASE("destroying a value does not construct the custom base class")
{
// regression test for #5995: destroying an array or object used a
// default-constructed basic_json as a stand-in, which constructed a base
// class that belonged to no value (and could allocate in the noexcept
// destructor); now every destroyed value destroys exactly its own base
// object, and none is constructed
const auto check_destroy = [](std::unique_ptr<json_with_counting_base> j, std::size_t values)
{
counting_base::reset();
j.reset();
CHECK(counting_base::constructions == 0);
CHECK(counting_base::destructions == values);
};
SECTION("scalar")
{
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(42)), 1);
}
SECTION("array and object")
{
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(json_with_counting_base::parse(R"({"a":[1,2,3],"b":{"c":true}})"))), 7);
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(json_with_counting_base::array())), 1);
}
SECTION("nested deeper than the recursion depth limit")
{
// alternating arrays and objects, so both kinds of container are
// walked by destroy_walk()
constexpr std::size_t depth = 1000;
std::unique_ptr<json_with_counting_base> j(new json_with_counting_base(json_with_counting_base::array({1, 2})));
for (std::size_t i = 0; i < depth; ++i)
{
json_with_counting_base outer = (i % 2 == 0) ? json_with_counting_base::object() : json_with_counting_base::array();
if (outer.is_object())
{
outer["x"] = std::move(*j);
outer["y"] = nullptr;
}
else
{
outer.push_back(std::move(*j));
outer.push_back(true);
}
*j = std::move(outer);
}
// the innermost array with its two elements, and two values per level
check_destroy(std::move(j), 3 + (2 * depth));
}
}
+22
View File
@@ -193,6 +193,28 @@ TEST_CASE("Better diagnostics with positions")
check_conversion(300); check_conversion(300);
} }
SECTION("exception about the root value (#5987)")
{
// the root has no path, but still has a byte range
const json root = json::parse("true");
const json nested = json::parse("[1, true]");
const json constructed = true;
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(root.get<std::string>(),
"[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean", json::type_error);
CHECK_THROWS_WITH_AS(nested[1].get<std::string>(),
"[json.exception.type_error.302] (/1) (bytes 4-8) type must be string, but is boolean", json::type_error);
#else
CHECK_THROWS_WITH_AS(root.get<std::string>(),
"[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean", json::type_error);
CHECK_THROWS_WITH_AS(nested[1].get<std::string>(),
"[json.exception.type_error.302] (bytes 4-8) type must be string, but is boolean", json::type_error);
#endif
// a value without positions gets no range and no stray whitespace
CHECK_THROWS_WITH_AS(constructed.get<std::string>(),
"[json.exception.type_error.302] type must be string, but is boolean", json::type_error);
}
SECTION("JSON patch add to primitive parent (#4292)") SECTION("JSON patch add to primitive parent (#4292)")
{ {
// the JSON Patch "add" target /foo/bar/baz has a string parent // the JSON Patch "add" target /foo/bar/baz has a string parent
+1 -1
View File
@@ -692,7 +692,7 @@ TEST_CASE("json_view builder: fraction digits")
check_same(number); check_same(number);
check_same("[" + number + "]"); check_same("[" + number + "]");
std::string pair; std::string pair;
pair.reserve((number.size() * 2) + 3); pair.reserve(number.size() * 2 + 3);
pair += '['; pair += '[';
pair += number; pair += number;
pair += ','; pair += ',';
+24 -8
View File
@@ -300,6 +300,27 @@ TEST_CASE("json_view materialize() source positions")
static_cast<void>(j.at("list").at(1).get<int>()); static_cast<void>(j.at("list").at(1).get<int>());
})); }));
} }
SECTION("exception about the root value (#5987)")
{
const json_document root = json_document::parse("true");
const json_document nested = json_document::parse("[1, true]");
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(root.root().get<std::string>());
}) == "[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean");
#if JSON_DIAGNOSTICS
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(nested.root()[1].get<std::string>());
}) == "[json.exception.type_error.302] (/1) (bytes 4-8) type must be string, but is boolean");
#else
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(nested.root()[1].get<std::string>());
}) == "[json.exception.type_error.302] (bytes 4-8) type must be string, but is boolean");
#endif
}
#endif #endif
SECTION("edited documents") SECTION("edited documents")
@@ -521,8 +542,7 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
std::string text = "7"; std::string text = "7";
for (int level = 0; level < 40; ++level) for (int level = 0; level < 40; ++level)
{ {
text.insert(0, R"([[1, [2]], )"); text = R"([[1, [2]], )" + text + R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
text += R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
} }
CAPTURE(text) CAPTURE(text)
const json j = json::parse(text); const json j = json::parse(text);
@@ -551,12 +571,8 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
expected_zz += "(bytes 12-35) key 'zz' not found"; expected_zz += "(bytes 12-35) key 'zz' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz); CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz);
// the root has no path, and with JSON_DIAGNOSTICS then no bytes either (as basic_json) // the root has no path, but still its bytes (#5987)
std::string expected_missing = "[json.exception.out_of_range.403] "; const std::string expected_missing = "[json.exception.out_of_range.403] (bytes 0-79) key 'missing' not found";
#if !JSON_DIAGNOSTICS
expected_missing += "(bytes 0-79) ";
#endif
expected_missing += "key 'missing' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing); CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing);
} }