Merge origin/develop into fix-self-aliasing-pointer-patch

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-26 21:39:59 +01:00
commit 8dd8f5ce8e
131 files changed
+5012 -4871

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
+6
View File
@@ -59,6 +59,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
+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;
}
+5 -3
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.
@@ -283,7 +283,7 @@ def main():
objects.append(obj)
binaries = {}
for bench in ['bench_view', 'bench_corpus', 'bench_edit']:
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
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
@@ -297,6 +297,8 @@ def main():
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
outputs['bench_decode'] = run([binaries['bench_decode'], 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)
@@ -330,7 +332,7 @@ def main():
f.write(f'\n## {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']:
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
path = os.path.join(args.build_dir, name + '.csv')
if os.path.isfile(path):
with open(path, encoding='utf-8') as f:
+126 -2
View File
@@ -2916,6 +2916,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 +3163,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 +3353,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 +3522,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 +4689,60 @@ 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({}));
}
}
+249 -17
View File
@@ -802,12 +802,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 +814,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")
@@ -1178,7 +1248,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 +1260,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 +1268,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 +1294,163 @@ 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>
{
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 +2155,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 +2170,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 +2201,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());
}
+148 -3
View File
@@ -1807,6 +1807,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 +2002,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 +2025,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;
+223 -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")
{
@@ -1618,6 +1621,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}};
@@ -2206,3 +2227,204 @@ 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")
{
const json doc = R"({"a": [1, 2], "o": {"k": "v"}})"_json;
const json before = 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 = 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);
}
}
+116
View File
@@ -493,6 +493,72 @@ 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("flatten")
{
json j =
@@ -1082,6 +1148,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 =
+2 -2
View File
@@ -592,7 +592,7 @@ TEST_CASE("json_view node integer bits")
})
{
CAPTURE(v)
n.kind = 0x5A;
n.kind = static_cast<nlohmann::detail::view::node_kind>(0x5A);
n.flags = 0xA5;
n.extra = 0x1234;
n.off = 0x89ABCDEFu;
@@ -601,7 +601,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(static_cast<std::uint8_t>(n.kind) == 0x5A);
CHECK(n.flags == 0xA5);
CHECK(n.extra == 0x1234);
CHECK(n.off == 0x89ABCDEFu);
+108 -1
View File
@@ -26,12 +26,119 @@ 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;
}
template<typename View>
auto overload(View v) -> decltype(accepts_editable(v))
{
return accepts_editable(v);
}
template<typename View>
auto overload(View v) -> decltype(accepts_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 +207,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());
+449
View File
@@ -0,0 +1,449 @@
// __ _____ _____ _____
// __| | __| | | | 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>
#endif
// 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
{
std::string name;
std::vector<point> points;
point center;
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};
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(with_defaults, a, b, c)
struct with_names
{
int value = 0;
std::string text;
};
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
};
NLOHMANN_JSON_SERIALIZE_ENUM(color,
{
{color::red, "red"},
{color::green, "green"},
{color::blue, nullptr},
})
enum class strict_color
{
red,
green
};
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;
};
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 std::valarray<int> 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
CHECK(v["points"][0].get<std::optional<point>>()->y == 2);
CHECK(!v["nothing"].get<std::optional<point>>().has_value());
#endif
// containers of views keep views of the elements
const std::vector<json_view> 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) && !JSON_DIAGNOSTICS
SECTION("std types: the same errors as from the materialized value")
{
const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])");
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 shape 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 with_defaults 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 with_names 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();
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
#if !JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
#endif
#endif
}
SECTION("derived types")
{
const json_document d = json_document::parse(R"({"id": 7, "c": "green", "extra": 3})");
const derived x = d.root().get<derived>();
CHECK(x.id == 7);
CHECK(x.c == color::green);
const derived_with_defaults y = d.root().get<derived_with_defaults>();
CHECK(y.id == 7);
CHECK(y.label == "none");
const derived_from_legacy 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("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) && !JSON_DIAGNOSTICS
SECTION("errors are those of the same conversion of a json value")
{
// (with JSON_DIAGNOSTICS, those of json name the path of the value)
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
}
+31 -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,9 @@ std::uint32_t rng()
TEST_CASE("json_view images: round trips")
{
static_assert(json_view_image_test::has_save<json_document>::value && json_view_image_test::has_load<json_document>::value, "save() and load() on a little-endian target");
static_assert(json_view_image_test::has_save<ordered_json_editable_document>::value && json_view_image_test::has_load<ordered_json_editable_document>::value, "save() and load() on a little-endian target");
SECTION("small documents")
{
for (const char* text :
@@ -480,7 +497,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 :
{
@@ -810,13 +827,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 +977,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 +1030,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 +1044,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 +1214,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 +1292,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
+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());
}
}
+53 -1
View File
@@ -2023,7 +2023,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 +3348,55 @@ 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());
}
}
}