mirror of
https://github.com/nlohmann/json.git
synced 2026-10-27 08:18:55 +00:00
Merge origin/develop into bench/json-view-comparison-fairness
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
131 files changed
+5012
-4871
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -300,7 +300,7 @@ def main():
|
||||
for label, build_dir, extra in variants:
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
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(build_dir, bench)
|
||||
run(cxx + flags + extra + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
||||
binaries[bench] = exe
|
||||
@@ -313,6 +313,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=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
|
||||
@@ -351,7 +353,7 @@ def main():
|
||||
with open(stem + '.csv', 'w', encoding='utf-8') as out:
|
||||
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
|
||||
for label, build_dir, _ in variants:
|
||||
for name in ['bench_view', 'bench_corpus', 'bench_edit']:
|
||||
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:
|
||||
|
||||
+126
-2
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
{
|
||||
@@ -1515,6 +1518,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 +2124,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);
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
@@ -1044,6 +1110,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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user