Merge remote-tracking branch 'origin/develop' into claude/fix-issue-3989-db7e45

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-11-02 13:47:30 +01:00
commit 02d2fa8605
532 files changed
+41420 -10319

No files matched your search

+2 -3
View File
@@ -420,9 +420,9 @@ json_test_add_test_for(src/unit-file_input.cpp
)
# the json_view parser again with the portable string scanning instead of
# NEON/SSE2, and on x86-64 with the SSSE3 UTF-8 check (JSON_VIEW_USE_SSSE3)
# NEON/SSE2 (JSON_NO_SIMD), and on x86-64 compiled for SSSE3 (no CPU check)
json_test_set_test_options(test-json_view_builder_portable
COMPILE_DEFINITIONS JSON_VIEW_NO_SIMD
COMPILE_DEFINITIONS JSON_NO_SIMD
)
json_test_add_test_for(src/unit-json_view_builder.cpp
NAME test-json_view_builder_portable
@@ -430,7 +430,6 @@ json_test_add_test_for(src/unit-json_view_builder.cpp
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC)
json_test_set_test_options(test-json_view_builder_ssse3
COMPILE_DEFINITIONS JSON_VIEW_USE_SSSE3
COMPILE_OPTIONS -mssse3
)
json_test_add_test_for(src/unit-json_view_builder.cpp
+1 -3
View File
@@ -24,9 +24,7 @@ foreach(test default noversion)
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1
JSON_DISABLE_STREAM_BULK_SCAN=1
JSON_DISABLE_UNLOCKED_FILE_IO=1
JSON_NO_SIMD
JSON_VIEW_NO_SIMD
JSON_VIEW_USE_SSSE3)
JSON_NO_SIMD)
target_link_libraries(abi_config_${test}_all_tags PRIVATE abi_compat_main)
add_test(
NAME test-abi_config_${test}_all_tags
-8
View File
@@ -64,14 +64,6 @@ TEST_CASE("default namespace")
expected += "_ns";
#endif
#ifdef JSON_VIEW_NO_SIMD
expected += "_vns";
#endif
#ifdef JSON_VIEW_USE_SSSE3
expected += "_vssse3";
#endif
expected += "_v" STRINGIZE(NLOHMANN_JSON_VERSION_MAJOR);
expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_MINOR);
expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_PATCH) "::basic_json";
-8
View File
@@ -65,14 +65,6 @@ TEST_CASE("default namespace without version component")
expected += "_ns";
#endif
#ifdef JSON_VIEW_NO_SIMD
expected += "_vns";
#endif
#ifdef JSON_VIEW_USE_SSSE3
expected += "_vssse3";
#endif
expected += "::basic_json";
// fallback for Clang
+33 -1
View File
@@ -21,6 +21,10 @@ python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory>
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
- `--aligned` additionally builds and runs everything with all functions aligned to 64 bytes (`-mllvm
-align-all-functions=6` for Clang, `-falign-functions=64 -falign-loops=64` for GCC), and reports both result
tables as "default build" and "aligned build". Code layout alone can move a result by a few percent; a difference
that appears in only one of the two tables is a layout artifact. Off by default, which doubles the run time.
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
@@ -39,7 +43,7 @@ where `json_view` stands on another architecture; they are not meant for publica
## What is measured
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
`bench_view.cpp` runs these workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
JSON-RPC request (`rpc`):
| workload | what it does |
@@ -48,6 +52,7 @@ JSON-RPC request (`rpc`):
| traverse | parse, then visit every value, convert every number, touch every string and key |
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
| dump | serialize a parsed document (compact) |
| parse+dump | build a fresh document, then serialize it (compact), in one timed call |
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
@@ -59,6 +64,12 @@ An editable `json_document` edits in place; yyjson copies its immutable document
(`yyjson_doc_mut_copy`); Boost.JSON and `json::parse` build mutable DOMs; simdjson cannot edit a document. All
outputs are checked to describe the same value.
`bench_decode.cpp` measures decoding into structs whose `from_json()` comes from the `NLOHMANN_DEFINE_TYPE_*` macros
(twitter statuses, citm_catalog performances, and 20,000 synthetic records): `get<T>()` on a view, which reads the
view directly, against `materialize().get<T>()`, `json::parse` plus `get<T>()`, and `get<T>()` on an already parsed
`json` value. Only nlohmann/json takes part, as the other libraries have no comparable mapping; all engines are
checked to decode the same values.
Before anything is timed, all engines must accept each document and agree on the traversal: the number of values, the
bytes of all strings and keys, and the sum of all numbers. All engines run interleaved in every round, and the best
round is reported, as time and as a factor of the `json_view` time (below 1 means faster than `json_view`). Each timed
@@ -66,11 +77,32 @@ call follows an untimed call of the same engine: otherwise the engine after `jso
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
slower on citm_catalog traverse).
`json_view` is an index into the source text, not a tree of owning nodes. Its like-for-like peers are yyjson and
simdjson DOM (immutable, indexed, no per-node allocation). Boost.JSON and `json::parse` build mutable, owning DOMs and
are reference points for what that costs, not equals; simdjson On-Demand is lazy and forward-only, so it is the other
way round: it has no random access to pay for. All engines validate UTF-8 and syntax by default.
Number conversion differs: `json_view` converts integers while parsing but converts floating-point numbers lazily,
when they are read or dumped; yyjson (default flags) and simdjson DOM convert every number while parsing. The cost of
float conversion therefore appears in the traverse and dump rows for `json_view`, and in the parse row for the others.
For float-heavy documents (canada), the fair reading is a sum of parse and use: the `traverse` workload already
includes the parse, and `parse+dump` measures a fresh document and its dump in one timed call; the `dump` row alone
leaves the conversion in `json_view` and out of the others. Eager conversion at parse time was prototyped as an opt-in
and measured: canada traverse (with its parse) was 4-7 % faster, but parse+dump 11-30 % slower and parse 36-90 % slower. It was
dropped, so lazy conversion is the only mode.
`json_view` can also be edited: `json_editable_document` has the same read API, and the `(editable)` rows in
`bench_view` measure it on the same workloads. They are the fair counterpart of Boost.JSON and `json::parse`, which are
mutable too; the plain `json_view` rows are the counterpart of yyjson and simdjson DOM, which are not. `bench_edit` adds
the read-modify-write workloads. Read the tables with that in mind: an immutable-only library is not compared with an
editable `json_view` unless it says so.
The engines do not all offer the same features, which the numbers should be read with:
| engine | document | random access | editable | notes |
|---|---|---|---|---|
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
| `json_view` (editable) | index into the text plus edits | yes | yes, in place | `json_editable_document`; keeps member order and number spellings |
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
+312
View File
@@ -0,0 +1,312 @@
// __ _____ _____ _____
// __| | __| | | | 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
// Decoding into structs: the same types, with from_json() generated by the
// NLOHMANN_DEFINE_TYPE_* macros, read three ways. Only nlohmann/json takes
// part; the other libraries have no comparable mapping.
//
// view json_document::parse, then get<T>() on the view (from_json reads the view)
// materialize json_document::parse, then materialize().get<T>() (a json value first)
// json::parse json::parse, then get<T>()
// json::get get<T>() on an already parsed json value (the decoding alone)
//
// Documents: twitter (the statuses with their users and hashtags), citm_catalog
// (the performances with their seat categories), and a synthetic document of
// 20,000 nested records.
//
// Build: see README.md (same flags as bench_view.cpp).
#include <nlohmann/json_view.hpp>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <functional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
using nlohmann::json;
using nlohmann::json_document;
static volatile std::size_t g_sink;
namespace twitter
{
struct hashtag
{
std::string text;
std::vector<int> indices;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(hashtag, text, indices)
struct entities
{
std::vector<hashtag> hashtags;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(entities, hashtags)
struct user
{
std::uint64_t id = 0;
std::string screen_name;
std::string description;
std::int64_t followers_count = 0;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(user, id, screen_name, description, followers_count)
struct status
{
std::uint64_t id = 0;
std::string text;
user user;
entities entities;
std::int64_t retweet_count = 0;
std::int64_t favorite_count = 0;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(status, id, text, user, entities, retweet_count, favorite_count)
struct document
{
std::vector<status> statuses;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(document, statuses)
std::size_t digest(const document& d)
{
std::size_t n = 0;
for (const auto& s : d.statuses)
{
n += s.text.size() + s.user.screen_name.size() + s.entities.hashtags.size() + static_cast<std::size_t>(s.retweet_count);
}
return n;
}
} // namespace twitter
namespace citm
{
struct area
{
std::int64_t areaId = 0;
std::vector<std::int64_t> blockIds;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(area, areaId, blockIds)
struct seat_category
{
std::vector<area> areas;
std::int64_t seatCategoryId = 0;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(seat_category, areas, seatCategoryId)
struct performance
{
std::int64_t id = 0;
std::int64_t eventId = 0;
std::vector<seat_category> seatCategories;
std::int64_t start = 0;
std::string venueCode;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(performance, id, eventId, seatCategories, start, venueCode)
struct document
{
std::vector<performance> performances;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(document, performances)
std::size_t digest(const document& d)
{
std::size_t n = 0;
for (const auto& p : d.performances)
{
n += p.seatCategories.size() + p.venueCode.size() + static_cast<std::size_t>(p.start % 1000);
for (const auto& c : p.seatCategories)
{
n += c.areas.size();
}
}
return n;
}
} // namespace citm
namespace synthetic
{
struct position
{
double x = 0;
double y = 0;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(position, x, y)
struct child
{
std::int64_t id = 0;
std::string label;
bool active = false;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(child, id, label, active)
struct record
{
std::int64_t id = 0;
std::string name;
std::vector<std::string> tags;
position pos;
std::vector<child> children;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(record, id, name, tags, pos, children)
struct document
{
std::vector<record> records;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(document, records)
std::string make_text(std::size_t n)
{
std::string s = "{\"records\":[";
for (std::size_t i = 0; i < n; ++i)
{
const std::string id = std::to_string(i);
s += (i == 0 ? "" : ",");
s += "{\"id\":" + id + ",\"name\":\"record number " + id + "\",\"tags\":[\"alpha\",\"beta\",\"gamma\"],";
s += "\"pos\":{\"x\":" + id + ".25,\"y\":-" + id + ".5},\"children\":[";
for (std::size_t k = 0; k < 3; ++k)
{
s += (k == 0 ? "" : ",");
s += "{\"id\":" + std::to_string(i * 3 + k) + ",\"label\":\"child " + std::to_string(k) + "\",\"active\":" + (k % 2 == 0 ? "true" : "false") + "}";
}
s += "]}";
}
s += "]}";
return s;
}
std::size_t digest(const document& d)
{
std::size_t n = 0;
for (const auto& r : d.records)
{
n += r.name.size() + r.tags.size() + r.children.size() + static_cast<std::size_t>(r.pos.x);
}
return n;
}
} // namespace synthetic
namespace
{
std::string read_file(const std::string& p)
{
std::ifstream f(p, std::ios::binary);
if (!f)
{
std::fprintf(stderr, "cannot open %s\n", p.c_str());
std::exit(1);
}
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
double time_ns(const std::function<void()>& f)
{
const auto t0 = std::chrono::steady_clock::now();
f();
const auto t1 = std::chrono::steady_clock::now();
return static_cast<double>(std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count());
}
struct engine
{
std::string name;
std::function<std::size_t()> run;
};
template<typename Document>
std::vector<engine> engines(const std::string& text, const json& parsed)
{
return
{
{"view", [&text] { const json_document d = json_document::parse(text); return digest(d.root().get<Document>()); }},
{"materialize", [&text] { const json_document d = json_document::parse(text); return digest(d.root().materialize().get<Document>()); }},
{"json::parse", [&text] { return digest(json::parse(text).get<Document>()); }},
{"json::get", [&parsed] { return digest(parsed.get<Document>()); }},
};
}
} // namespace
int main(int argc, char** argv)
{
if (argc < 2)
{
std::fprintf(stderr, "usage: %s <json_test_data directory> [rounds]\n", argv[0]);
return 1;
}
const std::string dir = argv[1];
const int rounds = argc > 2 ? std::max(1, std::atoi(argv[2])) : 30;
struct document_case
{
std::string name;
std::string text;
json parsed;
std::vector<engine> engines;
};
std::vector<document_case> cases;
cases.push_back({"twitter", read_file(dir + "/nativejson-benchmark/twitter.json"), json(), {}});
cases.push_back({"citm_catalog", read_file(dir + "/nativejson-benchmark/citm_catalog.json"), json(), {}});
cases.push_back({"synthetic", synthetic::make_text(20000), json(), {}});
for (auto& c : cases)
{
c.parsed = json::parse(c.text);
}
cases[0].engines = engines<twitter::document>(cases[0].text, cases[0].parsed);
cases[1].engines = engines<citm::document>(cases[1].text, cases[1].parsed);
cases[2].engines = engines<synthetic::document>(cases[2].text, cases[2].parsed);
std::FILE* csv = std::fopen("bench_decode.csv", "w");
if (csv == nullptr)
{
std::perror("bench_decode.csv");
return 1;
}
std::fprintf(csv, "doc,bytes,engine,ns\n");
for (const auto& c : cases)
{
// all engines must decode the same values
const std::size_t expected = c.engines[0].run();
bool ok = true;
for (const auto& e : c.engines)
{
ok = ok && e.run() == expected;
}
std::vector<double> best(c.engines.size(), 1e300);
for (int r = 0; r < rounds; ++r)
{
for (std::size_t k = 0; k < c.engines.size(); ++k)
{
g_sink = c.engines[k].run(); // untimed: see bench_view.cpp
best[k] = std::min(best[k], time_ns([&] { g_sink = c.engines[k].run(); }));
}
}
std::printf("%-13s %-7s %s", c.name.c_str(), "decode", ok ? "" : "[MISMATCH] ");
for (std::size_t k = 0; k < c.engines.size(); ++k)
{
const double us = best[k] / 1000.0;
std::printf(" %s %.*fus (%.2fx)", c.engines[k].name.c_str(), us < 10 ? 3 : (us < 1000 ? 1 : 0), us, best[k] / best[0]);
std::fprintf(csv, "%s,%zu,%s,%.1f\n", c.name.c_str(), c.text.size(), c.engines[k].name.c_str(), best[k]);
}
std::printf("\n");
}
std::fclose(csv);
return 0;
}
+92 -24
View File
@@ -8,8 +8,9 @@
// Same-feature-set benchmark: read-only JSON documents with random access.
//
// json_view nlohmann/json_view.hpp (fresh document per parse / reused)
// yyjson yyjson_read(): immutable document, random access
// json_view nlohmann/json_view.hpp (fresh document per parse / reused;
// "(editable)": json_editable_document, same read API)
// yyjson yyjson_read(): immutable document, random access
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
// per parse): immutable, random access
// references (different feature sets):
@@ -19,7 +20,11 @@
//
// Workloads: parse (build + free), traverse (visit everything, convert every
// number, touch every string and key), select (a few fields per document),
// dump (compact serialization of the parsed document).
// dump (compact serialization of the parsed document), and parse+dump (a fresh
// document, then its dump, in one timed call). json_view converts floating-point
// numbers lazily (when read or dumped), yyjson and simdjson DOM while parsing:
// for float-heavy documents, traverse (which includes the parse) and parse+dump
// are the fair reading of the parse and dump rows.
// All engines run interleaved in every round, each timed call after an untimed
// one of the same engine; the best round is reported.
#include <nlohmann/json_view.hpp>
@@ -43,6 +48,8 @@
using nlohmann::json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;
using nlohmann::json_view;
static volatile double g_sink;
@@ -55,7 +62,8 @@ struct stats
// ---------------- traversal ----------------
static void walk(json_view v, stats& st)
template<class V>
static void walk_view(V v, stats& st)
{
++st.nodes;
switch (v.type())
@@ -64,32 +72,42 @@ static void walk(json_view v, stats& st)
for (auto it = v.begin(); it != v.end(); ++it)
{
st.str += it.key().size();
walk(*it, st);
walk_view<V>(*it, st);
}
break;
case json::value_t::array:
for (const json_view e : v)
for (const V e : v)
{
walk(e, st);
walk_view<V>(e, st);
}
break;
case json::value_t::string:
st.str += v.get_string().size();
break;
case json::value_t::number_integer:
st.num += static_cast<double>(v.get<std::int64_t>());
st.num += static_cast<double>(v.template get<std::int64_t>());
break;
case json::value_t::number_unsigned:
st.num += static_cast<double>(v.get<std::uint64_t>());
st.num += static_cast<double>(v.template get<std::uint64_t>());
break;
case json::value_t::number_float:
st.num += v.get<double>();
st.num += v.template get<double>();
break;
default:
break;
}
}
static void walk(json_view v, stats& st)
{
walk_view<json_view>(v, st);
}
static void walk(json_editable_view v, stats& st)
{
walk_view<json_editable_view>(v, st);
}
static void walk(const json& j, stats& st)
{
++st.nodes;
@@ -300,20 +318,21 @@ static void walk(const boost::json::value& v, stats& st)
// status (one tweet): id, user.screen_name, retweet_count
// rpc: method, params.minuend, id
static double pick(const std::string& name, json_view r)
template<class V>
static double pick_view(const std::string& name, V r)
{
double acc = 0;
if (name == "twitter" || name == "status")
{
auto one = [&](json_view s)
auto one = [&](V s)
{
acc += static_cast<double>(s["id"].get<std::uint64_t>());
acc += static_cast<double>(s["id"].template get<std::uint64_t>());
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
};
if (name == "twitter")
{
for (const json_view s : r["statuses"])
for (const V s : r["statuses"])
{
one(s);
}
@@ -325,20 +344,20 @@ static double pick(const std::string& name, json_view r)
}
else if (name == "citm_catalog")
{
for (const json_view p : r["performances"])
for (const V p : r["performances"])
{
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
}
acc += static_cast<double>(r["events"].size());
}
else if (name == "canada")
{
const json_view g = r["features"][0]["geometry"];
const V g = r["features"][0]["geometry"];
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
}
else if (name == "jeopardy")
{
for (const json_view q : r)
for (const V q : r)
{
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
acc += static_cast<double>(q["category"].get_string().size());
@@ -347,11 +366,21 @@ static double pick(const std::string& name, json_view r)
else if (name == "rpc")
{
acc += static_cast<double>(r["method"].get_string().size());
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
}
return acc;
}
static double pick(const std::string& name, json_view r)
{
return pick_view<json_view>(name, r);
}
static double pick(const std::string& name, json_editable_view r)
{
return pick_view<json_editable_view>(name, r);
}
static double pick(const std::string& name, const json& r)
{
double acc = 0;
@@ -634,15 +663,35 @@ int main(int argc, char** argv)
walk(json::parse(dc.text), a);
auto d = json_document::parse(dc.text);
walk(d.root(), b);
auto ed = json_editable_document::parse(dc.text);
stats e;
walk(ed.root(), e);
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
walk(yyjson_doc_get_root(y), c);
simdjson::dom::parser p;
walk(p.parse(dc.text).value(), dd);
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
const bool ok = a.nodes == b.nodes && a.nodes == e.nodes && a.str == e.str && std::fabs(a.num - e.num) <= 1e-9 * std::fabs(a.num) && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH");
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pe = pick(dc.name, ed.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pe && pa == pc && pa == pd) ? "OK" : "MISMATCH");
yyjson_doc_free(y);
// the dumps of the engines may format floats differently, so only: every dump is non-empty, and the
// dump of json_view (also editable) and of json::parse parse back to the same number of nodes
const std::string dj = json::parse(dc.text).dump();
const std::string dv = d.root().dump();
const std::string de = ed.root().dump();
std::size_t yn = 0;
yyjson_doc* y2 = yyjson_read(dc.text.data(), dc.text.size(), 0);
char* yo = yyjson_write(y2, 0, &yn);
const bool dumps_ok = !dj.empty() && !dv.empty() && !de.empty() && yo != nullptr && yn > 0 && !simdjson::to_string(p.parse(dc.text).value()).empty();
std::free(yo);
yyjson_doc_free(y2);
stats sj_nodes, sv_nodes, se_nodes;
walk(json::parse(dj), sj_nodes);
walk(json::parse(dv), sv_nodes);
walk(json::parse(de), se_nodes);
std::printf("check %-13s dump %s\n", dc.name.c_str(), (dumps_ok && sj_nodes.nodes == a.nodes && sv_nodes.nodes == a.nodes && se_nodes.nodes == a.nodes) ? "OK" : "MISMATCH");
}
std::FILE* csv = std::fopen("bench_view.csv", "w");
@@ -669,6 +718,7 @@ int main(int argc, char** argv)
workloads.push_back({"parse", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = static_cast<double>(d.root().size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
@@ -680,6 +730,7 @@ int main(int argc, char** argv)
workloads.push_back({"traverse", {
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
@@ -691,15 +742,31 @@ int main(int argc, char** argv)
workloads.push_back({"select", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = pick(name, d.root()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
}});
// pipeline: a fresh document, then a dump of it, in one timed call
// (simdjson DOM reuses its parser, as in the parse row); the traverse
// workload above is already parse+traverse
workloads.push_back({"parse+dump", {
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
#if JSON_VIEW_BENCH_BOOST
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
#endif
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
}});
{
// serialization of an already parsed document
static json_document vd;
vd.read(s);
static json_editable_document ed = json_editable_document::parse(s);
ed = json_editable_document::parse(s);
static yyjson_doc* yd = nullptr;
if (yd)
{
@@ -713,6 +780,7 @@ int main(int argc, char** argv)
jd = json::parse(s);
workloads.push_back({"dump", {
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"json_view (editable)", [&] { std::string o = ed.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
@@ -740,7 +808,7 @@ int main(int argc, char** argv)
}
}
const double ref = best[0];
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
std::printf("%-13s %-14s", dc.name.c_str(), wl.first.c_str());
for (std::size_t k = 0; k < wl.second.size(); ++k)
{
const double us = best[k] / 1e3;
+48 -24
View File
@@ -9,7 +9,7 @@
"""Compare json_view with yyjson, simdjson, Boost.JSON, and json::parse.
Builds bench_view.cpp, bench_corpus.cpp, and bench_edit.cpp against the include/ directory of
Builds bench_view.cpp, bench_corpus.cpp, bench_edit.cpp, and bench_decode.cpp against the include/ directory of
this checkout, runs them, and writes the results with everything needed to
reproduce them (date, commit, CPU, OS, compiler, library versions, flags) to
results/<date>-<host>.md and .csv next to this script.
@@ -239,6 +239,8 @@ def main():
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
ap.add_argument('--aligned', action='store_true',
help='also build and run with all functions (and loops) aligned, to make code layout artifacts visible')
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
@@ -282,30 +284,46 @@ def main():
+ ['-I' + d for d in lib.include])
objects.append(obj)
binaries = {}
for bench in ['bench_view', 'bench_corpus', 'bench_edit']:
exe = os.path.join(args.build_dir, bench)
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
binaries[bench] = exe
cxx_version = output(cxx + ['--version'])
is_clang = 'clang' in cxx_version.lower()
# alignment flags: functions and (gcc) loops on 64-byte boundaries
align_flags = ['-mllvm', '-align-all-functions=6'] if is_clang else ['-falign-functions=64', '-falign-loops=64']
# the builds: the default one and, with --aligned, one with aligned code
variants = [('default build', args.build_dir, [])]
if args.aligned:
variants.append(('aligned build', os.path.join(args.build_dir, 'aligned'), align_flags))
# run: bench_view on its documents, bench_corpus on those and the given files
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
outputs = {}
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir,
capture_output=True, text=True).stdout
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir,
capture_output=True, text=True).stdout
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
capture_output=True, text=True).stdout
for name, text in outputs.items():
print(text)
# label -> {benchmark: output}
all_outputs = {}
for label, build_dir, extra in variants:
os.makedirs(build_dir, exist_ok=True)
binaries = {}
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
exe = os.path.join(build_dir, bench)
run(cxx + flags + extra + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
binaries[bench] = exe
# run: bench_view on its documents, bench_corpus on those and the given files
outputs = {}
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))],
cwd=build_dir, capture_output=True, text=True).stdout
for name, text in outputs.items():
print(text)
all_outputs[label] = outputs
# results with their metadata
now = datetime.datetime.now()
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
os.makedirs(os.path.dirname(stem), exist_ok=True)
cxx_version = output(cxx + ['--version'])
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
meta = [
('date', f'{now:%Y-%m-%d %H:%M}'),
@@ -314,6 +332,7 @@ def main():
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
('compiler', compiler_version),
('flags', ' '.join(flags)),
('aligned results', ('included (flags: ' + ' '.join(align_flags) + ')') if args.aligned else 'not included'),
('yyjson', libs['yyjson'].version),
('simdjson', libs['simdjson'].version),
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
@@ -326,15 +345,20 @@ def main():
f.write('| | |\n|---|---|\n')
for key, value in meta:
f.write(f'| {key} | {value} |\n')
for name, text in outputs.items():
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n')
for label, outputs in all_outputs.items():
# (without --aligned there is only the default build, and no extra heading level)
prefix = f'{label}: ' if args.aligned else ''
for name, text in outputs.items():
f.write(f'\n## {prefix}{name}\n\n```\n{text.rstrip()}\n```\n')
with open(stem + '.csv', 'w', encoding='utf-8') as out:
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
for name in ['bench_view', 'bench_corpus', 'bench_edit']:
path = os.path.join(args.build_dir, name + '.csv')
if os.path.isfile(path):
with open(path, encoding='utf-8') as f:
out.write(f'# {name}\n' + f.read())
for label, build_dir, _ in variants:
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
path = os.path.join(build_dir, name + '.csv')
if os.path.isfile(path):
with open(path, encoding='utf-8') as f:
# (the default build keeps its plain section names)
out.write(f'# {name}' + (f' ({label})' if args.aligned else '') + '\n' + f.read())
print(f'results: {stem}.md, {stem}.csv')
+22
View File
@@ -15,6 +15,7 @@
#include <nlohmann/json_view.hpp>
#include <algorithm> // any_of
#include <cstddef> // size_t
#include <functional> // function
#include <random> // mt19937
#include <string> // string
@@ -88,6 +89,27 @@ inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v)
});
}
// the message of an exception about the value at path (a JSON pointer, "" for
// the root) whose source bytes are [start, end): the context basic_json adds
// with JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS, if enabled, then what
inline std::string message(const std::string& id, const std::string& path, std::size_t start, std::size_t end, const std::string& what)
{
#if JSON_DIAGNOSTIC_POSITIONS
const std::string positions = "(bytes " + std::to_string(start) + "-" + std::to_string(end) + ") ";
#else
const std::string positions;
static_cast<void>(start);
static_cast<void>(end);
#endif
#if JSON_DIAGNOSTICS
const std::string context = path.empty() ? "" : "(" + path + ") " + positions;
#else
const std::string& context = positions;
static_cast<void>(path);
#endif
return "[json.exception." + id + "] " + context + what;
}
#if !defined(JSON_NOEXCEPTION)
// the exception a call throws, or "" if it throws none
inline std::string exception_of_call(const std::function<void()>& f)
+17
View File
@@ -373,6 +373,23 @@ TEST_CASE("alternative string type")
CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar")));
}
SECTION("non-const operator[](json_pointer)")
{
// creating values must compile without string_t::begin()
alt_json j = alt_json::parse(R"({"a":{"b":[1]},"c":null})");
const alt_json before = j;
j[alt_json::json_pointer("/a/b/3/x/-/y")] = 1; // pad, append, create
j[alt_json::json_pointer("/a/z")] = 2; // insert a key
j[alt_json::json_pointer("/c/0/q")] = 3; // convert null
j[alt_json::json_pointer("/a/b/-")] = 4; // append
CHECK(j.dump() == R"({"a":{"b":[1,null,null,{"x":[{"y":1}]},4],"z":2},"c":[{"q":3}]})");
// a failing access leaves the value unchanged
j = before;
CHECK_THROWS_AS(j[alt_json::json_pointer("/a/n/99999999999999999999999")], alt_json::out_of_range&);
CHECK(j == before);
}
SECTION("operator/(std::size_t)")
{
// json_pointer::operator/=(std::size_t) must compile without string_t
+203 -2
View File
@@ -26,11 +26,14 @@ using nlohmann::ordered_json;
#include <limits>
#include <iostream>
#include <fstream>
#include <list>
#include <set>
#include "make_test_data_available.hpp"
#include "test_utils.hpp"
#include "sax_countdown.hpp"
#include "sax_event_loggers.hpp"
using utils::SaxCountdown;
using utils::SaxEventLogger;
// trait_test_arg and the "value_in_range_of trait" TEST_CASE_TEMPLATE_DEFINE
@@ -2916,6 +2919,35 @@ TEST_CASE("BJData")
CHECK(json::from_bjdata(out_single_ok) == json({{"_ArrayType_", "single"}, {"_ArraySize_", {2, 1}}, {"_ArrayData_", {1.5f, -1.5f}}}));
}
SECTION("ndarray with _ArrayType_ \"char\" is limited to 0..127")
{
// both BJData drafts limit a char to 0..127; larger values must not
// be written as 'C' elements (the reader rejects them), so such an
// object stays a plain object that round-trips
for (const auto version :
{
json::bjdata_version_t::draft2, json::bjdata_version_t::draft3
})
{
json const j_ok = json({{"_ArrayType_", "char"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {0, 97, 98, 127}}});
const auto out_ok = json::to_bjdata(j_ok, true, true, version);
CHECK(out_ok == std::vector<uint8_t>({'[', '$', 'C', '#', '[', '$', 'i', '#', 'i', 2, 2, 2, 0, 97, 98, 127}));
CHECK(json::from_bjdata(out_ok) == j_ok);
for (const int bad :
{
128, 200, 255, 256, -1
})
{
CAPTURE(bad)
json const j_bad = json({{"_ArrayType_", "char"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {97, 98, 99, bad}}});
const auto out_bad = json::to_bjdata(j_bad, true, true, version);
CHECK(out_bad.at(0) == '{');
CHECK(json::from_bjdata(out_bad) == j_bad);
}
}
}
SECTION("ndarray annotation keys are read back in the documented order")
{
// from_bjdata() must emit the annotation object's keys in the order
@@ -3134,7 +3166,7 @@ TEST_CASE("BJData")
std::vector<uint8_t> const v1 = {'[', '#', 'i', 0xF1};
std::vector<uint8_t> const v2 = {'[', '$', 'I', '#', 'i', 0xF2};
std::vector<uint8_t> const v3 = {'[', '$', 'I', '#', '[', 'i', 0xF4, 'i', 0x02, ']'};
std::vector<uint8_t> const v4 = {'[', '$', 0xF6, '#', 'i', 0xF7};
std::vector<uint8_t> const v4 = {'[', '$', 'l', '#', 'i', 0xF7};
std::vector<uint8_t> const v5 = {'[', '$', 'I', '#', '[', 'i', 0xF5, 'i', 0xF1, ']'};
std::vector<uint8_t> const v6 = {'[', '#', '[', 'i', 0xF3, 'i', 0x02, ']'};
@@ -3324,6 +3356,44 @@ TEST_CASE("BJData")
}
}
SECTION("ndarray of half-precision floats can be read")
{
// the 'h' element type is read as _ArrayType_ "half"
json _;
const std::vector<uint8_t> half = {'[', '$', 'h', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 0x00, 0x3C, 0x00, 0x40, 0x00, 0x42, 0x00, 0x44};
const json j_half = json::parse(R"({"_ArrayType_":"half","_ArraySize_":[2,2],"_ArrayData_":[1.0,2.0,3.0,4.0]})");
CHECK(json::from_bjdata(half) == j_half);
// the writer does not pack "half": it writes a plain object that reads back equal
CHECK(json::from_bjdata(json::to_bjdata(j_half)) == j_half);
CHECK(json::from_bjdata(json::to_bjdata(j_half, true, true, json::bjdata_version_t::draft3)) == j_half);
// truncated half data
CHECK_THROWS_AS(_ = json::from_bjdata(std::vector<uint8_t>(half.begin(), half.end() - 1)), json::parse_error&);
}
SECTION("ndarray of char rejects values above 127")
{
json _;
const std::vector<uint8_t> ok = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c', 0x7F};
CHECK(json::from_bjdata(ok) == json::parse(R"({"_ArrayType_":"char","_ArraySize_":[2,2],"_ArrayData_":[97,98,99,127]})"));
std::vector<uint8_t> bad = ok;
bad.back() = 0xC8;
CHECK_THROWS_AS(_ = json::from_bjdata(bad), json::parse_error&);
CHECK(json::from_bjdata(bad, true, false).is_discarded());
// the same restriction holds for 1-D arrays
CHECK_THROWS_AS(_ = json::from_bjdata(std::vector<uint8_t>({'[', '$', 'C', '#', 'i', 1, 0xC8})), json::parse_error&);
// truncated input
CHECK_THROWS_AS(_ = json::from_bjdata(std::vector<uint8_t>(ok.begin(), ok.end() - 1)), json::parse_error&);
// 'B' (byte) elements may use the full range
const std::vector<uint8_t> byte_nd = {'[', '$', 'B', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 1, 2, 3, 0xC8};
CHECK(json::from_bjdata(byte_nd) == json::parse(R"({"_ArrayType_":"byte","_ArraySize_":[2,2],"_ArrayData_":[1,2,3,200]})"));
}
SECTION("do not accept NTFZ markers in ndarray optimized type (with count)")
{
json _;
@@ -3455,7 +3525,7 @@ TEST_CASE("BJData")
CHECK(json::from_bjdata(v0, true, false).is_discarded());
std::vector<uint8_t> const vi = {'[', '$', '#'};
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vi), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BJData value: unexpected end of input", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vi), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BJData type: marker 0x23 is not a permitted optimized array type", json::parse_error&);
CHECK(json::from_bjdata(vi, true, false).is_discarded());
std::vector<uint8_t> const vU = {'[', '$', 'U'};
@@ -4622,3 +4692,134 @@ TEST_CASE("BJData large strings and binaries (chunked reader)")
}
}
}
TEST_CASE("BJData optimized container type marker is validated for count 0")
{
// the '$' type marker must be a legal marker even if no element follows
SECTION("invalid marker")
{
// 'Z', 'T', 'F', 'N', 'S', 'H', '[' and '{' are excluded in BJData
for (const char marker :
{'x', 'E', 'Z', 'T', 'F', 'N', 'S', 'H', '[', '{'
})
{
for (const char open :
{'[', '{'
})
{
for (const int count :
{
0, 1
})
{
CAPTURE(marker)
CAPTURE(open)
CAPTURE(count)
const std::vector<std::uint8_t> v = {static_cast<std::uint8_t>(open), '$', static_cast<std::uint8_t>(marker), '#', 'i', static_cast<std::uint8_t>(count)};
json _;
CHECK_THROWS_AS(_ = json::from_bjdata(v), json::parse_error&);
CHECK(json::from_bjdata(v, true, false).is_discarded());
}
}
}
}
SECTION("error message")
{
const std::vector<std::uint8_t> v = {'[', '$', 'x', '#', 'i', 0};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(v), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BJData type: marker 0x78 is not a permitted optimized array type", json::parse_error&);
}
SECTION("valid marker")
{
for (const char marker :
{'i', 'U', 'I', 'u', 'l', 'm', 'L', 'M', 'h', 'd', 'D', 'C'
})
{
CAPTURE(marker)
const std::vector<std::uint8_t> va = {'[', '$', static_cast<std::uint8_t>(marker), '#', 'i', 0};
const std::vector<std::uint8_t> vo = {'{', '$', static_cast<std::uint8_t>(marker), '#', 'i', 0};
CHECK(json::from_bjdata(va) == json::array());
CHECK(json::from_bjdata(vo) == json::object());
}
// an optimized array of bytes is read as a binary value
const std::vector<std::uint8_t> vb = {'[', '$', 'B', '#', 'i', 0};
CHECK(json::from_bjdata(vb) == json::binary({}));
}
}
TEST_CASE("BJData optimized arrays from contiguous and non-contiguous input")
{
// contiguous input reads the numbers of an array with a type marker in
// one loop, other input number by number; both must give the same events,
// value, or error for the complete document and for every truncation of
// it, and stop at every event a SAX handler rejects
json document =
{
{"u8", {0, 200, 255}},
{"i8", {-5, 100, -128}},
{"i16", {-30000, 1000, 30000}},
{"i32", {-2000000000, 5, 70000}},
{"i64", {-9000000000000000000LL, 1, 5000000000LL}},
{"f64", {1.5, -0.1, 2.5}},
{"pairs", {{1.25, -3.75}, {1.25, -3.75}, json::array()}},
{"mixed", {1, "a", 2.5, nullptr}},
{"u16", {40000, 1, 65535}},
{"u32", {4000000000U, 1, 70000}},
{"u64", {18446744073709551615ULL, 1, 5000000000ULL}},
{"nd", {{"_ArrayType_", "double"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {1.5, -2.5, 3.5, -4.5}}}},
{"nd8", {{"_ArrayType_", "uint8"}, {"_ArraySize_", {3}}, {"_ArrayData_", {1, 2, 3}}}},
};
const std::vector<std::uint8_t> encoded = json::to_bjdata(document, true, true);
CHECK(json::from_bjdata(encoded) == document);
for (std::size_t length = 0; length <= encoded.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(encoded.begin(), encoded.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_bjdata(contiguous, true, false).dump() == json::from_bjdata(bytewise.begin(), bytewise.end(), true, false).dump());
#if !defined(JSON_NOEXCEPTION)
std::string contiguous_error;
std::string bytewise_error;
try
{
json _;
_ = json::from_bjdata(contiguous);
}
catch (const json::exception& e)
{
contiguous_error = e.what();
}
try
{
json _;
_ = json::from_bjdata(bytewise.begin(), bytewise.end());
}
catch (const json::exception& e)
{
bytewise_error = e.what();
}
CHECK(contiguous_error == bytewise_error);
#endif
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::bjdata)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::bjdata));
CHECK(from_contiguous.events == from_bytewise.events);
}
for (int events = 0; events <= 80; ++events)
{
CAPTURE(events)
const std::list<std::uint8_t> bytewise(encoded.begin(), encoded.end());
SaxCountdown from_contiguous(events);
SaxCountdown from_bytewise(events);
CHECK(json::sax_parse(encoded, &from_contiguous, json::input_format_t::bjdata)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::bjdata));
}
}
+475 -19
View File
@@ -19,9 +19,11 @@
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <array>
#include <cstdint>
#include <fstream>
#include <limits>
#include <list>
#include <sstream>
#include <vector>
#include "make_test_data_available.hpp"
@@ -802,12 +804,11 @@ TEST_CASE("BSON")
CHECK(json::from_bson(result, true, false) == j);
}
SECTION("binary member with subtype 0x02 (old binary) keeps its inner length prefix (lenient parsing)")
SECTION("binary member with subtype 0x02 (old binary) carries an inner length")
{
// documented lenient behavior (see gh-5333): the payload for
// binary subtype 0x02 ("old binary") is returned as-is,
// including its own inner 4-byte length prefix; it is not
// stripped or reinterpreted
// the payload of binary subtype 0x02 ("old binary") starts with an
// int32 that repeats the payload size; the bytes below are what
// pymongo and the bson-corpus (binary.json) use
std::vector<std::uint8_t> const input =
{
0x17, 0x00, 0x00, 0x00, // size (little endian)
@@ -815,16 +816,87 @@ TEST_CASE("BSON")
0x06, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x02, 0x00, 0x00, 0x00, // inner length prefix (part of the old-binary payload)
0x02, 0x00, 0x00, 0x00, // inner length
0x68, 0x69, // payload ('h', 'i')
0x00 // end marker
};
// the inner length prefix is part of the (unmodified) payload
const std::vector<std::uint8_t> expected_payload = {0x02, 0x00, 0x00, 0x00, 0x68, 0x69};
const json expected = { { "entry", json::binary(expected_payload, 0x02) } };
// the inner length is not part of the payload
const json expected = { { "entry", json::binary({0x68, 0x69}, 0x02) } };
CHECK(json::from_bson(input) == expected);
CHECK(json::to_bson(expected) == input);
CHECK(json::from_bson(json::to_bson(expected)) == expected);
}
SECTION("binary member with subtype 0x02 (old binary) and empty payload")
{
std::vector<std::uint8_t> const input =
{
0x15, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
0x04, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x00, 0x00, 0x00, 0x00, // inner length
0x00 // end marker
};
const json expected = { { "entry", json::binary({}, 0x02) } };
CHECK(json::from_bson(input) == expected);
CHECK(json::to_bson(expected) == input);
}
SECTION("binary member with subtype 0x02 (old binary) with an invalid inner length")
{
std::vector<std::uint8_t> input =
{
0x17, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
0x06, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x02, 0x00, 0x00, 0x00, // inner length
0x68, 0x69, // payload ('h', 'i')
0x00 // end marker
};
SECTION("inner length too long")
{
input[19] = 0x03;
}
SECTION("inner length too short")
{
input[19] = 0x01;
}
SECTION("inner length negative")
{
input[19] = 0xFF;
input[20] = 0xFF;
input[21] = 0xFF;
input[22] = 0xFF;
}
CAPTURE(input)
json _;
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
}
SECTION("binary member with subtype 0x02 (old binary) without an inner length")
{
// the payload is shorter than the inner length field
std::vector<std::uint8_t> const input =
{
0x14, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
0x03, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x01, 0x02, 0x03, // payload without an inner length
0x00 // end marker
};
json _;
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
}
SECTION("Some more complex document")
@@ -1094,7 +1166,13 @@ TEST_CASE("BSON keys from contiguous and stream input")
// contiguous input reads a key up to its \x00-byte in one step, a stream
// reads it byte by byte; both must give the same value or error for the
// complete document and for every truncation of it
const json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}};
json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}};
// keys and array element names ("0" to "11") that end at every position of
// an 8-byte word, which contiguous input scans at a time
for (std::size_t length = 1; length <= 17; ++length)
{
j[std::string(length, static_cast<char>('a' + length))] = json::array({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, {11}});
}
const std::vector<std::uint8_t> bson = json::to_bson(j);
CHECK(json::from_bson(bson) == j);
@@ -1114,7 +1192,7 @@ TEST_CASE("BSON keys from contiguous and stream input")
}
try
{
std::istringstream stream(std::string(input.begin(), input.end()));
std::istringstream stream(std::string(reinterpret_cast<const char*>(input.data()), input.size()));
from_stream = json::from_bson(stream).dump();
}
catch (const json::parse_error& e)
@@ -1122,8 +1200,72 @@ TEST_CASE("BSON keys from contiguous and stream input")
from_stream = e.what();
}
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);
}
}
TEST_CASE("BSON array element names")
{
// an array's element names are not part of the value, so they are not
// passed on; error_handler_t::strict still rejects ill-formed UTF-8 in them
const std::vector<std::uint8_t> bson =
{
0x1D, 0x00, 0x00, 0x00, // size (little endian)
0x04, 'a', 0x00, // entry: array "a"
0x15, 0x00, 0x00, 0x00, // size (little endian)
0x08, 0xFF, 0x00, 0x01, // entry: boolean named "\xFF", true
0x08, '1', '2', '3', '4', '5', '6', '7', '8', '9', 0x00, 0x00, // entry: boolean named "123456789", false
0x00, // end of the array
0x00 // end of the document
};
const json expected = {{"a", {true, false}}};
CHECK(json::from_bson(bson) == expected);
CHECK(json::from_bson(bson, true, true, json::error_handler_t::replace) == expected);
CHECK(json::from_bson(bson, true, true, json::error_handler_t::ignore) == expected);
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(bson, 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&);
std::istringstream stream(std::string(reinterpret_cast<const char*>(bson.data()), bson.size()));
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
TEST_CASE("Negative size of binary value")
@@ -1178,7 +1320,7 @@ TEST_CASE("BSON document size mismatch")
SECTION("top-level document with a negative size")
{
std::vector<std::uint8_t> const input = {0xFF, 0xFF, 0xFF, 0xFF, 0x00};
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size -1 does not match the number of bytes read (5)", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BSON document: document size -1 is less than 5", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
@@ -1190,7 +1332,7 @@ TEST_CASE("BSON document size mismatch")
// enclosing document instead of "d"
std::vector<std::uint8_t> const input =
{
0x00, 0x00, 0x00, 0x00, // outer size
0x14, 0x00, 0x00, 0x00, // outer size
0x03, 'd', 0x00, // entry: embedded document "d"
0xFF, 0xFF, 0xFF, 0x7F, // embedded size 0x7FFFFFFF
0x0A, 'a', 0x00, // entry: null "a"
@@ -1198,7 +1340,24 @@ TEST_CASE("BSON document size mismatch")
0x08, 'h', 0x00, 0x01, // entry: bool "h" = true
0x00 // outer end marker
};
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 2147483647 does not match the number of bytes read (8)", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 11: syntax error while parsing BSON document: document size 2147483647 exceeds the size of the enclosing document", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
SECTION("embedded document whose size is smaller than its content")
{
// {"a":null} is 8 bytes, but the length prefix claims 7
std::vector<std::uint8_t> const input =
{
0x14, 0x00, 0x00, 0x00, // outer size
0x03, 'd', 0x00, // entry: embedded document "d"
0x07, 0x00, 0x00, 0x00, // embedded size 7 (real is 8)
0x0A, 'a', 0x00, // entry: null "a"
0x00, // embedded end marker
0x08, 'h', 0x00, 0x01, // entry: bool "h" = true
0x00 // outer end marker
};
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 7 does not match the number of bytes read (8)", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
@@ -1207,18 +1366,165 @@ TEST_CASE("BSON document size mismatch")
// array [42] is 12 bytes, but the length prefix claims 13
std::vector<std::uint8_t> const input =
{
0x00, 0x00, 0x00, 0x00, // outer size
0x14, 0x00, 0x00, 0x00, // outer size
0x04, 'a', 0x00, // entry: array "a"
0x0D, 0x00, 0x00, 0x00, // array size 13 (real is 12)
0x10, '0', 0x00, 0x2A, 0x00, 0x00, 0x00, // entry: int32 "0" = 42
0x00, // array end marker
0x00 // outer end marker
};
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 13 does not match the number of bytes read (12)", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 11: syntax error while parsing BSON document: document size 13 exceeds the size of the enclosing document", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
}
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
{
events += "null ";
return true;
}
bool boolean(bool /*val*/) override
{
events += "boolean ";
return true;
}
bool number_integer(json::number_integer_t /*val*/) override
{
events += "integer ";
return true;
}
bool number_unsigned(json::number_unsigned_t /*val*/) override
{
events += "unsigned ";
return true;
}
bool number_float(json::number_float_t /*val*/, const std::string& /*s*/) override
{
events += "float ";
return true;
}
bool string(std::string& /*val*/) override
{
events += "string ";
return true;
}
bool binary(json::binary_t& /*val*/) override
{
events += "binary ";
return true;
}
bool start_object(std::size_t /*elements*/) override
{
events += "{ ";
return true;
}
bool key(std::string& /*val*/) override
{
events += "key ";
return true;
}
bool end_object() override
{
events += "} ";
return true;
}
bool start_array(std::size_t /*elements*/) override
{
events += "[ ";
return true;
}
bool end_array() override
{
events += "] ";
return true;
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override
{
events += "error ";
return false;
}
};
SECTION("the content of a too small document is not reported")
{
// {"a":1} is 12 bytes; the length prefix claims 5, so the element
// does not fit
std::vector<std::uint8_t> const input =
{
0x05, 0x00, 0x00, 0x00,
0x10, 'a', 0x00, 0x01, 0x00, 0x00, 0x00,
0x00
};
recorder rec;
CHECK(!json::sax_parse(input, &rec, json::input_format_t::bson));
CHECK(rec.events == "{ error ");
// also without strict mode
recorder rec2;
CHECK(!json::sax_parse(input, &rec2, json::input_format_t::bson, false));
CHECK(rec2.events == "{ error ");
}
SECTION("an embedded document that does not fit its parent is not opened")
{
// the embedded document ends where the enclosing one does, so there
// is no room for the terminator of the enclosing document
std::vector<std::uint8_t> const input =
{
0x0C, 0x00, 0x00, 0x00,
0x03, 'd', 0x00,
0x05, 0x00, 0x00, 0x00, 0x00,
0x00
};
recorder rec;
CHECK(!json::sax_parse(input, &rec, json::input_format_t::bson));
CHECK(rec.events == "{ key error ");
}
SECTION("an embedded document that fits exactly")
{
std::vector<std::uint8_t> const input =
{
0x0D, 0x00, 0x00, 0x00,
0x03, 'd', 0x00,
0x05, 0x00, 0x00, 0x00, 0x00,
0x00
};
CHECK(json::from_bson(input) == json({{"d", json::object()}}));
}
SECTION("a document size that is less than 5")
{
for (const std::int32_t size :
{
0, 1, 4, -1, (std::numeric_limits<std::int32_t>::min)()
})
{
CAPTURE(size)
std::vector<std::uint8_t> input =
{
0x00, 0x00, 0x00, 0x00, 0x00
};
input[0] = static_cast<std::uint8_t>(size & 0xFF);
input[1] = static_cast<std::uint8_t>((size >> 8) & 0xFF);
input[2] = static_cast<std::uint8_t>((size >> 16) & 0xFF);
input[3] = static_cast<std::uint8_t>((size >> 24) & 0xFF);
json _;
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
}
}
TEST_CASE("BSON nesting does not consume the call stack")
{
// An embedded document or array used to be read by calling back into the
@@ -1923,7 +2229,7 @@ TEST_CASE("Invalid document size handling")
{
std::vector<std::uint8_t> const v = {0x04, 0x00, 0x00, 0x00, 0x00};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 4 does not match the number of bytes read (5)", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BSON document: document size 4 is less than 5", json::parse_error&);
CHECK(json::from_bson(v, true, false).is_discarded());
}
@@ -1938,7 +2244,7 @@ TEST_CASE("Invalid document size handling")
0x00
};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 16: syntax error while parsing BSON document: document size 5 does not match the number of bytes read (16)", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 5 is too small for its content", json::parse_error&);
CHECK(json::from_bson(v, true, false).is_discarded());
}
@@ -1969,7 +2275,7 @@ TEST_CASE("Invalid document size handling")
0x00 // object terminator
};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 5 does not match the number of bytes read (12)", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 12: syntax error while parsing BSON document: document size 5 is too small for its content", json::parse_error&);
CHECK(json::from_bson(v, true, false).is_discarded());
}
@@ -2033,3 +2339,153 @@ TEST_CASE("BSON large strings and binaries (chunked reader)")
}
}
}
TEST_CASE("BSON written in one pass and in two passes")
{
// to_bson() into a returned vector writes each size once the object or
// array is complete; through an output adapter, all sizes are computed
// first. Both must produce the same bytes and throw the same exceptions.
const auto two_pass = [](const json & j, const json::error_handler_t handler)
{
std::vector<std::uint8_t> result;
json::to_bson(j, result, handler);
return result;
};
const auto check_same = [&](const json & j, const json::error_handler_t handler)
{
CAPTURE(j.dump(-1, ' ', true, json::error_handler_t::replace))
CAPTURE(static_cast<int>(handler))
#if !defined(JSON_NOEXCEPTION)
std::string one_pass_error;
std::string two_pass_error;
std::vector<std::uint8_t> one;
std::vector<std::uint8_t> two;
try
{
one = json::to_bson(j, handler);
}
catch (const json::exception& e)
{
one_pass_error = e.what();
}
try
{
two = two_pass(j, handler);
}
catch (const json::exception& e)
{
two_pass_error = e.what();
}
CHECK(one_pass_error == two_pass_error);
CHECK(one == two);
#else
CHECK(json::to_bson(j, handler) == two_pass(j, handler));
#endif
};
const std::array<json::error_handler_t, 4> handlers = {{
json::error_handler_t::keep, json::error_handler_t::strict,
json::error_handler_t::replace, json::error_handler_t::ignore
}
};
SECTION("corpus")
{
for (const auto& value : utils::round_trip_corpus::values())
{
const json document = value.is_object() ? value : json{{"value", value}};
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
}
SECTION("nesting and arrays of every length class")
{
json document = json::object();
document["empty object"] = json::object();
document["empty array"] = json::array();
for (const std::size_t n :
{
1u, 9u, 10u, 11u, 99u, 100u, 101u, 1000u
})
{
json numbers = json::array();
json mixed = json::array();
for (std::size_t i = 0; i < n; ++i)
{
numbers.push_back(i % 3 == 0 ? json(static_cast<double>(i) / 3) : json(static_cast<std::int64_t>(i) * 1000003));
switch (i % 4)
{
case 0:
mixed.push_back(json("s" + std::to_string(i)));
break;
case 1:
mixed.push_back(json::array({i, json::object({{"k", i}})}));
break;
case 2:
mixed.push_back(json(nullptr));
break;
default:
mixed.push_back(json(i % 2 == 0));
break;
}
}
document["numbers " + std::to_string(n)] = numbers;
document["mixed " + std::to_string(n)] = mixed;
document["nested " + std::to_string(n)] = json::object({{"a", numbers}, {"b", json::array({mixed, json::object()})}});
}
document["binary"] = json::binary({1, 2, 3}, 0x80);
document["binary old"] = json::binary({1, 2, 3}, 0x02);
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
#if !defined(JSON_NOEXCEPTION)
// every entry here throws, which aborts without exceptions
SECTION("errors at every position")
{
// an entry that cannot be written, at each place among valid ones
const std::string bad_utf8 = "\xC3\x28";
const std::array<json, 7> bad_entries =
{
{
json::object({{std::string("a\0b", 3), 1}}),
json::object({{"s", bad_utf8}}),
json::object({{bad_utf8, 1}}),
json::array({1, 2, bad_utf8}),
json::object({{"b", json::binary({1}, 0x100)}}),
json::object({{"d", json(json::value_t::discarded)}}),
json::array({json(json::value_t::discarded)}),
}
};
for (const auto& bad : bad_entries)
{
for (std::size_t pos = 0; pos < 4; ++pos)
{
json document = json::object();
json list = json::array();
for (std::size_t i = 0; i < 4; ++i)
{
if (i == pos)
{
document["x" + std::to_string(i)] = bad;
list.push_back(bad);
}
document["n" + std::to_string(i)] = json::array({i, "str", json::object({{"k", i}})});
list.push_back(i);
}
document["list"] = list;
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
}
}
#endif
}
+222 -3
View File
@@ -29,7 +29,9 @@ using nlohmann::json;
#include "round_trip_corpus.hpp"
#include "test_utils.hpp"
#include "sax_countdown.hpp"
#include "sax_event_loggers.hpp"
using utils::SaxCountdown;
using utils::SaxEventLogger;
TEST_CASE("CBOR")
@@ -1807,6 +1809,93 @@ TEST_CASE("CBOR")
CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded());
}
SECTION("tag in front of a key")
{
const auto ignore = json::cbor_tag_handler_t::ignore;
const auto store = json::cbor_tag_handler_t::store;
const json expected = {{"a", 10}};
// the tags are skipped in front of a text string key, like in
// front of a value
const std::vector<std::vector<std::uint8_t>> tagged =
{
{0xA1, 0xC0, 0x61, 0x61, 0x0A}, // tag in the head
{0xA1, 0xD8, 0x05, 0x61, 0x61, 0x0A}, // 1-byte tag number
{0xA1, 0xD9, 0x01, 0x2C, 0x61, 0x61, 0x0A}, // 2-byte tag number
{0xA1, 0xDA, 0x00, 0x01, 0x00, 0x00, 0x61, 0x61, 0x0A}, // 4-byte tag number
{0xA1, 0xDB, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x61, 0x61, 0x0A}, // 8-byte tag number
{0xA1, 0xC0, 0xD8, 0x05, 0xC1, 0x61, 0x61, 0x0A}, // chained tags
{0xBF, 0xC0, 0x61, 0x61, 0x0A, 0xFF}, // indefinite-length map
{0xBF, 0x61, 0x62, 0x01, 0xC0, 0x61, 0x61, 0x0A, 0xFF}, // second key tagged
{0xA1, 0xC0, 0x7F, 0x61, 0x61, 0xFF, 0x0A}, // indefinite-length text key
{0xA1, 0xC0, 0x61, 0x61, 0xC0, 0x0A}, // tagged key and value
};
for (const auto& input : tagged)
{
CAPTURE(input)
json const j_ignore = json::from_cbor(input, true, true, ignore);
json const j_store = json::from_cbor(input, true, true, store);
CHECK(j_ignore.at("a") == 10);
CHECK(j_store.at("a") == 10);
CHECK(j_ignore == j_store);
}
CHECK(json::from_cbor(tagged[0], true, true, ignore) == expected);
CHECK(json::from_cbor(tagged[1], true, true, store) == expected);
// nested maps
CHECK(json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xC0, 0x61, 0x61, 0xA1, 0xD8, 0x05, 0x61, 0x62, 0x01}), true, true, ignore) == json{{"a", {{"b", 1}}}});
// the tag handler error still rejects every tag
for (const auto& input : tagged)
{
CAPTURE(input)
json _;
CHECK_THROWS_AS(_ = json::from_cbor(input), json::parse_error&);
}
{
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xD8, 0x05, 0x61, 0x61, 0x0A})), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing CBOR object key: only string keys are supported, but found a tag; last byte: 0xD8", json::parse_error&);
}
// a tag does not turn a key of another type into a text string
{
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xC0, 0x01, 0x01}), true, true, ignore), "[json.exception.parse_error.113] parse error at byte 3: syntax error while parsing CBOR object key: only string keys are supported, but found an unsigned integer; last byte: 0x01", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xD8, 0x05, 0x41, 0x61, 0x01}), true, true, store), "[json.exception.parse_error.113] parse error at byte 4: syntax error while parsing CBOR object key: only string keys are supported, but found a byte string; last byte: 0x41", json::parse_error&);
CHECK(json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xC0, 0x01, 0x01}), true, false, ignore).is_discarded());
}
// end of input after the tag, or inside the tag number
{
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xC0}), true, true, ignore), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&);
CHECK_THROWS_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xD8}), true, true, ignore), json::parse_error&);
CHECK_THROWS_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xDB, 0x00, 0x00}), true, true, store), json::parse_error&);
CHECK_THROWS_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xBF, 0xC0}), true, true, store), json::parse_error&);
}
// SAX: the key event is reported without the tag (start_object,
// key, number_unsigned, end_object are four events)
{
const std::vector<std::uint8_t> input = {0xA1, 0xC0, 0x61, 0x61, 0x0A};
SaxCountdown four(4);
CHECK(json::sax_parse(input, &four, json::input_format_t::cbor, true, false, false, ignore));
SaxCountdown three(3);
CHECK(!json::sax_parse(input, &three, json::input_format_t::cbor, true, false, false, ignore));
SaxCountdown error_handler(4);
CHECK(!json::sax_parse(input, &error_handler, json::input_format_t::cbor));
}
// a long chain of tags needs no recursion
{
std::vector<std::uint8_t> input = {0xA1};
input.insert(input.end(), 100000, 0xC0);
input.insert(input.end(), {0x61, 0x61, 0x0A});
CHECK(json::from_cbor(input, true, true, ignore) == expected);
}
}
SECTION("non-string key (see #2766 and #3381)")
{
// only text strings map to JSON object keys; any other key is
@@ -1915,9 +2004,10 @@ TEST_CASE("CBOR")
{
json _;
// the chunks are concatenated as is, without checking that each
// chunk is valid UTF-8 on its own (RFC 8949, Section 3.2.3), so
// a code point split across two chunks yields a valid string
// with the default error handler keep, the chunks are concatenated
// as is, without checking that each chunk is valid UTF-8 on its
// own (RFC 8949, Section 3.2.3), so a code point split across two
// chunks yields a valid string
CHECK_NOTHROW(_ = json::from_cbor(std::vector<uint8_t>({0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff})));
CHECK(_ == "\xc3\xa9");
CHECK(_.dump() == "\"\xc3\xa9\"");
@@ -1937,6 +2027,63 @@ TEST_CASE("CBOR")
CHECK(json::from_cbor(std::vector<uint8_t>({0x7f, 0x62, 0xc3, 0xa9, 0x62, 0xc3, 0xb6, 0xff})) == "\xc3\xa9\xc3\xb6");
}
SECTION("ill-formed UTF-8 in a chunk of an indefinite-length string (RFC 8949, Section 3.2.3)")
{
const auto strict = json::error_handler_t::strict;
const auto replace = json::error_handler_t::replace;
const auto ignore = json::error_handler_t::ignore;
const auto keep = json::error_handler_t::keep;
const auto tags = json::cbor_tag_handler_t::error;
// a code point split over two chunks
const std::vector<std::uint8_t> split = {0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff};
// a 4-byte code point split over two chunks
const std::vector<std::uint8_t> split_emoji = {0x7f, 0x62, 0xf0, 0x9f, 0x62, 0x98, 0x80, 0xff};
// the same in an object key
const std::vector<std::uint8_t> split_key = {0xa1, 0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff, 0x01};
// a valid chunk followed by one with a truncated code point
const std::vector<std::uint8_t> truncated = {0x7f, 0x62, 0x41, 0xc3, 0x61, 0xa9, 0xff};
// strict: the string is invalid
for (const auto& input :
{
split, split_emoji, split_key, truncated
})
{
CAPTURE(input)
json _;
CHECK_THROWS_AS(_ = json::from_cbor(input, true, true, tags, strict), json::parse_error&);
CHECK(json::from_cbor(input, true, false, tags, strict).is_discarded());
}
{
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(split, true, true, tags, strict), "[json.exception.parse_error.113] parse error at byte 3: syntax error while parsing CBOR string: invalid string: ill-formed UTF-8 byte", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_cbor(split_key, true, true, tags, strict), "[json.exception.parse_error.113] parse error at byte 4: syntax error while parsing CBOR key: invalid string: ill-formed UTF-8 byte", json::parse_error&);
}
// replace and ignore: every chunk is sanitized on its own
CHECK(json::from_cbor(split, true, true, tags, replace) == "\xEF\xBF\xBD\xEF\xBF\xBD");
CHECK(json::from_cbor(split, true, true, tags, ignore) == "");
CHECK(json::from_cbor(truncated, true, true, tags, replace) == "A\xEF\xBF\xBD\xEF\xBF\xBD");
CHECK(json::from_cbor(truncated, true, true, tags, ignore) == "A");
CHECK(json::from_cbor(split_key, true, true, tags, replace) == json{{"\xEF\xBF\xBD\xEF\xBF\xBD", 1}});
// keep: unchanged, the chunks are concatenated as is
CHECK(json::from_cbor(split, true, true, tags, keep) == "\xc3\xa9");
CHECK(json::from_cbor(split_emoji, true, true, tags, keep) == "\xf0\x9f\x98\x80");
// conforming input is unaffected by the error handler
const std::vector<std::uint8_t> valid = {0x7f, 0x62, 0xc3, 0xa9, 0x64, 0xf0, 0x9f, 0x98, 0x80, 0x60, 0xff};
for (const auto handler :
{
strict, replace, ignore, keep
})
{
CHECK(json::from_cbor(valid, true, true, tags, handler) == "\xc3\xa9\xf0\x9f\x98\x80");
CHECK(json::from_cbor(std::vector<std::uint8_t>({0x7f, 0xff}), true, true, tags, handler) == "");
}
}
SECTION("many chunks in indefinite-length string")
{
constexpr std::size_t chunks = 100000;
@@ -2301,6 +2448,78 @@ TEST_CASE("CBOR SAX parsing stops at every event")
CHECK(count_events(std::vector<std::uint8_t>({0xBF, 0x61, 'a', 0x9F, 0x01, 0xFF, 0xFF})) == 6);
}
TEST_CASE("CBOR arrays of scalars from contiguous and non-contiguous input")
{
// contiguous input reads the integers, floats, booleans, and nulls an
// array begins with in one loop, other input value by value; both must
// give the same events, value, or error for the complete document and for
// every truncation of it, and stop at every event a SAX handler rejects
const std::vector<std::uint8_t> cbor =
{
0xA3, // map(3)
0x61, 'a', // "a"
0x98, 0x16, // array(22)
0x00, 0x17, 0x18, 0xFF, 0x19, 0x01, 0x00, // 0, 23, 255, 256
0x1A, 0x00, 0x01, 0x00, 0x00, // 65536
0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 18446744073709551615
0x20, 0x37, 0x38, 0xFF, 0x39, 0x01, 0x00, // -1, -24, -256, -257
0x3A, 0x00, 0x01, 0x00, 0x00, // -65537
0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -18446744073709551616 (a float)
0x3B, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -9223372036854775808
0xF4, 0xF5, 0xF6, // false, true, null
0xFA, 0x3F, 0xC0, 0x00, 0x00, // 1.5 (float32)
0xFB, 0xBF, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, // -0.1
0xF9, 0x3C, 0x00, // 1.0 (float16, ends the loop)
0x01, 0x61, 'a', // 1, "a"
0xFB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2.5
0x61, 'b', // "b"
0x83, // array(3)
0x82, 0xFB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
0xFB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
0x82, 0xFB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
0xFB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
0x80, // []
0x61, 'c', // "c"
0x84, 0xA0, 0x01, 0xC1, 0x02, 0x81, 0x03 // [{}, 1, tag(1) 2, [3]]
};
const auto tags = json::cbor_tag_handler_t::ignore;
const json j = json::from_cbor(cbor, true, true, tags);
CHECK(j["a"].size() == 22);
CHECK(j["a"][5] == 18446744073709551615u);
CHECK(j["a"][11].is_number_float());
CHECK(j["a"][11] == -18446744073709551616.0);
CHECK(j["a"][12] == (std::numeric_limits<std::int64_t>::min)());
CHECK(j["a"][17] == -0.1);
CHECK(j["b"] == json::parse("[[1.25, -3.75], [1.25, -3.75], []]"));
CHECK(j["c"] == json::parse("[{}, 1, 2, [3]]"));
for (std::size_t length = 0; length <= cbor.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(cbor.begin(), cbor.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_cbor(contiguous, true, false, tags).dump() == json::from_cbor(bytewise.begin(), bytewise.end(), true, false, tags).dump());
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::cbor, true, false, false, tags)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false, false, tags));
CHECK(from_contiguous.events == from_bytewise.events);
}
for (int events = 0; events <= 60; ++events)
{
CAPTURE(events)
const std::list<std::uint8_t> bytewise(cbor.begin(), cbor.end());
SaxCountdown from_contiguous(events);
SaxCountdown from_bytewise(events);
CHECK(json::sax_parse(cbor, &from_contiguous, json::input_format_t::cbor, true, false, false, tags)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false, false, tags));
}
}
TEST_CASE("CBOR indefinite-length strings do not recurse per chunk")
{
// Reading an indefinite-length string or byte array used to call itself
+202
View File
@@ -3627,6 +3627,67 @@ TEST_CASE("accept() agrees with parse() on floats around the overflow limit")
}
}
TEST_CASE("accept() agrees with parse() on strings, which it checks without copying them")
{
// accept() scans strings and object keys with a separate instantiation of
// the string scanner that does not copy them into the token buffer; it
// must accept and reject exactly what parse() does
const std::vector<std::string> pieces =
{
"", "abc", R"(\"\\\/\b\f\n\r\t)", R"(\u0041\u00e4\u20AC)", "\\ud83d\\ude00", "\\uD83D\\uDE00",
"\\ud800", "\\udc00", "\\ud800\\u0041", "\\ud800x", "\\udbff\\udfff", "\\u12", "\\u12g4", "\\x", "\\",
"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf", "\xc0\xaf", "\xed\xa0\x80", "\xf4\x90\x80\x80",
"\xff", "\xe2\x82", "\xc3", "\x01", "\x1f", "\x7f", "\n", std::string(1, '\0')
};
for (std::size_t run = 0; run <= 70; ++run)
{
for (const std::string& piece : pieces)
{
// a run of ordinary characters puts the piece at every offset of the bulk scanners
const std::string str = std::string(run, 'a') + piece;
std::string opened = "\"";
opened += str;
std::string closed = opened;
closed += '"';
std::vector<std::string> texts;
texts.push_back(closed);
texts.emplace_back("[");
texts.back() += closed;
texts.back() += ']';
texts.emplace_back("{");
texts.back() += closed;
texts.back() += ":1}";
texts.emplace_back("{\"k\":");
texts.back() += closed;
texts.back() += ',';
texts.back() += closed;
texts.back() += ":[";
texts.back() += closed;
texts.back() += "]}";
texts.emplace_back("[");
texts.back() += opened;
for (const std::string& text : texts)
{
CAPTURE(text)
for (int options = 0; options < 4; ++options)
{
const bool ignore_comments = (options & 1) != 0;
const bool ignore_trailing_commas = (options & 2) != 0;
const bool parses = !json::parse(text, nullptr, false, ignore_comments, ignore_trailing_commas).is_discarded();
CHECK(json::accept(text, ignore_comments, ignore_trailing_commas) == parses);
std::istringstream stream(text);
CHECK(json::accept(stream, ignore_comments, ignore_trailing_commas) == parses);
const std::list<char> chars(text.begin(), text.end());
CHECK(json::accept(chars.begin(), chars.end(), ignore_comments, ignore_trailing_commas) == parses);
}
}
}
}
}
namespace
{
// a DOM parser that reports parse errors without throwing, as in the sax_no_exception example
@@ -3675,3 +3736,144 @@ TEST_CASE("a json_sax_dom_parser keeps the values read before a parse error")
CHECK(read_before_error<nlohmann::ordered_json>(c.first) == c.second);
}
}
TEST_CASE("object keys are delivered decoded and validated")
{
// json_sax::key and the key event of a parser callback see the decoded name, and
// only after the whole name was scanned and validated
const auto keys_of_format = [](const std::string & text, const json::input_format_t format)
{
SaxEventLogger l;
json::sax_parse(text, &l, format);
std::vector<std::string> keys;
for (const auto& e : l.events)
{
if (e.compare(0, 4, "key(") == 0)
{
keys.push_back(e.substr(4, e.size() - 5));
}
}
return keys;
};
const auto keys_of = [&keys_of_format](const std::string & text)
{
return keys_of_format(text, json::input_format_t::json);
};
const auto callback_keys_of = [](const std::string & text)
{
std::vector<std::string> keys;
const json::parser_callback_t cb = [&keys](int /*depth*/, json::parse_event_t event, json & parsed)
{
if (event == json::parse_event_t::key)
{
keys.push_back(parsed.get<std::string>());
}
return true;
};
const json parsed = json::parse(text, cb, false);
static_cast<void>(parsed);
return keys;
};
SECTION("different spellings of a name yield the same string")
{
const std::string e_acute = "\xc3\xa9";
const std::string text = std::string(R"({"/":1,"\/":2,"\u002f":3,"\u00e9":4,"\u00E9":5,")") + e_acute + R"(":6})";
const std::vector<std::string> expected = {"/", "/", "/", e_acute, e_acute, e_acute};
CHECK(keys_of(text) == expected);
CHECK(callback_keys_of(text) == expected);
}
SECTION("escape sequences are decoded")
{
const std::string simple = R"({"\"\\\/\b\f\n\r\t":1})";
const std::vector<std::string> simple_expected = {std::string("\"\\/\b\f\n\r\t")};
CHECK(keys_of(simple) == simple_expected);
CHECK(callback_keys_of(simple) == simple_expected);
const std::string nul = R"({"\u0000":1})";
const std::vector<std::string> nul_expected = {std::string(1, '\0')};
CHECK(keys_of(nul) == nul_expected);
CHECK(callback_keys_of(nul) == nul_expected);
const std::string pair = R"({"\ud83d\ude00":1})";
const std::vector<std::string> pair_expected = {"\xf0\x9f\x98\x80"};
CHECK(keys_of(pair) == pair_expected);
CHECK(callback_keys_of(pair) == pair_expected);
}
SECTION("no normalization and no case conversion")
{
const std::string text = std::string("{\"\xc3\xa9\":1,\"e\xcc\x81\":2,\"A\":3,\"a\":4}");
const std::vector<std::string> expected = {"\xc3\xa9", "e\xcc\x81", "A", "a"};
CHECK(keys_of(text) == expected);
CHECK(callback_keys_of(text) == expected);
}
SECTION("a name that is not valid is not delivered")
{
const std::vector<std::string> texts =
{
R"({"\u12G4":1})", // malformed escape
R"({"\udc00":1})", // lone low surrogate
R"({"\ud800":1})", // lone high surrogate
R"({"\ud800\u0041":1})", // high surrogate not followed by a low one
R"({"\x":1})", // unknown escape
std::string("{\"a\x01\":1}"), // control character
std::string("{\"\xff\":1}") // ill-formed UTF-8
};
for (const auto& text : texts)
{
CAPTURE(text)
CHECK(!json::accept(text));
CHECK(keys_of(text).empty());
CHECK(callback_keys_of(text).empty());
}
}
SECTION("a name is delivered before the following colon is read")
{
const std::string text = R"({"a" 1})";
const std::vector<std::string> expected = {"a"};
CHECK(!json::accept(text));
CHECK(keys_of(text) == expected);
CHECK(callback_keys_of(text) == expected);
}
SECTION("a repeated name is delivered once per occurrence")
{
const std::string text = R"({"a":1,"a":2})";
const std::vector<std::string> expected = {"a", "a"};
CHECK(keys_of(text) == expected);
CHECK(callback_keys_of(text) == expected);
}
SECTION("binary formats pass the stored bytes through")
{
const std::string stored = "\xff\\";
const std::vector<std::string> expected = {stored};
const auto keys_of_bytes = [&keys_of_format](const std::vector<std::uint8_t>& v, json::input_format_t format)
{
std::string text;
for (const auto byte : v)
{
text.push_back(static_cast<char>(byte));
}
return keys_of_format(text, format);
};
CHECK(keys_of_bytes({0xA1, 0x62, 0xFF, 0x5C, 0x01}, json::input_format_t::cbor) == expected);
CHECK(keys_of_bytes({0x81, 0xA2, 0xFF, 0x5C, 0x01}, json::input_format_t::msgpack) == expected);
CHECK(keys_of_bytes({'{', 'i', 2, 0xFF, 0x5C, 'i', 1, '}'}, json::input_format_t::ubjson) == expected);
CHECK(keys_of_bytes({0x0D, 0, 0, 0, 0x10, 0xFF, 0x5C, 0, 1, 0, 0, 0, 0}, json::input_format_t::bson) == expected);
}
SECTION("BON8 rejects ill-formed UTF-8 in a name")
{
const std::vector<std::string> valid = {"\\"};
CHECK(keys_of_format(std::string("\x87\x5C\x91"), json::input_format_t::bon8) == valid);
CHECK(keys_of_format(std::string("\x87\xC3\x28\x91"), json::input_format_t::bon8).empty());
}
}
+1
View File
@@ -403,6 +403,7 @@ TEST_CASE("guard page")
CHECK(nlohmann::detail::find_string_special(p, n) <= n);
CHECK(nlohmann::detail::find_ascii_copyable_run(p, n) <= n);
CHECK(nlohmann::detail::valid_utf8_prefix(p, n) <= n);
CHECK(nlohmann::detail::valid_utf8_string(p, n) == (nlohmann::detail::valid_utf8_prefix(p, n) == n));
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
CHECK(nlohmann::detail::find_string_delimiter(p, n) <= n);
#endif
+351 -1
View File
@@ -15,6 +15,7 @@ using nlohmann::json;
#endif
#include <fstream>
#include <limits>
#include <string>
#include <vector>
#include "make_test_data_available.hpp"
@@ -490,7 +491,9 @@ TEST_CASE("JSON patch")
}
// A.13. Invalid JSON Patch Document
// not applicable
// not applicable: the library cannot tell the duplicate "op" members
// apart, because the parser resolves them (the last one is kept); see
// "duplicate member names in an operation" below
SECTION("A.14. Escape Ordering")
{
@@ -823,6 +826,129 @@ TEST_CASE("JSON patch")
CHECK(doc.at("foo") == "bar");
}
#endif // !defined(JSON_NOEXCEPTION)
SECTION("applying a value as patch to itself")
{
// a JSON patch is an array, so an array of operations can be
// applied to itself; the operations are read from the patch as
// it was before the first operation, as patch() does it
const json copy_op = {{"op", "copy"}, {"from", "/0"}, {"path", "/-"}};
json copy_ops = json::array();
for (int i = 0; i < 5; ++i)
{
copy_ops.push_back(copy_op);
}
json expected_copies = copy_ops;
for (int i = 0; i < 5; ++i)
{
expected_copies.push_back(copy_op);
}
SECTION("copy operations without spare capacity")
{
// a copy has exactly as much capacity as elements
json doc = copy_ops;
CHECK(doc.patch(doc) == expected_copies);
doc.patch_inplace(doc);
CHECK(doc == expected_copies);
}
#if JSON_DIAGNOSTICS
SECTION("patch stored inside the patched value")
{
// the parent pointers of JSON_DIAGNOSTICS make the aliasing detectable
const json op = {{"op", "replace"}, {"path", "/patch"}, {"value", 1}};
json doc = {{"a", 1}, {"patch", json::array({op})}};
const json expected = doc.patch(doc["patch"]);
doc.patch_inplace(doc["patch"]);
CHECK(doc == expected);
}
#endif
SECTION("copy operations with spare capacity")
{
// reserve while the array is empty: reallocating non-empty
// elements behind the library's back leaves stale parent
// pointers with JSON_DIAGNOSTICS
json doc = json::array();
doc.get_ref<json::array_t&>().reserve(100);
for (int i = 0; i < 5; ++i)
{
doc.push_back(copy_op);
}
CHECK(doc.patch(doc) == expected_copies);
doc.patch_inplace(doc);
CHECK(doc == expected_copies);
}
SECTION("remove operations")
{
json doc = R"([
{"op": "remove", "path": "/0"},
{"op": "remove", "path": "/0"},
{"op": "remove", "path": "/0"}
])"_json;
CHECK(doc.patch(doc) == json::array());
doc.patch_inplace(doc);
CHECK(doc == json::array());
}
SECTION("operations that read a changed element")
{
// the second operation is still a "test" operation, although
// the first one replaced it in the document
json doc = R"([
{"op": "replace", "path": "/1", "value": 1},
{"op": "test", "path": "/1", "value": 1},
{"op": "add", "path": "/-", "value": 2}
])"_json;
const json expected = R"([
{"op": "replace", "path": "/1", "value": 1},
1,
{"op": "add", "path": "/-", "value": 2},
2
])"_json;
CHECK(doc.patch(doc) == expected);
doc.patch_inplace(doc);
CHECK(doc == expected);
}
SECTION("replacing the root")
{
json doc = R"([
{"op": "add", "path": "", "value": [1, 2]},
{"op": "add", "path": "/-", "value": 3},
{"op": "replace", "path": "", "value": {"a": 4}}
])"_json;
const json expected = {{"a", 4}};
CHECK(doc.patch(doc) == expected);
doc.patch_inplace(doc);
CHECK(doc == expected);
}
#if !defined(JSON_NOEXCEPTION)
SECTION("failing operation")
{
// the operations before the failing one remain applied
json doc = R"([
{"op": "add", "path": "/-", "value": 1},
{"op": "test", "path": "/0/op", "value": "remove"}
])"_json;
CHECK_THROWS_AS(doc.patch(doc), json::other_error&);
CHECK_THROWS_AS(doc.patch_inplace(doc), json::other_error&);
CHECK(doc.size() == 3);
CHECK(doc.back() == 1);
}
#endif // !defined(JSON_NOEXCEPTION)
}
}
SECTION("errors")
@@ -1515,6 +1641,24 @@ TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
#endif
}
SECTION("null parent")
{
json const doc = {{"foo", nullptr}};
json const patch = {{{"op", "add"}, {"path", "/foo/bar"}, {"value", 1}}};
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] (/foo) cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
#else
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
#endif
}
SECTION("null document")
{
json const doc = nullptr;
json const patch = {{{"op", "add"}, {"path", "/-"}, {"value", 1}}};
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
}
SECTION("number parent")
{
json const doc = {{"foo", 1}};
@@ -2103,3 +2247,209 @@ TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json,
roundtrip(Json(1), Json::parse("[1]"));
}
}
TEST_CASE("JSON patch - behavior documented in the API reference")
{
SECTION("duplicate member names in an operation")
{
// RFC 6902, A.13: the parser resolves the duplicates (the last one
// is kept), so this is a "remove" and not an "add"
const json patch = json::parse(R"([{"op": "add", "op": "remove", "path": "/a"}])");
CHECK(patch == json::parse(R"([{"op": "remove", "path": "/a"}])"));
CHECK(json({{"a", 1}, {"b", 2}}).patch(patch) == json({{"b", 2}}));
}
SECTION("add with a leading zero in an array index")
{
const json patch = R"([{"op": "add", "path": "/01", "value": 1}])"_json;
json doc = json::array();
#if !defined(JSON_NOEXCEPTION)
CHECK_THROWS_AS(doc.patch(patch), json::parse_error&);
CHECK_THROWS_AS(doc.patch_inplace(patch), json::parse_error&);
#endif
CHECK(doc == json::array());
}
SECTION("test compares numbers by value")
{
const json doc = R"({"a": 1, "b": 0, "c": 1.5})"_json;
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": 1.0}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/b", "value": -0.0}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/c", "value": 1.5}])"_json) == doc);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": 2}])"_json), json::other_error&);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": "1"}])"_json), json::other_error&);
}
SECTION("test ignores the order of object members, but not the order of array elements")
{
const json doc = R"({"o": {"x": 1, "y": 2}, "a": [1, 2]})"_json;
CHECK(doc.patch(R"([{"op": "test", "path": "/o", "value": {"y": 2, "x": 1}}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": [1, 2]}])"_json) == doc);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": [2, 1]}])"_json), json::other_error&);
}
SECTION("test on a path that does not exist")
{
const json doc = R"({"a": 1})"_json;
// with a "value" member, the failed lookup is a failed test
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": 1}])"_json), json::other_error&);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": null}])"_json), json::other_error&);
// without it, an exception is thrown, too; which one depends on the
// order of evaluation of the operands of ==, so it is not checked
CHECK_THROWS(doc.patch(R"([{"op": "test", "path": "/b"}])"_json));
}
SECTION("patch() has the strong guarantee")
{
// independent values, so that the comparison below checks the untouched document
const auto make_doc = []
{
return R"({"a": [1, 2], "o": {"k": "v"}})"_json;
};
const json doc = make_doc();
const json before = make_doc();
const std::vector<json> failing =
{
// remove of a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "remove", "path": "/missing"}])"_json,
// replace of a member that does not exist
R"([{"op": "remove", "path": "/a/0"}, {"op": "replace", "path": "/missing", "value": 1}])"_json,
// move from a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "move", "from": "/missing", "path": "/m"}])"_json,
// copy from a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "copy", "from": "/missing", "path": "/m"}])"_json,
// out-of-range array index
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "add", "path": "/a/5", "value": 1}])"_json,
// failing test after a successful operation
R"([{"op": "remove", "path": "/o"}, {"op": "test", "path": "/a", "value": []}])"_json,
};
for (const auto& patch : failing)
{
CAPTURE(patch.dump())
CHECK_THROWS_AS(doc.patch(patch), json::exception&);
CHECK(doc == before);
json copy = make_doc();
CHECK_THROWS_AS(copy.patch(patch), json::exception&);
CHECK(copy == before);
}
}
}
TEST_CASE("JSON patch - diff behavior documented in the API reference")
{
SECTION("path is a verbatim prefix")
{
// not escaped and not validated
CHECK(json::diff(1, 2) == R"([{"op": "replace", "path": "", "value": 2}])"_json);
CHECK(json::diff(1, 2, "abc") == R"([{"op": "replace", "path": "abc", "value": 2}])"_json);
CHECK(json::diff(1, 2, "/root") == R"([{"op": "replace", "path": "/root", "value": 2}])"_json);
// an escaped token in a non-root prefix is kept as given
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/x~1y/~0")
== R"([{"op": "replace", "path": "/x~1y/~0/a", "value": 2}])"_json);
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/p")
== R"([{"op": "replace", "path": "/p/a", "value": 2}])"_json);
}
SECTION("keys are escaped in the paths")
{
const json source = json::object();
const json target =
{
{"", 0}, {"/", 1}, {"~", 2}, {"~1", 3}, {"~01", 4}
};
const json patch = json::diff(source, target);
REQUIRE(patch.size() == 5);
// the order of the operations is not checked
json paths = json::object();
for (const auto& op : patch)
{
CHECK(op["op"] == "add");
paths[op["path"].get<std::string>()] = op["value"];
}
CHECK(paths == json({{"/", 0}, {"/~1", 1}, {"/~0", 2}, {"/~01", 3}, {"/~001", 4}}));
CHECK(source.patch(patch) == target);
}
SECTION("NaN is not equal to itself")
{
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
const json patch = json::diff(nan, nan);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_number_float());
CHECK(nan.patch(patch) != nan);
// a NaN in a patch is serialized as null
CHECK(json::parse(patch.dump())[0]["value"].is_null());
const json nested = {{"a", {1, nan}}};
const json nested_patch = json::diff(nested, nested);
CHECK(nested_patch.size() == 1);
CHECK(nested_patch[0]["path"] == "/a/1");
// infinity is equal to itself
const json inf = std::numeric_limits<json::number_float_t>::infinity();
CHECK(json::diff(inf, inf).empty());
}
SECTION("discarded values are not equal to themselves")
{
const json discarded(json::value_t::discarded);
const json patch = json::diff(discarded, discarded);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_discarded());
CHECK(discarded.patch(patch) != discarded);
}
SECTION("numbers that compare equal produce no operation")
{
CHECK(json::diff(json(1), json(1.0)).empty());
CHECK(json::diff(json(1.0), json(1)).empty());
CHECK(json::diff(json(1U), json(1)).empty());
CHECK(json::diff(json(1), json(1U)).empty());
CHECK(json::diff(json(0.0), json(-0.0)).empty());
// the patched value keeps the number type of the source
const json source = 1;
const json patched = source.patch(json::diff(source, json(1.0)));
CHECK(patched.is_number_integer());
CHECK(patched == json(1.0));
// different values are replaced
CHECK(json::diff(json(1), json(2.0)) == R"([{"op": "replace", "path": "", "value": 2.0}])"_json);
CHECK(json::diff(json(1), json(-1)) == R"([{"op": "replace", "path": "", "value": -1}])"_json);
CHECK(json::diff(json(1.0), json(1.5)) == R"([{"op": "replace", "path": "", "value": 1.5}])"_json);
CHECK(json::diff(json(1), json("1")) == R"([{"op": "replace", "path": "", "value": "1"}])"_json);
}
SECTION("binary values")
{
const json a = json::binary({1, 2});
const json b = json::binary({1, 3});
CHECK(json::diff(a, a).empty());
const json patch = json::diff(a, b);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_binary());
CHECK(a.patch(patch) == b);
// serialized, the binary value is an object
const json parsed = json::parse(patch.dump());
CHECK(parsed[0]["value"].is_object());
CHECK(parsed[0]["value"] == R"({"bytes": [1, 3], "subtype": null})"_json);
}
SECTION("a difference as a whole is a replace of the root")
{
CHECK(json::diff(json::array({1}), json::object()) == R"([{"op": "replace", "path": "", "value": {}}])"_json);
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
}
}
+228
View File
@@ -29,9 +29,64 @@ using nlohmann::json;
#include <map>
#include <random>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace
{
// the non-const operator[] must throw for pointer and leave the value parsed
// from init unchanged (strong exception safety)
template<typename Json>
void check_strong_guarantee(const char* init, const std::string& pointer, const char* message)
{
CAPTURE(init)
CAPTURE(pointer)
Json j = Json::parse(init);
const Json before = Json::parse(init);
const typename Json::json_pointer ptr(pointer);
CHECK_THROWS_WITH_AS(j[ptr], message, typename Json::exception&);
CHECK(j == before);
}
// as above, for an index that fits into size_type, but not into an array
template<typename Json>
void check_strong_guarantee_length_error(const char* init, const std::string& pointer)
{
CAPTURE(init)
CAPTURE(pointer)
Json j = Json::parse(init);
const Json before = Json::parse(init);
const typename Json::json_pointer ptr(pointer);
CHECK_THROWS_AS(j[ptr], std::length_error&);
CHECK(j == before);
}
template<typename Json>
void check_strong_guarantee_cases()
{
const char* const unresolved = "[json.exception.out_of_range.404] unresolved reference token '99999999999999999999999'";
// the first change converts a null value
check_strong_guarantee<Json>("null", "/99999999999999999999999", unresolved);
check_strong_guarantee<Json>("null", "/a/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":null})", "/a/b/-/99999999999999999999999", unresolved);
// the first change inserts a key into an object
check_strong_guarantee<Json>("{}", "/a/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":{"b":1}})", "/a/c/d/99999999999999999999999", unresolved);
// the first change pads or appends to an array
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/5/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/-/x/1/99999999999999999999999", unresolved);
// an index that does not fit into size_type
check_strong_guarantee<Json>("null", "/x/18446744073709551615",
"[json.exception.out_of_range.410] array index 18446744073709551615 exceeds size_type");
// an index that fits into size_type, but not into an array
const auto too_large = std::to_string((std::numeric_limits<typename Json::size_type>::max)() - 1);
check_strong_guarantee_length_error<Json>("null", "/" + too_large);
check_strong_guarantee_length_error<Json>(R"({"a":[[1]]})", "/a/0/3/" + too_large);
}
} // namespace
TEST_CASE("JSON pointers")
{
SECTION("errors")
@@ -493,6 +548,89 @@ TEST_CASE("JSON pointers")
}
}
SECTION("creating intermediate levels")
{
SECTION("tokens that are valid array indices create arrays")
{
json j;
j["/0"_json_pointer] = 1;
CHECK(j == json({1}));
json j2;
j2["/2"_json_pointer] = 1;
CHECK(j2 == json({nullptr, nullptr, 1}));
json j3;
j3["/-"_json_pointer] = 1;
CHECK(j3 == json({1}));
json j4;
j4["/foo/0/0"_json_pointer] = 1;
CHECK(j4 == json({{"foo", {{1}}}}));
}
SECTION("tokens that are no valid array indices create objects")
{
json j;
j["/one"_json_pointer] = 1;
CHECK(j == json({{"one", 1}}));
// leading '0' can never be a valid array index (RFC 6901, Sect. 4)
json j2;
j2["/01"_json_pointer] = 1;
CHECK(j2 == json({{"01", 1}}));
// the empty token is a valid object key, but no valid array index
json j3;
j3["/"_json_pointer] = 1;
CHECK(j3 == json({{"", 1}}));
}
SECTION("creating a level yields the same result as reusing it (#5357)")
{
json j;
j["/a/b/01/d"_json_pointer] = "value";
json j_init = json::object();
j_init["/a/b"_json_pointer] = json::object();
j_init["/a/b/01/d"_json_pointer] = "value";
const json expected = json::parse(R"({"a":{"b":{"01":{"d":"value"}}}})");
CHECK(j == expected);
CHECK(j_init == expected);
// unflatten uses the same key
const json flat = {{"/a/b/01/d", "value"}};
CHECK(flat.unflatten() == expected);
}
SECTION("existing arrays still reject invalid indices")
{
json j = {1, 2, 3};
CHECK_THROWS_WITH_AS(j["/01"_json_pointer],
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer),
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
}
SECTION("a failing operator[] leaves the value unchanged")
{
check_strong_guarantee_cases<json>();
check_strong_guarantee_cases<nlohmann::ordered_json>();
}
SECTION("a successful operator[] still creates the same values")
{
json j = {{"a", {1}}};
j["/a/3/x/-/y"_json_pointer] = 1;
CHECK(j == json::parse(R"({"a":[1,null,null,{"x":[{"y":1}]}]})"));
j["/a/-"_json_pointer] = 2;
j["/a/0"_json_pointer] = 3;
j["/b"_json_pointer] = 4;
CHECK(j == json::parse(R"({"a":[3,null,null,{"x":[{"y":1}]},2],"b":4})"));
}
}
SECTION("flatten")
{
json j =
@@ -779,6 +917,46 @@ TEST_CASE("JSON pointers")
CHECK(ptr.to_string() == "/object/~1");
}
SECTION("appending a pointer to itself")
{
// operator/= must not pass iterators into its own token vector to
// insert(); each pointer is appended to itself and must yield the
// pointer whose string representation is the original one twice
const std::vector<std::string> pointers =
{
"",
"/",
"/a",
"/a/b",
"/foo/0/-",
"/a~1b/~0c/~01",
"/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17"
};
for (const auto& s : pointers)
{
CAPTURE(s)
json::json_pointer ptr(s);
const json::json_pointer expected(s + s);
const json::json_pointer& ret = (ptr /= ptr);
CHECK(&ret == &ptr);
CHECK(ptr == expected);
CHECK(ptr.to_string() == s + s);
// appending to itself again doubles the pointer once more
ptr /= ptr;
std::string quadruple = s + s;
quadruple += quadruple;
CHECK(ptr.to_string() == quadruple);
// operator/ appends to a copy and leaves the operand unchanged
const json::json_pointer original(s);
CHECK((original / original) == expected);
CHECK(original.to_string() == s);
}
}
SECTION("equality comparison")
{
std::string ptr_string{"/foo/bar"};
@@ -1044,6 +1222,56 @@ TEST_CASE("flatten of structured values")
CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})"));
}
#if !defined(JSON_NOEXCEPTION)
SECTION("an object with the key \"0\" next to a non-index key does not round-trip")
{
// the third documented case in which j != j.flatten().unflatten()
const json value = {{"a", {{"0", 1}, {"b", 2}}}};
const json flat = value.flatten();
CHECK(flat == json({{"/a/0", 1}, {"/a/b", 2}}));
CHECK_THROWS_WITH_AS(flat.unflatten(),
"[json.exception.parse_error.109] parse error: array index 'b' is not a number", json::parse_error&);
// the same for other keys that are not valid array indices
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1a", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token '1a'", json::out_of_range&);
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/-", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&);
}
SECTION("unflatten with the empty token among array indices")
{
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1", 2}, {"/a/", 3}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
// without an index "0", the empty token is an ordinary object key
CHECK(json({{"/a/", 2}}).unflatten() == json({{"a", {{"", 2}}}}));
}
SECTION("value() with the empty pointer on a primitive root")
{
// unlike at(), value() rejects primitive values before it looks at the pointer
const json number = 1;
CHECK(number.at(""_json_pointer) == 1);
CHECK_THROWS_WITH_AS(number.value(""_json_pointer, 7),
"[json.exception.type_error.306] cannot use value() with number", json::type_error&);
const json string = "s";
CHECK_THROWS_WITH_AS(string.value(""_json_pointer, 7),
"[json.exception.type_error.306] cannot use value() with string", json::type_error&);
// contains() accepts the empty pointer on every value
CHECK(number.contains(""_json_pointer));
CHECK_FALSE(number.contains("/a"_json_pointer));
// arrays and objects return the default for a missing location
CHECK(json::array().value(""_json_pointer, json(7)) == json::array());
CHECK(json::array().value("/0"_json_pointer, 7) == 7);
}
#endif
SECTION("objects and arrays interleaved")
{
const json value =
+87 -50
View File
@@ -225,7 +225,7 @@ TEST_CASE("json_view")
#if JSON_DIAGNOSTICS
// the parents are set, so errors name the path
const json m = materialized<json_document>(R"({"a":{"b":[1]}})");
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), "[json.exception.type_error.304] (/a/b/0) cannot use at() with number", json::type_error&);
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), json_view_test::message("type_error.304", "/a/b/0", 11, 12, "cannot use at() with number").c_str(), json::type_error&);
#endif
}
@@ -666,6 +666,61 @@ TEST_CASE("json_view")
CHECK(small.root().materialize() == json::parse("[1,[2,3],{\"a\":\"b\\n\"}]"));
}
SECTION("block sizes")
{
// documents whose node estimate lies around the size that is allocated
// with the header, and whose decoded strings need the first growth of
// the buffer, in all sizes around the points where the policies change
for (const int count :
{
1, 2, 10, 50, 100, 255, 256, 257, 500, 1000, 1023, 1024, 1025, 2000, 5000
})
{
// strings with escapes (decoded text, minified)
std::string escaped = "[";
for (int i = 0; i < count; ++i)
{
escaped += (i != 0 ? ",\"a\\n" : "\"a\\n") + std::to_string(i) + "\\u00e4\"";
}
escaped += ']';
json_document e = json_document::parse(escaped);
CHECK(e.root().materialize() == json::parse(escaped));
e.shrink_to_fit();
CHECK(e.root().materialize() == json::parse(escaped));
// dense numbers (more nodes per byte than the estimate expects)
std::string dense = "[";
for (int i = 0; i < count; ++i)
{
dense += (i != 0 ? ",1" : "1");
}
dense += ']';
json_document n = json_document::parse(dense);
CHECK(n.node_count() == static_cast<std::size_t>(count) + 1);
CHECK(n.root().materialize() == json::parse(dense));
n.shrink_to_fit();
CHECK(n.root().size() == static_cast<std::size_t>(count));
// the same documents, pretty-printed
const std::string pretty = json::parse(escaped).dump(2);
json_document p = json_document::parse(pretty);
CHECK(p.root().materialize() == json::parse(escaped));
}
// a large document with a single escape does not need a buffer of the
// size of its input
std::string large = R"(["a\n")";
for (int i = 0; i < 100000; ++i)
{
large += ",1";
}
large += ']';
const json_document l = json_document::parse(large);
CHECK(l.root().size() == 100001);
CHECK(l.root()[0].get_string() == "a\n");
CHECK(l.root()[100000].get<int>() == 1);
}
SECTION("source offsets")
{
const std::string text = R"( {"key": "value", "escaped": "a\nb", "n": 42})";
@@ -694,6 +749,7 @@ std::string exception_of(F f)
}
return "";
}
#endif
// compares a view with the ordered_json value materialize() gives for it:
@@ -914,7 +970,8 @@ TEST_CASE("json_view element access and iteration")
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()); }) == exception_of([&] { static_cast<void>(j.begin().key()); }));
// (without the context of basic_json: view iterators do not know the value they iterate over)
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == (j.is_object() ? "" : "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators"));
}
if (!j.empty() || j.is_null())
{
@@ -931,8 +988,8 @@ TEST_CASE("json_view element access and iteration")
const json_document d = json_document::parse(R"({"a":[]})");
CHECK(d.root()["b"].is_discarded());
CHECK(d.root()["a"][0].is_discarded());
CHECK_THROWS_WITH_AS(d.root()["a"].front(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.root()["a"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.root()["a"].front(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.root()["a"].back(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
const json_view invalid{};
CHECK(invalid.begin() == invalid.end());
CHECK(std::string(invalid.type_name()) == "discarded");
@@ -963,10 +1020,10 @@ TEST_CASE("json_view element access and iteration")
CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded());
#if !defined(JSON_NOEXCEPTION)
// type errors on values that are not discarded stay
CHECK_THROWS_WITH_AS(v[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
CHECK_THROWS_WITH_AS(v["s"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", json::type_error&);
CHECK_THROWS_WITH_AS(v["s"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", json::type_error&);
CHECK_THROWS_WITH_AS(v[0], json_view_test::message("type_error.305", "", 0, 29, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], json_view_test::message("type_error.305", "/a/b", 10, 17, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["s"]["c"], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a string argument with string").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["s"][0], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a numeric argument with string").c_str(), json::type_error&);
CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&);
CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&);
// at() keeps throwing on a discarded view
@@ -1041,9 +1098,9 @@ TEST_CASE("json_view element access and iteration")
CHECK(v[(std::numeric_limits<std::size_t>::max)()].is_discarded());
CHECK(v[std::numeric_limits<int>::max()].is_discarded());
#if !defined(JSON_NOEXCEPTION)
CHECK_THROWS_WITH_AS(v.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(3u), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(3), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(3u), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_AS(v.at(-1), json::out_of_range&);
CHECK_THROWS_AS(v.at(-1L), json::out_of_range&);
CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&);
@@ -1177,26 +1234,6 @@ TEST_CASE("json_view element access and iteration")
namespace
{
#if !defined(JSON_NOEXCEPTION)
// an exception message without the context that basic_json adds with
// JSON_DIAGNOSTICS ("(/path) ") and JSON_DIAGNOSTIC_POSITIONS ("(bytes 1-2) ");
// the view's exceptions have no such context
std::string without_path(std::string msg)
{
for (const char* prefix :
{"] (/", "] (bytes "
})
{
const std::size_t open = msg.find(prefix);
if (open != std::string::npos)
{
msg.erase(open + 2, msg.find(") ", open) + 2 - (open + 2));
}
}
return msg;
}
#endif
// the bits of a float, to compare values bit for bit
std::uint64_t bits(double x)
{
@@ -1267,18 +1304,18 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std::
// conversions to the wrong type throw what basic_json throws
if (!j.is_number())
{
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<int>()); })));
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == exception_of([&] { static_cast<void>(j.get<int>()); }));
}
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<bool>()); })));
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::string>()); })));
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); })));
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == exception_of([&] { static_cast<void>(j.get<bool>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == exception_of([&] { static_cast<void>(j.get<std::string>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); }));
if (!j.is_array())
{
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); })));
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); }));
}
if (!j.is_object())
{
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); })));
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); }));
}
#endif
@@ -1421,8 +1458,8 @@ TEST_CASE("json_view values")
CHECK(v[3].number_token() == "123456789012345678901234567890");
CHECK(v[4].number_token() == "-12");
CHECK(v[5].number_token() == "7");
CHECK_THROWS_WITH_AS(v[6].number_token(), "[json.exception.type_error.302] type must be number, but is string", json::type_error&);
CHECK_THROWS_WITH_AS(v.get_string(), "[json.exception.type_error.302] type must be string, but is array", json::type_error&);
CHECK_THROWS_WITH_AS(v[6].number_token(), json_view_test::message("type_error.302", "/6", 56, 59, "type must be number, but is string").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v.get_string(), json_view_test::message("type_error.302", "", 0, 60, "type must be string, but is array").c_str(), json::type_error&);
}
SECTION("conversions")
@@ -1496,15 +1533,15 @@ TEST_CASE("json_view values")
CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0));
CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3));
#if !defined(JSON_NOEXCEPTION)
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); })));
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast<void>(j.value("s", 0)); })));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", 0)); })));
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); })));
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == exception_of([&] { static_cast<void>(j.value("s", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == exception_of([&] { static_cast<void>(j["n"].value("x", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); }));
// string defaults: the member is not a string, or the value is neither an object nor an array
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value("n", "default")); })));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", "default")); })));
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); })));
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); })));
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == exception_of([&] { static_cast<void>(j.value("n", "default")); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == exception_of([&] { static_cast<void>(j["n"].value("x", "default")); }));
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); }));
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); }));
#endif
// (defaults and members longer than the short string optimization)
CHECK(v.value("s", "a default longer than fifteen characters") == "text");
@@ -1563,7 +1600,7 @@ TEST_CASE("json_view JSON pointers")
{
CAPTURE(pointer)
const json::json_pointer p(pointer);
const std::string at_error = without_path(exception_of([&] { static_cast<void>(j.at(p)); }));
const std::string at_error = exception_of([&] { static_cast<void>(j.at(p)); });
CHECK(exception_of([&] { static_cast<void>(v.at(p)); }) == at_error);
if (at_error.empty())
{
@@ -1577,7 +1614,7 @@ TEST_CASE("json_view JSON pointers")
}
else
{
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == without_path(exception_of([&] { static_cast<void>(j[p]); })));
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == exception_of([&] { static_cast<void>(j[p]); }));
}
// (basic_json::contains() throws out_of_range.404 for an empty
// array index token, although it is not meant to throw; the view
@@ -1588,7 +1625,7 @@ TEST_CASE("json_view JSON pointers")
static_cast<void>(found);
});
CHECK(v.contains(p) == (contains_error.empty() && j.contains(p)));
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast<void>(j.value(p, 5)); })));
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == exception_of([&] { static_cast<void>(j.value(p, 5)); }));
}
}
#endif
+249 -3
View File
@@ -13,6 +13,7 @@
#include <nlohmann/detail/view/builder.hpp>
#include <nlohmann/detail/view/materialize.hpp>
#include <nlohmann/detail/view/string_ref.hpp>
#include <nlohmann/json_view.hpp> // json_document::accept
#else
#include <nlohmann/json_view.hpp> // the single header contains the internal headers
#endif
@@ -59,7 +60,8 @@ built build(const std::string& text, bool comments, bool trailing_commas, bool s
r.copy.assign(text.begin(), text.end());
src = r.copy.data();
}
r.ok = nlohmann::detail::view::build < FloatType, !nlohmann::detail::abi_config::strict_nul_handling > (*r.data, src, text.size(), comments, trailing_commas, sentinel, r.failure);
constexpr bool nul_is_end = !nlohmann::detail::abi_config::strict_nul_handling;
r.ok = nlohmann::detail::view::build<FloatType, nul_is_end>(*r.data, src, text.size(), comments, trailing_commas, sentinel, false, r.failure);
r.data->src = src;
r.data->base[0] = src;
r.data->base[1] = r.data->arena.data();
@@ -172,6 +174,22 @@ void check_string(const std::string& content)
}
}
// accept() stores no nodes: it must accept and reject what json::accept does, for all options and for inputs with
// and without a terminating NUL
void check_accept(const std::string& text)
{
CAPTURE(text)
const std::vector<char> bytes(text.begin(), text.end());
for (int options = 0; options < 4; ++options)
{
const bool comments = (options & 1) != 0;
const bool trailing_commas = (options & 2) != 0;
const bool accepted = json::accept(text, comments, trailing_commas);
CHECK(nlohmann::json_document::accept(text, comments, trailing_commas) == accepted);
CHECK(nlohmann::json_document::accept(bytes, comments, trailing_commas) == accepted);
}
}
// a small deterministic generator of documents
struct generator
{
@@ -423,6 +441,133 @@ TEST_CASE("json_view builder")
}
}
SECTION("accept() without an index")
{
for (const char* text :
{
"", " ", "\n", "null", "nul", "true", "[", "]", "[]", "{}", "[1,2,3]", "[1,2,3", "[1,2,3,]", "{\"a\":1,}", "{\"a\"}", "{\"a\":}",
"[1] x", "[1][2]", "\xEF\xBB\xBF[1]", "[\"a\\", R"(["\u12"])", R"(["\ud800"])", "[\"\x01\"]", "[\"\xff\"]", "[01]", "[1.]",
"[.5]", "[-]", "[1e]", "// c\n[1]", "/* c */ {\"a\" /* x */ : 1}", "/* c", "[1] // c", "1e400", "-1e400", "123456789012345678901234567890",
R"({"a":1,"a":2})", "[[[]]]", R"("\u00e4\n\ud83d\ude00")"
})
{
check_accept(text);
}
// a null byte inside the text, and after it
check_accept(std::string("[1,\0]", 5));
check_accept(std::string("[1]\0", 4));
check_accept(std::string("\0", 1));
// generated documents and damaged copies, as for the builder above
generator g;
for (int i = 0; i < 2000; ++i)
{
std::string text;
g.value(text, 0);
check_accept(text);
std::string damaged = text;
const auto at = static_cast<std::size_t>(g.r(static_cast<int>(damaged.size())));
static const char replacements[] = {'x', '"', '\\', ',', ':', ']', '}', '[', '{', '1', '-', '.', 'e', '\0', '\n', '/'}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
damaged[at] = replacements[g.r(16)];
check_accept(damaged);
check_accept(text.substr(0, at));
}
// nesting beyond the 64 levels that are kept on the builder's own stack, closed and not closed
for (const std::size_t depth :
{
63u, 64u, 65u, 66u, 200u, 1000u
})
{
check_accept(std::string(depth, '[') + std::string(depth, ']'));
check_accept(std::string(depth, '[') + std::string(depth - 1, ']'));
std::string objects;
for (std::size_t i = 0; i < depth; ++i)
{
objects += "{\"k\":";
}
objects += "1" + std::string(depth, '}');
check_accept(objects);
check_accept(objects.substr(0, objects.size() - 1));
}
// objects with enough members to get a hash index when they are indexed (and duplicate keys)
std::string large = "{";
for (int i = 0; i < 300; ++i)
{
large += (i == 0 ? "\"k" : ",\"k") + std::to_string(i % 200) + "\":" + std::to_string(i);
}
check_accept(large + "}");
check_accept(large);
check_accept("[" + large + "},{" + large.substr(1) + "}]");
// the overflow check uses the floating-point type of the document
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
for (const char* text :
{
"1e39", "-1e39", "3.4028235e38", "3.4028236e38", "[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})", "340282356779733661637539395458142568448"
})
{
CAPTURE(text)
CHECK(nlohmann::basic_json_document<float_json>::accept(text) == float_json::accept(text));
}
// strings are checked and not decoded: runs of text of every length from 0 to 40 between valid and invalid
// escapes, UTF-8 sequences and control characters, in values and in keys, cut near the end of the text
const std::array<const char*, 9> valid = {{"\\n", "\\\"", "\\\\", "\\/", "\\t", "\\u00e4", "\\u20ac", "\\ud83d\\ude00", "\\u0000"}};
const std::array<const char*, 12> invalid = {{"\\x", "\\ud800", "\\udc00", "\\u12G4", "\\u12", "\\ud83dx", "\\ud83d\\u0041", "\\ud83d\\ud83d", "\\", "\x01", "\x1f", "\x7f"}};
const std::array<const char*, 6> utf8 = {{"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xc3", "\xff", "\x80"}};
std::mt19937 rng(2029); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
for (std::size_t run = 0; run <= 40; ++run)
{
for (int i = 0; i < 30; ++i)
{
std::string content = std::string(rng() % 20, 'a');
const auto pieces = 1 + (rng() % 6);
for (unsigned k = 0; k < pieces; ++k)
{
content += valid[rng() % valid.size()];
content += std::string(k == 0 ? run : rng() % 41, static_cast<char>('b' + (rng() % 20)));
switch (rng() % 8)
{
case 0:
content += utf8[rng() % 3];
break;
case 1:
content += utf8[rng() % utf8.size()];
break;
case 2:
content += invalid[rng() % invalid.size()];
break;
default:
break;
}
}
for (const std::string& text :
{
"[\"" + content + "\"]", "{\"" + content + "\":1}", "[\"" + content, "[\"" + content.substr(0, content.size() - (rng() % 4)) + "\"]"
})
{
check_accept(text);
}
}
}
}
SECTION("escapes in a row")
{
for (const char* content :
{
"\\n", R"(\n\t\\)", R"(\u00e9\u00e9\u4e2d)", "\\u00e9\\u00e", "\\u00e9\\", "\\n\\q", R"(\n\ud83d\ude00\n)",
R"(\ud83d\ude00\ud83d)", R"(\n\n\n abc \n\n)", "\\n\x01", "\\n\xC3\x28", "\\n\\"
})
{
check_accept(std::string("[\"") + content + "\"]");
check_accept(std::string("[\"") + content);
check_accept(std::string("{\"") + content + "\":1}");
}
}
SECTION("test files")
{
for (const char* name :
@@ -523,6 +668,54 @@ TEST_CASE("json_view builder: strings across vector blocks")
}
}
TEST_CASE("json_view builder: fraction digits")
{
// The digits of a fraction are skipped 16 bytes at a time where there are
// that many left (SSE2), and one at a time after that. Fractions of every
// length from 0 to 40 digits, followed by every kind of byte that ends a
// number or lies next to the digits in ASCII ('/' and ':'), as values, as
// keys' neighbours, and at the very end of the text (a vector without a
// terminating NUL). The length of the number is part of the value that
// the test compares with json::parse().
const std::string followers = ",]}/:.eE+-x \n\t\"09";
std::mt19937 rng(2028); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
for (std::size_t digits = 0; digits <= 40; ++digits)
{
for (int i = 0; i < 3; ++i)
{
std::string fraction;
for (std::size_t k = 0; k < digits; ++k)
{
fraction += static_cast<char>('0' + (rng() % 10));
}
const std::string number = std::string(rng() % 2 == 0 ? "-" : "") + std::to_string(rng() % 100000) + "." + fraction;
check_same(number);
check_same("[" + number + "]");
std::string pair;
pair.reserve((number.size() * 2) + 3);
pair += '[';
pair += number;
pair += ',';
pair += number;
pair += ']';
check_same(pair);
check_same("{\"a\":" + number + "}");
for (const char follower : followers)
{
check_same(number + follower);
check_same("[" + number + follower + "]");
check_same("[" + number + follower + std::string(rng() % 20, '1'));
}
// everything near the end of the text: the number cut in every place
for (std::size_t cut = 1; cut <= 18 && cut <= number.size(); ++cut)
{
check_same(number.substr(0, number.size() - cut));
check_same("[" + number.substr(0, number.size() - cut));
}
}
}
}
TEST_CASE("json_view builder: escapes between runs of text")
{
// The text between escapes is copied 16 bytes at a time where there are
@@ -575,6 +768,59 @@ TEST_CASE("json_view builder: escapes between runs of text")
}
}
TEST_CASE("json_view builder: chains of unicode escapes")
{
// Consecutive \uXXXX escapes are decoded in a loop of their own. Chains of
// every length from 1 to 40 of escapes that give one, two, and three
// bytes of UTF-8, in upper and lower case, followed by what ends a chain:
// the end of the string, text, another escape, UTF-8, and everything that
// is not a valid escape (a bad digit at every position of the last
// escape, a lone surrogate, a surrogate pair, a cut in every place).
const std::array<const char*, 8> chain = {{"\\u0041", "\\u00e4", "\\u00E4", "\\u20ac", "\\u3042", "\\uFFFF", "\\u0000", "\\u07ff"}};
const std::array<const char*, 12> ends = {{"", "abc", "\\n", "\\u00e9", "\\ud83d\\ude00", "\\ud83d", "\\ude00", "\\ud83d\\u0041", "\\u12G4", "\\u12", "\xc3\xa4", "\\"}};
std::mt19937 rng(2027); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
for (std::size_t length = 1; length <= 40; ++length)
{
for (int i = 0; i < 20; ++i)
{
std::string content = std::string(rng() % 20, 'a');
for (std::size_t k = 0; k < length; ++k)
{
content += chain[rng() % chain.size()];
}
const std::string whole = content;
for (const char* end : ends)
{
check_string(whole + end);
check_string(whole + end + std::string(rng() % 19, 'z'));
}
// a bad digit at every position of every escape near the end of the chain
for (std::size_t back = 1; back <= 6 && back <= length; ++back)
{
for (std::size_t digit = 2; digit < 6; ++digit)
{
std::string broken = whole;
broken[broken.size() - (6 * back) + digit] = "gG\"\x01 z"[rng() % 6];
check_string(broken);
}
}
// the text cut in every place of the last three escapes
for (std::size_t cut = 0; cut <= 18 && cut <= whole.size(); ++cut)
{
check_string(whole.substr(0, whole.size() - cut));
}
}
}
// a chain longer than the first growth of the arena, in a key and in a value
std::string long_chain;
for (int i = 0; i < 3000; ++i)
{
long_chain += chain[static_cast<std::size_t>(i) % chain.size()];
}
check_string(long_chain);
check_string(long_chain + "\\ud83d\\ude00" + long_chain);
}
TEST_CASE("json_view node integer bits")
{
using nlohmann::detail::view::integer_bits;
@@ -592,7 +838,7 @@ TEST_CASE("json_view node integer bits")
})
{
CAPTURE(v)
n.kind = 0x5A;
n.kind = nlohmann::detail::view::node_kind::link;
n.flags = 0xA5;
n.extra = 0x1234;
n.off = 0x89ABCDEFu;
@@ -601,7 +847,7 @@ TEST_CASE("json_view node integer bits")
CHECK(n.len == static_cast<std::uint32_t>(v));
CHECK(n.next == static_cast<std::uint32_t>(v >> 32));
// the other fields are untouched
CHECK(n.kind == 0x5A);
CHECK(n.kind == nlohmann::detail::view::node_kind::link);
CHECK(n.flags == 0xA5);
CHECK(n.extra == 0x1234);
CHECK(n.off == 0x89ABCDEFu);
+364
View File
@@ -0,0 +1,364 @@
// __ _____ _____ _____
// __| | __| | | | 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
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::ordered_json_editable_document;
using nlohmann::json_view;
using nlohmann::ordered_json_document;
#include "json_view_test_helpers.hpp"
using json_view_test::generator;
#include <array>
#include <cstddef>
#include <string>
#include <vector>
// These tests were split off unit-json_view_dump.cpp, whose object file got too
// large for the MinGW assembler ("file too big"). Like that file, this one is
// also built with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("objects whose first keys differ or repeat")
{
const std::array<const char*, 17> texts =
{
{
R"({"a":1,"b":2})", R"({"a":1,"c":2})", R"({"b":1,"a":2})", R"({"a":1,"a":2,"b":3})", R"({"a":2,"b":3})",
R"({"a":1,"b":1,"a":2})", R"({"a":2,"b":1})", R"({"a":1,"a":2})", R"({"b":1})", R"({"a":1,"b":2,"c":3,"d":4,"e":5})",
R"({"a":1,"b":2,"c":3,"d":4,"f":5})", R"({"a":1,"b":2,"c":3,"a":4,"e":5})", R"({"a":4,"b":2,"c":3,"e":5})",
R"({"\u0061":1,"b":2})", R"({})", R"([{"a":1,"b":2}])", R"([{"a":1,"c":2}])"
}
};
for (const char* const x : texts)
{
for (const char* const y : texts)
{
CAPTURE(x)
CAPTURE(y)
const ordered_json_document dx = ordered_json_document::parse(x);
const ordered_json_document dy = ordered_json_document::parse(y);
const ordered_json_editable_document ex = ordered_json_editable_document::parse(x);
const ordered_json jx = ordered_json::parse(x);
const ordered_json jy = ordered_json::parse(y);
CHECK((dx.root() == dy.root()) == (jx == jy));
CHECK((dx.root() == jy) == (jx == jy));
CHECK((ex.root() == dy.root()) == (jx == jy));
}
}
}
SECTION("objects with duplicate keys, small and large")
{
// n members with keys in descending order; with duplicates, some keys
// repeat later (the first after unique ones in key order); in reverse,
// the members are in ascending key order
const auto object = [](int n, bool duplicates, bool reverse, int value)
{
std::string s = "{";
for (int i = 0; i < n; ++i)
{
const int k = reverse ? i : n - 1 - i;
s += (i != 0 ? ",\"k" : "\"k") + std::to_string(100 + k) + "\":" + std::to_string(value + k);
}
if (duplicates)
{
for (int k = n / 2; k < n; k += 3)
{
s += ",\"k" + std::to_string(100 + k) + "\":" + std::to_string(value + k);
}
}
return s + "}";
};
std::vector<std::string> texts;
for (const int n :
{
3, 16, 17, 40
})
{
for (const int flags :
{
0, 1, 2, 3
})
{
texts.push_back(object(n, (flags & 1) != 0, (flags & 2) != 0, 0));
}
texts.push_back(object(n, true, false, 1)); // other values
}
// many objects in one comparison (working memory reused across objects)
std::string array = "[";
std::string array_other = "[";
for (std::size_t i = 0; i < texts.size(); ++i)
{
array += (i != 0 ? "," : "") + texts[i];
array_other += (i != 0 ? "," : "") + texts[texts.size() - 1 - i];
}
texts.push_back(array + "]");
texts.push_back(array_other + "]");
for (const std::string& x : texts)
{
for (const std::string& y : texts)
{
CAPTURE(x)
CAPTURE(y)
const ordered_json_document dx = ordered_json_document::parse(x);
const ordered_json_document dy = ordered_json_document::parse(y);
const ordered_json jx = ordered_json::parse(x);
const ordered_json jy = ordered_json::parse(y);
CHECK((dx.root() == dy.root()) == (jx == jy));
CHECK((dx.root() == jy) == (jx == jy));
const json_document ux = json_document::parse(x);
const json uy = json::parse(y);
CHECK((ux.root() == uy) == (json::parse(x) == uy));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("values that are the same text, or the same value written differently, at any depth")
{
// nested arrays and objects around depths where the walk over equal text gives up
for (const int depth :
{
1, 5, 47, 48, 49, 50, 60, 300
})
{
CAPTURE(depth)
std::string head;
std::string tail;
for (int i = 0; i < depth; ++i)
{
head += i % 2 == 0 ? R"({"k":[1,"s",{"x":true},)" : R"([null,2.5,{"y":)";
tail.insert(0, i % 2 == 0 ? "]}" : "}]");
}
// the same text, one number changed, and one number written differently
const auto with_number = [&](const char* number)
{
std::string result = head;
result += number;
result += tail;
return result;
};
const std::string text = with_number("7");
const std::string changed = with_number("8");
const std::string spelled = with_number("7.0");
const json_document a = json_document::parse(text);
const json_document b = json_document::parse(text);
const json_document c = json_document::parse(changed);
const json_document d = json_document::parse(spelled);
CHECK(a.root() == b.root());
CHECK(a.root() != c.root());
CHECK(c.root() != a.root());
CHECK(a.root() == d.root());
CHECK(d.root() == a.root());
CHECK(a.root() == json::parse(text));
CHECK(a.root() != json::parse(changed));
CHECK(json::parse(spelled) == a.root());
const nlohmann::json_editable_document e = nlohmann::json_editable_document::parse(text);
CHECK(a.root() == e.root());
CHECK(e.root() == a.root());
CHECK(c.root() != e.root());
CHECK(e.root() == e.root());
const ordered_json_document oa = ordered_json_document::parse(text);
const ordered_json_document oc = ordered_json_document::parse(changed);
CHECK(oa.root() != oc.root());
CHECK(oa.root() == ordered_json::parse(text));
CHECK(oc.root() != ordered_json::parse(text));
}
// not the same text, but the same value
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
const bool equal = dx.root() == dy.root();
CHECK(equal == (json::parse(x) == json::parse(y)));
CHECK(equal == (dx.root() == json::parse(y)));
return equal;
};
CHECK(same(R"({"a":1,"a":2})", R"({"a":3,"a":2})"));
CHECK(same(R"({"a":1,"b":[1,{"c":2,"c":3}]})", R"({"b":[1,{"c":9,"c":3}],"a":1})"));
CHECK(!same(R"({"a":1,"a":2})", R"({"a":2,"a":3})"));
CHECK(same("[1.0,-0,0,1e2]", "[1.00,0,-0,100.0]"));
CHECK(same(R"(["\u00e9","\n"])", "[\"\xc3\xa9\",\"\\n\"]"));
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":false}]})"));
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"c":null}]})"));
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":null,"c":1}]})"));
// edited documents: moved sequences, links, and values of new nodes
nlohmann::json_editable_document edited = nlohmann::json_editable_document::parse(R"({"a":[1,2,3],"b":{"c":"d"},"e":0.5})");
const json_document plain = json_document::parse(R"({"a":[1,2,3,4],"b":{"c":"x","y":null},"e":0.5,"z":[]})");
edited.push_back(edited.root()["a"], 4);
edited.set(edited.root()["b"], "c", "x");
edited.set(edited.root()["b"], "y", nullptr);
edited.set(edited.root(), "z", json::array());
CHECK(edited.root() == plain.root());
CHECK(plain.root() == edited.root());
edited.erase(edited.root()["a"], 0);
CHECK(edited.root() != plain.root());
CHECK(edited.root()["a"] == json::parse("[2,3,4]"));
const json_document tail = json_document::parse("[2,3,4]");
CHECK(edited.root()["a"] == tail.root());
}
SECTION("large documents, with a difference at any place")
{
const auto make = [](std::size_t count, std::size_t changed, bool swap_members)
{
std::string text = R"({"b":1,"list":[)";
for (std::size_t i = 0; i < count; ++i)
{
text += (i != 0 ? "," : "");
const std::string id = std::to_string(i == changed ? i + 1 : i);
if (swap_members && i % 3 == 0)
{
text += R"({"v":[1.5,"s",{"k":)";
text += id;
text += R"(}],"id":)";
text += id;
text += "}";
}
else
{
text += R"({"id":)";
text += id;
text += R"(,"v":[1.5,"s",{"k":)";
text += id;
text += "}]}";
}
}
return text + R"(],"a":{"y":2,"x":3}})";
};
const std::size_t count = 3000;
for (const std::size_t changed :
{
count, std::size_t{0}, std::size_t{1}, count / 2, count - 1
})
{
for (const bool swap :
{
false, true
})
{
CAPTURE(changed)
CAPTURE(swap)
const std::string text_a = make(count, count, false);
const std::string text_b = make(count, changed, swap);
const json_document a = json_document::parse(text_a);
const json_document b = json_document::parse(text_b);
const bool equal = changed == count;
CHECK((a.root() == b.root()) == equal);
CHECK((b.root() == a.root()) == equal);
CHECK((a.root() == json::parse(text_b)) == equal);
CHECK((json::parse(text_b) == a.root()) == equal);
CHECK((a.root()["list"] == b.root()["list"]) == equal);
const ordered_json_document oa = ordered_json_document::parse(text_a);
const ordered_json_document ob = ordered_json_document::parse(text_b);
CHECK((oa.root() == ob.root()) == (equal && !swap));
CHECK((oa.root() == ordered_json::parse(text_b)) == (equal && !swap));
}
}
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
+85 -767
View File
@@ -35,9 +35,9 @@ using json_view_test::has_duplicate_keys;
#include <string>
#include <vector>
// These tests were split off unit-json_view.cpp, whose object file got too large
// for the MinGW linker (see json_view_test_helpers.hpp). Like that file, this one
// is also built with C++17, as it mentions JSON_HAS_CPP_17.
// These tests were split off unit-json_view.cpp, whose object file got too large for the MinGW
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
// with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view dump")
{
@@ -150,6 +150,88 @@ TEST_CASE("json_view dump")
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
}
// tokens of 16 and 17 digits without an exponent (like GeoJSON
// coordinates), which dump() converts in batches: doubles exactly
// halfway between two candidates for the shortest digits, random
// digits, and digits near a shorter decimal (...99999997,
// ...00000003), whose rounding carries; in pairs and among other
// values, so that the batches are interrupted; the last token near the
// end of the text
{
std::mt19937_64 tokens(5627); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
const auto draw = [&tokens](std::size_t n)
{
const std::uint64_t r = tokens() % n;
return static_cast<std::size_t>(r);
};
std::string coordinates = "[754570942.33984373,257769445.56835938,858106901.94921875,97944964.876953125,"
"[0.5,1e-7],[1.5,-0.0,2.5],[1E2,12.5,0.1,3.25,4.5,5.75,6.125,7.0,8.5],[],[1.5],[1,2.5],[2.5,\"x\"],[[1.5],2.5],";
for (int i = 0; i < 20000; ++i)
{
const std::size_t length = 16 + draw(2);
std::string digits(1, static_cast<char>('1' + draw(9)));
const std::size_t kept = draw(3) == 0 ? length : 2 + draw(length - 3);
for (std::size_t k = 1; k < kept; ++k)
{
digits += static_cast<char>('0' + draw(10));
}
digits += std::string(length - kept, draw(2) == 0 ? '9' : '0');
digits.back() = static_cast<char>('0' + draw(10));
const std::size_t point = 1 + draw(9);
const std::string token = (draw(2) == 0 ? "-" : "") + digits.substr(0, point) + "." + digits.substr(point);
if (i != 0)
{
coordinates += ',';
}
if (draw(8) == 0)
{
coordinates += R"({"x":)";
coordinates += token;
coordinates += R"(,"s":"a"})";
}
else
{
coordinates += '[';
coordinates += token;
coordinates += ',';
coordinates += token;
coordinates += ']';
}
}
coordinates += ",-65.613616999999977]";
const json_document parsed = json_document::parse(coordinates);
const ordered_json j = ordered_json::parse(coordinates);
CHECK(parsed.root().dump() == j.dump());
CHECK(parsed.root().dump(1) == j.dump(1));
CHECK(parsed.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(parsed.root().back().get<double>() == j.back().get<double>());
CHECK(parsed.root().back().dump() == "-65.61361699999998");
std::size_t i = 0;
for (const json_view v : parsed.root())
{
const ordered_json& e = j[i++];
CHECK(v.dump() == e.dump());
if (v.is_array() && !v.empty() && v[0].is_number())
{
CHECK(v[0].get<double>() == e[0].get<double>());
}
}
// an editable document: edits move some arrays, so that the walk
// leaves the order of the parsed nodes
ordered_json_editable_document editable = ordered_json_editable_document::parse(coordinates);
ordered_json expected = j;
for (std::size_t k = 0; k < 20000; k += 97)
{
if (expected[k].is_array())
{
editable.push_back(editable.root()[k], ordered_json(1.5));
expected[k].push_back(1.5);
}
}
CHECK(editable.root().dump() == expected.dump());
}
// random doubles, written as parse() and dump() would
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::string many = "[";
@@ -171,768 +253,4 @@ TEST_CASE("json_view dump")
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
}
SECTION("members in document order, all of them")
{
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
// dump() writes all members, though the lookup finds the first
CHECK(d.root()["b"].dump() == "1");
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document deep_document = json_document::parse(deep);
CHECK(deep_document.root().dump() == deep);
}
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
{
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
for (const char* const special :
{
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
})
{
for (std::size_t length = 0; length <= 40; ++length)
{
for (std::size_t position = 0; position <= length; ++position)
{
std::string content(length, 'a');
content.insert(position, special);
const std::array<std::string, 3> texts =
{
{
"\"" + content + "\"",
"[\"" + content + "\"]",
"{\"" + content + "\":1}",
}
};
for (const std::string& text : texts)
{
CAPTURE(text)
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
CHECK(d.root().dump() == j.dump());
}
}
}
}
}
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
{
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
std::string text = "[";
std::size_t in_chunk = 0;
const auto append = [&](std::uint32_t cp)
{
if (cp < 0x80)
{
if (cp < 0x20 || cp == '"' || cp == '\\')
{
return;
}
text += static_cast<char>(cp);
}
else if (cp < 0x800)
{
text += static_cast<char>(0xC0 | (cp >> 6));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
else if (cp < 0x10000)
{
text += static_cast<char>(0xE0 | (cp >> 12));
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
else
{
text += static_cast<char>(0xF0 | (cp >> 18));
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
};
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
{
if (cp >= 0xD800 && cp < 0xE000)
{
continue;
}
if (in_chunk == 0)
{
text += text.size() > 1 ? ",\"" : "\"";
}
append(cp);
if (++in_chunk == 37)
{
text += "\"";
in_chunk = 0;
}
}
text += in_chunk != 0 ? "\"]" : "]";
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(2) == j.dump(2));
}
SECTION("deep nesting, indented")
{
// indentations of up to and beyond the width of the writer's fixed-size copy
for (const int depth :
{
1, 5, 20, 70, 300
})
{
std::string text;
for (int i = 0; i < depth; ++i)
{
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
}
text += "[]";
for (int i = depth - 1; i >= 0; --i)
{
text += i % 2 == 0 ? "]" : "}";
}
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
for (const int indent :
{
0, 1, 2, 3, 8
})
{
CHECK(d.root().dump(indent) == j.dump(indent));
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
}
}
}
SECTION("the output buffer of a small value is small")
{
// an escaped key after the value: its node lies in the arena, so the
// source extent of the value cannot be read from the next node
const std::string big(100000, 'a');
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
const json_document d = json_document::parse(text);
const auto small = d.root()["small"].dump();
CHECK(small == "1");
CHECK(small.capacity() < 4096);
const auto list = d.root()["list"].dump();
CHECK(list == "[1,2,3]");
CHECK(list.capacity() < 4096);
CHECK(d.root()["list"].dump(2).capacity() < 4096);
// the whole document and the large value are unaffected
CHECK(d.root().dump() == ordered_json::parse(text).dump());
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
}
SECTION("output that outgrows the estimate")
{
// ensure_ascii writes six bytes for each two-byte character
std::string chars;
for (int i = 0; i < 5000; ++i)
{
chars += "\xC3\xA9";
}
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
const json expected = json::parse("\"" + chars + "\"");
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
}
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
{
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
// the second text has a DEL, which ensure_ascii writes as an escape
for (const char* const del :
{"", "\x7F"
})
{
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
+ "\"n\":[1,2.5,-0,true,null]}";
const ordered_json j = ordered_json::parse(text);
const ordered_json_document d = ordered_json_document::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
CHECK(d.root().dump() == j.dump());
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
// strings that an edit wrote (the source strings that they replace were plain)
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
ordered_json k = j;
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
k["plain"] = "\xC3\xA4gypten";
e.set(e.root()["list"][0], std::string("ascii after edit"));
k["list"][0] = "ascii after edit";
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
k["list"][1] = "\xC3\xA4";
e.set(e.root()["del"], std::string("tab\there"));
k["del"] = "tab\there";
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
CHECK(e.root().dump() == k.dump());
// strings that were edited from or to ASCII text
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
k["list"][2] = "\xC3\xA4";
e.set(e.root()["list"][4], std::string("plain again"));
k["list"][4] = "plain again";
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
}
}
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
{
// the check whether a string of the source has no byte >= 0x7F reads
// eight bytes at a time and then the rest one by one
for (std::size_t length = 1; length <= 20; ++length)
{
for (std::size_t pos = 0; pos < length; ++pos)
{
for (const char* const c :
{
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
})
{
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
std::string text = "[\"";
text.append(s).append("\",{\"").append(s).append("\":1}]");
const json_document d = json_document::parse(text);
const json j = json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
}
}
}
}
SECTION("streams and discarded views")
{
const json_document d = json_document::parse(R"({"a": [1, 2]})");
std::ostringstream compact;
compact << d.root();
CHECK(compact.str() == R"({"a":[1,2]})");
std::ostringstream pretty;
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
}
}
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
TEST_CASE("json_view large objects")
{
// objects with 128 members or more are looked up with a hash index
for (const std::size_t members :
{
127u, 128u, 129u, 10000u
})
{
CAPTURE(members)
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
}
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
CHECK(v[key].get<std::size_t>() == i);
CHECK(v.contains(key));
CHECK(v.find(key).key() == key);
CHECK(v.at(key).get<std::size_t>() == i);
CHECK(!v.contains(key + "x"));
}
CHECK(v[""].get_string() == "empty key");
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
CHECK(!v.contains("missing"));
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK(v == j);
CHECK(v.materialize() == j);
}
SECTION("duplicate keys: the first member wins, with and without a table")
{
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
// three keys repeated twice more (one copy in the middle, one at the
// end), and two keys repeated once; the value of a member is its
// position, so that the first member of a key can be told apart
struct member
{
std::string key;
std::size_t position;
};
const auto make_members = [](std::size_t total)
{
std::vector<std::string> keys;
for (std::size_t i = 0; i + 8 < total; ++i)
{
keys.push_back("k" + std::to_string(i));
}
const std::size_t n = keys.size();
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
const std::array<std::size_t, 2> twice = {{5, n / 2}};
std::vector<std::string> ordered = keys;
for (const std::size_t i : triple)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
}
for (const std::size_t i : twice)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
}
for (const std::size_t i : triple)
{
ordered.push_back(keys[i]);
}
std::vector<member> result;
result.reserve(ordered.size());
for (std::size_t i = 0; i < ordered.size(); ++i)
{
result.push_back({ordered[i], i});
}
return result;
};
// 100 and 127 members: no table; 128 members and more: a table
for (const std::size_t total :
{
100u, 127u, 128u, 200u, 5000u
})
{
CAPTURE(total)
const std::vector<member> members = make_members(total);
REQUIRE(members.size() >= total);
REQUIRE(members.size() >= 100);
std::string text = "{";
std::map<std::string, std::size_t> first;
std::map<std::string, std::size_t> last;
std::map<std::string, std::size_t> occurrences;
for (const member& m : members)
{
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
first.insert({m.key, m.position});
last[m.key] = m.position;
++occurrences[m.key];
}
text += '}';
REQUIRE(first.size() < members.size());
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
CHECK(v.size() == members.size()); // every occurrence is visited
const json m = v.materialize();
for (const auto& entry : first)
{
CAPTURE(entry.first)
const std::size_t expected = entry.second;
CHECK(v[entry.first].get<std::size_t>() == expected);
CHECK(v.at(entry.first).get<std::size_t>() == expected);
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
CHECK(v.value(entry.first, std::size_t{0}) == expected);
CHECK(v.contains(entry.first));
CHECK(v.count(entry.first) == occurrences[entry.first]);
const std::string pointer = "/" + entry.first;
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
CHECK(v.contains(json::json_pointer(pointer)));
// materialize() and parse() keep the last value instead
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
}
CHECK(!v.contains("k"));
CHECK(v["missing"].is_discarded());
CHECK(v.materialize() == j);
CHECK(v == j);
}
}
SECTION("colliding keys")
{
// the hash is not seeded, so keys that all land in one place must not
// make the table build quadratic: such an object gets no table and is
// searched linearly
constexpr std::size_t members = 300;
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
std::vector<std::string> colliding;
std::vector<std::string> spread;
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
{
std::string key(8, 'a');
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key[i] = static_cast<char>('a' + (x % 26));
}
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
if (lands_in_slot_zero && colliding.size() < members)
{
colliding.push_back(key);
}
else if (!lands_in_slot_zero && spread.size() < members)
{
spread.push_back(key);
}
}
const auto make_text = [](const std::vector<std::string>& keys)
{
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
}
return text + "}";
};
const std::string colliding_text = make_text(colliding);
const std::string spread_text = make_text(spread);
json_document with_collisions = json_document::parse(colliding_text);
json_document without_collisions = json_document::parse(spread_text);
for (const auto* pair :
{
&colliding, &spread
})
{
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
CHECK(!v.contains((*pair)[i] + "x"));
}
CHECK(!v.contains("missing"));
}
CHECK(with_collisions.root() == json::parse(colliding_text));
// only the object with the spread keys got a table (both texts have the
// same length, so the tables are the only difference)
with_collisions.shrink_to_fit();
without_collisions.shrink_to_fit();
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
}
SECTION("keys that differ in their last bytes are spread over the table")
{
// The hash must spread the usual families of keys (numbered names, hex
// strings, dates, short names): a key farther than index_max_displacement
// from its home slot would leave the object without a table. The keys
// are placed with linear probing, as the table does.
const auto max_displacement = [](const std::vector<std::string>& keys)
{
std::size_t slots = 16;
while (slots < 2 * keys.size())
{
slots *= 2;
}
std::vector<bool> used(slots, false);
std::size_t farthest = 0;
for (const std::string& key : keys)
{
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
std::size_t distance = 0;
while (used[i])
{
i = (i + 1) & (slots - 1);
++distance;
}
used[i] = true;
farthest = (std::max)(farthest, distance);
}
return farthest;
};
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
// (a format string that is not a literal is a Flawfinder error)
const auto numbered = [](const char* format, int count, int step)
{
const std::string pattern(format);
const std::size_t percent = pattern.find('%');
std::size_t conversion = percent + 1;
std::size_t width = 0;
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
{
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
++conversion;
}
const bool hex = pattern[conversion] == 'x';
std::vector<std::string> keys;
for (int i = 0; i < count; ++i)
{
std::ostringstream key;
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
keys.push_back(key.str());
}
return keys;
};
for (const char* format :
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
})
{
CAPTURE(format)
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
}
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
for (const std::size_t length :
{
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
})
{
CAPTURE(length)
std::set<std::string> distinct;
// (there are 26 and 676 keys of one and two letters)
std::size_t wanted = 700;
if (length == 1)
{
wanted = 26;
}
else if (length == 2)
{
wanted = 500;
}
while (distinct.size() < wanted)
{
std::string key(length, 'a');
for (char& c : key)
{
c = static_cast<char>('a' + (rng() % 26));
}
distinct.insert(key);
}
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
}
}
SECTION("keys of every length that differ in one byte")
{
// In a large object, a key is found by comparing its bytes with those of the member the
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::vector<std::string> keys;
std::set<std::string> distinct;
for (std::size_t length = 0; length <= 40; ++length)
{
for (int copy = 0; copy < 5; ++copy)
{
std::string key(length, 'a');
for (char& c : key)
{
c = static_cast<char>('a' + (rng() % 26));
}
if (distinct.insert(key).second)
{
keys.push_back(key);
}
}
}
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
}
text += "}";
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
const json_document d = json_document::parse(text);
const json_view v = d.root();
for (std::size_t i = 0; i < keys.size(); ++i)
{
CAPTURE(keys[i])
CHECK(v[keys[i]].get<std::size_t>() == i);
CHECK(v.at(keys[i]).get<std::size_t>() == i);
CHECK(v.contains(keys[i]));
for (std::size_t at = 0; at < keys[i].size(); ++at)
{
std::string other = keys[i];
other[at] = other[at] == 'z' ? 'y' : 'z';
CAPTURE(other)
CHECK(v.contains(other) == (distinct.count(other) != 0));
}
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
if (!keys[i].empty())
{
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
}
}
}
SECTION("shrink_to_fit releases the tables' spare capacity")
{
const auto make_text = [](int objects, int members)
{
std::string text = "[";
for (int object = 0; object < objects; ++object)
{
text += object != 0 ? ",{" : "{";
for (int i = 0, n = members + object; i < n; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
}
return text + "]";
};
const std::string small_text = make_text(5, 150);
const std::string big_text = make_text(40, 400);
// reading a big text, and then a small one, leaves the spare capacity
// of the big one: shrink_to_fit() brings the document to the size of
// one parsed from the small text alone
json_document d = json_document::parse(big_text);
const std::size_t big = d.memory_usage();
d.read(small_text);
CHECK(d.memory_usage() >= big);
d.shrink_to_fit();
json_document fresh = json_document::parse(small_text);
fresh.shrink_to_fit();
CHECK(d.memory_usage() == fresh.memory_usage());
CHECK(d.memory_usage() < big / 2);
CHECK(d.root() == json::parse(small_text));
for (int object = 0; object < 5; ++object)
{
const json_view v = d.root()[static_cast<std::size_t>(object)];
for (int i = 0, n = 150 + object; i < n; ++i)
{
CHECK(v[std::to_string(i)].get<int>() == i);
}
}
}
SECTION("nested, reused, and in arrays")
{
std::string inner = "{";
for (int i = 0; i < 300; ++i)
{
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
}
inner += '}';
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
json_document d = json_document::parse(text);
CHECK(d.root()[0]["m299"].get<int>() == 299);
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
CHECK(d.root()[2]["m0"].get<int>() == 0);
const std::size_t with_index = d.memory_usage();
d.read(std::string("{\"small\": 1}"));
CHECK(d.root()["small"].get<int>() == 1);
d.read(text);
CHECK(d.root()[2]["m7"].get<int>() == 7);
CHECK(d.memory_usage() >= with_index / 2);
}
}
+298
View File
@@ -0,0 +1,298 @@
// __ _____ _____ _____
// __| | __| | | | 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
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::ordered_json_editable_document;
using nlohmann::json_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_view;
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <map>
#include <random>
#include <set>
#include <sstream>
#include <string>
#include <vector>
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
// with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view dump: escaping, nesting, and output buffers")
{
SECTION("members in document order, all of them")
{
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
// dump() writes all members, though the lookup finds the first
CHECK(d.root()["b"].dump() == "1");
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document deep_document = json_document::parse(deep);
CHECK(deep_document.root().dump() == deep);
}
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
{
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
for (const char* const special :
{
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
})
{
for (std::size_t length = 0; length <= 40; ++length)
{
for (std::size_t position = 0; position <= length; ++position)
{
std::string content(length, 'a');
content.insert(position, special);
const std::array<std::string, 3> texts =
{
{
"\"" + content + "\"",
"[\"" + content + "\"]",
"{\"" + content + "\":1}",
}
};
for (const std::string& text : texts)
{
CAPTURE(text)
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
CHECK(d.root().dump() == j.dump());
}
}
}
}
}
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
{
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
std::string text = "[";
std::size_t in_chunk = 0;
const auto append = [&](std::uint32_t cp)
{
if (cp < 0x80)
{
if (cp < 0x20 || cp == '"' || cp == '\\')
{
return;
}
text += static_cast<char>(cp);
}
else if (cp < 0x800)
{
text += static_cast<char>(0xC0 | (cp >> 6));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
else if (cp < 0x10000)
{
text += static_cast<char>(0xE0 | (cp >> 12));
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
else
{
text += static_cast<char>(0xF0 | (cp >> 18));
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
};
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
{
if (cp >= 0xD800 && cp < 0xE000)
{
continue;
}
if (in_chunk == 0)
{
text += text.size() > 1 ? ",\"" : "\"";
}
append(cp);
if (++in_chunk == 37)
{
text += "\"";
in_chunk = 0;
}
}
text += in_chunk != 0 ? "\"]" : "]";
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(2) == j.dump(2));
}
SECTION("deep nesting, indented")
{
// indentations of up to and beyond the width of the writer's fixed-size copy
for (const int depth :
{
1, 5, 20, 70, 300
})
{
std::string text;
for (int i = 0; i < depth; ++i)
{
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
}
text += "[]";
for (int i = depth - 1; i >= 0; --i)
{
text += i % 2 == 0 ? "]" : "}";
}
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
for (const int indent :
{
0, 1, 2, 3, 8
})
{
CHECK(d.root().dump(indent) == j.dump(indent));
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
}
}
}
SECTION("the output buffer of a small value is small")
{
// an escaped key after the value: its node lies in the arena, so the
// source extent of the value cannot be read from the next node
const std::string big(100000, 'a');
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
const json_document d = json_document::parse(text);
const auto small = d.root()["small"].dump();
CHECK(small == "1");
CHECK(small.capacity() < 4096);
const auto list = d.root()["list"].dump();
CHECK(list == "[1,2,3]");
CHECK(list.capacity() < 4096);
CHECK(d.root()["list"].dump(2).capacity() < 4096);
// the whole document and the large value are unaffected
CHECK(d.root().dump() == ordered_json::parse(text).dump());
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
}
SECTION("output that outgrows the estimate")
{
// ensure_ascii writes six bytes for each two-byte character
std::string chars;
for (int i = 0; i < 5000; ++i)
{
chars += "\xC3\xA9";
}
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
const json expected = json::parse("\"" + chars + "\"");
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
}
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
{
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
// the second text has a DEL, which ensure_ascii writes as an escape
for (const char* const del :
{"", "\x7F"
})
{
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
+ "\"n\":[1,2.5,-0,true,null]}";
const ordered_json j = ordered_json::parse(text);
const ordered_json_document d = ordered_json_document::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
CHECK(d.root().dump() == j.dump());
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
// strings that an edit wrote (the source strings that they replace were plain)
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
ordered_json k = j;
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
k["plain"] = "\xC3\xA4gypten";
e.set(e.root()["list"][0], std::string("ascii after edit"));
k["list"][0] = "ascii after edit";
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
k["list"][1] = "\xC3\xA4";
e.set(e.root()["del"], std::string("tab\there"));
k["del"] = "tab\there";
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
CHECK(e.root().dump() == k.dump());
// strings that were edited from or to ASCII text
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
k["list"][2] = "\xC3\xA4";
e.set(e.root()["list"][4], std::string("plain again"));
k["list"][4] = "plain again";
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
}
}
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
{
// the check whether a string of the source has no byte >= 0x7F reads
// eight bytes at a time and then the rest one by one
for (std::size_t length = 1; length <= 20; ++length)
{
for (std::size_t pos = 0; pos < length; ++pos)
{
for (const char* const c :
{
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
})
{
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
std::string text = "[\"";
text.append(s).append("\",{\"").append(s).append("\":1}]");
const json_document d = json_document::parse(text);
const json j = json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
}
}
}
}
SECTION("streams and discarded views")
{
const json_document d = json_document::parse(R"({"a": [1, 2]})");
std::ostringstream compact;
compact << d.root();
CHECK(compact.str() == R"({"a":[1,2]})");
std::ostringstream pretty;
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
}
}
+131 -11
View File
@@ -532,18 +532,18 @@ TEST_CASE("json_view edits: errors")
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), json_view_test::message("type_error.305", "/a", 21, 27, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), json_view_test::message("type_error.305", "/o", 6, 14, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index -1 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), json_view_test::message("type_error.308", "/o", 6, 14, "cannot use push_back() with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), json_view_test::message("type_error.309", "/n", 34, 35, "cannot use insert() with number").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), json_view_test::message("type_error.307", "/n", 34, 35, "cannot use erase() with number").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), json_view_test::message("type_error.307", "/o", 6, 14, "cannot use erase() with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), json_view_test::message("out_of_range.403", "", 0, 47, "key 'missing' not found").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
@@ -941,3 +941,123 @@ TEST_CASE("json_view edits: duplicate keys")
CHECK(d.root()["o"]["d"].get<int>() == 0);
}
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view edits: check_utf8 agrees with the byte-wise check")
{
namespace dv = nlohmann::detail::view;
// "ok" or the exception message
const auto run = [](bool fast, const std::string & s) -> std::string
{
try
{
if (fast)
{
dv::check_utf8(s.data(), s.size());
}
else
{
dv::check_utf8_bytewise(reinterpret_cast<const unsigned char*>(s.data()), s.size(), 0);
}
}
catch (const json::type_error& e)
{
return {e.what()};
}
return {"ok"};
};
std::size_t errors = 0;
const auto same = [&](const std::string & s)
{
const std::string a = run(true, s);
const std::string b = run(false, s);
if (a != b)
{
INFO("fast: " << a << ", byte-wise: " << b << ", length " << s.size());
CHECK(a == b);
}
errors += b != "ok" ? 1u : 0u;
};
const std::vector<std::string> pieces =
{
"a", "Z", " ", "\"", "\\", "\n", "\x01", "\x7f", "\xc3\xa4", "\xe4\xb8\xad", "\xf0\x9f\x98\x80",
"\x80", "\xbf", "\xc0\x80", "\xc1\xbf", "\xc2", "\xe0\x80\x80", "\xe0\x9f\xbf", "\xed\xa0\x80", "\xed\xbf\xbf",
"\xf0\x80\x80\x80", "\xf0\x8f\xbf\xbf", "\xf4\x90\x80\x80", "\xf4\x8f\xbf\xbf", "\xf5", "\xf8", "\xff", "\xfe",
"\xe4\xb8", "\xf0\x9f\x98", "\xf0\x9f", "\xf0"
};
// every piece behind prefixes of every length, and followed by more bytes
for (std::size_t prefix = 0; prefix <= 64; ++prefix)
{
for (const auto& piece : pieces)
{
same(std::string(prefix, 'x') + piece);
same(std::string(prefix, 'x') + piece + "yz");
same(std::string(prefix, '"') + piece);
same(std::string(prefix, '\xc3') + piece);
same(std::string(prefix / 2, '\xc3').append(prefix / 2, '\xa4') + piece);
same(std::string(prefix, 'x') + "\xe4\xb8\xad" + piece);
}
}
// an invalid byte at every position of valid strings of every length
for (std::size_t len = 0; len <= 64; ++len)
{
const std::string ascii(len, 'a');
std::string mixed;
while (mixed.size() < len)
{
if (mixed.size() % 7 == 0)
{
mixed += "\xc3\xa4";
}
else if (mixed.size() % 11 == 0)
{
mixed += "\xf0\x9f\x98\x80";
}
else
{
mixed += "a";
}
}
same(ascii);
same(mixed);
for (std::size_t pos = 0; pos < len; ++pos)
{
for (const char bad :
{'\x80', '\xc0', '\xc2', '\xe0', '\xf4', '\xff', '"', '\\', '\x1f'
})
{
std::string s = ascii;
s[pos] = bad;
same(s);
s = mixed;
s[pos] = bad;
same(s);
}
}
}
// random bytes, biased to the interesting ones
std::mt19937 gen(5869); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
static const std::array<unsigned char, 23> interesting = {{0x00, 0x1f, 0x22, 0x5c, 0x7f, 0x80, 0x8f, 0x90, 0x9f, 0xa0, 0xbf, 0xc0, 0xc2, 0xdf, 0xe0, 0xe1, 0xed, 0xef, 0xf0, 0xf1, 0xf4, 0xf5, 0xff}};
for (int i = 0; i < 100000; ++i)
{
std::string s(gen() % 80, 'a');
for (auto& c : s)
{
const unsigned r = gen() % 8;
if (r == 0)
{
c = static_cast<char>(gen() & 0xff);
}
else if (r == 1)
{
c = static_cast<char>(interesting[gen() % interesting.size()]);
}
}
same(s);
}
CHECK(errors > 1000);
}
#endif
+112 -1
View File
@@ -26,12 +26,123 @@ using nlohmann::json_editable_view;
#include <limits>
#include <map>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
// These tests were split off unit-json_view_edit.cpp, whose object file got too
// large for the MinGW linker (see json_view_test_helpers.hpp).
namespace
{
// the three ways a function can accept views: editable ones, read-only ones, or both
template<typename BasicJsonType>
int accepts_editable(nlohmann::basic_json_view<BasicJsonType, nlohmann::view_mode::editable> /*unused*/)
{
return 1;
}
template<typename BasicJsonType>
int accepts_read_only(nlohmann::basic_json_view<BasicJsonType, nlohmann::view_mode::read_only> /*unused*/)
{
return 2;
}
template<typename BasicJsonType, nlohmann::view_mode Mode>
int accepts_any(nlohmann::basic_json_view<BasicJsonType, Mode> /*unused*/)
{
return Mode == nlohmann::view_mode::editable ? 3 : 4;
}
// overloads that differ only in the mode; deduction ignores the conversion from
// read-only to editable views, so each view picks the overload of its own mode
// (two templates that differ only in a decltype return type are a redefinition
// for MSVC 2015)
template<typename BasicJsonType>
int overload(nlohmann::basic_json_view<BasicJsonType, nlohmann::view_mode::editable> v)
{
return accepts_editable(v);
}
template<typename BasicJsonType>
int overload(nlohmann::basic_json_view<BasicJsonType, nlohmann::view_mode::read_only> v)
{
return accepts_read_only(v);
}
// a function (no template) that takes an editable view also accepts a read-only one
std::size_t sum_of(json_editable_view v)
{
std::size_t sum = 0;
for (const json_editable_view x : v.at("values"))
{
sum += x.get<std::size_t>();
}
return sum;
}
} // namespace
TEST_CASE("json_view modes")
{
using nlohmann::basic_json_document;
using nlohmann::basic_json_view;
using nlohmann::ordered_json;
using nlohmann::view_mode;
SECTION("read-only is the default, and the shortcuts name the modes")
{
static_assert(std::is_same<basic_json_view<json>, basic_json_view<json, view_mode::read_only>>::value, "default mode");
static_assert(std::is_same<basic_json_document<json>, basic_json_document<json, view_mode::read_only>>::value, "default mode");
static_assert(std::is_same<nlohmann::json_view, basic_json_view<json, view_mode::read_only>>::value, "json_view");
static_assert(std::is_same<json_editable_view, basic_json_view<json, view_mode::editable>>::value, "json_editable_view");
static_assert(std::is_same<json_editable_document, basic_json_document<json, view_mode::editable>>::value, "json_editable_document");
static_assert(std::is_same<nlohmann::ordered_json_editable_view, basic_json_view<ordered_json, view_mode::editable>>::value, "ordered_json_editable_view");
static_assert(std::is_same<json_editable_document::view_type, json_editable_view>::value, "view_type");
static_assert(std::is_same<json_document::view_type, nlohmann::json_view>::value, "view_type");
}
SECTION("a function template selects views by their mode")
{
const json_document d = json_document::parse("[1]");
const json_editable_document e = json_editable_document::parse("[1]");
const nlohmann::ordered_json_editable_document o = nlohmann::ordered_json_editable_document::parse("[1]");
CHECK(overload(e.root()) == 1);
CHECK(overload(o.root()) == 1);
CHECK(overload(d.root()) == 2);
CHECK(accepts_any(e.root()) == 3);
CHECK(accepts_any(d.root()) == 4);
}
SECTION("a view of a read-only document converts to an editable view, not the other way")
{
static_assert(std::is_convertible<nlohmann::json_view, json_editable_view>::value, "read-only to editable");
static_assert(!std::is_convertible<json_editable_view, nlohmann::json_view>::value, "not editable to read-only");
static_assert(!std::is_convertible<nlohmann::json_view, nlohmann::ordered_json_editable_view>::value, "same BasicJsonType only");
const json_document d = json_document::parse(R"({"values": [1, 2, 3], "name": "a"})");
json_editable_document e = json_editable_document::parse(R"({"values": [1, 2, 3], "name": "a"})");
CHECK(sum_of(d.root()) == 6);
CHECK(sum_of(e.root()) == 6);
const json_editable_view v = d.root();
CHECK(v.size() == 2);
CHECK(v.at("name").get<std::string>() == "a");
CHECK(v.source_offset() == d.root().source_offset());
CHECK((v == e.root()));
CHECK((v == d.root()));
e.push_back(e.root().at("values"), 4);
CHECK(sum_of(e.root()) == 10);
CHECK((v != e.root()));
#if !defined(JSON_NOEXCEPTION)
// the converted view still belongs to the read-only document
CHECK_THROWS_WITH_AS(e.push_back(v.at("values"), 5), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
#endif
CHECK(sum_of(d.root()) == 6);
}
}
TEST_CASE("json_view edits: views and values")
{
SECTION("a value that is no longer part of the document")
@@ -100,7 +211,7 @@ TEST_CASE("json_view edits: views and values")
// doubles have their own path to the output; other float types are
// written as basic_json writes them, non-finite values as null
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
using document_float = nlohmann::basic_json_document<json_float, true>;
using document_float = nlohmann::basic_json_document<json_float, nlohmann::view_mode::editable>;
document_float d = document_float::parse("[1.5]");
d.push_back(d.root(), std::numeric_limits<float>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<float>::infinity());
+560
View File
@@ -0,0 +1,560 @@
// __ _____ _____ _____
// __| | __| | | | 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
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::json_document;
using nlohmann::json_view;
using nlohmann::json_editable_document;
using nlohmann::ordered_json_document;
#include "json_view_test_helpers.hpp"
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
#endif
#include <array>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <forward_list>
#include <list>
#include <map>
#include <set>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <valarray>
#include <vector>
#ifdef JSON_HAS_CPP_17
#include <optional>
#include <string_view>
#endif
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal")
DOCTEST_GCC_SUPPRESS_WARNING_PUSH
DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal")
// The from_json() functions of the NLOHMANN_DEFINE_TYPE_* and
// NLOHMANN_JSON_SERIALIZE_ENUM macros read views directly; other from_json()
// functions get the value of materialize().
namespace view_from_json
{
struct point
{
int x = 0;
int y = 0;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(point, x, y)
friend bool operator==(const point& a, const point& b)
{
return a.x == b.x && a.y == b.y;
}
};
struct shape
{
// initialized for GCC -Weffc++
// NOLINTNEXTLINE(readability-redundant-member-init)
std::string name{};
// NOLINTNEXTLINE(readability-redundant-member-init)
std::vector<point> points{};
point center{};
// NOLINTNEXTLINE(readability-redundant-member-init)
std::map<std::string, point> named{};
NLOHMANN_DEFINE_TYPE_INTRUSIVE(shape, name, points, center, named)
};
struct with_defaults
{
int a = 1;
std::string b = "default";
std::vector<int> c = {1, 2};
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(with_defaults, a, b, c)
struct with_names
{
int value = 0;
// initialized for GCC -Weffc++
// NOLINTNEXTLINE(readability-redundant-member-init)
std::string text{};
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_NAMES(with_names, "Value", value, "Text", text)
struct empty
{
NLOHMANN_DEFINE_TYPE_INTRUSIVE(empty)
};
enum class color
{
red,
green,
blue
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
NLOHMANN_JSON_SERIALIZE_ENUM(color,
{
{color::red, "red"},
{color::green, "green"},
{color::blue, nullptr},
})
enum class strict_color
{
red,
green
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(strict_color,
{
{strict_color::red, "red"},
{strict_color::green, 1},
})
struct base
{
int id = 0;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(base, id)
};
struct derived : base
{
color c = color::red;
NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(derived, base, c)
};
struct derived_with_defaults : base
{
std::string label = "none";
NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT(derived_with_defaults, base, label)
};
// a base whose from_json() is written for json only: the derived type reads
// its own members from the view and the base part from materialize()
struct legacy_base
{
int id = 0;
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
inline void from_json(const json& j, legacy_base& b)
{
b.id = j.at("id").get<int>();
}
struct derived_from_legacy : legacy_base
{
int extra = 0;
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(derived_from_legacy, legacy_base, extra)
// from_json() for json only: converted through materialize()
struct legacy
{
int v = 0;
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
inline void from_json(const json& j, legacy& l)
{
l.v = j.at("v").get<int>();
}
// an unconstrained from_json() template, as in the documentation of
// arbitrary type conversions: converted through materialize()
struct unconstrained
{
int v = 0;
};
template<typename BasicJsonType>
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
void from_json(const BasicJsonType& j, unconstrained& u)
{
u.v = j.at("v").template get<int>();
}
// a from_json() constrained by the user to views and json: reads the view
struct constrained
{
int v = 0;
};
template < typename J, typename std::enable_if < nlohmann::is_json_view<J>::value || std::is_same<J, json>::value, int >::type = 0 >
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
void from_json(const J& j, constrained& c)
{
c.v = j.at("v").template get<int>();
}
} // namespace view_from_json
namespace
{
template<typename T, typename View = json_view>
struct reads_view : nlohmann::detail::has_view_from_json<nlohmann::adl_serializer<T, void>, View, T> {};
// whether a member converts to the same T from the view and from the materialized value
template<typename T>
bool same(const json_view& v, const json& j, const char* key)
{
return v[key].get<T>() == j[key].get<T>();
}
} // namespace
TEST_CASE("json_view from_json")
{
using namespace view_from_json; // NOLINT(google-build-using-namespace)
SECTION("which from_json() functions read views")
{
static_assert(reads_view<point>::value, "macro types read views");
static_assert(reads_view<shape>::value, "macro types read views");
static_assert(reads_view<with_defaults>::value, "macro types read views");
static_assert(reads_view<with_names>::value, "macro types read views");
static_assert(reads_view<empty>::value, "macro types read views");
static_assert(reads_view<derived>::value, "macro types read views");
static_assert(reads_view<derived_from_legacy>::value, "macro types read views");
static_assert(reads_view<color>::value, "enum macro types read views");
static_assert(reads_view<strict_color>::value, "enum macro types read views");
static_assert(reads_view<constrained>::value, "constrained from_json() reads views");
static_assert(reads_view<point, nlohmann::json_editable_view>::value, "editable views as well");
static_assert(reads_view<point, nlohmann::ordered_json_view>::value, "views of ordered_json as well");
static_assert(!reads_view<legacy>::value, "from_json() for json");
static_assert(!reads_view<legacy_base>::value, "from_json() for json");
static_assert(!reads_view<unconstrained>::value, "unconstrained from_json()");
static_assert(nlohmann::is_json_view<json_view>::value, "is_json_view");
static_assert(nlohmann::is_json_view<nlohmann::ordered_json_editable_view>::value, "is_json_view");
static_assert(!nlohmann::is_json_view<json>::value, "is_json_view");
static_assert(!nlohmann::is_json_view<json_document>::value, "is_json_view");
static_assert(!nlohmann::is_json_view<int>::value, "is_json_view");
}
SECTION("the library's own conversions read views")
{
static_assert(reads_view<std::list<int>>::value, "std types");
static_assert(reads_view<std::deque<point>>::value, "std types");
static_assert(reads_view<std::set<std::string>>::value, "std types");
static_assert(reads_view<std::unordered_set<int>>::value, "std types");
static_assert(reads_view<std::forward_list<int>>::value, "std types");
static_assert(reads_view<std::valarray<double>>::value, "std types");
static_assert(reads_view<std::array<int, 3>>::value, "std types");
static_assert(reads_view<std::pair<int, std::string>>::value, "std types");
static_assert(reads_view<std::tuple<int, bool, point>>::value, "std types");
static_assert(reads_view<std::map<int, int>>::value, "std types");
static_assert(reads_view<std::unordered_map<int, point>>::value, "std types");
static_assert(reads_view<std::map<color, int>>::value, "std types");
static_assert(reads_view<std::map<std::string, int>>::value, "std types");
static_assert(reads_view<std::vector<point>>::value, "std types");
static_assert(reads_view<std::vector<bool>>::value, "std types");
static_assert(reads_view<std::vector<std::uint8_t>>::value, "std types");
static_assert(reads_view<std::string>::value, "std types");
static_assert(reads_view<int>::value, "std types");
static_assert(reads_view<bool>::value, "std types");
static_assert(reads_view<double>::value, "std types");
static_assert(reads_view<std::nullptr_t>::value, "std types");
static_assert(!reads_view<json>::value, "get<BasicJsonType>() is materialize()");
static_assert(reads_view<json::binary_t>::value, "std types");
#ifdef JSON_HAS_CPP_17
static_assert(reads_view<std::optional<point>>::value, "std types");
#endif
CHECK(true);
}
SECTION("std types: the same values as from the materialized value")
{
const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]],
"points": [{"x": 1, "y": 2}, {"x": 3, "y": 4}], "object": {"b": 1, "a": 2}, "triple": [1, true, {"x": 5, "y": 6}],
"colors": {"green": 1, "red": 2}, "bytes": [1, 2, 255], "nested": [[1, 2], [3]], "number": 1.5, "flag": true,
"text": "hello", "nothing": null, "mixed": [1, 2.5, -3, 4], "flags": [true, false, true]})");
const json j = d.root().materialize();
const json_view v = d.root();
CHECK(same<std::list<int>>(v, j, "ints"));
CHECK(same<std::deque<int>>(v, j, "ints"));
CHECK(same<std::set<int>>(v, j, "ints"));
CHECK(same<std::multiset<int>>(v, j, "ints"));
CHECK(same<std::unordered_set<std::string>>(v, j, "strings"));
CHECK(same<std::forward_list<int>>(v, j, "ints"));
CHECK(same<std::array<int, 4>>(v, j, "ints"));
CHECK(same<std::array<double, 4>>(v, j, "mixed"));
CHECK((v["pairs"][0].get<std::pair<int, std::string>>() == j["pairs"][0].get<std::pair<int, std::string>>()));
CHECK(same<std::vector<std::pair<int, std::string>>>(v, j, "pairs"));
CHECK(same<std::tuple<int, bool, point>>(v, j, "triple"));
CHECK(same<std::map<int, std::string>>(v, j, "pairs"));
CHECK(same<std::unordered_map<int, std::string>>(v, j, "pairs"));
CHECK(same<std::map<std::string, int>>(v, j, "object"));
CHECK(same<std::unordered_map<std::string, int>>(v, j, "object"));
CHECK(same<std::map<color, int>>(v, j, "colors"));
CHECK(same<std::vector<point>>(v, j, "points"));
CHECK(same<std::vector<int>>(v, j, "ints"));
CHECK(same<std::vector<bool>>(v, j, "flags"));
CHECK(same<std::vector<std::uint8_t>>(v, j, "bytes"));
CHECK(same<std::vector<std::vector<int>>>(v, j, "nested"));
CHECK(same<std::vector<json>>(v, j, "mixed"));
CHECK(same<double>(v, j, "number"));
CHECK(same<bool>(v, j, "flag"));
CHECK(same<std::string>(v, j, "text"));
CHECK(same<std::nullptr_t>(v, j, "nothing"));
CHECK(same<json>(v, j, "object"));
const auto va = v["ints"].get<std::valarray<int>>();
CHECK(va.size() == 4);
CHECK(va[0] == 3);
int c_array[2][2] = {{0, 0}, {0, 0}}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): the conversion to C arrays
v["nested"][0].get_to(c_array[0]);
CHECK(c_array[0][1] == 2);
#ifdef JSON_HAS_CPP_17
const auto first = v["points"][0].get<std::optional<point>>();
CHECK((first.has_value() && first->y == 2));
CHECK(!v["nothing"].get<std::optional<point>>().has_value());
#endif
// containers of views keep views of the elements
const auto views = v["points"].get<std::vector<json_view>>();
CHECK(views.size() == 2);
CHECK(views[1]["x"].get<int>() == 3);
CHECK(v["object"].get<std::map<std::string, json_view>>().at("a").get<int>() == 2);
}
SECTION("std types: duplicate keys keep the last value, as from the materialized value")
{
const json_document d = json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
CHECK((d.root().get<std::map<std::string, int>>() == d.root().materialize().get<std::map<std::string, int>>()));
CHECK(d.root().get<std::map<std::string, int>>().at("a") == 3);
CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3);
}
#if !defined(JSON_NOEXCEPTION)
SECTION("std types: the same errors as from the materialized value")
{
const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])");
const json j = d.root().materialize();
for (std::size_t i = 0; i < 6; ++i)
{
CAPTURE(i)
const json_view v = d.root()[i];
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::list<int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::list<int>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::forward_list<int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::forward_list<int>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::array<int, 2>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::array<int, 2>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::pair<int, std::string>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::pair<int, std::string>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::map<std::string, int>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::map<int, int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::map<int, int>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::vector<std::uint8_t>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::vector<std::uint8_t>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<json::binary_t>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<json::binary_t>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::valarray<int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::valarray<int>>()); }));
}
}
#endif
SECTION("macro types")
{
const json_document d = json_document::parse(R"({"name": "s", "points": [{"x": 1, "y": 2}, {"x": 3, "y": 4}],
"center": {"x": 5, "y": 6}, "named": {"a": {"x": 7, "y": 8}}})");
const auto s = d.root().get<shape>();
CHECK(s.name == "s");
CHECK(s.points.size() == 2);
CHECK(s.points[1].y == 4);
CHECK(s.center.x == 5);
CHECK(s.named.at("a").y == 8);
point p;
d.root()["center"].get_to(p);
CHECK(p.x == 5);
CHECK(p.y == 6);
static_cast<void>(d.root()["center"].get<empty>());
}
SECTION("defaults and names")
{
const json_document d = json_document::parse(R"({"a": 5, "Value": 3, "Text": "t"})");
const auto w = d.root().get<with_defaults>();
CHECK(w.a == 5);
CHECK(w.b == "default");
CHECK(w.c == std::vector<int> {1, 2});
const json_document null_document = json_document::parse("null");
CHECK(null_document.root().get<with_defaults>().a == 1);
const auto n = d.root().get<with_names>();
CHECK(n.value == 3);
CHECK(n.text == "t");
}
SECTION("enums")
{
const json_document d = json_document::parse(R"(["green", null, "purple", 1, "red"])");
CHECK(d.root()[0].get<color>() == color::green);
CHECK(d.root()[1].get<color>() == color::blue);
CHECK(d.root()[2].get<color>() == color::red); // unknown values map to the first entry
CHECK(d.root()[3].get<strict_color>() == strict_color::green);
CHECK(d.root()[4].get<strict_color>() == strict_color::red);
#if !defined(JSON_NOEXCEPTION)
const json j = d.root().materialize();
#if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
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\"");
#endif
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
}
SECTION("derived types")
{
const json_document d = json_document::parse(R"({"id": 7, "c": "green", "extra": 3})");
const auto x = d.root().get<derived>();
CHECK(x.id == 7);
CHECK(x.c == color::green);
const auto y = d.root().get<derived_with_defaults>();
CHECK(y.id == 7);
CHECK(y.label == "none");
const auto z = d.root().get<derived_from_legacy>();
CHECK(z.id == 7);
CHECK(z.extra == 3);
}
SECTION("other from_json() functions get the value of materialize()")
{
const json_document d = json_document::parse(R"({"v": 4})");
CHECK(d.root().get<legacy>().v == 4);
CHECK(d.root().get<unconstrained>().v == 4);
CHECK(d.root().get<constrained>().v == 4);
legacy l;
d.root().get_to(l);
CHECK(l.v == 4);
}
SECTION("duplicate keys: members are looked up in the view, where the first one wins")
{
const json_document d = json_document::parse(R"({"x": 1, "y": 2, "x": 3})");
CHECK(d.root().get<point>().x == 1);
// materialize() keeps the last value, like parse()
CHECK(d.root().materialize().get<point>().x == 3);
}
SECTION("escaped keys: members are found by their decoded names")
{
// keys are decoded before they are compared, as by parse(); a lookup
// that compares the raw bytes of the source would miss these members
const json_document d = json_document::parse(R"([{"\u0078": 1, "y": 2}, {"V\u0061lue": 3, "Te\u0078t": "t"}, {"\u0062": "escaped"}, {"x": 1, "\u0078": 3, "y": 2}])");
const json j = d.root().materialize();
CHECK(d.root()[0].get<point>().x == 1);
CHECK(d.root()[0].get<point>().y == 2);
CHECK((d.root()[0].get<point>() == j[0].get<point>()));
CHECK(d.root()[1].get<with_names>().value == 3);
CHECK(d.root()[1].get<with_names>().text == "t");
CHECK(d.root()[2].get<with_defaults>().b == "escaped");
CHECK(d.root()[2].get<with_defaults>().a == 1);
// a duplicate key written once plain and once escaped: still the first member
CHECK(d.root()[3].get<point>().x == 1);
CHECK(j[3].get<point>().x == 3);
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
// an escaped backslash is part of the decoded key: "x\\" is not "x"
const json_document e = json_document::parse(R"({"x\\": 1, "y": 2})");
CHECK(exception_of_call([&] { static_cast<void>(e.root().get<point>()); }) == exception_of_call([&] { static_cast<void>(e.root().materialize().get<point>()); }));
CHECK(exception_of_call([&] { static_cast<void>(e.root().get<point>()); }) == "[json.exception.out_of_range.403] key 'x' not found");
#endif
}
SECTION("editable documents and ordered_json")
{
json_editable_document e = json_editable_document::parse(R"({"x": 1, "y": 2})");
e.set(e.root(), "x", 10);
CHECK(e.root().get<point>().x == 10);
const ordered_json_document o = ordered_json_document::parse(R"({"name": "o", "points": [], "center": {"x": 1, "y": 2}, "named": {}})");
CHECK(o.root().get<shape>().center.y == 2);
}
#if !defined(JSON_NOEXCEPTION)
SECTION("errors are those of the same conversion of a json 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)
{
CAPTURE(i)
CHECK(exception_of_call([&] { static_cast<void>(d.root()[i].get<point>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<point>()); }));
}
CHECK(exception_of_call([&] { static_cast<void>(d.root()[3].get<derived>()); }) == exception_of_call([&] { static_cast<void>(j[3].get<derived>()); }));
}
#endif
}
#ifdef JSON_HAS_CPP_17
namespace view_from_json
{
// a string_view member is not copied: it points into the source text or, for a
// string with escape sequences, into the buffer of decoded strings of the document
struct named_view
{
// initialized for GCC -Weffc++
// NOLINTNEXTLINE(readability-redundant-member-init)
std::string_view name{};
int n = 0;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(named_view, name, n)
};
} // namespace view_from_json
TEST_CASE("json_view from_json: string_view members borrow from the document")
{
using view_from_json::named_view;
// whether all of s is inside the bytes of text
const auto within = [](std::string_view s, std::string_view text)
{
const auto first = reinterpret_cast<std::uintptr_t>(text.data());
const auto begin = reinterpret_cast<std::uintptr_t>(s.data());
return begin >= first && begin + s.size() <= first + text.size();
};
SECTION("a string without escapes points into the source text")
{
const std::string text = R"({"name": "hello", "n": 1})";
const json_document d = json_document::parse(text);
REQUIRE_FALSE(d.owns_source());
const auto r = d.root().get<named_view>();
CHECK(r.name == "hello");
CHECK(r.n == 1);
CHECK(within(r.name, text));
CHECK(within(r.name, d.source()));
}
SECTION("a string with escapes points into the document, not into the source text")
{
const std::string text = R"({"name": "a\nb", "n": 2})";
json_document d = json_document::parse(text);
REQUIRE_FALSE(d.owns_source());
const auto r = d.root().get<named_view>();
CHECK(r.name == "a\nb");
CHECK(r.n == 2);
CHECK_FALSE(within(r.name, text));
// valid as long as the document is alive, even if the document is moved
const json_document moved = std::move(d);
CHECK(r.name == "a\nb");
CHECK(moved.root().get<named_view>().name == r.name);
}
SECTION("a document that owns its text keeps the members readable while it is alive")
{
const json_document d = json_document::parse(std::string(R"({"name": "owned", "n": 3})"));
REQUIRE(d.owns_source());
const auto r = d.root().get<named_view>();
CHECK(r.name == "owned");
CHECK(within(r.name, d.source()));
}
}
#endif
DOCTEST_CLANG_SUPPRESS_WARNING_POP
DOCTEST_GCC_SUPPRESS_WARNING_POP
+91 -10
View File
@@ -28,11 +28,25 @@ using nlohmann::detail::view::node;
#include <random>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include <test_data.hpp>
namespace json_view_image_test
{
// whether a document type has save() and load() (only on little-endian targets)
template<typename T, typename = void>
struct has_save : std::false_type {};
template<typename T>
struct has_save<T, decltype(static_cast<void>(std::declval<const T&>().save()))> : std::true_type {};
template<typename T, typename = void>
struct has_load : std::false_type {};
template<typename T>
struct has_load<T, decltype(static_cast<void>(T::load(std::declval<const std::vector<std::uint8_t>&>())))> : std::true_type {};
} // namespace json_view_image_test
#if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
namespace
@@ -171,6 +185,11 @@ std::uint32_t rng()
TEST_CASE("json_view images: round trips")
{
static_assert(json_view_image_test::has_save<json_document>::value, "save() on a little-endian target");
static_assert(json_view_image_test::has_load<json_document>::value, "load() on a little-endian target");
static_assert(json_view_image_test::has_save<ordered_json_editable_document>::value, "save() on a little-endian target");
static_assert(json_view_image_test::has_load<ordered_json_editable_document>::value, "load() on a little-endian target");
SECTION("small documents")
{
for (const char* text :
@@ -480,7 +499,7 @@ TEST_CASE("json_view images: edited documents")
{
CAPTURE(i)
CHECK((node_at(image, i).flags & nlohmann::detail::view::node_flags::linked) == 0);
CHECK(node_at(image, i).kind != nlohmann::detail::view::kind_link);
CHECK((node_at(image, i).kind != nlohmann::detail::view::node_kind::link));
}
for (const image_check check :
{
@@ -682,6 +701,64 @@ TEST_CASE("json_view images: ownership")
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view images: errors")
{
SECTION("float tokens: a changed byte is accepted if the text is still a number with the digits the node records")
{
for (const char* const token :
{
"-12.375", "0.5", "123456.0", "-0.0", "9999999999999999999.5", "0.000000000000000000000001", "1.25e10", "5E-3"
})
{
CAPTURE(token)
const std::string text = std::string("[") + token + "]";
const json_document d = json_document::parse(text);
const std::vector<std::uint8_t> image = d.save();
const std::size_t at = text_at(image) + 1; // the first byte of the token
const std::uint16_t recorded = node_at(image, 1).extra;
CHECK(loaded_dump(image) == json::parse(text).dump());
for (std::size_t pos = 0; token[pos] != '\0'; ++pos)
{
for (unsigned int byte = 0; byte < 256; ++byte)
{
std::vector<std::uint8_t> changed = image;
changed[at + pos] = static_cast<std::uint8_t>(byte);
std::string t = token;
t[pos] = static_cast<char>(byte);
// the digits of the integer part and of the fraction of the changed token
std::size_t integer = 0;
std::size_t fraction = 0;
std::size_t k = (t[0] == '-') ? 1 : 0;
for (; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
{
++integer;
}
if (k < t.size() && t[k] == '.')
{
for (++k; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
{
++fraction;
}
}
// (as a JSON text, the token may have white space around it)
const bool float_token = json::accept(t) && t.find_first_of(std::string(" \t\r\n\0", 5)) == std::string::npos && (t.find('.') != std::string::npos || t.find_first_of("eE") != std::string::npos) // NOLINT(abseil-string-find-str-contains)
&& ((integer | (fraction << 8U)) == recorded);
CAPTURE(pos)
CAPTURE(byte)
std::string result;
try
{
result = loaded_dump(changed);
}
catch (const json::parse_error& e)
{
CHECK(std::string(e.what()) == check_failed);
result = "rejected";
}
CHECK((result != "rejected") == float_token);
}
}
}
}
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
{
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
@@ -810,13 +887,13 @@ TEST_CASE("json_view images: check")
{
rejected(corrupted(image, 12, [&](node & n)
{
n.kind = kind;
n.kind = static_cast<nlohmann::detail::view::node_kind>(kind);
}), true);
}
// a key that is not a string
rejected(corrupted(image, 1, [](node & n)
{
n.kind = 0;
n.kind = nlohmann::detail::view::node_kind::null;
n.len = 0;
n.off = 0;
}), true);
@@ -960,11 +1037,11 @@ TEST_CASE("json_view images: check")
}), true);
rejected(corrupted(image, 0, [](node & n)
{
n.kind = 2; // an array: the "keys" are values, and the counts do not match
n.kind = nlohmann::detail::view::node_kind::array; // an array: the "keys" are values, and the counts do not match
}), true);
const std::vector<std::uint8_t> as_array = corrupted(image, 16, [](node & n)
{
n.kind = 2; // {} as []: fine
n.kind = nlohmann::detail::view::node_kind::array; // {} as []: fine
});
CHECK(load_result(as_array, image_check::full).empty());
const std::string expected_dump = R"({"s":"x\"y","i":-12,"u":7,"f":1.5e+300,"b":true,"n":null,"a":["t",[]]})";
@@ -1013,13 +1090,13 @@ TEST_CASE("json_view images: check")
rejected(at_token(u6, 0, '8'), false); // 8 != 7
rejected(corrupted(image, 4, [](node & n)
{
n.kind = 6; // "-12" as unsigned: a sign
n.kind = nlohmann::detail::view::node_kind::number_unsigned; // "-12" as unsigned: a sign
n.extra = 3;
}), false);
// a non-negative number_integer (as edits write it): fine
const std::vector<std::uint8_t> positive = corrupted(image, 6, [](node & n)
{
n.kind = 5;
n.kind = nlohmann::detail::view::node_kind::number_integer;
n.extra = 0;
});
CHECK(load_result(positive, image_check::full).empty());
@@ -1027,7 +1104,7 @@ TEST_CASE("json_view images: check")
CHECK(pos.root()["u"].is_number_integer());
rejected(corrupted(image, 8, [](node & n)
{
n.kind = 6; // a float token as integer
n.kind = nlohmann::detail::view::node_kind::number_unsigned; // a float token as integer
n.extra = 7;
}), false);
}
@@ -1197,7 +1274,7 @@ TEST_CASE("json_view images: check")
{
std::vector<std::uint8_t> b = img;
node n = node_at(b, i);
n.kind = kind;
n.kind = static_cast<nlohmann::detail::view::node_kind>(kind);
n.extra = extra;
set_node(b, i, n);
return load_result(b, image_check::full);
@@ -1275,8 +1352,12 @@ TEST_CASE("json_view images: damaged images")
TEST_CASE("json_view images: big-endian targets")
{
// the image format is little-endian: save() and load() do not exist here
static_assert(!json_view_image_test::has_save<json_document>::value, "no save() on a big-endian target");
static_assert(!json_view_image_test::has_load<json_document>::value, "no load() on a big-endian target");
static_assert(!json_view_image_test::has_save<json_editable_document>::value, "no save() on a big-endian target");
const json_document d = json_document::parse("[1]");
CHECK_THROWS_WITH_AS(d.save(), "[json.exception.type_error.320] json_document images need a little-endian target", json::type_error&);
CHECK(d.root().size() == 1);
}
#endif
+544
View File
@@ -0,0 +1,544 @@
// __ _____ _____ _____
// __| | __| | | | 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
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::ordered_json_editable_document;
using nlohmann::json_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_view;
#include "json_view_test_helpers.hpp"
using json_view_test::generator;
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <map>
#include <random>
#include <set>
#include <sstream>
#include <string>
#include <vector>
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
// with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
TEST_CASE("json_view large objects")
{
// objects with 128 members or more are looked up with a hash index
for (const std::size_t members :
{
127u, 128u, 129u, 10000u
})
{
CAPTURE(members)
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
}
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
CHECK(v[key].get<std::size_t>() == i);
CHECK(v.contains(key));
CHECK(v.find(key).key() == key);
CHECK(v.at(key).get<std::size_t>() == i);
CHECK(!v.contains(key + "x"));
}
CHECK(v[""].get_string() == "empty key");
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
CHECK(!v.contains("missing"));
CHECK_THROWS_WITH_AS(v.at("missing"), json_view_test::message("out_of_range.403", "", 0, text.size(), "key 'missing' not found").c_str(), json::out_of_range&);
CHECK(v == j);
CHECK(v.materialize() == j);
}
SECTION("duplicate keys: the first member wins, with and without a table")
{
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
// three keys repeated twice more (one copy in the middle, one at the
// end), and two keys repeated once; the value of a member is its
// position, so that the first member of a key can be told apart
struct member
{
std::string key;
std::size_t position;
};
const auto make_members = [](std::size_t total)
{
std::vector<std::string> keys;
for (std::size_t i = 0; i + 8 < total; ++i)
{
keys.push_back("k" + std::to_string(i));
}
const std::size_t n = keys.size();
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
const std::array<std::size_t, 2> twice = {{5, n / 2}};
std::vector<std::string> ordered = keys;
for (const std::size_t i : triple)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
}
for (const std::size_t i : twice)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
}
for (const std::size_t i : triple)
{
ordered.push_back(keys[i]);
}
std::vector<member> result;
result.reserve(ordered.size());
for (std::size_t i = 0; i < ordered.size(); ++i)
{
result.push_back({ordered[i], i});
}
return result;
};
// 100 and 127 members: no table; 128 members and more: a table
for (const std::size_t total :
{
100u, 127u, 128u, 200u, 5000u
})
{
CAPTURE(total)
const std::vector<member> members = make_members(total);
REQUIRE(members.size() >= total);
REQUIRE(members.size() >= 100);
std::string text = "{";
std::map<std::string, std::size_t> first;
std::map<std::string, std::size_t> last;
std::map<std::string, std::size_t> occurrences;
for (const member& m : members)
{
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
first.insert({m.key, m.position});
last[m.key] = m.position;
++occurrences[m.key];
}
text += '}';
REQUIRE(first.size() < members.size());
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
CHECK(v.size() == members.size()); // every occurrence is visited
const json m = v.materialize();
for (const auto& entry : first)
{
CAPTURE(entry.first)
const std::size_t expected = entry.second;
CHECK(v[entry.first].get<std::size_t>() == expected);
CHECK(v.at(entry.first).get<std::size_t>() == expected);
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
CHECK(v.value(entry.first, std::size_t{0}) == expected);
CHECK(v.contains(entry.first));
CHECK(v.count(entry.first) == occurrences[entry.first]);
const std::string pointer = "/" + entry.first;
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
CHECK(v.contains(json::json_pointer(pointer)));
// materialize() and parse() keep the last value instead
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
}
CHECK(!v.contains("k"));
CHECK(v["missing"].is_discarded());
CHECK(v.materialize() == j);
CHECK(v == j);
}
}
SECTION("colliding keys")
{
// the hash is not seeded, so keys that all land in one place must not
// make the table build quadratic: such an object gets no table and is
// searched linearly
constexpr std::size_t members = 300;
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
std::vector<std::string> colliding;
std::vector<std::string> spread;
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
{
std::string key(8, 'a');
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key[i] = static_cast<char>('a' + (x % 26));
}
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
if (lands_in_slot_zero && colliding.size() < members)
{
colliding.push_back(key);
}
else if (!lands_in_slot_zero && spread.size() < members)
{
spread.push_back(key);
}
}
const auto make_text = [](const std::vector<std::string>& keys)
{
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
}
return text + "}";
};
const std::string colliding_text = make_text(colliding);
const std::string spread_text = make_text(spread);
json_document with_collisions = json_document::parse(colliding_text);
json_document without_collisions = json_document::parse(spread_text);
for (const auto* pair :
{
&colliding, &spread
})
{
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
CHECK(!v.contains((*pair)[i] + "x"));
}
CHECK(!v.contains("missing"));
}
CHECK(with_collisions.root() == json::parse(colliding_text));
// only the object with the spread keys got a table (both texts have the
// same length, so the tables are the only difference)
with_collisions.shrink_to_fit();
without_collisions.shrink_to_fit();
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
}
SECTION("keys that differ in their last bytes are spread over the table")
{
// The hash must spread the usual families of keys (numbered names, hex
// strings, dates, short names): a key farther than index_max_displacement
// from its home slot would leave the object without a table. The keys
// are placed with linear probing, as the table does.
const auto max_displacement = [](const std::vector<std::string>& keys)
{
std::size_t slots = 16;
while (slots < 2 * keys.size())
{
slots *= 2;
}
std::vector<bool> used(slots, false);
std::size_t farthest = 0;
for (const std::string& key : keys)
{
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
std::size_t distance = 0;
while (used[i])
{
i = (i + 1) & (slots - 1);
++distance;
}
used[i] = true;
farthest = (std::max)(farthest, distance);
}
return farthest;
};
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
// (a format string that is not a literal is a Flawfinder error)
const auto numbered = [](const char* format, int count, int step)
{
const std::string pattern(format);
const std::size_t percent = pattern.find('%');
std::size_t conversion = percent + 1;
std::size_t width = 0;
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
{
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
++conversion;
}
const bool hex = pattern[conversion] == 'x';
std::vector<std::string> keys;
for (int i = 0; i < count; ++i)
{
std::ostringstream key;
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
keys.push_back(key.str());
}
return keys;
};
for (const char* format :
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
})
{
CAPTURE(format)
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
}
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
for (const std::size_t length :
{
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
})
{
CAPTURE(length)
std::set<std::string> distinct;
// (there are 26 and 676 keys of one and two letters)
std::size_t wanted = 700;
if (length == 1)
{
wanted = 26;
}
else if (length == 2)
{
wanted = 500;
}
while (distinct.size() < wanted)
{
std::string key(length, 'a');
for (char& c : key)
{
c = static_cast<char>('a' + (rng() % 26));
}
distinct.insert(key);
}
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
}
}
SECTION("keys of every length that differ in one byte")
{
// In a large object, a key is found by comparing its bytes with those of the member the
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::vector<std::string> keys;
std::set<std::string> distinct;
for (std::size_t length = 0; length <= 40; ++length)
{
for (int copy = 0; copy < 5; ++copy)
{
std::string key(length, 'a');
for (char& c : key)
{
c = static_cast<char>('a' + (rng() % 26));
}
if (distinct.insert(key).second)
{
keys.push_back(key);
}
}
}
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
}
text += "}";
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
const json_document d = json_document::parse(text);
const json_view v = d.root();
for (std::size_t i = 0; i < keys.size(); ++i)
{
CAPTURE(keys[i])
CHECK(v[keys[i]].get<std::size_t>() == i);
CHECK(v.at(keys[i]).get<std::size_t>() == i);
CHECK(v.contains(keys[i]));
for (std::size_t at = 0; at < keys[i].size(); ++at)
{
std::string other = keys[i];
other[at] = other[at] == 'z' ? 'y' : 'z';
CAPTURE(other)
CHECK(v.contains(other) == (distinct.count(other) != 0));
}
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
if (!keys[i].empty())
{
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
}
}
}
SECTION("shrink_to_fit releases the tables' spare capacity")
{
const auto make_text = [](int objects, int members)
{
std::string text = "[";
for (int object = 0; object < objects; ++object)
{
text += object != 0 ? ",{" : "{";
for (int i = 0, n = members + object; i < n; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
}
return text + "]";
};
const std::string small_text = make_text(5, 150);
const std::string big_text = make_text(40, 400);
// reading a big text, and then a small one, leaves the spare capacity
// of the big one: shrink_to_fit() brings the document to the size of
// one parsed from the small text alone
json_document d = json_document::parse(big_text);
const std::size_t big = d.memory_usage();
d.read(small_text);
CHECK(d.memory_usage() >= big);
d.shrink_to_fit();
json_document fresh = json_document::parse(small_text);
fresh.shrink_to_fit();
CHECK(d.memory_usage() == fresh.memory_usage());
CHECK(d.memory_usage() < big / 2);
CHECK(d.root() == json::parse(small_text));
for (int object = 0; object < 5; ++object)
{
const json_view v = d.root()[static_cast<std::size_t>(object)];
for (int i = 0, n = 150 + object; i < n; ++i)
{
CHECK(v[std::to_string(i)].get<int>() == i);
}
}
}
SECTION("nested, reused, and in arrays")
{
std::string inner = "{";
for (int i = 0; i < 300; ++i)
{
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
}
inner += '}';
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
json_document d = json_document::parse(text);
CHECK(d.root()[0]["m299"].get<int>() == 299);
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
CHECK(d.root()[2]["m0"].get<int>() == 0);
const std::size_t with_index = d.memory_usage();
d.read(std::string("{\"small\": 1}"));
CHECK(d.root()["small"].get<int>() == 1);
d.read(text);
CHECK(d.root()[2]["m7"].get<int>() == 7);
CHECK(d.memory_usage() >= with_index / 2);
}
}
+1 -1
View File
@@ -25,7 +25,7 @@
#endif
// the view's own macros do not leak
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) \
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) || defined(NLOHMANN_VIEW_NOCLONE) \
|| defined(NLOHMANN_VIEW_NODISCARD) || defined(NLOHMANN_VIEW_THROW) || defined(NLOHMANN_VIEW_HAS_CPP_17) || defined(NLOHMANN_VIEW_LITTLE_ENDIAN) \
|| defined(NLOHMANN_VIEW_REPEAT16)
#error "json_view.hpp leaks a macro"
+574
View File
@@ -0,0 +1,574 @@
// __ _____ _____ _____
// __| | __| | | | 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
{
// not constexpr: VS 2015 does not accept std::string::npos in a constant expression (C2131)
const 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());
}
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
// the exceptions about every value of v are those about the value at the same
// place in j, a value of the whole document from parse() or materialize() (so
// that its parents and positions are set); members whose key repeats are
// skipped (the view finds the first one, j holds the last)
template<typename View>
void check_exceptions(const View& v, const json& j)
{
CAPTURE(j.dump())
if (!v.is_number())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<int>()); }) == exception_of_call([&] { static_cast<void>(j.get<int>()); }));
}
if (!v.is_string())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::string>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::string>()); }));
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::vector<int>>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::vector<int>>()); }));
}
CHECK(exception_of_call([&] { static_cast<void>(v.at("missing")); }) == exception_of_call([&] { static_cast<void>(j.at("missing")); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(1000)); }) == exception_of_call([&] { static_cast<void>(j.at(1000)); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/missing"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/missing"))); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/-"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/-"))); }));
CHECK(exception_of_call([&] { static_cast<void>(v.value("k", 0)); }) == exception_of_call([&] { static_cast<void>(j.value("k", 0)); }));
if (!v.is_object())
{
CHECK(exception_of_call([&] { static_cast<void>(v["k"]); }) == exception_of_call([&] { static_cast<void>(j["k"]); }));
// (without the context of basic_json: view iterators do not know the value they iterate over)
CHECK(exception_of_call([&] { static_cast<void>(v.begin().key()); }) == "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators");
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { static_cast<void>(v[0]); }) == exception_of_call([&] { static_cast<void>(j[0]); }));
}
if (v.is_null())
{
CHECK(exception_of_call([&] { static_cast<void>(v.front()); }) == exception_of_call([&] { static_cast<void>(j.front()); }));
}
if (v.is_object())
{
for (auto it = v.begin(); it != v.end(); ++it)
{
const std::string key(it.key().data(), it.key().size());
if (v.count(key) == 1)
{
check_exceptions(it.value(), j.at(key));
}
}
}
else if (v.is_array())
{
for (std::size_t i = 0; i < v.size(); ++i)
{
check_exceptions(v[i], j[i]);
}
}
}
// as check_exceptions, for edits that fail (they change neither d nor j)
void check_edit_exceptions(json_editable_document& d, const json_editable_document::view_type& v, json& j)
{
CAPTURE(j.dump())
if (!v.is_object() && !v.is_null())
{
CHECK(exception_of_call([&] { d.set(v, "k", 1); }) == exception_of_call([&] { j["k"] = 1; }));
CHECK(exception_of_call([&] { d.erase(v, "k"); }) == exception_of_call([&] { j.erase("k"); }));
}
if (!v.is_array() && !v.is_null())
{
CHECK(exception_of_call([&] { d.set(v, 0, 1); }) == exception_of_call([&] { j[0] = 1; }));
CHECK(exception_of_call([&] { d.push_back(v, 1); }) == exception_of_call([&] { j.push_back(1); }));
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { d.insert(v, 0, 1); }) == exception_of_call([&] { j.insert(j.cbegin(), 1); }));
CHECK(exception_of_call([&] { d.erase(v, 0); }) == exception_of_call([&] { j.erase(0); }));
}
else
{
CHECK(exception_of_call([&] { d.erase(v, 1000); }) == exception_of_call([&] { j.erase(1000); }));
}
if (v.is_object())
{
for (auto it = v.begin(); it != v.end(); ++it)
{
const std::string key(it.key().data(), it.key().size());
if (v.count(key) == 1)
{
check_edit_exceptions(d, it.value(), j.at(key));
}
}
}
else if (v.is_array())
{
for (std::size_t i = 0; i < v.size(); ++i)
{
check_edit_exceptions(d, v[i], j[i]);
}
}
}
#endif
} // 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":[ "\/" ]})",
R"([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",
R"(["\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));
const auto load_image = [&image]
{
// GCC ignores a cast to void for [[nodiscard]]-like results, so keep the document
const json_document d = json_document::load(image);
static_cast<void>(d);
};
CHECK_THROWS_AS(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
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view exceptions name the path and the bytes of the value")
{
SECTION("as those of basic_json")
{
const std::vector<std::string> texts =
{
"null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})",
R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})",
R"([1 /* ] */, {"a": [] // }
, "b": {} } , ])",
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
};
for (const auto& text : texts)
{
CAPTURE(text)
const json j = json::parse(text, nullptr, true, true, true);
const json_document d = json_document::parse(text, true, true, true);
check_exceptions(d.root(), j);
json_editable_document e = json_editable_document::parse(text, true, true, true);
check_exceptions(e.root(), j);
json m = e.root().materialize();
check_edit_exceptions(e, e.root(), m);
}
}
SECTION("generated documents")
{
json_view_test::generator gen;
for (int i = 0; i < 100; ++i)
{
std::string text;
gen.value(text, 0);
CAPTURE(text)
const json_document d = json_document::parse(text);
check_exceptions(d.root(), json::parse(text));
}
}
SECTION("edited documents: as those of materialize()")
{
const std::string text = R"({"a": [1, 2], "b": {"c": "\n", "d": true}, "e": [3, [4]], "f": "x"})";
json_editable_document d = json_editable_document::parse(text);
d.set(d.root()["f"], 5);
d.push_back(d.root()["a"], json::parse(R"({"n": [null]})"));
d.erase(d.root()["e"], 0);
d.set(d.root()["b"], "c2", d.root()["e"]);
d.insert(d.root()["e"], 0, "new");
json m = d.root().materialize();
check_exceptions(d.root(), m);
check_edit_exceptions(d, d.root(), m);
// (a value inside a new container: a path, but no bytes)
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/a/2/n/0) ";
#endif
expected += "type must be number, but is null";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"][2]["n"][0].get<int>()); }) == expected);
}
SECTION("deep documents with siblings after every nested container")
{
// the search for the path pushes and pops one frame per level (the
// stack reallocates while it grows, and shrinks again at every
// sibling): run under a sanitizer, a reference to a popped or moved
// frame would show here
std::string text = "7";
for (int level = 0; level < 40; ++level)
{
text.insert(0, R"([[1, [2]], )");
text += R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
}
CAPTURE(text)
const json j = json::parse(text);
const json_document d = json_document::parse(text);
check_exceptions(d.root(), j);
json_editable_document e = json_editable_document::parse(text);
e.set(e.root()[0][0], 9);
check_exceptions(e.root(), e.root().materialize());
}
SECTION("examples")
{
const std::string text = R"({"people": [{"name": "a", "age": 1}, {"name": "c", "age": "x"}], "o": {"k": 1}})";
const json_document d = json_document::parse(text);
std::string expected_age = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected_age += "(/people/1/age) ";
#endif
expected_age += "(bytes 58-61) type must be number, but is string";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["people"][1]["age"].get<int>()); }) == expected_age);
std::string expected_zz = "[json.exception.out_of_range.403] ";
#if JSON_DIAGNOSTICS
expected_zz += "(/people/0) ";
#endif
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);
// the root has no path, and with JSON_DIAGNOSTICS then no bytes either (as basic_json)
std::string expected_missing = "[json.exception.out_of_range.403] ";
#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);
}
SECTION("duplicate keys: the value the view finds")
{
const json_document d = json_document::parse(R"({"x": "1", "x": "22"})");
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/x) ";
#endif
expected += "(bytes 6-9) type must be number, but is string";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["x"].get<int>()); }) == expected);
}
}
#endif
+83
View File
@@ -423,3 +423,86 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)")
}
}
}
TEST_CASE("JSON Merge Patch on non-object, discarded, and binary values")
{
SECTION("an empty object patch turns a non-object target into an empty object")
{
json target = 1;
target.merge_patch(json::object());
CHECK(target == json::object());
json from_null;
from_null.merge_patch(json::object());
CHECK(from_null == json::object());
}
SECTION("a non-object patch replaces a deeply nested target")
{
// checked by type, since == recurses
SECTION("number")
{
json target = json::parse(nested_objects(1000, 0));
target.merge_patch(json(42));
CHECK(target.is_number_integer());
CHECK(target.get<int>() == 42);
}
SECTION("array")
{
json target = json::parse(nested_objects(1000, 0));
target.merge_patch(json({1, nullptr}));
CHECK(target.is_array());
CHECK(target.size() == 2);
CHECK(target[1].is_null());
}
SECTION("null")
{
json target = json::parse(nested_objects(1000, 0));
target.merge_patch(json(nullptr));
CHECK(target.is_null());
}
}
SECTION("discarded values")
{
const json discarded(json::value_t::discarded);
// a discarded patch is not an object and replaces the target
json target = {{"a", 1}};
target.merge_patch(discarded);
CHECK(target.is_discarded());
// a discarded target is replaced by an empty object first
json discarded_target(json::value_t::discarded);
discarded_target.merge_patch(json({{"a", 1}}));
CHECK(discarded_target == json({{"a", 1}}));
json both(json::value_t::discarded);
both.merge_patch(discarded);
CHECK(both.is_discarded());
}
SECTION("binary values")
{
const json bin = json::binary({1, 2, 3}, 7);
// a binary patch is not an object and replaces the target
json target = {{"a", 1}};
target.merge_patch(bin);
CHECK(target.is_binary());
CHECK(target.get_binary() == bin.get_binary());
// a binary target is replaced by an object
json binary_target = bin;
binary_target.merge_patch(json({{"a", 1}}));
CHECK(binary_target == json({{"a", 1}}));
// binary values nested in the patch are kept
json nested = json::object();
nested.merge_patch(json({{"a", bin}}));
CHECK(nested["a"].is_binary());
CHECK(nested["a"].get_binary() == bin.get_binary());
}
}
+101
View File
@@ -27,10 +27,15 @@ using nlohmann::json;
#include <sstream>
#include <iomanip>
#include <limits>
#include <list>
#include <set>
#include "make_test_data_available.hpp"
#include "round_trip_corpus.hpp"
#include "test_utils.hpp"
#include "sax_countdown.hpp"
#include "sax_event_loggers.hpp"
using utils::SaxCountdown;
using utils::SaxEventLogger;
TEST_CASE("MessagePack")
@@ -2137,3 +2142,99 @@ TEST_CASE("MessagePack large strings and binaries (chunked reader)")
}
}
}
TEST_CASE("MessagePack arrays of scalars from contiguous and non-contiguous input")
{
// contiguous input reads the integers, floats, booleans, and nils an
// array begins with in one loop, other input value by value; both must
// give the same events, value, or error for the complete document and for
// every truncation of it, and stop at every event a SAX handler rejects
const std::vector<std::uint8_t> msgpack =
{
0x83, // map(3)
0xA1, 'a', // "a"
0xDC, 0x00, 0x16, // array 16 (22)
0x00, 0x7F, 0xCC, 0xFF, 0xCD, 0x01, 0x00, // 0, 127, 255, 256
0xCE, 0x00, 0x01, 0x00, 0x00, // 65536
0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 18446744073709551615
0xFF, 0xE0, 0xD0, 0x80, 0xD1, 0xFF, 0x00, // -1, -32, -128, -256
0xD2, 0xFF, 0xFE, 0xFF, 0xFF, // -65537
0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -9223372036854775808
0xC2, 0xC3, 0xC0, // false, true, nil
0xCA, 0x3F, 0xC0, 0x00, 0x00, // 1.5 (float 32)
0xCB, 0xBF, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, // -0.1
0xA1, 'x', // "x" (ends the loop)
0x01, // 1
0xCB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2.5
0xD9, 0x01, 'y', // "y" (str 8)
0xC0, // nil
0xA1, 'b', // "b"
0x93, // fixarray(3)
0x92, 0xCB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
0xCB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
0x92, 0xCB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
0xCB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
0x90, // []
0xA1, 'c', // "c"
0x94, 0x80, 0x01, 0xC4, 0x01, 0x05, 0x91, 0x03 // [{}, 1, bin 8, [3]]
};
const json j = json::from_msgpack(msgpack);
CHECK(j["a"].size() == 22);
CHECK(j["a"][5] == 18446744073709551615u);
CHECK(j["a"][7] == -32);
CHECK(j["a"][11] == (std::numeric_limits<std::int64_t>::min)());
CHECK(j["a"][15] == 1.5);
CHECK(j["a"][16] == -0.1);
CHECK(j["a"][17] == "x");
CHECK(j["b"] == json::parse("[[1.25, -3.75], [1.25, -3.75], []]"));
CHECK(j["c"][2].is_binary());
for (std::size_t length = 0; length <= msgpack.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(msgpack.begin(), msgpack.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_msgpack(contiguous, true, false).dump() == json::from_msgpack(bytewise.begin(), bytewise.end(), true, false).dump());
#if !defined(JSON_NOEXCEPTION)
std::string contiguous_error;
std::string bytewise_error;
try
{
json _;
_ = json::from_msgpack(contiguous);
}
catch (const json::parse_error& e)
{
contiguous_error = e.what();
}
try
{
json _;
_ = json::from_msgpack(bytewise.begin(), bytewise.end());
}
catch (const json::parse_error& e)
{
bytewise_error = e.what();
}
CHECK(contiguous_error == bytewise_error);
#endif
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::msgpack)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
CHECK(from_contiguous.events == from_bytewise.events);
}
for (int events = 0; events <= 60; ++events)
{
CAPTURE(events)
const std::list<std::uint8_t> bytewise(msgpack.begin(), msgpack.end());
SaxCountdown from_contiguous(events);
SaxCountdown from_bytewise(events);
CHECK(json::sax_parse(msgpack, &from_contiguous, json::input_format_t::msgpack)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
}
}
+10
View File
@@ -141,6 +141,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
const test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -170,6 +171,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -199,6 +201,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
const test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -232,6 +235,7 @@ TEST_CASE("pointer access")
const test_type* p2 = value.get_ptr<const test_type*>();
CHECK(p2 == value.get_ptr<const test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p2 == value.get<test_type>());
const test_type* const p3 = value.get_ptr<const test_type* const>();
@@ -257,6 +261,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
const test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -286,6 +291,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -315,6 +321,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
const test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -344,6 +351,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -373,6 +381,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
const test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == Approx(value.get<test_type>()));
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -402,6 +411,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == Approx(value.get<test_type>()));
const test_type* p2 = value.get_ptr<const test_type*>();
+20
View File
@@ -1499,6 +1499,26 @@ TEST_CASE("regression tests 1")
CHECK(order.dump() == R"({"a":2,"m":{"y":1,"b":2},"new_key":3})");
}
SECTION("operator[](json_pointer) when emplace() finds a key the lookup does not")
{
// a copied fifo_map restarts its timestamps, so a new key "k" gets
// the timestamp of "x": the lookup does not find "k", but emplace()
// finds "x" and inserts nothing; the pointer then continues with the
// member of "x", as basic_json::operator[] does. The object_t is
// copied explicitly: copying the my_json would build the object
// member by member (with fresh timestamps) when built with
// JSON_NO_THREAD_LOCAL.
const my_json orig = my_json::parse(R"({"x":1})");
const std::array<const char*, 3> pointers = {{"/k/0", "/k/a/b", "/k/-"}};
for (const char* pointer : pointers)
{
CAPTURE(pointer)
my_json j(orig.get_ref<const my_json::object_t&>());
CHECK_THROWS_AS(j[my_json::json_pointer(pointer)], my_json::out_of_range&);
CHECK(j.dump() == R"({"x":1})");
}
}
SECTION("issue #977 - Assigning between different json types")
{
foo_json lj = ns::foo{3};
+46
View File
@@ -23,6 +23,7 @@ using nlohmann::json;
#include <cstdint>
#include <sstream>
#include <iomanip>
#include <limits>
#include <string>
#include <vector>
@@ -248,6 +249,51 @@ TEST_CASE_TEMPLATE("serialization for extreme integer values", T, int32_t, uint3
}
}
TEST_CASE("integers of every digit count are written like std::to_string")
{
// dump_integer() writes up to 16 digits in two groups of eight and 17 to
// 20 digits as one to four digits and two groups of eight
std::vector<std::uint64_t> values;
std::uint64_t power = 1;
for (int digits = 1; digits <= 20; ++digits)
{
values.push_back(power); // 1 followed by zeros
values.push_back(power + 1);
values.push_back((power * 2) - 1);
if (digits < 20)
{
values.push_back((power * 10) - 1); // all 9s
values.push_back((power * 10) - power); // 9 followed by zeros
power *= 10;
}
}
values.push_back((std::numeric_limits<std::uint64_t>::max)());
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()));
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1);
std::uint64_t state = 0x9E3779B97F4A7C15u;
for (int i = 0; i < 20000; ++i)
{
state = (state * 6364136223846793005u) + 1442695040888963407u;
values.push_back(state >> (state % 61)); // every bit length
}
for (const auto v : values)
{
CAPTURE(v)
CHECK(json(v).dump() == std::to_string(v));
if (v <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
{
const auto s = static_cast<std::int64_t>(v);
CHECK(json(s).dump() == std::to_string(s));
CHECK(json(-s).dump() == std::to_string(-s));
}
}
CHECK(json((std::numeric_limits<std::int64_t>::min)()).dump() == "-9223372036854775808");
// in a container, so the digits are followed by other output
CHECK(json::array({std::uint64_t{18446744073709551615u}, std::int64_t{-1234567890123456789}, 1}).dump() == "[18446744073709551615,-1234567890123456789,1]");
}
TEST_CASE("dump with binary values")
{
auto binary = json::binary({1, 2, 3, 4});
+125 -1
View File
@@ -21,12 +21,15 @@ using nlohmann::json;
#include <iostream>
#include <fstream>
#include <list>
#include <set>
#include "make_test_data_available.hpp"
#include "round_trip_corpus.hpp"
#include "test_utils.hpp"
#include "sax_countdown.hpp"
#include "sax_event_loggers.hpp"
using utils::SaxCountdown;
using utils::SaxEventLogger;
TEST_CASE("UBJSON")
@@ -2023,7 +2026,7 @@ TEST_CASE("UBJSON")
CHECK(json::from_ubjson(v0, true, false).is_discarded());
std::vector<uint8_t> const vi = {'[', '$', '#'};
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(vi), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing UBJSON value: unexpected end of input", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(vi), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing UBJSON type: marker 0x23 is not a permitted optimized array type", json::parse_error&);
CHECK(json::from_ubjson(vi, true, false).is_discarded());
std::vector<uint8_t> const vT = {'[', '$', 'T'};
@@ -3348,3 +3351,124 @@ TEST_CASE("UBJSON large strings (chunked reader)")
}
}
}
TEST_CASE("UBJSON optimized container type marker is validated for count 0")
{
// the '$' type marker must be a legal marker even if no element follows
SECTION("invalid marker")
{
for (const char marker :
{'x', 'E', 'B', 'u', 'm', 'M', 'h'
})
{
for (const char open :
{'[', '{'
})
{
for (const int count :
{
0, 1
})
{
CAPTURE(marker)
CAPTURE(open)
CAPTURE(count)
const std::vector<std::uint8_t> v = {static_cast<std::uint8_t>(open), '$', static_cast<std::uint8_t>(marker), '#', 'i', static_cast<std::uint8_t>(count)};
json _;
CHECK_THROWS_AS(_ = json::from_ubjson(v), json::parse_error&);
CHECK(json::from_ubjson(v, true, false).is_discarded());
}
}
}
}
SECTION("error message")
{
const std::vector<std::uint8_t> v = {'[', '$', 'x', '#', 'i', 0};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(v), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing UBJSON type: marker 0x78 is not a permitted optimized array type", json::parse_error&);
}
SECTION("valid marker")
{
for (const char marker :
{'Z', 'N', 'T', 'F', 'i', 'U', 'I', 'l', 'L', 'd', 'D', 'H', 'C', 'S', '[', '{'
})
{
CAPTURE(marker)
const std::vector<std::uint8_t> va = {'[', '$', static_cast<std::uint8_t>(marker), '#', 'i', 0};
const std::vector<std::uint8_t> vo = {'{', '$', static_cast<std::uint8_t>(marker), '#', 'i', 0};
CHECK(json::from_ubjson(va) == json::array());
CHECK(json::from_ubjson(vo) == json::object());
}
}
}
TEST_CASE("UBJSON optimized arrays from contiguous and non-contiguous input")
{
// contiguous input reads the numbers of an array with a type marker in
// one loop, other input number by number; both must give the same events,
// value, or error for the complete document and for every truncation of
// it, and stop at every event a SAX handler rejects
json document =
{
{"u8", {0, 200, 255}},
{"i8", {-5, 100, -128}},
{"i16", {-30000, 1000, 30000}},
{"i32", {-2000000000, 5, 70000}},
{"i64", {-9000000000000000000LL, 1, 5000000000LL}},
{"f64", {1.5, -0.1, 2.5}},
{"pairs", {{1.25, -3.75}, {1.25, -3.75}, json::array()}},
{"mixed", {1, "a", 2.5, nullptr}},
};
const std::vector<std::uint8_t> encoded = json::to_ubjson(document, true, true);
CHECK(json::from_ubjson(encoded) == document);
for (std::size_t length = 0; length <= encoded.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(encoded.begin(), encoded.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_ubjson(contiguous, true, false).dump() == json::from_ubjson(bytewise.begin(), bytewise.end(), true, false).dump());
#if !defined(JSON_NOEXCEPTION)
std::string contiguous_error;
std::string bytewise_error;
try
{
json _;
_ = json::from_ubjson(contiguous);
}
catch (const json::exception& e)
{
contiguous_error = e.what();
}
try
{
json _;
_ = json::from_ubjson(bytewise.begin(), bytewise.end());
}
catch (const json::exception& e)
{
bytewise_error = e.what();
}
CHECK(contiguous_error == bytewise_error);
#endif
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::ubjson)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::ubjson));
CHECK(from_contiguous.events == from_bytewise.events);
}
for (int events = 0; events <= 80; ++events)
{
CAPTURE(events)
const std::list<std::uint8_t> bytewise(encoded.begin(), encoded.end());
SaxCountdown from_contiguous(events);
SaxCountdown from_bytewise(events);
CHECK(json::sax_parse(encoded, &from_contiguous, json::input_format_t::ubjson)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::ubjson));
}
}
+38
View File
@@ -13,6 +13,7 @@ using nlohmann::json;
#include <algorithm>
#include <fstream>
#include <random>
#include <string>
#include "make_test_data_available.hpp"
#include "test_utils.hpp"
@@ -1631,3 +1632,40 @@ TEST_CASE("Unicode RFC 3629")
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP
TEST_CASE("UTF-8 check of whole strings")
{
// valid_utf8_string() decides short strings with a word test and long ones
// 16 bytes at a time where a vector check is available; it must agree with
// the byte-wise check for every mix of ASCII, sequences of each length,
// ill-formed and truncated sequences, at every length and offset
const char* const pieces[] = // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
"a", "\x7F", "\xC3\xA9", "\xE2\x82\xAC", "\xE0\xA0\x80", "\xEF\xBF\xBF", "\xF0\x9F\x98\x80", "\xF4\x8F\xBF\xBF",
"\x80", "\xC0\xAF", "\xC3", "\xE2\x82", "\xED\xA0\x80", "\xF0\x9F", "\xF4\x90\x80\x80", "\xFF"
};
// std::mt19937's sequence is fixed by the standard; no distribution is used
std::mt19937 rng(5); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::size_t valid = 0;
for (int i = 0; i < 100000; ++i)
{
std::string s;
const std::size_t length = rng() % 100;
const auto ascii = static_cast<std::uint32_t>(rng() % 101);
while (s.size() < length)
{
// mostly well-formed pieces (the first eight), so that long valid
// strings are common
s += (rng() % 100 < ascii) ? std::string("x") : std::string(pieces[rng() % (rng() % 8 == 0 ? 16 : 8)]);
}
const auto* data = reinterpret_cast<const unsigned char*>(s.data());
const bool expected = nlohmann::detail::is_valid_utf8(s);
valid += expected ? 1 : 0;
CAPTURE(s)
CHECK(nlohmann::detail::valid_utf8_string(data, s.size()) == expected);
CHECK((nlohmann::detail::valid_utf8_prefix(data, s.size()) == s.size()) == expected);
}
// both outcomes are common enough to be tested
CHECK(valid > 20000);
CHECK(valid < 80000);
}